diff --git a/src/lib/components/admin/PostListItem.svelte b/src/lib/components/admin/PostListItem.svelte index 9ea61dc..72ab35d 100644 --- a/src/lib/components/admin/PostListItem.svelte +++ b/src/lib/components/admin/PostListItem.svelte @@ -2,32 +2,18 @@ import { goto } from '$app/navigation' import { createEventDispatcher, onMount } from 'svelte' import AdminByline from './AdminByline.svelte' - - interface Post { - id: number - slug: string - postType: string - title: string | null - content: any // JSON content - excerpt: string | null - status: string - tags: string[] | null - featuredImage: string | null - publishedAt: string | null - createdAt: string - updatedAt: string - } + import type { AdminPost } from '$lib/types/admin' interface Props { - post: Post + post: AdminPost } let { post }: Props = $props() const dispatch = createEventDispatcher<{ - edit: { post: Post } - togglePublish: { post: Post } - delete: { post: Post } + edit: { post: AdminPost } + togglePublish: { post: AdminPost } + delete: { post: AdminPost } }>() let isDropdownOpen = $state(false) @@ -77,7 +63,7 @@ return () => document.removeEventListener('closeDropdowns', handleCloseDropdowns) }) - function getPostSnippet(post: Post): string { + function getPostSnippet(post: AdminPost): string { // Try excerpt first if (post.excerpt) { return post.excerpt.length > 150 ? post.excerpt.substring(0, 150) + '...' : post.excerpt @@ -161,7 +147,12 @@ {/if} diff --git a/src/lib/types/admin.ts b/src/lib/types/admin.ts index d391ed9..cf15578 100644 --- a/src/lib/types/admin.ts +++ b/src/lib/types/admin.ts @@ -13,3 +13,20 @@ export interface AdminProject { createdAt: string updatedAt: string } + +export interface AdminPost { + id: number + slug: string + postType: string + title: string | null + content: unknown + excerpt?: string | null + status: string + tags: string[] | null + featuredImage: string | null + publishedAt: string | null + createdAt: string + updatedAt: string + attachments?: unknown + linkDescription?: string | null +} diff --git a/src/routes/admin/posts/+page.server.ts b/src/routes/admin/posts/+page.server.ts new file mode 100644 index 0000000..092b9e2 --- /dev/null +++ b/src/routes/admin/posts/+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 { AdminPost } from '$lib/types/admin' + +interface PostsResponse { + posts: AdminPost[] +} + +function toStatusCounts(posts: AdminPost[]) { + return posts.reduce( + (counts, post) => { + counts.all += 1 + counts[post.status as 'draft' | 'published'] += 1 + return counts + }, + { all: 0, published: 0, draft: 0 } + ) +} + +function toTypeCounts(posts: AdminPost[]) { + return posts.reduce( + (counts, post) => { + counts.all += 1 + if (post.postType === 'post') counts.post += 1 + if (post.postType === 'essay') counts.essay += 1 + return counts + }, + { all: 0, post: 0, essay: 0 } + ) +} + +export const load = (async (event) => { + event.depends('admin:posts') + + const { posts } = await adminFetchJson(event, '/api/posts') + + return { + items: posts, + filters: { + statusCounts: toStatusCounts(posts), + typeCounts: toTypeCounts(posts) + } + } +}) 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/posts/${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 post id' }) + } + + await adminFetch(event, `/api/posts/${id}`, { + method: 'DELETE' + }) + + return { success: true } + } +} satisfies Actions diff --git a/src/routes/admin/posts/+page.svelte b/src/routes/admin/posts/+page.svelte index 8abe1a0..5fb0026 100644 --- a/src/routes/admin/posts/+page.svelte +++ b/src/routes/admin/posts/+page.svelte @@ -1,54 +1,38 @@ @@ -228,84 +159,72 @@ import { api } from '$lib/admin/api' {#snippet actions()} - + {/snippet} - {#if error} -
{error}
+ {#if showInlineComposer} +
+ +
+ {/if} + + + {#snippet left()} + + {/snippet} + {#snippet right()} + - - {/snippet} - - - - {#if isLoading} -
- -
- {:else if filteredPosts.length === 0} -
-
📝
-

No posts found

-

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

-
- {:else} -
- {#each filteredPosts as post} - - {/each} -
- {/if} + {/each} + {/if}
@@ -315,12 +234,19 @@ import { api } from '$lib/admin/api' message="Are you sure you want to delete this post? This action cannot be undone." confirmText="Delete Post" onConfirm={confirmDelete} - onCancel={() => { - showDeleteConfirmation = false - postToDelete = null - }} + onCancel={cancelDelete} /> +
+ + + +
+ +
+ +
+