mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-15 08:32:03 +00:00
Nettoyage frontend : styles et fonctions sans appelant, drapeau ?layout=sugiyama, en-tête d'autorisation partagé
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
f8d1a48240
commit
4c80dea44a
28 files changed
+100
-1069
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 : []
|
||||
|
||||
+45
-163
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
className={`nav-button ${active ? 'active' : ''} ${disabled ? 'disabled' : ''}`}
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
>
|
||||
<span className="nav-button-icon">{icon}</span>
|
||||
{badge !== null && badge > 0 && (
|
||||
<span className={`nav-button-badge ${badgeColor}`}>
|
||||
{badge > 99 ? '99+' : badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default NavButton
|
||||
@@ -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'
|
||||
@@ -1,3 +0,0 @@
|
||||
// Export central pour les composants search
|
||||
export { default as ExpandableSearch } from './ExpandableSearch'
|
||||
export { default as SearchResultsDropdown } from './SearchResultsDropdown'
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user