refactor(admin): load posts list on server
This commit is contained in:
parent
dbcd7a9e1b
commit
3a588fdf89
4 changed files with 266 additions and 246 deletions
|
|
@ -2,32 +2,18 @@
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import { createEventDispatcher, onMount } from 'svelte'
|
import { createEventDispatcher, onMount } from 'svelte'
|
||||||
import AdminByline from './AdminByline.svelte'
|
import AdminByline from './AdminByline.svelte'
|
||||||
|
import type { AdminPost } from '$lib/types/admin'
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
post: Post
|
post: AdminPost
|
||||||
}
|
}
|
||||||
|
|
||||||
let { post }: Props = $props()
|
let { post }: Props = $props()
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
edit: { post: Post }
|
edit: { post: AdminPost }
|
||||||
togglePublish: { post: Post }
|
togglePublish: { post: AdminPost }
|
||||||
delete: { post: Post }
|
delete: { post: AdminPost }
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
let isDropdownOpen = $state(false)
|
let isDropdownOpen = $state(false)
|
||||||
|
|
@ -77,7 +63,7 @@
|
||||||
return () => document.removeEventListener('closeDropdowns', handleCloseDropdowns)
|
return () => document.removeEventListener('closeDropdowns', handleCloseDropdowns)
|
||||||
})
|
})
|
||||||
|
|
||||||
function getPostSnippet(post: Post): string {
|
function getPostSnippet(post: AdminPost): string {
|
||||||
// Try excerpt first
|
// Try excerpt first
|
||||||
if (post.excerpt) {
|
if (post.excerpt) {
|
||||||
return post.excerpt.length > 150 ? post.excerpt.substring(0, 150) + '...' : post.excerpt
|
return post.excerpt.length > 150 ? post.excerpt.substring(0, 150) + '...' : post.excerpt
|
||||||
|
|
@ -161,7 +147,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dropdown-container">
|
<div class="dropdown-container">
|
||||||
<button class="action-button" onclick={handleToggleDropdown} aria-label="Post actions">
|
<button
|
||||||
|
class="action-button"
|
||||||
|
type="button"
|
||||||
|
onclick={handleToggleDropdown}
|
||||||
|
aria-label="Post actions"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
width="20"
|
width="20"
|
||||||
height="20"
|
height="20"
|
||||||
|
|
@ -177,12 +168,16 @@
|
||||||
|
|
||||||
{#if isDropdownOpen}
|
{#if isDropdownOpen}
|
||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
<button class="dropdown-item" onclick={handleEdit}>Edit post</button>
|
<button class="dropdown-item" type="button" onclick={handleEdit}>
|
||||||
<button class="dropdown-item" onclick={handleTogglePublish}>
|
Edit post
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" type="button" onclick={handleTogglePublish}>
|
||||||
{post.status === 'published' ? 'Unpublish' : 'Publish'} post
|
{post.status === 'published' ? 'Unpublish' : 'Publish'} post
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<button class="dropdown-item danger" onclick={handleDelete}>Delete post</button>
|
<button class="dropdown-item danger" type="button" onclick={handleDelete}>
|
||||||
|
Delete post
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,20 @@ export interface AdminProject {
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: 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
|
||||||
|
}
|
||||||
|
|
|
||||||
85
src/routes/admin/posts/+page.server.ts
Normal file
85
src/routes/admin/posts/+page.server.ts
Normal file
|
|
@ -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<PostsResponse>(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
|
||||||
|
|
@ -1,54 +1,38 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import { goto } from '$app/navigation'
|
import { goto, invalidate } from '$app/navigation'
|
||||||
import { api } from '$lib/admin/api'
|
|
||||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||||
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
||||||
import PostListItem from '$lib/components/admin/PostListItem.svelte'
|
import PostListItem from '$lib/components/admin/PostListItem.svelte'
|
||||||
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
|
|
||||||
import Select from '$lib/components/admin/Select.svelte'
|
|
||||||
import InlineComposerModal from '$lib/components/admin/InlineComposerModal.svelte'
|
import InlineComposerModal from '$lib/components/admin/InlineComposerModal.svelte'
|
||||||
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
|
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
|
||||||
import Button from '$lib/components/admin/Button.svelte'
|
import Button from '$lib/components/admin/Button.svelte'
|
||||||
|
import Select from '$lib/components/admin/Select.svelte'
|
||||||
|
import type { PageData } from './$types'
|
||||||
|
import type { AdminPost } from '$lib/types/admin'
|
||||||
|
|
||||||
interface Post {
|
const { data, form } = $props<{ data: PageData; form?: { message?: string } }>()
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
let posts = $state<Post[]>([])
|
let showInlineComposer = $state(true)
|
||||||
let filteredPosts = $state<Post[]>([])
|
let showDeleteConfirmation = $state(false)
|
||||||
let isLoading = $state(true)
|
let postToDelete = $state<AdminPost | null>(null)
|
||||||
let error = $state('')
|
|
||||||
let total = $state(0)
|
|
||||||
let postTypeCounts = $state<Record<string, number>>({})
|
|
||||||
let statusCounts = $state<Record<string, number>>({})
|
|
||||||
|
|
||||||
// Filter state
|
|
||||||
let selectedTypeFilter = $state<string>('all')
|
let selectedTypeFilter = $state<string>('all')
|
||||||
let selectedStatusFilter = $state<string>('all')
|
let selectedStatusFilter = $state<string>('all')
|
||||||
let sortBy = $state<string>('newest')
|
let sortBy = $state<string>('newest')
|
||||||
|
|
||||||
// Composer state
|
const actionError = $derived(form?.message ?? '')
|
||||||
let showInlineComposer = $state(true)
|
const posts = $derived(data.items ?? [])
|
||||||
let isInteractingWithFilters = $state(false)
|
|
||||||
|
|
||||||
// Delete confirmation state
|
let toggleForm: HTMLFormElement | null = null
|
||||||
let showDeleteConfirmation = $state(false)
|
let toggleIdField: HTMLInputElement | null = null
|
||||||
let postToDelete = $state<Post | null>(null)
|
let toggleStatusField: HTMLInputElement | null = null
|
||||||
|
let toggleUpdatedAtField: HTMLInputElement | null = null
|
||||||
|
|
||||||
|
let deleteForm: HTMLFormElement | null = null
|
||||||
|
let deleteIdField: HTMLInputElement | null = null
|
||||||
|
|
||||||
// Create filter options
|
|
||||||
const typeFilterOptions = $derived([
|
const typeFilterOptions = $derived([
|
||||||
{ value: 'all', label: 'All posts' },
|
{ value: 'all', label: 'All posts' },
|
||||||
{ value: 'post', label: 'Posts' },
|
{ value: 'post', label: 'Posts' },
|
||||||
|
|
@ -70,154 +54,101 @@ import { api } from '$lib/admin/api'
|
||||||
{ value: 'status-draft', label: 'Draft first' }
|
{ value: 'status-draft', label: 'Draft first' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const postTypeIcons: Record<string, string> = {
|
const filteredPosts = $derived(() => {
|
||||||
post: '💭',
|
let next = [...posts]
|
||||||
essay: '📝'
|
|
||||||
}
|
|
||||||
|
|
||||||
const postTypeLabels: Record<string, string> = {
|
|
||||||
post: 'Post',
|
|
||||||
essay: 'Essay'
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await loadPosts()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function loadPosts() {
|
|
||||||
try {
|
|
||||||
const data = await api.get('/api/posts')
|
|
||||||
posts = data.posts || []
|
|
||||||
total = data.pagination?.total || posts.length
|
|
||||||
|
|
||||||
// Calculate post type counts
|
|
||||||
const typeCounts: Record<string, number> = {
|
|
||||||
all: posts.length,
|
|
||||||
post: 0,
|
|
||||||
essay: 0
|
|
||||||
}
|
|
||||||
|
|
||||||
posts.forEach((post) => {
|
|
||||||
if (post.postType === 'post') {
|
|
||||||
typeCounts.post++
|
|
||||||
} else if (post.postType === 'essay') {
|
|
||||||
typeCounts.essay++
|
|
||||||
}
|
|
||||||
})
|
|
||||||
postTypeCounts = typeCounts
|
|
||||||
|
|
||||||
// Calculate status counts
|
|
||||||
const statusCountsTemp: Record<string, number> = {
|
|
||||||
all: posts.length,
|
|
||||||
published: posts.filter((p) => p.status === 'published').length,
|
|
||||||
draft: posts.filter((p) => p.status === 'draft').length
|
|
||||||
}
|
|
||||||
statusCounts = statusCountsTemp
|
|
||||||
|
|
||||||
// Apply initial filter and sort
|
|
||||||
applyFilterAndSort()
|
|
||||||
} catch (err) {
|
|
||||||
error = 'Failed to load posts'
|
|
||||||
console.error(err)
|
|
||||||
} finally {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFilterAndSort() {
|
|
||||||
let filtered = [...posts]
|
|
||||||
|
|
||||||
// Apply type filter
|
|
||||||
if (selectedTypeFilter !== 'all') {
|
if (selectedTypeFilter !== 'all') {
|
||||||
filtered = filtered.filter((post) => post.postType === selectedTypeFilter)
|
next = next.filter((post) => post.postType === selectedTypeFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply status filter
|
|
||||||
if (selectedStatusFilter !== 'all') {
|
if (selectedStatusFilter !== 'all') {
|
||||||
filtered = filtered.filter((post) => post.status === selectedStatusFilter)
|
next = next.filter((post) => post.status === selectedStatusFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sorting
|
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case 'oldest':
|
case 'oldest':
|
||||||
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
next.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||||
break
|
break
|
||||||
case 'title-asc':
|
case 'title-asc':
|
||||||
filtered.sort((a, b) => (a.title || '').localeCompare(b.title || ''))
|
next.sort((a, b) => (a.title || '').localeCompare(b.title || ''))
|
||||||
break
|
break
|
||||||
case 'title-desc':
|
case 'title-desc':
|
||||||
filtered.sort((a, b) => (b.title || '').localeCompare(a.title || ''))
|
next.sort((a, b) => (b.title || '').localeCompare(a.title || ''))
|
||||||
break
|
break
|
||||||
case 'status-published':
|
case 'status-published':
|
||||||
filtered.sort((a, b) => {
|
next.sort((a, b) => {
|
||||||
if (a.status === b.status) return 0
|
if (a.status === b.status) return 0
|
||||||
return a.status === 'published' ? -1 : 1
|
return a.status === 'published' ? -1 : 1
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'status-draft':
|
case 'status-draft':
|
||||||
filtered.sort((a, b) => {
|
next.sort((a, b) => {
|
||||||
if (a.status === b.status) return 0
|
if (a.status === b.status) return 0
|
||||||
return a.status === 'draft' ? -1 : 1
|
return a.status === 'draft' ? -1 : 1
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'newest':
|
case 'newest':
|
||||||
default:
|
default:
|
||||||
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
next.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredPosts = filtered
|
return next
|
||||||
}
|
})
|
||||||
|
|
||||||
function handleTypeFilterChange() {
|
onMount(() => {
|
||||||
applyFilterAndSort()
|
document.addEventListener('click', handleOutsideClick)
|
||||||
}
|
return () => document.removeEventListener('click', handleOutsideClick)
|
||||||
|
})
|
||||||
|
|
||||||
function handleStatusFilterChange() {
|
function handleOutsideClick(event: MouseEvent) {
|
||||||
applyFilterAndSort()
|
const target = event.target as HTMLElement
|
||||||
}
|
if (!target.closest('.dropdown-container')) {
|
||||||
|
document.dispatchEvent(new CustomEvent('closeDropdowns'))
|
||||||
function handleSortChange() {
|
}
|
||||||
applyFilterAndSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleComposerSaved() {
|
|
||||||
// Reload posts when a new post is created
|
|
||||||
loadPosts()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNewEssay() {
|
function handleNewEssay() {
|
||||||
goto('/admin/posts/new?type=essay')
|
goto('/admin/posts/new?type=essay')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTogglePublish(event: CustomEvent<{ post: Post }>) {
|
async function handleComposerSaved() {
|
||||||
const { post } = event.detail
|
await invalidate('admin:posts')
|
||||||
const newStatus = post.status === 'published' ? 'draft' : 'published'
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.patch(`/api/posts/${post.id}`, { status: newStatus, updatedAt: post.updatedAt })
|
|
||||||
await loadPosts()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to toggle publish status:', error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDeletePost(event: CustomEvent<{ post: Post }>) {
|
function handleEdit(event: CustomEvent<{ post: AdminPost }>) {
|
||||||
|
goto(`/admin/posts/${event.detail.post.id}/edit`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTogglePublish(event: CustomEvent<{ post: AdminPost }>) {
|
||||||
|
const post = event.detail.post
|
||||||
|
if (!toggleForm || !toggleIdField || !toggleStatusField || !toggleUpdatedAtField) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleIdField.value = String(post.id)
|
||||||
|
toggleStatusField.value = post.status === 'published' ? 'draft' : 'published'
|
||||||
|
toggleUpdatedAtField.value = post.updatedAt
|
||||||
|
toggleForm.requestSubmit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(event: CustomEvent<{ post: AdminPost }>) {
|
||||||
postToDelete = event.detail.post
|
postToDelete = event.detail.post
|
||||||
showDeleteConfirmation = true
|
showDeleteConfirmation = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirmDelete() {
|
function confirmDelete() {
|
||||||
if (!postToDelete) return
|
if (!postToDelete || !deleteForm || !deleteIdField) return
|
||||||
|
|
||||||
try {
|
deleteIdField.value = String(postToDelete.id)
|
||||||
await api.delete(`/api/posts/${postToDelete.id}`)
|
showDeleteConfirmation = false
|
||||||
showDeleteConfirmation = false
|
deleteForm.requestSubmit()
|
||||||
postToDelete = null
|
postToDelete = null
|
||||||
await loadPosts()
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete post:', error)
|
function cancelDelete() {
|
||||||
}
|
showDeleteConfirmation = false
|
||||||
|
postToDelete = null
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -228,84 +159,72 @@ import { api } from '$lib/admin/api'
|
||||||
<AdminPage>
|
<AdminPage>
|
||||||
<AdminHeader title="Universe" slot="header">
|
<AdminHeader title="Universe" slot="header">
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button variant="primary" buttonSize="large" onclick={handleNewEssay}>New Essay</Button>
|
<Button variant="primary" buttonSize="large" onclick={handleNewEssay}>
|
||||||
|
New Essay
|
||||||
|
</Button>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</AdminHeader>
|
</AdminHeader>
|
||||||
|
|
||||||
{#if error}
|
{#if showInlineComposer}
|
||||||
<div class="error-message">{error}</div>
|
<div class="composer-section">
|
||||||
|
<InlineComposerModal
|
||||||
|
isOpen={true}
|
||||||
|
initialMode="page"
|
||||||
|
initialPostType="post"
|
||||||
|
closeOnSave={false}
|
||||||
|
on:saved={handleComposerSaved}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<AdminFilters>
|
||||||
|
{#snippet left()}
|
||||||
|
<Select
|
||||||
|
bind:value={selectedTypeFilter}
|
||||||
|
options={typeFilterOptions}
|
||||||
|
size="small"
|
||||||
|
variant="minimal"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
bind:value={selectedStatusFilter}
|
||||||
|
options={statusFilterOptions}
|
||||||
|
size="small"
|
||||||
|
variant="minimal"
|
||||||
|
/>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet right()}
|
||||||
|
<Select bind:value={sortBy} options={sortOptions} size="small" variant="minimal" />
|
||||||
|
{/snippet}
|
||||||
|
</AdminFilters>
|
||||||
|
|
||||||
|
{#if actionError}
|
||||||
|
<div class="error-message">{actionError}</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if filteredPosts.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-icon">📝</div>
|
||||||
|
<h3>No posts found</h3>
|
||||||
|
<p>
|
||||||
|
{#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}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Inline Composer -->
|
<div class="posts-list">
|
||||||
{#if showInlineComposer}
|
{#each filteredPosts as post (post.id)}
|
||||||
<div class="composer-section">
|
<PostListItem
|
||||||
<InlineComposerModal
|
{post}
|
||||||
isOpen={true}
|
on:edit={handleEdit}
|
||||||
initialMode="page"
|
on:togglePublish={handleTogglePublish}
|
||||||
initialPostType="post"
|
on:delete={handleDelete}
|
||||||
closeOnSave={false}
|
|
||||||
on:saved={handleComposerSaved}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
{/each}
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<AdminFilters>
|
|
||||||
{#snippet left()}
|
|
||||||
<Select
|
|
||||||
bind:value={selectedTypeFilter}
|
|
||||||
options={typeFilterOptions}
|
|
||||||
size="small"
|
|
||||||
variant="minimal"
|
|
||||||
onchange={handleTypeFilterChange}
|
|
||||||
/>
|
|
||||||
<Select
|
|
||||||
bind:value={selectedStatusFilter}
|
|
||||||
options={statusFilterOptions}
|
|
||||||
size="small"
|
|
||||||
variant="minimal"
|
|
||||||
onchange={handleStatusFilterChange}
|
|
||||||
/>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet right()}
|
|
||||||
<Select
|
|
||||||
bind:value={sortBy}
|
|
||||||
options={sortOptions}
|
|
||||||
size="small"
|
|
||||||
variant="minimal"
|
|
||||||
onchange={handleSortChange}
|
|
||||||
/>
|
|
||||||
{/snippet}
|
|
||||||
</AdminFilters>
|
|
||||||
|
|
||||||
<!-- Posts List -->
|
|
||||||
{#if isLoading}
|
|
||||||
<div class="loading-container">
|
|
||||||
<LoadingSpinner />
|
|
||||||
</div>
|
|
||||||
{:else if filteredPosts.length === 0}
|
|
||||||
<div class="empty-state">
|
|
||||||
<div class="empty-icon">📝</div>
|
|
||||||
<h3>No posts found</h3>
|
|
||||||
<p>
|
|
||||||
{#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}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="posts-list">
|
|
||||||
{#each filteredPosts as post}
|
|
||||||
<PostListItem
|
|
||||||
{post}
|
|
||||||
on:togglePublish={handleTogglePublish}
|
|
||||||
on:delete={handleDeletePost}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</AdminPage>
|
</AdminPage>
|
||||||
|
|
||||||
|
|
@ -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."
|
message="Are you sure you want to delete this post? This action cannot be undone."
|
||||||
confirmText="Delete Post"
|
confirmText="Delete Post"
|
||||||
onConfirm={confirmDelete}
|
onConfirm={confirmDelete}
|
||||||
onCancel={() => {
|
onCancel={cancelDelete}
|
||||||
showDeleteConfirmation = false
|
|
||||||
postToDelete = null
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<form method="POST" action="?/toggle-status" class="hidden-form" bind:this={toggleForm}>
|
||||||
|
<input type="hidden" name="id" bind:this={toggleIdField} />
|
||||||
|
<input type="hidden" name="status" bind:this={toggleStatusField} />
|
||||||
|
<input type="hidden" name="updatedAt" bind:this={toggleUpdatedAtField} />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="?/delete" class="hidden-form" bind:this={deleteForm}>
|
||||||
|
<input type="hidden" name="id" bind:this={deleteIdField} />
|
||||||
|
</form>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '$styles/variables.scss';
|
@import '$styles/variables.scss';
|
||||||
|
|
||||||
|
|
@ -334,11 +260,9 @@ import { api } from '$lib/admin/api'
|
||||||
margin-bottom: $unit-4x;
|
margin-bottom: $unit-4x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-container {
|
.composer-section {
|
||||||
display: flex;
|
margin-bottom: $unit-4x;
|
||||||
justify-content: center;
|
padding: 0 $unit;
|
||||||
align-items: center;
|
|
||||||
min-height: 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
|
|
@ -370,8 +294,7 @@ import { api } from '$lib/admin/api'
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.composer-section {
|
.hidden-form {
|
||||||
margin-bottom: $unit-4x;
|
display: none;
|
||||||
padding: 0 $unit;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue