"use client"; import Link from "next/link"; import { useParams } from "next/navigation"; import { useRef, useState } from "react"; import { api } from "@/lib/api/client"; import type { components } from "@/lib/api/schema"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; type Report = { counts: Record; unmapped_tags: string[] }; type Preview = components["schemas"]["ImportPreview"]; type Dup = components["schemas"]["DuplicateMatch"]; type Action = "new" | "skip" | "merge" | "overwrite"; const ACTIONS: { value: Action; label: string }[] = [ { value: "new", label: "Import as new" }, { value: "merge", label: "Merge into existing" }, { value: "skip", label: "Skip (use existing)" }, { value: "overwrite", label: "Overwrite existing" }, ]; const fieldCls = "h-9 rounded-md border border-[var(--border)] bg-[var(--surface)] px-2 text-sm"; export default function GedcomPage() { const params = useParams<{ id: string }>(); const treeId = params.id; const [target, setTarget] = useState<"new" | "this">("new"); const [newName, setNewName] = useState(""); const [busy, setBusy] = useState(false); const [report, setReport] = useState(null); const [importedTreeId, setImportedTreeId] = useState(null); const fileRef = useRef(null); // Two-step dedupe flow (only when importing into an existing tree). const [file, setFile] = useState(null); const [preview, setPreview] = useState(null); const [resolutions, setResolutions] = useState>({}); function resetAll() { setReport(null); setImportedTreeId(null); setPreview(null); setFile(null); setResolutions({}); } async function postImport( tid: string, f: File, opts?: { resolutions?: string; defaultAction?: Action }, ) { const fd = new FormData(); fd.append("file", f); if (opts?.defaultAction) fd.append("default_action", opts.defaultAction); if (opts?.resolutions) fd.append("resolutions", opts.resolutions); const resp = await fetch(`/api/v1/trees/${tid}/gedcom/import`, { method: "POST", body: fd, credentials: "include", }); if (resp.ok) { setReport(await resp.json()); setImportedTreeId(tid); } } async function onFile(e: React.ChangeEvent) { const f = e.target.files?.[0]; if (fileRef.current) fileRef.current.value = ""; if (!f) return; setBusy(true); resetAll(); if (target === "new") { // Fresh tree — nothing to dedupe against, import directly. const { data } = await api.POST("/api/v1/trees", { body: { name: newName.trim() || "Imported tree" }, }); if (data) await postImport(data.id, f); setBusy(false); return; } // Existing tree — preview for duplicates first. setFile(f); const fd = new FormData(); fd.append("file", f); const resp = await fetch(`/api/v1/trees/${treeId}/gedcom/preview`, { method: "POST", body: fd, credentials: "include", }); if (resp.ok) { const pv: Preview = await resp.json(); setPreview(pv); // Default: high-confidence matches merge, lower ones come in as new. const init: Record = {}; for (const d of pv.potential_duplicates) init[d.xref] = d.score === "high" ? "merge" : "new"; setResolutions(init); } setBusy(false); } async function runImport() { if (!file) return; setBusy(true); const map: Record = {}; for (const d of preview?.potential_duplicates ?? []) { const action = resolutions[d.xref] ?? "new"; if (action !== "new") map[d.xref] = { action, target_id: d.existing_person_id }; } await postImport(treeId, file, { resolutions: JSON.stringify(map) }); setPreview(null); setFile(null); setBusy(false); } async function exportGed() { const resp = await fetch(`/api/v1/trees/${treeId}/gedcom/export`, { credentials: "include" }); if (!resp.ok) return; const blob = await resp.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "tree.ged"; a.click(); URL.revokeObjectURL(url); } const dups = preview?.potential_duplicates ?? []; return (

Import & export GEDCOM

Import a GEDCOM file {target === "new" && ( setNewName(e.target.value)} /> )} {target === "this" && !preview && (

We'll scan the file and flag anyone who looks like a person already in this tree, so you can merge, skip, or overwrite before anything is saved.

)} {!preview && ( )} {/* Duplicate-resolution step */} {preview && (
{Object.entries(preview.counts).map(([k, v]) => ( {v} {k} ))}
{dups.length === 0 ? (

No likely duplicates found — everyone will be imported as new.

) : (

{dups.length} possible duplicate{dups.length === 1 ? "" : "s"}

    {dups.map((d: Dup) => (
  • {d.incoming_name} {d.incoming_birth_year && ( b. {d.incoming_birth_year} )} {d.existing_name} {d.existing_birth_year && ( b. {d.existing_birth_year} )} {d.score}
  • ))}
)}
)} {report && (
Import complete
{Object.entries(report.counts).map(([k, v]) => ( {v} {k} ))}
{report.unmapped_tags.length > 0 && (
Unmapped tags (skipped): {report.unmapped_tags.join(", ")}
)} {importedTreeId && ( Open the imported tree → )}
)}
Export this tree

Download this tree as a GEDCOM file — people, relationships, events, and sources.

); }