From c67dbeaf38bea53a095c529205539dc3793c4b92 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Tue, 7 Oct 2025 07:30:01 -0700 Subject: [PATCH] fix(admin): make filters reactive in Svelte 5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/lib/components/admin/Select.svelte | 12 +++ src/routes/admin/posts/+page.svelte | 131 +++++++++++++++---------- src/routes/admin/projects/+page.svelte | 77 ++++++++++----- 3 files changed, 139 insertions(+), 81 deletions(-) diff --git a/src/lib/components/admin/Select.svelte b/src/lib/components/admin/Select.svelte index e1a3227..2d9f375 100644 --- a/src/lib/components/admin/Select.svelte +++ b/src/lib/components/admin/Select.svelte @@ -14,6 +14,10 @@ variant?: 'default' | 'minimal' fullWidth?: boolean pill?: boolean + onchange?: (event: Event) => void + oninput?: (event: Event) => void + onfocus?: (event: FocusEvent) => void + onblur?: (event: FocusEvent) => void } let { @@ -23,6 +27,10 @@ variant = 'default', fullWidth = false, pill = true, + onchange, + oninput, + onfocus, + onblur, class: className = '', ...restProps }: Props = $props() @@ -34,6 +42,10 @@ class="select select-{size} select-{variant} {className}" class:select-full-width={fullWidth} class:select-pill={pill} + onchange={(e) => onchange?.(e)} + oninput={(e) => oninput?.(e)} + onfocus={(e) => onfocus?.(e)} + onblur={(e) => onblur?.(e)} {...restProps} > {#each options as option} diff --git a/src/routes/admin/posts/+page.svelte b/src/routes/admin/posts/+page.svelte index 5fb0026..449d657 100644 --- a/src/routes/admin/posts/+page.svelte +++ b/src/routes/admin/posts/+page.svelte @@ -12,18 +12,19 @@ import type { PageData } from './$types' import type { AdminPost } from '$lib/types/admin' - const { data, form } = $props<{ data: PageData; form?: { message?: string } }>() +const { data, form } = $props<{ data: PageData; form?: { message?: string } }>() - let showInlineComposer = $state(true) - let showDeleteConfirmation = $state(false) - let postToDelete = $state(null) +let showInlineComposer = true +let showDeleteConfirmation = false +let postToDelete: AdminPost | null = null - let selectedTypeFilter = $state('all') - let selectedStatusFilter = $state('all') - let sortBy = $state('newest') +let selectedTypeFilter = 'all' +let selectedStatusFilter = 'all' +let sortBy = 'newest' - const actionError = $derived(form?.message ?? '') - const posts = $derived(data.items ?? []) +const actionError = form?.message ?? '' +const posts = data.items ?? [] +let filteredPosts = $state([...posts]) let toggleForm: HTMLFormElement | null = null let toggleIdField: HTMLInputElement | null = null @@ -33,17 +34,17 @@ let deleteForm: HTMLFormElement | null = null let deleteIdField: HTMLInputElement | null = null - const typeFilterOptions = $derived([ - { value: 'all', label: 'All posts' }, - { value: 'post', label: 'Posts' }, - { value: 'essay', label: 'Essays' } - ]) +const typeFilterOptions = [ + { value: 'all', label: 'All posts' }, + { value: 'post', label: 'Posts' }, + { value: 'essay', label: 'Essays' } +] - const statusFilterOptions = $derived([ +const statusFilterOptions = [ { value: 'all', label: 'All statuses' }, { value: 'published', label: 'Published' }, { value: 'draft', label: 'Draft' } - ]) +] const sortOptions = [ { value: 'newest', label: 'Newest first' }, @@ -54,47 +55,61 @@ { value: 'status-draft', label: 'Draft first' } ] - const filteredPosts = $derived(() => { - let next = [...posts] +function applyFilterAndSort() { + let next = [...posts] - if (selectedTypeFilter !== 'all') { - next = next.filter((post) => post.postType === selectedTypeFilter) - } + if (selectedTypeFilter !== 'all') { + next = next.filter((post) => post.postType === selectedTypeFilter) + } - if (selectedStatusFilter !== 'all') { - next = next.filter((post) => post.status === selectedStatusFilter) - } + if (selectedStatusFilter !== 'all') { + next = next.filter((post) => post.status === selectedStatusFilter) + } - switch (sortBy) { - case 'oldest': - next.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) - break - case 'title-asc': - next.sort((a, b) => (a.title || '').localeCompare(b.title || '')) - break - case 'title-desc': - next.sort((a, b) => (b.title || '').localeCompare(a.title || '')) - break - case 'status-published': - next.sort((a, b) => { - if (a.status === b.status) return 0 - return a.status === 'published' ? -1 : 1 - }) - break - case 'status-draft': - next.sort((a, b) => { - if (a.status === b.status) return 0 - return a.status === 'draft' ? -1 : 1 - }) - break - case 'newest': - default: - next.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) - break - } + switch (sortBy) { + case 'oldest': + next.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()) + break + case 'title-asc': + next.sort((a, b) => (a.title || '').localeCompare(b.title || '')) + break + case 'title-desc': + next.sort((a, b) => (b.title || '').localeCompare(a.title || '')) + break + case 'status-published': + next.sort((a, b) => { + if (a.status === b.status) return 0 + return a.status === 'published' ? -1 : 1 + }) + break + case 'status-draft': + next.sort((a, b) => { + if (a.status === b.status) return 0 + return a.status === 'draft' ? -1 : 1 + }) + break + case 'newest': + default: + next.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()) + break + } - return next - }) + filteredPosts = next +} + +applyFilterAndSort() + +function handleTypeFilterChange() { + applyFilterAndSort() +} + +function handleStatusFilterChange() { + applyFilterAndSort() +} + +function handleSortChange() { + applyFilterAndSort() +} onMount(() => { document.addEventListener('click', handleOutsideClick) @@ -184,16 +199,24 @@ options={typeFilterOptions} size="small" variant="minimal" + onchange={handleTypeFilterChange} /> + - + +