~james/core

core/web/src/lib/components/profile/PinSuggestions.svelte -rw-r--r-- 3.5 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
101
102
103
104
105
106
107
108
109
110
111
112
<script lang="ts">
	import type { Did } from "@atcute/lexicons/syntax";
	import type { Snippet } from "svelte";
	import Pin from "$icon/pin";
	import PinOff from "$icon/pin-off";
	import { createLoad } from "#lib/action.svelte.ts";
	import { createBobbinClient } from "#lib/api/client.ts";
	import { pinKeyOf, setRepositoryPinned, type PinnedKey } from "#lib/api/pinning.ts";
	import { getAuth } from "#lib/auth.svelte.ts";
	import repoRepocardWithStarButton from "#lib/bones/repo-repocard--with-star-button.bones.json";
	import { fetchPinSuggestions } from "#lib/components/profile/pages.ts";
	import type { PinnedEdit } from "#lib/components/profile/pinnedEdit.svelte.ts";
	import { repoKey, type RepoCardData } from "#lib/components/profile/types.ts";
	import RepoCard from "#lib/components/repo/RepoCard.svelte";
	import Bones from "#lib/components/ui/Bones.svelte";
	import Button from "#lib/components/ui/Button.svelte";
	import ErrorAlert from "#lib/components/ui/Error.svelte";

	const SUGGESTION_LIMIT = 4;

	interface Props {
		edit: PinnedEdit;
		action?: Snippet;
	}

	let { edit, action }: Props = $props();

	const auth = getAuth();
	const ctx = $derived(createBobbinClient({ serviceUrl: auth.bobbinUrl }));

	const suggestions = createLoad(() =>
		fetchPinSuggestions(ctx, {
			did: edit.did,
			handle: edit.handle,
			viewerDid: auth.currentDid ?? undefined,
			limit: SUGGESTION_LIMIT
		})
	);

	let pending = $state<Record<string, boolean>>({});
	let error = $state<string>();
	let writes: Promise<void> = Promise.resolve();

	const isPinned = (key: PinnedKey) => pending[key] ?? edit.saved?.includes(key) ?? false;

	const toggle = (repo: RepoCardData) => {
		const agent = auth.agent;
		if (!agent) return;
		const key = pinKeyOf(repo);
		const pinned = !isPinned(key);
		pending[key] = pinned;
		edit.cards[key] = repo;
		edit.suggesting = true;
		writes = writes.then(async () => {
			try {
				const record = await setRepositoryPinned(ctx, agent, edit.did as Did, key, pinned);
				edit.saved = record.pinnedRepositories ?? [];
				error = undefined;
			} catch (cause) {
				error = cause instanceof Error ? cause.message : String(cause);
			} finally {
				delete pending[key];
			}
		});
	};
</script>

{#if suggestions.loading || (suggestions.data && suggestions.data.length > 0)}
	<section class="@container space-y-xs" data-testid="pinSuggestions">
		<div class="flex items-center justify-between gap-xs">
			<h2 class="px-xs typography-paragraph-regular font-medium">
				Suggested repositories to pin
			</h2>
			{@render action?.()}
		</div>
		<ErrorAlert label={error} />
		<div class="grid grid-cols-1 gap-xs @min-[42rem]:grid-cols-2">
			{#if suggestions.data}
				{#each suggestions.data as repo (repoKey(repo))}
					{@const key = pinKeyOf(repo)}
					{@const pinned = isPinned(key)}
					{#snippet action()}
						<Button
							data-testid="pinSuggestion"
							variant="ghost"
							size="small"
							icon={pinned ? PinOff : Pin}
							class="-mt-2xs -mr-2xs shrink-0"
							loading={key in pending}
							onclick={() => toggle(repo)}>{pinned ? "Unpin" : "Pin"}</Button
						>
					{/snippet}
					<RepoCard
						{repo}
						starButton={false}
						showOwner={repo.ownerHandle !== edit.handle}
						uniformHeight
						{action}
					/>
				{/each}
			{:else}
				<Bones
					capture={repoRepocardWithStarButton}
					repeat={SUGGESTION_LIMIT}
					class="contents"
				/>
			{/if}
		</div>
	</section>
{:else if action}
	<div class="flex justify-end">{@render action()}</div>
{/if}