~james/core

core/web/src/lib/components/timeline/NewsletterCard.svelte -rw-r--r-- 3.0 KiB
452f17b7 — james chore(import): snapshot james.dad.town/core at f70c71c7696a 16 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
<script lang="ts">
	import { page } from "$app/state";
	import { resolve } from "$app/paths";
	import Check from "$icon/check";
	import Mail from "$icon/mail";
	import X from "$icon/x";
	import { createAction, createLoad } from "#lib/action.svelte.ts";
	import { ClientResponseError } from "#lib/api/client.ts";
	import { createDeliberiClient } from "#lib/api/deliberi.ts";
	import {
		dismissNewsletter,
		getNewsletterStatus,
		subscribeNewsletter
	} from "#lib/api/newsletter.ts";
	import { getAuth } from "#lib/auth.svelte.ts";
	import PreviewCard from "#lib/components/timeline/PreviewCard.svelte";
	import Button from "#lib/components/ui/Button.svelte";

	const auth = getAuth();
	const deliberi = () => {
		const agent = auth.agent;
		const deliberiUrl = page.data.publicConfig?.deliberiUrl as string | undefined;
		return agent && deliberiUrl ? createDeliberiClient({ deliberiUrl, agent }) : null;
	};

	// stays hidden while loading or on failure; it's a prompt, not content
	const status = createLoad(async () => {
		const ctx = deliberi();
		return ctx ? ((await getNewsletterStatus(ctx)) ?? "unset") : null;
	});

	let dismissed = $state(false);

	const subscribe = createAction(
		async () => {
			const ctx = deliberi();
			if (!ctx) throw new Error("Your session is still loading. Try again.");
			await subscribeNewsletter(ctx);
			return true;
		},
		() => "Couldn't sign you up. Try again later."
	);
	const needsEmail = $derived(
		subscribe.cause instanceof ClientResponseError &&
			subscribe.cause.error === "NoVerifiedPrimaryEmail"
	);

	// fire and forget like the old widget: a failed write only brings the card back next visit
	const dismiss = () => {
		dismissed = true;
		const ctx = deliberi();
		if (ctx) dismissNewsletter(ctx).catch(() => {});
	};
</script>

{#if status.data === "unset" && !dismissed}
	<div data-testid="newsletterCard">
		<PreviewCard title="Newsletter" icon={Mail}>
			{#snippet action()}
				<Button
					variant="ghost"
					size="small"
					square
					icon={X}
					aria-label="Dismiss"
					class="self-stretch [--btn-radius:var(--radius-md)]"
					onclick={dismiss}
				/>
			{/snippet}
			<div class="flex flex-col gap-sm p-sm typography-paragraph-small">
				<p class="text-foreground-default">
					We've got a newsletter! Get updates on what we're shipping at Tangled, sent to
					your primary email.
				</p>
				{#if subscribe.data}
					<p class="flex items-center gap-xs text-foreground-success" role="status">
						<Check class="size-3.5" aria-hidden="true" />
						You're signed up!
					</p>
				{:else}
					<Button
						class="self-start"
						loading={subscribe.loading}
						onclick={() => subscribe.run()}
					>
						Subscribe
					</Button>
					{#if needsEmail}
						<p class="text-foreground-danger" role="alert">
							Add and verify an email address in
							<a href={resolve("/settings/emails")}>settings</a> first.
						</p>
					{:else if subscribe.error}
						<p class="text-foreground-danger" role="alert">{subscribe.error}</p>
					{/if}
				{/if}
			</div>
		</PreviewCard>
	</div>
{/if}