diff --git a/src/lib/admin/api.ts b/src/lib/admin/api.ts index f41b7fa..acbc331 100644 --- a/src/lib/admin/api.ts +++ b/src/lib/admin/api.ts @@ -3,90 +3,99 @@ import { goto } from '$app/navigation' export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' export interface RequestOptions { - method?: HttpMethod - body?: TBody - signal?: AbortSignal - headers?: Record + method?: HttpMethod + body?: TBody + signal?: AbortSignal + headers?: Record } export interface ApiError extends Error { - status: number - details?: unknown + status: number + details?: unknown } function getAuthHeader() { - return {} + return {} } async function handleResponse(res: Response) { - if (res.status === 401) { - // Redirect to login for unauthorized requests - try { - goto('/admin/login') - } catch {} - } + if (res.status === 401) { + // Redirect to login for unauthorized requests + try { + goto('/admin/login') + } catch {} + } - const contentType = res.headers.get('content-type') || '' - const isJson = contentType.includes('application/json') - const data = isJson ? await res.json().catch(() => undefined) : undefined + const contentType = res.headers.get('content-type') || '' + const isJson = contentType.includes('application/json') + const data = isJson ? await res.json().catch(() => undefined) : undefined - if (!res.ok) { - const err: ApiError = Object.assign(new Error('Request failed'), { - status: res.status, - details: data - }) - throw err - } - return data + if (!res.ok) { + const err: ApiError = Object.assign(new Error('Request failed'), { + status: res.status, + details: data + }) + throw err + } + return data } export async function request( - url: string, - opts: RequestOptions = {} + url: string, + opts: RequestOptions = {} ): Promise { - const { method = 'GET', body, signal, headers } = opts + const { method = 'GET', body, signal, headers } = opts - const isFormData = typeof FormData !== 'undefined' && body instanceof FormData - const mergedHeaders: Record = { - ...(isFormData ? {} : { 'Content-Type': 'application/json' }), - ...getAuthHeader(), - ...(headers || {}) - } + const isFormData = typeof FormData !== 'undefined' && body instanceof FormData + const mergedHeaders: Record = { + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), + ...getAuthHeader(), + ...(headers || {}) + } - const res = await fetch(url, { - method, - headers: mergedHeaders, - body: body ? (isFormData ? (body as any) : JSON.stringify(body)) : undefined, - signal, - credentials: 'same-origin' - }) + const res = await fetch(url, { + method, + headers: mergedHeaders, + body: body ? (isFormData ? (body as any) : JSON.stringify(body)) : undefined, + signal, + credentials: 'same-origin' + }) - return handleResponse(res) as Promise + return handleResponse(res) as Promise } export const api = { - get: (url: string, opts: Omit = {}) => - request(url, { ...opts, method: 'GET' }), - post: (url: string, body: B, opts: Omit, 'method' | 'body'> = {}) => - request(url, { ...opts, method: 'POST', body }), - put: (url: string, body: B, opts: Omit, 'method' | 'body'> = {}) => - request(url, { ...opts, method: 'PUT', body }), - patch: (url: string, body: B, opts: Omit, 'method' | 'body'> = {}) => - request(url, { ...opts, method: 'PATCH', body }), - delete: (url: string, opts: Omit = {}) => - request(url, { ...opts, method: 'DELETE' }) + get: (url: string, opts: Omit = {}) => + request(url, { ...opts, method: 'GET' }), + post: ( + url: string, + body: B, + opts: Omit, 'method' | 'body'> = {} + ) => request(url, { ...opts, method: 'POST', body }), + put: ( + url: string, + body: B, + opts: Omit, 'method' | 'body'> = {} + ) => request(url, { ...opts, method: 'PUT', body }), + patch: ( + url: string, + body: B, + opts: Omit, 'method' | 'body'> = {} + ) => request(url, { ...opts, method: 'PATCH', body }), + delete: (url: string, opts: Omit = {}) => + request(url, { ...opts, method: 'DELETE' }) } export function createAbortable() { - let controller: AbortController | null = null - return { - nextSignal() { - if (controller) controller.abort() - controller = new AbortController() - return controller.signal - }, - abort() { - if (controller) controller.abort() - } - } + let controller: AbortController | null = null + return { + nextSignal() { + if (controller) controller.abort() + controller = new AbortController() + return controller.signal + }, + abort() { + if (controller) controller.abort() + } + } } diff --git a/src/lib/admin/autoSave.svelte.ts b/src/lib/admin/autoSave.svelte.ts index 5f71242..30e72d5 100644 --- a/src/lib/admin/autoSave.svelte.ts +++ b/src/lib/admin/autoSave.svelte.ts @@ -1,20 +1,20 @@ export type AutoSaveStatus = 'idle' | 'saving' | 'saved' | 'error' | 'offline' export interface AutoSaveStoreOptions { - debounceMs?: number - idleResetMs?: number - getPayload: () => TPayload | null | undefined - save: (payload: TPayload, ctx: { signal: AbortSignal }) => Promise - onSaved?: (res: TResponse, ctx: { prime: (payload: TPayload) => void }) => void + debounceMs?: number + idleResetMs?: number + getPayload: () => TPayload | null | undefined + save: (payload: TPayload, ctx: { signal: AbortSignal }) => Promise + onSaved?: (res: TResponse, ctx: { prime: (payload: TPayload) => void }) => void } export interface AutoSaveStore { - readonly status: AutoSaveStatus - readonly lastError: string | null - schedule: () => void - flush: () => Promise - destroy: () => void - prime: (payload: TPayload) => void + readonly status: AutoSaveStatus + readonly lastError: string | null + schedule: () => void + flush: () => Promise + destroy: () => void + prime: (payload: TPayload) => void } /** @@ -35,109 +35,109 @@ export interface AutoSaveStore { * // Trigger save: autoSave.schedule() */ export function createAutoSaveStore( - opts: AutoSaveStoreOptions + opts: AutoSaveStoreOptions ): AutoSaveStore { - const debounceMs = opts.debounceMs ?? 2000 - const idleResetMs = opts.idleResetMs ?? 2000 - let timer: ReturnType | null = null - let idleResetTimer: ReturnType | null = null - let controller: AbortController | null = null - let lastSentHash: string | null = null + const debounceMs = opts.debounceMs ?? 2000 + const idleResetMs = opts.idleResetMs ?? 2000 + let timer: ReturnType | null = null + let idleResetTimer: ReturnType | null = null + let controller: AbortController | null = null + let lastSentHash: string | null = null - let status = $state('idle') - let lastError = $state(null) + let status = $state('idle') + let lastError = $state(null) - function setStatus(next: AutoSaveStatus) { - if (idleResetTimer) { - clearTimeout(idleResetTimer) - idleResetTimer = null - } + function setStatus(next: AutoSaveStatus) { + if (idleResetTimer) { + clearTimeout(idleResetTimer) + idleResetTimer = null + } - status = next + status = next - // Auto-transition from 'saved' to 'idle' after idleResetMs - if (next === 'saved') { - idleResetTimer = setTimeout(() => { - status = 'idle' - idleResetTimer = null - }, idleResetMs) - } - } + // Auto-transition from 'saved' to 'idle' after idleResetMs + if (next === 'saved') { + idleResetTimer = setTimeout(() => { + status = 'idle' + idleResetTimer = null + }, idleResetMs) + } + } - function prime(payload: TPayload) { - lastSentHash = safeHash(payload) - } + function prime(payload: TPayload) { + lastSentHash = safeHash(payload) + } - function schedule() { - if (timer) clearTimeout(timer) - timer = setTimeout(() => void run(), debounceMs) - } + function schedule() { + if (timer) clearTimeout(timer) + timer = setTimeout(() => void run(), debounceMs) + } - async function run() { - if (timer) { - clearTimeout(timer) - timer = null - } + async function run() { + if (timer) { + clearTimeout(timer) + timer = null + } - const payload = opts.getPayload() - if (!payload) return + const payload = opts.getPayload() + if (!payload) return - const hash = safeHash(payload) - if (lastSentHash && hash === lastSentHash) return + const hash = safeHash(payload) + if (lastSentHash && hash === lastSentHash) return - if (controller) controller.abort() - controller = new AbortController() + if (controller) controller.abort() + controller = new AbortController() - setStatus('saving') - lastError = null - try { - const res = await opts.save(payload, { signal: controller.signal }) - lastSentHash = hash - setStatus('saved') - if (opts.onSaved) opts.onSaved(res, { prime }) - } catch (e: any) { - if (e?.name === 'AbortError') { - // Newer save superseded this one - return - } - if (typeof navigator !== 'undefined' && navigator.onLine === false) { - setStatus('offline') - } else { - setStatus('error') - } - lastError = e?.message || 'Auto-save failed' - } - } + setStatus('saving') + lastError = null + try { + const res = await opts.save(payload, { signal: controller.signal }) + lastSentHash = hash + setStatus('saved') + if (opts.onSaved) opts.onSaved(res, { prime }) + } catch (e: any) { + if (e?.name === 'AbortError') { + // Newer save superseded this one + return + } + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + setStatus('offline') + } else { + setStatus('error') + } + lastError = e?.message || 'Auto-save failed' + } + } - function flush() { - return run() - } + function flush() { + return run() + } - function destroy() { - if (timer) clearTimeout(timer) - if (idleResetTimer) clearTimeout(idleResetTimer) - if (controller) controller.abort() - } + function destroy() { + if (timer) clearTimeout(timer) + if (idleResetTimer) clearTimeout(idleResetTimer) + if (controller) controller.abort() + } - return { - get status() { - return status - }, - get lastError() { - return lastError - }, - schedule, - flush, - destroy, - prime - } + return { + get status() { + return status + }, + get lastError() { + return lastError + }, + schedule, + flush, + destroy, + prime + } } function safeHash(obj: unknown): string { - try { - return JSON.stringify(obj) - } catch { - // Fallback for circular structures; not expected for form payloads - return String(obj) - } + try { + return JSON.stringify(obj) + } catch { + // Fallback for circular structures; not expected for form payloads + return String(obj) + } } diff --git a/src/lib/admin/autoSave.ts b/src/lib/admin/autoSave.ts index fe917eb..89a205b 100644 --- a/src/lib/admin/autoSave.ts +++ b/src/lib/admin/autoSave.ts @@ -1,139 +1,139 @@ export type AutoSaveStatus = 'idle' | 'saving' | 'saved' | 'error' | 'offline' export interface AutoSaveController { - status: { subscribe: (run: (v: AutoSaveStatus) => void) => () => void } - lastError: { subscribe: (run: (v: string | null) => void) => () => void } - schedule: () => void - flush: () => Promise - destroy: () => void - prime: (payload: T) => void + status: { subscribe: (run: (v: AutoSaveStatus) => void) => () => void } + lastError: { subscribe: (run: (v: string | null) => void) => () => void } + schedule: () => void + flush: () => Promise + destroy: () => void + prime: (payload: T) => void } interface CreateAutoSaveControllerOptions { - debounceMs?: number - idleResetMs?: number - getPayload: () => TPayload | null | undefined - save: (payload: TPayload, ctx: { signal: AbortSignal }) => Promise - onSaved?: (res: TResponse, ctx: { prime: (payload: TPayload) => void }) => void + debounceMs?: number + idleResetMs?: number + getPayload: () => TPayload | null | undefined + save: (payload: TPayload, ctx: { signal: AbortSignal }) => Promise + onSaved?: (res: TResponse, ctx: { prime: (payload: TPayload) => void }) => void } export function createAutoSaveController( - opts: CreateAutoSaveControllerOptions + opts: CreateAutoSaveControllerOptions ) { - const debounceMs = opts.debounceMs ?? 2000 - const idleResetMs = opts.idleResetMs ?? 2000 - let timer: ReturnType | null = null - let idleResetTimer: ReturnType | null = null - let controller: AbortController | null = null - let lastSentHash: string | null = null + const debounceMs = opts.debounceMs ?? 2000 + const idleResetMs = opts.idleResetMs ?? 2000 + let timer: ReturnType | null = null + let idleResetTimer: ReturnType | null = null + let controller: AbortController | null = null + let lastSentHash: string | null = null - let _status: AutoSaveStatus = 'idle' - let _lastError: string | null = null - const statusSubs = new Set<(v: AutoSaveStatus) => void>() - const errorSubs = new Set<(v: string | null) => void>() + let _status: AutoSaveStatus = 'idle' + let _lastError: string | null = null + const statusSubs = new Set<(v: AutoSaveStatus) => void>() + const errorSubs = new Set<(v: string | null) => void>() - function setStatus(next: AutoSaveStatus) { - if (idleResetTimer) { - clearTimeout(idleResetTimer) - idleResetTimer = null - } + function setStatus(next: AutoSaveStatus) { + if (idleResetTimer) { + clearTimeout(idleResetTimer) + idleResetTimer = null + } - _status = next - statusSubs.forEach((fn) => fn(_status)) + _status = next + statusSubs.forEach((fn) => fn(_status)) - // Auto-transition from 'saved' to 'idle' after idleResetMs - if (next === 'saved') { - idleResetTimer = setTimeout(() => { - _status = 'idle' - statusSubs.forEach((fn) => fn(_status)) - idleResetTimer = null - }, idleResetMs) - } - } + // Auto-transition from 'saved' to 'idle' after idleResetMs + if (next === 'saved') { + idleResetTimer = setTimeout(() => { + _status = 'idle' + statusSubs.forEach((fn) => fn(_status)) + idleResetTimer = null + }, idleResetMs) + } + } - function prime(payload: TPayload) { - lastSentHash = safeHash(payload) - } + function prime(payload: TPayload) { + lastSentHash = safeHash(payload) + } - function schedule() { - if (timer) clearTimeout(timer) - timer = setTimeout(() => void run(), debounceMs) - } + function schedule() { + if (timer) clearTimeout(timer) + timer = setTimeout(() => void run(), debounceMs) + } - async function run() { - if (timer) { - clearTimeout(timer) - timer = null - } + async function run() { + if (timer) { + clearTimeout(timer) + timer = null + } - const payload = opts.getPayload() - if (!payload) return + const payload = opts.getPayload() + if (!payload) return - const hash = safeHash(payload) - if (lastSentHash && hash === lastSentHash) return + const hash = safeHash(payload) + if (lastSentHash && hash === lastSentHash) return - if (controller) controller.abort() - controller = new AbortController() + if (controller) controller.abort() + controller = new AbortController() - setStatus('saving') - _lastError = null - try { - const res = await opts.save(payload, { signal: controller.signal }) - lastSentHash = hash - setStatus('saved') - if (opts.onSaved) opts.onSaved(res, { prime }) - } catch (e: any) { - if (e?.name === 'AbortError') { - // Newer save superseded this one - return - } - if (typeof navigator !== 'undefined' && navigator.onLine === false) { - setStatus('offline') - } else { - setStatus('error') - } - _lastError = e?.message || 'Auto-save failed' - errorSubs.forEach((fn) => fn(_lastError)) - } - } + setStatus('saving') + _lastError = null + try { + const res = await opts.save(payload, { signal: controller.signal }) + lastSentHash = hash + setStatus('saved') + if (opts.onSaved) opts.onSaved(res, { prime }) + } catch (e: any) { + if (e?.name === 'AbortError') { + // Newer save superseded this one + return + } + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + setStatus('offline') + } else { + setStatus('error') + } + _lastError = e?.message || 'Auto-save failed' + errorSubs.forEach((fn) => fn(_lastError)) + } + } - function flush() { - return run() - } + function flush() { + return run() + } - function destroy() { - if (timer) clearTimeout(timer) - if (idleResetTimer) clearTimeout(idleResetTimer) - if (controller) controller.abort() - } + function destroy() { + if (timer) clearTimeout(timer) + if (idleResetTimer) clearTimeout(idleResetTimer) + if (controller) controller.abort() + } - return { - status: { - subscribe(run: (v: AutoSaveStatus) => void) { - run(_status) - statusSubs.add(run) - return () => statusSubs.delete(run) - } - }, - lastError: { - subscribe(run: (v: string | null) => void) { - run(_lastError) - errorSubs.add(run) - return () => errorSubs.delete(run) - } - }, - schedule, - flush, - destroy, - prime - } + return { + status: { + subscribe(run: (v: AutoSaveStatus) => void) { + run(_status) + statusSubs.add(run) + return () => statusSubs.delete(run) + } + }, + lastError: { + subscribe(run: (v: string | null) => void) { + run(_lastError) + errorSubs.add(run) + return () => errorSubs.delete(run) + } + }, + schedule, + flush, + destroy, + prime + } } function safeHash(obj: unknown): string { - try { - return JSON.stringify(obj) - } catch { - // Fallback for circular structures; not expected for form payloads - return String(obj) - } + try { + return JSON.stringify(obj) + } catch { + // Fallback for circular structures; not expected for form payloads + return String(obj) + } } diff --git a/src/lib/admin/autoSaveLifecycle.ts b/src/lib/admin/autoSaveLifecycle.ts index 710ae39..fa8695d 100644 --- a/src/lib/admin/autoSaveLifecycle.ts +++ b/src/lib/admin/autoSaveLifecycle.ts @@ -4,58 +4,58 @@ import type { AutoSaveController } from './autoSave' import type { AutoSaveStore } from './autoSave.svelte' interface AutoSaveLifecycleOptions { - isReady?: () => boolean - onFlushError?: (error: unknown) => void - enableShortcut?: boolean + isReady?: () => boolean + onFlushError?: (error: unknown) => void + enableShortcut?: boolean } export function initAutoSaveLifecycle( - controller: AutoSaveController | AutoSaveStore, - options: AutoSaveLifecycleOptions = {} + controller: AutoSaveController | AutoSaveStore, + options: AutoSaveLifecycleOptions = {} ) { - const { isReady = () => true, onFlushError, enableShortcut = true } = options + const { isReady = () => true, onFlushError, enableShortcut = true } = options - if (typeof window === 'undefined') { - onDestroy(() => controller.destroy()) - return - } + if (typeof window === 'undefined') { + onDestroy(() => controller.destroy()) + return + } - function handleKeydown(event: KeyboardEvent) { - if (!enableShortcut) return - if (!isReady()) return - const key = event.key.toLowerCase() - const isModifier = event.metaKey || event.ctrlKey - if (!isModifier || key !== 's') return - event.preventDefault() - controller.flush().catch((error) => { - onFlushError?.(error) - }) - } + function handleKeydown(event: KeyboardEvent) { + if (!enableShortcut) return + if (!isReady()) return + const key = event.key.toLowerCase() + const isModifier = event.metaKey || event.ctrlKey + if (!isModifier || key !== 's') return + event.preventDefault() + controller.flush().catch((error) => { + onFlushError?.(error) + }) + } - if (enableShortcut) { - document.addEventListener('keydown', handleKeydown) - } + if (enableShortcut) { + document.addEventListener('keydown', handleKeydown) + } - const stopNavigating = beforeNavigate(async (navigation) => { - if (!isReady()) return - navigation.cancel() - try { - await controller.flush() - navigation.retry() - } catch (error) { - onFlushError?.(error) - } - }) + const stopNavigating = beforeNavigate(async (navigation) => { + if (!isReady()) return + navigation.cancel() + try { + await controller.flush() + navigation.retry() + } catch (error) { + onFlushError?.(error) + } + }) - const stop = () => { - if (enableShortcut) { - document.removeEventListener('keydown', handleKeydown) - } - stopNavigating?.() - controller.destroy() - } + const stop = () => { + if (enableShortcut) { + document.removeEventListener('keydown', handleKeydown) + } + stopNavigating?.() + controller.destroy() + } - onDestroy(stop) + onDestroy(stop) - return { stop } + return { stop } } diff --git a/src/lib/admin/draftStore.ts b/src/lib/admin/draftStore.ts index 08fc1ed..717b18b 100644 --- a/src/lib/admin/draftStore.ts +++ b/src/lib/admin/draftStore.ts @@ -1,49 +1,49 @@ export type Draft = { payload: T; ts: number } export function makeDraftKey(type: string, id: string | number) { - return `admin:draft:${type}:${id}` + return `admin:draft:${type}:${id}` } export function saveDraft(key: string, payload: T) { - try { - const entry: Draft = { payload, ts: Date.now() } - localStorage.setItem(key, JSON.stringify(entry)) - } catch { - // Ignore quota or serialization errors - } + try { + const entry: Draft = { payload, ts: Date.now() } + localStorage.setItem(key, JSON.stringify(entry)) + } catch { + // Ignore quota or serialization errors + } } export function loadDraft(key: string): Draft | null { - try { - const raw = localStorage.getItem(key) - if (!raw) return null - return JSON.parse(raw) as Draft - } catch { - return null - } + try { + const raw = localStorage.getItem(key) + if (!raw) return null + return JSON.parse(raw) as Draft + } catch { + return null + } } export function clearDraft(key: string) { - try { - localStorage.removeItem(key) - } catch {} + try { + localStorage.removeItem(key) + } catch {} } export function timeAgo(ts: number): string { - const diff = Date.now() - ts - const sec = Math.floor(diff / 1000) - if (sec < 5) return 'just now' - if (sec < 60) return `${sec} second${sec !== 1 ? 's' : ''} ago` - const min = Math.floor(sec / 60) - if (min < 60) return `${min} minute${min !== 1 ? 's' : ''} ago` - const hr = Math.floor(min / 60) - if (hr < 24) return `${hr} hour${hr !== 1 ? 's' : ''} ago` - const day = Math.floor(hr / 24) - if (day <= 29) { - if (day < 7) return `${day} day${day !== 1 ? 's' : ''} ago` - const wk = Math.floor(day / 7) - return `${wk} week${wk !== 1 ? 's' : ''} ago` - } - // Beyond 29 days, show a normal localized date - return new Date(ts).toLocaleDateString() + const diff = Date.now() - ts + const sec = Math.floor(diff / 1000) + if (sec < 5) return 'just now' + if (sec < 60) return `${sec} second${sec !== 1 ? 's' : ''} ago` + const min = Math.floor(sec / 60) + if (min < 60) return `${min} minute${min !== 1 ? 's' : ''} ago` + const hr = Math.floor(min / 60) + if (hr < 24) return `${hr} hour${hr !== 1 ? 's' : ''} ago` + const day = Math.floor(hr / 24) + if (day <= 29) { + if (day < 7) return `${day} day${day !== 1 ? 's' : ''} ago` + const wk = Math.floor(day / 7) + return `${wk} week${wk !== 1 ? 's' : ''} ago` + } + // Beyond 29 days, show a normal localized date + return new Date(ts).toLocaleDateString() } diff --git a/src/lib/admin/listFilters.svelte.ts b/src/lib/admin/listFilters.svelte.ts index 44eee41..9637832 100644 --- a/src/lib/admin/listFilters.svelte.ts +++ b/src/lib/admin/listFilters.svelte.ts @@ -127,38 +127,54 @@ export function createListFilters( */ export const commonSorts = { /** Sort by date field, newest first */ - dateDesc: (field: keyof T) => (a: T, b: T) => - new Date(b[field] as string).getTime() - new Date(a[field] as string).getTime(), + dateDesc: + (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: (field: keyof T) => (a: T, b: T) => - new Date(a[field] as string).getTime() - new Date(b[field] as string).getTime(), + dateAsc: + (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: (field: keyof T) => (a: T, b: T) => - String(a[field] || '').localeCompare(String(b[field] || '')), + stringAsc: + (field: keyof T) => + (a: T, b: T) => + String(a[field] || '').localeCompare(String(b[field] || '')), /** Sort by string field, Z-A */ - stringDesc: (field: keyof T) => (a: T, b: T) => - String(b[field] || '').localeCompare(String(a[field] || '')), + stringDesc: + (field: keyof T) => + (a: T, b: T) => + String(b[field] || '').localeCompare(String(a[field] || '')), /** Sort by number field, ascending */ - numberAsc: (field: keyof T) => (a: T, b: T) => - Number(a[field]) - Number(b[field]), + numberAsc: + (field: keyof T) => + (a: T, b: T) => + Number(a[field]) - Number(b[field]), /** Sort by number field, descending */ - numberDesc: (field: keyof T) => (a: T, b: T) => - Number(b[field]) - Number(a[field]), + numberDesc: + (field: keyof T) => + (a: T, b: T) => + Number(b[field]) - Number(a[field]), /** Sort by status field, published first */ - statusPublishedFirst: (field: keyof T) => (a: T, b: T) => { - if (a[field] === b[field]) return 0 - return a[field] === 'published' ? -1 : 1 - }, + statusPublishedFirst: + (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: (field: keyof T) => (a: T, b: T) => { - if (a[field] === b[field]) return 0 - return a[field] === 'draft' ? -1 : 1 - } + statusDraftFirst: + (field: keyof T) => + (a: T, b: T) => { + if (a[field] === b[field]) return 0 + return a[field] === 'draft' ? -1 : 1 + } } diff --git a/src/lib/components/Album.svelte b/src/lib/components/Album.svelte index 9a26d6b..4bc5228 100644 --- a/src/lib/components/Album.svelte +++ b/src/lib/components/Album.svelte @@ -101,10 +101,10 @@ // Use the album's isNowPlaying status directly - single source of truth const isNowPlaying = $derived(album?.isNowPlaying ?? false) const nowPlayingTrack = $derived(album?.nowPlayingTrack) - + // Use Apple Music URL if available, otherwise fall back to Last.fm const albumUrl = $derived(album?.appleMusicData?.url || album?.url || '#') - + // Debug logging $effect(() => { if (album && (isNowPlaying || album.isNowPlaying)) { diff --git a/src/lib/components/AppleMusicSearchModal.svelte b/src/lib/components/AppleMusicSearchModal.svelte index 678c08a..b246499 100644 --- a/src/lib/components/AppleMusicSearchModal.svelte +++ b/src/lib/components/AppleMusicSearchModal.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte' import XIcon from '$icons/x.svg' import LoaderIcon from '$icons/loader.svg' - + let isOpen = $state(false) let searchQuery = $state('') let storefront = $state('us') @@ -10,7 +10,7 @@ let searchResults = $state(null) let searchError = $state(null) let responseTime = $state(0) - + // Available storefronts const storefronts = [ { value: 'us', label: 'United States' }, @@ -26,7 +26,7 @@ { value: 'cn', label: 'China' }, { value: 'br', label: 'Brazil' } ] - + export function open() { isOpen = true searchQuery = '' @@ -34,23 +34,23 @@ searchError = null responseTime = 0 } - + function close() { isOpen = false } - + async function performSearch() { if (!searchQuery.trim()) { searchError = 'Please enter a search query' return } - + isSearching = true searchError = null searchResults = null - + const startTime = performance.now() - + try { const response = await fetch('/api/admin/debug/apple-music-search', { method: 'POST', @@ -60,13 +60,13 @@ storefront }) }) - + responseTime = Math.round(performance.now() - startTime) - + if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } - + searchResults = await response.json() } catch (error) { searchError = error instanceof Error ? error.message : 'Unknown error occurred' @@ -75,7 +75,7 @@ isSearching = false } } - + function handleKeydown(e: KeyboardEvent) { if (e.key === 'Escape' && isOpen) { close() @@ -83,7 +83,7 @@ performSearch() } } - + onMount(() => { window.addEventListener('keydown', handleKeydown) return () => window.removeEventListener('keydown', handleKeydown) @@ -99,7 +99,7 @@ - +