Monorepo for Tangled
tangled.org
1<script lang="ts">
2 import Star from "$icon/star";
3 import { getAuth } from "$lib/auth.svelte";
4 import { createStar, deleteStar } from "$lib/api/graph";
5 import Button from "$lib/components/ui/Button.svelte";
6 import { getProfileCounts } from "$lib/components/profile/counts.svelte";
7 import { createOptimisticRelation, createOptimisticCount } from "$lib/optimistic.svelte";
8
9 interface Props {
10 repoDid: string;
11 repoOwnerHandle: string;
12 repoName: string;
13 initialCount?: number;
14 initialRkey?: string | null;
15 }
16
17 let { repoDid, repoOwnerHandle, repoName, initialCount, initialRkey }: Props = $props();
18
19 const auth = getAuth();
20 const profileCounts = getProfileCounts();
21 const signedIn = $derived(Boolean(auth.currentDid));
22 const relation = createOptimisticRelation({
23 key: () => `${auth.currentDid ?? ""}:${repoDid}`,
24 loadedRkey: () => initialRkey
25 });
26 const starCount = createOptimisticCount({
27 key: () => repoDid,
28 loaded: () => initialCount
29 });
30
31 let busy = $state(false);
32 const starred = $derived(relation.active);
33 const failed = $derived(relation.failed || starCount.failed);
34
35 const toggle = async () => {
36 const agent = auth.agent;
37 if (!agent || busy || !relation.known || !repoDid) return;
38 busy = true;
39 relation.resetFailure();
40 starCount.resetFailure();
41 try {
42 if (relation.active && relation.rkey) {
43 await deleteStar(agent, relation.rkey);
44 relation.deleted();
45 starCount.adjust(-1);
46 profileCounts?.adjust(agent.sub, "stars", -1);
47 } else {
48 const rkey = await createStar(agent, repoDid);
49 relation.created(rkey);
50 starCount.adjust(1);
51 profileCounts?.adjust(agent.sub, "stars", 1);
52 }
53 } catch {
54 relation.fail();
55 starCount.fail();
56 } finally {
57 busy = false;
58 }
59 };
60</script>
61
62{#if signedIn && repoDid}
63 <div class="flex flex-col items-end">
64 <div
65 class="inline-flex max-h-8 items-stretch divide-x divide-border-default overflow-clip rounded border border-border-default"
66 >
67 <Button
68 variant="default"
69 size="sm"
70 class="min-h-[30px] flex-1 rounded-none border-0 before:rounded-none before:rounded-l-sm"
71 loading={busy}
72 disabled={!relation.known}
73 onclick={toggle}
74 >
75 <Star class={`size-4 shrink-0 ${starred ? "fill-current" : ""}`} aria-hidden="true" />
76 <span>{starred ? "Unstar" : "Star"}</span>
77 </Button>
78 <Button
79 href={`/${repoOwnerHandle}/${repoName}/stars`}
80 variant="flat"
81 size="sm"
82 class="min-h-[30px] rounded-none"
83 title="Starred by"
84 >
85 {starCount.value}
86 </Button>
87 </div>
88 {#if failed}
89 <p class="mt-1 text-xs text-foreground-danger">Something went wrong. Try again.</p>
90 {/if}
91 </div>
92{/if}