refactor(admin): migrate media list to use URL params and server loads
Refactored media list to follow SvelteKit data loading patterns:
- Removed client-side fetch() calls and manual state management
- Filter/sort/search state now driven by URL search params
- Page navigation triggers server-side reloads via goto()
- Mutations use invalidate('admin:media') to reload data
- Replaced error state with toast notifications for better UX
- Removed redundant loading state (handled by SvelteKit)
This completes Task 2 - all admin lists now use server-side data loading with proper session authentication.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
39e82146d9
commit
eebaf86b64
1 changed files with 60 additions and 96 deletions
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { goto, invalidate } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import AdminPage from '$lib/components/admin/AdminPage.svelte'
|
||||
import AdminHeader from '$lib/components/admin/AdminHeader.svelte'
|
||||
import AdminFilters from '$lib/components/admin/AdminFilters.svelte'
|
||||
|
|
@ -13,25 +14,24 @@
|
|||
import AlbumSelectorModal from '$lib/components/admin/AlbumSelectorModal.svelte'
|
||||
import ChevronDown from '$icons/chevron-down.svg?component'
|
||||
import PlayIcon from '$icons/play.svg?component'
|
||||
import { toast } from '$lib/stores/toast'
|
||||
import type { Media } from '@prisma/client'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
const { data } = $props<{ data: PageData }>()
|
||||
|
||||
let media = $state<Media[]>(data.items ?? [])
|
||||
let isLoading = $state(false)
|
||||
let error = $state('')
|
||||
let currentPage = $state(data.pagination?.page ?? 1)
|
||||
let totalPages = $state(data.pagination?.totalPages ?? 1)
|
||||
let total = $state(data.pagination?.total ?? (data.items?.length ?? 0))
|
||||
// Only using grid view
|
||||
const media = $derived(data.items ?? [])
|
||||
const currentPage = $derived(data.pagination?.page ?? 1)
|
||||
const totalPages = $derived(data.pagination?.totalPages ?? 1)
|
||||
const total = $derived(data.pagination?.total ?? 0)
|
||||
|
||||
// Filter states
|
||||
let filterType = $state<string>('all')
|
||||
let publishedFilter = $state<string>('all')
|
||||
let searchQuery = $state('')
|
||||
// Read filter states from URL
|
||||
const filterType = $derived($page.url.searchParams.get('mimeType') ?? 'all')
|
||||
const publishedFilter = $derived($page.url.searchParams.get('publishedFilter') ?? 'all')
|
||||
const sortBy = $derived($page.url.searchParams.get('sort') ?? 'newest')
|
||||
|
||||
let searchQuery = $state($page.url.searchParams.get('search') ?? '')
|
||||
let searchTimeout: ReturnType<typeof setTimeout>
|
||||
let sortBy = $state<string>('newest')
|
||||
|
||||
// Filter options
|
||||
const typeFilterOptions = [
|
||||
|
|
@ -77,64 +77,45 @@
|
|||
if (searchQuery !== undefined) {
|
||||
clearTimeout(searchTimeout)
|
||||
searchTimeout = setTimeout(() => {
|
||||
handleSearch()
|
||||
updateURL({ search: searchQuery || undefined })
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
async function loadMedia(page = 1) {
|
||||
try {
|
||||
isLoading = true
|
||||
let url = `/api/media?page=${page}&limit=24`
|
||||
if (filterType !== 'all') {
|
||||
url += `&mimeType=${filterType}`
|
||||
}
|
||||
if (publishedFilter !== 'all') {
|
||||
url += `&publishedFilter=${publishedFilter}`
|
||||
}
|
||||
if (searchQuery) {
|
||||
url += `&search=${encodeURIComponent(searchQuery)}`
|
||||
}
|
||||
if (sortBy) {
|
||||
url += `&sort=${sortBy}`
|
||||
}
|
||||
function updateURL(params: Record<string, string | undefined>) {
|
||||
const url = new URL($page.url)
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
// Update or remove params
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value && value !== 'all') {
|
||||
url.searchParams.set(key, value)
|
||||
} else {
|
||||
url.searchParams.delete(key)
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) throw new Error('Failed to load media')
|
||||
|
||||
const data = await response.json()
|
||||
media = data.media
|
||||
currentPage = data.pagination.page
|
||||
totalPages = data.pagination.totalPages
|
||||
total = data.pagination.total
|
||||
} catch (err) {
|
||||
error = 'Failed to load media'
|
||||
console.error(err)
|
||||
} finally {
|
||||
isLoading = false
|
||||
// Reset to page 1 if filters changed (not page navigation)
|
||||
if (!params.page) {
|
||||
url.searchParams.delete('page')
|
||||
}
|
||||
|
||||
goto(url.toString(), { replaceState: false, keepFocus: true })
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
loadMedia(page)
|
||||
updateURL({ page: String(page) })
|
||||
}
|
||||
|
||||
function handleFilterChange() {
|
||||
currentPage = 1
|
||||
loadMedia(1)
|
||||
function handleTypeFilterChange(value: string) {
|
||||
updateURL({ mimeType: value })
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
currentPage = 1
|
||||
loadMedia(1)
|
||||
function handlePublishedFilterChange(value: string) {
|
||||
updateURL({ publishedFilter: value })
|
||||
}
|
||||
|
||||
function handleSortChange() {
|
||||
currentPage = 1
|
||||
loadMedia(1)
|
||||
function handleSortChange(value: string) {
|
||||
updateURL({ sort: value })
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
|
|
@ -167,17 +148,14 @@
|
|||
isDetailsModalOpen = false
|
||||
}
|
||||
|
||||
function handleMediaUpdate(updatedMedia: Media) {
|
||||
// Update the media item in the list
|
||||
const index = media.findIndex((m) => m.id === updatedMedia.id)
|
||||
if (index !== -1) {
|
||||
media[index] = updatedMedia
|
||||
}
|
||||
async function handleMediaUpdate(updatedMedia: Media) {
|
||||
// Invalidate to reload from server
|
||||
await invalidate('admin:media')
|
||||
}
|
||||
|
||||
function handleUploadComplete() {
|
||||
async function handleUploadComplete() {
|
||||
// Reload media list after successful upload
|
||||
loadMedia(currentPage)
|
||||
await invalidate('admin:media')
|
||||
}
|
||||
|
||||
function openUploadModal() {
|
||||
|
|
@ -263,18 +241,15 @@
|
|||
}
|
||||
await response.json()
|
||||
|
||||
// Remove deleted media from the list
|
||||
media = media.filter((m) => !selectedMediaIds.has(m.id))
|
||||
|
||||
// Clear selection and exit multiselect mode
|
||||
selectedMediaIds.clear()
|
||||
selectedMediaIds = new Set()
|
||||
isMultiSelectMode = false
|
||||
|
||||
// Reload to get updated total count
|
||||
await loadMedia(currentPage)
|
||||
// Reload to get updated data
|
||||
await invalidate('admin:media')
|
||||
} catch (err) {
|
||||
error = 'Failed to delete media files. Please try again.'
|
||||
toast.error('Failed to delete media files. Please try again.')
|
||||
console.error('Failed to delete media:', err)
|
||||
} finally {
|
||||
isDeleting = false
|
||||
|
|
@ -304,17 +279,15 @@
|
|||
|
||||
await Promise.all(promises)
|
||||
|
||||
// Update local media items
|
||||
media = media.map((item) =>
|
||||
selectedMediaIds.has(item.id) ? { ...item, isPhotography: true } : item
|
||||
)
|
||||
|
||||
// Clear selection
|
||||
selectedMediaIds.clear()
|
||||
selectedMediaIds = new Set()
|
||||
isMultiSelectMode = false
|
||||
|
||||
// Reload to get updated data
|
||||
await invalidate('admin:media')
|
||||
} catch (err) {
|
||||
error = 'Failed to mark items as photography. Please try again.'
|
||||
toast.error('Failed to mark items as photography. Please try again.')
|
||||
console.error('Failed to mark as photography:', err)
|
||||
}
|
||||
}
|
||||
|
|
@ -342,17 +315,15 @@
|
|||
|
||||
await Promise.all(promises)
|
||||
|
||||
// Update local media items
|
||||
media = media.map((item) =>
|
||||
selectedMediaIds.has(item.id) ? { ...item, isPhotography: false } : item
|
||||
)
|
||||
|
||||
// Clear selection
|
||||
selectedMediaIds.clear()
|
||||
selectedMediaIds = new Set()
|
||||
isMultiSelectMode = false
|
||||
|
||||
// Reload to get updated data
|
||||
await invalidate('admin:media')
|
||||
} catch (err) {
|
||||
error = 'Failed to remove photography status. Please try again.'
|
||||
toast.error('Failed to remove photography status. Please try again.')
|
||||
console.error('Failed to unmark photography:', err)
|
||||
}
|
||||
}
|
||||
|
|
@ -388,39 +359,35 @@
|
|||
{/snippet}
|
||||
</AdminHeader>
|
||||
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{:else}
|
||||
<!-- Filters -->
|
||||
<!-- Filters -->
|
||||
<AdminFilters>
|
||||
{#snippet left()}
|
||||
<Select
|
||||
bind:value={filterType}
|
||||
value={filterType}
|
||||
options={typeFilterOptions}
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleFilterChange}
|
||||
onchange={(e) => handleTypeFilterChange((e.target as HTMLSelectElement).value)}
|
||||
/>
|
||||
<Select
|
||||
bind:value={publishedFilter}
|
||||
value={publishedFilter}
|
||||
options={publishedFilterOptions}
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleFilterChange}
|
||||
onchange={(e) => handlePublishedFilterChange((e.target as HTMLSelectElement).value)}
|
||||
/>
|
||||
{/snippet}
|
||||
{#snippet right()}
|
||||
<Select
|
||||
bind:value={sortBy}
|
||||
value={sortBy}
|
||||
options={sortOptions}
|
||||
size="small"
|
||||
variant="minimal"
|
||||
onchange={handleSortChange}
|
||||
onchange={(e) => handleSortChange((e.target as HTMLSelectElement).value)}
|
||||
/>
|
||||
<Input
|
||||
type="search"
|
||||
bind:value={searchQuery}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="Search files..."
|
||||
buttonSize="small"
|
||||
fullWidth={false}
|
||||
|
|
@ -500,9 +467,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="loading">Loading media...</div>
|
||||
{:else if media.length === 0}
|
||||
{#if media.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>No media files found.</p>
|
||||
<Button variant="primary" onclick={openUploadModal}>Upload your first file</Button>
|
||||
|
|
@ -601,7 +566,6 @@
|
|||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</AdminPage>
|
||||
|
||||
<!-- Media Details Modal -->
|
||||
|
|
|
|||
Loading…
Reference in a new issue