From d40d4b21e11deb81800936725495fa55724c21b8 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:07:46 +0100 Subject: [PATCH 01/11] refactor(utils): consolidate date utilities into \$lib/utils/date.ts Move isoToGerman and germanToIso from utils.ts into utils/date.ts alongside formatDate, and add handleGermanDateInput for the shared date field handler. Make utils.ts a re-export shim so existing imports continue to work. Closes part of #75 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/lib/utils.ts | 21 +---------------- frontend/src/lib/utils/date.ts | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 8b4fe98d..51ca79f9 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -1,20 +1 @@ -/** - * Converts an ISO date string (YYYY-MM-DD) to German display format (DD.MM.YYYY). - * Returns an empty string for invalid or empty input. - */ -export function isoToGerman(iso: string): string { - if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ''; - const [y, m, d] = iso.split('-'); - return `${d}.${m}.${y}`; -} - -/** - * Converts a German date string (DD.MM.YYYY) to ISO format (YYYY-MM-DD). - * Returns an empty string for invalid or empty input. - */ -export function germanToIso(german: string): string { - const match = german.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); - if (!match) return ''; - const [, d, m, y] = match; - return `${y}-${m}-${d}`; -} +export { isoToGerman, germanToIso } from '$lib/utils/date'; diff --git a/frontend/src/lib/utils/date.ts b/frontend/src/lib/utils/date.ts index 9f8b5c54..c2ab4e8e 100644 --- a/frontend/src/lib/utils/date.ts +++ b/frontend/src/lib/utils/date.ts @@ -9,3 +9,44 @@ export function formatDate(isoDate: string): string { year: 'numeric' }).format(new Date(isoDate + 'T12:00:00')); } + +/** + * Converts an ISO date string (YYYY-MM-DD) to German display format (DD.MM.YYYY). + * Returns an empty string for invalid or empty input. + */ +export function isoToGerman(iso: string): string { + if (!iso || !/^\d{4}-\d{2}-\d{2}$/.test(iso)) return ''; + const [y, m, d] = iso.split('-'); + return `${d}.${m}.${y}`; +} + +/** + * Converts a German date string (DD.MM.YYYY) to ISO format (YYYY-MM-DD). + * Returns an empty string for invalid or empty input. + */ +export function germanToIso(german: string): string { + const match = german.match(/^(\d{2})\.(\d{2})\.(\d{4})$/); + if (!match) return ''; + const [, d, m, y] = match; + return `${y}-${m}-${d}`; +} + +/** + * Handles a date input event for German-format date fields (DD.MM.YYYY). + * Strips non-digits, formats with dots, mutates the input's displayed value, + * and returns the display string and its ISO equivalent. + */ +export function handleGermanDateInput(e: Event): { display: string; iso: string } { + const input = e.target as HTMLInputElement; + const digits = input.value.replace(/\D/g, '').slice(0, 8); + let display: string; + if (digits.length <= 2) { + display = digits; + } else if (digits.length <= 4) { + display = `${digits.slice(0, 2)}.${digits.slice(2)}`; + } else { + display = `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`; + } + input.value = display; + return { display, iso: germanToIso(display) }; +} -- 2.49.1 From d46764ef4fdf8a8842f7ac4622cb826afa1cabad Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:10:54 +0100 Subject: [PATCH 02/11] refactor(layout): extract AppNav and UserMenu sub-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split +layout.svelte (205 lines) into: - AppNav.svelte: logo + nav links with active-state styling - UserMenu.svelte: avatar button, dropdown, click-outside handler Layout drops from 205 → 80 lines. Part of #75 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/+layout.svelte | 130 +--------------------------- frontend/src/routes/AppNav.svelte | 59 +++++++++++++ frontend/src/routes/UserMenu.svelte | 81 +++++++++++++++++ 3 files changed, 144 insertions(+), 126 deletions(-) create mode 100644 frontend/src/routes/AppNav.svelte create mode 100644 frontend/src/routes/UserMenu.svelte diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 11f31185..32d85fe3 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,11 +1,11 @@
@@ -59,58 +45,7 @@ function clickOutside(node: HTMLElement) {
- +
@@ -134,64 +69,7 @@ function clickOutside(node: HTMLElement) { -
{ if (e.key === 'Escape') userMenuOpen = false; }} - role="none" - > - {#if userInitials} - - {:else} - - {/if} - - {#if userMenuOpen} - - {/if} -
+
diff --git a/frontend/src/routes/AppNav.svelte b/frontend/src/routes/AppNav.svelte new file mode 100644 index 00000000..49e6888e --- /dev/null +++ b/frontend/src/routes/AppNav.svelte @@ -0,0 +1,59 @@ + + + diff --git a/frontend/src/routes/UserMenu.svelte b/frontend/src/routes/UserMenu.svelte new file mode 100644 index 00000000..cb954c84 --- /dev/null +++ b/frontend/src/routes/UserMenu.svelte @@ -0,0 +1,81 @@ + + +
{ + if (e.key === 'Escape') userMenuOpen = false; + }} + role="none" +> + {#if userInitials} + + {:else} + + {/if} + + {#if userMenuOpen} + + {/if} +
-- 2.49.1 From af59ed4de48938da1287056679829706a56713f6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:13:06 +0100 Subject: [PATCH 03/11] refactor(admin): split admin page into tab sub-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split admin/+page.svelte (573 lines) into: - UsersTab.svelte: user table with delete action - TagsTab.svelte: tag list with inline rename and delete - GroupsTab.svelte: groups table with inline edit + create form - SystemTab.svelte: backfill buttons with own state Page drops from 573 → ~40 lines. Part of #75 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/admin/+page.svelte | 522 +-------------------- frontend/src/routes/admin/GroupsTab.svelte | 221 +++++++++ frontend/src/routes/admin/SystemTab.svelte | 72 +++ frontend/src/routes/admin/TagsTab.svelte | 127 +++++ frontend/src/routes/admin/UsersTab.svelte | 120 +++++ 5 files changed, 552 insertions(+), 510 deletions(-) create mode 100644 frontend/src/routes/admin/GroupsTab.svelte create mode 100644 frontend/src/routes/admin/SystemTab.svelte create mode 100644 frontend/src/routes/admin/TagsTab.svelte create mode 100644 frontend/src/routes/admin/UsersTab.svelte diff --git a/frontend/src/routes/admin/+page.svelte b/frontend/src/routes/admin/+page.svelte index a40ca891..75d458f2 100644 --- a/frontend/src/routes/admin/+page.svelte +++ b/frontend/src/routes/admin/+page.svelte @@ -1,66 +1,14 @@
@@ -107,466 +55,20 @@ async function backfillFileHashes() { {/if} {#if activeTab === 'users'} -
-
-

{m.admin_section_users()}

- - - - - {m.admin_btn_new_user()} - -
- - - - - - - - - - - - {#each data.users as user (user.id)} - - - - - - - {/each} - -
{m.admin_col_login()}{m.admin_col_full_name()}{m.admin_col_groups()}{m.admin_col_actions()}
- {user.username} - - {#if user.firstName || user.lastName} - {user.firstName ?? ''} {user.lastName ?? ''} - {:else} - - {/if} - -
- {#if user.groups && user.groups.length > 0} - {#each user.groups as group (group.id)} - - {group.name} - - {/each} - {:else} - {m.admin_no_groups()} - {/if} -
-
-
- - {m.btn_edit()} - - -
{ - if (!confirm(m.admin_user_delete_confirm({ username: user.username }))) { - cancel(); - } - return async ({ update }) => { - await update(); - }; - }} - class="flex items-center" - > - - -
-
-
+
+
{:else if activeTab === 'tags'} -
-
-

{m.admin_section_tags()}

-

- {m.admin_tags_warning()} -

-
- -
    - {#each data.tags as tag (tag.id)} -
  • - {#if editingTagId === tag.id} -
    - async ({ update }) => { - await update(); - cancelEditTag(); - }} - class="flex flex-1 items-center gap-2" - > - - - - -
    - {:else} - - {tag.name} - -
    - -
    { - if ( - !confirm(m.admin_tag_delete_confirm()) - ) { - cancel(); - } - return async ({ update }) => { - await update(); - }; - }} - class="inline" - > - - -
    -
    - {/if} -
  • - {/each} -
+
+
{:else if activeTab === 'groups'} -
-
-

{m.admin_section_groups()}

-
- - - - - - - - - - - {#each data.groups as group (group.id)} - - {#if editingGroupId === group.id} - - - {:else} - - - - - {/if} - - {/each} - -
{m.admin_col_name()}{m.admin_col_permissions()}{m.admin_col_actions()}
-
- async ({ update }) => { - await update(); - cancelEditGroup(); - }} - class="flex w-full flex-col items-start gap-4 sm:flex-row" - > - - -
- -
- -
- {#each availablePermissions as perm (perm)} - - {/each} -
- -
- - -
-
-
- {group.name} - -
- {#each group.permissions as perm (perm)} - - {perm} - - {/each} -
-
-
- - -
{ - if (!confirm(m.admin_group_delete_confirm())) { - cancel(); - } - return async ({ update }) => { - await update(); - }; - }} - > - - -
-
-
- - -
-

- {m.admin_section_new_group()} -

-
-
- -
- -
- {#each availablePermissions as perm (perm)} - - {/each} -
- - -
-
+
+
{:else if activeTab === 'system'} -
-

{m.admin_system_backfill_heading()}

-

{m.admin_system_backfill_description()}

- - {#if backfillResult !== null} -

- {m.admin_system_backfill_success({ count: backfillResult })} -

- {/if} -
- -
-

- {m.admin_system_backfill_hashes_heading()} -

-

{m.admin_system_backfill_hashes_description()}

- - {#if backfillHashesResult !== null} -

- {m.admin_system_backfill_hashes_success({ count: backfillHashesResult })} -

- {/if} +
+
{/if}
diff --git a/frontend/src/routes/admin/GroupsTab.svelte b/frontend/src/routes/admin/GroupsTab.svelte new file mode 100644 index 00000000..0f96cc22 --- /dev/null +++ b/frontend/src/routes/admin/GroupsTab.svelte @@ -0,0 +1,221 @@ + + +
+
+

{m.admin_section_groups()}

+
+ + + + + + + + + + + {#each groups as group (group.id)} + + {#if editingGroupId === group.id} + + + {:else} + + + + + {/if} + + {/each} + +
{m.admin_col_name()}{m.admin_col_permissions()}{m.admin_col_actions()}
+
+ async ({ update }) => { + await update(); + cancelEditGroup(); + }} + class="flex w-full flex-col items-start gap-4 sm:flex-row" + > + + +
+ +
+ +
+ {#each availablePermissions as perm (perm)} + + {/each} +
+ +
+ + +
+
+
+ {group.name} + +
+ {#each group.permissions as perm (perm)} + + {perm} + + {/each} +
+
+
+ + +
{ + if (!confirm(m.admin_group_delete_confirm())) { + cancel(); + } + return async ({ update }) => { + await update(); + }; + }} + > + + +
+
+
+ + +
+

+ {m.admin_section_new_group()} +

+
+
+ +
+ +
+ {#each availablePermissions as perm (perm)} + + {/each} +
+ + +
+
+
diff --git a/frontend/src/routes/admin/SystemTab.svelte b/frontend/src/routes/admin/SystemTab.svelte new file mode 100644 index 00000000..bd80e61c --- /dev/null +++ b/frontend/src/routes/admin/SystemTab.svelte @@ -0,0 +1,72 @@ + + +
+

{m.admin_system_backfill_heading()}

+

{m.admin_system_backfill_description()}

+ + {#if backfillResult !== null} +

+ {m.admin_system_backfill_success({ count: backfillResult })} +

+ {/if} +
+ +
+

+ {m.admin_system_backfill_hashes_heading()} +

+

{m.admin_system_backfill_hashes_description()}

+ + {#if backfillHashesResult !== null} +

+ {m.admin_system_backfill_hashes_success({ count: backfillHashesResult })} +

+ {/if} +
diff --git a/frontend/src/routes/admin/TagsTab.svelte b/frontend/src/routes/admin/TagsTab.svelte new file mode 100644 index 00000000..01a1fec0 --- /dev/null +++ b/frontend/src/routes/admin/TagsTab.svelte @@ -0,0 +1,127 @@ + + +
+
+

{m.admin_section_tags()}

+

+ {m.admin_tags_warning()} +

+
+ +
    + {#each tags as tag (tag.id)} +
  • + {#if editingTagId === tag.id} +
    + async ({ update }) => { + await update(); + cancelEditTag(); + }} + class="flex flex-1 items-center gap-2" + > + + + + +
    + {:else} + + {tag.name} + +
    + +
    { + if (!confirm(m.admin_tag_delete_confirm())) { + cancel(); + } + return async ({ update }) => { + await update(); + }; + }} + class="inline" + > + + +
    +
    + {/if} +
  • + {/each} +
+
diff --git a/frontend/src/routes/admin/UsersTab.svelte b/frontend/src/routes/admin/UsersTab.svelte new file mode 100644 index 00000000..de8dd02a --- /dev/null +++ b/frontend/src/routes/admin/UsersTab.svelte @@ -0,0 +1,120 @@ + + +
+
+

{m.admin_section_users()}

+ + + + + {m.admin_btn_new_user()} + +
+ + + + + + + + + + + + {#each users as user (user.id)} + + + + + + + {/each} + +
{m.admin_col_login()}{m.admin_col_full_name()}{m.admin_col_groups()}{m.admin_col_actions()}
+ {user.username} + + {#if user.firstName || user.lastName} + {user.firstName ?? ''} {user.lastName ?? ''} + {:else} + + {/if} + +
+ {#if user.groups && user.groups.length > 0} + {#each user.groups as group (group.id)} + + {group.name} + + {/each} + {:else} + {m.admin_no_groups()} + {/if} +
+
+
+ + {m.btn_edit()} + + +
{ + if (!confirm(m.admin_user_delete_confirm({ username: user.username }))) { + cancel(); + } + return async ({ update }) => { + await update(); + }; + }} + class="flex items-center" + > + + +
+
+
+
-- 2.49.1 From 6f716824549adf55a576cffa7e14927a8e9f405a Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:14:20 +0100 Subject: [PATCH 04/11] refactor(profile): extract PersonalInfoForm and PasswordChangeForm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split profile/+page.svelte (240 lines) into: - PersonalInfoForm.svelte: name/birth-date/email/contact with own date state - PasswordChangeForm.svelte: current/new/confirm password fields Page drops from 240 → ~25 lines. Date utilities now imported from \$lib/utils/date instead of duplicated inline. Part of #75 Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/profile/+page.svelte | 214 +----------------- .../routes/profile/PasswordChangeForm.svelte | 78 +++++++ .../routes/profile/PersonalInfoForm.svelte | 123 ++++++++++ 3 files changed, 205 insertions(+), 210 deletions(-) create mode 100644 frontend/src/routes/profile/PasswordChangeForm.svelte create mode 100644 frontend/src/routes/profile/PersonalInfoForm.svelte diff --git a/frontend/src/routes/profile/+page.svelte b/frontend/src/routes/profile/+page.svelte index 5ae5e90c..3447118e 100644 --- a/frontend/src/routes/profile/+page.svelte +++ b/frontend/src/routes/profile/+page.svelte @@ -1,41 +1,9 @@
@@ -59,181 +27,7 @@ function handleBirthDateInput(e: Event) {

{m.profile_heading()}

- -
-

- {m.profile_section_personal()} -

- - {#if form?.updateSuccess} -
- {m.profile_saved()} -
- {/if} - {#if form?.updateError} -
- {form.updateError} -
- {/if} - -
-
- - - - - - - - - -
- - -
-
- - -
-

- {m.profile_section_password()} -

- - {#if form?.passwordSuccess} -
- {m.profile_password_changed()} -
- {/if} - {#if form?.passwordError} -
- {#if form.passwordError === 'PASSWORDS_DO_NOT_MATCH'} - {m.profile_password_mismatch()} - {:else} - {form.passwordError} - {/if} -
- {/if} - -
-
- - - - - -
- - -
-
+ +
diff --git a/frontend/src/routes/profile/PasswordChangeForm.svelte b/frontend/src/routes/profile/PasswordChangeForm.svelte new file mode 100644 index 00000000..27f22efd --- /dev/null +++ b/frontend/src/routes/profile/PasswordChangeForm.svelte @@ -0,0 +1,78 @@ + + +
+

+ {m.profile_section_password()} +

+ + {#if form?.passwordSuccess} +
+ {m.profile_password_changed()} +
+ {/if} + {#if form?.passwordError} +
+ {#if form.passwordError === 'PASSWORDS_DO_NOT_MATCH'} + {m.profile_password_mismatch()} + {:else} + {form.passwordError} + {/if} +
+ {/if} + +
+
+ + + + + +
+ + +
+
diff --git a/frontend/src/routes/profile/PersonalInfoForm.svelte b/frontend/src/routes/profile/PersonalInfoForm.svelte new file mode 100644 index 00000000..8cd37f7a --- /dev/null +++ b/frontend/src/routes/profile/PersonalInfoForm.svelte @@ -0,0 +1,123 @@ + + +
+

+ {m.profile_section_personal()} +

+ + {#if form?.updateSuccess} +
+ {m.profile_saved()} +
+ {/if} + {#if form?.updateError} +
+ {form.updateError} +
+ {/if} + +
+
+ + + + + + + + + +
+ + +
+
-- 2.49.1 From 25014cce2d35e9417d14d11c6aae86d7a28dad6d Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:17:55 +0100 Subject: [PATCH 05/11] refactor(admin/users): extract user form sub-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared (src/lib/components/user/): - UserProfileSection.svelte: name/birth-date/email/contact fields - UserGroupsSection.svelte: group checkboxes - UserPasswordSection.svelte: new/confirm password fields Page-local: - admin/users/new/AccountSection.svelte: username + initial password admin/users/[id] drops from 224 → ~35 lines. admin/users/new drops from 191 → ~30 lines. Date utilities imported from \$lib/utils/date. Part of #75 Co-Authored-By: Claude Sonnet 4.6 --- .../components/user/UserGroupsSection.svelte | 24 +++ .../user/UserPasswordSection.svelte | 31 ++++ .../components/user/UserProfileSection.svelte | 95 +++++++++++ .../src/routes/admin/users/[id]/+page.svelte | 155 ++---------------- .../src/routes/admin/users/new/+page.svelte | 131 +-------------- .../admin/users/new/AccountSection.svelte | 31 ++++ 6 files changed, 200 insertions(+), 267 deletions(-) create mode 100644 frontend/src/lib/components/user/UserGroupsSection.svelte create mode 100644 frontend/src/lib/components/user/UserPasswordSection.svelte create mode 100644 frontend/src/lib/components/user/UserProfileSection.svelte create mode 100644 frontend/src/routes/admin/users/new/AccountSection.svelte diff --git a/frontend/src/lib/components/user/UserGroupsSection.svelte b/frontend/src/lib/components/user/UserGroupsSection.svelte new file mode 100644 index 00000000..911cde23 --- /dev/null +++ b/frontend/src/lib/components/user/UserGroupsSection.svelte @@ -0,0 +1,24 @@ + + +
+ {#each groups as group (group.id)} + + {/each} +
diff --git a/frontend/src/lib/components/user/UserPasswordSection.svelte b/frontend/src/lib/components/user/UserPasswordSection.svelte new file mode 100644 index 00000000..cf25b239 --- /dev/null +++ b/frontend/src/lib/components/user/UserPasswordSection.svelte @@ -0,0 +1,31 @@ + + +
+ + + +
diff --git a/frontend/src/lib/components/user/UserProfileSection.svelte b/frontend/src/lib/components/user/UserProfileSection.svelte new file mode 100644 index 00000000..b3b8d214 --- /dev/null +++ b/frontend/src/lib/components/user/UserProfileSection.svelte @@ -0,0 +1,95 @@ + + +
+
+ + + +
+ + + + + + +
diff --git a/frontend/src/routes/admin/users/[id]/+page.svelte b/frontend/src/routes/admin/users/[id]/+page.svelte index 85369c6d..2159d6ed 100644 --- a/frontend/src/routes/admin/users/[id]/+page.svelte +++ b/frontend/src/routes/admin/users/[id]/+page.svelte @@ -1,41 +1,13 @@
@@ -76,77 +48,13 @@ function handleBirthDateInput(e: Event) {

{m.profile_section_personal()}

- -
-
- - - -
- - - - - - -
+
@@ -154,21 +62,7 @@ function handleBirthDateInput(e: Event) {

{m.admin_col_groups()}

- -
- {#each data.groups as group (group.id)} - - {/each} -
+
@@ -176,30 +70,7 @@ function handleBirthDateInput(e: Event) {

{m.admin_label_new_password_optional()}

- -
- - - -
+
diff --git a/frontend/src/routes/admin/users/new/+page.svelte b/frontend/src/routes/admin/users/new/+page.svelte index 01fe3e7f..ae79f96b 100644 --- a/frontend/src/routes/admin/users/new/+page.svelte +++ b/frontend/src/routes/admin/users/new/+page.svelte @@ -1,31 +1,11 @@
@@ -55,118 +35,19 @@ function handleBirthDateInput(e: Event) {
- -

- {m.admin_section_users()} -

- - - - +

{m.profile_section_personal()}

- -
- - - -
- - - - - - +

{m.admin_col_groups()}

- -
- {#each data.groups as group (group.id)} - - {/each} -
+
+import { m } from '$lib/paraglide/messages.js'; + + +

+ {m.admin_section_users()} +

+ + + + -- 2.49.1 From 1facf9cd6033ada3d6c7fb2b5b75f1b95cee184c Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:20:34 +0100 Subject: [PATCH 06/11] refactor(documents): extract document form sub-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shared (src/lib/components/document/): - WhoWhenSection.svelte: date/location/sender/receivers; owns date state - DescriptionSection.svelte: title/archive-loc/tags/summary; owns tag binding - TranscriptionSection.svelte: transcription textarea Page-local: - documents/[id]/edit/FileSectionEdit.svelte: current file + replace input - documents/[id]/edit/SaveBar.svelte: sticky bar with two-step delete confirm - documents/new/FileSectionNew.svelte: initial file upload input documents/[id]/edit drops from 319 → ~40 lines. documents/new drops from 254 → ~30 lines. Date handling imported from \$lib/utils/date. Part of #75 Co-Authored-By: Claude Sonnet 4.6 --- .../document/DescriptionSection.svelte | 80 +++++ .../document/TranscriptionSection.svelte | 19 ++ .../components/document/WhoWhenSection.svelte | 102 ++++++ .../routes/documents/[id]/edit/+page.svelte | 292 ++---------------- .../[id]/edit/FileSectionEdit.svelte | 40 +++ .../routes/documents/[id]/edit/SaveBar.svelte | 72 +++++ .../src/routes/documents/new/+page.svelte | 206 +----------- .../documents/new/FileSectionNew.svelte | 25 ++ 8 files changed, 374 insertions(+), 462 deletions(-) create mode 100644 frontend/src/lib/components/document/DescriptionSection.svelte create mode 100644 frontend/src/lib/components/document/TranscriptionSection.svelte create mode 100644 frontend/src/lib/components/document/WhoWhenSection.svelte create mode 100644 frontend/src/routes/documents/[id]/edit/FileSectionEdit.svelte create mode 100644 frontend/src/routes/documents/[id]/edit/SaveBar.svelte create mode 100644 frontend/src/routes/documents/new/FileSectionNew.svelte diff --git a/frontend/src/lib/components/document/DescriptionSection.svelte b/frontend/src/lib/components/document/DescriptionSection.svelte new file mode 100644 index 00000000..10cda974 --- /dev/null +++ b/frontend/src/lib/components/document/DescriptionSection.svelte @@ -0,0 +1,80 @@ + + +
+

+ {m.doc_section_description()} +

+ +
+ +
+ + +
+ + +
+ + +

{m.form_helper_archive_location()}

+
+ + +
+

{m.form_label_tags()}

+ + +
+ + +
+ + +
+
+
diff --git a/frontend/src/lib/components/document/TranscriptionSection.svelte b/frontend/src/lib/components/document/TranscriptionSection.svelte new file mode 100644 index 00000000..142af5ae --- /dev/null +++ b/frontend/src/lib/components/document/TranscriptionSection.svelte @@ -0,0 +1,19 @@ + + +
+

+ {m.form_label_transcription()} +

+ +
diff --git a/frontend/src/lib/components/document/WhoWhenSection.svelte b/frontend/src/lib/components/document/WhoWhenSection.svelte new file mode 100644 index 00000000..71b2feaf --- /dev/null +++ b/frontend/src/lib/components/document/WhoWhenSection.svelte @@ -0,0 +1,102 @@ + + +
+

+ {m.doc_section_who_when()} +

+ +
+ +
+ + + + {#if dateInvalid} +

{m.form_date_error()}

+ {/if} +
+ + +
+ + +
+ + +
+ +
+ + +
+

{m.form_label_receivers()}

+ +
+
+
diff --git a/frontend/src/routes/documents/[id]/edit/+page.svelte b/frontend/src/routes/documents/[id]/edit/+page.svelte index cac183a9..0adb8955 100644 --- a/frontend/src/routes/documents/[id]/edit/+page.svelte +++ b/frontend/src/routes/documents/[id]/edit/+page.svelte @@ -1,11 +1,12 @@
@@ -65,253 +42,30 @@ function handleDateInput(e: Event) { {/if} - -
-

- {m.doc_section_who_when()} -

- -
- -
- - - - {#if dateInvalid} -

{m.form_date_error()}

- {/if} -
- - -
- - -
- - -
- -
- - -
-

{m.form_label_receivers()}

- -
-
-
- - -
-

- {m.doc_section_description()} -

- -
- -
- - -
- - -
- - -

{m.form_helper_archive_location()}

-
- - -
-

{m.form_label_tags()}

- - -
- - -
- - -
-
-
- - -
-

- {m.form_label_transcription()} -

- -
- - -
-

- {m.doc_section_file()} -

- -
- - {m.doc_current_file_label()} - {doc.originalFilename} -
- - - -
- - -
- -
- {#if confirmDelete} - {m.doc_delete_confirm()} - - - {:else} - - {/if} -
- - -
- - {m.btn_cancel()} - - -
-
+ + + + +
diff --git a/frontend/src/routes/documents/[id]/edit/FileSectionEdit.svelte b/frontend/src/routes/documents/[id]/edit/FileSectionEdit.svelte new file mode 100644 index 00000000..1896861c --- /dev/null +++ b/frontend/src/routes/documents/[id]/edit/FileSectionEdit.svelte @@ -0,0 +1,40 @@ + + +
+

+ {m.doc_section_file()} +

+ +
+ + {m.doc_current_file_label()} + {originalFilename} +
+ + + +
diff --git a/frontend/src/routes/documents/[id]/edit/SaveBar.svelte b/frontend/src/routes/documents/[id]/edit/SaveBar.svelte new file mode 100644 index 00000000..8fd6547b --- /dev/null +++ b/frontend/src/routes/documents/[id]/edit/SaveBar.svelte @@ -0,0 +1,72 @@ + + +
+ +
+ {#if confirmDelete} + {m.doc_delete_confirm()} + + + {:else} + + {/if} +
+ + +
+ + {m.btn_cancel()} + + +
+
diff --git a/frontend/src/routes/documents/new/+page.svelte b/frontend/src/routes/documents/new/+page.svelte index 0b8355f6..b0eee120 100644 --- a/frontend/src/routes/documents/new/+page.svelte +++ b/frontend/src/routes/documents/new/+page.svelte @@ -1,10 +1,11 @@
@@ -75,167 +46,16 @@ function handleDateInput(e: Event) { {/if}
- -
-

- {m.doc_section_who_when()} -

+ + + + -
- -
- - - - {#if dateInvalid} -

- {m.form_date_error()} -

- {/if} -
- - -
- - -
- - -
- -
- - -
-

{m.form_label_receivers()}

- -
-
-
- - -
-

- {m.doc_section_description()} -

- -
- -
- - -
- - -
- - -

{m.form_helper_archive_location()}

-
- - -
-

{m.form_label_tags()}

- - -
- - -
- - -
-
-
- - -
-

- {m.form_label_transcription()} -

- -
- - -
-

- {m.doc_section_file()} -

- - - -
- - +
diff --git a/frontend/src/routes/documents/new/FileSectionNew.svelte b/frontend/src/routes/documents/new/FileSectionNew.svelte new file mode 100644 index 00000000..3e99fa47 --- /dev/null +++ b/frontend/src/routes/documents/new/FileSectionNew.svelte @@ -0,0 +1,25 @@ + + +
+

+ {m.doc_section_file()} +

+ + + +
-- 2.49.1 From 90e94b350aeb030f9bd37f6e4ee233dd1efd6009 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:22:38 +0100 Subject: [PATCH 07/11] refactor(conversations): extract filter bar and timeline sub-components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Split conversations/+page.svelte (346 lines) into: - ConversationFilterBar.svelte: person A/B typeaheads, swap button, date range, sort toggle - ConversationTimeline.svelte: summary bar, chat bubbles, year dividers, new-doc link Page drops from 346 → ~70 lines; navigation logic and filter state stay in the page. Part of #75 Co-Authored-By: Claude Sonnet 4.6 --- .../src/routes/conversations/+page.svelte | 281 ++---------------- .../ConversationFilterBar.svelte | 142 +++++++++ .../conversations/ConversationTimeline.svelte | 164 ++++++++++ 3 files changed, 326 insertions(+), 261 deletions(-) create mode 100644 frontend/src/routes/conversations/ConversationFilterBar.svelte create mode 100644 frontend/src/routes/conversations/ConversationTimeline.svelte diff --git a/frontend/src/routes/conversations/+page.svelte b/frontend/src/routes/conversations/+page.svelte index 4ba259af..25665cdb 100644 --- a/frontend/src/routes/conversations/+page.svelte +++ b/frontend/src/routes/conversations/+page.svelte @@ -1,10 +1,10 @@
@@ -74,124 +55,18 @@ const enrichedDocuments = $derived(

- -
-
- -
- applyFilters()} - /> -
- - -
- -
- - -
- applyFilters()} - /> -
-
- -
- -
- - applyFilters()} - class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" - /> -
- - -
- - applyFilters()} - class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" - /> -
- - -
- -
-
-
+ {#if !senderId || !receiverId} @@ -219,127 +94,11 @@ const enrichedDocuments = $derived(

{m.conv_no_results_text()}

{:else} - -
- {#if yearFrom !== null && yearTo !== null} -

- {m.conv_summary({ count: data.documents.length, yearFrom, yearTo })} -

- {:else} -

- {data.documents.length} -

- {/if} - {#if data.canWrite} - - - - - {m.conv_new_doc_link()} - - {/if} -
- - -
- - - -
-
- {#each enrichedDocuments as { doc, year, showYearDivider } (doc.id)} - {#if showYearDivider} -
-
- {year} -
-
- {/if} - {@const isRight = doc.sender?.id === senderId} - - - - {/each} -
-
-
+ {/if}
diff --git a/frontend/src/routes/conversations/ConversationFilterBar.svelte b/frontend/src/routes/conversations/ConversationFilterBar.svelte new file mode 100644 index 00000000..1dfdd1f4 --- /dev/null +++ b/frontend/src/routes/conversations/ConversationFilterBar.svelte @@ -0,0 +1,142 @@ + + +
+
+ +
+ onapplyFilters()} + /> +
+ + +
+ +
+ + +
+ onapplyFilters()} + /> +
+
+ +
+ +
+ + onapplyFilters()} + class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" + /> +
+ + +
+ + onapplyFilters()} + class="block w-full border-line py-2.5 text-sm shadow-sm focus:border-ink focus:ring-ink" + /> +
+ + +
+ +
+
+
diff --git a/frontend/src/routes/conversations/ConversationTimeline.svelte b/frontend/src/routes/conversations/ConversationTimeline.svelte new file mode 100644 index 00000000..c1cac55f --- /dev/null +++ b/frontend/src/routes/conversations/ConversationTimeline.svelte @@ -0,0 +1,164 @@ + + + +
+ {#if yearFrom !== null && yearTo !== null} +

+ {m.conv_summary({ count: documents.length, yearFrom, yearTo })} +

+ {:else} +

+ {documents.length} +

+ {/if} + {#if canWrite} + + + + + {m.conv_new_doc_link()} + + {/if} +
+ + +
+ + + +
+
+ {#each enrichedDocuments as { doc, year, showYearDivider } (doc.id)} + {#if showYearDivider} +
+
+ {year} +
+
+ {/if} + {@const isRight = doc.sender?.id === senderId} + + + + {/each} +
+
+
-- 2.49.1 From e831de4f8517cdbf8fa52462bb04afcef0570b3c Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:28:18 +0100 Subject: [PATCH 08/11] refactor(home): extract SearchFilterBar, DropZone, and DocumentList Split the 580-line home page into three focused co-located components: - SearchFilterBar: full-text search + collapsible advanced filters - DropZone: drag-and-drop / click-to-upload with progress and messages - DocumentList: document list with new-doc link and empty state Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/+page.svelte | 535 +-------------------- frontend/src/routes/DocumentList.svelte | 177 +++++++ frontend/src/routes/DropZone.svelte | 213 ++++++++ frontend/src/routes/SearchFilterBar.svelte | 164 +++++++ 4 files changed, 577 insertions(+), 512 deletions(-) create mode 100644 frontend/src/routes/DocumentList.svelte create mode 100644 frontend/src/routes/DropZone.svelte create mode 100644 frontend/src/routes/SearchFilterBar.svelte diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 71a5c93b..e5130ff1 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,13 +1,10 @@ -
- -
- -
- -
- (qFocused = true)} - onblur={() => (qFocused = false)} - placeholder={m.docs_search_placeholder()} - class="block w-full border-line py-2.5 pr-10 pl-3 placeholder-ink-3 shadow-sm focus:border-ink focus:ring-ink" - /> -
- -
-
- - - - - - - - -
- - - {#if showAdvanced} -
- -
-

- {m.docs_filter_label_tags()} -

- -
- - -
-
- -
-
- - -
-
- -
-
- - -
-
- - -
-
- - -
-
-
- {/if} -
+ (qFocused = true)} + onblur={() => (qFocused = false)} + /> {#if data.canWrite} - -
fileInput.click()} - onkeydown={(e) => e.key === 'Enter' && fileInput.click()} - > - - {#if isUploading} -
-
-
-
- {uploadProgress}% -
- {:else} - {m.upload_drop_hint()} - {m.upload_accepted_types()} - {/if} -
- - {#if uploadMessages.length > 0} -
- {#each uploadMessages as msg, i (i)} - - {/each} -
- {/if} + {/if} - -
- {#if data.canWrite} - - - {m.docs_btn_new()} - - {/if} -
- - - - +
diff --git a/frontend/src/routes/DocumentList.svelte b/frontend/src/routes/DocumentList.svelte new file mode 100644 index 00000000..9ac8537a --- /dev/null +++ b/frontend/src/routes/DocumentList.svelte @@ -0,0 +1,177 @@ + + + +
+ {#if canWrite} + + + {m.docs_btn_new()} + + {/if} +
+ + + diff --git a/frontend/src/routes/DropZone.svelte b/frontend/src/routes/DropZone.svelte new file mode 100644 index 00000000..d845d027 --- /dev/null +++ b/frontend/src/routes/DropZone.svelte @@ -0,0 +1,213 @@ + + +
fileInput.click()} + onkeydown={(e) => e.key === 'Enter' && fileInput.click()} +> + + {#if isUploading} +
+
+
+
+ {uploadProgress}% +
+ {:else} + {m.upload_drop_hint()} + {m.upload_accepted_types()} + {/if} +
+ +{#if uploadMessages.length > 0} +
+ {#each uploadMessages as msg, i (i)} + + {/each} +
+{/if} + + diff --git a/frontend/src/routes/SearchFilterBar.svelte b/frontend/src/routes/SearchFilterBar.svelte new file mode 100644 index 00000000..7b471b89 --- /dev/null +++ b/frontend/src/routes/SearchFilterBar.svelte @@ -0,0 +1,164 @@ + + +
+ +
+ +
+ +
+ +
+
+ + + + + + + + +
+ + + {#if showAdvanced} +
+ +
+

+ {m.docs_filter_label_tags()} +

+ +
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ {/if} +
-- 2.49.1 From 0db68da00c4da5c2c357abb9ec2cf2e9011db2ea Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:32:01 +0100 Subject: [PATCH 09/11] refactor(persons): extract PersonCard, PersonMergePanel, CoCorrespondentsList, PersonDocumentList Split the 610-line person detail page into four focused co-located components: - PersonCard: view/edit card with inline form (owns editMode) - PersonMergePanel: merge target typeahead + two-step confirm (state reset via {#key}) - CoCorrespondentsList: frequency-ranked correspondent chips linking to conversations - PersonDocumentList: reusable sorted/paginated document list (used for sent + received) Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/persons/[id]/+page.svelte | 555 +----------------- .../persons/[id]/CoCorrespondentsList.svelte | 30 + .../src/routes/persons/[id]/PersonCard.svelte | 246 ++++++++ .../persons/[id]/PersonDocumentList.svelte | 132 +++++ .../persons/[id]/PersonMergePanel.svelte | 83 +++ 5 files changed, 508 insertions(+), 538 deletions(-) create mode 100644 frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte create mode 100644 frontend/src/routes/persons/[id]/PersonCard.svelte create mode 100644 frontend/src/routes/persons/[id]/PersonDocumentList.svelte create mode 100644 frontend/src/routes/persons/[id]/PersonMergePanel.svelte diff --git a/frontend/src/routes/persons/[id]/+page.svelte b/frontend/src/routes/persons/[id]/+page.svelte index 5566055c..a37e87da 100644 --- a/frontend/src/routes/persons/[id]/+page.svelte +++ b/frontend/src/routes/persons/[id]/+page.svelte @@ -1,10 +1,10 @@
@@ -110,500 +64,25 @@ $effect(() => {
- -
-
+ -
- {#if editMode && data.canWrite} - - -
-

- {m.person_edit_heading()} -

- - {#if form?.updateError} -

- {form.updateError} -

- {/if} - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
- - {:else} - -
-
-
- {person.firstName[0]}{person.lastName[0]} -
-
- -
-
-

- {person.firstName} - {person.lastName} -

-
- {#if data.canWrite} - - {/if} -
-
- -
-
- {m.person_label_full_name()} - {person.firstName} {person.lastName} -
- - {#if person.alias} -
- {m.form_label_alias()} - "{person.alias}" -
- {/if} - - {#if person.birthYear || person.deathYear} -
- - {#if person.birthYear && person.deathYear}{m.person_label_birth_year()} / {m.person_label_death_year()}{:else if person.birthYear}{m.person_label_birth_year()}{:else}{m.person_label_death_year()}{/if} - - - {#if person.birthYear}* {person.birthYear}{/if}{#if person.birthYear && person.deathYear} -  {/if}{#if person.deathYear}† {person.deathYear}{/if} - -
- {/if} - - {#if person.notes} -
- {m.person_label_notes()} -

- {person.notes} -

-
- {/if} -
-
-
- {/if} -
-
- - {#if data.canWrite} {#key person.id} -
-
-

{m.person_merge_heading()}

-

- {m.person_merge_description()} -

- - {#if form?.mergeError} -

- {form.mergeError} -

- {/if} - -
- - -
-
- { mergeTargetId = value; showMergeConfirm = false; }} - /> -
- - {#if !showMergeConfirm} - - {:else} -
- - -
- {/if} -
- - {#if showMergeConfirm} -

- {m.person_merge_warning()} {person.firstName} {person.lastName} - {m.person_merge_will_be_deleted()} -

- {/if} -
-
-
+ {/key} {/if} - - {#if coCorrespondents.length > 0} -
-

- {m.person_co_correspondents_heading()} -

-
- {#each coCorrespondents as c (c.id)} - - {c.name} - ({c.count}) - - {/each} -
-
- {/if} + - -
-
-

{m.person_docs_heading()}

- - {sentDocuments.length} - - {#if sentYearRange} - {sentYearRange} - {/if} - {#if sentDocuments.length > 1} - - {/if} -
+ - {#if sentDocuments.length === 0} -
-

{m.person_no_docs()}

-
- {:else} - - {#if sentDocuments.length > DOCS_PREVIEW_LIMIT && !showAllSent} - - {/if} - {/if} -
- - -
-
-

{m.person_received_docs_heading()}

- - {receivedDocuments.length} - - {#if receivedYearRange} - {receivedYearRange} - {/if} - {#if receivedDocuments.length > 1} - - {/if} -
- - {#if receivedDocuments.length === 0} -
-

{m.person_no_received_docs()}

-
- {:else} - - {#if receivedDocuments.length > DOCS_PREVIEW_LIMIT && !showAllReceived} - - {/if} - {/if} -
+
diff --git a/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte new file mode 100644 index 00000000..5b0409eb --- /dev/null +++ b/frontend/src/routes/persons/[id]/CoCorrespondentsList.svelte @@ -0,0 +1,30 @@ + + +{#if coCorrespondents.length > 0} +
+

+ {m.person_co_correspondents_heading()} +

+
+ {#each coCorrespondents as c (c.id)} + + {c.name} + ({c.count}) + + {/each} +
+
+{/if} diff --git a/frontend/src/routes/persons/[id]/PersonCard.svelte b/frontend/src/routes/persons/[id]/PersonCard.svelte new file mode 100644 index 00000000..c5e2e9e0 --- /dev/null +++ b/frontend/src/routes/persons/[id]/PersonCard.svelte @@ -0,0 +1,246 @@ + + +
+
+ +
+ {#if editMode && canWrite} + +
+
+

+ {m.person_edit_heading()} +

+ + {#if form?.updateError} +

+ {form.updateError} +

+ {/if} + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ {:else} + +
+
+
+ {person.firstName[0]}{person.lastName[0]} +
+
+ +
+
+

+ {person.firstName} + {person.lastName} +

+
+ {#if canWrite} + + {/if} +
+
+ +
+
+ {m.person_label_full_name()} + {person.firstName} {person.lastName} +
+ + {#if person.alias} +
+ {m.form_label_alias()} + "{person.alias}" +
+ {/if} + + {#if person.birthYear || person.deathYear} +
+ + {#if person.birthYear && person.deathYear}{m.person_label_birth_year()} / {m.person_label_death_year()}{:else if person.birthYear}{m.person_label_birth_year()}{:else}{m.person_label_death_year()}{/if} + + + {#if person.birthYear}* {person.birthYear}{/if}{#if person.birthYear && person.deathYear} +  {/if}{#if person.deathYear}† {person.deathYear}{/if} + +
+ {/if} + + {#if person.notes} +
+ {m.person_label_notes()} +

+ {person.notes} +

+
+ {/if} +
+
+
+ {/if} +
+
diff --git a/frontend/src/routes/persons/[id]/PersonDocumentList.svelte b/frontend/src/routes/persons/[id]/PersonDocumentList.svelte new file mode 100644 index 00000000..97e6a7e1 --- /dev/null +++ b/frontend/src/routes/persons/[id]/PersonDocumentList.svelte @@ -0,0 +1,132 @@ + + +
+
+

{heading}

+ + {documents.length} + + {#if yearRange} + {yearRange} + {/if} + {#if documents.length > 1} + + {/if} +
+ + {#if documents.length === 0} +
+

{emptyMessage}

+
+ {:else} + + {#if documents.length > DOCS_PREVIEW_LIMIT && !showAll} + + {/if} + {/if} +
diff --git a/frontend/src/routes/persons/[id]/PersonMergePanel.svelte b/frontend/src/routes/persons/[id]/PersonMergePanel.svelte new file mode 100644 index 00000000..0a771b0e --- /dev/null +++ b/frontend/src/routes/persons/[id]/PersonMergePanel.svelte @@ -0,0 +1,83 @@ + + +
+
+

{m.person_merge_heading()}

+

+ {m.person_merge_description()} +

+ + {#if form?.mergeError} +

+ {form.mergeError} +

+ {/if} + +
+ + +
+
+ { + mergeTargetId = value; + showMergeConfirm = false; + }} + /> +
+ + {#if !showMergeConfirm} + + {:else} +
+ + +
+ {/if} +
+ + {#if showMergeConfirm} +

+ {m.person_merge_warning()} {person.firstName} {person.lastName} + {m.person_merge_will_be_deleted()} +

+ {/if} +
+
+
-- 2.49.1 From 40f01a77122876a9b9a4df4ce76b278033fb3dce Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 26 Mar 2026 12:42:39 +0100 Subject: [PATCH 10/11] refactor(comments): extract commentEntry snippet to remove duplicated markup The root-comment and reply rendering blocks were near-identical (view mode with author/time/edit-delete, and edit mode with textarea/save/cancel). Extracted a local {#snippet commentEntry(comment, threadId, showReplyButton)} that handles both states, introducing Svelte 5 snippets to the codebase. Co-Authored-By: Claude Sonnet 4.6 --- .../src/lib/components/CommentThread.svelte | 213 +++++++----------- 1 file changed, 77 insertions(+), 136 deletions(-) diff --git a/frontend/src/lib/components/CommentThread.svelte b/frontend/src/lib/components/CommentThread.svelte index 775f1ef6..3e6f6218 100644 --- a/frontend/src/lib/components/CommentThread.svelte +++ b/frontend/src/lib/components/CommentThread.svelte @@ -189,154 +189,95 @@ onMount(() => { }); + +{#snippet commentEntry(comment: Comment | CommentReply, threadId: string, showReplyButton: boolean)} + {#if editingId === comment.id} +
+ +
+ + +
+
+ {:else} +
+
+
+ {comment.authorName} + {timeAgo(comment.createdAt)} + {#if wasEdited(comment)} + + {m.comment_edited_label()} + {timeAgo(comment.updatedAt)} + + {/if} +
+

{comment.content}

+
+ {#if canModify(comment)} +
+ + +
+ {/if} +
+ {#if showReplyButton && canComment} +
+ +
+ {/if} + {/if} +{/snippet} +
{#each comments as thread, ti (thread.id)}
0 ? 'border-t border-line pt-4' : ''}>
- {#if editingId === thread.id} -
- -
- - -
-
- {:else} -
-
-
- {thread.authorName} - {timeAgo(thread.createdAt)} - {#if wasEdited(thread)} - - {m.comment_edited_label()} - {timeAgo(thread.updatedAt)} - - {/if} -
-

{thread.content}

-
- {#if canModify(thread)} -
- - -
- {/if} -
- - {#if thread.replies.length === 0 && canComment} -
- -
- {/if} - {/if} + {@render commentEntry(thread, thread.id, thread.replies.length === 0)}
{#each thread.replies as reply, ri (reply.id)}
- {#if editingId === reply.id} -
- -
- - -
-
- {:else} -
-
-
- {reply.authorName} - {timeAgo(reply.createdAt)} - {#if wasEdited(reply)} - - {m.comment_edited_label()} - {timeAgo(reply.updatedAt)} - - {/if} -
-

{reply.content}

-
- {#if canModify(reply)} -
- - -
- {/if} -
- - {#if ri === thread.replies.length - 1 && canComment} -
- -
- {/if} - {/if} + {@render commentEntry(reply, thread.id, ri === thread.replies.length - 1)}
{/each} - + {#if replyingTo === thread.id}