diff --git a/CHANGELOG.md b/CHANGELOG.md index 00f740b..2773c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,15 @@ Notable changes to Parenthèse. Dates are in YYYY-MM-DD format. ### Search for someone elsewhere -- "Someone elsewhere" in the "+" menu opens a panel on the right of the tree: type a name, birth year and place, and search public records. Links to Gallica (old newspapers), FranceArchives (military registers), Mémoire des hommes, Léonore, Geneanet, Open Archives, WikiTree, FreeBMD and FamilySearch open in the browser with the name typed in when the site allows it. +- "Someone elsewhere" in the bottom-right menu opens a panel on the right of the tree: type a name, birth year and place, and search public records. Links to Gallica (old newspapers), FranceArchives (military registers), Mémoire des hommes, Léonore, Geneanet, Open Archives, WikiTree, FreeBMD and FamilySearch open in the browser with the name typed in when the site allows it. - The server can query deaths in France since 1970 (INSEE records via matchID) and WikiTree, once listed in `SEARCH_SOURCES`. Each source says what it covers and what is sent. The owner adds a result to a person in the tree: empty fields only, marked as needing checking, source kept in the note. +### Trees + +- A tree can have an icon, picked from fifteen emoji in Settings > Your tree. It replaces the first letter in the tree menu, on the account page and on the invitation page. +- For whoever manages the tree, the button at the bottom right now reads Edit, with a pencil. The family keeps Add. +- A tree name starting with an emoji no longer shows a broken letter in the menus. + ### Fixes - An admin can switch a person's profile to edit mode from a new Edit button on the profile. diff --git a/backend/prisma/migrations/20261025_002_icone_arbre/migration.sql b/backend/prisma/migrations/20261025_002_icone_arbre/migration.sql new file mode 100644 index 0000000..a8953e0 --- /dev/null +++ b/backend/prisma/migrations/20261025_002_icone_arbre/migration.sql @@ -0,0 +1,2 @@ +-- Icône d'un arbre (un emoji), facultative. Migration additive : colonne vide par défaut. +ALTER TABLE "trees" ADD COLUMN "icon" TEXT; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 6d7fdaa..581350e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -126,6 +126,8 @@ model Tree { slug String @unique name String description String? + // Icône choisie pour l'arbre (un emoji), à la place de la première lettre dans les menus ; null = la lettre + icon String? rootPersonId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/backend/src/lib/tree-invitations.ts b/backend/src/lib/tree-invitations.ts index 4c4a986..376545f 100644 --- a/backend/src/lib/tree-invitations.ts +++ b/backend/src/lib/tree-invitations.ts @@ -37,7 +37,7 @@ export async function findPendingInvitationByToken(db: Db, token: string) { if (!token || token.length > 200) return null return db.treeInvitation.findFirst({ where: { tokenHash: hashInvitationToken(token), ...pendingInvitationWhere() }, - include: { tree: { select: { id: true, name: true, ownerUserId: true, owner: { select: { firstName: true } } } } }, + include: { tree: { select: { id: true, name: true, icon: true, ownerUserId: true, owner: { select: { firstName: true } } } } }, }) } diff --git a/backend/src/routes/managers.ts b/backend/src/routes/managers.ts index 1f88080..94ae651 100644 --- a/backend/src/routes/managers.ts +++ b/backend/src/routes/managers.ts @@ -210,6 +210,7 @@ export const managerRoutes: FastifyPluginAsync = async (app) => { } return reply.send({ treeName: invitation.tree.name, + treeIcon: invitation.tree.icon, invitedBy: invitation.tree.owner.firstName || null, emailHint: maskEmail(invitation.email), expiresAt: invitation.expiresAt, diff --git a/backend/src/routes/trees.ts b/backend/src/routes/trees.ts index b3f4009..e461f31 100644 --- a/backend/src/routes/trees.ts +++ b/backend/src/routes/trees.ts @@ -40,6 +40,8 @@ const patchTreeSchema = z slug: z.string().min(1).max(128).optional(), description: z.string().max(500).optional().nullable(), rootPersonId: z.string().min(1).optional().nullable(), + // Un emoji (graphème composé compris), ou null pour revenir à la première lettre + icon: z.string().trim().max(16).regex(/^\p{Extended_Pictographic}/u).optional().nullable(), }) .refine((value) => Object.keys(value).length > 0, { message: 'at_least_one_field_required', @@ -392,6 +394,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { slug: true, name: true, description: true, + icon: true, rootPersonId: true, createdAt: true, updatedAt: true, @@ -413,6 +416,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { slug: true, name: true, description: true, + icon: true, rootPersonId: true, createdAt: true, updatedAt: true, @@ -442,6 +446,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { slug: string name: string description: string | null + icon: string | null rootPersonId: string | null createdAt: Date updatedAt: Date @@ -665,6 +670,7 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { name?: string slug?: string description?: string | null + icon?: string | null rootPersonId?: string | null } = {} @@ -693,6 +699,10 @@ export const treeRoutes: FastifyPluginAsync = async (app) => { updateData.description = payload.data.description } + if (payload.data.icon !== undefined) { + updateData.icon = payload.data.icon + } + if (payload.data.rootPersonId !== undefined) { if (payload.data.rootPersonId === null) { updateData.rootPersonId = null diff --git a/backend/test/co-gestion.db.test.ts b/backend/test/co-gestion.db.test.ts index a73b778..14e2435 100644 --- a/backend/test/co-gestion.db.test.ts +++ b/backend/test/co-gestion.db.test.ts @@ -2,7 +2,7 @@ import type { FastifyInstance } from 'fastify' import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' // Co-gestion d'un arbre (#145), sur une vraie base : une fonction de droits se vérifie avec de vraies requêtes, -// pas avec des mocks. Lancé seulement avec TEST_DATABASE_URL (base jetable, migrée) : +// pas avec des mocks. Lancé seulement avec TEST_DATABASE_URL (base jetable, migrée, en UTF-8 pour les emoji) : // TEST_DATABASE_URL=postgresql://postgres:postgres@localhost:5432/genealogy_test npx vitest run test/co-gestion.db.test.ts const TEST_DB = process.env.TEST_DATABASE_URL @@ -206,6 +206,14 @@ describe.skipIf(!TEST_DB)('co-gestion, vraie base', () => { expect((await call('GET', `/trees/${treeId}/graph`, co.token)).status).toBe(403) }) + it("l'icône de l'arbre : un emoji ou rien, posée par un gestionnaire, rendue dans la liste des arbres", async () => { + expect((await call('PATCH', `/trees/${treeId}`, owner.token, { icon: 'abc' })).status).toBe(400) + expect((await call('PATCH', `/trees/${treeId}`, owner.token, { icon: '🌳' })).status).toBe(200) + const list = await call('GET', '/trees', owner.token) + expect(list.body.trees.find((t: { id: string }) => t.id === treeId).icon).toBe('🌳') + expect((await call('PATCH', `/trees/${treeId}`, owner.token, { icon: null })).status).toBe(200) + }) + describe('OIDC', () => { let address = 0 async function oidcLogin(identity: { subject: string; email: string; emailVerified: boolean }) { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 731f34a..32f39b7 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3404,6 +3404,7 @@ function App() { onTreeDeleted={handleTreeDeleted} onLeftTree={handleTreeDeleted} onOpenShare={() => { setAdminPanelVisible(false); handleOpenContributionPanel() }} + onTreeUpdated={() => { if (auth.userAuth.token) auth.loadAccountTrees(auth.userAuth.token).catch(() => {}) }} onGedcomImported={handleGedcomImported} onOpenPhotoStart={() => { setAdminPanelVisible(false); openPhotoStart() }} onClose={() => setAdminPanelVisible(false)} diff --git a/frontend/src/components/AccountDashboard.jsx b/frontend/src/components/AccountDashboard.jsx index 7f693ec..2ea95b8 100644 --- a/frontend/src/components/AccountDashboard.jsx +++ b/frontend/src/components/AccountDashboard.jsx @@ -279,7 +279,7 @@ const AccountDashboard = ({ className={`tree-item pz-tree-item ${isActive ? 'active' : ''}`} onClick={() => onOpenTree(tree.id)} > - + {tree.name} {isActive && {t('Ouvert')}} diff --git a/frontend/src/components/AccountScreens.css b/frontend/src/components/AccountScreens.css index 249495f..1f3ab0c 100644 --- a/frontend/src/components/AccountScreens.css +++ b/frontend/src/components/AccountScreens.css @@ -66,3 +66,7 @@ .ap-item-name { flex: 1; min-width: 0; font: 400 16px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .ap-item-name em { font-style: italic; font-weight: 300; } .invite-doors { display: grid; gap: 10px; } + +/* Arbre avec une icône choisie : l'emoji seul, sans la pastille de couleur de la lettre */ +.pz-tree-mono--emoji { background: transparent; font-size: 22px; line-height: 1; } +.invite-tree-icon { font-size: 30px; line-height: 1; } diff --git a/frontend/src/components/AdminPanel.jsx b/frontend/src/components/AdminPanel.jsx index c63384a..866f919 100644 --- a/frontend/src/components/AdminPanel.jsx +++ b/frontend/src/components/AdminPanel.jsx @@ -34,6 +34,9 @@ function normalizeSlug(value) { .replace(/^-+|-+$/g, '') } +// Icônes proposées pour un arbre (Patch, 25/10) +const TREE_ICONS = ['🌳', '🌿', '🌻', '🏡', '❤️', '🧡', '⭐', '📖', '🕊️', '🌊', '⛰️', '🍂', '🌸', '🐚', '☀️'] + const AdminPanel = ({ familyTabs = true, onFamilyTabsChange = null, @@ -49,6 +52,7 @@ const AdminPanel = ({ onTreeDeleted, onLeftTree, onOpenShare, + onTreeUpdated, onGedcomImported, onOpenPhotoStart, }) => { @@ -61,6 +65,7 @@ const AdminPanel = ({ const [tree, setTree] = useState(null) const [treeName, setTreeName] = useState('') const [treeDescription, setTreeDescription] = useState('') + const [treeIcon, setTreeIcon] = useState(null) // Contributions de la famille : 'pending' = validées par l'admin, 'direct' = appliquées immédiatement const [contributionPolicy, setContributionPolicy] = useState('pending') const [savingPolicy, setSavingPolicy] = useState(false) @@ -132,6 +137,7 @@ const AdminPanel = ({ setTree(loadedTree) setTreeName(loadedTree?.name || '') setTreeDescription(loadedTree?.description || '') + setTreeIcon(loadedTree?.icon || null) }) .catch(() => { if (!active) return @@ -186,6 +192,7 @@ const AdminPanel = ({ const payload = { name: nextName, description: treeDescription.trim() || null, + icon: treeIcon, } const normalized = normalizeSlug(nextName) @@ -203,7 +210,9 @@ const AdminPanel = ({ setTree(updated) setTreeName(updated?.name || nextName) setTreeDescription(updated?.description || '') + setTreeIcon(updated?.icon || null) setSuccessMessage(t('Modifications enregistrées.')) + onTreeUpdated?.(updated) } catch { setErrorMessage(t("Impossible d'enregistrer les modifications.")) } finally { @@ -456,6 +465,22 @@ const AdminPanel = ({ /> + {/* Icône de l'arbre : un emoji à la place de la première lettre dans les menus et sur la page d'invitation */} +
{t("Elle remplace la première lettre dans vos menus et sur la page d'invitation.")}
+{invitation.invitedBy diff --git a/frontend/src/components/SettingsPanels.css b/frontend/src/components/SettingsPanels.css index 0261340..6938b00 100644 --- a/frontend/src/components/SettingsPanels.css +++ b/frontend/src/components/SettingsPanels.css @@ -56,6 +56,13 @@ .ts-row-actions { justify-content: flex-start; } } +/* Icône de l'arbre : la lettre ou un emoji */ +.ts-icons { display: flex; flex-wrap: wrap; gap: 6px; } +.ts-icon { width: 38px; height: 38px; border-radius: 12px; border: 1px solid var(--pz-line); background: var(--pz-surface); font-size: 19px; line-height: 1; cursor: pointer; } +.ts-icon--letter { width: auto; padding: 0 12px; border-radius: 999px; font: 500 13px var(--pz-sans); color: var(--pz-ink-2); } +.ts-icon[aria-checked="true"] { border-color: var(--pz-a500); background: var(--pz-a50); box-shadow: 0 0 0 3px var(--pz-a100); } +.ts-icon:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; } + /* Cinq onglets : ils rétrécissent (texte sur deux lignes) au lieu de déborder du panneau sur un petit téléphone */ .ts-tabs .pz-tab { min-width: 0; } @media (max-width: 420px) { .ts-tabs .pz-tab { padding: 0 4px; font-size: 12.5px; line-height: 1.2; } } diff --git a/frontend/src/components/navbar/AvatarMenu.css b/frontend/src/components/navbar/AvatarMenu.css index e480cc3..1c3a2e7 100644 --- a/frontend/src/components/navbar/AvatarMenu.css +++ b/frontend/src/components/navbar/AvatarMenu.css @@ -47,3 +47,6 @@ .navm-current:hover { background: transparent !important; } /* Arbre en cours : désactivé pour le clavier et le clic, mais texte à sa couleur normale (pas la mise en retrait de .navm-item:disabled) */ .navm-item.navm-current:disabled { opacity: 1; cursor: default; color: var(--pz-ink); } + +/* Arbre avec une icône choisie : l'emoji seul, sans la pastille de couleur de la lettre */ +.navm-tree-mono--emoji { background: transparent; font-size: 17px; line-height: 1; } diff --git a/frontend/src/components/navbar/AvatarMenu.jsx b/frontend/src/components/navbar/AvatarMenu.jsx index 3d6f206..c96726d 100644 --- a/frontend/src/components/navbar/AvatarMenu.jsx +++ b/frontend/src/components/navbar/AvatarMenu.jsx @@ -104,7 +104,7 @@ function AvatarMenu({ return (