Enhance routing and type definitions for new API endpoints. Added support for notifications and SSO routes, updated layout parameters, and improved type safety in route metadata. Refactored component imports for user and profile pages.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m27s

This commit is contained in:
eewing
2026-02-23 15:06:11 -06:00
parent 121a6b5844
commit 7de66442aa
196 changed files with 491 additions and 135994 deletions
+8
View File
@@ -0,0 +1,8 @@
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ fetch, depends }) => {
depends('app:notifications');
const res = await fetch('/api/notifications');
const data = res.ok ? await res.json() : { notifications: [] };
return { notifications: data.notifications ?? [] };
};
+256
View File
@@ -0,0 +1,256 @@
<script lang="ts">
import { invalidate } from '$app/navigation';
import Trash2 from '@lucide/svelte/icons/trash-2';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
type NotificationItem = {
id: string;
type: string;
title: string;
message: string;
data: Record<string, unknown> | null;
isRead: boolean;
isDeleted: boolean;
created: string;
};
let { data }: { data: { notifications: NotificationItem[] } } = $props();
let sending = $state(false);
let error = $state<string | null>(null);
let updatingId = $state<string | null>(null);
let deletingId = $state<string | null>(null);
const unread = $derived(data.notifications.filter((n) => !n.isRead));
const read = $derived(data.notifications.filter((n) => n.isRead));
async function sendTestNotification() {
sending = true;
error = null;
try {
const res = await fetch('/api/notifications', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: 'Test notification',
message: 'This is a test notification sent to yourself.',
type: 'system'
})
});
if (!res.ok) {
const d = await res.json().catch(() => ({}));
throw new Error(d.error ?? 'Failed to send');
}
await invalidate('app:notifications');
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to send notification';
} finally {
sending = false;
}
}
function formatDate(created: string) {
if (!created) return '';
const d = new Date(created);
return d.toLocaleDateString(undefined, {
month: 'short',
day: 'numeric',
year: d.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined,
hour: '2-digit',
minute: '2-digit'
});
}
function typeLabel(type: string) {
if (!type) return '';
return type.charAt(0).toUpperCase() + type.slice(1);
}
async function toggleRead(notification: NotificationItem, checked: boolean) {
updatingId = notification.id;
error = null;
try {
const res = await fetch(`/api/notifications/${notification.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isRead: checked })
});
if (!res.ok) {
const d = await res.json().catch(() => ({}));
throw new Error(d.error ?? 'Failed to update');
}
await invalidate('app:notifications');
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to update';
} finally {
updatingId = null;
}
}
async function deleteNotification(notification: NotificationItem) {
deletingId = notification.id;
error = null;
try {
const res = await fetch(`/api/notifications/${notification.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ isDeleted: true })
});
if (!res.ok) {
const d = await res.json().catch(() => ({}));
throw new Error(d.error ?? 'Failed to delete');
}
await invalidate('app:notifications');
} catch (e) {
error = e instanceof Error ? e.message : 'Failed to delete';
} finally {
deletingId = null;
}
}
</script>
<svelte:head>
<title>Notifications — Base</title>
</svelte:head>
<main class="flex min-h-0 flex-1 flex-col gap-4 p-4 sm:gap-6 sm:p-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<h1 class="text-2xl font-semibold">Notifications</h1>
<Button
type="button"
variant="outline"
size="sm"
disabled={sending}
onclick={sendTestNotification}
>
{sending ? 'Sending…' : 'Test notification'}
</Button>
</div>
{#if error}
<p class="text-destructive text-sm">{error}</p>
{/if}
{#if data.notifications.length === 0}
<p class="text-muted-foreground text-sm">No notifications yet.</p>
{:else}
{#if unread.length > 0}
<section>
<h2 class="text-muted-foreground mb-2 text-sm font-medium">Unread</h2>
<ul class="flex flex-col gap-3">
{#each unread as notification (notification.id)}
<li>
<Card.Root>
<Card.Header class="pb-2">
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<Card.Title class="text-base">{notification.title}</Card.Title>
{#if notification.type}
<span class="rounded bg-muted px-1.5 py-0.5 text-muted-foreground text-xs font-medium">
{typeLabel(notification.type)}
</span>
{/if}
</div>
<Card.Description class="text-xs">
{formatDate(notification.created)}
</Card.Description>
</div>
<div class="flex shrink-0 items-center gap-2">
<Button
type="button"
variant="ghost"
size="sm"
class="text-muted-foreground h-auto px-2 py-1 text-xs font-normal hover:text-foreground"
disabled={updatingId === notification.id}
onclick={() => toggleRead(notification, !notification.isRead)}
>
{updatingId === notification.id ? '…' : notification.isRead ? 'Unread' : 'Read'}
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
class="text-muted-foreground hover:text-destructive"
disabled={deletingId === notification.id}
onclick={() => deleteNotification(notification)}
title="Delete"
>
{#if deletingId === notification.id}
<span class="text-xs"></span>
{:else}
<Trash2 class="size-4" />
{/if}
</Button>
</div>
</div>
</Card.Header>
<Card.Content class="pt-0">
<p class="text-muted-foreground text-sm">{notification.message}</p>
</Card.Content>
</Card.Root>
</li>
{/each}
</ul>
</section>
{/if}
{#if read.length > 0}
<section>
<h2 class="text-muted-foreground mb-2 text-sm font-medium">Read</h2>
<ul class="flex flex-col gap-3">
{#each read as notification (notification.id)}
<li>
<Card.Root class="opacity-75">
<Card.Header class="pb-2">
<div class="flex flex-wrap items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<Card.Title class="text-base">{notification.title}</Card.Title>
{#if notification.type}
<span class="rounded bg-muted px-1.5 py-0.5 text-muted-foreground text-xs font-medium">
{typeLabel(notification.type)}
</span>
{/if}
</div>
<Card.Description class="text-xs">
{formatDate(notification.created)}
</Card.Description>
</div>
<div class="flex shrink-0 items-center gap-2">
<Button
type="button"
variant="ghost"
size="sm"
class="text-muted-foreground h-auto px-2 py-1 text-xs font-normal hover:text-foreground"
disabled={updatingId === notification.id}
onclick={() => toggleRead(notification, !notification.isRead)}
>
{updatingId === notification.id ? '…' : notification.isRead ? 'Unread' : 'Read'}
</Button>
<Button
type="button"
variant="ghost"
size="icon-sm"
class="text-muted-foreground hover:text-destructive"
disabled={deletingId === notification.id}
onclick={() => deleteNotification(notification)}
title="Delete"
>
{#if deletingId === notification.id}
<span class="text-xs"></span>
{:else}
<Trash2 class="size-4" />
{/if}
</Button>
</div>
</div>
</Card.Header>
<Card.Content class="pt-0">
<p class="text-muted-foreground text-sm">{notification.message}</p>
</Card.Content>
</Card.Root>
</li>
{/each}
</ul>
</section>
{/if}
{/if}
</main>