Files
parenthese/frontend/src/data/mockData.js
T
IliesAllaliandClaude Opus 5.5 8b50356c4b Démo : vraie vidéo d'époque pour le mariage de Simone, vidéos de remplacement retirées
Le souvenir « Mariage Simone 1943 » ouvrait Big Buck Bunny sur YouTube.
Remplacé par 25 s des actualités Polygoon « Bruiloft te Giethoorn » (1947,
Beeld en Geluid via Open Beelden, CC BY-SA 3.0 NL), sans le son, en H.264
pour Safari, servi par le site (media-src 'self'). Crédit dans CREDITS.md
et sources.json. Les trois autres vidéos YouTube de remplacement de la démo
(Témoignage filmé 1960, Anniversaire 2020, Interview archive radio) sont
retirées.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 14:55:05 +02:00

395 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// Data model: Person / Union / Filiation / Media / Annotation
// Compatible with the current Galaxy frontend.
// ============================================================
import { t } from '../i18n/index.js'
import { buildMediaUrl } from '../utils/mediaToken.js'
// Démo : noms et lieux restent français, les textes descriptifs sont traduits.
export let persons = [
{ id: 1, firstName: 'Jean', lastName: 'Dupont', birthYear: 1890, deathYear: 1965, isAlive: false, photo: '/demo/jean.webp', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: t('Menuisier'), nationality: t('Française'), note: t('Patriarche de la famille') },
{ id: 2, firstName: 'Marie', lastName: 'Martin', birthYear: 1895, deathYear: 1970, isAlive: false, photo: '/demo/marie.webp', frameType: 'round', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: t('Couturière'), note: t('Atelier de couture rue du Commerce') },
{ id: 3, firstName: 'Robert', lastName: 'Dupont', birthYear: 1920, deathYear: 1995, isAlive: false, photo: '/demo/robert.webp', frameType: 'rect', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: t('Instituteur'), note: t("A enseigné 35 ans à l'école communale") },
{ id: 4, firstName: 'André', lastName: 'Dupont', birthYear: 1923, deathYear: 2000, isAlive: false, photo: '/demo/andre.webp', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: t('Charpentier') },
{ id: 5, firstName: 'Simone', lastName: 'Petit', birthYear: 1925, deathYear: 2010, isAlive: false, photo: '/demo/simone.webp', frameType: 'round', birthPlace: 'Guéret', region: 'Creuse', profession: t('Infirmière') },
{ id: 9, firstName: 'Isabelle', lastName: 'Bernard', birthYear: 1928, isAlive: true, photo: '/demo/isabelle.webp', frameType: 'rect', birthPlace: 'Fort-de-France', region: 'Martinique', profession: t('Sage-femme'), note: t('Doyenne de la famille, vit à Brive') },
{ id: 6, firstName: 'Jacques', lastName: 'Dupont', birthYear: 1945, deathYear: 2015, isAlive: false, photo: '/demo/jacques.webp', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: t('Journaliste'), note: t('Reporter au Populaire du Centre') },
{ id: 7, firstName: 'Philippe', lastName: 'Dupont', birthYear: 1948, isAlive: true, photo: '/demo/philippe.webp', frameType: 'round', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: t('Pharmacien') },
{ id: 8, firstName: 'Sophie', lastName: 'Dupont', birthYear: 1960, isAlive: true, photo: '/demo/sophie.webp', frameType: 'rect', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: t('Architecte'), note: t('Cabinet d’architecture à Bordeaux') },
{ id: 10, firstName: 'Thomas', lastName: 'Dupont', birthYear: 1962, isAlive: true, photo: '/demo/thomas.webp', frameType: 'polaroid', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: t('Vétérinaire') },
{ id: 11, firstName: 'Claire', lastName: 'Dupont', birthYear: 1955, isAlive: true, photo: '/demo/claire.webp', birthPlace: 'Paris', region: 'Ile-de-France', profession: t('Enseignante') },
]
export let medias = [
{ id: 'm1', personId: 1, type: 'photo', url: '/demo/souvenir-mariage-1918-thumb.webp', urlHd: '/demo/souvenir-mariage-1918.webp', label: t('Mariage 1918') },
{ id: 'm2', personId: 1, type: 'photo', url: '/demo/souvenir-atelier-thumb.webp', urlHd: '/demo/souvenir-atelier.webp', label: t('Atelier de menuiserie') },
{ id: 'm4', personId: 2, type: 'photo', url: '/demo/souvenir-jeunesse-thumb.webp', urlHd: '/demo/souvenir-jeunesse.webp', label: t('Jeunesse') },
{ id: 'm5', personId: 2, type: 'photo', url: '/demo/souvenir-portrait-famille-thumb.webp', urlHd: '/demo/souvenir-portrait-famille.webp', label: t('Portrait de famille') },
{ id: 'm6', personId: 3, type: 'photo', url: '/demo/souvenir-service-militaire-thumb.webp', urlHd: '/demo/souvenir-service-militaire.webp', label: t('Service militaire') },
// Actualités Polygoon 1947, CC BY-SA 3.0 NL, voir public/demo/CREDITS.md
{ id: 'm7', personId: 3, type: 'video', url: '/demo/souvenir-mariage-1947.mp4', mimeType: 'video/mp4', label: t('Mariage Simone 1943') },
{ id: 'm8', personId: 3, type: 'photo', url: '/demo/souvenir-annees-70-thumb.webp', urlHd: '/demo/souvenir-annees-70.webp', label: t('Années 70') },
{ id: 'm9', personId: 8, type: 'photo', url: '/demo/souvenir-diplome-thumb.webp', urlHd: '/demo/souvenir-diplome.webp', label: t('Diplôme') },
{ id: 'm10', personId: 8, type: 'photo', url: '/demo/souvenir-plan-cabinet-thumb.webp', urlHd: '/demo/souvenir-plan-cabinet.webp', label: t('Plan du cabinet 1985') },
{ id: 'm12', personId: 6, type: 'photo', url: '/demo/souvenir-redaction-thumb.webp', urlHd: '/demo/souvenir-redaction.webp', label: t('En rédaction') },
]
export let unions = [
{ id: 'u1', partner1Id: 1, partner2Id: 2, unionType: 'mariage', startYear: 1918, endYear: null, displayOrder: 1 },
{ id: 'u2', partner1Id: 3, partner2Id: 5, unionType: 'mariage', startYear: 1943, endYear: 1958, displayOrder: 1 },
{ id: 'u3', partner1Id: 3, partner2Id: 9, unionType: 'mariage', startYear: 1959, endYear: null, displayOrder: 2 },
]
export let filiations = [
{ id: 'f1', childId: 3, unionId: 'u1', parentId: null, parentageType: 'biologique', displayOrder: 1 },
{ id: 'f2', childId: 4, unionId: 'u1', parentId: null, parentageType: 'biologique', displayOrder: 2 },
{ id: 'f3', childId: 6, unionId: 'u2', parentId: null, parentageType: 'biologique', displayOrder: 1 },
{ id: 'f4', childId: 7, unionId: 'u2', parentId: null, parentageType: 'biologique', displayOrder: 2 },
{ id: 'f5', childId: 8, unionId: 'u3', parentId: null, parentageType: 'biologique', displayOrder: 1 },
{ id: 'f6', childId: 10, unionId: 'u3', parentId: null, parentageType: 'biologique', displayOrder: 2 },
{ id: 'f7', childId: 11, unionId: null, parentId: 4, parentageType: 'inconnu', displayOrder: 1 },
]
export let annotations = []
const INITIAL_PERSONS = JSON.parse(JSON.stringify(persons))
const INITIAL_MEDIAS = JSON.parse(JSON.stringify(medias))
const INITIAL_UNIONS = JSON.parse(JSON.stringify(unions))
const INITIAL_FILIATIONS = JSON.parse(JSON.stringify(filiations))
function parseYearFromDate(value) {
if (!value || typeof value !== 'string') {
return null
}
const year = Number.parseInt(value.slice(0, 4), 10)
return Number.isFinite(year) ? year : null
}
function pickFrameType(id) {
const frameTypes = ['round', 'rect', 'polaroid']
const text = String(id)
let hash = 0
for (let i = 0; i < text.length; i += 1) {
hash += text.charCodeAt(i)
}
return frameTypes[hash % frameTypes.length]
}
// Adresse d'un fichier de l'arbre avec le jeton médias (voir utils/mediaToken.js)
function buildProtectedAssetUrl(pathOrUrl, apiBaseUrl, mediaToken, cacheBust = null) {
let protectedUrl = buildMediaUrl(pathOrUrl, apiBaseUrl, mediaToken)
if (!protectedUrl) {
return null
}
if (cacheBust !== null && cacheBust !== undefined) {
const separator = protectedUrl.includes('?') ? '&' : '?'
protectedUrl = `${protectedUrl}${separator}v=${encodeURIComponent(String(cacheBust))}`
}
return protectedUrl
}
function mapRemotePersonToLocal(person, current = null) {
const birthYear = parseYearFromDate(person?.birthDate)
const deathYear = parseYearFromDate(person?.deathDate)
return {
id: person?.id ?? current?.id,
firstName: person?.firstName || current?.firstName || t('Inconnu'),
lastName: person?.lastName || current?.lastName || '',
birthName: person?.birthName ?? current?.birthName ?? null,
birthYear,
birthDate: person?.birthDate || current?.birthDate || null,
deathDate: person?.deathDate || current?.deathDate || null,
deathYear,
isAlive: !deathYear,
frameType: current?.frameType || pickFrameType(person?.id),
note: person?.notes || null,
birthPlace: person?.birthPlace || null,
region: person?.region || null,
profession: person?.profession || null,
nationality: person?.nationality || null,
photo: current?.photo || null,
}
}
export function resetToDemoData() {
persons = JSON.parse(JSON.stringify(INITIAL_PERSONS))
medias = JSON.parse(JSON.stringify(INITIAL_MEDIAS))
unions = JSON.parse(JSON.stringify(INITIAL_UNIONS))
filiations = JSON.parse(JSON.stringify(INITIAL_FILIATIONS))
}
export function loadGraphData(graph, options = {}) {
const mediaToken = options.mediaToken || ''
const apiBaseUrl = (options.apiBaseUrl || '').replace(/\/$/, '')
const avatarCacheBust = options.avatarCacheBust ?? null
const remotePersons = Array.isArray(graph?.persons) ? graph.persons : []
const remoteUnions = Array.isArray(graph?.unions) ? graph.unions : []
const remoteFiliations = Array.isArray(graph?.filiations) ? graph.filiations : []
const remoteMedias = Array.isArray(graph?.medias) ? graph.medias : []
const remoteAnnotations = Array.isArray(graph?.annotations) ? graph.annotations : []
persons = remotePersons.map((person) => {
const localPerson = mapRemotePersonToLocal(person)
localPerson.photo = buildProtectedAssetUrl(person.avatarUrlPath || null, apiBaseUrl, mediaToken, avatarCacheBust)
return localPerson
})
const personIdSet = new Set(persons.map((person) => person.id))
unions = remoteUnions
.map((union, index) => ({
id: union.id,
partner1Id: union.partner1PersonId,
partner2Id: union.partner2PersonId,
unionType: union.unionType || null,
startYear: parseYearFromDate(union.startDate),
endYear: parseYearFromDate(union.endDate),
displayOrder: Number.isFinite(union.displayOrder) ? union.displayOrder : index + 1,
}))
.filter((union) => (
personIdSet.has(union.partner1Id) &&
personIdSet.has(union.partner2Id)
))
const unionIdSet = new Set(unions.map((union) => union.id))
const seenUnionChildKey = new Set()
filiations = remoteFiliations
.map((filiation, index) => ({
id: filiation.id,
childId: filiation.childPersonId,
unionId: filiation.viaUnionId || null,
parentId: filiation.viaUnionId ? null : filiation.parentPersonId,
parentageType: filiation.parentageType || 'biologique',
displayOrder: Number.isFinite(filiation.displayOrder) ? filiation.displayOrder : index + 1,
}))
.filter((filiation) => {
if (!personIdSet.has(filiation.childId)) {
return false
}
if (!filiation.unionId) {
return filiation.parentId ? personIdSet.has(filiation.parentId) : false
}
if (!unionIdSet.has(filiation.unionId)) {
return false
}
const key = `${filiation.unionId}:${filiation.childId}`
if (seenUnionChildKey.has(key)) {
return false
}
seenUnionChildKey.add(key)
return true
})
medias = remoteMedias
.filter((media) => personIdSet.has(media.personId))
.map((media, index) => {
const mediaUrl = buildProtectedAssetUrl(media.urlPath || '', apiBaseUrl, mediaToken)
return {
id: media.id,
personId: media.personId,
type: media.type || 'photo',
url: mediaUrl,
urlHd: mediaUrl,
label: media.caption || t('Média {n}', { n: index + 1 }),
source: media.source || null,
displayOrder: Number.isFinite(media.displayOrder) ? media.displayOrder : index + 1,
isFeatured: Boolean(media.isFeatured),
mimeType: media.mimeType || null,
}
})
annotations = remoteAnnotations.map((ann) => {
let content = ann.content || ''
// Annotation photo : l'adresse est reconstruite ici depuis photoPath, le serveur n'en garde aucune
if (ann.type === 'photo' && content) {
try {
const parsed = typeof content === 'string' ? JSON.parse(content) : content
if (parsed.photoPath) {
parsed.url = buildProtectedAssetUrl(parsed.photoPath, apiBaseUrl, mediaToken)
content = JSON.stringify(parsed)
}
} catch { /* ignore */ }
}
return {
id: ann.id,
type: ann.type || 'text',
x: ann.x ?? 0,
y: ann.y ?? 0,
content,
style: ann.style || {},
zIndex: ann.zIndex ?? 0,
createdBy: ann.createdBy || null,
}
})
}
// Index par personne, reconstruits quand le tableau est réassigné (les tableaux ne sont jamais
// mutés en place). Appelés pour chaque nœud à chaque frame par la galaxie : un parcours linéaire
// coûterait nœuds × éléments comparaisons par frame sur un grand arbre.
const EMPTY_MEDIAS = []
let mediasIndexSource = null
let mediasByPerson = new Map()
export function getPersonMedias(personId) {
if (mediasIndexSource !== medias) {
mediasIndexSource = medias
mediasByPerson = new Map()
for (const media of medias) {
if (!mediasByPerson.has(media.personId)) mediasByPerson.set(media.personId, [])
mediasByPerson.get(media.personId).push(media)
}
for (const list of mediasByPerson.values()) {
list.sort((a, b) => {
const aOrder = Number.isFinite(a.displayOrder) ? a.displayOrder : Number.MAX_SAFE_INTEGER
const bOrder = Number.isFinite(b.displayOrder) ? b.displayOrder : Number.MAX_SAFE_INTEGER
return aOrder - bOrder
})
}
}
return mediasByPerson.get(personId) || EMPTY_MEDIAS
}
let personsIndexSource = null
let personsById = new Map()
export function getPersonById(personId) {
if (personsIndexSource !== persons) {
personsIndexSource = persons
personsById = new Map(persons.map((person) => [String(person.id), person]))
}
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 = []
const seenParentIds = new Set()
for (const filiation of fil) {
if (filiation.unionId) {
const union = unions.find((currentUnion) => currentUnion.id === filiation.unionId)
if (union) {
const p1 = persons.find((person) => person.id === union.partner1Id)
const p2 = persons.find((person) => person.id === union.partner2Id)
if (p1 && !seenParentIds.has(p1.id)) {
seenParentIds.add(p1.id)
result.push(p1)
}
if (p2 && !seenParentIds.has(p2.id)) {
seenParentIds.add(p2.id)
result.push(p2)
}
}
} else if (filiation.parentId) {
const parent = persons.find((person) => person.id === filiation.parentId)
if (parent && !seenParentIds.has(parent.id)) {
seenParentIds.add(parent.id)
result.push(parent)
}
}
}
return result
}
export function getChildren(personId) {
const childIds = new Set()
const personUnions = unions.filter((union) => union.partner1Id === personId || union.partner2Id === personId)
for (const union of personUnions) {
const childrenInUnion = filiations.filter((filiation) => filiation.unionId === union.id)
childrenInUnion.forEach((filiation) => childIds.add(filiation.childId))
}
const directFiliations = filiations.filter((filiation) => filiation.parentId === personId)
directFiliations.forEach((filiation) => childIds.add(filiation.childId))
return [...childIds]
.map((id) => persons.find((person) => person.id === id))
.filter(Boolean)
.sort((a, b) => {
const aYear = Number.isFinite(a.birthYear) ? a.birthYear : Number.MAX_SAFE_INTEGER
const bYear = Number.isFinite(b.birthYear) ? b.birthYear : Number.MAX_SAFE_INTEGER
return aYear - bYear
})
}
export function getPersonUnions(personId) {
return unions
.filter((union) => union.partner1Id === personId || union.partner2Id === 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
export function addAnnotation(ann) {
annotationIdCounter += 1
const id = ann.id || `local-ann-${annotationIdCounter}`
const newAnn = { ...ann, id }
annotations = [...annotations, newAnn]
return newAnn
}
export function updateAnnotation(id, changes) {
const index = annotations.findIndex((a) => a.id === id)
if (index < 0) return null
const updated = { ...annotations[index], ...changes }
annotations = [...annotations.slice(0, index), updated, ...annotations.slice(index + 1)]
return updated
}
export function removeAnnotation(id) {
annotations = annotations.filter((a) => a.id !== id)
}