diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 158f7e2..f2a998f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,19 +9,19 @@ "version": "0.0.0", "dependencies": { "lucide-react": "^0.564.0", - "posthog-js": "^1.353.0", - "react": "^19.2.0", - "react-dom": "^19.2.0" + "posthog-js": "^1.435.6", + "react": "^19.3.0", + "react-dom": "^19.3.0" }, "devDependencies": { "@eslint/js": "^9.39.1", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^5.1.1", - "autoprefixer": "^10.4.24", + "autoprefixer": "^10.6.1", "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", "globals": "^16.5.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", @@ -1070,28 +1070,28 @@ } }, "node_modules/@posthog/browser-common": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.9.0.tgz", - "integrity": "sha512-EWoVzs1vVY7ihpn8yOjj3jyjriHUEEMVSBNL/TW3hk09fTriWRe76vJXrRg0G0yjTYxHmtlSkNn4mOV24ZEzzA==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.9.1.tgz", + "integrity": "sha512-Dl8uV2OZAxfHfWyC7K0Y3HtT7ld+WGLbXIwgCgAGziOMGDIa/bepCRrCxy7IxsEzqY1HOrq/VDdcoNN7hBTSQQ==", "license": "MIT", "dependencies": { - "@posthog/core": "^1.54.2", - "@posthog/types": "^1.412.1" + "@posthog/core": "^1.55.3", + "@posthog/types": "^1.413.0" } }, "node_modules/@posthog/core": { - "version": "1.55.2", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.55.2.tgz", - "integrity": "sha512-2mYxGbDmTLUs096VbcZqSWo0lrI48/BrX65r54NynxxFq409BOx9w73HxOAtOKZ1vblkdV6/GxQTe7KDcIQidg==", + "version": "1.55.3", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.55.3.tgz", + "integrity": "sha512-XnmG9jo7WlxTeN4oCNWH4ESLQHR6ZPZ+mcq+8JxU62k5i1BAe3BSrybnmXHjqKp7e3rUvLK2bOzGZGSlfHvlNA==", "license": "MIT", "dependencies": { - "@posthog/types": "^1.412.4" + "@posthog/types": "^1.413.0" } }, "node_modules/@posthog/types": { - "version": "1.412.4", - "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.412.4.tgz", - "integrity": "sha512-Q7lV9O9TbLngjOYw1ucm3bS3tn48xb1B4ZQdDy8gv7yfKe99hCTeWYG4xZ36nJM80VxpTF9TriiJt5hngDOkBg==", + "version": "1.413.0", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.413.0.tgz", + "integrity": "sha512-tYAVzNojWckuG/wxBB0fwE70sFruQQq4xbhkci7eH2ARU3mMfUTkvpN1HwBMpm0AtbxidLiH0IFvagmdJ8opPw==", "license": "MIT" }, "node_modules/@rolldown/pluginutils": { @@ -1530,21 +1530,23 @@ "dev": true }, "node_modules/@types/react": { - "version": "19.2.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", - "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", "devOptional": true, + "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", "dev": true, + "license": "MIT", "peerDependencies": { - "@types/react": "^19.2.0" + "@types/react": "^19.3.0" } }, "node_modules/@types/trusted-types": { @@ -1795,9 +1797,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.24", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz", - "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "version": "10.6.1", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz", + "integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==", "dev": true, "funding": [ { @@ -1813,9 +1815,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.28.1", - "caniuse-lite": "^1.0.30001766", + "browserslist": "^4.28.9", + "caniuse-lite": "^1.0.30001810", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -2286,10 +2289,11 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", @@ -2301,16 +2305,17 @@ "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz", + "integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==", "dev": true, + "license": "MIT", "peerDependencies": { - "eslint": ">=8.40" + "eslint": "^9 || ^10" } }, "node_modules/eslint-scope": { @@ -3356,14 +3361,14 @@ "dev": true }, "node_modules/posthog-js": { - "version": "1.434.12", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.434.12.tgz", - "integrity": "sha512-gD7Xv8/iuSGiYYQHB9fxnfTsiPvi+WNJiEW9OXnZ/UFS9ZVkOzMVmEq29/NFLeYebVGQpVCBh0H4xbK1sJ7j5g==", + "version": "1.435.6", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.435.6.tgz", + "integrity": "sha512-PdBZw1Np4Ch2ZzvQ/Ij0CTBrJSkSeEmDJsyJfbaipTYH8XJKJk45BYLtNgL4z9DlevRcYJxhwb15VrDzbw7shA==", "license": "(Apache-2.0 AND MIT)", "dependencies": { - "@posthog/browser-common": "^0.9.0", - "@posthog/core": "^1.55.2", - "@posthog/types": "^1.412.4", + "@posthog/browser-common": "^0.9.1", + "@posthog/core": "^1.55.3", + "@posthog/types": "^1.413.0", "core-js": "^3.49.0", "dompurify": "^3.4.13", "fflate": "^0.4.8", @@ -3448,22 +3453,24 @@ ] }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", "dependencies": { - "scheduler": "^0.27.0" + "scheduler": "^0.28.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.3.0" } }, "node_modules/react-refresh": { @@ -3618,9 +3625,10 @@ } }, "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", diff --git a/frontend/package.json b/frontend/package.json index 5de3475..3abb110 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,19 +13,19 @@ }, "dependencies": { "lucide-react": "^0.564.0", - "posthog-js": "^1.353.0", - "react": "^19.2.0", - "react-dom": "^19.2.0" + "posthog-js": "^1.435.6", + "react": "^19.3.0", + "react-dom": "^19.3.0" }, "devDependencies": { "@eslint/js": "^9.39.1", - "@types/react": "^19.2.7", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.3.0", + "@types/react-dom": "^19.3.0", "@vitejs/plugin-react": "^5.1.1", - "autoprefixer": "^10.4.24", + "autoprefixer": "^10.6.1", "eslint": "^9.39.1", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.7", "globals": "^16.5.0", "postcss": "^8.5.6", "tailwindcss": "^3.4.19", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4b29553..e58af4c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -436,6 +436,8 @@ function App() { useEffect(() => { setBooting(tree.bootState === 'loading') }, [tree.bootState]) // Arbre partagé d'où l'on vient quand on ouvre l'écran compte : on y revient après connexion/création const pendingSharedTreeRef = useRef(null) + // Son nom, affiché par l'écran compte : un ref lu au rendu ne redessine rien quand il change + const [pendingSharedTreeName, setPendingSharedTreeName] = useState('') // Écran de compte ouvert depuis un arbre affiché : la croix y ramène const [accountFromTree, setAccountFromTree] = useState(false) const refreshTreeAndKeepSelection = useCallback(async (personId, options = {}) => { @@ -2064,6 +2066,7 @@ function App() { treeOwnerEmail: ctx.treeOwnerEmail, } : null + setPendingSharedTreeName(pendingSharedTreeRef.current?.treeName || '') setTreeNotFound(false) tree.setGateError('') auth.setAccountError('') @@ -2130,6 +2133,7 @@ function App() { return true } finally { pendingSharedTreeRef.current = null + setPendingSharedTreeName('') } } @@ -2456,7 +2460,7 @@ function App() { trees={auth.accountTrees} activeTreeId={tree.treeContext.treeId} defaultMode={accountEntryMode} - pendingTreeName={pendingSharedTreeRef.current?.treeName || ''} + pendingTreeName={pendingSharedTreeName} onLogin={handleLogin} onRegister={handleRegister} onOpenTree={handleOpenTreeFromAccount} diff --git a/frontend/src/components/Galaxy.jsx b/frontend/src/components/Galaxy.jsx index 18f3c23..816ce80 100644 --- a/frontend/src/components/Galaxy.jsx +++ b/frontend/src/components/Galaxy.jsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useRef, useState, useCallback } from 'react' +import { memo, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react' import { getPersonMedias, getParents, getChildren, getPersonUnions } from '../data/mockData' import { PERSON_R, FLOAT_SPEED, FLOAT_AMPLITUDE, REPULSION_RADIUS, REPULSION_STRENGTH, MAX_ORBIT_MEDIAS, ENTRANCE_GEN_DELAY, ENTRANCE_NODE_DURATION, ENTRANCE_NODE_STAGGER, ENTRANCE_MAX_GEN_SPAN, ENTRANCE_MAX_NODE_SPAN, ENTRANCE_LINE_DELAY, ENTRANCE_LINE_DURATION, ENTRANCE_ORBIT_DELAY, ENTRANCE_ORBIT_DURATION, ENTRANCE_ORBIT_STAGGER } from './galaxy/constants' import { easeOutCubic } from './galaxy/utils' @@ -33,7 +33,8 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig const canvasRef = useRef(null) // Lu au calcul du cadrage seulement : changer de personne de départ ne recadre pas une vue en cours const focusPersonIdRef = useRef(focusPersonId) - focusPersonIdRef.current = focusPersonId + // Mis à jour hors du rendu, avant l'effet de cadrage déclaré plus bas (même phase, ordre de déclaration) + useEffect(() => { focusPersonIdRef.current = focusPersonId }, [focusPersonId]) const layoutDataRef = useRef(null) const [layoutReady, setLayoutReady] = useState(false) const transformRef = useRef({ x: 0, y: 0, scale: 1 }) @@ -118,48 +119,46 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig maxScale: MAX_SCALE, }) - // Expose zoom API to parent (ZoomControl component) - if (zoomApiRef) { - zoomApiRef.current = { - zoomTo: (newScale) => { - const canvas = canvasRef.current - if (!canvas) return - const rect = canvas.getBoundingClientRect() - const cx = rect.width / 2 - const cy = rect.height / 2 - const base = targetTransformRef.current - const clamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale)) - const ratio = clamped / base.scale - targetTransformRef.current = { - scale: clamped, - x: cx - ratio * (cx - base.x), - y: cy - ratio * (cy - base.y), - } - }, - resetZoom: () => { - if (initialTransformRef.current) { - targetTransformRef.current = { ...initialTransformRef.current } - } - }, - // Recherche : glisse jusqu'à la personne, dans la partie de l'écran que la fiche ne couvre pas - // (fiche à gauche sur ordinateur, 408 px ; par le bas sur téléphone) - focusPerson: (personId) => { - const canvas = canvasRef.current - const node = layoutDataRef.current?.children.find(n => n.id === `p-${personId}`) - if (!canvas || !node) return - const rect = canvas.getBoundingClientRect() - const pos = getAnimatedPos(node, performance.now()) - const scale = Math.min(MAX_SCALE, Math.max(targetTransformRef.current.scale, 1)) - const phone = rect.width <= 768 - const screenX = phone ? rect.width / 2 : (408 + rect.width) / 2 - const screenY = phone ? rect.height * 0.22 : rect.height / 2 - targetTransformRef.current = { scale, x: screenX - pos.cx * scale, y: screenY - pos.cy * scale } - }, - getScale: () => transformRef.current.scale, - MIN_SCALE, - MAX_SCALE, - } - } + // Expose zoom API to parent (ZoomControl component), posée au commit et non pendant le rendu + useImperativeHandle(zoomApiRef, () => ({ + zoomTo: (newScale) => { + const canvas = canvasRef.current + if (!canvas) return + const rect = canvas.getBoundingClientRect() + const cx = rect.width / 2 + const cy = rect.height / 2 + const base = targetTransformRef.current + const clamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale)) + const ratio = clamped / base.scale + targetTransformRef.current = { + scale: clamped, + x: cx - ratio * (cx - base.x), + y: cy - ratio * (cy - base.y), + } + }, + resetZoom: () => { + if (initialTransformRef.current) { + targetTransformRef.current = { ...initialTransformRef.current } + } + }, + // Recherche : glisse jusqu'à la personne, dans la partie de l'écran que la fiche ne couvre pas + // (fiche à gauche sur ordinateur, 408 px ; par le bas sur téléphone) + focusPerson: (personId) => { + const canvas = canvasRef.current + const node = layoutDataRef.current?.children.find(n => n.id === `p-${personId}`) + if (!canvas || !node) return + const rect = canvas.getBoundingClientRect() + const pos = getAnimatedPos(node, performance.now()) + const scale = Math.min(MAX_SCALE, Math.max(targetTransformRef.current.scale, 1)) + const phone = rect.width <= 768 + const screenX = phone ? rect.width / 2 : (408 + rect.width) / 2 + const screenY = phone ? rect.height * 0.22 : rect.height / 2 + targetTransformRef.current = { scale, x: screenX - pos.cx * scale, y: screenY - pos.cy * scale } + }, + getScale: () => transformRef.current.scale, + MIN_SCALE, + MAX_SCALE, + }), [getAnimatedPos]) // Zoom util (used by pinch) const zoomAt = useCallback((factor, center) => { diff --git a/frontend/src/hooks/useTreeAccess.js b/frontend/src/hooks/useTreeAccess.js index 216a138..28fb03d 100644 --- a/frontend/src/hooks/useTreeAccess.js +++ b/frontend/src/hooks/useTreeAccess.js @@ -52,8 +52,9 @@ export function useTreeAccess() { // 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). - const avatarCacheBustRef = useRef(Date.now()) + // 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) @@ -70,7 +71,7 @@ export function useTreeAccess() { loadGraphData(graphPayload.graph, { mediaToken: graphPayload.mediaToken, apiBaseUrl: API_BASE_URL, - avatarCacheBust: options.avatarCacheBust ?? avatarCacheBustRef.current, + avatarCacheBust: options.avatarCacheBust ?? (avatarCacheBustRef.current ??= Date.now()), }) if (options.avatarCacheBust) avatarCacheBustRef.current = options.avatarCacheBust setAssetRevision((current) => current + 1)