<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}