refactor: move lib-root files to lib/shared/ and finalize domain structure
- Move api.server.ts, errors.ts, types.ts, utils.ts, relativeTime.ts to lib/shared/ - Move person relationship components to lib/person/relationship/ - Move Stammbaum components to lib/person/genealogy/ - Move HelpPopover to lib/shared/primitives/ - Update all import paths across routes, specs, and lib files - Update vi.mock() paths in server-project test files - Remove now-empty legacy directories (components/, hooks/, server/, etc.) - Update vite.config.ts coverage include paths for new structure - Update frontend/CLAUDE.md to reflect domain-based lib/ layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
174
frontend/src/lib/person/genealogy/StammbaumCard.svelte
Normal file
174
frontend/src/lib/person/genealogy/StammbaumCard.svelte
Normal file
@@ -0,0 +1,174 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import RelationshipChip from '$lib/person/relationship/RelationshipChip.svelte';
|
||||
import AddRelationshipForm from '$lib/person/relationship/AddRelationshipForm.svelte';
|
||||
import { chipLabel, otherName, inferredRelationshipLabel } from '$lib/person/relationshipLabels';
|
||||
import type { components } from '$lib/generated/api';
|
||||
|
||||
type RelationshipDTO = components['schemas']['RelationshipDTO'];
|
||||
type InferredRelationshipWithPersonDTO = components['schemas']['InferredRelationshipWithPersonDTO'];
|
||||
|
||||
interface Props {
|
||||
personId: string;
|
||||
familyMember: boolean;
|
||||
relationships: RelationshipDTO[];
|
||||
inferredRelationships: InferredRelationshipWithPersonDTO[];
|
||||
canWrite: boolean;
|
||||
relationshipError?: string | null;
|
||||
}
|
||||
|
||||
let {
|
||||
personId,
|
||||
familyMember,
|
||||
relationships,
|
||||
inferredRelationships,
|
||||
canWrite,
|
||||
relationshipError = null
|
||||
}: Props = $props();
|
||||
|
||||
type RelationType = NonNullable<RelationshipDTO['relationType']>;
|
||||
|
||||
const sortedDirect = $derived([...relationships].sort(byTypeThenYear));
|
||||
const topDerived = $derived(inferredRelationships.slice(0, 5));
|
||||
|
||||
function byTypeThenYear(a: RelationshipDTO, b: RelationshipDTO): number {
|
||||
const order = relationTypeOrder(a.relationType) - relationTypeOrder(b.relationType);
|
||||
if (order !== 0) return order;
|
||||
return (a.fromYear ?? 0) - (b.fromYear ?? 0);
|
||||
}
|
||||
|
||||
function relationTypeOrder(t: RelationType | undefined): number {
|
||||
const order: Record<string, number> = {
|
||||
PARENT_OF: 1,
|
||||
SPOUSE_OF: 2,
|
||||
SIBLING_OF: 3,
|
||||
FRIEND: 4,
|
||||
COLLEAGUE: 5,
|
||||
EMPLOYER: 6,
|
||||
DOCTOR: 7,
|
||||
NEIGHBOR: 8,
|
||||
OTHER: 9
|
||||
};
|
||||
return order[t ?? 'OTHER'] ?? 99;
|
||||
}
|
||||
|
||||
function yearRange(rel: RelationshipDTO): string {
|
||||
const from = rel.fromYear;
|
||||
const to = rel.toYear;
|
||||
if (from && to) return `${from}–${to}`;
|
||||
if (from) return m.relation_year_from({ year: from });
|
||||
if (to) return m.relation_year_to({ year: to });
|
||||
return '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-6 rounded-sm border border-line bg-surface p-6 shadow-sm">
|
||||
<!-- Header row: heading + family-member toggle -->
|
||||
<div class="mb-5 flex items-start justify-between gap-4">
|
||||
<h2 class="text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.stammbaum_relationships_heading()}
|
||||
</h2>
|
||||
{#if canWrite}
|
||||
<form method="POST" action="?/toggleFamilyMember" use:enhance>
|
||||
<input type="hidden" name="familyMember" value={familyMember ? 'false' : 'true'} />
|
||||
<button
|
||||
type="submit"
|
||||
role="switch"
|
||||
aria-checked={familyMember}
|
||||
aria-label={familyMember
|
||||
? m.relation_toggle_remove_from_tree()
|
||||
: m.relation_toggle_add_to_tree()}
|
||||
class="inline-flex items-center gap-2 font-sans text-xs font-medium text-ink-2 transition-colors hover:text-ink"
|
||||
>
|
||||
<span
|
||||
class="relative inline-block h-4 w-7 rounded-full transition-colors {familyMember
|
||||
? 'bg-primary'
|
||||
: 'bg-line'}"
|
||||
>
|
||||
<span
|
||||
class="absolute top-0.5 left-0.5 inline-block h-3 w-3 rounded-full bg-white transition-transform {familyMember
|
||||
? 'translate-x-3'
|
||||
: ''}"
|
||||
></span>
|
||||
</span>
|
||||
{m.relation_label_family_member()}
|
||||
</button>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if relationshipError}
|
||||
<p class="mb-3 text-sm text-red-700" role="alert">{relationshipError}</p>
|
||||
{/if}
|
||||
|
||||
<!-- In-tree banner -->
|
||||
{#if familyMember}
|
||||
<div
|
||||
class="mb-4 flex items-center justify-between rounded-sm border border-accent/30 bg-accent/10 px-3 py-2"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-accent"></span>
|
||||
<span class="font-sans text-xs font-medium text-ink-2">{m.relation_label_in_tree()}</span>
|
||||
</div>
|
||||
<a
|
||||
href="/stammbaum?focus={personId}"
|
||||
class="font-sans text-xs font-medium text-primary hover:underline"
|
||||
>
|
||||
{m.relation_label_view_in_tree()}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Direkte Beziehungen -->
|
||||
<h3 class="mb-2 text-xs font-bold tracking-widest text-ink-3 uppercase">
|
||||
{m.relation_label_direct()}
|
||||
</h3>
|
||||
{#if sortedDirect.length === 0}
|
||||
<p class="mb-2 text-sm text-ink-2 italic">{m.person_relationships_empty()}</p>
|
||||
{:else}
|
||||
<ul class="mb-2 divide-y divide-line">
|
||||
{#each sortedDirect as rel (rel.id)}
|
||||
<RelationshipChip
|
||||
chipLabel={chipLabel(rel, personId)}
|
||||
otherName={otherName(rel, personId)}
|
||||
yearRange={yearRange(rel)}
|
||||
canWrite={canWrite}
|
||||
relId={rel.id}
|
||||
/>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
{#if canWrite}
|
||||
<AddRelationshipForm personId={personId} />
|
||||
{/if}
|
||||
|
||||
<!-- Abgeleitete Beziehungen -->
|
||||
{#if topDerived.length > 0}
|
||||
<details class="mt-6">
|
||||
<summary
|
||||
class="cursor-pointer text-xs font-bold tracking-widest text-ink-3 uppercase select-none"
|
||||
>
|
||||
{m.relation_label_derived()}
|
||||
</summary>
|
||||
<ul class="mt-2 space-y-2">
|
||||
{#each topDerived as derived (derived.person.id)}
|
||||
<li class="flex items-center gap-2">
|
||||
<span
|
||||
class="inline-flex shrink-0 items-center rounded-full border border-line bg-muted/50 px-2 py-0.5 font-sans text-xs font-bold tracking-widest text-ink-2 uppercase"
|
||||
>
|
||||
{inferredRelationshipLabel(derived.label)}
|
||||
</span>
|
||||
<a
|
||||
href="/persons/{derived.person.id}"
|
||||
class="min-w-0 flex-1 truncate font-serif text-sm text-ink-2 hover:underline"
|
||||
>
|
||||
{derived.person.displayName}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user