12 KiB
Admin Interface Modernization Plan
Goals
- Deliver an admin surface that uses idiomatic Svelte 5 + Runes with first-class TypeScript.
- Replace client-side authentication fallbacks with server-validated sessions and consistent typing.
- Reduce duplication across resource screens (projects, posts, media) by extracting reusable list, form, and dropdown primitives.
- Improve reliability by centralizing data loading, mutation, and invalidation logic.
Guiding Principles
- Prefer
+layout.server.ts/+page.server.tswith typedloadresults overonMountfetches; usesatisfiesclauses for strong typing. - Use Svelte runes (
$derived,$state,$effect) inside components, but push cross-route state into stores orloaddata. - Model mutations as form
actions(with optionalenhance) to avoid bespokefetchcalls and to keep optimistic UI localized. - Encode shared behaviors (filters, dropdowns, autosave) as reusable helpers or actions so we can verify and test them once.
- Annotate shared helpers with explicit generics, exported types, and narrow
ReturnTypehelpers for downstream safety. - Leverage the Runed utility library where it meaningfully reduces rune boilerplate while keeping bundle size in check.
Task 0 – Adopt Runed Utility Layer
Objective: Introduce Runed as a shared dependency for rune-focused utilities, formalize usage boundaries, and pilot it in list/data flows.
Steps
- Add the dependency:
pnpm add runed(or equivalent) and ensure type declarations are available to the TypeScript compiler. - Create
src/lib/runed/README.mddocumenting approved utilities (e.g.,asyncState,memo,taskQueue,clickOutside) and guidelines for contributions. - Establish a thin wrapper export in
src/lib/runed/index.tsso future refactors can swap implementations without touching call sites. - Update Task 2 prototype (projects list) to replace manual async state handling with
resourceand memoized filters via$derivedhelpers. - Evaluate bundle impact via
pnpm run buildand record findings in the doc, adjusting the allowed utility list if necessary.
Current Adoption: Projects index page now uses resource for data fetching and onClickOutside for dropdowns as the pilot integration.
Implementation Notes
- Prefer wrapping Runed utilities so downstream components import from a single local module (
import { asyncState } from '$lib/runed'). - Pair Runed helpers with
satisfiesclauses to keep returned state strongly typed. - Audit for tree-shaking compliance; Runed utilities are individually exported to support dead code elimination.
Dependencies
- None; execute before Task 1 to unlock downstream usage.
Task 1 – Server-Side Authentication & Session Flow
Objective: Move credential validation out of the browser and expose typed session data to all admin routes.
Steps
- Create
src/routes/admin/+layout.server.tsthat:- Reads an HttpOnly cookie (e.g.,
admin_session). - Validates credentials via shared server utility (reusable by API routes).
- Returns
{ user }(ornull) while throwingredirect(303, '/admin/login')for unauthenticated requests.
- Reads an HttpOnly cookie (e.g.,
- Add
src/routes/admin/login/+page.server.tswith:- A
loadthat returns any flash errors. - A default
actionsexport that validates the submitted password, sets the cookie viacookies.set, andredirects into/admin.
- A
- Update
src/routes/admin/+layout.svelteto:- Remove
onMount,$pagederived auth checks, andgotousage. - Read the session via
const { user } = await parent()and gate rendering accordingly. - Handle the login route by checking
datafrom parent rather than client state.
- Remove
- Replace all
localStorage.getItem('admin_auth')references (e.g.,Admin API, media page) with reliance on server session (see Task 2).
Implementation Notes
- Use
LayoutServerLoadtyping:export const load = (async (event) => { ... }) satisfies LayoutServerLoad;. - Define a
SessionUsertype insrc/lib/types/session.tsto share across routes and endpoint handlers. - For Basic auth compatibility during transition, consider reading the existing header and issuing the new cookie so legacy API calls keep working.
Dependencies
- Requires shared credential validation utility (see Task 2 Step 1).
- Requires infra support for HttpOnly cookie (name, maxAge, secure flag).
Task 2 – Unified Data Fetching & Mutation Pipeline
Objective: Standardize how admin pages load data and mutate resources with TypeScript-checked flows.
Steps
- Extract a server helper
src/lib/server/admin/authenticated-fetch.tsthat wrapsevent.fetch, injects auth headers if needed, and narrows error handling. - Convert project, post, media list routes to use server loads:
- Add
+page.server.tsreturning{ items, filters }withdepends('admin:projects')-style cache keys. - Update
+page.sveltefiles to readexport let dataand derive view state fromdata.items. - Use
$derivedto compute filtered lists inside the component rather than re-fetching.
- Add
- Replace manual
fetchcalls for mutations with typed form actions:- Define actions in
+page.server.ts(export const actions = { toggleStatus: async (event) => { ... } }). - In Svelte, use
<form use:enhance>orformwrappers to submit withfetch, readingevent.detail.result.
- Define actions in
- After successful mutations, call
invalidate('admin:projects')(client side) or returninvalidateinstructions within actions to refresh data.
Implementation Notes
- Leverage
type ProjectListData = Awaited<ReturnType<typeof load>>for consumer typing. - Use discriminated union responses from actions (
{ type: 'success'; payload: ... } | { type: 'error'; message: string }). - For media pagination, accept
url.searchParamsin the server load and returnpaginationmetadata for the UI.
Dependencies
- Requires Task 1 cookie/session handling.
- Coordinate with API endpoint typing to avoid duplicating DTO definitions (reuse from
src/lib/schemas/...).
Task 3 – Project Form Modularization & Store Extraction
Objective: Split ProjectForm.svelte into composable, typed stores and view modules.
Steps
- Create
src/lib/components/admin/project-form/folder with:ProjectFormShell.svelte(layout, header, actions).ProjectMetadataSection.svelte,ProjectBrandingSection.svelte,ProjectAssetsSection.svelte(each consuming a shared store).CaseStudyEditor.sveltefor the composer panel.
- Extract a store factory
createProjectFormStore(project?: Project)insrc/lib/stores/project-form.tsthat returns typed read/write handles (fields,validation,status,actions). - Move autosave and draft recovery logic into dedicated helpers:
useProjectAutosave(store, { projectId })(wraps Task 6 autosave store).useDraftRecovery(store, draftKey)to encapsulate prompt/timer behavior.
- Rebuild the route components (
projects/new,[id]/edit) to instantiate the store and pass it into the shell via props.
Implementation Notes
- Expose store fields as
readonlyderived getters (const title = $derived(store.fields.title)) to stay idiomatic with runes. - Use
z.infer<typeof projectSchema>for the store’s form type to stay aligned with validation. - Prefer
export type ProjectFormStore = ReturnType<typeof createProjectFormStore>;for downstream usage.
Dependencies
- Depends on Task 2 actions for create/update to avoid duplicating mutation logic.
- Leverages Task 6 autosave refactor.
Task 4 – Shared List Filtering Utilities
Objective: Remove duplicated filter/sort code across projects, posts, and media.
Steps
- Introduce
src/lib/admin/listFilters.tsprovidingcreateListFilters<T>(items, config)that returns:- Rune-backed state stores for selected filters (
$state), $derivedfiltered/sorted output,- Helpers
setFilter,reset, and computed counts.
- Rune-backed state stores for selected filters (
- Define filter configuration types using generics (
FilterConfig<T, K extends keyof T>etc.) for compile-time safety. - Update each admin list page to:
- Import the helper, pass initial data from the server load, and drive the UI from the returned stores.
- Replace manual event handlers with
filters.set('status', value)style interactions.
- Add lightweight unit tests (Vitest) for the utility to confirm sort stability and predicate correctness.
Implementation Notes
- Use
export interface ListFiltersResult<T>to codify the return signature. - Provide optional
serializerhooks for search params so UI state can round-trip URL query strings.
Dependencies
- Task 2 ensures initial data arrives via server load.
Task 5 – Dropdown, Modal, and Click-Outside Primitives
Objective: Centralize interaction patterns to reduce ad-hoc document listeners.
Steps
- Create
src/lib/actions/clickOutside.tsthat dispatches acustom:eventwhen the user clicks outside an element; write in TypeScript with generics for event detail types. - Replace manual
document.addEventListenerusages inProjectListItem,PostListItem, media dropdowns withuse:clickOutsideand component-local state. - Evolve
BaseDropdown.svelteintoDropdown.svelte+DropdownTrigger.svelte+DropdownMenu.sveltecomponents backed by a shared store (manages open state, keyboard navigation). - Standardize action buttons to use
<button type="button">and move repeated SVG markup into icon components (src/lib/icons).
Implementation Notes
- Ensure dropdown components accept slots typed via
Snippetand exposeexport type DropdownContextfor advanced use cases. - Add focus-trap support with optional dependency on
@floating-ui/domif necessary, wrapped in a utility to keep types consistent.
Dependencies
- No external dependencies beyond existing component imports; can be implemented incrementally per list.
Task 6 – Autosave Store & Draft Persistence
Objective: Turn autosave logic into a typed store for reuse across forms.
Steps
- Wrap
createAutoSaveControllerintocreateAutoSaveStore<TPayload, TResponse>(options)that returns:- Writable rune store for status (
$state), - Methods
schedule,flush,cancel, - Optional
onErrorandonSavedcallbacks.
- Writable rune store for status (
- Update
ProjectForm(post-refactor) to consume the store instead of managing controller lifecycles manually. - Extend
draftStore.tswith typed helpers (loadDraft<T extends DraftPayload>()) and integrate into the new store so persistence happens automatically when schedules run. - Document the pattern for future forms (e.g., post editor) in
docs/autosave-completion-guide.md.
Implementation Notes
- Use TypeScript overloads or generics for
saveto ensure the response type flows back to callers. - Prefer rune subscriptions inside the store (
const status = $state<AutoSaveStatus>('idle')).
Dependencies
- Requires Task 2 mutation path so autosave can call shared helpers.
Task 7 – Styling & Theming Harmonization
Objective: Reduce SCSS duplication and make layout adjustments easier.
Steps
- Create
src/lib/styles/admin.cssexposing CSS variables for spacing, typography, and colors consumed by admin components. - Replace per-component
@import '$styles/variables.scss'with@usein a single scoped stylesheet or with CSS variable access. - Introduce layout wrappers (e.g.,
AdminLayoutShell.svelte) that centralize container widths and card backgrounds, removing repeated SCSS fromAdminPage,AdminNavBar, etc. - Audit component classes to ensure consistent BEM-ish naming and remove redundant selectors (e.g., duplicate
.loadingstyles across pages).
Implementation Notes
- Consider PostCSS or Svelte’s
<style global>for variable declarations; keep component styles scoped. - Document variable names and usage in a short appendix within this doc once finalized.
Dependencies
- Largely independent; best executed after structural refactors to avoid churn.
Rollout Strategy
- Phase 0: Execute Task 0 to integrate Runed and document approved utility usage.
- Phase 1: Complete Task 1 & Task 2 to stabilize auth/data flows (blocks others).
- Phase 2: Execute Task 3 and Task 6 to modernize the heaviest form.
- Phase 3: Apply Task 4 and Task 5 across lists; retrofit dropdowns and filters.
- Phase 4: Finish with Task 7 styling clean-up and documentation updates.
Each task section above can serve as a standalone issue. Ensure QA includes regression passes for projects, posts, and media operations after every phase.