From dbcd7a9e1b7e844f7603d89340f91358478c60b7 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 7 Oct 2025 05:30:34 -0700 Subject: [PATCH] refactor(admin): load projects list on server --- .../components/admin/ProjectListItem.svelte | 41 +- src/lib/types/admin.ts | 15 + src/routes/admin/projects/+page.server.ts | 85 ++++ src/routes/admin/projects/+page.svelte | 403 ++++++++---------- 4 files changed, 289 insertions(+), 255 deletions(-) create mode 100644 src/lib/types/admin.ts create mode 100644 src/routes/admin/projects/+page.server.ts diff --git a/src/lib/components/admin/ProjectListItem.svelte b/src/lib/components/admin/ProjectListItem.svelte index aa9be5e..409aca9 100644 --- a/src/lib/components/admin/ProjectListItem.svelte +++ b/src/lib/components/admin/ProjectListItem.svelte @@ -3,32 +3,18 @@ import { createEventDispatcher, onMount } from 'svelte' import AdminByline from './AdminByline.svelte' - interface Project { - id: number - title: string - subtitle: string | null - year: number - client: string | null - status: string - projectType: string - logoUrl: string | null - backgroundColor: string | null - highlightColor: string | null - publishedAt: string | null - createdAt: string - updatedAt: string - } + import type { AdminProject } from '$lib/types/admin' interface Props { - project: Project + project: AdminProject } let { project }: Props = $props() const dispatch = createEventDispatcher<{ - edit: { project: Project } - togglePublish: { project: Project } - delete: { project: Project } + edit: { project: AdminProject } + togglePublish: { project: AdminProject } + delete: { project: AdminProject } }>() let isDropdownOpen = $state(false) @@ -114,7 +100,12 @@ {/if} diff --git a/src/lib/types/admin.ts b/src/lib/types/admin.ts new file mode 100644 index 0000000..d391ed9 --- /dev/null +++ b/src/lib/types/admin.ts @@ -0,0 +1,15 @@ +export interface AdminProject { + id: number + title: string + subtitle: string | null + year: number + client: string | null + status: string + projectType: string + logoUrl: string | null + backgroundColor: string | null + highlightColor: string | null + publishedAt: string | null + createdAt: string + updatedAt: string +} diff --git a/src/routes/admin/projects/+page.server.ts b/src/routes/admin/projects/+page.server.ts new file mode 100644 index 0000000..4e2a2d5 --- /dev/null +++ b/src/routes/admin/projects/+page.server.ts @@ -0,0 +1,85 @@ +import { fail } from '@sveltejs/kit' +import type { Actions, PageServerLoad } from './$types' +import { adminFetch, adminFetchJson } from '$lib/server/admin/authenticated-fetch' +import type { AdminProject } from '$lib/types/admin' + +interface ProjectsResponse { + projects: AdminProject[] +} + +function toStatusCounts(projects: AdminProject[]) { + return projects.reduce( + (counts, project) => { + counts.all += 1 + counts[project.status as 'draft' | 'published'] += 1 + return counts + }, + { all: 0, published: 0, draft: 0 } + ) +} + +function toTypeCounts(projects: AdminProject[]) { + return projects.reduce( + (counts, project) => { + counts.all += 1 + if (project.projectType === 'work') counts.work += 1 + if (project.projectType === 'labs') counts.labs += 1 + return counts + }, + { all: 0, work: 0, labs: 0 } + ) +} + +export const load = (async (event) => { + event.depends('admin:projects') + + const { projects } = await adminFetchJson(event, '/api/projects') + + return { + items: projects, + filters: { + statusCounts: toStatusCounts(projects), + typeCounts: toTypeCounts(projects) + } + } +}) satisfies PageServerLoad + +export const actions = { + toggleStatus: async (event) => { + const formData = await event.request.formData() + const id = Number(formData.get('id')) + const status = formData.get('status') + const updatedAt = formData.get('updatedAt') + + if (!Number.isFinite(id) || typeof status !== 'string' || typeof updatedAt !== 'string') { + return fail(400, { message: 'Invalid toggle request' }) + } + + await adminFetch(event, `/api/projects/${id}`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + status, + updatedAt + }) + }) + + return { success: true } + }, + delete: async (event) => { + const formData = await event.request.formData() + const id = Number(formData.get('id')) + + if (!Number.isFinite(id)) { + return fail(400, { message: 'Invalid project id' }) + } + + await adminFetch(event, `/api/projects/${id}`, { + method: 'DELETE' + }) + + return { success: true } + } +} satisfies Actions diff --git a/src/routes/admin/projects/+page.svelte b/src/routes/admin/projects/+page.svelte index baa9fb3..7273f05 100644 --- a/src/routes/admin/projects/+page.svelte +++ b/src/routes/admin/projects/+page.svelte @@ -1,7 +1,6 @@ - Projects - Admin @jedmund + Work - Admin @jedmund - + {#snippet actions()} - + {/snippet} - {#if error} -
{error}
- {:else} - - - {#snippet left()} - - {/snippet} - {#snippet right()} - + + {/snippet} + - - {#if isLoading} -
-
-

Loading projects...

-
- {:else if filteredProjects.length === 0} -
-

- {#if selectedStatusFilter === 'all' && selectedTypeFilter === 'all'} - No projects found. Create your first project! - {:else} - No projects found matching the current filters. Try adjusting your filters or create a - new project. - {/if} -

-
- {:else} -
- {#each filteredProjects as project} - - {/each} -
- {/if} + {#if actionError} +
{actionError}
+ {/if} + + {#if filteredProjects.length === 0} +
+

No projects found

+

+ {#if selectedTypeFilter === 'all' && selectedStatusFilter === 'all'} + Create your first project to get started! + {:else} + No projects found matching the current filters. Try adjusting your filters or create a new + project. + {/if} +

+
+ {:else} +
+ {#each filteredProjects as project (project.id)} + + {/each} +
{/if}
+
+ + + +
+ +
+ +
+