This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web/components: add IssueCard and all issues page

author
oppiliappan
committer
dawn
date (Jul 30, 2026, 7:45 PM +0300) commit ac5c0e15 parent 24c7eb24 change-id tkmqxtyu
+154
+31
web/src/lib/components/repo/issues/IssueCard.stories.svelte
··· 1 + <script module lang="ts"> 2 + import { defineMeta } from "@storybook/addon-svelte-csf"; 3 + import IssueCard from "./IssueCard.svelte"; 4 + import type { IssueSummary } from "$lib/components/repo/types"; 5 + 6 + const issue: IssueSummary = { 7 + uri: "at://did:plc:3p7ejjxnufohsygt5vbyxb2i/sh.tangled.repo.issue/3mrkgcxb4m422", 8 + rkey: "3mrkgcxb4m422", 9 + title: "Add a full trending repositories view", 10 + state: "open", 11 + authorHandle: "joshka.net", 12 + authorDid: "did:plc:3p7ejjxnufohsygt5vbyxb2i", 13 + createdAt: "2025-06-01T10:30:00Z", 14 + commentCount: 3 15 + }; 16 + 17 + const { Story } = defineMeta({ 18 + title: "Repo/Issues/IssueCard", 19 + component: IssueCard, 20 + tags: ["autodocs"], 21 + args: { 22 + ownerHandle: "tangled.org", 23 + repoName: "core", 24 + issue 25 + } 26 + }); 27 + </script> 28 + 29 + <Story name="Open" /> 30 + <Story name="Closed" args={{ issue: { ...issue, state: "closed" } }} /> 31 + <Story name="No comments" args={{ issue: { ...issue, commentCount: 0 } }} />
+47
web/src/lib/components/repo/issues/IssueCard.svelte
··· 1 + <script lang="ts"> 2 + import { resolve } from "$app/paths"; 3 + import User from "$lib/components/ui/User.svelte"; 4 + import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; 5 + import IssueStatePill from "./IssueStatePill.svelte"; 6 + import type { IssueSummary } from "$lib/components/repo/types"; 7 + 8 + interface Props { 9 + ownerHandle: string; 10 + repoName: string; 11 + issue: IssueSummary; 12 + } 13 + 14 + let { ownerHandle, repoName, issue }: Props = $props(); 15 + 16 + const href = $derived( 17 + resolve(`/${ownerHandle}/${repoName}/issues/${encodeURIComponent(issue.uri)}` as "/") 18 + ); 19 + </script> 20 + 21 + <div class="rounded bg-background-default px-6 py-4 shadow-sm"> 22 + <div class="pb-2"> 23 + <a {href} class="text-foreground-default no-underline"> 24 + <span class="hover:underline">{issue.title}</span> 25 + <span class="text-foreground-subtle">#{issue.rkey}</span> 26 + </a> 27 + </div> 28 + 29 + <div class="flex flex-wrap items-center gap-x-1 gap-y-1.5 text-sm text-foreground-subtle"> 30 + <IssueStatePill state={issue.state} /> 31 + 32 + <span class="ml-1 flex items-center gap-1"> 33 + <User handle={issue.authorHandle} did={issue.authorDid} /> 34 + </span> 35 + 36 + <span class="before:mr-1 before:content-['·']"> 37 + <TimeAgo value={issue.createdAt} /> 38 + </span> 39 + 40 + <span class="before:mr-1 before:content-['·']"> 41 + <a {href} class="text-foreground-subtle no-underline hover:underline"> 42 + {issue.commentCount} 43 + {issue.commentCount === 1 ? "comment" : "comments"} 44 + </a> 45 + </span> 46 + </div> 47 + </div>
+20
web/src/routes/[handle]/[repo]/issues/+page.svelte
··· 1 + <script lang="ts"> 2 + import IssueList from "$lib/components/repo/issues/IssueList.svelte"; 3 + import IssueToolbar from "$lib/components/repo/issues/IssueToolbar.svelte"; 4 + 5 + let { data } = $props(); 6 + </script> 7 + 8 + <section class="mt-2 rounded bg-background-default px-6 py-4 text-foreground-default shadow-sm"> 9 + <IssueToolbar 10 + ownerHandle={data.repo.ownerHandle} 11 + repoName={data.repo.name} 12 + state={data.state} 13 + openCount={data.openCount} 14 + closedCount={data.closedCount} 15 + /> 16 + </section> 17 + 18 + <div class="mt-2"> 19 + <IssueList ownerHandle={data.repo.ownerHandle} repoName={data.repo.name} issues={data.issues} /> 20 + </div>
+56
web/src/routes/[handle]/[repo]/issues/+page.ts
··· 1 + import { createBobbinClient } from "$lib/api/client"; 2 + import { count } from "$lib/api/count"; 3 + import { IdentityCache } from "$lib/api/identity"; 4 + import { listIssues } from "$lib/api/records"; 5 + import { didFromUri, rkeyFromUri } from "$lib/api/uri"; 6 + import type { IssueSummary } from "$lib/components/repo/types"; 7 + import type { PageLoad } from "./$types"; 8 + 9 + export const load: PageLoad = async (event) => { 10 + const parent = await event.parent(); 11 + const state = event.url.searchParams.get("state") === "closed" ? "closed" : "open"; 12 + const repoDid = parent.repo.repoDid; 13 + 14 + // no repoDid means bobbin never indexed this repo, so there is nothing to list 15 + if (!repoDid) { 16 + return { 17 + state: state as "open" | "closed", 18 + issues: [] as IssueSummary[], 19 + openCount: parent.counts.issues, 20 + closedCount: 0 21 + }; 22 + } 23 + 24 + const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch }); 25 + 26 + const [page, closed] = await Promise.all([ 27 + listIssues(ctx, repoDid, { state }), 28 + // the layout only knows the open count; the closed tab needs its own 29 + count(ctx, "sh.tangled.repo.countIssues", repoDid, { state: "closed" }).catch(() => null) 30 + ]); 31 + 32 + const identity = new IdentityCache(ctx); 33 + const issues: IssueSummary[] = await Promise.all( 34 + page.items.map(async (item): Promise<IssueSummary> => { 35 + const authorDid = didFromUri(item.uri); 36 + const author = await identity.resolve(authorDid).catch(() => null); 37 + return { 38 + uri: item.uri, 39 + rkey: rkeyFromUri(item.uri), 40 + title: item.value.title, 41 + state: item.state === "closed" ? "closed" : "open", 42 + authorDid, 43 + authorHandle: author?.handle ?? authorDid, 44 + createdAt: item.value.createdAt, 45 + commentCount: item.commentCount 46 + }; 47 + }) 48 + ); 49 + 50 + return { 51 + state: state as "open" | "closed", 52 + issues, 53 + openCount: parent.counts.issues, 54 + closedCount: closed?.count ?? 0 55 + }; 56 + };