refactor(frontend): split large page components into focused sub-components #75
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
Several
+page.sveltefiles have grown large enough to hurt readability. The goal is to decompose each page into focused sub-components co-located next to the page file — not for reuse, but to give each visual block a clear name and a single responsibility.Affected files (by line count)
persons/[id]/+page.svelte+page.svelte(home / document search)admin/+page.svelteconversations/+page.sveltedocuments/[id]/edit/+page.sveltedocuments/new/+page.svelteprofile/+page.svelteadmin/users/[id]/+page.svelteadmin/users/new/+page.svelte+layout.svelteApproach
PascalCase.sveltefile co-located in the same route folder (e.g.routes/persons/[id]/PersonMetadataCard.svelte).+page.server.ts— sub-components do not load their own data..svelte.tsmodule.src/lib/components/only for components used in more than one route.Acceptance criteria
+page.sveltereads as a clear composition of named sub-components with minimal inline markup.npm run checkandnpm run lintpass after each page.persons/[id]/+page.svelte(610 lines)Proposed split →
routes/persons/[id]/PersonCard.svelteperson,canWrite,form?; bindseditModeback to pagePersonMergePanel.svelteperson,form?; ownsmergeTargetId,showMergeConfirmCoCorrespondentsList.sveltecorrespondents(pre-computed array) +personIdPersonDocumentList.sveltedocuments,heading,emptyMessage; ownssortDir,showAll+page.sveltekeeps: data derivations (coCorrespondents,yearRange),editModestate (needed for the$effectthat closes the form on save), and composes the four components.Note on
PersonDocumentList: sent and received lists are structurally identical (same 60-line<li>template), so one generic component serves both. Only the heading and empty-state text differ — pass those as props.Result: page drops from 610 → ~40 lines.
+page.svelte— home / document search (580 lines)Proposed split →
routes/SearchFilterBar.svelteinitialValuesas props; emits filter change via callbacks; ownsshowAdvancedDropZone.sveltecanWrite; owns all upload state (isDragging,windowDragging,isUploading,uploadProgress,uploadMessages); callsinvalidateAll()on successDocumentList.svelte<ul>of document rows + empty statedocuments,error,canWrite+page.sveltekeeps: URL-synced filter state (q,from,to,senderId,receiverId,tagNames),triggerSearch/handleTextSearchfunctions, and the three$effectblocks for syncing state with server data and tag changes.Why not extract search state too? The
$effectsync blocks depend ondata.filters(server data) and triggergoto()— that's page-level navigation logic, not UI. Keeping it in the page avoids prop-drillinggotointo the filter bar.Result: page drops from 580 → ~60 lines.
admin/+page.svelte(573 lines)Proposed split →
routes/admin/UsersTab.svelteusersTagsTab.sveltetags; ownseditingTagId,editingTagNameGroupsTab.sveltegroups; ownseditingGroupIdSystemTab.svelte+page.sveltekeeps:activeTabstate, the tab navigation buttons, the global success flash (form?.message), and renders the active tab via{#if activeTab === ...}.Each tab's state is completely independent — no state crosses tab boundaries, making this a clean split with zero shared reactive state.
Result: page drops from 573 → ~40 lines.
conversations/+page.svelte(346 lines)Proposed split →
routes/conversations/ConversationFilterBar.svelteinitialValuesas props; emits changes via callbacksConversationTimeline.sveltedocuments,senderId,canWrite; ownsenrichedDocumentsderivation andyearFrom/yearTo+page.sveltekeeps: all filter state (senderId,receiverId,fromDate,toDate,sortDir),applyFilters,toggleSort,swapPersons, the navigation$effect, and the three render branches (no selection / no results / timeline).Note on
senderIdcoupling: The timeline needssenderIdto determine which bubble aligns right (sender = current user A). Passing it as a prop is clean — no shared store needed.Result: page drops from 346 → ~70 lines.
documents/[id]/edit/+page.svelte(319 lines) +documents/new/+page.svelte(254 lines)These two pages share four identical form sections and a save bar. Grouping them together because the right split produces shared components — a rare reuse opportunity within this refactor.
Proposed shared components →
src/lib/components/document/WhoWhenSection.svelteDescriptionSection.svelteTranscriptionSection.svelteFileSectionEdit.svelteFileSectionNew.svelteProposed page-local components
SaveBar.svelteroutes/documents/[id]/edit/State ownership:
dateDisplay,dateIso,dateDirty→ live inWhoWhenSection(self-contained; hidden<input name="documentDate">is submitted with parent form)tags,senderId,selectedReceivers→ declared in the page, passed as bindable props into the sectionsconfirmDelete→ lives inSaveBarWhy
lib/components/document/? These sections are used in two routes, so they cross the "co-location" threshold. They're not general-purpose enough for the top-levellib/components/, so adocument/subdirectory scopes them clearly.Also:
handleDateInput/germanToIso/isoToGermanare duplicated acrossedit,new,profile, andadmin/users. This is the right moment to consolidate them into$lib/utils/date.ts(which already hasformatDate).Result: edit page 319 → ~40 lines, new page 254 → ~30 lines.
profile/+page.svelte(240 lines)Proposed split →
routes/profile/PersonalInfoForm.svelteuser,form?; ownsbirthDateDisplay,birthDateIsoPasswordChangeForm.svelteform?+page.sveltebecomes: back link, page heading, two-column grid, and the two components. No shared state between the two forms.Note:
isoToGerman,germanToIso,handleBirthDateInputare duplicated here from the admin user pages. These should be consolidated into$lib/utils/date.tsas part of this refactor (see document edit comment above).Result: page drops from 240 → ~25 lines.
admin/users/[id]/+page.svelte(224 lines) +admin/users/new/+page.svelte(191 lines)Similar situation to the document edit/new pair — both pages share the same form sections. Grouping them.
Proposed shared components →
src/lib/components/user/UserProfileSection.svelte[id]+newUserGroupsSection.svelte[id]+newUserPasswordSection.svelte[id](optional reset) +new(initial password — slight variation:[id]has norequired,newhas a separate username/password account section)Proposed page-local components
AccountSection.svelteroutes/admin/users/new/State ownership: All form fields use plain
<input name="...">with no reactive state exceptbirthDateDisplay/birthDateIso— those live insideUserProfileSection.Result:
[id]page 224 → ~35 lines,newpage 191 → ~30 lines.+layout.svelte(205 lines)The layout is already relatively focused (it's the global shell), but the user menu dropdown adds 50+ lines of state + markup that reads better as its own unit.
Proposed split →
routes/AppNav.svelteisAdminUserMenu.svelteuser,userInitials; ownsuserMenuOpen,clickOutsideaction+layout.sveltekeeps:isAuthPagederivation, the language switcher (3 buttons, ~15 lines — not worth extracting),ThemeToggle, and the outer<header>/<main>scaffold.Result: layout drops from 205 → ~80 lines.