From 4c80dea44a067911c188459eb01938fcb53d272e Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Thu, 12 Nov 2026 12:51:56 +0100 Subject: [PATCH] =?UTF-8?q?Nettoyage=20frontend=20:=20styles=20et=20foncti?= =?UTF-8?q?ons=20sans=20appelant,=20drapeau=20=3Flayout=3Dsugiyama,=20en-t?= =?UTF-8?q?=C3=AAte=20d'autorisation=20partag=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CSS : règles des anciens écrans (assistant de création, gestionnaire de souvenirs, réglages, lecteur vidéo), keyframes et variable sans lecteur - Fonctions sans appelant : mockData (3), treeApi (stats, audit), getMediaToken, drawPathLabel, NavButton - Fichiers-barils navbar/ et search/ remplacés par des imports directs - Galaxy : le placement par blocs est le seul chemin - api/client.js : bearer(token) remplace 59 en-têtes écrits à la main ; import dynamique inutile de client.js retiré Co-Authored-By: Claude Fable 5.2 --- frontend/src/App.css | 587 ------------------ frontend/src/App.jsx | 4 +- frontend/src/api/authApi.js | 26 +- frontend/src/api/client.js | 5 +- frontend/src/api/immichApi.js | 8 +- frontend/src/api/managersApi.js | 15 +- frontend/src/api/searchSourcesApi.js | 6 +- frontend/src/api/treeApi.js | 208 ++----- frontend/src/components/Galaxy.jsx | 16 +- frontend/src/components/MediaViewer.css | 50 -- frontend/src/components/galaxy/renderers.js | 42 +- .../components/navbar/ContextualNavbar.css | 9 - frontend/src/components/navbar/NavButton.css | 41 -- frontend/src/components/navbar/NavButton.jsx | 44 -- frontend/src/components/navbar/index.js | 5 - frontend/src/components/search/index.js | 3 - frontend/src/data/mockData.js | 34 - frontend/src/hooks/useUndoable.js | 2 +- frontend/src/index.css | 10 +- frontend/src/styles/pz.css | 22 +- frontend/src/utils/geoMedia.js | 8 +- frontend/src/utils/ghosts.js | 2 +- frontend/src/utils/kinship/facts.js | 2 +- frontend/src/utils/mediaToken.js | 4 - frontend/src/utils/mediaUpload.js | 2 +- frontend/src/utils/portraitCrop.js | 4 +- frontend/src/utils/sheetDrag.js | 6 +- frontend/src/utils/zipReader.js | 4 +- 28 files changed, 100 insertions(+), 1069 deletions(-) delete mode 100644 frontend/src/components/navbar/NavButton.css delete mode 100644 frontend/src/components/navbar/NavButton.jsx delete mode 100644 frontend/src/components/navbar/index.js delete mode 100644 frontend/src/components/search/index.js diff --git a/frontend/src/App.css b/frontend/src/App.css index ba1ac67..f1edc44 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -21,258 +21,10 @@ place-items: center; } -.account-dashboard-card { - width: min(720px, 100%); - max-height: calc(100vh - 40px); - overflow: auto; - display: grid; - gap: 12px; - padding: 24px; - border-radius: 18px; - border: 1px solid #ebe6df; - background: rgba(255, 255, 255, 0.82); - box-shadow: 0 16px 38px rgba(42, 38, 34, 0.14); -} - -.account-dashboard-card h1 { - font-size: 28px; - color: #2a2622; -} - -.account-dashboard-card h2 { - font-size: 20px; - color: #4f4841; -} - -.account-dashboard-card p { - color: #4f4841; -} - -.account-form, -.create-tree-form { - display: grid; - gap: 8px; - background: rgba(255, 255, 255, 0.65); - border: 1px solid #ebe6df; - border-radius: 12px; - padding: 14px; -} - -.account-form label, -.create-tree-form label { - font-size: 14px; - color: #4f4841; -} - -.account-form input, -.create-tree-form input { - width: 100%; - border-radius: 10px; - border: 1px solid #e6e1da; - background: #ffffff; - color: #2a2622; - padding: 10px 12px; - outline: none; -} - -.account-form input:focus, -.create-tree-form input:focus { - border-color: #d2694a; - box-shadow: 0 0 0 2px rgba(210, 105, 74, 0.15); -} - -.account-form button, -.create-tree-form button { - border: 0; - border-radius: 10px; - padding: 11px 14px; - font-weight: 450; - color: #ffffff; - background: #2a2622; - cursor: pointer; - margin-top: 4px; -} - -.account-form button:disabled, -.create-tree-form button:disabled { - opacity: 0.65; - cursor: not-allowed; -} - .tree-name { font-weight: 700; } -.tree-meta { - font-size: 14px; - color: #4f4841; -} - -.wizard-overlay { - position: absolute; - inset: 0; - z-index: 30; - display: grid; - place-items: center; - padding: 20px; - background: rgba(34, 33, 31, 0.34); -} - -.wizard-card { - width: min(460px, 100%); - max-height: calc(100vh - 40px); - overflow: auto; - display: grid; - gap: 10px; - border-radius: 18px; - border: 1px solid #f4d6c9; - background: #ffffff; - padding: 22px 20px 18px; - box-shadow: 0 24px 45px rgba(36, 33, 30, 0.34); -} - -/* ── Sections personnes ── */ -.wizard-persons { - display: grid; - gap: 8px; -} - -.wizard-person { - display: grid; - gap: 7px; - border: 1px solid #edb6a4; - border-radius: 12px; - background: rgba(251, 250, 249, 0.9); - padding: 11px 12px; -} - -.wizard-person-label { - font-size: 11px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - color: #8a847d; -} - -.wizard-person-label--self { - color: #2a2622; -} - -.wizard-person-fields { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 7px; -} - -.wizard-card input { - width: 100%; - border-radius: 9px; - border: 1px solid #e6e1da; - background: #ffffff; - color: #2a2622; - padding: 9px 11px; - font-size: 14px; - outline: none; - box-sizing: border-box; -} - -.wizard-card input::placeholder { - color: #a7a29c; -} - -.wizard-card input:focus { - border-color: #d2694a; - box-shadow: 0 0 0 2px rgba(210, 105, 74, 0.15); -} - -.wizard-year { - grid-column: 1 / -1; - font-size: 13px !important; -} - -.wizard-card button { - border: 0; - border-radius: 10px; - padding: 11px 14px; - font-size: 14px; - font-weight: 500; - color: #ffffff; - background: #2a2622; - cursor: pointer; - margin-top: 4px; - transition: filter 0.15s; -} - -.wizard-card button:not(:disabled):hover { - filter: brightness(1.08); -} - -.wizard-card button:disabled { - opacity: 0.55; - cursor: not-allowed; -} - -.wizard-card .ghost { - margin-top: 0; - background: transparent; - color: #5f5a54; - border: 1px solid #efc7b8; - font-weight: 450; -} - -.wizard-error { - background: rgba(171, 49, 49, 0.1); - border: 1px solid rgba(171, 49, 49, 0.32); - color: #862f2f; - border-radius: 10px; - padding: 10px 12px; - font-size: 14px; -} - -/* ============================================================ - Bannière de bienvenue — onboarding premier utilisateur - ============================================================ */ -.demo-welcome { - position: fixed; - top: 20px; - left: 20px; - z-index: 800; - max-width: 260px; - padding: 14px 16px; - background: rgba(242, 239, 234, 0.93); - backdrop-filter: blur(16px); - -webkit-backdrop-filter: blur(16px); - border-radius: 16px; - border: 1px solid rgba(42, 38, 34, 0.1); - box-shadow: - 0 4px 24px rgba(42, 38, 34, 0.14), - 0 1px 4px rgba(42, 38, 34, 0.06); - font-family: 'DM Sans', system-ui, sans-serif; - font-size: 13px; - color: #2A2622; - line-height: 1.55; - pointer-events: none; - animation: demoWelcomeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1) 0.3s both; -} - -@keyframes demoWelcomeIn { - from { - opacity: 0; - transform: translateY(-8px) scale(0.96); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -.demo-welcome strong { - color: #2a2622; - display: block; - margin-bottom: 5px; - font-size: 14px; -} - /* ============================================================ Nœud vide arbre — premier arbre sans personnes ============================================================ */ @@ -349,190 +101,6 @@ margin-left: 2px; } -.media-manager-overlay { - position: absolute; - inset: 0; - z-index: 32; - display: grid; - place-items: center; - padding: 20px; - background: rgba(34, 33, 31, 0.3); -} - -.media-manager-card { - width: min(760px, 100%); - max-height: calc(100vh - 40px); - overflow: auto; - display: grid; - gap: 10px; - border-radius: 16px; - border: 1px solid #f4d6c9; - background: #ffffff; - padding: 20px; - box-shadow: 0 24px 45px rgba(36, 33, 30, 0.34); -} - -.media-manager-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 10px; -} - -.media-manager-header h1 { - font-size: 24px; - color: #2a2622; -} - -.media-manager-form { - display: grid; - gap: 8px; - border: 1px solid #ebe6df; - border-radius: 12px; - padding: 12px; - background: rgba(255, 255, 255, 0.7); -} - -.media-manager-form label { - font-size: 14px; - color: #4f4841; -} - -.media-manager-form input, -.media-manager-form select { - width: 100%; - border-radius: 10px; - border: 1px solid #e6e1da; - background: #ffffff; - color: #2a2622; - padding: 10px 12px; - outline: none; -} - -.media-manager-form button, -.media-manager-actions button { - border: 0; - border-radius: 10px; - padding: 10px 12px; - font-weight: 450; - color: #ffffff; - background: #2a2622; - cursor: pointer; -} - -.media-manager-card .ghost { - border: 1px solid #efc7b8; - border-radius: 10px; - padding: 10px 12px; - background: transparent; - color: #2a2622; - font-weight: 450; - cursor: pointer; -} - -.media-manager-list { - display: grid; - gap: 8px; -} - -.media-manager-item { - display: grid; - grid-template-columns: 74px 1fr auto; - align-items: center; - gap: 10px; - border: 1px solid #f4d6c9; - background: #ffffff; - border-radius: 10px; - padding: 8px; -} - -.media-manager-preview { - width: 74px; - height: 74px; - border-radius: 8px; - background: #f4f2ee; - overflow: hidden; - display: grid; - place-items: center; - color: #4f4841; - font-size: 14px; -} - -.media-manager-preview img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.media-manager-meta { - display: grid; - gap: 2px; - color: #4f4841; -} - -.media-manager-meta span { - color: #6b645d; - font-size: 14px; -} - -.media-manager-actions { - display: flex; - gap: 6px; -} - -.media-manager-empty { - color: #4f4841; - background: rgba(255, 255, 255, 0.72); - border: 1px dashed #f4d6c9; - border-radius: 10px; - padding: 12px; -} - -.media-manager-error { - background: rgba(171, 49, 49, 0.1); - border: 1px solid rgba(171, 49, 49, 0.32); - color: #862f2f; - border-radius: 10px; - padding: 10px 12px; - font-size: 14px; -} - -/* ── Contribution change cards ── */ - -/* champs modifiés */ - -/* Boutons décision dans le header de carte */ - -/* Aperçu annotations */ - -/* résumé décisions dans la meta */ - -/* footer session */ - -/* Groupement par type d'entité */ - -/* Confirmation "Tout approuver" inline */ - -@media (max-width: 700px) { - - .media-manager-item { - grid-template-columns: 1fr; - } - - .media-manager-actions { - width: 100%; - } - - .media-manager-actions button { - flex: 1; - } -} - -.account-dashboard-card.auth-only { - width: min(480px, 100%); - text-align: center; -} - .tree-created-prompt { position: fixed; top: 20px; @@ -563,123 +131,6 @@ font-size: 14px; } -.settings-card { - width: min(900px, 100%); -} - -.settings-tabs { - display: flex; - flex-wrap: wrap; - gap: 8px; -} - -.settings-tabs button { - border: 1px solid #efc7b8; - border-radius: 999px; - padding: 8px 12px; - background: transparent; - color: #2a2622; - cursor: pointer; -} - -.settings-tabs button.active { - color: #ffffff; - border-color: #2a2622; - background: #2a2622; -} - -.settings-share-card { - display: grid; - gap: 8px; - border: 1px solid #ebe6df; - border-radius: 12px; - padding: 12px; - background: rgba(255, 255, 255, 0.65); -} - -.settings-share-card h3 { - color: #4f4841; - font-size: 16px; -} - -.settings-share-link-row { - display: flex; - gap: 8px; - align-items: center; -} - -.settings-share-link-row input { - flex: 1; - min-width: 0; -} - -.settings-password-row { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; -} - -.settings-password-row code { - border: 1px dashed #f4d6c9; - border-radius: 8px; - background: #ffffff; - padding: 6px 10px; - color: #4f4841; -} - -.settings-hint { - color: #4f4841; - font-size: 14px; - border-left: 3px solid #f4d6c9; - padding-left: 10px; -} - -.settings-data-row { - display: flex; - justify-content: space-between; - gap: 12px; - border: 1px solid #ebe6df; - border-radius: 10px; - padding: 12px; - align-items: center; -} - -.settings-data-row p { - color: #6B645D; - font-size: 14px; - margin-top: 4px; -} - -.settings-data-row--muted { - opacity: 0.72; -} - -.settings-advanced-toggle { - justify-self: start; -} - -.settings-danger-box { - display: grid; - gap: 8px; - border: 1px solid rgba(171, 49, 49, 0.34); - border-radius: 12px; - background: rgba(171, 49, 49, 0.06); - padding: 12px; -} - -@media (max-width: 700px) { - .settings-share-link-row { - flex-direction: column; - align-items: stretch; - } - - .settings-data-row { - flex-direction: column; - align-items: stretch; - } -} - .account-overlay--open { visibility: visible; pointer-events: auto; @@ -697,23 +148,6 @@ color: var(--ink-2, #6B645D); } -.account-hint { - margin: -6px 0 10px; - font-size: 12px; - color: var(--ink-2, #6B645D); -} - -/* Paramètres : contributions de la famille */ - -/* Écran compte : suppression du compte */ -.account-delete-zone { - display: flex; - justify-content: flex-end; - margin-top: 6px; - padding-top: 10px; - border-top: 1px solid #f8e4db; -} - .account-delete-link { border: 0; background: transparent; @@ -733,27 +167,6 @@ cursor: not-allowed; } -.account-delete-list { - display: grid; - gap: 6px; - margin: 0; - padding-left: 18px; - list-style: disc; - color: #4f4841; - font-size: 14px; - line-height: 1.45; -} - -.account-delete-form { - display: grid; - gap: 8px; -} - -.account-delete-form label { - font-size: 14px; - color: #4f4841; -} - .account-delete-modal .ghost { border: 1px solid #efc7b8; border-radius: 10px; diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 86d5a82..49c03ae 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -24,9 +24,9 @@ import DraftRestoreBanner from './components/DraftRestoreBanner' import AddPersonPanel from './components/AddPersonPanel' import SearchSourcesPanel from './components/SearchSourcesPanel.jsx' import ContributionSessionModal from './components/ContributionSessionModal' -import { ContextualNavbar } from './components/navbar' +import ContextualNavbar from './components/navbar/ContextualNavbar' import ZoomControl from './components/ZoomControl' -import { ExpandableSearch } from './components/search' +import ExpandableSearch from './components/search/ExpandableSearch' import { t, dateLocale } from './i18n/index.js' import { createParentChildLink, diff --git a/frontend/src/api/authApi.js b/frontend/src/api/authApi.js index db6e02f..54ca5e9 100644 --- a/frontend/src/api/authApi.js +++ b/frontend/src/api/authApi.js @@ -1,4 +1,4 @@ -import { API_BASE_URL, apiRequest } from './client' +import { API_BASE_URL, apiRequest, bearer } from './client' export async function loginUser(email, password) { const data = await apiRequest('/auth/login', { @@ -22,9 +22,7 @@ export async function registerUser(firstName, email, password, invitationToken) export async function logoutUser(token) { const data = await apiRequest('/auth/logout', { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data @@ -34,9 +32,7 @@ export async function logoutUser(token) { export async function deleteAccount(token, confirmation) { const data = await apiRequest('/auth/me', { method: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(confirmation), }) @@ -47,9 +43,7 @@ export async function deleteAccount(token, confirmation) { export async function fetchAccountProfile(token) { const data = await apiRequest('/auth/me', { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data @@ -58,9 +52,7 @@ export async function fetchAccountProfile(token) { export async function fetchLastOpenedTree(token) { const data = await apiRequest('/auth/me/last-tree', { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data?.lastTree || null @@ -69,9 +61,7 @@ export async function fetchLastOpenedTree(token) { export async function updateLastOpenedTree(token, payload) { const data = await apiRequest('/auth/me/last-tree', { method: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -92,9 +82,7 @@ export function oidcStartUrl(returnTo = '/') { export async function requestOidcLinkTicket(token) { const data = await apiRequest('/auth/oidc/link', { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data?.ticket diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index b6e6efb..32dc575 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -104,4 +104,7 @@ async function apiRequest(path, options = {}) { return payload } -export { ApiError, API_BASE_URL, apiRequest } +// En-tête d'un appel fait au nom d'un compte ou d'un accès partagé +const bearer = (token) => ({ Authorization: `Bearer ${token}` }) + +export { ApiError, API_BASE_URL, apiRequest, bearer } diff --git a/frontend/src/api/immichApi.js b/frontend/src/api/immichApi.js index 3c01293..dc3691e 100644 --- a/frontend/src/api/immichApi.js +++ b/frontend/src/api/immichApi.js @@ -1,4 +1,4 @@ -import { API_BASE_URL, apiRequest } from './client.js' +import { API_BASE_URL, apiRequest, bearer } from './client.js' // Reconnaissance des visages : la connexion Immich est propre à chaque arbre, configurée par // ses administrateurs. L'adresse est choisie parmi celles autorisées par l'opérateur du @@ -7,7 +7,7 @@ import { API_BASE_URL, apiRequest } from './client.js' function withAuth(options = {}, token) { if (!token) return options - return { ...options, headers: { ...(options.headers || {}), Authorization: `Bearer ${token}` } } + return { ...options, headers: { ...(options.headers || {}), ...bearer(token) } } } export function getImmichErrorMessage(error, t) { @@ -38,7 +38,7 @@ export function listImmichAssets(treeId, token, page = 1, type = 'IMAGE', { quer export async function fetchImmichAsset(treeId, assetId, token, variant = 'thumbnail', signal) { const response = await fetch(`${API_BASE_URL}/trees/${encodeURIComponent(treeId)}/immich/assets/${encodeURIComponent(assetId)}/${variant}`, { - headers: { Authorization: `Bearer ${token}` }, signal, + headers: bearer(token), signal, }) if (!response.ok) { const error = new Error('immich_asset_failed') @@ -106,7 +106,7 @@ export async function fetchImmichLink(treeId, personId, token) { export async function fetchImmichThumbnail(treeId, immichPersonId, token) { const path = `/trees/${encodeURIComponent(treeId)}/immich/thumbnail/${encodeURIComponent(immichPersonId)}` const response = await fetch(`${API_BASE_URL}${path}`, { - headers: token ? { Authorization: `Bearer ${token}` } : {}, + headers: token ? bearer(token) : {}, }) if (!response.ok) { throw new Error(`immich_thumbnail_failed_${response.status}`) diff --git a/frontend/src/api/managersApi.js b/frontend/src/api/managersApi.js index 79fad1a..aa4fcf7 100644 --- a/frontend/src/api/managersApi.js +++ b/frontend/src/api/managersApi.js @@ -1,29 +1,28 @@ -import { apiRequest } from './client' +import { apiRequest, bearer } from './client' // Co-gestion d'un arbre (issue #145) : gestionnaires, invitations, et la page du lien d'invitation -const auth = (token) => ({ Authorization: `Bearer ${token}` }) const tree = (treeId) => `/trees/${encodeURIComponent(treeId)}` export function listManagers(treeId, token) { - return apiRequest(`${tree(treeId)}/managers`, { headers: auth(token) }) + return apiRequest(`${tree(treeId)}/managers`, { headers: bearer(token) }) } export function inviteManager(treeId, email, token) { - return apiRequest(`${tree(treeId)}/invitations`, { method: 'POST', headers: auth(token), body: JSON.stringify({ email }) }) + return apiRequest(`${tree(treeId)}/invitations`, { method: 'POST', headers: bearer(token), body: JSON.stringify({ email }) }) } export function renewInvitationLink(treeId, invitationId, token) { - return apiRequest(`${tree(treeId)}/invitations/${encodeURIComponent(invitationId)}/link`, { method: 'POST', headers: auth(token) }) + return apiRequest(`${tree(treeId)}/invitations/${encodeURIComponent(invitationId)}/link`, { method: 'POST', headers: bearer(token) }) } export function revokeInvitation(treeId, invitationId, token) { - return apiRequest(`${tree(treeId)}/invitations/${encodeURIComponent(invitationId)}`, { method: 'DELETE', headers: auth(token) }) + return apiRequest(`${tree(treeId)}/invitations/${encodeURIComponent(invitationId)}`, { method: 'DELETE', headers: bearer(token) }) } // Retirer un co-gestionnaire (le propriétaire), ou quitter la gestion (son propre identifiant) export function removeManager(treeId, userId, token) { - return apiRequest(`${tree(treeId)}/managers/${encodeURIComponent(userId)}`, { method: 'DELETE', headers: auth(token) }) + return apiRequest(`${tree(treeId)}/managers/${encodeURIComponent(userId)}`, { method: 'DELETE', headers: bearer(token) }) } // Sous /api comme /api/arbre/:slug : nginx ne relaie à l'API que /api (préfixe retiré), /auth et /trees @@ -32,7 +31,7 @@ export function fetchInvitation(invitationToken) { } export function acceptInvitation(invitationToken, token) { - return apiRequest(`/api/invitations/${encodeURIComponent(invitationToken)}/accept`, { method: 'POST', headers: auth(token) }) + return apiRequest(`/api/invitations/${encodeURIComponent(invitationToken)}/accept`, { method: 'POST', headers: bearer(token) }) } export function invitationUrl(invitationToken) { diff --git a/frontend/src/api/searchSourcesApi.js b/frontend/src/api/searchSourcesApi.js index b056be6..61edaf8 100644 --- a/frontend/src/api/searchSourcesApi.js +++ b/frontend/src/api/searchSourcesApi.js @@ -1,9 +1,9 @@ -import { apiRequest } from './client' +import { apiRequest, bearer } from './client' // Sources que ce serveur interroge (SEARCH_SOURCES) : une liste d'identifiants, vide par défaut export async function fetchSearchSources(treeId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/search-sources`, { - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), }) return Array.isArray(data?.sources) ? data.sources : [] } @@ -11,7 +11,7 @@ export async function fetchSearchSources(treeId, token) { export async function searchSource(treeId, sourceId, query, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/search-sources/${encodeURIComponent(sourceId)}`, { method: 'POST', - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), body: JSON.stringify(query), }) return Array.isArray(data?.results) ? data.results : [] diff --git a/frontend/src/api/treeApi.js b/frontend/src/api/treeApi.js index 8a83ac8..45c640a 100644 --- a/frontend/src/api/treeApi.js +++ b/frontend/src/api/treeApi.js @@ -1,4 +1,4 @@ -import { apiRequest } from './client' +import { API_BASE_URL, apiRequest, bearer } from './client' import { readContributorName } from '../utils/contributorName.js' export async function resolveTreeBySlug(slug) { @@ -12,14 +12,9 @@ export async function resolveTreeBySlug(slug) { } export async function unlockTreeAccess(treeId, password, userToken = '') { - const headers = {} - if (userToken) { - headers.Authorization = `Bearer ${userToken}` - } - const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/access/unlock`, { method: 'POST', - headers, + headers: userToken ? bearer(userToken) : {}, body: JSON.stringify({ password }), }) @@ -30,9 +25,7 @@ export async function unlockTreeAccess(treeId, password, userToken = '') { export async function linkTreeAccess(treeId, accessToken, userToken) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/access/link`, { method: 'POST', - headers: { - Authorization: `Bearer ${userToken}`, - }, + headers: bearer(userToken), body: JSON.stringify({ accessToken }), }) @@ -46,7 +39,7 @@ export async function fetchTreeGraph(treeId, token) { method: 'GET', cache: 'no-store', headers: { - Authorization: `Bearer ${token}`, + ...bearer(token), ...(visitorName ? { 'X-Visitor-Name': encodeURIComponent(visitorName) } : {}), }, }) @@ -63,7 +56,7 @@ export async function fetchTreeGraph(treeId, token) { export async function fetchPersonLife(treeId, personId, token) { return apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/life`, { method: 'GET', - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), }) } @@ -72,7 +65,7 @@ export async function createLifeEvent(treeId, personId, payload, token) { return apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/life/events`, { method: 'POST', idempotent: true, - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), body: JSON.stringify(payload), }) } @@ -80,7 +73,7 @@ export async function createLifeEvent(treeId, personId, payload, token) { export async function updateLifeEvent(treeId, eventId, payload, token) { return apiRequest(`/trees/${encodeURIComponent(treeId)}/life/events/${encodeURIComponent(eventId)}`, { method: 'PATCH', - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), body: JSON.stringify(payload), }) } @@ -89,16 +82,14 @@ export async function deleteLifeEvent(treeId, eventId, token, { keepalive = fals return apiRequest(`/trees/${encodeURIComponent(treeId)}/life/events/${encodeURIComponent(eventId)}`, { method: 'DELETE', keepalive, - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), }) } export async function listTrees(token) { const data = await apiRequest('/trees', { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return { @@ -110,9 +101,7 @@ export async function listTrees(token) { export async function getTree(treeId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}`, { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return { @@ -124,9 +113,7 @@ export async function getTree(treeId, token) { export async function createTree(payload, token) { const data = await apiRequest('/trees', { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -136,9 +123,7 @@ export async function createTree(payload, token) { export async function updateTree(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -148,9 +133,7 @@ export async function updateTree(treeId, payload, token) { export async function deleteTree(treeId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}`, { method: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data @@ -159,9 +142,7 @@ export async function deleteTree(treeId, token) { export async function createPerson(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons`, { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -171,9 +152,7 @@ export async function createPerson(treeId, payload, token) { export async function updatePerson(treeId, personId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -184,9 +163,7 @@ export async function deletePerson(treeId, personId, token, { keepalive = false const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}`, { method: 'DELETE', keepalive, - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data @@ -195,9 +172,7 @@ export async function deletePerson(treeId, personId, token, { keepalive = false export async function createUnion(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/unions`, { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -207,9 +182,7 @@ export async function createUnion(treeId, payload, token) { export async function createParentChildLink(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/links`, { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -220,9 +193,7 @@ export async function createParentChildLink(treeId, payload, token) { export async function updateUnion(treeId, unionId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/unions/${encodeURIComponent(unionId)}`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -232,9 +203,7 @@ export async function updateUnion(treeId, unionId, payload, token) { export async function deleteUnion(treeId, unionId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/unions/${encodeURIComponent(unionId)}`, { method: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data @@ -244,9 +213,7 @@ export async function deleteUnion(treeId, unionId, token) { export async function reorderSiblings(treeId, order, token) { return apiRequest(`/trees/${encodeURIComponent(treeId)}/links/order`, { method: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify({ order }), }) } @@ -255,9 +222,7 @@ export async function reorderSiblings(treeId, order, token) { export async function updateParentChildLink(treeId, linkId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/links/${encodeURIComponent(linkId)}`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -267,9 +232,7 @@ export async function updateParentChildLink(treeId, linkId, payload, token) { export async function deleteParentChildLink(treeId, linkId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/links/${encodeURIComponent(linkId)}`, { method: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data @@ -278,9 +241,7 @@ export async function deleteParentChildLink(treeId, linkId, token) { export async function rotateTreePasswords(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/access/passwords`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -290,39 +251,22 @@ export async function rotateTreePasswords(treeId, payload, token) { export async function fetchSharePassword(treeId, token) { return apiRequest(`/trees/${encodeURIComponent(treeId)}/access/passwords`, { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) } export async function rememberSharePassword(treeId, password, token) { return apiRequest(`/trees/${encodeURIComponent(treeId)}/access/passwords/remember`, { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify({ password }), }) } -export async function fetchTreeStats(treeId, token) { - const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/stats`, { - method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return data?.stats || null -} - export async function fetchTreeSettings(treeId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/settings`, { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data?.settings || null @@ -331,51 +275,18 @@ export async function fetchTreeSettings(treeId, token) { export async function updateTreeSettings(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/settings`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) return data?.settings || null } -export async function listTreeAuditLogs(treeId, token, options = {}) { - const queryParams = new URLSearchParams() - - if (options.limit) { - queryParams.set('limit', String(options.limit)) - } - - if (options.offset !== undefined && options.offset !== null) { - queryParams.set('offset', String(options.offset)) - } - - if (options.action) { - queryParams.set('action', options.action) - } - - const query = queryParams.toString() - const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/audit${query ? `?${query}` : ''}`, { - method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - return { - logs: Array.isArray(data?.logs) ? data.logs : [], - nextOffset: typeof data?.nextOffset === 'number' ? data.nextOffset : null, - } -} - // Journal des visites (administrateurs de l'arbre) export async function listTreeVisits(treeId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/visits`, { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return { @@ -388,9 +299,7 @@ export async function submitContributionSession(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/contributions/sessions`, { method: 'POST', idempotent: true, - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -401,9 +310,7 @@ export async function listContributionSessions(treeId, token, status = 'pending' const query = status ? `?status=${encodeURIComponent(status)}` : '' const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/contributions/sessions${query}`, { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return Array.isArray(data?.sessions) ? data.sessions : [] @@ -412,9 +319,7 @@ export async function listContributionSessions(treeId, token, status = 'pending' export async function reviewContributionSession(treeId, sessionId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/contributions/sessions/${encodeURIComponent(sessionId)}/review`, { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -425,9 +330,7 @@ export async function reviewContributionSession(treeId, sessionId, payload, toke export async function fetchContributionStatus(treeId, sessionId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/contributions/sessions/${encodeURIComponent(sessionId)}/status`, { method: 'GET', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data?.status || null @@ -437,9 +340,7 @@ export async function uploadAnnotationPhoto(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/annotation-photos`, { method: 'POST', idempotent: true, - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -450,9 +351,7 @@ export async function uploadPersonMedia(treeId, personId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/media`, { method: 'POST', idempotent: true, - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -462,9 +361,7 @@ export async function uploadPersonMedia(treeId, personId, payload, token) { export async function reorderPersonMedia(treeId, personId, orderedMediaIds, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/media/reorder`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify({ orderedMediaIds }), }) @@ -477,9 +374,7 @@ export async function deletePersonMedia(treeId, personId, mediaId, token, { keep { method: 'DELETE', keepalive, - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }, ) @@ -492,9 +387,7 @@ export async function updatePersonMedia(treeId, personId, mediaId, payload, toke `/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/media/${encodeURIComponent(mediaId)}`, { method: 'PATCH', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }, ) @@ -509,9 +402,7 @@ export async function updateMediaPeople(treeId, mediaId, payload, token) { `/trees/${encodeURIComponent(treeId)}/media/${encodeURIComponent(mediaId)}/people`, { method: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }, ) @@ -520,9 +411,7 @@ export async function updateMediaPeople(treeId, mediaId, payload, token) { export async function uploadPersonAvatar(treeId, personId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/avatar`, { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -533,9 +422,7 @@ export async function uploadPersonAvatar(treeId, personId, payload, token) { export async function saveAvatarCrop(treeId, personId, crop, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/avatar/crop`, { method: 'PUT', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify({ crop }), }) @@ -545,9 +432,7 @@ export async function saveAvatarCrop(treeId, personId, crop, token) { export async function deletePersonAvatar(treeId, personId, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/persons/${encodeURIComponent(personId)}/avatar`, { method: 'DELETE', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), }) return data @@ -556,9 +441,7 @@ export async function deletePersonAvatar(treeId, personId, token) { export async function batchAnnotations(treeId, payload, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/annotations/batch`, { method: 'POST', - headers: { - Authorization: `Bearer ${token}`, - }, + headers: bearer(token), body: JSON.stringify(payload), }) @@ -566,9 +449,8 @@ export async function batchAnnotations(treeId, payload, token) { } export async function exportGedcom(treeId, token) { - const { API_BASE_URL } = await import('./client') const resp = await fetch(`${API_BASE_URL}/trees/${encodeURIComponent(treeId)}/export/gedcom`, { - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), }) if (!resp.ok) { throw new Error('export_failed') @@ -583,7 +465,7 @@ export async function exportGedcom(treeId, token) { export async function importGedcom(treeId, fileContent, token) { const data = await apiRequest(`/trees/${encodeURIComponent(treeId)}/import/gedcom`, { method: 'POST', - headers: { Authorization: `Bearer ${token}` }, + headers: bearer(token), body: JSON.stringify({ content: fileContent }), // Un gros arbre se crée en une transaction d'une minute au plus côté serveur timeoutMs: 3 * 60_000, diff --git a/frontend/src/components/Galaxy.jsx b/frontend/src/components/Galaxy.jsx index 38415b7..b06f18a 100644 --- a/frontend/src/components/Galaxy.jsx +++ b/frontend/src/components/Galaxy.jsx @@ -2,12 +2,10 @@ import { memo, useEffect, useImperativeHandle, useRef, useState, useCallback } f import { getPersonMedias, getParents, getChildren, getPersonUnions, persons as graphPersons, unions as graphUnions, filiations as graphFiliations } from '../data/mockData' import { PERSON_R, FLOAT_SPEED, FLOAT_AMPLITUDE, REPULSION_RADIUS, REPULSION_STRENGTH, MAX_ORBIT_MEDIAS, ENTRANCE_NODE_DURATION, ENTRANCE_CAMERA_DURATION, ENTRANCE_CAMERA_FROM_SCALE, ENTRANCE_REDUCED_DURATION, ENTRANCE_LINE_DELAY, ENTRANCE_LINE_DURATION, ENTRANCE_ORBIT_DELAY, ENTRANCE_ORBIT_DURATION, ENTRANCE_ORBIT_STAGGER } from './galaxy/constants' import { easeOutCubic } from './galaxy/utils' -import { computeLayout, generateNodeRandomData, coupleBarMeta, restoreCoupleBarMeta } from './galaxy/elkLayout' +import { generateNodeRandomData, coupleBarMeta, restoreCoupleBarMeta } from './galaxy/elkLayout' import { computeBlockLayout, familyTabsOf, lineToFamily, subgraphOf, takeRootLayout } from './galaxy/blockLayout' import { hasGhosts, withGhosts } from '../utils/ghosts' -// Placement par blocs familiaux par défaut (mesures : tools/layout-bench). ?layout=sugiyama = placement Sugiyama. -const USE_BLOCK_LAYOUT = typeof window === 'undefined' || new URLSearchParams(window.location.search).get('layout') !== 'sugiyama' import { findPathBFS } from './galaxy/pathfinding' import { drawBackground, drawCoupleLinks, drawFiliations, drawAnnotations, drawPathHighlight, drawNodes, drawRenvoiPills, nodeRadius } from './galaxy/renderers' import { buildSiblingGroups, moveInOrder, pickSlot, rubberBand, springStep, isSettled, buildOrderPayload, filiationsWithOrder } from './galaxy/siblingReorder' @@ -277,7 +275,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig }, []) const canReorderNow = useCallback(() => ( - reorderEnabledRef.current && USE_BLOCK_LAYOUT && Boolean(layoutDataRef.current) && Boolean(entranceRef.current?.finished) + reorderEnabledRef.current && Boolean(layoutDataRef.current) && Boolean(entranceRef.current?.finished) ), []) const worldX = useCallback((clientX) => { @@ -458,7 +456,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig }, [canReorderNow, getSiblingGroups, swapLayout, layoutForOrder, commitOrder]) const getSiblingPlace = useCallback((personId) => { - if (!USE_BLOCK_LAYOUT || !layoutDataRef.current) return null + if (!layoutDataRef.current) return null const group = getSiblingGroups().get(`p-${personId}`) return group ? { index: group.ids.indexOf(`p-${personId}`), count: group.ids.length } : null }, [getSiblingGroups]) @@ -640,7 +638,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig let freshTabs = null // Liste en attente du worker : l'arbre affiché est celui de la famille de départ let tabsPending = false - if (USE_BLOCK_LAYOUT && familyTabs && (cache.rev !== graphRevision || cache.start !== start)) { + if (familyTabs && (cache.rev !== graphRevision || cache.start !== start)) { const pending = tabsRequestRef.current // Dans un onglet d'une autre famille, ou à l'arrivée par le lien d'un onglet, l'arbre affiché dépend de la // liste : elle est calculée sur place @@ -682,10 +680,10 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig // Annotations d'avant l'ancrage (x, y seuls) : rattachées une fois à la personne la plus proche dans le // placement de l'onglet de départ, puis enregistrées. Calculé avant l'arbre affiché (coupleBarMeta, voir plus haut) const legacy = annotationData.some((a) => !readAnchor(a)) - const departure = legacy && USE_BLOCK_LAYOUT && graph ? computeBlockLayout(null, startRefRef.current, { familyTabs }) : null + const departure = legacy && graph ? computeBlockLayout(null, startRefRef.current, { familyTabs }) : null // Famille de départ sans propositions : son placement vient d'être calculé avec la liste des onglets - const kept = USE_BLOCK_LAYOUT && !graph && tab === freshTabs?.[0] ? takeRootLayout(freshTabs, focusPersonIdRef.current) : null - const result = kept || (USE_BLOCK_LAYOUT ? computeBlockLayout(graph, focusPersonIdRef.current, { familyTabs }) : computeLayout(null, focusPersonIdRef.current)) + const kept = !graph && tab === freshTabs?.[0] ? takeRootLayout(freshTabs, focusPersonIdRef.current) : null + const result = kept || computeBlockLayout(graph, focusPersonIdRef.current, { familyTabs }) if (legacy) { const anchored = anchorLegacy(annotationData, layoutCenters(departure || result)) for (const ann of anchored) updateAnnotationInStore(ann.id, { style: ann.style }) diff --git a/frontend/src/components/MediaViewer.css b/frontend/src/components/MediaViewer.css index b9e7cf1..bf0cd23 100644 --- a/frontend/src/components/MediaViewer.css +++ b/frontend/src/components/MediaViewer.css @@ -90,56 +90,6 @@ overflow: hidden; } -.media-video-perfs { - position: absolute; - left: 8px; - top: 0; - bottom: 0; - display: flex; - flex-direction: column; - justify-content: space-around; - gap: 6px; - padding: 10px 0; -} - -.media-video-perfs.right { - left: auto; - right: 8px; -} - -.media-video-perf { - width: 8px; - height: 8px; - border-radius: 1px; - background: rgba(255, 255, 255, 0.06); -} - -.media-video-play { - width: 64px; - height: 64px; - border-radius: 50%; - background: rgba(255, 255, 255, 0.1); - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: background 0.2s ease, transform 0.2s ease; -} - -.media-video-play:hover { - background: rgba(255, 255, 255, 0.18); - transform: scale(1.05); -} - -.media-video-play-triangle { - width: 0; - height: 0; - border-left: 20px solid rgba(255, 255, 255, 0.7); - border-top: 12px solid transparent; - border-bottom: 12px solid transparent; - margin-left: 5px; -} - .media-video-text { color: rgba(255, 255, 255, 0.35); font-size: 14px; diff --git a/frontend/src/components/galaxy/renderers.js b/frontend/src/components/galaxy/renderers.js index c02f0ee..668c9e0 100644 --- a/frontend/src/components/galaxy/renderers.js +++ b/frontend/src/components/galaxy/renderers.js @@ -101,9 +101,9 @@ function computeCouplePoints(p1Pos, p2Pos, unionPos, p1R, p2R, nodeRandomData, s // commune à la fratrie, chaque enfant s'en détache par une courbe. // Coins arrondis de rayon fixe (accolade franche, forme A). // ============================================================ -export const FIL = { color: '#E3A590', width: 1.1, dot: '#D2694A', dotR: 2.6 } +const FIL = { color: '#E3A590', width: 1.1, dot: '#D2694A', dotR: 2.6 } // Proposé, pas encore relu (utils/ghosts.js) : pointillés terracotta, portrait délavé, étiquette au-dessus -export const GHOST = { color: '#D2694A', dash: [5, 4], wash: 'rgba(251, 250, 247, 0.55)', annotationAlpha: 0.45 } +const GHOST = { color: '#D2694A', dash: [5, 4], wash: 'rgba(251, 250, 247, 0.55)', annotationAlpha: 0.45 } const GHOST_LABELS = { draft: () => t('Brouillon'), sent: () => t('En attente'), pending: () => t('Proposé') } // Ligne de la fratrie : sous les prénoms et années des parents, au-dessus des enfants const BUS_DY = 100 @@ -1507,41 +1507,3 @@ function drawActiveDrawingPath(ctx, pathData) { ctx.stroke() ctx.restore() } - -// ============================================================ -// Badge lien de parenté -// ============================================================ -export function drawPathLabel(ctx, pathLabelRef, pathLabelAlpha, pathNodesRef, posMap) { - if (!pathLabelRef || pathLabelAlpha <= 0.02) return - - const personNodesOnPath = pathNodesRef.filter(id => id.startsWith('p-')) - if (personNodesOnPath.length < 2) return - - const midIdx = Math.floor(personNodesOnPath.length / 2) - const midPos1 = posMap.get(personNodesOnPath[midIdx - 1]) - const midPos2 = posMap.get(personNodesOnPath[midIdx]) - if (!midPos1 || !midPos2) return - - const labelX = (midPos1.cx + midPos2.cx) / 2 - const labelY = (midPos1.cy + midPos2.cy) / 2 - 20 - - ctx.save() - ctx.globalAlpha = pathLabelAlpha * 0.9 - - ctx.font = '500 11px "DM Sans", system-ui, sans-serif' - const metrics = ctx.measureText(pathLabelRef) - const pillW = metrics.width + 16 - const pillH = 22 - - ctx.fillStyle = 'rgba(42, 38, 34, 0.85)' - ctx.beginPath() - ctx.roundRect(labelX - pillW / 2, labelY - pillH / 2, pillW, pillH, pillH / 2) - ctx.fill() - - ctx.fillStyle = '#FFFFFF' - ctx.textAlign = 'center' - ctx.textBaseline = 'middle' - ctx.fillText(pathLabelRef, labelX, labelY) - - ctx.restore() -} diff --git a/frontend/src/components/navbar/ContextualNavbar.css b/frontend/src/components/navbar/ContextualNavbar.css index 96bb973..92e5fad 100644 --- a/frontend/src/components/navbar/ContextualNavbar.css +++ b/frontend/src/components/navbar/ContextualNavbar.css @@ -300,15 +300,6 @@ transition: all 0.15s cubic-bezier(0.22, 1, 0.36, 1); } -/* --- Add person button --- */ -.nav-annot-btn--add { - color: #93402A; -} - -.nav-annot-btn--add:hover:not(:disabled) { - color: #3D3731; -} - /* --- Sub-panel (options) above navbar --- */ .navbar-annot-panel { position: absolute; diff --git a/frontend/src/components/navbar/NavButton.css b/frontend/src/components/navbar/NavButton.css deleted file mode 100644 index d794262..0000000 --- a/frontend/src/components/navbar/NavButton.css +++ /dev/null @@ -1,41 +0,0 @@ -/* Bouton rond de la barre d'actions (DA terracotta) */ -.nav-button { - position: relative; - width: 44px; - height: 44px; - display: grid; - place-items: center; - border: 0; - border-radius: 50%; - background: transparent; - color: var(--pz-ink); - cursor: pointer; - transition: background-color 160ms ease, color 160ms ease, transform 140ms var(--pz-ease); - -webkit-tap-highlight-color: transparent; -} -@media (hover: hover) and (pointer: fine) { .nav-button:hover:not(.disabled) { background: var(--pz-line-soft); } } -.nav-button:active:not(.disabled) { transform: scale(.94); } -.nav-button:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; } -.nav-button.active { background: var(--pz-a100); color: var(--pz-a700); } -.nav-button.disabled { opacity: .35; cursor: not-allowed; } - -.nav-button-icon { display: grid; place-items: center; line-height: 1; } -.nav-button-icon svg { width: 20px; height: 20px; } - -.nav-button-badge { - position: absolute; - top: 2px; - right: 2px; - min-width: 18px; - height: 18px; - padding: 0 5px; - display: grid; - place-items: center; - border-radius: 999px; - border: 2px solid var(--pz-surface); - font: 600 10.5px/1 var(--pz-sans); - color: #fff; - box-sizing: content-box; -} -.nav-button-badge.orange { background: var(--pz-a500); } -.nav-button-badge.gray { background: var(--pz-faint); } diff --git a/frontend/src/components/navbar/NavButton.jsx b/frontend/src/components/navbar/NavButton.jsx deleted file mode 100644 index a368460..0000000 --- a/frontend/src/components/navbar/NavButton.jsx +++ /dev/null @@ -1,44 +0,0 @@ -import './NavButton.css' - -/** - * Bouton de navigation contextuel - * Style glassmorphism - * - * @param {Object} props - * @param {string} props.icon - Emoji ou texte de l'icône - * @param {string} props.label - Label au hover (tooltip) - * @param {Function} props.onClick - Handler du clic - * @param {boolean} props.active - État actif (pour toggle) - * @param {boolean} props.disabled - État désactivé - * @param {number} props.badge - Nombre à afficher dans le badge (optionnel) - * @param {string} props.badgeColor - Couleur du badge ('orange' | 'gray') - */ -function NavButton({ - icon, - label, - onClick, - active = false, - disabled = false, - badge = null, - badgeColor = 'orange', -}) { - return ( - - ) -} - -export default NavButton diff --git a/frontend/src/components/navbar/index.js b/frontend/src/components/navbar/index.js deleted file mode 100644 index 125f80a..0000000 --- a/frontend/src/components/navbar/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// Export central pour les composants navbar -export { default as ContextualNavbar } from './ContextualNavbar' -export { default as NavButton } from './NavButton' -export { default as AvatarMenu } from './AvatarMenu' -export { default as TreeDropdown } from './TreeDropdown' diff --git a/frontend/src/components/search/index.js b/frontend/src/components/search/index.js deleted file mode 100644 index da76d1c..0000000 --- a/frontend/src/components/search/index.js +++ /dev/null @@ -1,3 +0,0 @@ -// Export central pour les composants search -export { default as ExpandableSearch } from './ExpandableSearch' -export { default as SearchResultsDropdown } from './SearchResultsDropdown' diff --git a/frontend/src/data/mockData.js b/frontend/src/data/mockData.js index bd7e4f5..736b4e1 100644 --- a/frontend/src/data/mockData.js +++ b/frontend/src/data/mockData.js @@ -355,28 +355,6 @@ export function getPersonById(personId) { return personsById.get(String(personId)) || null } -export function upsertPersonFromGraphPayload(person) { - if (!person?.id) { - return null - } - - const index = persons.findIndex((candidate) => String(candidate.id) === String(person.id)) - const current = index >= 0 ? persons[index] : null - const localPerson = mapRemotePersonToLocal(person, current) - - if (index >= 0) { - persons = [ - ...persons.slice(0, index), - localPerson, - ...persons.slice(index + 1), - ] - } else { - persons = [...persons, localPerson] - } - - return localPerson -} - export function getParents(personId) { const fil = filiations.filter((filiation) => filiation.childId === personId) const result = [] @@ -438,18 +416,6 @@ export function getPersonUnions(personId) { .sort((a, b) => a.displayOrder - b.displayOrder) } -export function getUnionChildren(unionId) { - return filiations - .filter((filiation) => filiation.unionId === unionId) - .sort((a, b) => a.displayOrder - b.displayOrder) - .map((filiation) => persons.find((person) => person.id === filiation.childId)) - .filter(Boolean) -} - -export function getChildFiliation(childId) { - return filiations.find((filiation) => filiation.childId === childId) || null -} - // ---- Annotations helpers ---- let annotationIdCounter = 0 diff --git a/frontend/src/hooks/useUndoable.js b/frontend/src/hooks/useUndoable.js index 7247c63..4be2a71 100644 --- a/frontend/src/hooks/useUndoable.js +++ b/frontend/src/hooks/useUndoable.js @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' // Délai pendant lequel une suppression reste annulable avant de partir au serveur -export const UNDO_DELAY_MS = 6000 +const UNDO_DELAY_MS = 6000 // Une suppression annulable à la fois. L'écran change tout de suite ; la requête ne part qu'à la fin du délai, // à la suppression suivante, ou à la fermeture de la page (fetch keepalive). diff --git a/frontend/src/index.css b/frontend/src/index.css index bbe2891..7aed9b8 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -39,14 +39,10 @@ /* DM Sans pour les petits éléments UI : labels, meta, nav, badges */ button, input, select, textarea, label, - .tree-meta, .admin-meta, .admin-stat-card span, - .contrib-invite-header span, .contrib-item-meta span, + .contrib-item-meta span, .contrib-session-date, .contrib-role-badge, - .contrib-link-url, .contrib-pass-value, .contrib-pass-reveal, - .access-gate-tree-meta, .media-manager-meta span, - .admin-audit-main span, .admin-audit-meta, - .admin-person-item span, - .account-help { + .contrib-link-url, .contrib-pass-reveal, + .access-gate-tree-meta { font-family: 'DM Sans', system-ui, sans-serif; } } diff --git a/frontend/src/styles/pz.css b/frontend/src/styles/pz.css index d814e97..fb975ae 100644 --- a/frontend/src/styles/pz.css +++ b/frontend/src/styles/pz.css @@ -31,7 +31,6 @@ --pz-sans: 'DM Sans', system-ui, -apple-system, sans-serif; --pz-ease: cubic-bezier(.23, 1, .32, 1); --pz-ease-drawer: cubic-bezier(.32, .72, 0, 1); - --pz-ease-pop: cubic-bezier(.34, 1.4, .64, 1); --pz-r-s: 10px; --pz-r-m: 14px; --pz-r-l: 22px; @@ -46,8 +45,6 @@ p, li, blockquote, figcaption, dd, .pz-sub, .pz-small, .pz-hint, .pz-error, .pz- .pz-eyebrow { font: 500 12px/1.3 var(--pz-sans); letter-spacing: .1em; text-transform: uppercase; color: var(--pz-muted); margin: 0; } .pz-title { font: 400 30px/1.1 var(--pz-serif); letter-spacing: -.01em; color: var(--pz-ink); margin: 0; text-wrap: balance; } .pz-title em { font-style: italic; font-weight: 300; } -.pz-title-lg { font: 300 clamp(36px, 5vw, 48px)/1.04 var(--pz-serif); letter-spacing: -.02em; color: var(--pz-ink); margin: 0; text-wrap: balance; } -.pz-title-lg em { font-style: italic; } .pz-sub { font: 400 15px/1.55 var(--pz-sans); color: var(--pz-ink-2); margin: 0; } .pz-small { font: 400 13px/1.5 var(--pz-sans); color: var(--pz-muted); margin: 0; } .pz-small a, .pz-sub a { color: var(--pz-a700); text-underline-offset: 3px; } @@ -190,8 +187,6 @@ p, li, blockquote, figcaption, dd, .pz-sub, .pz-small, .pz-hint, .pz-error, .pz- .pz-menu-item--danger { color: var(--pz-danger); } .pz-menu-item--danger svg { color: var(--pz-danger); } @media (hover: hover) and (pointer: fine) { .pz-menu-item:hover { background: var(--pz-line-soft); } .pz-menu-item.is-active:hover { background: var(--pz-a100); } } -.pz-menu-sep { height: 1px; margin: 4px 6px; background: var(--pz-line); } -.pz-menu-label { padding: 8px 12px 4px; font: 500 11.5px var(--pz-sans); letter-spacing: .08em; text-transform: uppercase; color: var(--pz-faint); } /* ── Le rond qui attend (motion validée le 25/09/2026) ────────── */ /* Le rond se divise en trois points qui respirent, puis se referme avec un léger rebond. */ @@ -200,13 +195,6 @@ p, li, blockquote, figcaption, dd, .pz-sub, .pz-small, .pz-hint, .pz-error, .pz- .pz-mark-c { fill: var(--pz-a500); transform-box: fill-box; transform-origin: center; } .pz-mark-p { transition: transform 420ms var(--pz-ease); } .pz-mark-s { transform-box: fill-box; transform-origin: center; transition: transform 420ms var(--pz-ease); } -.pz-mark.is-loading .pz-mark-p0 { transform: translateX(-300px); } -.pz-mark.is-loading .pz-mark-p2 { transform: translateX(300px); } -.pz-mark.is-loading .pz-mark-s { transform: scale(.27); } -.pz-mark.is-loading .pz-mark-c { animation: pz-souffle 1200ms ease-in-out 420ms infinite; } -.pz-mark.is-loading .pz-mark-p1 .pz-mark-c { animation-delay: 580ms; } -.pz-mark.is-loading .pz-mark-p2 .pz-mark-c { animation-delay: 740ms; } -.pz-mark.is-merging .pz-mark-p, .pz-mark.is-merging .pz-mark-s { transition-duration: 560ms; transition-timing-function: var(--pz-ease-pop); } @keyframes pz-souffle { 0%, 100% { opacity: 1; transform: none; } 50% { opacity: .32; transform: scale(.78); } } .pz-boot { @@ -238,7 +226,7 @@ button:disabled:has(.pz-busy.is-busy) { opacity: 1; cursor: progress; } .pz-btn:active:not(:disabled) { transform: none; } /* les points apparaissent sur place et clignotent en fondu, sans déplacement */ .pz-mark-p, .pz-mark-s, .pz-boot .pz-mark { transition-duration: 1ms !important; } - .pz-mark.is-loading .pz-mark-c, .pz-busy.is-busy .pz-dots i { animation-name: pz-fade-pulse !important; } + .pz-busy.is-busy .pz-dots i { animation-name: pz-fade-pulse !important; } .pz-busy.is-busy .pz-busy-lbl { filter: none; transform: none; } } @keyframes pz-fade-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } } @@ -256,14 +244,6 @@ button:disabled:has(.pz-busy.is-busy) { opacity: 1; cursor: progress; } .pz-lang .pz-lang-opt:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; } @media (hover: hover) and (pointer: fine) { .pz-lang .pz-lang-opt:not(.is-active):hover { color: var(--pz-ink); background: var(--pz-line-soft); } } .pz-lang-sep { font: 400 12px/1 var(--pz-sans); color: var(--pz-faint); user-select: none; } -/* Ligne de menu : libellé à gauche, choix à droite */ -.pz-lang-row { - display: flex; align-items: center; justify-content: space-between; gap: 10px; min-height: 40px; padding: 4px 6px 4px 12px; - font: 500 14px/1.3 var(--pz-sans); color: var(--pz-ink); -} -.pz-lang-row-label { display: inline-flex; align-items: center; gap: 10px; } -.pz-lang-row-label svg { color: var(--pz-muted); flex: none; } -/* Dans la barre d'actions, séparé des boutons par un filet */ /* Sous les écrans d'accès et de connexion */ .pz-lang--screen { justify-self: center; } diff --git a/frontend/src/utils/geoMedia.js b/frontend/src/utils/geoMedia.js index 118a262..c04f95a 100644 --- a/frontend/src/utils/geoMedia.js +++ b/frontend/src/utils/geoMedia.js @@ -1,8 +1,8 @@ -export const DEFAULT_MAP_VIEWBOX_WIDTH = 420 -export const DEFAULT_MAP_VIEWBOX_HEIGHT = 280 -export const MAX_MERCATOR_LAT = 85.05112878 +const DEFAULT_MAP_VIEWBOX_WIDTH = 420 +const DEFAULT_MAP_VIEWBOX_HEIGHT = 280 +const MAX_MERCATOR_LAT = 85.05112878 -export function clamp(value, min, max) { +function clamp(value, min, max) { return Math.min(max, Math.max(min, value)) } diff --git a/frontend/src/utils/ghosts.js b/frontend/src/utils/ghosts.js index c3b2b50..bb6355f 100644 --- a/frontend/src/utils/ghosts.js +++ b/frontend/src/utils/ghosts.js @@ -118,7 +118,7 @@ export function ghostsFromChanges(changes = [], meta = {}, graph = {}) { return { persons, unions, filiations, annotations, patches } } -export function emptyGhosts() { +function emptyGhosts() { return { persons: [], unions: [], filiations: [], annotations: [], patches: new Map() } } diff --git a/frontend/src/utils/kinship/facts.js b/frontend/src/utils/kinship/facts.js index 0268ba2..dd2ac47 100644 --- a/frontend/src/utils/kinship/facts.js +++ b/frontend/src/utils/kinship/facts.js @@ -25,7 +25,7 @@ export function kinshipFacts(path) { } // Suite des pas d'un chemin, pour reconnaître les liens que le compte des générations confond (#218) -export const movesOf = (path) => path.steps.map((step) => step.move).join(',') +const movesOf = (path) => path.steps.map((step) => step.move).join(',') // Demi-frère ou demi-sœur : un seul parent en commun. Deux enfants du même couple passent par 'sibling' // (le chemin le plus court) ; deux enfants d'un même parent avec deux conjoints passent par ce parent. export const isHalfSibling = (path) => movesOf(path) === 'up,down' diff --git a/frontend/src/utils/mediaToken.js b/frontend/src/utils/mediaToken.js index 91d465a..954cb9d 100644 --- a/frontend/src/utils/mediaToken.js +++ b/frontend/src/utils/mediaToken.js @@ -7,10 +7,6 @@ export function setMediaToken(token) { currentMediaToken = typeof token === 'string' ? token : '' } -export function getMediaToken() { - return currentMediaToken -} - // Adresse d'un fichier servi par l'API. Une adresse externe (YouTube) ne reçoit jamais de jeton. export function buildMediaUrl(pathOrUrl, apiBaseUrl = '', token = currentMediaToken) { if (!pathOrUrl) { diff --git a/frontend/src/utils/mediaUpload.js b/frontend/src/utils/mediaUpload.js index 9b35882..ef9e447 100644 --- a/frontend/src/utils/mediaUpload.js +++ b/frontend/src/utils/mediaUpload.js @@ -1,7 +1,7 @@ import { t } from '../i18n/index.js' export const MAX_IMAGE_BYTES = 5 * 1024 * 1024 -export const MAX_FILE_BYTES = 20 * 1024 * 1024 +const MAX_FILE_BYTES = 20 * 1024 * 1024 export function inferMediaTypeFromMimeType(mimeType) { const normalized = String(mimeType || '').toLowerCase() diff --git a/frontend/src/utils/portraitCrop.js b/frontend/src/utils/portraitCrop.js index e50fbd7..4f5e35e 100644 --- a/frontend/src/utils/portraitCrop.js +++ b/frontend/src/utils/portraitCrop.js @@ -2,14 +2,14 @@ // (1 = l'image entière recouvre le cadre, 4 = gros plan). Null = cadrage par défaut (centré, zoom 1). // Ce module ne fait que des calculs : l'éditeur, la fiche et le canevas s'y réfèrent tous. -export const CROP_MAX_ZOOM = 4 +const CROP_MAX_ZOOM = 4 export const DEFAULT_CROP = { x: 0.5, y: 0.5, zoom: 1 } const clamp = (value, min, max) => Math.min(max, Math.max(min, value)) // Formes qu'on choisit au recadrage, gardées dans le cadrage (crop.shape). Sans choix, la forme suit la photo // (très large ou très haute) ou le cadre tiré au hasard pour la personne. -export const FRAME_SHAPES = ['round', 'square', 'polaroid', 'portrait', 'landscape'] +const FRAME_SHAPES = ['round', 'square', 'polaroid', 'portrait', 'landscape'] export const cropShape = (crop) => (FRAME_SHAPES.includes(crop?.shape) ? crop.shape : null) // Forme du cadre dans le canevas : même règle que resolveFrameType et drawFrame (renderers.js). diff --git a/frontend/src/utils/sheetDrag.js b/frontend/src/utils/sheetDrag.js index 7bb0355..d4a19a8 100644 --- a/frontend/src/utils/sheetDrag.js +++ b/frontend/src/utils/sheetDrag.js @@ -7,10 +7,10 @@ export const PHONE_QUERY = '(max-width: 767px), (pointer: coarse)' export const SHEET_WIDTH_QUERY = '(max-width: 768px)' // Distance avant de trancher entre glisser vertical, horizontal et tap -export const DIRECTION_LOCK_PX = 8 +const DIRECTION_LOCK_PX = 8 // Lâcher rapide vers le bas : vitesse en px/ms, et course minimale pour qu'il compte -export const FLICK_VELOCITY = 0.6 -export const FLICK_MIN_OFFSET = 24 +const FLICK_VELOCITY = 0.6 +const FLICK_MIN_OFFSET = 24 // Deux points plus espacés que ça ne donnent pas une vitesse fiable const VELOCITY_WINDOW_MS = 120 diff --git a/frontend/src/utils/zipReader.js b/frontend/src/utils/zipReader.js index 955c975..43c01a6 100644 --- a/frontend/src/utils/zipReader.js +++ b/frontend/src/utils/zipReader.js @@ -33,7 +33,7 @@ function decodeName(bytes, utf8) { return out } -export function isZipSignature(bytes) { +function isZipSignature(bytes) { return bytes.length >= 4 && bytes[0] === 0x50 && bytes[1] === 0x4b && (bytes[2] === 0x03 || bytes[2] === 0x05) && (bytes[3] === 0x04 || bytes[3] === 0x06) } @@ -104,7 +104,7 @@ const MIME_BY_EXTENSION = { } export const extensionOf = (name) => (String(name).match(/\.([a-z0-9]{1,5})$/i)?.[1] || '').toLowerCase() -export const mimeTypeOf = (name) => MIME_BY_EXTENSION[extensionOf(name)] || '' +const mimeTypeOf = (name) => MIME_BY_EXTENSION[extensionOf(name)] || '' // Fichier du zip qu'on peut rapprocher comme un fichier de dossier (nom, chemin), et ne lire qu'au moment // de l'envoyer : toFile() le décompresse et renvoie un vrai File