diff --git a/frontend/app/trees/[id]/tree/chart.css b/frontend/app/trees/[id]/tree/chart.css new file mode 100644 index 0000000..64e9c33 --- /dev/null +++ b/frontend/app/trees/[id]/tree/chart.css @@ -0,0 +1,880 @@ +.f3 { + --female-color: rgb(196, 138, 146); + --male-color: rgb(120, 159, 172); + --genderless-color: lightgray; + --background-color: rgb(33, 33, 33); + --text-color: #fff; + + font-family: 'Roboto', sans-serif; +} + +.f3 * { + box-sizing: border-box; +} + +.f3 .cursor-pointer { + cursor: pointer; +} +.f3 svg.main_svg { + width: 100%; + height: 100%; +} +.f3 svg.main_svg text { + fill: currentColor; +} +.f3 rect.card-female, .f3 .card-female .card-body-rect, .f3 .card-female .text-overflow-mask { + fill: var(--female-color); +} +.f3 rect.card-male, .f3 .card-male .card-body-rect, .f3 .card-male .text-overflow-mask { + fill: var(--male-color); +} +.f3 .card-genderless .card-body-rect, .f3 .card-genderless .text-overflow-mask { + fill: var(--genderless-color); +} +.f3 .card_add .card-body-rect { + fill: #3b5560; + stroke-width: 4px; + stroke: #fff; + cursor: pointer; +} +.f3 g.card_add text { + fill: #fff; +} +.f3 .card-main-outline { + stroke: currentColor; + stroke-width: 3px; +} +.f3 .card_family_tree rect { + transition: 0.3s; +} +.f3 .card_family_tree:hover rect { + transform: scale(1.1); +} +.f3 .card_add_relative { + cursor: pointer; + color: #fff; + transition: 0.3s; +} +.f3 .card_add_relative circle { + fill: rgba(0, 0, 0, 0); +} +.f3 .card_add_relative:hover { + color: black; +} +.f3 .card_edit.pencil_icon { + color: #fff; + transition: 0.3s; +} +.f3 .card_edit.pencil_icon:hover { + color: black; +} +.f3 .card_break_link, .f3 .link_upper, .f3 .link_lower, .f3 .link_particles { + transform-origin: 50% 50%; + transition: 1s; +} +.f3 .card_break_link { + color: #fff; +} +.f3 .card_break_link.closed .link_upper { + transform: translate(-140.5px, 655.6px); +} +.f3 .card_break_link.closed .link_upper g { + transform: rotate(-58deg); +} +.f3 .card_break_link.closed .link_particles { + transform: scale(0); +} +.f3 .input-field input { + height: 2.5rem !important; +} +.f3 .input-field > label:not(.label-icon).active { + -webkit-transform: translateY(-8px) scale(0.8); + transform: translateY(-8px) scale(0.8); +} +.f3.f3-cont { + width:100%; + height:900px; + max-height:70vh; + background-color: var(--background-color); + color: var(--text-color); +} +.f3 { + position: relative; + display: flex; +} + + + + +/* form-info */ +.f3-form input[type="text"], +.f3-form textarea, +.f3-form select { + width: 100%; + padding: 8px 12px; + margin: 8px 0; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; + background: var(--background-color); + color: currentColor; +} + +.f3-form input[type="text"]:focus, +.f3-form textarea:focus, +.f3-form select:focus { + box-shadow: 0 0 5px rgba(76, 175, 80, 0.2); +} + +.f3-form button { + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + margin: 10px 0; + transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out, color 0.3s ease-in-out; +} + +.f3-form button[type="submit"] { + background-color: #4CAF50; + color: white; +} + +.f3-cancel-btn { + background-color: #ccc; +} + +.f3-form .f3-delete-btn { + background-color: transparent; + border: 1px solid #f44336; + color: #f44336; + width: 100%; + padding: 5px 10px; +} + +.f3-delete-btn:hover { + background-color: #da190b; + border-color: #da190b; + color: #fff; +} + +.f3-delete-btn:disabled { + opacity: 0.5; + background-color: transparent; + color: #f44336; + cursor: not-allowed; +} + +.f3-form .f3-remove-relative-btn { + background-color: transparent; + border: 1px solid currentColor; + color: currentColor; + width: 100%; + padding: 5px 10px; +} + +.f3-remove-relative-btn:hover, .f3-remove-relative-btn.active { + background-color: var(--text-color); + border-color: var(--text-color); + color: var(--background-color); +} + +.f3-radio-group { + margin: 15px 0; +} + +.f3-radio-group label { + margin-right: 15px; + cursor: pointer; +} + +.f3-radio-group input[type="radio"] { + margin-right: 5px; +} + +.f3-info-field-label, .f3-form-field label { + font-weight: bold; + font-size: 12px; + display: block; + opacity: 0.8; +} + +.f3-info-field-value { + font-weight: normal; + display: block; + border: none; + outline: none; + border-bottom: 1px solid rgba(255,255,255,0.2); + padding-bottom: 1px; + margin-bottom: 10px; + min-height: 18px; +} + +.f3-form-buttons { + text-align: right; +} + +.f3-form-title { + text-align: center; +} + +.f3-form.non-editable .f3-form-buttons, +.f3-form.non-editable .f3-delete-btn, +.f3-form.non-editable .f3-remove-relative-btn, +.f3-form.non-editable .f3-link-existing-relative { + display: none; +} + +.f3-close-btn { + cursor: pointer; + position: absolute; + left: 10px; + top: 8px; + font-size: 30px; + color: var(--text-color); +} + +.f3-edit-btn { + position: relative; + top: -1px; + width: 24px; + height: 24px; + cursor: pointer; + display: inline-block; +} + +.f3-add-relative-btn { + cursor: pointer; + width: 27px; + height: 27px; + margin-right: 5px; + display: inline-block; +} + +/* card-html */ + +.f3 div.card { + cursor: pointer; + color: var(--text-color); + position: relative; + line-height: 1.2; +} + +.f3 div.card-image-circle { + border-radius: 50%; + padding: 5px; + width: 90px; + height: 90px; +} + +.f3 div.card-image-circle div.card-label { + position: absolute; + bottom: -10px; + left: 50%; + transform: translate(-50%, 50%); + max-width: 150%; + min-height: 22px; + text-align: center; + background-color: rgba(0, 0, 0, 0.5); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: 3px; + padding: 0 5px; +} + +.f3 div.card-image-circle img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.f3 div.card-image-circle svg { + width: 100%; + height: 100%; + padding: 5px; + border-radius: 50%; + object-fit: cover; +} + +.f3 div.card-image-circle img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.f3 div.card-rect { + padding: 5px; + border-radius: 3px; + width: 120px; + min-height: 70px; + overflow: hidden; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; +} + + +.f3 div.card-image-rect { + width: 200px; + min-height: 70px; + display: flex; + align-items: center; + border-radius: 5px; +} + +.f3 div.card-image-rect .person-icon { + height: 70px; + width: 70px; + object-fit: cover; + flex: 0 0 auto; + padding: 5px; + margin-right: 10px; +} + +.f3 div.card-image-rect img { + height: 70px; + width: 70px; + object-fit: cover; + flex: 0 0 auto; + padding: 5px; + margin-right: 10px; + border-radius: 8px; +} + +.f3 div.card-image-rect svg { + object-fit: cover; + width: 100%; + height: 100%; + padding: 5px; + border-radius: 7px; +} + +.f3 div.card-image-rect div.card-label { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; +} + +.f3 div.mini-tree { + text-align: right; + position: absolute; + top: -15px; + right: -2px; + z-index: -1; +} +.f3 div.mini-tree svg { + width: 55px; +} + +.f3 .f3-card-duplicate-tag { + position: absolute; + top: 2px; + right: 2px; + color: rgb(255, 251, 220); + background-color: rgba(255, 251, 220, 0); + border-radius: 50%; + padding: 2px; + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; +} + +.f3 .f3-card-duplicate-hover div.card-inner { + transform: translate(0, -2px); + outline: 4px solid rgb(255, 251, 220); +} + +.f3 .f3-card-duplicate-hover .f3-card-duplicate-tag { + background-color: rgba(255, 251, 220, .8); + color: #000; +} + +.f3 .f3-remove-relative-active .card { + background-color: var(--background-color); +} + +.f3 .f3-remove-relative-active .card-inner { + transition: border 0.2s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s ease-in-out; + opacity: .75; +} + +.f3 .f3-remove-relative-active .card:hover .card-inner { + opacity: .25; +} + +.f3 .f3-remove-relative-active .card-male.card-depth--1:hover .card-inner { + transform: translate(-8px, -8px); +} + +.f3 .f3-remove-relative-active .card.card-female.card-depth--1:hover .card-inner { + transform: translate(8px, -8px); +} + +.f3 .f3-remove-relative-active .card.card-female.card-depth-0:hover .card-inner { + transform: translate(8px, 0); +} + +.f3 .f3-remove-relative-active .card.card-male.card-depth-0:hover .card-inner { + transform: translate(-8px, 0); +} + +.f3 .f3-remove-relative-active .card.card-depth-1:hover .card-inner { + transform: translate(0, 8px); +} + +.f3 .f3-remove-relative-active .card.card-main .card-inner { + transform: translate(0, 0)!important; + opacity: 1!important; +} + + + +.f3 div.card > div { + transition: transform 0.2s ease-in-out; + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.8); +} + +.f3 .card-inner { + outline: 0px solid rgba(255, 255, 255, 1); + transition: outline 0.5s ease-in-out; +} + +.f3 div.card-female .card-inner, .f3 div.card-female .person-icon svg { + background-color: var(--female-color); +} +.f3 div.card-male .card-inner, .f3 div.card-male .person-icon svg { + background-color: var(--male-color); +} +.f3 div.card-genderless .card-inner, .f3 div.card-genderless .person-icon svg { + background-color: var(--genderless-color); +} + +.f3 div.card-new-rel .card-inner, .f3 div.card-new-rel .person-icon svg { + background-color: var(--background-color); +} + +.f3 div.card-to-add .card-inner { + background-color: var(--background-color); + border: 1px solid; +} + +.f3 div.card-to-add .card-inner .card-label { + margin: 0 auto; +} + +.f3 div.card-to-add .person-icon { + display: none; +} + +.f3 div.card-new-rel .card-inner { + border-width: 1px; + border-style: dashed; + outline: 0px !important; +} +.f3 div.card-new-rel.card-female .card-inner, .f3 div.card-to-add.card-female .card-inner { + border-color: var(--female-color); + color: var(--female-color); +} +.f3 div.card-new-rel.card-male .card-inner, .f3 div.card-to-add.card-male .card-inner { + color: var(--male-color); + border-color: var(--male-color); +} + +.f3 div.card-unknown .card-inner { + background-color: var(--background-color); + border: 1px solid; +} + +.f3 div.card-unknown .card-inner .card-label { + margin: 0 auto; +} + +.f3 div.card-unknown .person-icon { + display: none; +} + +.f3 div.card-new-rel .card-inner { + border-width: 1px; + border-style: dashed; + outline: 0px !important; +} +.f3 div.card-new-rel.card-female .card-inner, .f3 div.card-unknown.card-female .card-inner { + border-color: var(--female-color); + color: var(--female-color); +} +.f3 div.card-new-rel.card-male .card-inner, .f3 div.card-unknown.card-male .card-inner { + color: var(--male-color); + border-color: var(--male-color); +} + +.f3 div.card:hover > div { + transform: translate(0, -2px); +} +.f3 div.card-main .card-inner, .f3 div.card:hover .card-inner { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.8); +} + +.f3 div.card-main .card-inner { + outline: 4px solid rgba(220, 220, 220, 1); +} + +.f3 div.card-inner.f3-path-to-main { + outline: 4px solid rgba(255, 255, 255, 1); +} + +.f3 .link { + transition: stroke-width 0.2s ease-in-out; +} + +.f3 .link.f3-path-to-main { + stroke-width: 4px; +} + + + + + +.f3-form-cont { + position: relative; + z-index: 6; + right: 0; + top: 0; + width: 0; + height: 100%; + background-color: var(--background-color); + overflow: auto; + flex: 0 0 auto; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); +} + +.f3-form-cont.opened { + width: 350px; +} + +.f3-form { + padding: 20px; +} + +.f3-form hr { + border-style: solid; + border-width: thin 0 0 0; + opacity: 0.15; +} + +.f3-nav-cont { + position: absolute; + top: 0; + left: 0; + width: 100%; + display: flex; +} + +.f3-history-controls { + padding: 8px 5px 7px 9px; + display: inline-block; + position: relative; + z-index: 2; +} + +.f3-back-button, .f3-forward-button { + width: 30px; + height: 30px; + transition: opacity 0.3s ease; + cursor: pointer; + display: inline-block; + background-color: transparent; + border: none; + margin-right: 10px; + color: currentColor; +} + +.f3-history-controls svg { + height: 100%; +} + +.f3-back-button.disabled, .f3-forward-button.disabled { + opacity: 0.5; +} + +.f3-modal { + display: none; + position: absolute; + z-index: 10; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0,0,0); + background-color: rgba(0,0,0,0.4); +} + +.f3-modal-content { + position: relative; + background-color: var(--background-color); + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + border-radius: 5px; + width: 500px; + max-width: 90%; +} + +.f3-modal-close { + color: #aaa; + position: absolute; + right: 10px; + top: 7px; + font-size: 28px; + font-weight: bold; +} + +.f3-modal-close:hover, +.f3-modal-close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +.f3-popup { + position: fixed; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.8); +} + +.f3-popup-content { + position: relative; + background-color: var(--background-color); + border: 1px solid #888; + border-radius: 5px; + overflow: hidden; + width: 100%; + height: 100%; +} + +.f3-popup-nav { + height: 20px; +} + +.f3-popup-content-inner { + width: 100%; + height: 100%; +} + +.f3-popup-close { + color: #aaa; + position: absolute; + z-index: 4; + right: 6px; + top: 1px; + font-size: 28px; + font-weight: bold; + line-height: 1; +} + +.f3-popup-close:hover, +.f3-popup-close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + + + +.f3-btn { + position: relative; + cursor: pointer; + padding: 5px 10px; + + overflow: hidden; + + border-width: 0; + outline: none; + border-radius: 3px; + box-shadow: 0 1px 4px rgba(0, 0, 0, .6); + + background-color: var(--text-color); + color: var(--background-color); + + transition: background-color .3s; + font-size: 14px; +} + +.f3-btn:hover, .f3-btn:focus { + background-color: var(--background-color); + color: var(--text-color); +} + +.f3-female-bg { + background-color: var(--female-color); +} + +.f3-male-bg { + background-color: var(--male-color); +} + +.f3-genderless-bg { + background-color: var(--genderless-color); +} + +.f3-female-color { + color: var(--female-color); +} + +.f3-male-color { + color: var(--male-color); +} + +.f3-genderless-color { + color: var(--genderless-color); +} + +.f3-autocomplete-cont { + position: relative; + display: inline-block; + z-index: 2; + font-size: 14px; + width: 200px; +} + +.f3-autocomplete input { + border: 1px solid rgba(255, 255, 255, 0.2); + background-color: var(--background-color); + color: var(--text-color); + padding: 10px; + width: 100%; +} +.f3-autocomplete input:focus { + outline: none; +} + +.f3-autocomplete-toggle { + position: absolute; + right: 10px; + top: 10px; + cursor: pointer; + color: var(--text-color); + transition: color 0.3s ease-in-out; + width: 20px; +} + +.f3-autocomplete-items { + border: 1px solid rgba(255, 255, 255, 0.2); + border-top: none; + overflow-y: auto; + max-height: 0; + background-color: var(--background-color); + transition: max-height 0.3s ease-in-out; +} + +.f3-autocomplete.active .f3-autocomplete-items { + max-height: 300px; +} + +.f3-autocomplete-item > div { + padding: 10px; + cursor: pointer; + background-color: var(--background-color); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out; +} +.f3-autocomplete-item > div:hover, .f3-autocomplete-item.f3-selected > div { + background-color: var(--text-color); + color: var(--background-color); +} + +.f3-autocomplete-active { + background-color: DodgerBlue !important; + color: #ffffff; +} + +.f3-kinship-info { + padding: 10px 20px; +} + +.f3-kinship-info .f3-info-field { + color:#b3b01e +} + +.f3-kinship-info-icon { + cursor:pointer; + display:inline-block; + width:18px; + height:18px; + color:#04a4f4; + position:relative; + top:4px; + left:2px; +} + +.f3-kinship-info .f3 { + width:100%; + height: 100%; + position:relative; + background-color:rgb(33,33,33); + color:#fff; +} + +.f3 .f3-kinship-info .card-kinship-self { + min-height: 0px; + width: 60px; + height: 60px; + border-radius: 50%; + background-color: var(--background-color) !important; + border: solid 3px; + color: #437fae; + font-weight: bold; +} + +.f3 .f3-kinship-info .card-kinship-self.f3-real-label { + width: 150px; + height: 50px; + border-radius: 50px; +} + +.f3 .f3-kinship-info .card-kinship-rel { + min-height: 0px; + width: 150px; + height: 50px; + border-radius: 50px; + background-color: #1d3456 !important; + font-weight: bold; +} + +.f3 .f3-kinship-info .card-kinship-default { + min-height: 0px; + width: 150px; + height: 50px; + border-radius: 50px; + background-color: var(--background-color) !important; + border: solid 1px; +} + +.f3-kinship-labels-toggle { + position: absolute; + top: 0; + left: 0; + z-index: 10; + font-size: 12px; +} + +.f3-kinship-labels-toggle label { + cursor: pointer; + color: #fff; + font-weight: bold; + text-align: center; + padding: 2px 5px; +} + +.f3-kinship-labels-toggle input[type="checkbox"] { + cursor: pointer; + margin-left: 5px; + margin-right: 5px; + margin-top: 5px; + margin-bottom: 5px; +} \ No newline at end of file diff --git a/frontend/app/trees/[id]/tree/page.tsx b/frontend/app/trees/[id]/tree/page.tsx new file mode 100644 index 0000000..01d0034 --- /dev/null +++ b/frontend/app/trees/[id]/tree/page.tsx @@ -0,0 +1,127 @@ +"use client"; + +// Vendored from family-chart/dist/styles (the package blocks the CSS subpath export). +import "./chart.css"; + +import { useParams, useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; + +import { api } from "@/lib/api/client"; +import type { components } from "@/lib/api/schema"; + +type Relationship = components["schemas"]["RelationshipRead"]; +type Event = components["schemas"]["EventRead"]; + +function splitName(name: string | null | undefined): [string, string] { + const t = (name ?? "").trim().split(/\s+/).filter(Boolean); + if (t.length <= 1) return [name ?? "", ""]; + return [t.slice(0, -1).join(" "), t[t.length - 1]]; +} + +export default function TreePage() { + const router = useRouter(); + const params = useParams<{ id: string }>(); + const treeId = params.id; + const containerRef = useRef(null); + const [status, setStatus] = useState<"loading" | "empty" | "ready" | "error">("loading"); + + useEffect(() => { + let cancelled = false; + + (async () => { + const p = await api.GET("/api/v1/trees/{tree_id}/persons", { + params: { path: { tree_id: treeId } }, + }); + if (p.response.status === 401) { + router.push("/login"); + return; + } + const [r, e] = await Promise.all([ + api.GET("/api/v1/trees/{tree_id}/relationships", { params: { path: { tree_id: treeId } } }), + api.GET("/api/v1/trees/{tree_id}/events", { params: { path: { tree_id: treeId } } }), + ]); + const people = p.data ?? []; + const rels: Relationship[] = r.data ?? []; + const events: Event[] = e.data ?? []; + if (people.length === 0) { + if (!cancelled) setStatus("empty"); + return; + } + + const parentsOf = (id: string) => + rels.filter((x) => x.type === "parent_child" && x.person_to_id === id).map((x) => x.person_from_id); + const childrenOf = (id: string) => + rels.filter((x) => x.type === "parent_child" && x.person_from_id === id).map((x) => x.person_to_id); + const partnersOf = (id: string) => + rels + .filter((x) => x.type === "partnership" && (x.person_from_id === id || x.person_to_id === id)) + .map((x) => (x.person_from_id === id ? x.person_to_id : x.person_from_id)); + + const birthYear = new Map(); + for (const ev of events) { + if (ev.person_id && ev.event_type === "birth" && !birthYear.has(ev.person_id)) { + const y = ev.date_start ? ev.date_start.slice(0, 4) : ev.date_value ?? ""; + if (y) birthYear.set(ev.person_id, y); + } + } + + const data = people.map((pp) => { + const [fn, ln] = splitName(pp.primary_name); + return { + id: pp.id, + data: { + "first name": fn || "Unnamed", + "last name": ln, + birthday: birthYear.get(pp.id) ?? "", + gender: pp.gender === "female" ? "F" : "M", + }, + rels: { + spouses: partnersOf(pp.id), + parents: parentsOf(pp.id), + children: childrenOf(pp.id), + }, + }; + }); + + if (cancelled || !containerRef.current) return; + try { + const f3 = await import("family-chart"); + containerRef.current.innerHTML = ""; + const chart = f3.createChart(containerRef.current, data); + chart.setCardHtml().setCardDisplay([["first name", "last name"], ["birthday"]]); + chart.updateTree({ initial: true }); + if (!cancelled) setStatus("ready"); + } catch { + if (!cancelled) setStatus("error"); + } + })().catch(() => { + if (!cancelled) setStatus("error"); + }); + + return () => { + cancelled = true; + }; + }, [router, treeId]); + + return ( +
+
+

Tree

+ + Drag to pan · scroll to zoom · click a person to recenter + +
+ {status === "empty" && ( +

+ No people yet — add some under People, or import a GEDCOM. +

+ )} + {status === "error" &&

Could not render the tree.

} +
+
+ ); +} diff --git a/frontend/app/trees/page.tsx b/frontend/app/trees/page.tsx index ab401b2..7316737 100644 --- a/frontend/app/trees/page.tsx +++ b/frontend/app/trees/page.tsx @@ -77,7 +77,7 @@ export default function TreesPage() {
  • - +
    {tree.name}
    {tree.visibility} diff --git a/frontend/components/app-sidebar.tsx b/frontend/components/app-sidebar.tsx index 8decfe3..4351f84 100644 --- a/frontend/components/app-sidebar.tsx +++ b/frontend/components/app-sidebar.tsx @@ -7,6 +7,7 @@ import { FolderTree, Image as ImageIcon, LogOut, + Network, Users, } from "lucide-react"; import Link from "next/link"; @@ -77,6 +78,12 @@ export function AppSidebar() {
    {treeName ?? "Tree"}
    + = 10" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "dev": true }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1134,6 +1513,14 @@ } } }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1156,6 +1543,14 @@ "node": ">=10.13.0" } }, + "node_modules/family-chart": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/family-chart/-/family-chart-0.9.0.tgz", + "integrity": "sha512-+JdLr1Oo+YFnQWUXgdnk4nCMTbe1MXKdpbx3KEBXPeq2oX+2v5ccmrcK39CZ761/zQfgSHFZ2cT/+gbaeeACcA==", + "dependencies": { + "d3": "^7.9.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1181,6 +1576,17 @@ "node": ">= 14" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/index-to-position": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", @@ -1193,6 +1599,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", @@ -1734,6 +2148,21 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 643fffe..0fccdc5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,23 +10,24 @@ "gen:api": "openapi-typescript ./openapi.json -o ./lib/api/schema.d.ts --default-non-nullable false" }, "dependencies": { - "next": "^15.1.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "openapi-fetch": "^0.13.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "tailwind-merge": "^2.6.0", - "lucide-react": "^0.469.0" + "family-chart": "^0.9.0", + "lucide-react": "^0.469.0", + "next": "^15.1.0", + "openapi-fetch": "^0.13.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "tailwind-merge": "^2.6.0" }, "devDependencies": { - "typescript": "^5.7.0", + "@tailwindcss/postcss": "^4.0.0", "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", - "tailwindcss": "^4.0.0", - "@tailwindcss/postcss": "^4.0.0", + "openapi-typescript": "^7.5.0", "postcss": "^8.4.49", - "openapi-typescript": "^7.5.0" + "tailwindcss": "^4.0.0", + "typescript": "^5.7.0" } } diff --git a/frontend/types/family-chart.d.ts b/frontend/types/family-chart.d.ts new file mode 100644 index 0000000..f24c9ad --- /dev/null +++ b/frontend/types/family-chart.d.ts @@ -0,0 +1,2 @@ +declare module "family-chart"; +declare module "family-chart/dist/styles/family-chart.css";