diff --git a/frontend/src/components/SearchSourcesPanel.css b/frontend/src/components/SearchSourcesPanel.css index 1bf1d9b..80b1c2d 100644 --- a/frontend/src/components/SearchSourcesPanel.css +++ b/frontend/src/components/SearchSourcesPanel.css @@ -36,14 +36,25 @@ .sp-results { gap: 8px; } .sp-results-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; } .sp-results-title { margin: 0; font: 400 17px/1.25 var(--pz-serif); color: var(--pz-ink); } +.sp-results-sub { margin: 1px 0 0; font: 400 12px var(--pz-sans); color: var(--pz-muted); } + +/* Recherche repliée : une ligne au-dessus des résultats */ +.sp-summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-radius: var(--pz-r-m); background: var(--pz-bg); border: 1px solid var(--pz-line); } +.sp-summary-text { min-width: 0; } +.sp-summary-who { margin: 0; font: 400 16px/1.25 var(--pz-serif); color: var(--pz-ink); } +.sp-summary-who em { font-style: italic; font-weight: 300; } +.sp-summary .pz-hint { margin-top: 2px; } .sp-result { padding: 12px; border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); background: var(--pz-surface); } .sp-result--done { background: var(--pz-success-bg); border-color: transparent; } +.sp-result-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; } +.sp-result-head .sp-match { flex: none; } .sp-result-name { font: 400 16px/1.25 var(--pz-serif); color: var(--pz-ink); } +.sp-result-years { margin: 2px 0 4px; font: 400 12.5px var(--pz-sans); color: var(--pz-muted); font-variant-numeric: tabular-nums; } +.sp-result-reason { margin: 6px 0 0; font: 400 12px/1.4 var(--pz-sans); color: var(--pz-muted); } .sp-result-name em { font-style: italic; font-weight: 300; } .sp-result-line { margin: 3px 0 0; font: 400 13px/1.45 var(--pz-sans); color: var(--pz-ink-2); font-variant-numeric: tabular-nums; } -.sp-result-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; margin-top: 10px; } -.sp-result-foot .pz-btn { margin-left: auto; } +.sp-result-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px 12px; margin-top: 10px; } .sp-result-link { display: inline-flex; align-items: center; gap: 2px; font-size: 12.5px; } .sp-result-applied { margin: 8px 0 0; font: 500 12.5px var(--pz-sans); color: var(--pz-success); } @@ -62,9 +73,9 @@ .sp-link-list { list-style: none; margin: 0; padding: 0; } .sp-link { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; min-height: 40px; padding: 9px 0; border-bottom: 1px solid var(--pz-line-soft); text-decoration: none; color: var(--pz-ink); } .sp-link-list li:last-child .sp-link { border-bottom: 0; } -.sp-link-name { display: inline-flex; align-items: center; gap: 3px; font: 500 14px var(--pz-sans); } -.sp-link-name svg { color: var(--pz-faint); } -.sp-link-detail { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); text-align: right; } +.sp-link-name { font: 500 14px/1.35 var(--pz-sans); } +.sp-link-name svg { display: inline-block; color: var(--pz-faint); vertical-align: -1px; margin-left: 3px; } +.sp-link-detail { max-width: 55%; font: 400 12.5px var(--pz-sans); color: var(--pz-muted); text-align: right; } @media (hover: hover) and (pointer: fine) { .sp-link:hover .sp-link-name { text-decoration: underline; text-decoration-color: var(--pz-a300); text-underline-offset: 3px; } } diff --git a/frontend/src/components/SearchSourcesPanel.jsx b/frontend/src/components/SearchSourcesPanel.jsx index b7c14b5..cac4d99 100644 --- a/frontend/src/components/SearchSourcesPanel.jsx +++ b/frontend/src/components/SearchSourcesPanel.jsx @@ -15,11 +15,13 @@ const SOURCE_INFO = { name: t('Décès en France depuis 1970'), detail: t("Fichier de l'INSEE, par matchID. Date et lieu de décès, rien avant 1970. Une femme se cherche sous son nom de naissance."), sent: t("Envoie le nom, le prénom et l'année de naissance à matchid.io"), + short: t("Fichier de l'INSEE, par matchID"), }, wikitree: { name: 'WikiTree', detail: t('Un arbre mondial tenu par ses membres. Profils publics seulement.'), sent: t("Envoie le nom, le prénom et l'année de naissance à wikitree.com"), + short: t('Arbre tenu par ses membres'), }, } @@ -30,11 +32,50 @@ function formatDate(value) { if (!month) return String(year) const date = new Date(Date.UTC(year, month - 1, day || 1)) const options = day ? { day: 'numeric', month: 'long', year: 'numeric' } : { month: 'long', year: 'numeric' } - return date.toLocaleDateString(dateLocale, { ...options, timeZone: 'UTC' }) + const text = date.toLocaleDateString(dateLocale, { ...options, timeZone: 'UTC' }) + // « 1er janvier », pas « 1 janvier » + return dateLocale === 'fr-FR' && day === 1 ? text.replace(/^1 /, '1er ') : text } -function eventLine(event) { - return [formatDate(event?.date), event?.place].filter(Boolean).join(', ') +// « Né le 24 août 1898 à Monségur (40) », accordé au sexe quand la source le donne +const VERBS = { + birth: { M: (when) => t('Né {when}', { when }), F: (when) => t('Née {when}', { when }), null: (when) => t('Né(e) {when}', { when }) }, + death: { M: (when) => t('Mort {when}', { when }), F: (when) => t('Morte {when}', { when }), null: (when) => t('Mort(e) {when}', { when }) }, +} + +function eventSentence(kind, event, sex) { + if (!event?.date && !event?.place) return null + const parts = [] + if (event.date) parts.push(event.date.length === 10 ? t('le {date}', { date: formatDate(event.date) }) : t('en {date}', { date: formatDate(event.date) })) + if (event.place) parts.push(t('à {place}', { place: event.place })) + return (VERBS[kind][sex] || VERBS[kind].null)(parts.join(' ')) +} + +// « 1898 – 1987, 89 ans au décès » ; l'âge seulement quand les deux dates sont complètes +function lifespan(result) { + const birth = result.birth?.date + const death = result.death?.date + // Une seule date : la phrase « Né le … » la donne déjà + if (!birth || !death) return '' + const years = `${birth.slice(0, 4)} – ${death.slice(0, 4)}` + if (birth.length !== 10 || death.length !== 10) return years + const age = Number(death.slice(0, 4)) - Number(birth.slice(0, 4)) - (death.slice(5) < birth.slice(5) ? 1 : 0) + return `${years}, ${t('{age} ans au décès', { age })}` +} + +// Pourquoi « Correspond bien » ou « À comparer » : l'année et le lieu cherchés, face à ceux du résultat +function matchReason(result, query) { + const reasons = [] + const year = Number(result.birth?.date?.slice(0, 4)) || null + if (query.birthYear) { + if (!year) reasons.push(t("pas d'année de naissance")) + else reasons.push(Math.abs(year - query.birthYear) <= 1 ? t('même année') : t('autre année')) + } + if (query.place) { + if (!result.birth?.place) reasons.push(t('pas de lieu de naissance')) + else reasons.push(plain(result.birth.place).startsWith(plain(query.place)) ? t('même lieu') : t('autre lieu de naissance')) + } + return reasons.length ? reasons.join(', ') : t('seuls le nom et le prénom sont comparés') } function personYears(person) { @@ -65,6 +106,9 @@ function SearchSourcesPanel({ visible, treeId, token, persons = [], canApply = f const [region, setRegion] = useState('fr') const [picker, setPicker] = useState(null) const [applied, setApplied] = useState({}) + // Après une recherche, le formulaire se replie en une ligne : les résultats passent devant + const [searched, setSearched] = useState(null) + const [editing, setEditing] = useState(true) useEffect(() => { if (!visible || !treeId || !token || loadedSources.treeId === treeId) return @@ -100,6 +144,8 @@ function SearchSourcesPanel({ visible, treeId, token, persons = [], canApply = f event.preventDefault() if (!canSearch) return setSearching(true) + setSearched(query) + setEditing(false) setPicker(null) setResults(Object.fromEntries(checked.map((id) => [id, { status: 'loading', items: [] }]))) await Promise.all(checked.map(async (id) => { @@ -168,6 +214,18 @@ function SearchSourcesPanel({ visible, treeId, token, persons = [], canApply = f
{searched.firstName} {searched.lastName}
+ {(searched.birthYear || searched.place) && ( +{[searched.birthYear && t('né(e) vers {year}', { year: searched.birthYear }), searched.place].filter(Boolean).join(', ')}
+ )} +