Files
familienarchiv/frontend/src/routes/persons/[id]/PersonCard.svelte
Marcel 446611e3cc feat(person): add a curator "Ereignis für diese Person" link to PersonCard
A curator on a person's page can now seed a timeline event from that
person: a gated link to #781's /zeitstrahl/events/new?personId={id},
which prefills the person and returns to /persons/{id} on save. Hidden
for a Reader. New i18n key person_add_event in de/en/es.

Refs #842
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 08:16:10 +02:00

158 lines
5.0 KiB
Svelte
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.
<script lang="ts">
import { m } from '$lib/paraglide/messages.js';
import { formatLifeDate } from '$lib/person/personLifeDates';
import PersonTypeBadge from '$lib/person/PersonTypeBadge.svelte';
import type { DatePrecision } from '$lib/shared/utils/documentDate';
let {
person,
canWrite
}: {
person: {
id: string;
firstName?: string | null;
lastName: string;
displayName: string;
personType?: string | null;
title?: string | null;
alias?: string | null;
birthDate?: string | null;
birthDatePrecision?: DatePrecision | null;
deathDate?: string | null;
deathDatePrecision?: DatePrecision | null;
notes?: string | null;
};
canWrite: boolean;
} = $props();
const birthText = $derived(formatLifeDate(person.birthDate, person.birthDatePrecision));
const deathText = $derived(formatLifeDate(person.deathDate, person.deathDatePrecision));
</script>
<div class="overflow-hidden rounded-sm border border-line bg-surface shadow-sm">
<!-- Navy top accent bar -->
<div class="h-1.5 w-full bg-primary"></div>
<div class="p-6">
<!-- Avatar — navy circle, centered -->
<div class="mb-4 flex justify-center">
<div
class="flex h-16 w-16 items-center justify-center rounded-full bg-primary font-serif text-xl font-bold text-primary-fg"
>
{#if person.personType && person.personType !== 'PERSON'}
<svg
class="h-7 w-7"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
{#if person.personType === 'INSTITUTION'}
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0H5m14 0h2m-2 0v-2M5 21H3m2 0v-2m4-12h2m-2 4h2m4-4h2m-2 4h2"
/>
{:else if person.personType === 'GROUP'}
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"
/>
{:else}
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01"
/>
{/if}
</svg>
{:else}
{person.firstName ? person.firstName[0] : person.lastName[0]}{person.lastName[0]}
{/if}
</div>
</div>
{#if person.personType === 'PERSON' && person.title}
<p
class="mb-0.5 text-center font-sans text-xs tracking-widest text-ink-3 [font-variant:small-caps]"
>
{person.title}
</p>
{/if}
<!-- Name — centered, serif -->
<h1 class="mb-1 text-center font-serif text-xl font-bold text-ink">
{person.displayName}
</h1>
{#if person.personType && person.personType !== 'PERSON'}
<div class="mb-1 flex justify-center">
<PersonTypeBadge personType={person.personType} />
</div>
{/if}
<!-- Alias — centered, italic -->
{#if person.alias}
<p class="mb-1 text-center font-sans text-sm text-ink-2 italic">{person.alias}"</p>
{/if}
<!-- Life dates — centered; glyphs aria-hidden so screen readers only hear the dates -->
{#if birthText || deathText}
<p class="mb-4 text-center font-sans text-sm text-ink-3">
{#if birthText}
<span aria-hidden="true">*</span> {birthText}
{/if}
{#if birthText && deathText}{/if}
{#if deathText}
<span aria-hidden="true"></span> {deathText}
{/if}
</p>
{:else}
<div class="mb-4"></div>
{/if}
<!-- Notes -->
{#if person.notes}
<div class="mb-4">
<span
class="mb-1 block font-sans text-[10px] font-bold tracking-widest text-ink-3 uppercase"
>
{m.person_label_notes()}
</span>
<p
class="rounded border border-line bg-muted p-3 font-sans text-sm leading-relaxed text-ink-2"
>
{person.notes}
</p>
</div>
{/if}
<!-- Curator actions — full width, outlined. Both links are gated to
WRITE_ALL; the gate is UX only (the #781 route guard is the boundary). -->
{#if canWrite}
<a
href="/persons/{person.id}/edit"
class="flex w-full items-center justify-center gap-1.5 rounded border border-line px-3 py-2 font-sans text-xs font-bold tracking-widest text-ink-2 uppercase transition-colors hover:border-primary hover:text-ink"
>
<img
src="/degruyter-icons/Simple/Small-16px/SVG/Action/Edit-Content-SM.svg"
alt=""
aria-hidden="true"
class="h-3.5 w-3.5"
/>
{m.btn_edit()}
</a>
<!-- Opens #781's create form pre-seeded with this person; on save it
returns to /persons/{id} (originPersonId). #842 REQ-003/010. -->
<a
data-testid="person-add-event"
href="/zeitstrahl/events/new?personId={person.id}"
class="mt-2 flex min-h-[44px] w-full items-center justify-center gap-1.5 rounded border border-line px-3 py-2 font-sans text-xs font-bold tracking-widest text-ink-2 uppercase transition-colors hover:border-primary hover:text-ink"
>
{m.person_add_event()}
</a>
{/if}
</div>
</div>