Add sorts in admin interface
This commit is contained in:
parent
f753d5fb8b
commit
81af86ae7f
5 changed files with 469 additions and 47 deletions
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import AdminByline from './AdminByline.svelte'
|
||||
|
||||
interface Post {
|
||||
|
|
@ -23,15 +24,59 @@
|
|||
|
||||
let { post }: Props = $props()
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
edit: { post: Post }
|
||||
togglePublish: { post: Post }
|
||||
delete: { post: Post }
|
||||
}>()
|
||||
|
||||
let isDropdownOpen = $state(false)
|
||||
|
||||
const postTypeLabels: Record<string, string> = {
|
||||
post: 'Post',
|
||||
essay: 'Essay'
|
||||
}
|
||||
|
||||
function handlePostClick() {
|
||||
function handlePostClick(event: MouseEvent) {
|
||||
// Don't navigate if clicking on the dropdown button
|
||||
if ((event.target as HTMLElement).closest('.dropdown-container')) {
|
||||
return
|
||||
}
|
||||
goto(`/admin/posts/${post.id}/edit`)
|
||||
}
|
||||
|
||||
function handleToggleDropdown(event: MouseEvent) {
|
||||
event.stopPropagation()
|
||||
isDropdownOpen = !isDropdownOpen
|
||||
}
|
||||
|
||||
function handleEdit(event: MouseEvent) {
|
||||
event.stopPropagation()
|
||||
dispatch('edit', { post })
|
||||
goto(`/admin/posts/${post.id}/edit`)
|
||||
}
|
||||
|
||||
function handleTogglePublish(event: MouseEvent) {
|
||||
event.stopPropagation()
|
||||
dispatch('togglePublish', { post })
|
||||
isDropdownOpen = false
|
||||
}
|
||||
|
||||
function handleDelete(event: MouseEvent) {
|
||||
event.stopPropagation()
|
||||
dispatch('delete', { post })
|
||||
isDropdownOpen = false
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
function handleCloseDropdowns() {
|
||||
isDropdownOpen = false
|
||||
}
|
||||
|
||||
document.addEventListener('closeDropdowns', handleCloseDropdowns)
|
||||
return () => document.removeEventListener('closeDropdowns', handleCloseDropdowns)
|
||||
})
|
||||
|
||||
function getPostSnippet(post: Post): string {
|
||||
// Try excerpt first
|
||||
if (post.excerpt) {
|
||||
|
|
@ -95,23 +140,52 @@
|
|||
</script>
|
||||
|
||||
<article class="post-item" onclick={handlePostClick}>
|
||||
{#if post.title}
|
||||
<h3 class="post-title">{post.title}</h3>
|
||||
{/if}
|
||||
<div class="post-main">
|
||||
{#if post.title}
|
||||
<h3 class="post-title">{post.title}</h3>
|
||||
{/if}
|
||||
|
||||
<div class="post-content">
|
||||
<p class="post-preview">{getPostSnippet(post)}</p>
|
||||
<div class="post-content">
|
||||
<p class="post-preview">{getPostSnippet(post)}</p>
|
||||
</div>
|
||||
|
||||
<AdminByline
|
||||
sections={[
|
||||
postTypeLabels[post.postType] || post.postType,
|
||||
post.status === 'published' ? 'Published' : 'Draft',
|
||||
post.status === 'published' && post.publishedAt
|
||||
? `published ${formatDate(post.publishedAt)}`
|
||||
: `created ${formatDate(post.createdAt)}`
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<AdminByline
|
||||
sections={[
|
||||
postTypeLabels[post.postType] || post.postType,
|
||||
post.status === 'published' ? 'Published' : 'Draft',
|
||||
post.status === 'published' && post.publishedAt
|
||||
? `published ${formatDate(post.publishedAt)}`
|
||||
: `created ${formatDate(post.createdAt)}`
|
||||
]}
|
||||
/>
|
||||
<div class="dropdown-container">
|
||||
<button class="action-button" onclick={handleToggleDropdown} aria-label="Post actions">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle cx="10" cy="4" r="1.5" fill="currentColor" />
|
||||
<circle cx="10" cy="10" r="1.5" fill="currentColor" />
|
||||
<circle cx="10" cy="16" r="1.5" fill="currentColor" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{#if isDropdownOpen}
|
||||
<div class="dropdown-menu">
|
||||
<button class="dropdown-item" onclick={handleEdit}>Edit post</button>
|
||||
<button class="dropdown-item" onclick={handleTogglePublish}>
|
||||
{post.status === 'published' ? 'Unpublish' : 'Publish'} post
|
||||
</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item danger" onclick={handleDelete}>Delete post</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
@ -123,7 +197,8 @@
|
|||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: $unit-2x;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -131,6 +206,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
.post-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.post-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
|
|
@ -163,6 +246,70 @@
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: $unit;
|
||||
cursor: pointer;
|
||||
color: $grey-30;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: $unit-half;
|
||||
background: white;
|
||||
border: 1px solid $grey-85;
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-width: 180px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
width: 100%;
|
||||
padding: $unit-2x $unit-3x;
|
||||
background: none;
|
||||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-20;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-95;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: $red-60;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-90;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@media (max-width: 768px) {
|
||||
.post-item {
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@
|
|||
|
||||
// Filter state
|
||||
let photographyFilter = $state<string>('all')
|
||||
let sortBy = $state<string>('newest')
|
||||
|
||||
// Filter options
|
||||
const filterOptions = [
|
||||
|
|
@ -57,6 +58,17 @@
|
|||
{ value: 'false', label: 'Regular albums' }
|
||||
]
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'newest', label: 'Newest first' },
|
||||
{ value: 'oldest', label: 'Oldest first' },
|
||||
{ value: 'title-asc', label: 'Title (A-Z)' },
|
||||
{ value: 'title-desc', label: 'Title (Z-A)' },
|
||||
{ value: 'date-desc', label: 'Date (newest)' },
|
||||
{ value: 'date-asc', label: 'Date (oldest)' },
|
||||
{ value: 'status-published', label: 'Published first' },
|
||||
{ value: 'status-draft', label: 'Draft first' }
|
||||
]
|
||||
|
||||
onMount(async () => {
|
||||
await loadAlbums()
|
||||
// Close dropdown when clicking outside
|
||||
|
|
@ -103,8 +115,8 @@
|
|||
}
|
||||
albumTypeCounts = counts
|
||||
|
||||
// Apply initial filter
|
||||
applyFilter()
|
||||
// Apply initial filter and sort
|
||||
applyFilterAndSort()
|
||||
} catch (err) {
|
||||
error = 'Failed to load albums'
|
||||
console.error(err)
|
||||
|
|
@ -113,14 +125,62 @@
|
|||
}
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
if (photographyFilter === 'all') {
|
||||
filteredAlbums = albums
|
||||
} else if (photographyFilter === 'true') {
|
||||
filteredAlbums = albums.filter((album) => album.isPhotography === true)
|
||||
function applyFilterAndSort() {
|
||||
let filtered = [...albums]
|
||||
|
||||
// Apply filter
|
||||
if (photographyFilter === 'true') {
|
||||
filtered = filtered.filter((album) => album.isPhotography === true)
|
||||
} else if (photographyFilter === 'false') {
|
||||
filteredAlbums = albums.filter((album) => album.isPhotography === false)
|
||||
filtered = filtered.filter((album) => album.isPhotography === false)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
switch (sortBy) {
|
||||
case 'oldest':
|
||||
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
break
|
||||
case 'title-asc':
|
||||
filtered.sort((a, b) => a.title.localeCompare(b.title))
|
||||
break
|
||||
case 'title-desc':
|
||||
filtered.sort((a, b) => b.title.localeCompare(a.title))
|
||||
break
|
||||
case 'date-desc':
|
||||
filtered.sort((a, b) => {
|
||||
if (!a.date && !b.date) return 0
|
||||
if (!a.date) return 1
|
||||
if (!b.date) return -1
|
||||
return new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
})
|
||||
break
|
||||
case 'date-asc':
|
||||
filtered.sort((a, b) => {
|
||||
if (!a.date && !b.date) return 0
|
||||
if (!a.date) return 1
|
||||
if (!b.date) return -1
|
||||
return new Date(a.date).getTime() - new Date(b.date).getTime()
|
||||
})
|
||||
break
|
||||
case 'status-published':
|
||||
filtered.sort((a, b) => {
|
||||
if (a.status === b.status) return 0
|
||||
return a.status === 'published' ? -1 : 1
|
||||
})
|
||||
break
|
||||
case 'status-draft':
|
||||
filtered.sort((a, b) => {
|
||||
if (a.status === b.status) return 0
|
||||
return a.status === 'draft' ? -1 : 1
|
||||
})
|
||||
break
|
||||
case 'newest':
|
||||
default:
|
||||
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
break
|
||||
}
|
||||
|
||||
filteredAlbums = filtered
|
||||
}
|
||||
|
||||
function handleToggleDropdown(event: CustomEvent<{ albumId: number; event: MouseEvent }>) {
|
||||
|
|
@ -199,7 +259,11 @@
|
|||
}
|
||||
|
||||
function handleFilterChange() {
|
||||
applyFilter()
|
||||
applyFilterAndSort()
|
||||
}
|
||||
|
||||
function handleSortChange() {
|
||||
applyFilterAndSort()
|
||||
}
|
||||
|
||||
function handleNewAlbum() {
|
||||
|
|
@ -223,11 +287,20 @@
|
|||
<Select
|
||||
bind:value={photographyFilter}
|
||||
options={filterOptions}
|
||||
buttonSize="small"
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleFilterChange}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet right()}
|
||||
<Select
|
||||
bind:value={sortBy}
|
||||
options={sortOptions}
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleSortChange}
|
||||
/>
|
||||
{/snippet}
|
||||
</AdminFilters>
|
||||
|
||||
<!-- Albums List -->
|
||||
|
|
@ -253,10 +326,10 @@
|
|||
<AlbumListItem
|
||||
{album}
|
||||
isDropdownActive={activeDropdown === album.id}
|
||||
ontoggleDropdown={handleToggleDropdown}
|
||||
onedit={handleEdit}
|
||||
ontogglePublish={handleTogglePublish}
|
||||
ondelete={handleDelete}
|
||||
on:toggleDropdown={handleToggleDropdown}
|
||||
on:edit={handleEdit}
|
||||
on:togglePublish={handleTogglePublish}
|
||||
on:delete={handleDelete}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -290,7 +363,7 @@
|
|||
width: 32px;
|
||||
height: 32px;
|
||||
border: 3px solid $grey-80;
|
||||
border-top-color: $primary-color;
|
||||
border-top-color: $grey-40;
|
||||
border-radius: 50%;
|
||||
margin: 0 auto $unit-2x;
|
||||
animation: spin 0.8s linear infinite;
|
||||
|
|
|
|||
|
|
@ -339,14 +339,14 @@
|
|||
<Select
|
||||
bind:value={filterType}
|
||||
options={typeFilterOptions}
|
||||
buttonSize="small"
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleFilterChange}
|
||||
/>
|
||||
<Select
|
||||
bind:value={photographyFilter}
|
||||
options={photographyFilterOptions}
|
||||
buttonSize="small"
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleFilterChange}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
import LoadingSpinner from '$lib/components/admin/LoadingSpinner.svelte'
|
||||
import Select from '$lib/components/admin/Select.svelte'
|
||||
import UniverseComposer from '$lib/components/admin/UniverseComposer.svelte'
|
||||
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
|
||||
import Button from '$lib/components/admin/Button.svelte'
|
||||
|
||||
interface Post {
|
||||
id: number
|
||||
|
|
@ -35,11 +37,16 @@
|
|||
// Filter state
|
||||
let selectedTypeFilter = $state<string>('all')
|
||||
let selectedStatusFilter = $state<string>('all')
|
||||
let sortBy = $state<string>('newest')
|
||||
|
||||
// Composer state
|
||||
let showInlineComposer = $state(true)
|
||||
let isInteractingWithFilters = $state(false)
|
||||
|
||||
// Delete confirmation state
|
||||
let showDeleteConfirmation = $state(false)
|
||||
let postToDelete = $state<Post | null>(null)
|
||||
|
||||
// Create filter options
|
||||
const typeFilterOptions = $derived([
|
||||
{ value: 'all', label: 'All posts' },
|
||||
|
|
@ -53,6 +60,15 @@
|
|||
{ value: 'draft', label: 'Draft' }
|
||||
])
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'newest', label: 'Newest first' },
|
||||
{ value: 'oldest', label: 'Oldest first' },
|
||||
{ value: 'title-asc', label: 'Title (A-Z)' },
|
||||
{ value: 'title-desc', label: 'Title (Z-A)' },
|
||||
{ value: 'status-published', label: 'Published first' },
|
||||
{ value: 'status-draft', label: 'Draft first' }
|
||||
]
|
||||
|
||||
const postTypeIcons: Record<string, string> = {
|
||||
post: '💭',
|
||||
essay: '📝'
|
||||
|
|
@ -115,8 +131,8 @@
|
|||
}
|
||||
statusCounts = statusCountsTemp
|
||||
|
||||
// Apply initial filter
|
||||
applyFilter()
|
||||
// Apply initial filter and sort
|
||||
applyFilterAndSort()
|
||||
} catch (err) {
|
||||
error = 'Failed to load posts'
|
||||
console.error(err)
|
||||
|
|
@ -125,8 +141,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
let filtered = posts
|
||||
function applyFilterAndSort() {
|
||||
let filtered = [...posts]
|
||||
|
||||
// Apply type filter
|
||||
if (selectedTypeFilter !== 'all') {
|
||||
|
|
@ -138,25 +154,126 @@
|
|||
filtered = filtered.filter((post) => post.status === selectedStatusFilter)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
switch (sortBy) {
|
||||
case 'oldest':
|
||||
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
break
|
||||
case 'title-asc':
|
||||
filtered.sort((a, b) => (a.title || '').localeCompare(b.title || ''))
|
||||
break
|
||||
case 'title-desc':
|
||||
filtered.sort((a, b) => (b.title || '').localeCompare(a.title || ''))
|
||||
break
|
||||
case 'status-published':
|
||||
filtered.sort((a, b) => {
|
||||
if (a.status === b.status) return 0
|
||||
return a.status === 'published' ? -1 : 1
|
||||
})
|
||||
break
|
||||
case 'status-draft':
|
||||
filtered.sort((a, b) => {
|
||||
if (a.status === b.status) return 0
|
||||
return a.status === 'draft' ? -1 : 1
|
||||
})
|
||||
break
|
||||
case 'newest':
|
||||
default:
|
||||
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
break
|
||||
}
|
||||
|
||||
filteredPosts = filtered
|
||||
}
|
||||
|
||||
function handleTypeFilterChange() {
|
||||
applyFilter()
|
||||
applyFilterAndSort()
|
||||
}
|
||||
|
||||
function handleStatusFilterChange() {
|
||||
applyFilter()
|
||||
applyFilterAndSort()
|
||||
}
|
||||
|
||||
function handleSortChange() {
|
||||
applyFilterAndSort()
|
||||
}
|
||||
|
||||
function handleComposerSaved() {
|
||||
// Reload posts when a new post is created
|
||||
loadPosts()
|
||||
}
|
||||
|
||||
function handleNewEssay() {
|
||||
goto('/admin/posts/new?type=essay')
|
||||
}
|
||||
|
||||
async function handleTogglePublish(event: CustomEvent<{ post: Post }>) {
|
||||
const { post } = event.detail
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
goto('/admin/login')
|
||||
return
|
||||
}
|
||||
|
||||
const newStatus = post.status === 'published' ? 'draft' : 'published'
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/posts/${post.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${auth}`
|
||||
},
|
||||
body: JSON.stringify({ status: newStatus })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Reload posts to refresh the list
|
||||
await loadPosts()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle publish status:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeletePost(event: CustomEvent<{ post: Post }>) {
|
||||
postToDelete = event.detail.post
|
||||
showDeleteConfirmation = true
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!postToDelete) return
|
||||
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
goto('/admin/login')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/posts/${postToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Basic ${auth}` }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
showDeleteConfirmation = false
|
||||
postToDelete = null
|
||||
// Reload posts to refresh the list
|
||||
await loadPosts()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete post:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AdminPage>
|
||||
<AdminHeader title="Universe" slot="header" />
|
||||
<AdminHeader title="Universe" slot="header">
|
||||
{#snippet actions()}
|
||||
<Button variant="primary" buttonSize="large" onclick={handleNewEssay}>New Essay</Button>
|
||||
{/snippet}
|
||||
</AdminHeader>
|
||||
|
||||
{#if error}
|
||||
<div class="error-message">{error}</div>
|
||||
|
|
@ -192,6 +309,15 @@
|
|||
onchange={handleStatusFilterChange}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet right()}
|
||||
<Select
|
||||
bind:value={sortBy}
|
||||
options={sortOptions}
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleSortChange}
|
||||
/>
|
||||
{/snippet}
|
||||
</AdminFilters>
|
||||
|
||||
<!-- Posts List -->
|
||||
|
|
@ -215,13 +341,29 @@
|
|||
{:else}
|
||||
<div class="posts-list">
|
||||
{#each filteredPosts as post}
|
||||
<PostListItem {post} />
|
||||
<PostListItem
|
||||
{post}
|
||||
on:togglePublish={handleTogglePublish}
|
||||
on:delete={handleDeletePost}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</AdminPage>
|
||||
|
||||
<DeleteConfirmationModal
|
||||
bind:isOpen={showDeleteConfirmation}
|
||||
title="Delete Post?"
|
||||
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
|
||||
}}
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$styles/variables.scss';
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
// Filter state
|
||||
let selectedTypeFilter = $state<string>('all')
|
||||
let selectedStatusFilter = $state<string>('all')
|
||||
let sortBy = $state<string>('newest')
|
||||
|
||||
// Create filter options
|
||||
const typeFilterOptions = $derived([
|
||||
|
|
@ -50,6 +51,17 @@
|
|||
{ value: 'draft', label: 'Draft' }
|
||||
])
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'newest', label: 'Newest first' },
|
||||
{ value: 'oldest', label: 'Oldest first' },
|
||||
{ value: 'title-asc', label: 'Title (A-Z)' },
|
||||
{ value: 'title-desc', label: 'Title (Z-A)' },
|
||||
{ value: 'year-desc', label: 'Year (newest)' },
|
||||
{ value: 'year-asc', label: 'Year (oldest)' },
|
||||
{ value: 'status-published', label: 'Published first' },
|
||||
{ value: 'status-draft', label: 'Draft first' }
|
||||
]
|
||||
|
||||
onMount(async () => {
|
||||
await loadProjects()
|
||||
// Handle clicks outside dropdowns
|
||||
|
|
@ -96,8 +108,8 @@
|
|||
}
|
||||
statusCounts = counts
|
||||
|
||||
// Apply initial filter
|
||||
applyFilter()
|
||||
// Apply initial filter and sort
|
||||
applyFilterAndSort()
|
||||
} catch (err) {
|
||||
error = 'Failed to load projects'
|
||||
console.error(err)
|
||||
|
|
@ -166,8 +178,8 @@
|
|||
projectToDelete = null
|
||||
}
|
||||
|
||||
function applyFilter() {
|
||||
let filtered = projects
|
||||
function applyFilterAndSort() {
|
||||
let filtered = [...projects]
|
||||
|
||||
// Apply status filter
|
||||
if (selectedStatusFilter !== 'all') {
|
||||
|
|
@ -179,15 +191,54 @@
|
|||
filtered = filtered.filter((project) => project.projectType === selectedTypeFilter)
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
switch (sortBy) {
|
||||
case 'oldest':
|
||||
filtered.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
|
||||
break
|
||||
case 'title-asc':
|
||||
filtered.sort((a, b) => a.title.localeCompare(b.title))
|
||||
break
|
||||
case 'title-desc':
|
||||
filtered.sort((a, b) => b.title.localeCompare(a.title))
|
||||
break
|
||||
case 'year-desc':
|
||||
filtered.sort((a, b) => b.year - a.year)
|
||||
break
|
||||
case 'year-asc':
|
||||
filtered.sort((a, b) => a.year - b.year)
|
||||
break
|
||||
case 'status-published':
|
||||
filtered.sort((a, b) => {
|
||||
if (a.status === b.status) return 0
|
||||
return a.status === 'published' ? -1 : 1
|
||||
})
|
||||
break
|
||||
case 'status-draft':
|
||||
filtered.sort((a, b) => {
|
||||
if (a.status === b.status) return 0
|
||||
return a.status === 'draft' ? -1 : 1
|
||||
})
|
||||
break
|
||||
case 'newest':
|
||||
default:
|
||||
filtered.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
|
||||
break
|
||||
}
|
||||
|
||||
filteredProjects = filtered
|
||||
}
|
||||
|
||||
function handleStatusFilterChange() {
|
||||
applyFilter()
|
||||
applyFilterAndSort()
|
||||
}
|
||||
|
||||
function handleTypeFilterChange() {
|
||||
applyFilter()
|
||||
applyFilterAndSort()
|
||||
}
|
||||
|
||||
function handleSortChange() {
|
||||
applyFilterAndSort()
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -219,6 +270,15 @@
|
|||
onchange={handleStatusFilterChange}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet right()}
|
||||
<Select
|
||||
bind:value={sortBy}
|
||||
options={sortOptions}
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleSortChange}
|
||||
/>
|
||||
{/snippet}
|
||||
</AdminFilters>
|
||||
|
||||
<!-- Projects List -->
|
||||
|
|
|
|||
Loading…
Reference in a new issue