refactor(admin): extract shared list filtering utilities
Introduces createListFilters() with type-safe, reactive filtering and sorting for admin list pages. Eliminates ~120 lines of duplicate code across projects and posts pages. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c67dbeaf38
commit
66d5240240
3 changed files with 220 additions and 146 deletions
164
src/lib/admin/listFilters.svelte.ts
Normal file
164
src/lib/admin/listFilters.svelte.ts
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
/**
|
||||||
|
* Shared list filtering and sorting utilities for admin pages.
|
||||||
|
* Eliminates duplication across projects, posts, and media list pages.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type FilterValue = string | number | boolean
|
||||||
|
|
||||||
|
interface FilterDefinition<T> {
|
||||||
|
field: keyof T
|
||||||
|
default: FilterValue
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterConfig<T> {
|
||||||
|
[key: string]: FilterDefinition<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SortConfig<T> {
|
||||||
|
[key: string]: (a: T, b: T) => number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ListFiltersConfig<T> {
|
||||||
|
filters: FilterConfig<T>
|
||||||
|
sorts: SortConfig<T>
|
||||||
|
defaultSort: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListFiltersResult<T> {
|
||||||
|
/** Current filter values */
|
||||||
|
values: Record<string, FilterValue>
|
||||||
|
/** Current sort key */
|
||||||
|
sort: string
|
||||||
|
/** Filtered and sorted items */
|
||||||
|
items: T[]
|
||||||
|
/** Number of items after filtering */
|
||||||
|
count: number
|
||||||
|
/** Set a filter value */
|
||||||
|
set: (filterKey: string, value: FilterValue) => void
|
||||||
|
/** Set the current sort */
|
||||||
|
setSort: (sortKey: string) => void
|
||||||
|
/** Reset all filters to defaults */
|
||||||
|
reset: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a reactive list filter store using Svelte 5 runes.
|
||||||
|
* Must be called within component context.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const filters = createListFilters(projects, {
|
||||||
|
* filters: {
|
||||||
|
* type: { field: 'projectType', default: 'all' },
|
||||||
|
* status: { field: 'status', default: 'all' }
|
||||||
|
* },
|
||||||
|
* sorts: {
|
||||||
|
* newest: (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
|
* oldest: (a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||||
|
* },
|
||||||
|
* defaultSort: 'newest'
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
export function createListFilters<T>(
|
||||||
|
sourceItems: T[],
|
||||||
|
config: ListFiltersConfig<T>
|
||||||
|
): ListFiltersResult<T> {
|
||||||
|
// Initialize filter state from config defaults
|
||||||
|
const initialValues = Object.entries(config.filters).reduce(
|
||||||
|
(acc, [key, def]) => {
|
||||||
|
acc[key] = def.default
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, FilterValue>
|
||||||
|
)
|
||||||
|
|
||||||
|
let filterValues = $state<Record<string, FilterValue>>(initialValues)
|
||||||
|
let currentSort = $state<string>(config.defaultSort)
|
||||||
|
|
||||||
|
// Derived filtered and sorted items
|
||||||
|
const filteredItems = $derived.by(() => {
|
||||||
|
let result = [...sourceItems]
|
||||||
|
|
||||||
|
// Apply all filters
|
||||||
|
for (const [filterKey, filterDef] of Object.entries(config.filters)) {
|
||||||
|
const value = filterValues[filterKey]
|
||||||
|
// Skip filtering if value is 'all' (common default for show-all state)
|
||||||
|
if (value !== 'all') {
|
||||||
|
result = result.filter((item) => item[filterDef.field] === value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sort
|
||||||
|
const sortFn = config.sorts[currentSort]
|
||||||
|
if (sortFn) {
|
||||||
|
result.sort(sortFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
get values() {
|
||||||
|
return filterValues
|
||||||
|
},
|
||||||
|
get sort() {
|
||||||
|
return currentSort
|
||||||
|
},
|
||||||
|
get items() {
|
||||||
|
return filteredItems
|
||||||
|
},
|
||||||
|
get count() {
|
||||||
|
return filteredItems.length
|
||||||
|
},
|
||||||
|
set(filterKey: string, value: FilterValue) {
|
||||||
|
filterValues[filterKey] = value
|
||||||
|
},
|
||||||
|
setSort(sortKey: string) {
|
||||||
|
currentSort = sortKey
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
filterValues = { ...initialValues }
|
||||||
|
currentSort = config.defaultSort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common sort functions for reuse across list pages
|
||||||
|
*/
|
||||||
|
export const commonSorts = {
|
||||||
|
/** Sort by date field, newest first */
|
||||||
|
dateDesc: <T>(field: keyof T) => (a: T, b: T) =>
|
||||||
|
new Date(b[field] as string).getTime() - new Date(a[field] as string).getTime(),
|
||||||
|
|
||||||
|
/** Sort by date field, oldest first */
|
||||||
|
dateAsc: <T>(field: keyof T) => (a: T, b: T) =>
|
||||||
|
new Date(a[field] as string).getTime() - new Date(b[field] as string).getTime(),
|
||||||
|
|
||||||
|
/** Sort by string field, A-Z */
|
||||||
|
stringAsc: <T>(field: keyof T) => (a: T, b: T) =>
|
||||||
|
String(a[field] || '').localeCompare(String(b[field] || '')),
|
||||||
|
|
||||||
|
/** Sort by string field, Z-A */
|
||||||
|
stringDesc: <T>(field: keyof T) => (a: T, b: T) =>
|
||||||
|
String(b[field] || '').localeCompare(String(a[field] || '')),
|
||||||
|
|
||||||
|
/** Sort by number field, ascending */
|
||||||
|
numberAsc: <T>(field: keyof T) => (a: T, b: T) =>
|
||||||
|
Number(a[field]) - Number(b[field]),
|
||||||
|
|
||||||
|
/** Sort by number field, descending */
|
||||||
|
numberDesc: <T>(field: keyof T) => (a: T, b: T) =>
|
||||||
|
Number(b[field]) - Number(a[field]),
|
||||||
|
|
||||||
|
/** Sort by status field, published first */
|
||||||
|
statusPublishedFirst: <T>(field: keyof T) => (a: T, b: T) => {
|
||||||
|
if (a[field] === b[field]) return 0
|
||||||
|
return a[field] === 'published' ? -1 : 1
|
||||||
|
},
|
||||||
|
|
||||||
|
/** Sort by status field, draft first */
|
||||||
|
statusDraftFirst: <T>(field: keyof T) => (a: T, b: T) => {
|
||||||
|
if (a[field] === b[field]) return 0
|
||||||
|
return a[field] === 'draft' ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
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 Select from '$lib/components/admin/Select.svelte'
|
||||||
|
import { createListFilters, commonSorts } from '$lib/admin/listFilters.svelte'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
import type { AdminPost } from '$lib/types/admin'
|
import type { AdminPost } from '$lib/types/admin'
|
||||||
|
|
||||||
|
|
@ -18,13 +19,25 @@ let showInlineComposer = true
|
||||||
let showDeleteConfirmation = false
|
let showDeleteConfirmation = false
|
||||||
let postToDelete: AdminPost | null = null
|
let postToDelete: AdminPost | null = null
|
||||||
|
|
||||||
let selectedTypeFilter = 'all'
|
|
||||||
let selectedStatusFilter = 'all'
|
|
||||||
let sortBy = 'newest'
|
|
||||||
|
|
||||||
const actionError = form?.message ?? ''
|
const actionError = form?.message ?? ''
|
||||||
const posts = data.items ?? []
|
const posts = data.items ?? []
|
||||||
let filteredPosts = $state<AdminPost[]>([...posts])
|
|
||||||
|
// Create reactive filters
|
||||||
|
const filters = createListFilters(posts, {
|
||||||
|
filters: {
|
||||||
|
type: { field: 'postType', default: 'all' },
|
||||||
|
status: { field: 'status', default: 'all' }
|
||||||
|
},
|
||||||
|
sorts: {
|
||||||
|
newest: commonSorts.dateDesc<AdminPost>('createdAt'),
|
||||||
|
oldest: commonSorts.dateAsc<AdminPost>('createdAt'),
|
||||||
|
'title-asc': commonSorts.stringAsc<AdminPost>('title'),
|
||||||
|
'title-desc': commonSorts.stringDesc<AdminPost>('title'),
|
||||||
|
'status-published': commonSorts.statusPublishedFirst<AdminPost>('status'),
|
||||||
|
'status-draft': commonSorts.statusDraftFirst<AdminPost>('status')
|
||||||
|
},
|
||||||
|
defaultSort: 'newest'
|
||||||
|
})
|
||||||
|
|
||||||
let toggleForm: HTMLFormElement | null = null
|
let toggleForm: HTMLFormElement | null = null
|
||||||
let toggleIdField: HTMLInputElement | null = null
|
let toggleIdField: HTMLInputElement | null = null
|
||||||
|
|
@ -55,62 +68,6 @@ const statusFilterOptions = [
|
||||||
{ value: 'status-draft', label: 'Draft first' }
|
{ value: 'status-draft', label: 'Draft first' }
|
||||||
]
|
]
|
||||||
|
|
||||||
function applyFilterAndSort() {
|
|
||||||
let next = [...posts]
|
|
||||||
|
|
||||||
if (selectedTypeFilter !== 'all') {
|
|
||||||
next = next.filter((post) => post.postType === selectedTypeFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredPosts = next
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilterAndSort()
|
|
||||||
|
|
||||||
function handleTypeFilterChange() {
|
|
||||||
applyFilterAndSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStatusFilterChange() {
|
|
||||||
applyFilterAndSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSortChange() {
|
|
||||||
applyFilterAndSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.addEventListener('click', handleOutsideClick)
|
document.addEventListener('click', handleOutsideClick)
|
||||||
return () => document.removeEventListener('click', handleOutsideClick)
|
return () => document.removeEventListener('click', handleOutsideClick)
|
||||||
|
|
@ -195,27 +152,27 @@ function handleSortChange() {
|
||||||
<AdminFilters>
|
<AdminFilters>
|
||||||
{#snippet left()}
|
{#snippet left()}
|
||||||
<Select
|
<Select
|
||||||
bind:value={selectedTypeFilter}
|
value={filters.values.type}
|
||||||
options={typeFilterOptions}
|
options={typeFilterOptions}
|
||||||
size="small"
|
size="small"
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
onchange={handleTypeFilterChange}
|
onchange={(e) => filters.set('type', (e.target as HTMLSelectElement).value)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
bind:value={selectedStatusFilter}
|
value={filters.values.status}
|
||||||
options={statusFilterOptions}
|
options={statusFilterOptions}
|
||||||
size="small"
|
size="small"
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
onchange={handleStatusFilterChange}
|
onchange={(e) => filters.set('status', (e.target as HTMLSelectElement).value)}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet right()}
|
{#snippet right()}
|
||||||
<Select
|
<Select
|
||||||
bind:value={sortBy}
|
value={filters.sort}
|
||||||
options={sortOptions}
|
options={sortOptions}
|
||||||
size="small"
|
size="small"
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
onchange={handleSortChange}
|
onchange={(e) => filters.setSort((e.target as HTMLSelectElement).value)}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</AdminFilters>
|
</AdminFilters>
|
||||||
|
|
@ -224,12 +181,12 @@ function handleSortChange() {
|
||||||
<div class="error-message">{actionError}</div>
|
<div class="error-message">{actionError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filteredPosts.length === 0}
|
{#if filters.items.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<div class="empty-icon">📝</div>
|
<div class="empty-icon">📝</div>
|
||||||
<h3>No posts found</h3>
|
<h3>No posts found</h3>
|
||||||
<p>
|
<p>
|
||||||
{#if selectedTypeFilter === 'all' && selectedStatusFilter === 'all'}
|
{#if filters.values.type === 'all' && filters.values.status === 'all'}
|
||||||
Create your first post to get started!
|
Create your first post to get started!
|
||||||
{:else}
|
{:else}
|
||||||
No posts found matching the current filters. Try adjusting your filters or create a new
|
No posts found matching the current filters. Try adjusting your filters or create a new
|
||||||
|
|
@ -239,7 +196,7 @@ function handleSortChange() {
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="posts-list">
|
<div class="posts-list">
|
||||||
{#each filteredPosts as post (post.id)}
|
{#each filters.items as post (post.id)}
|
||||||
<PostListItem
|
<PostListItem
|
||||||
{post}
|
{post}
|
||||||
on:edit={handleEdit}
|
on:edit={handleEdit}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
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 Select from '$lib/components/admin/Select.svelte'
|
||||||
|
import { createListFilters, commonSorts } from '$lib/admin/listFilters.svelte'
|
||||||
import type { PageData } from './$types'
|
import type { PageData } from './$types'
|
||||||
import type { AdminProject } from '$lib/types/admin'
|
import type { AdminProject } from '$lib/types/admin'
|
||||||
|
|
||||||
|
|
@ -16,13 +17,27 @@
|
||||||
let showDeleteModal = false
|
let showDeleteModal = false
|
||||||
let projectToDelete: AdminProject | null = null
|
let projectToDelete: AdminProject | null = null
|
||||||
|
|
||||||
let selectedTypeFilter: string = 'all'
|
|
||||||
let selectedStatusFilter: string = 'all'
|
|
||||||
let sortBy: string = 'newest'
|
|
||||||
|
|
||||||
const actionError = form?.message ?? ''
|
const actionError = form?.message ?? ''
|
||||||
const projects = data.items ?? []
|
const projects = data.items ?? []
|
||||||
let filteredProjects = $state<AdminProject[]>([...projects])
|
|
||||||
|
// Create reactive filters
|
||||||
|
const filters = createListFilters(projects, {
|
||||||
|
filters: {
|
||||||
|
type: { field: 'projectType', default: 'all' },
|
||||||
|
status: { field: 'status', default: 'all' }
|
||||||
|
},
|
||||||
|
sorts: {
|
||||||
|
newest: commonSorts.dateDesc<AdminProject>('createdAt'),
|
||||||
|
oldest: commonSorts.dateAsc<AdminProject>('createdAt'),
|
||||||
|
'title-asc': commonSorts.stringAsc<AdminProject>('title'),
|
||||||
|
'title-desc': commonSorts.stringDesc<AdminProject>('title'),
|
||||||
|
'year-desc': commonSorts.numberDesc<AdminProject>('year'),
|
||||||
|
'year-asc': commonSorts.numberAsc<AdminProject>('year'),
|
||||||
|
'status-published': commonSorts.statusPublishedFirst<AdminProject>('status'),
|
||||||
|
'status-draft': commonSorts.statusDraftFirst<AdminProject>('status')
|
||||||
|
},
|
||||||
|
defaultSort: 'newest'
|
||||||
|
})
|
||||||
|
|
||||||
let toggleForm: HTMLFormElement | null = null
|
let toggleForm: HTMLFormElement | null = null
|
||||||
let toggleIdField: HTMLInputElement | null = null
|
let toggleIdField: HTMLInputElement | null = null
|
||||||
|
|
@ -55,68 +70,6 @@
|
||||||
{ value: 'status-draft', label: 'Draft first' }
|
{ value: 'status-draft', label: 'Draft first' }
|
||||||
]
|
]
|
||||||
|
|
||||||
function applyFilterAndSort() {
|
|
||||||
let next = [...projects]
|
|
||||||
|
|
||||||
if (selectedStatusFilter !== 'all') {
|
|
||||||
next = next.filter((project) => project.status === selectedStatusFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedTypeFilter !== 'all') {
|
|
||||||
next = next.filter((project) => project.projectType === selectedTypeFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 'year-desc':
|
|
||||||
next.sort((a, b) => b.year - a.year)
|
|
||||||
break
|
|
||||||
case 'year-asc':
|
|
||||||
next.sort((a, b) => a.year - b.year)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredProjects = next
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilterAndSort()
|
|
||||||
|
|
||||||
function handleTypeFilterChange() {
|
|
||||||
applyFilterAndSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleStatusFilterChange() {
|
|
||||||
applyFilterAndSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSortChange() {
|
|
||||||
applyFilterAndSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.addEventListener('click', handleOutsideClick)
|
document.addEventListener('click', handleOutsideClick)
|
||||||
return () => document.removeEventListener('click', handleOutsideClick)
|
return () => document.removeEventListener('click', handleOutsideClick)
|
||||||
|
|
@ -185,27 +138,27 @@
|
||||||
<AdminFilters>
|
<AdminFilters>
|
||||||
{#snippet left()}
|
{#snippet left()}
|
||||||
<Select
|
<Select
|
||||||
bind:value={selectedTypeFilter}
|
value={filters.values.type}
|
||||||
options={typeFilterOptions}
|
options={typeFilterOptions}
|
||||||
size="small"
|
size="small"
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
onchange={handleTypeFilterChange}
|
onchange={(e) => filters.set('type', (e.target as HTMLSelectElement).value)}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
bind:value={selectedStatusFilter}
|
value={filters.values.status}
|
||||||
options={statusFilterOptions}
|
options={statusFilterOptions}
|
||||||
size="small"
|
size="small"
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
onchange={handleStatusFilterChange}
|
onchange={(e) => filters.set('status', (e.target as HTMLSelectElement).value)}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet right()}
|
{#snippet right()}
|
||||||
<Select
|
<Select
|
||||||
bind:value={sortBy}
|
value={filters.sort}
|
||||||
options={sortOptions}
|
options={sortOptions}
|
||||||
size="small"
|
size="small"
|
||||||
variant="minimal"
|
variant="minimal"
|
||||||
onchange={handleSortChange}
|
onchange={(e) => filters.setSort((e.target as HTMLSelectElement).value)}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</AdminFilters>
|
</AdminFilters>
|
||||||
|
|
@ -214,11 +167,11 @@
|
||||||
<div class="error">{actionError}</div>
|
<div class="error">{actionError}</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if filteredProjects.length === 0}
|
{#if filters.items.length === 0}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>No projects found</h3>
|
<h3>No projects found</h3>
|
||||||
<p>
|
<p>
|
||||||
{#if selectedTypeFilter === 'all' && selectedStatusFilter === 'all'}
|
{#if filters.values.type === 'all' && filters.values.status === 'all'}
|
||||||
Create your first project to get started!
|
Create your first project to get started!
|
||||||
{:else}
|
{:else}
|
||||||
No projects found matching the current filters. Try adjusting your filters or create a new
|
No projects found matching the current filters. Try adjusting your filters or create a new
|
||||||
|
|
@ -228,7 +181,7 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="projects-list">
|
<div class="projects-list">
|
||||||
{#each filteredProjects as project (project.id)}
|
{#each filters.items as project (project.id)}
|
||||||
<ProjectListItem
|
<ProjectListItem
|
||||||
{project}
|
{project}
|
||||||
on:edit={handleEdit}
|
on:edit={handleEdit}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue