diff --git a/frontend/src/components/AddPersonButton.css b/frontend/src/components/AddPersonButton.css
deleted file mode 100644
index ad59fb0..0000000
--- a/frontend/src/components/AddPersonButton.css
+++ /dev/null
@@ -1,172 +0,0 @@
-/* ============================================================
- Add Person Button - FAB draggable pour ajouter personne
- ============================================================ */
-
-.add-person-button {
- position: fixed;
- width: 64px;
- height: 64px;
- border-radius: 50%;
-
- background: rgba(242, 239, 234, 0.95);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- border: 2px solid rgba(147, 64, 42, 0.3);
-
- box-shadow:
- 0 8px 32px rgba(42, 38, 34, 0.25),
- 0 2px 8px rgba(42, 38, 34, 0.12);
-
- cursor: grab;
- user-select: none;
-
- display: flex;
- align-items: center;
- justify-content: center;
-
- transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
- z-index: 900;
-
- /* Position initiale : bas-gauche */
- bottom: 24px;
- left: 24px;
-}
-
-.add-person-button:hover {
- transform: scale(1.12);
- box-shadow:
- 0 12px 40px rgba(147, 64, 42, 0.35),
- 0 4px 12px rgba(42, 38, 34, 0.18);
- border-color: rgba(147, 64, 42, 0.5);
-}
-
-.add-person-button.dragging {
- cursor: grabbing;
- transform: scale(1.2) rotate(5deg);
- box-shadow:
- 0 16px 48px rgba(147, 64, 42, 0.45),
- 0 6px 16px rgba(42, 38, 34, 0.25);
- border-color: rgba(147, 64, 42, 0.7);
- z-index: 1100;
-}
-
-/* Icône + */
-.add-person-button-icon {
- font-size: 32px;
- font-weight: 300;
- color: #93402A;
- line-height: 1;
- filter: drop-shadow(0 1px 3px rgba(42, 38, 34, 0.15));
- transition: transform 0.2s ease;
-}
-
-.add-person-button:hover .add-person-button-icon {
- transform: rotate(90deg);
-}
-
-.add-person-button.dragging .add-person-button-icon {
- transform: rotate(180deg);
-}
-
-/* Pulse animation sur apparition */
-@keyframes addPersonPulse {
- 0% {
- opacity: 0;
- transform: scale(0.7);
- }
- 50% {
- transform: scale(1.15);
- }
- 100% {
- opacity: 1;
- transform: scale(1);
- }
-}
-
-.add-person-button.entering {
- animation: addPersonPulse 0.5s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-/* Tooltip */
-.add-person-button-tooltip {
- position: absolute;
- top: 50%;
- left: calc(100% + 12px);
- transform: translateY(-50%);
-
- padding: 8px 14px;
- background: rgba(42, 38, 34, 0.95);
- color: #FFFFFF;
- font-size: 14px;
- font-weight: 500;
- border-radius: 8px;
- white-space: nowrap;
-
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.2s ease;
-
- box-shadow: 0 4px 12px rgba(42, 38, 34, 0.2);
-}
-
-.add-person-button:hover .add-person-button-tooltip {
- opacity: 1;
-}
-
-.add-person-button.dragging .add-person-button-tooltip {
- opacity: 0;
-}
-
-/* Instructions drag (affichées pendant drag) */
-.add-person-drag-hint {
- position: fixed;
- top: 24px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 1050;
-
- padding: 10px 18px;
- background: rgba(242, 239, 234, 0.98);
- backdrop-filter: blur(24px);
- border-radius: 50px;
- border: 1px solid rgba(147, 64, 42, 0.2);
-
- font-size: 14px;
- font-weight: 450;
- color: #2A2622;
-
- box-shadow:
- 0 6px 24px rgba(42, 38, 34, 0.18),
- 0 2px 8px rgba(42, 38, 34, 0.08);
-
- animation: hintSlideDown 0.3s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-@keyframes hintSlideDown {
- from {
- opacity: 0;
- transform: translateX(-50%) translateY(-10px);
- }
- to {
- opacity: 1;
- transform: translateX(-50%) translateY(0);
- }
-}
-
-/* Responsive mobile */
-@media (max-width: 768px) {
- .add-person-button {
- width: 56px;
- height: 56px;
- bottom: 20px;
- left: 20px;
- }
-
- .add-person-button-icon {
- font-size: 28px;
- }
-
- .add-person-button-tooltip {
- display: none;
- }
-}
diff --git a/frontend/src/components/AddPersonButton.jsx b/frontend/src/components/AddPersonButton.jsx
deleted file mode 100644
index 7087c49..0000000
--- a/frontend/src/components/AddPersonButton.jsx
+++ /dev/null
@@ -1,84 +0,0 @@
-import { useRef, useEffect, useState } from 'react'
-import { Plus, Baby, Heart, Users, MapPin } from 'lucide-react'
-import './AddPersonButton.css'
-import { t } from '../i18n/index.js'
-
-/**
- * Bouton FAB draggable pour ajouter une personne
- * Utilisé en mode édition pour drag-and-drop dans la galaxie
- *
- * @param {Object} props
- * @param {boolean} props.isDragging - État dragging (du hook)
- * @param {Object} props.dragPosition - Position drag {x, y}
- * @param {Function} props.onDragStart - Handler début drag
- * @param {string} props.linkType - Type de lien détecté ('child', 'spouse', 'sibling')
- */
-function AddPersonButton({ isDragging, dragPosition, onDragStart, linkType }) {
- const buttonRef = useRef(null)
- const [entering, setEntering] = useState(true)
-
- // Animation d'entrée
- useEffect(() => {
- const timer = setTimeout(() => setEntering(false), 500)
- return () => clearTimeout(timer)
- }, [])
-
- const handleMouseDown = (e) => {
- e.preventDefault()
- if (!buttonRef.current) return
-
- const rect = buttonRef.current.getBoundingClientRect()
- const buttonPosition = {
- x: rect.left + rect.width / 2,
- y: rect.top + rect.height / 2,
- }
-
- onDragStart?.(e, buttonPosition)
- }
-
- // Position inline si dragging
- const style = isDragging
- ? {
- left: `${dragPosition.x - 32}px`,
- top: `${dragPosition.y - 32}px`,
- transition: 'none',
- }
- : {}
-
- // Message tooltip selon linkType
- let tooltipText = t('Glisser pour ajouter une personne')
- if (isDragging) {
- if (linkType === 'child') tooltipText = t('Ajouter un enfant')
- else if (linkType === 'spouse') tooltipText = t('Ajouter un conjoint')
- else if (linkType === 'sibling') tooltipText = t('Ajouter un frère/sœur')
- else tooltipText = t('Relâcher pour créer')
- }
-
- return (
- <>
-
-
- {/* Hint pendant drag */}
- {isDragging && (
-
- {linkType === 'child' && <> {t('Enfant')}>}
- {linkType === 'spouse' && <> {t('Conjoint')}>}
- {linkType === 'sibling' && <> {t('Frère·Sœur')}>}
- {!linkType && <> {t('Positionnez la personne')}>}
-
- )}
- >
- )
-}
-
-export default AddPersonButton
diff --git a/frontend/src/components/CreateMyTreeButton.css b/frontend/src/components/CreateMyTreeButton.css
deleted file mode 100644
index fa43248..0000000
--- a/frontend/src/components/CreateMyTreeButton.css
+++ /dev/null
@@ -1,261 +0,0 @@
-/* ============================================================
- CreateMyTreeButton — Pill + Panel morph, bas-droite
- Même langage visuel que la ContextualNavbar
- ============================================================ */
-
-/* ----------------------------------------------------------
- Pill bouton (état fermé) — même aesthetic que la navbar
- ---------------------------------------------------------- */
-.cmt-pill {
- position: fixed;
- bottom: 100px;
- right: 20px;
- z-index: 1001;
-
- display: flex;
- align-items: center;
- padding: 13px 22px;
- border: 1px solid rgba(42, 38, 34, 0.1);
- border-radius: 50px;
- background: rgba(242, 239, 234, 0.97);
- backdrop-filter: blur(20px);
- -webkit-backdrop-filter: blur(20px);
- box-shadow:
- 0 4px 40px rgba(42, 38, 34, 0.22),
- 0 1px 6px rgba(42, 38, 34, 0.1);
-
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 14px;
- font-weight: 500;
- color: #2A2622;
- cursor: pointer;
- white-space: nowrap;
-
- animation: cmtPillIn 0.5s cubic-bezier(0.22, 1, 0.36, 1);
- transition: box-shadow 0.2s ease, transform 0.2s ease, background 0.2s ease;
-}
-
-@keyframes cmtPillIn {
- from {
- opacity: 0;
- transform: translateY(12px) scale(0.9);
- }
- to {
- opacity: 1;
- transform: translateY(0) scale(1);
- }
-}
-
-.cmt-pill:hover {
- background: rgba(242, 239, 234, 1);
- box-shadow:
- 0 8px 48px rgba(42, 38, 34, 0.28),
- 0 2px 8px rgba(42, 38, 34, 0.12);
- transform: translateY(-2px);
-}
-
-.cmt-pill:active {
- transform: translateY(0) scale(0.97);
-}
-
-/* ----------------------------------------------------------
- Panel (état ouvert) — card qui morphe depuis la pill
- ---------------------------------------------------------- */
-.cmt-panel {
- position: fixed;
- bottom: 100px;
- right: 20px;
- z-index: 1001;
-
- width: 300px;
- background: rgba(255, 255, 255, 0.97);
- backdrop-filter: blur(28px);
- -webkit-backdrop-filter: blur(28px);
- border-radius: 20px;
- border: 1px solid rgba(147, 64, 42, 0.18);
- box-shadow:
- 0 12px 48px rgba(42, 38, 34, 0.22),
- 0 2px 10px rgba(42, 38, 34, 0.1);
-
- transform-origin: bottom right;
- animation: cmtMorphIn 0.38s cubic-bezier(0.22, 1, 0.36, 1);
- overflow: hidden;
-}
-
-@keyframes cmtMorphIn {
- 0% {
- opacity: 0;
- transform: scale(0.55) translateY(8px);
- border-radius: 50px;
- }
- 60% {
- border-radius: 22px;
- }
- 100% {
- opacity: 1;
- transform: scale(1) translateY(0);
- border-radius: 20px;
- }
-}
-
-/* Header */
-.cmt-panel-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 16px 18px 12px;
- border-bottom: 1px solid rgba(147, 64, 42, 0.1);
-}
-
-.cmt-panel-title {
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 13px;
- font-weight: 600;
- color: #2A2622;
- line-height: 1.3;
-}
-
-.cmt-panel-close {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- height: 28px;
- border: none;
- border-radius: 50%;
- background: rgba(42, 38, 34, 0.07);
- color: #6B645D;
- font-size: 12px;
- cursor: pointer;
- flex-shrink: 0;
- transition: background 0.15s, color 0.15s;
-}
-
-.cmt-panel-close:hover {
- background: rgba(42, 38, 34, 0.13);
- color: #2A2622;
-}
-
-/* Body */
-.cmt-panel-body {
- padding: 16px 18px 18px;
-}
-
-.cmt-label {
- display: block;
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 12px;
- color: #6B645D;
- margin-bottom: 7px;
- letter-spacing: 0.02em;
-}
-
-.cmt-input {
- width: 100%;
- box-sizing: border-box;
- border-radius: 12px;
- border: 1px solid rgba(147, 64, 42, 0.25);
- background: rgba(255, 255, 255, 0.9);
- color: #2a2622;
- padding: 10px 13px;
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 14px;
- outline: none;
- transition: border-color 0.15s, box-shadow 0.15s;
-}
-
-.cmt-input:focus {
- border-color: #d2694a;
- box-shadow: 0 0 0 3px rgba(210, 105, 74, 0.12);
-}
-
-.cmt-slug-preview {
- margin-top: 8px;
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 11px;
- color: #93402A;
-}
-
-.cmt-slug-preview code {
- background: rgba(147, 64, 42, 0.1);
- border-radius: 4px;
- padding: 1px 5px;
- color: #2a2622;
-}
-
-.cmt-error {
- margin-top: 8px;
- background: rgba(171, 49, 49, 0.08);
- border: 1px solid rgba(171, 49, 49, 0.25);
- color: #862f2f;
- border-radius: 10px;
- padding: 9px 12px;
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 12px;
-}
-
-.cmt-btn-create {
- margin-top: 14px;
- width: 100%;
- padding: 11px 16px;
- border-radius: 12px;
- border: none;
- background: #2a2622;
- color: #ffffff;
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 14px;
- font-weight: 500;
- cursor: pointer;
- transition: background 0.15s, box-shadow 0.15s, transform 0.15s;
-}
-
-.cmt-btn-create:hover:not(:disabled) {
- background: linear-gradient(135deg, #93402a, #2a2622);
- box-shadow: 0 4px 14px rgba(147, 64, 42, 0.32);
- transform: translateY(-1px);
-}
-
-.cmt-btn-create:active:not(:disabled) {
- transform: translateY(0);
-}
-
-.cmt-btn-create:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-}
-
-/* ----------------------------------------------------------
- Mobile — bottom sheet
- ---------------------------------------------------------- */
-@media (max-width: 768px) {
- .cmt-pill {
- bottom: 84px;
- right: 12px;
- font-size: 13px;
- padding: 11px 18px;
- }
-
- .cmt-panel {
- bottom: 0;
- right: 0;
- left: 0;
- width: 100%;
- border-radius: 20px 20px 0 0;
- transform-origin: bottom center;
- }
-
- @keyframes cmtMorphIn {
- from {
- opacity: 0;
- transform: translateY(100%);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
- }
-
- .cmt-panel-body {
- padding: 16px 20px 32px;
- }
-}
diff --git a/frontend/src/components/CreateMyTreeButton.jsx b/frontend/src/components/CreateMyTreeButton.jsx
deleted file mode 100644
index ebbf6cd..0000000
--- a/frontend/src/components/CreateMyTreeButton.jsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import { useState, useMemo } from 'react'
-import './CreateMyTreeButton.css'
-import PzBusy from './PzBusy.jsx'
-import { t } from '../i18n/index.js'
-
-function normalizeSlug(input) {
- return input
- .toLowerCase()
- .normalize('NFD')
- .replace(/[\u0300-\u036f]/g, '')
- .replace(/[^a-z0-9]+/g, '-')
- .replace(/^-+|-+$/g, '')
-}
-
-const CreateMyTreeButton = ({ onCreateTree, loading }) => {
- const [open, setOpen] = useState(false)
- const [name, setName] = useState('')
- const [error, setError] = useState('')
-
- const slug = useMemo(() => normalizeSlug(name.trim()), [name])
- const canSubmit = name.trim().length >= 2 && slug.length >= 3 && !loading
-
- const handleOpen = () => {
- setError('')
- setOpen(true)
- }
-
- const handleClose = () => {
- setOpen(false)
- setName('')
- setError('')
- }
-
- const handleSubmit = async (e) => {
- e.preventDefault()
- if (!canSubmit) return
- setError('')
- const result = await onCreateTree(name.trim())
- if (result !== null && result !== undefined) {
- setError(typeof result === 'string' ? result : t('Erreur lors de la création. Essayez un autre nom.'))
- }
- }
-
- return (
- <>
- {!open && (
-
- )}
-
- {open && (
-
-
- {t('Créer votre arbre dès maintenant')}
-
-
-
-
- )}
- >
- )
-}
-
-export default CreateMyTreeButton
diff --git a/frontend/src/components/EditModeOverlay.jsx b/frontend/src/components/EditModeOverlay.jsx
deleted file mode 100644
index a58ce18..0000000
--- a/frontend/src/components/EditModeOverlay.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useMemo } from 'react'
-import { X } from 'lucide-react'
-import './EditModeOverlay.css'
-import { t } from '../i18n/index.js'
-
-function EditModeOverlay({ active, hasDraft, role = 'contributor', onDeactivate, onSubmit }) {
- const isAdmin = role === 'admin'
-
- const borderClass = useMemo(() => (
- `edit-mode-border ${isAdmin ? 'edit-mode-border--admin' : 'edit-mode-border--contributor'}`
- ), [isAdmin])
-
- const toastLabel = isAdmin ? t('Mode édition actif') : t('Mode contribution actif')
- const submitLabel = isAdmin ? t("Mettre à jour l'arbre") : t('Envoyer les modifications')
-
- if (!active) return null
-
- return (
- <>
-
-
-
- {toastLabel}
-
- {onSubmit && (
-
- )}
-
-
-
- >
- )
-}
-
-export default EditModeOverlay
diff --git a/frontend/src/components/InitialTreeWizard.jsx b/frontend/src/components/InitialTreeWizard.jsx
deleted file mode 100644
index 351f0a2..0000000
--- a/frontend/src/components/InitialTreeWizard.jsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import { useMemo, useState } from 'react'
-import PzBusy from './PzBusy.jsx'
-import { t } from '../i18n/index.js'
-
-const EMPTY_FORM = {
- selfFirstName: '',
- selfLastName: '',
- selfBirthYear: '',
- parent1FirstName: '',
- parent1LastName: '',
- parent2FirstName: '',
- parent2LastName: '',
-}
-
-function toDateFromYear(yearText) {
- const year = Number.parseInt(yearText, 10)
- if (!Number.isFinite(year) || year < 1000 || year > 2999) {
- return null
- }
-
- return `${year.toString().padStart(4, '0')}-01-01`
-}
-
-const InitialTreeWizard = ({ loading, errorMessage, onSubmit, onSkip }) => {
- const [form, setForm] = useState(EMPTY_FORM)
-
- const canSubmit = useMemo(() => {
- return (
- form.selfFirstName.trim().length > 0 &&
- form.selfLastName.trim().length > 0 &&
- form.parent1FirstName.trim().length > 0 &&
- form.parent1LastName.trim().length > 0 &&
- form.parent2FirstName.trim().length > 0 &&
- form.parent2LastName.trim().length > 0 &&
- !loading
- )
- }, [form, loading])
-
- const updateField = (key, value) => {
- setForm((current) => ({ ...current, [key]: value }))
- }
-
- const handleSubmit = async (event) => {
- event.preventDefault()
- if (!canSubmit) return
-
- await onSubmit({
- self: {
- firstName: form.selfFirstName.trim(),
- lastName: form.selfLastName.trim(),
- birthDate: toDateFromYear(form.selfBirthYear.trim()),
- },
- parent1: {
- firstName: form.parent1FirstName.trim(),
- lastName: form.parent1LastName.trim(),
- },
- parent2: {
- firstName: form.parent2FirstName.trim(),
- lastName: form.parent2LastName.trim(),
- },
- })
- }
-
- return (
-
- )
-}
-
-export default InitialTreeWizard
diff --git a/frontend/src/components/InlinePersonForm.css b/frontend/src/components/InlinePersonForm.css
deleted file mode 100644
index 8b67551..0000000
--- a/frontend/src/components/InlinePersonForm.css
+++ /dev/null
@@ -1,262 +0,0 @@
-/* ============================================================
- Inline Person Form - Mini-formulaire après drop
- ============================================================ */
-
-.inline-person-form-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(42, 38, 34, 0.4);
- backdrop-filter: blur(4px);
- z-index: 1200;
- animation: overlayFadeIn 0.2s ease;
-}
-
-@keyframes overlayFadeIn {
- from {
- opacity: 0;
- }
- to {
- opacity: 1;
- }
-}
-
-.inline-person-form {
- position: fixed;
- padding: 24px;
- background: rgba(242, 239, 234, 0.98);
- backdrop-filter: blur(32px);
- -webkit-backdrop-filter: blur(32px);
- border-radius: 16px;
- border: 1px solid rgba(42, 38, 34, 0.12);
-
- box-shadow:
- 0 12px 48px rgba(42, 38, 34, 0.3),
- 0 4px 16px rgba(42, 38, 34, 0.15);
-
- min-width: 320px;
- max-width: 400px;
-
- z-index: 1201;
-
- animation: formPopIn 0.3s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-@keyframes formPopIn {
- from {
- opacity: 0;
- transform: translate(-50%, -45%) scale(0.9);
- }
- to {
- opacity: 1;
- transform: translate(-50%, -50%) scale(1);
- }
-}
-
-/* Header */
-.inline-person-form-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 20px;
- padding-bottom: 16px;
- border-bottom: 1px solid rgba(42, 38, 34, 0.1);
-}
-
-.inline-person-form-title {
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 18px;
- font-weight: 700;
- color: #2A2622;
- display: flex;
- align-items: center;
- gap: 10px;
-}
-
-.inline-person-form-icon {
- font-size: 24px;
-}
-
-.inline-person-form-close {
- width: 32px;
- height: 32px;
- border-radius: 50%;
- border: none;
- background: transparent;
- color: #6B645D;
- font-size: 20px;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.2s ease;
-}
-
-.inline-person-form-close:hover {
- background: rgba(42, 38, 34, 0.1);
- color: #2A2622;
- transform: scale(1.1);
-}
-
-/* Badge lien créé */
-.inline-person-form-badge {
- display: inline-block;
- padding: 6px 12px;
- border-radius: 12px;
- font-size: 14px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- margin-bottom: 16px;
-}
-
-.inline-person-form-badge.child {
- background: rgba(76, 217, 100, 0.15);
- color: #4CD964;
-}
-
-.inline-person-form-badge.spouse {
- background: rgba(255, 59, 48, 0.15);
- color: #FF3B30;
-}
-
-.inline-person-form-badge.sibling {
- background: rgba(210, 105, 74, 0.15);
- color: #93402A;
-}
-
-/* Fields */
-.inline-person-form-fields {
- display: flex;
- flex-direction: column;
- gap: 14px;
- margin-bottom: 20px;
-}
-
-.inline-person-form-field {
- display: flex;
- flex-direction: column;
- gap: 6px;
-}
-
-.inline-person-form-label {
- font-size: 14px;
- font-weight: 450;
- color: #2A2622;
- letter-spacing: 0.2px;
-}
-
-.inline-person-form-label .required {
- color: #FF3B30;
- margin-left: 2px;
-}
-
-.inline-person-form-input {
- padding: 10px 14px;
- border-radius: 10px;
- border: 1.5px solid rgba(42, 38, 34, 0.15);
- background: rgba(255, 255, 255, 0.6);
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 16px; /* ≥16px prevents iOS Safari from auto-zooming on focus */
- color: #2A2622;
- transition: all 0.2s ease;
-}
-
-.inline-person-form-input:focus {
- outline: none;
- border-color: #93402A;
- background: rgba(255, 255, 255, 0.9);
- box-shadow: 0 0 0 3px rgba(147, 64, 42, 0.1);
-}
-
-.inline-person-form-input::placeholder {
- color: #6B645D;
- opacity: 0.6;
-}
-
-/* Row (2 champs côte-à-côte) */
-.inline-person-form-row {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 12px;
-}
-
-/* Actions */
-.inline-person-form-actions {
- display: flex;
- gap: 10px;
- justify-content: flex-end;
-}
-
-.inline-person-form-button {
- padding: 10px 20px;
- border-radius: 10px;
- border: none;
- font-family: 'DM Sans', system-ui, sans-serif;
- font-size: 14px;
- font-weight: 450;
- cursor: pointer;
- transition: all 0.2s ease;
-}
-
-.inline-person-form-button.cancel {
- background: transparent;
- color: #6B645D;
-}
-
-.inline-person-form-button.cancel:hover {
- background: rgba(42, 38, 34, 0.08);
- color: #2A2622;
-}
-
-.inline-person-form-button.submit {
- background: #2a2622;
- color: #FFFFFF;
- box-shadow: 0 4px 12px rgba(147, 64, 42, 0.3);
-}
-
-.inline-person-form-button.submit:hover {
- transform: translateY(-2px);
- box-shadow: 0 6px 16px rgba(147, 64, 42, 0.4);
-}
-
-.inline-person-form-button.submit:active {
- transform: translateY(0);
-}
-
-.inline-person-form-button.submit:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- transform: none;
-}
-
-/* Loading state */
-.inline-person-form.loading {
- pointer-events: none;
- opacity: 0.7;
-}
-
-/* Responsive mobile */
-@media (max-width: 768px) {
- .inline-person-form {
- left: 12px;
- right: 12px;
- top: 50%;
- transform: translateY(-50%);
- min-width: auto;
- max-width: none;
- }
-
- @keyframes formPopIn {
- from {
- opacity: 0;
- transform: translateY(-45%) scale(0.9);
- }
- to {
- opacity: 1;
- transform: translateY(-50%) scale(1);
- }
- }
-}
diff --git a/frontend/src/components/InlinePersonForm.jsx b/frontend/src/components/InlinePersonForm.jsx
deleted file mode 100644
index 3a533d6..0000000
--- a/frontend/src/components/InlinePersonForm.jsx
+++ /dev/null
@@ -1,215 +0,0 @@
-import { useState, useEffect, useRef } from 'react'
-import { UserPlus, X } from 'lucide-react'
-import './InlinePersonForm.css'
-import PzBusy from './PzBusy.jsx'
-import { t } from '../i18n/index.js'
-
-/**
- * Mini-formulaire inline pour créer une personne après drop
- * Affiche le type de lien créé et demande les infos de base
- *
- * @param {Object} props
- * @param {boolean} props.visible - Afficher le formulaire
- * @param {Object} props.position - Position d'affichage {x, y}
- * @param {string} props.linkType - Type de lien ('child', 'spouse', 'sibling', null)
- * @param {Object} props.linkedPerson - Personne liée (si linkType)
- * @param {Function} props.onSubmit - Callback soumission (formData)
- * @param {Function} props.onCancel - Callback annulation
- * @param {boolean} props.loading - État chargement
- */
-function InlinePersonForm({
- visible,
- position,
- linkType,
- linkedPerson,
- onSubmit,
- onCancel,
- loading = false,
-}) {
- const [formData, setFormData] = useState({
- firstName: '',
- lastName: '',
- birthDate: '',
- birthPlace: '',
- })
-
- const firstInputRef = useRef(null)
-
- // Reset form au show
- useEffect(() => {
- if (visible) {
- setFormData({
- firstName: '',
- lastName: '',
- birthDate: '',
- birthPlace: '',
- })
- // Focus premier input
- setTimeout(() => firstInputRef.current?.focus(), 100)
- }
- }, [visible])
-
- // Handle ESC
- useEffect(() => {
- if (!visible) return
-
- const handleEscape = (e) => {
- if (e.key === 'Escape') onCancel?.()
- }
-
- document.addEventListener('keydown', handleEscape)
- return () => document.removeEventListener('keydown', handleEscape)
- }, [visible, onCancel])
-
- if (!visible) return null
-
- const handleChange = (field, value) => {
- setFormData((prev) => ({ ...prev, [field]: value }))
- }
-
- const handleSubmit = (e) => {
- e.preventDefault()
- if (!formData.firstName.trim()) return
-
- onSubmit?.({
- ...formData,
- linkType,
- linkedPersonId: linkedPerson?.id,
- })
- }
-
- const isValid = formData.firstName.trim().length > 0
-
- // Labels type lien
- const linkLabels = {
- child: (name) => t('Enfant de {name}', { name }),
- spouse: (name) => t('Conjoint de {name}', { name }),
- sibling: (name) => t('Frère·Sœur de {name}', { name }),
- }
-
- const linkTitle = {
- child: t('Nouvel enfant'),
- spouse: t('Nouveau conjoint'),
- sibling: t('Nouveau frère/sœur'),
- }
-
- const title = linkType ? linkTitle[linkType] : t('Nouvelle personne')
-
- return (
- <>
- {/* Overlay */}
-
-
- {/* Form */}
-
- >
- )
-}
-
-export default InlinePersonForm
diff --git a/frontend/src/components/ProximityFeedback.css b/frontend/src/components/ProximityFeedback.css
deleted file mode 100644
index 79575d2..0000000
--- a/frontend/src/components/ProximityFeedback.css
+++ /dev/null
@@ -1,146 +0,0 @@
-/* ============================================================
- Proximity Feedback - Feedback visuel drag proximity
- ============================================================ */
-
-/* Ligne pointillée entre drag et personne proche */
-.proximity-line {
- position: fixed;
- pointer-events: none;
- z-index: 1000;
-}
-
-.proximity-line-svg {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- overflow: visible;
-}
-
-.proximity-line-path {
- fill: none;
- stroke-width: 2;
- stroke-dasharray: 6, 4;
- stroke-linecap: round;
- animation: proximityLineDash 1s linear infinite;
-}
-
-.proximity-line-path.child {
- stroke: rgba(76, 217, 100, 0.8);
-}
-
-.proximity-line-path.spouse {
- stroke: rgba(255, 59, 48, 0.7);
-}
-
-.proximity-line-path.sibling {
- stroke: rgba(210, 105, 74, 0.7);
-}
-
-.proximity-line-path.none {
- stroke: rgba(142, 142, 147, 0.5);
-}
-
-@keyframes proximityLineDash {
- to {
- stroke-dashoffset: -10;
- }
-}
-
-/* Badge type de lien sur la ligne */
-.proximity-badge {
- position: fixed;
- padding: 6px 12px;
- border-radius: 12px;
- font-size: 14px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- white-space: nowrap;
- pointer-events: none;
- z-index: 1001;
-
- box-shadow: 0 4px 12px rgba(42, 38, 34, 0.15);
-
- animation: badgePulse 0.5s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-@keyframes badgePulse {
- 0% {
- opacity: 0;
- transform: translate(-50%, -50%) scale(0.8);
- }
- 100% {
- opacity: 1;
- transform: translate(-50%, -50%) scale(1);
- }
-}
-
-.proximity-badge.child {
- background: rgba(76, 217, 100, 0.95);
- color: white;
-}
-
-.proximity-badge.spouse {
- background: rgba(255, 59, 48, 0.95);
- color: white;
-}
-
-.proximity-badge.sibling {
- background: rgba(210, 105, 74, 0.95);
- color: white;
-}
-
-/* Highlight glow sur la personne proche */
-.proximity-highlight {
- position: fixed;
- pointer-events: none;
- z-index: 999;
-}
-
-.proximity-highlight-glow {
- width: 100px;
- height: 100px;
- border-radius: 50%;
- opacity: 0.6;
- animation: proximityGlow 1.5s ease-in-out infinite;
-}
-
-.proximity-highlight-glow.child {
- background: radial-gradient(
- circle,
- rgba(76, 217, 100, 0.4) 0%,
- rgba(76, 217, 100, 0.2) 50%,
- transparent 100%
- );
-}
-
-.proximity-highlight-glow.spouse {
- background: radial-gradient(
- circle,
- rgba(255, 59, 48, 0.35) 0%,
- rgba(255, 59, 48, 0.15) 50%,
- transparent 100%
- );
-}
-
-.proximity-highlight-glow.sibling {
- background: radial-gradient(
- circle,
- rgba(210, 105, 74, 0.35) 0%,
- rgba(210, 105, 74, 0.15) 50%,
- transparent 100%
- );
-}
-
-@keyframes proximityGlow {
- 0%, 100% {
- transform: scale(1);
- opacity: 0.6;
- }
- 50% {
- transform: scale(1.15);
- opacity: 0.8;
- }
-}
diff --git a/frontend/src/components/ProximityFeedback.jsx b/frontend/src/components/ProximityFeedback.jsx
deleted file mode 100644
index d40f82c..0000000
--- a/frontend/src/components/ProximityFeedback.jsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { t } from '../i18n/index.js'
-import './ProximityFeedback.css'
-
-/**
- * Feedback visuel de proximité pendant le drag
- * Affiche ligne pointillée + glow sur personne proche + badge type lien
- *
- * @param {Object} props
- * @param {boolean} props.visible - Afficher le feedback
- * @param {Object} props.dragPosition - Position drag {x, y}
- * @param {Object} props.closestPerson - Personne proche {id, x, y, person}
- * @param {string} props.linkType - Type de lien ('child', 'spouse', 'sibling')
- */
-function ProximityFeedback({ visible, dragPosition, closestPerson, linkType }) {
- if (!visible || !closestPerson || !linkType) return null
-
- const { x: personX, y: personY } = closestPerson
- const { x: dragX, y: dragY } = dragPosition
-
- // Position badge au milieu de la ligne
- const badgeX = (dragX + personX) / 2
- const badgeY = (dragY + personY) / 2
-
- // Labels
- const linkLabels = {
- child: t('Enfant'),
- spouse: t('Conjoint'),
- sibling: t('Frère·Sœur'),
- }
-
- return (
- <>
- {/* Ligne pointillée */}
-
-
-
-
- {/* Badge type lien */}
-
- {linkLabels[linkType] || t('Lien')}
-
-
- {/* Glow sur personne proche */}
-
- >
- )
-}
-
-export default ProximityFeedback
diff --git a/frontend/src/components/annotations/DropTrashZone.css b/frontend/src/components/annotations/DropTrashZone.css
deleted file mode 100644
index 5705a5a..0000000
--- a/frontend/src/components/annotations/DropTrashZone.css
+++ /dev/null
@@ -1,91 +0,0 @@
-.drop-trash-zone {
- position: fixed;
- top: 24px;
- left: 50%;
- transform: translateX(-50%);
- z-index: 1060;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- padding: 16px 32px 14px;
- border-radius: 20px;
- background: rgba(255, 255, 255, 0.95);
- backdrop-filter: blur(24px);
- -webkit-backdrop-filter: blur(24px);
- border: 2px dashed rgba(147, 64, 42, 0.3);
- box-shadow:
- 0 8px 40px rgba(42, 38, 34, 0.2),
- 0 2px 8px rgba(42, 38, 34, 0.08);
- animation: trashSlideDown 0.35s cubic-bezier(0.22, 1, 0.36, 1);
- transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-@keyframes trashSlideDown {
- from {
- opacity: 0;
- transform: translateX(-50%) translateY(-20px) scale(0.85);
- }
- to {
- opacity: 1;
- transform: translateX(-50%) translateY(0) scale(1);
- }
-}
-
-.drop-trash-zone__circle {
- width: 52px;
- height: 52px;
- border-radius: 50%;
- background: rgba(147, 64, 42, 0.1);
- border: 2px solid rgba(147, 64, 42, 0.2);
- display: flex;
- align-items: center;
- justify-content: center;
- color: #6B645D;
- transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-.drop-trash-zone__icon {
- transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
-}
-
-.drop-trash-zone__label {
- font-size: 14px;
- font-weight: 450;
- color: #2A2622;
- transition: all 0.2s ease;
- white-space: nowrap;
-}
-
-/* --- Hover state: over trash --- */
-.drop-trash-zone--over {
- border-color: #C0392B;
- background: rgba(192, 57, 43, 0.08);
- transform: translateX(-50%) scale(1.08);
- box-shadow:
- 0 12px 48px rgba(192, 57, 43, 0.25),
- 0 4px 12px rgba(192, 57, 43, 0.12);
-}
-
-.drop-trash-zone--over .drop-trash-zone__circle {
- width: 64px;
- height: 64px;
- background: rgba(192, 57, 43, 0.12);
- border-color: #C0392B;
- color: #C0392B;
- animation: trashPulse 0.8s ease infinite;
-}
-
-@keyframes trashPulse {
- 0%, 100% {
- box-shadow: 0 0 0 0 rgba(192, 57, 43, 0.3);
- }
- 50% {
- box-shadow: 0 0 0 10px rgba(192, 57, 43, 0);
- }
-}
-
-.drop-trash-zone--over .drop-trash-zone__label {
- color: #C0392B;
- font-weight: 700;
-}
diff --git a/frontend/src/components/annotations/DropTrashZone.jsx b/frontend/src/components/annotations/DropTrashZone.jsx
deleted file mode 100644
index de3d342..0000000
--- a/frontend/src/components/annotations/DropTrashZone.jsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { Trash2 } from 'lucide-react'
-import { t } from '../../i18n/index.js'
-import './DropTrashZone.css'
-
-export default function DropTrashZone({ visible, isOver }) {
- if (!visible) return null
-
- return (
-
-
-
-
-
- {isOver ? t('Relâcher pour supprimer') : t('Glisser ici pour supprimer')}
-
-
- )
-}
diff --git a/frontend/src/hooks/useAddPersonDrag.js b/frontend/src/hooks/useAddPersonDrag.js
deleted file mode 100644
index 410fa8e..0000000
--- a/frontend/src/hooks/useAddPersonDrag.js
+++ /dev/null
@@ -1,183 +0,0 @@
-import { useState, useRef, useCallback, useEffect } from 'react'
-
-/**
- * Seuils de proximité pour détection de liens
- */
-const PROXIMITY_THRESHOLDS = {
- CHILD: 150, // Pixels en dessous pour détecter lien enfant
- SPOUSE: 120, // Pixels côte-à-côte pour détecter lien conjoint
- SIBLING: 100, // Pixels même niveau pour détecter lien frère/sœur
-}
-
-/**
- * Détecte le type de lien selon position relative
- * @param {Object} dragPos - Position drag {x, y}
- * @param {Object} personPos - Position personne proche {x, y}
- * @param {number} distance - Distance euclidienne
- * @returns {string|null} Type de lien ('child', 'spouse', 'sibling') ou null
- */
-function detectLinkType(dragPos, personPos, distance) {
- const dx = dragPos.x - personPos.x
- const dy = dragPos.y - personPos.y
-
- // Angle pour déterminer direction
-
- // ENFANT : en dessous (angle ~90°, dy > 0)
- if (dy > 50 && Math.abs(dx) < PROXIMITY_THRESHOLDS.CHILD && distance < PROXIMITY_THRESHOLDS.CHILD) {
- return 'child'
- }
-
- // CONJOINT : à côté (angle ~0° ou ~180°, dy faible)
- if (Math.abs(dy) < 60 && Math.abs(dx) < PROXIMITY_THRESHOLDS.SPOUSE && distance < PROXIMITY_THRESHOLDS.SPOUSE) {
- return 'spouse'
- }
-
- // FRÈRE/SŒUR : même niveau vertical (dy faible mais dx moyen)
- if (Math.abs(dy) < 80 && Math.abs(dx) < PROXIMITY_THRESHOLDS.SIBLING + 50 && distance < PROXIMITY_THRESHOLDS.SIBLING + 50) {
- return 'sibling'
- }
-
- return null
-}
-
-/**
- * Hook pour gérer le drag-and-drop d'ajout de personne
- * Détecte automatiquement la proximité avec personnes existantes
- *
- * @param {Object} options
- * @param {Function} options.getPersonPositions - Fonction qui retourne [{id, x, y, person}]
- * @param {Function} options.onDrop - Callback au drop (dragPos, closestPerson, linkType)
- * @returns {Object} API du drag
- */
-export function useAddPersonDrag({ getPersonPositions, onDrop }) {
- const [isDragging, setIsDragging] = useState(false)
- const [dragPosition, setDragPosition] = useState({ x: 0, y: 0 })
- const [closestPerson, setClosestPerson] = useState(null)
- const [linkType, setLinkType] = useState(null)
-
- const dragStartPos = useRef({ x: 0, y: 0 })
- const buttonStartPos = useRef({ x: 0, y: 0 })
-
- /**
- * Démarre le drag
- */
- const handleDragStart = useCallback((event, buttonPosition) => {
- setIsDragging(true)
- dragStartPos.current = {
- x: event.clientX,
- y: event.clientY,
- }
- buttonStartPos.current = buttonPosition
- setDragPosition(buttonPosition)
- }, [])
-
- /**
- * Update position pendant le drag + détection proximité
- */
- const handleDragMove = useCallback((event) => {
- if (!isDragging) return
-
- const dx = event.clientX - dragStartPos.current.x
- const dy = event.clientY - dragStartPos.current.y
-
- const newPos = {
- x: buttonStartPos.current.x + dx,
- y: buttonStartPos.current.y + dy,
- }
-
- setDragPosition(newPos)
-
- // Détection proximité
- const personPositions = getPersonPositions?.() || []
- if (personPositions.length === 0) {
- setClosestPerson(null)
- setLinkType(null)
- return
- }
-
- // Trouver la personne la plus proche
- let minDistance = Infinity
- let closest = null
-
- for (const personPos of personPositions) {
- const dx = newPos.x - personPos.x
- const dy = newPos.y - personPos.y
- const distance = Math.sqrt(dx * dx + dy * dy)
-
- if (distance < minDistance) {
- minDistance = distance
- closest = personPos
- }
- }
-
- // Si une personne proche trouvée, déterminer le type de lien
- if (closest && minDistance < 200) {
- const detectedLinkType = detectLinkType(newPos, closest, minDistance)
- setClosestPerson(closest)
- setLinkType(detectedLinkType)
- } else {
- setClosestPerson(null)
- setLinkType(null)
- }
- }, [isDragging, getPersonPositions])
-
- /**
- * Termine le drag
- */
- const handleDragEnd = useCallback(() => {
- if (!isDragging) return
-
- setIsDragging(false)
-
- // Callback avec infos drop
- onDrop?.({
- position: dragPosition,
- closestPerson,
- linkType,
- })
-
- // Reset
- setClosestPerson(null)
- setLinkType(null)
- }, [isDragging, dragPosition, closestPerson, linkType, onDrop])
-
- /**
- * Cancel drag (ESC)
- */
- const handleDragCancel = useCallback(() => {
- setIsDragging(false)
- setClosestPerson(null)
- setLinkType(null)
- setDragPosition({ x: 0, y: 0 })
- }, [])
-
- // Listeners globaux pour drag
- useEffect(() => {
- if (!isDragging) return
-
- const handleMouseMove = (e) => handleDragMove(e)
- const handleMouseUp = () => handleDragEnd()
- const handleEscape = (e) => {
- if (e.key === 'Escape') handleDragCancel()
- }
-
- document.addEventListener('mousemove', handleMouseMove)
- document.addEventListener('mouseup', handleMouseUp)
- document.addEventListener('keydown', handleEscape)
-
- return () => {
- document.removeEventListener('mousemove', handleMouseMove)
- document.removeEventListener('mouseup', handleMouseUp)
- document.removeEventListener('keydown', handleEscape)
- }
- }, [isDragging, handleDragMove, handleDragEnd, handleDragCancel])
-
- return {
- isDragging,
- dragPosition,
- closestPerson,
- linkType,
- handleDragStart,
- handleDragCancel,
- }
-}
diff --git a/frontend/src/i18n/en/fiche.js b/frontend/src/i18n/en/fiche.js
index 75018d8..96911c8 100644
--- a/frontend/src/i18n/en/fiche.js
+++ b/frontend/src/i18n/en/fiche.js
@@ -197,7 +197,7 @@ export default {
'Création en cours': 'Creating',
'Créer': 'Create',
- // Création d'un arbre (InitialTreeWizard, TreeCreationWizard, CreateMyTreeButton)
+ // Création d'un arbre (TreeCreationWizard)
'Vous': 'You',
'Année de naissance (optionnel)': 'Year of birth (optional)',
'Parent 1': 'Parent 1',
diff --git a/frontend/src/utils/graphAdapter.js b/frontend/src/utils/graphAdapter.js
deleted file mode 100644
index 9ef7b63..0000000
--- a/frontend/src/utils/graphAdapter.js
+++ /dev/null
@@ -1,108 +0,0 @@
-/**
- * Adaptateur pour transformer les données de l'API backend
- * vers le format attendu par le composant Galaxy
- */
-
-import { t } from '../i18n/index.js'
-
-/**
- * Convertit une date ISO (YYYY-MM-DD) en année
- * @param {string | null} isoDate - Date au format ISO
- * @returns {number | null} - Année ou null
- */
-function dateToYear(isoDate) {
- if (!isoDate) return null
- const year = parseInt(isoDate.split('-')[0], 10)
- return isNaN(year) ? null : year
-}
-
-/**
- * Détermine si une personne est vivante
- * @param {string | null} deathDate - Date de décès
- * @returns {boolean}
- */
-function isAlive(deathDate) {
- return !deathDate
-}
-
-/**
- * Adapte les données du graphe API vers le format Galaxy
- * @param {Object} apiGraph - Graphe depuis l'API { persons, unions, filiations, medias }
- * @param {string} treeId - ID de l'arbre pour construire les URLs
- * @returns {Object} - Format compatible Galaxy { persons, unions, filiations, medias }
- */
-export function adaptGraphForGalaxy(apiGraph, _treeId) {
- if (!apiGraph) {
- return {
- persons: [],
- unions: [],
- filiations: [],
- medias: [],
- }
- }
-
- const { persons = [], unions = [], filiations = [], medias = [] } = apiGraph
-
- // Adapter les personnes
- const adaptedPersons = persons.map((p) => ({
- id: p.id,
- firstName: p.firstName || '',
- lastName: p.lastName || '',
- birthName: p.birthName || null,
- nickname: p.nickname || null,
- birthYear: dateToYear(p.birthDate),
- deathYear: dateToYear(p.deathDate),
- isAlive: isAlive(p.deathDate),
- sex: p.sex || null,
- note: p.notes || null,
- birthDate: p.birthDate,
- deathDate: p.deathDate,
- photo: p.avatarUrlPath ? `/api${p.avatarUrlPath}` : null,
- avatarFromImmich: Boolean(p.avatarFromImmich),
- avatarCrop: p.avatarCrop ?? null,
- frameType: 'round', // Default
- }))
-
- // Adapter les unions
- const adaptedUnions = unions.map((u) => ({
- id: u.id,
- partner1Id: u.partner1PersonId,
- partner2Id: u.partner2PersonId,
- unionType: u.unionType || 'union',
- startYear: dateToYear(u.startDate),
- endYear: dateToYear(u.endDate),
- displayOrder: u.displayOrder,
- startDate: u.startDate,
- endDate: u.endDate,
- }))
-
- // Adapter les filiations
- const adaptedFiliations = filiations.map((f) => ({
- id: f.id,
- childId: f.childPersonId,
- parentId: f.parentPersonId,
- unionId: f.viaUnionId,
- parentageType: f.parentageType || 'biologique',
- displayOrder: f.displayOrder,
- }))
-
- // Adapter les médias
- const adaptedMedias = medias.map((m) => ({
- id: m.id,
- personId: m.personId,
- type: m.type,
- url: m.type === 'photo' ? `/api${m.urlPath}` : null,
- urlHd: m.type === 'photo' ? `/api${m.urlPath}` : null,
- label: m.caption || t('Média {n}', { n: m.displayOrder }),
- source: m.source || null,
- displayOrder: m.displayOrder,
- isFeatured: m.isFeatured,
- }))
-
- return {
- persons: adaptedPersons,
- unions: adaptedUnions,
- filiations: adaptedFiliations,
- medias: adaptedMedias,
- }
-}
diff --git a/frontend/src/utils/graphDataHelpers.js b/frontend/src/utils/graphDataHelpers.js
deleted file mode 100644
index 9bb0081..0000000
--- a/frontend/src/utils/graphDataHelpers.js
+++ /dev/null
@@ -1,117 +0,0 @@
-/**
- * Fonctions helper pour manipuler les données du graphe
- * Ces fonctions acceptent les données en paramètre au lieu d'utiliser les imports statiques
- */
-
-/**
- * Récupère les médias d'une personne
- * @param {string} personId
- * @param {Array} medias - Liste des médias
- * @returns {Array}
- */
-export function getPersonMedias(personId, medias = []) {
- return medias
- .filter((media) => media.personId === personId)
- .sort((a, b) => {
- const orderDiff = (a.displayOrder || 0) - (b.displayOrder || 0)
- if (orderDiff !== 0) return orderDiff
- return (a.id || '').localeCompare(b.id || '')
- })
- .slice(0, 8) // Max 8 médias
-}
-
-/**
- * Récupère les parents d'une personne
- * @param {string} personId
- * @param {Array} persons
- * @param {Array} unions
- * @param {Array} filiations
- * @returns {Array}
- */
-export function getParents(personId, persons = [], unions = [], filiations = []) {
- const fil = filiations.filter((filiation) => filiation.childId === personId)
- const result = []
-
- fil.forEach((f) => {
- if (f.unionId) {
- const union = unions.find((u) => u.id === f.unionId)
- if (union) {
- const p1 = persons.find((p) => p.id === union.partner1Id)
- const p2 = persons.find((p) => p.id === union.partner2Id)
- if (p1) result.push(p1)
- if (p2) result.push(p2)
- }
- } else if (f.parentId) {
- const parent = persons.find((p) => p.id === f.parentId)
- if (parent) result.push(parent)
- }
- })
-
- // Dédupliquer
- const seen = new Set()
- return result.filter((p) => {
- if (seen.has(p.id)) return false
- seen.add(p.id)
- return true
- })
-}
-
-/**
- * Récupère les enfants d'une personne
- * @param {string} personId
- * @param {Array} persons
- * @param {Array} unions
- * @param {Array} filiations
- * @returns {Array}
- */
-export function getChildren(personId, persons = [], unions = [], filiations = []) {
- const childIds = new Set()
-
- // Via unions
- const personUnions = unions.filter((union) => union.partner1Id === personId || union.partner2Id === personId)
- personUnions.forEach((union) => {
- const childrenOfUnion = filiations.filter((fil) => fil.unionId === union.id)
- childrenOfUnion.forEach((fil) => childIds.add(fil.childId))
- })
-
- // Via parentId direct
- const directChildren = filiations.filter((fil) => fil.parentId === personId)
- directChildren.forEach((fil) => childIds.add(fil.childId))
-
- return persons.filter((p) => childIds.has(p.id))
-}
-
-/**
- * Récupère les unions d'une personne
- * @param {string} personId
- * @param {Array} unions
- * @returns {Array}
- */
-export function getPersonUnions(personId, unions = []) {
- return unions
- .filter((union) => union.partner1Id === personId || union.partner2Id === personId)
- .sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0))
-}
-
-/**
- * Récupère la filiation d'une personne (comment elle est reliée à ses parents)
- * @param {string} personId
- * @param {Array} filiations
- * @returns {Object | null}
- */
-export function getChildFiliation(personId, filiations = []) {
- const childFiliations = filiations
- .filter((f) => f.childId === personId)
- .sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0))
-
- if (childFiliations.length === 0) {
- return null
- }
-
- const viaUnion = childFiliations.find((f) => Boolean(f.unionId))
- if (viaUnion) {
- return viaUnion
- }
-
- return childFiliations.find((f) => (f.parentageType || 'biologique') === 'biologique') || childFiliations[0]
-}