mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-13 00:16:09 +00:00
Sur téléphone, la fiche de la personne de départ ne s'ouvre plus à l'arrivée : elle couvrait le bas de l'écran et la personne avec, laissant un écran vide. La personne de départ arrive à 40 % de la hauteur au lieu de 56 %. Toucher quelqu'un dans l'arbre le remonte dans le tiers du haut, au-dessus de la fiche, comme le fait déjà la recherche (focusPerson). Ordinateur inchangé. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
223 lines
7.9 KiB
JavaScript
223 lines
7.9 KiB
JavaScript
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
import { setMediaToken } from '../utils/mediaToken.js'
|
|
import { API_BASE_URL } from '../api/client'
|
|
import { fetchTreeGraph, unlockTreeAccess } from '../api/treeApi'
|
|
import { loadGraphData, persons as graphPersons, resetToDemoData } from '../data/mockData'
|
|
import { getShareErrorMessage } from '../utils/errorMessages'
|
|
|
|
export const TREE_ID_FROM_ENV = (import.meta.env.VITE_TREE_ID || '').trim()
|
|
|
|
const TOKEN_STORAGE_PREFIX = 'tree_access_token_'
|
|
|
|
// Mot de passe de partage unique (25/09/2026) : tout accès partagé peut proposer.
|
|
// Les jetons et accès « visitor » d'avant valent contributeur.
|
|
function normalizeRole(role) {
|
|
const value = String(role || '').trim().toLowerCase()
|
|
return value === 'visitor' ? 'contributor' : value
|
|
}
|
|
|
|
export function getTreeTokenStorageKey(treeId) {
|
|
return `${TOKEN_STORAGE_PREFIX}${treeId}`
|
|
}
|
|
|
|
// Rôle d'un jeton d'accès partagé : toujours contributeur depuis le mot de passe unique (le serveur tranche)
|
|
export function readTreeAccessRole() {
|
|
return 'contributor'
|
|
}
|
|
|
|
export function getSlugFromPathname() {
|
|
const match = window.location.pathname.match(/^\/arbre\/([^/]+)$/)
|
|
return match ? decodeURIComponent(match[1]) : null
|
|
}
|
|
|
|
export function useTreeAccess() {
|
|
const [treeContext, setTreeContext] = useState({
|
|
treeId: '',
|
|
treeName: '',
|
|
treeDescription: '',
|
|
treeOwnerName: '',
|
|
treeOwnerEmail: '',
|
|
role: '',
|
|
accessMode: '',
|
|
rootPersonId: null,
|
|
accessToken: '',
|
|
})
|
|
const [bootState, setBootState] = useState('loading')
|
|
const [gateLoading, setGateLoading] = useState(false)
|
|
const [gateError, setGateError] = useState('')
|
|
const [selectedPerson, setSelectedPerson] = useState(null)
|
|
const [selectedMedia, setSelectedMedia] = useState(null)
|
|
const [wizardState, setWizardState] = useState({ visible: false, loading: false, error: '' })
|
|
const [graphRevision, setGraphRevision] = useState(0)
|
|
// Change à chaque chargement des données, même sans nouveau placement : l'arbre y charge les images manquantes
|
|
const [assetRevision, setAssetRevision] = useState(0)
|
|
// Horodatage des portraits stable pendant la session : un rechargement ne force pas à tout retélécharger.
|
|
// Il ne change que lorsqu'un portrait change (envoi d'une nouvelle photo). Posé au premier chargement
|
|
// d'un arbre, pas au rendu (Date.now() rendrait le rendu impur).
|
|
const avatarCacheBustRef = useRef(null)
|
|
|
|
const canEditCurrentTree = useMemo(() => {
|
|
const role = normalizeRole(treeContext.role)
|
|
return role === 'owner' || role === 'admin'
|
|
}, [treeContext.role])
|
|
const canSubmitContribution = useMemo(() => {
|
|
const role = normalizeRole(treeContext.role)
|
|
return Boolean(role) && role !== 'visitor'
|
|
}, [treeContext.role])
|
|
|
|
const openTreeGraph = async (treeId, token, options = {}) => {
|
|
const graphPayload = await fetchTreeGraph(treeId, token)
|
|
setMediaToken(graphPayload.mediaToken)
|
|
loadGraphData(graphPayload.graph, {
|
|
mediaToken: graphPayload.mediaToken,
|
|
apiBaseUrl: API_BASE_URL,
|
|
avatarCacheBust: options.avatarCacheBust ?? (avatarCacheBustRef.current ??= Date.now()),
|
|
})
|
|
if (options.avatarCacheBust) avatarCacheBustRef.current = options.avatarCacheBust
|
|
setAssetRevision((current) => current + 1)
|
|
if (options.relayout !== false) {
|
|
setGraphRevision((current) => current + 1)
|
|
}
|
|
const preferredRootPersonId = options.rootPersonId ?? graphPayload.rootPersonId ?? null
|
|
const rootPerson = preferredRootPersonId
|
|
? graphPersons.find((person) => String(person.id) === String(preferredRootPersonId)) || null
|
|
: null
|
|
// Par le lien de partage, la famille arrive sur l'arbre et sa carte d'accueil, pas sur une fiche ouverte.
|
|
// Sur téléphone non plus : la fiche y couvre le bas de l'écran, et la personne de départ avec (#188).
|
|
const openedByShareLink = (options.accessMode ?? '') === 'share'
|
|
const phone = typeof window !== 'undefined' && window.matchMedia?.('(max-width: 768px)').matches
|
|
setSelectedPerson(openedByShareLink || phone ? null : rootPerson)
|
|
setSelectedMedia(null)
|
|
|
|
setTreeContext((current) => ({
|
|
treeId,
|
|
treeName: options.treeName ?? current.treeName,
|
|
treeDescription: options.treeDescription ?? current.treeDescription,
|
|
treeOwnerName: options.treeOwnerName ?? current.treeOwnerName,
|
|
treeOwnerEmail: options.treeOwnerEmail ?? current.treeOwnerEmail,
|
|
role: normalizeRole(options.role ?? current.role),
|
|
accessMode: options.accessMode ?? current.accessMode,
|
|
rootPersonId: preferredRootPersonId,
|
|
accessToken: token,
|
|
}))
|
|
|
|
const effectiveRole = normalizeRole(options.role)
|
|
const shouldPromptWizard =
|
|
Boolean(options.promptWizard) &&
|
|
(effectiveRole === 'owner' || effectiveRole === 'admin') &&
|
|
Array.isArray(graphPayload?.graph?.persons) &&
|
|
graphPayload.graph.persons.length === 0
|
|
|
|
if (shouldPromptWizard) {
|
|
setWizardState({ visible: true, loading: false, error: '' })
|
|
} else {
|
|
setWizardState((current) => ({ ...current, visible: false, loading: false, error: '' }))
|
|
}
|
|
|
|
setBootState('ready')
|
|
return graphPayload
|
|
}
|
|
|
|
const refreshCurrentTreeGraph = async (options = {}) => {
|
|
await openTreeGraph(treeContext.treeId, treeContext.accessToken, {
|
|
treeName: treeContext.treeName,
|
|
treeDescription: treeContext.treeDescription,
|
|
treeOwnerName: treeContext.treeOwnerName,
|
|
treeOwnerEmail: treeContext.treeOwnerEmail,
|
|
role: treeContext.role,
|
|
accessMode: treeContext.accessMode,
|
|
rootPersonId: treeContext.rootPersonId,
|
|
promptWizard: false,
|
|
relayout: options.relayout !== false,
|
|
avatarCacheBust: options.avatarCacheBust,
|
|
})
|
|
}
|
|
|
|
const bumpGraphRevision = useCallback(() => {
|
|
setGraphRevision((current) => current + 1)
|
|
}, [])
|
|
|
|
const handleUnlock = async (treeId, password, options = {}) => {
|
|
setGateLoading(true)
|
|
setGateError('')
|
|
|
|
try {
|
|
const access = await unlockTreeAccess(treeId, password, options.userToken || '')
|
|
localStorage.setItem(getTreeTokenStorageKey(treeId), access.token)
|
|
await openTreeGraph(treeId, access.token, {
|
|
treeName: options.treeName ?? treeContext.treeName,
|
|
treeDescription: options.treeDescription ?? treeContext.treeDescription,
|
|
treeOwnerName: options.treeOwnerName ?? treeContext.treeOwnerName,
|
|
treeOwnerEmail: options.treeOwnerEmail ?? treeContext.treeOwnerEmail,
|
|
role: access.role,
|
|
accessMode: 'share',
|
|
promptWizard: false,
|
|
})
|
|
return access
|
|
} catch (error) {
|
|
setGateError(getShareErrorMessage(error))
|
|
return null
|
|
} finally {
|
|
setGateLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleMediaSelect = (media, person) => {
|
|
setSelectedMedia({ media, person })
|
|
}
|
|
|
|
const setWizardVisibility = (visible, error = '') => {
|
|
setWizardState((current) => ({
|
|
...current,
|
|
visible,
|
|
error,
|
|
}))
|
|
}
|
|
|
|
const handleSkipWizard = () => {
|
|
setWizardState({ visible: false, loading: false, error: '' })
|
|
}
|
|
|
|
const handleUseDemo = () => {
|
|
resetToDemoData()
|
|
setGraphRevision((current) => current + 1)
|
|
setSelectedPerson(null)
|
|
setSelectedMedia(null)
|
|
setGateError('')
|
|
setWizardState({ visible: false, loading: false, error: '' })
|
|
setTreeContext({
|
|
treeId: '',
|
|
treeName: '',
|
|
treeDescription: '',
|
|
treeOwnerName: '',
|
|
treeOwnerEmail: '',
|
|
role: '',
|
|
accessMode: 'demo',
|
|
rootPersonId: null,
|
|
accessToken: '',
|
|
})
|
|
setBootState('ready')
|
|
}
|
|
|
|
return {
|
|
treeContext, setTreeContext,
|
|
bootState, setBootState,
|
|
assetRevision,
|
|
gateLoading, gateError, setGateError,
|
|
selectedPerson, setSelectedPerson,
|
|
selectedMedia, setSelectedMedia,
|
|
wizardState, setWizardState,
|
|
graphRevision,
|
|
bumpGraphRevision,
|
|
canEditCurrentTree,
|
|
canSubmitContribution,
|
|
openTreeGraph,
|
|
refreshCurrentTreeGraph,
|
|
handleUnlock,
|
|
handleMediaSelect,
|
|
setWizardVisibility,
|
|
handleSkipWizard,
|
|
handleUseDemo,
|
|
}
|
|
}
|