~james/core

core/web/src/lib/components/timeline/NotificationsPreview.svelte -rw-r--r-- 3.3 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 15 hours ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<script lang="ts">
	import { browser } from "$app/env";
	import { resolve } from "$app/paths";
	import ArrowRight from "$icon/arrow-right";
	import Bell from "$icon/bell";
	import BellOff from "$icon/bell-off";
	import Inbox from "$icon/inbox";
	import { createLoad } from "#lib/action.svelte.ts";
	import NotificationList from "#lib/components/notifications/NotificationList.svelte";
	import type { NotificationSummary } from "#lib/components/notifications/types.ts";
	import { getNotifications, type Notifications } from "#lib/notifications.svelte.ts";
	import PreviewCard from "#lib/components/timeline/PreviewCard.svelte";
	import PreviewEmpty from "#lib/components/timeline/PreviewEmpty.svelte";
	import timelineNotificationspreviewSkeletonFixture from "#lib/bones/timeline-notificationspreview--skeleton-fixture.bones.json";
	import Bones from "#lib/components/ui/Bones.svelte";
	import Button from "#lib/components/ui/Button.svelte";

	const PREVIEW_LIMIT = 5;

	let {
		signedIn = false,
		notifications: fixtures
	}: {
		signedIn?: boolean;
		notifications?: NotificationSummary[] | Promise<NotificationSummary[]>;
	} = $props();

	const context = getNotifications() as Notifications | undefined;
	const path = "/notifications";

	const fixturePreview = $derived.by(() => {
		if (!fixtures) return undefined;
		if (fixtures instanceof Promise) return fixtures;
		return { notifications: fixtures, totalUnread: fixtures.filter((n) => !n.read).length };
	});

	const previewLoad = createLoad(async (): Promise<NotificationSummary[] | null> => {
		if (!browser || !signedIn || !context) return null;
		return context.list({ limit: PREVIEW_LIMIT });
	});

	const live = $derived(
		previewLoad.data ? (context?.overlay(previewLoad.data) ?? previewLoad.data) : null
	);

	const loading = $derived(
		signedIn &&
			fixtures === undefined &&
			(previewLoad.loading || previewLoad.data === undefined)
	);
</script>

{#snippet content(items: NotificationSummary[], totalUnread: number | null)}
	{#if items.length > 0}
		<NotificationList notifications={items} border={false} onToggleRead={context?.setRead} />
	{:else if totalUnread === 0}
		<PreviewEmpty icon={Inbox}>
			<p>All caught up!</p>
		</PreviewEmpty>
	{:else}
		<PreviewEmpty icon={Bell}>
			<a href={resolve(path as "/")}>Open your inbox</a>
		</PreviewEmpty>
	{/if}
{/snippet}

<PreviewCard title="Notifications" icon={Bell} href={path}>
	{#snippet action()}
		<Button
			href={resolve(path as "/")}
			variant="ghost"
			size="small"
			class="self-stretch [--btn-radius:var(--radius-md)]"
		>
			View all
			<ArrowRight class="size-3.5" aria-hidden="true" />
		</Button>
	{/snippet}

	{#if !signedIn}
		<PreviewEmpty icon={BellOff}>
			<a href={resolve("/login" as "/")}>Sign in</a> to view your inbox
		</PreviewEmpty>
	{:else if loading}
		<Bones capture={timelineNotificationspreviewSkeletonFixture} />
	{:else if fixturePreview}
		{#if fixturePreview instanceof Promise}
			{#await fixturePreview}
				<Bones capture={timelineNotificationspreviewSkeletonFixture} />
			{:then resolved}
				{@render content(resolved, null)}
			{/await}
		{:else}
			{@render content(fixturePreview.notifications, fixturePreview.totalUnread)}
		{/if}
	{:else if live}
		{@render content(live, context?.unread ?? null)}
	{:else}
		<PreviewEmpty icon={Bell}>
			<a href={resolve(path as "/")}>Open your inbox</a>
		</PreviewEmpty>
	{/if}
</PreviewCard>