diff --git a/frontend/src/lib/components/DashboardActivityFeed.svelte b/frontend/src/lib/components/DashboardActivityFeed.svelte new file mode 100644 index 00000000..f11691b0 --- /dev/null +++ b/frontend/src/lib/components/DashboardActivityFeed.svelte @@ -0,0 +1,81 @@ + + +
+
+

+ {m.feed_caption()} +

+ Alle → +
+ + {#if feed.length > 0} + + {/if} +
diff --git a/frontend/src/lib/components/DashboardActivityFeed.svelte.spec.ts b/frontend/src/lib/components/DashboardActivityFeed.svelte.spec.ts new file mode 100644 index 00000000..b12c682c --- /dev/null +++ b/frontend/src/lib/components/DashboardActivityFeed.svelte.spec.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +import DashboardActivityFeed from './DashboardActivityFeed.svelte'; +import type { components } from '$lib/generated/api'; + +type ActivityFeedItemDTO = components['schemas']['ActivityFeedItemDTO']; + +afterEach(() => { + cleanup(); +}); + +const baseItem: ActivityFeedItemDTO = { + kind: 'TEXT_SAVED', + actor: { initials: 'MR', color: '#7a4f9a', name: 'Max Raddatz' }, + documentId: 'doc-1', + documentTitle: 'Brief 1920', + happenedAt: '2026-04-19T10:00:00Z', + youMentioned: false +}; + +describe('DashboardActivityFeed', () => { + it('renders "für dich" badge when youMentioned is true', async () => { + const item: ActivityFeedItemDTO = { ...baseItem, kind: 'MENTION_CREATED', youMentioned: true }; + render(DashboardActivityFeed, { feed: [item] }); + const badge = page.getByText('für dich'); + await expect.element(badge).toBeInTheDocument(); + }); + + it('does not render "für dich" badge when youMentioned is false', async () => { + render(DashboardActivityFeed, { feed: [baseItem] }); + const badge = page.getByText('für dich'); + await expect.element(badge).not.toBeInTheDocument(); + }); + + it('renders empty state when feed is empty', async () => { + render(DashboardActivityFeed, { feed: [] }); + const section = page.getByText('Kommentare & Aktivität'); + await expect.element(section).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/lib/components/DashboardFamilyPulse.svelte b/frontend/src/lib/components/DashboardFamilyPulse.svelte new file mode 100644 index 00000000..20c50268 --- /dev/null +++ b/frontend/src/lib/components/DashboardFamilyPulse.svelte @@ -0,0 +1,70 @@ + + +{#if pulse !== null} +
+

+ {m.pulse_eyebrow()} +

+ + {#if pulse.pages > 0} +

+ Ihr habt {pulse.pages} Seiten bearbeitet. +

+ {/if} + + {#if pulse.yourPages > 0} +

+ Du selbst hast {pulse.yourPages} davon bearbeitet. +

+ {/if} + + {#if pulse.contributors.length > 0} +
+

{m.pulse_contributors()}

+ {#each pulse.contributors as c (c.initials)} + {c.initials} + {/each} +
+ {/if} + +
+
+ {pulse.annotated} + + {m.pulse_transcribed()} + +
+
+ {pulse.transcribed} + + {m.pulse_reviewed()} + +
+
+ {pulse.uploaded} + + {m.pulse_uploaded()} + +
+
+
+{/if} diff --git a/frontend/src/lib/components/DashboardResumeStrip.svelte b/frontend/src/lib/components/DashboardResumeStrip.svelte index 1e7e9e31..ef18cb0d 100644 --- a/frontend/src/lib/components/DashboardResumeStrip.svelte +++ b/frontend/src/lib/components/DashboardResumeStrip.svelte @@ -1,37 +1,115 @@ -{#if lastVisited} +{#if resumeDoc === null}
- {m.dashboard_resume_label()} - - {lastVisited.title || m.dashboard_resume_fallback()} + + + + +

{m.dashboard_empty_title()}

+

{m.dashboard_empty_body()}

+
+ {m.dashboard_empty_cta()}
+{:else} +
+ + +
+

+ + {m.dashboard_resume_label()} + · + {m.dashboard_page_of({ page: resumeDoc.page, pages: resumeDoc.pages })} +

+ +

{resumeDoc.title}

+ +

{resumeDoc.caption}

+ +
+ {resumeDoc.excerpt} +
+ +
+ {resumeDoc.pct}% +
+
+
+ {#each resumeDoc.collaborators.slice(0, 3) as collab (collab.initials)} + {collab.initials} + {/each} +
+ + +
+
{/if} diff --git a/frontend/src/lib/components/DashboardResumeStrip.svelte.spec.ts b/frontend/src/lib/components/DashboardResumeStrip.svelte.spec.ts index 2fac46b0..ec241bf5 100644 --- a/frontend/src/lib/components/DashboardResumeStrip.svelte.spec.ts +++ b/frontend/src/lib/components/DashboardResumeStrip.svelte.spec.ts @@ -3,48 +3,48 @@ import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import DashboardResumeStrip from './DashboardResumeStrip.svelte'; +import type { components } from '$lib/generated/api'; + +type DashboardResumeDTO = components['schemas']['DashboardResumeDTO']; afterEach(() => { cleanup(); - localStorage.clear(); }); +const mockResume: DashboardResumeDTO = { + documentId: 'doc-123', + title: 'Geburtsurkunde 1920', + caption: 'Max Mustermann · 1920-01-01', + excerpt: 'Hiermit wird beurkundet…', + page: 1, + pages: 4, + pct: 75, + collaborators: [] +}; + describe('DashboardResumeStrip', () => { - it('renders nothing when no last-visited document in localStorage', async () => { - render(DashboardResumeStrip, {}); - const strip = page.getByTestId('resume-strip'); - await expect.element(strip).not.toBeInTheDocument(); + it('renders empty state heading when resumeDoc is null', async () => { + render(DashboardResumeStrip, { resumeDoc: null }); + const heading = page.getByRole('heading', { name: /Noch kein Dokument begonnen/i }); + await expect.element(heading).toBeInTheDocument(); }); - it('shows the strip with link when localStorage has a document', async () => { - localStorage.setItem( - 'familienarchiv.lastVisited', - JSON.stringify({ id: 'doc-123', title: 'Geburtsurkunde 1920' }) - ); - render(DashboardResumeStrip, {}); - const strip = page.getByTestId('resume-strip'); - await expect.element(strip).toBeInTheDocument(); - const link = page.getByRole('link', { name: /Geburtsurkunde 1920/ }); - await expect.element(link).toBeInTheDocument(); + it('renders progressbar with correct aria-valuenow when resumeDoc is provided', async () => { + render(DashboardResumeStrip, { resumeDoc: mockResume }); + const bar = page.getByRole('progressbar'); + await expect.element(bar).toBeInTheDocument(); + await expect.element(bar).toHaveAttribute('aria-valuenow', '75'); + }); + + it('shows document title when resumeDoc is provided', async () => { + render(DashboardResumeStrip, { resumeDoc: mockResume }); + const title = page.getByRole('heading', { name: /Geburtsurkunde 1920/i }); + await expect.element(title).toBeInTheDocument(); + }); + + it('links to the document for the CTA', async () => { + render(DashboardResumeStrip, { resumeDoc: mockResume }); + const link = page.getByRole('link', { name: /Weitertranskribieren/i }); await expect.element(link).toHaveAttribute('href', '/documents/doc-123'); }); - - it('uses title fallback text when title is empty', async () => { - localStorage.setItem( - 'familienarchiv.lastVisited', - JSON.stringify({ id: 'doc-456', title: '' }) - ); - render(DashboardResumeStrip, {}); - const strip = page.getByTestId('resume-strip'); - await expect.element(strip).toBeInTheDocument(); - const link = page.getByRole('link'); - await expect.element(link).toHaveAttribute('href', '/documents/doc-456'); - }); - - it('renders nothing when localStorage contains malformed JSON', async () => { - localStorage.setItem('familienarchiv.lastVisited', '{not valid json'); - render(DashboardResumeStrip, {}); - const strip = page.getByTestId('resume-strip'); - await expect.element(strip).not.toBeInTheDocument(); - }); }); diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 3aada446..0469053e 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -28,7 +28,9 @@ onMount(() => { }); const isAuthPage = $derived( - ['/login', '/forgot-password', '/reset-password'].some((p) => page.url.pathname.startsWith(p)) + ['/login', '/register', '/forgot-password', '/reset-password'].some((p) => + page.url.pathname.startsWith(p) + ) ); const userInitials = $derived.by(() => { @@ -50,6 +52,30 @@ const userInitials = $derived.by(() => {
+ {#if data?.user} + + + Hochladen + + {/if}