From 006e1db96efacde5918d9527b4f4e1232a9f2dc2 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 7 Oct 2025 04:49:11 -0700 Subject: [PATCH] Add modernization plan --- docs/admin-modernization-plan.md | 214 +++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 docs/admin-modernization-plan.md diff --git a/docs/admin-modernization-plan.md b/docs/admin-modernization-plan.md new file mode 100644 index 0000000..8fe91c0 --- /dev/null +++ b/docs/admin-modernization-plan.md @@ -0,0 +1,214 @@ +# 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.ts` with typed `load` results over `onMount` fetches; use `satisfies` clauses for strong typing. +- Use Svelte runes (`$derived`, `$state`, `$effect`) inside components, but push cross-route state into stores or `load` data. +- Model mutations as form `actions` (with optional `enhance`) to avoid bespoke `fetch` calls 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 `ReturnType` helpers for downstream safety. +- Leverage the [Runed](https://runed.dev) 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 +1. Add the dependency: `pnpm add runed` (or equivalent) and ensure type declarations are available to the TypeScript compiler. +2. Create `src/lib/runed/README.md` documenting approved utilities (e.g., `asyncState`, `memo`, `taskQueue`, `clickOutside`) and guidelines for contributions. +3. Establish a thin wrapper export in `src/lib/runed/index.ts` so future refactors can swap implementations without touching call sites. +4. Update Task 2 prototype (projects list) to replace manual async state handling with `resource` and memoized filters via `$derived` helpers. +5. Evaluate bundle impact via `pnpm run build` and 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 `satisfies` clauses 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 +1. Create `src/routes/admin/+layout.server.ts` that: + - Reads an HttpOnly cookie (e.g., `admin_session`). + - Validates credentials via shared server utility (reusable by API routes). + - Returns `{ user }` (or `null`) while throwing `redirect(303, '/admin/login')` for unauthenticated requests. +2. Add `src/routes/admin/login/+page.server.ts` with: + - A `load` that returns any flash errors. + - A default `actions` export that validates the submitted password, sets the cookie via `cookies.set`, and `redirect`s into `/admin`. +3. Update `src/routes/admin/+layout.svelte` to: + - Remove `onMount`, `$page` derived auth checks, and `goto` usage. + - Read the session via `const { user } = await parent()` and gate rendering accordingly. + - Handle the login route by checking `data` from parent rather than client state. +4. Replace all `localStorage.getItem('admin_auth')` references (e.g., `Admin API`, media page) with reliance on server session (see Task 2). + +### Implementation Notes +- Use `LayoutServerLoad` typing: `export const load = (async (event) => { ... }) satisfies LayoutServerLoad;`. +- Define a `SessionUser` type in `src/lib/types/session.ts` to 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 +1. Extract a server helper `src/lib/server/admin/authenticated-fetch.ts` that wraps `event.fetch`, injects auth headers if needed, and narrows error handling. +2. Convert project, post, media list routes to use server loads: + - Add `+page.server.ts` returning `{ items, filters }` with `depends('admin:projects')`-style cache keys. + - Update `+page.svelte` files to read `export let data` and derive view state from `data.items`. + - Use `$derived` to compute filtered lists inside the component rather than re-fetching. +3. Replace manual `fetch` calls for mutations with typed form actions: + - Define actions in `+page.server.ts` (`export const actions = { toggleStatus: async (event) => { ... } }`). + - In Svelte, use `
` or `form` wrappers to submit with `fetch`, reading `event.detail.result`. +4. After successful mutations, call `invalidate('admin:projects')` (client side) or return `invalidate` instructions within actions to refresh data. + +### Implementation Notes +- Leverage `type ProjectListData = Awaited>` for consumer typing. +- Use discriminated union responses from actions (`{ type: 'success'; payload: ... } | { type: 'error'; message: string }`). +- For media pagination, accept `url.searchParams` in the server load and return `pagination` metadata 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 +1. 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.svelte` for the composer panel. +2. Extract a store factory `createProjectFormStore(project?: Project)` in `src/lib/stores/project-form.ts` that returns typed read/write handles (`fields`, `validation`, `status`, `actions`). +3. 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. +4. 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 `readonly` derived getters (`const title = $derived(store.fields.title)`) to stay idiomatic with runes. +- Use `z.infer` for the store’s form type to stay aligned with validation. +- Prefer `export type ProjectFormStore = ReturnType;` 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 +1. Introduce `src/lib/admin/listFilters.ts` providing `createListFilters(items, config)` that returns: + - Rune-backed state stores for selected filters (`$state`), + - `$derived` filtered/sorted output, + - Helpers `setFilter`, `reset`, and computed counts. +2. Define filter configuration types using generics (`FilterConfig` etc.) for compile-time safety. +3. 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. +4. Add lightweight unit tests (Vitest) for the utility to confirm sort stability and predicate correctness. + +### Implementation Notes +- Use `export interface ListFiltersResult` to codify the return signature. +- Provide optional `serializer` hooks 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 +1. Create `src/lib/actions/clickOutside.ts` that dispatches a `custom:event` when the user clicks outside an element; write in TypeScript with generics for event detail types. +2. Replace manual `document.addEventListener` usages in `ProjectListItem`, `PostListItem`, media dropdowns with `use:clickOutside` and component-local state. +3. Evolve `BaseDropdown.svelte` into `Dropdown.svelte` + `DropdownTrigger.svelte` + `DropdownMenu.svelte` components backed by a shared store (manages open state, keyboard navigation). +4. Standardize action buttons to use `