jedmund-svelte/src/routes/admin/projects/+page.svelte
Justin Edmund 4337b57dee refactor: migrate createEventDispatcher to Svelte 5 callback props
Migrate 5 components from Svelte 4 createEventDispatcher to Svelte 5 callback props:
- DropdownMenu.svelte (removed unused dispatcher)
- ProjectListItem.svelte (edit, togglePublish, delete events)
- PostListItem.svelte (edit, togglePublish, delete events)
- AlbumListItem.svelte (toggleDropdown, edit, togglePublish, delete events)
- InlineComposerModal.svelte (close, saved events + migrate export let to $props)

Updated parent components to use onevent={handler} syntax instead of on:event={handler}.
2025-11-04 19:35:53 -08:00

211 lines
6.3 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation'
import AdminPage from '$lib/components/admin/AdminPage.svelte'
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
import ProjectListItem from '$lib/components/admin/ProjectListItem.svelte'
import DeleteConfirmationModal from '$lib/components/admin/DeleteConfirmationModal.svelte'
import EmptyState from '$lib/components/admin/EmptyState.svelte'
import ErrorMessage from '$lib/components/admin/ErrorMessage.svelte'
import Button from '$lib/components/admin/Button.svelte'
import Select from '$lib/components/admin/Select.svelte'
import { createListFilters, commonSorts } from '$lib/admin/listFilters.svelte'
import type { PageData } from './$types'
import type { AdminProject } from '$lib/types/admin'
const { data, form } = $props<{ data: PageData; form?: { message?: string } }>()
let showDeleteModal = false
let projectToDelete: AdminProject | null = null
const actionError = form?.message ?? ''
const projects = data.items ?? []
// 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 toggleIdField: HTMLInputElement | null = null
let toggleStatusField: HTMLInputElement | null = null
let toggleUpdatedAtField: HTMLInputElement | null = null
let deleteForm: HTMLFormElement | null = null
let deleteIdField: HTMLInputElement | null = null
const typeFilterOptions = [
{ value: 'all', label: 'All projects' },
{ value: 'work', label: 'Work' },
{ value: 'labs', label: 'Labs' }
]
const statusFilterOptions = [
{ value: 'all', label: 'All statuses' },
{ value: 'published', label: 'Published' },
{ 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' }
]
function handleEdit(event: CustomEvent<{ project: AdminProject }>) {
goto(`/admin/projects/${event.detail.project.id}/edit`)
}
function handleTogglePublish(event: CustomEvent<{ project: AdminProject }>) {
const project = event.detail.project
if (!toggleForm || !toggleIdField || !toggleStatusField || !toggleUpdatedAtField) {
return
}
toggleIdField.value = String(project.id)
toggleStatusField.value = project.status === 'published' ? 'draft' : 'published'
toggleUpdatedAtField.value = project.updatedAt
toggleForm.requestSubmit()
}
function handleDelete(event: CustomEvent<{ project: AdminProject }>) {
projectToDelete = event.detail.project
showDeleteModal = true
}
function confirmDelete() {
if (!projectToDelete || !deleteForm || !deleteIdField) return
deleteIdField.value = String(projectToDelete.id)
showDeleteModal = false
deleteForm.requestSubmit()
projectToDelete = null
}
function cancelDelete() {
showDeleteModal = false
projectToDelete = null
}
</script>
<svelte:head>
<title>Work - Admin @jedmund</title>
</svelte:head>
<AdminPage>
<AdminHeader title="Work" slot="header">
{#snippet actions()}
<Button
variant="primary"
buttonSize="medium"
onclick={() => goto('/admin/projects/new')}
>
New project
</Button>
{/snippet}
</AdminHeader>
<AdminFilters>
{#snippet left()}
<Select
value={filters.values.type}
options={typeFilterOptions}
size="small"
variant="minimal"
onchange={(e) => filters.set('type', (e.target as HTMLSelectElement).value)}
/>
<Select
value={filters.values.status}
options={statusFilterOptions}
size="small"
variant="minimal"
onchange={(e) => filters.set('status', (e.target as HTMLSelectElement).value)}
/>
{/snippet}
{#snippet right()}
<Select
value={filters.sort}
options={sortOptions}
size="small"
variant="minimal"
onchange={(e) => filters.setSort((e.target as HTMLSelectElement).value)}
/>
{/snippet}
</AdminFilters>
{#if actionError}
<ErrorMessage message={actionError} />
{/if}
{#if filters.items.length === 0}
<EmptyState
title="No projects found"
message={filters.values.type === 'all' && filters.values.status === 'all'
? 'Create your first project to get started!'
: 'No projects found matching the current filters. Try adjusting your filters or create a new project.'}
/>
{:else}
<div class="projects-list">
{#each filters.items as project (project.id)}
<ProjectListItem
{project}
onedit={handleEdit}
ontogglepublish={handleTogglePublish}
ondelete={handleDelete}
/>
{/each}
</div>
{/if}
</AdminPage>
<DeleteConfirmationModal
bind:isOpen={showDeleteModal}
title="Delete Project?"
message="Are you sure you want to delete this project? This action cannot be undone."
confirmText="Delete Project"
onConfirm={confirmDelete}
onCancel={cancelDelete}
/>
<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">
@import '$styles/variables.scss';
.projects-list {
display: flex;
flex-direction: column;
gap: $unit-2x;
}
.hidden-form {
display: none;
}
</style>