csr tangled client in solid-js
15

Configure Feed

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

improve search page results, show repo metrics for repo results

dawn (May 30, 2026, 4:41 AM +0300) a058612b c48c38a5

+118 -94
+97 -63
src/components/repo.tsx
··· 29 29 import { A } from '@solidjs/router'; 30 30 import { For, Show, createMemo, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js'; 31 31 import type { RepoContext, TreeEntry } from '../lib/api/repos'; 32 + import type { Did } from '@atcute/lexicons/syntax'; 32 33 import { createQuery } from '@tanstack/solid-query'; 33 - import { getRepo, getRepoLanguages } from '../lib/api/repos'; 34 + import { getRepo, getRepoLanguages, getRepoForkCount, getRepoByDid } from '../lib/api/repos'; 34 35 import { getRepoStarCount } from '../lib/api/stars'; 35 36 import { getRepoIssueCount } from '../lib/api/issues'; 36 37 import { getRepoPullCount } from '../lib/api/pulls'; ··· 1654 1655 showOwner?: boolean; 1655 1656 } 1656 1657 1657 - export const RepoCard: Component<RepoCardProps> = (props) => { 1658 - const linkHref = () => props.href || `/${props.owner}/${props.name}`; 1658 + export interface RepoStatsListProps { 1659 + repoDid?: string; 1660 + owner?: string; 1661 + name?: string; 1662 + language?: string; 1663 + stars?: number; 1664 + forks?: number; 1665 + openIssues?: number; 1666 + openPRs?: number; 1667 + } 1659 1668 1660 - // Define queries for statistics 1669 + export const RepoStatsList: Component<RepoStatsListProps> = (props) => { 1661 1670 const repoContextQuery = createQuery(() => ({ 1662 - queryKey: ['repo-context', props.owner, props.name], 1663 - queryFn: () => getRepo(props.owner, props.name), 1671 + queryKey: props.repoDid ? ['repo-by-did', props.repoDid] : ['repo-context', props.owner, props.name], 1672 + queryFn: async () => { 1673 + if (props.repoDid) { 1674 + return getRepoByDid(props.repoDid as Did); 1675 + } else if (props.owner && props.name) { 1676 + return getRepo(props.owner, props.name); 1677 + } 1678 + return null; 1679 + }, 1680 + enabled: Boolean(props.repoDid || (props.owner && props.name)), 1664 1681 staleTime: 300_000, 1665 1682 })); 1666 1683 ··· 1669 1686 const starCountQuery = createQuery(() => ({ 1670 1687 queryKey: ['repo-stars-count', repoContext()?.repoDid], 1671 1688 queryFn: () => getRepoStarCount(repoContext()!), 1672 - enabled: !!repoContext(), 1689 + enabled: !!repoContext() && props.stars === undefined, 1690 + staleTime: 300_000, 1691 + })); 1692 + 1693 + const forkCountQuery = createQuery(() => ({ 1694 + queryKey: ['repo-fork-count', repoContext()?.repoDid], 1695 + queryFn: () => getRepoForkCount(repoContext()!), 1696 + enabled: !!repoContext() && props.forks === undefined, 1673 1697 staleTime: 300_000, 1674 1698 })); 1675 1699 1676 1700 const issueCountQuery = createQuery(() => ({ 1677 1701 queryKey: ['repo-issues-count', repoContext()?.repoDid], 1678 1702 queryFn: () => getRepoIssueCount(repoContext()!), 1679 - enabled: !!repoContext(), 1703 + enabled: !!repoContext() && props.openIssues === undefined, 1680 1704 staleTime: 300_000, 1681 1705 })); 1682 1706 1683 1707 const pullCountQuery = createQuery(() => ({ 1684 1708 queryKey: ['repo-pulls-count', repoContext()?.repoDid], 1685 1709 queryFn: () => getRepoPullCount(repoContext()!), 1686 - enabled: !!repoContext(), 1710 + enabled: !!repoContext() && props.openPRs === undefined, 1687 1711 staleTime: 300_000, 1688 1712 })); 1689 1713 1690 1714 const languagesQuery = createQuery(() => ({ 1691 1715 queryKey: ['repo-languages', repoContext()?.repoDid], 1692 1716 queryFn: () => getRepoLanguages(repoContext()!, 'HEAD'), 1693 - enabled: !!repoContext(), 1717 + enabled: !!repoContext() && props.language === undefined, 1694 1718 staleTime: 300_000, 1695 1719 })); 1696 1720 ··· 1708 1732 }; 1709 1733 1710 1734 const stars = () => props.stars !== undefined ? props.stars : starCountQuery.data; 1735 + const forks = () => props.forks !== undefined ? props.forks : forkCountQuery.data; 1711 1736 const openIssues = () => props.openIssues !== undefined ? props.openIssues : issueCountQuery.data; 1712 1737 const openPRs = () => props.openPRs !== undefined ? props.openPRs : pullCountQuery.data; 1713 - const forks = () => props.forks; 1714 1738 1715 1739 const isMetricsLoading = () => { 1716 - const langLoading = props.language === undefined && (languagesQuery.isLoading || repoContextQuery.isLoading); 1717 - const starsLoading = props.stars === undefined && (starCountQuery.isLoading || repoContextQuery.isLoading); 1718 - const issuesLoading = props.openIssues === undefined && (issueCountQuery.isLoading || repoContextQuery.isLoading); 1719 - const prsLoading = props.openPRs === undefined && (pullCountQuery.isLoading || repoContextQuery.isLoading); 1720 - return langLoading || starsLoading || issuesLoading || prsLoading; 1740 + const contextLoading = repoContextQuery.isLoading && (props.repoDid !== undefined || (props.owner !== undefined && props.name !== undefined)); 1741 + const langLoading = props.language === undefined && (languagesQuery.isLoading || contextLoading); 1742 + const starsLoading = props.stars === undefined && (starCountQuery.isLoading || contextLoading); 1743 + const forksLoading = props.forks === undefined && (forkCountQuery.isLoading || contextLoading); 1744 + const issuesLoading = props.openIssues === undefined && (issueCountQuery.isLoading || contextLoading); 1745 + const prsLoading = props.openPRs === undefined && (pullCountQuery.isLoading || contextLoading); 1746 + return langLoading || starsLoading || forksLoading || issuesLoading || prsLoading; 1721 1747 }; 1722 1748 1723 1749 return ( 1750 + <Show 1751 + when={!isMetricsLoading()} 1752 + fallback={ 1753 + <div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto"> 1754 + <SkeletonBlock class="h-4 w-16" /> 1755 + <SkeletonBlock class="h-4 w-12" /> 1756 + <SkeletonBlock class="h-4 w-12" /> 1757 + <SkeletonBlock class="h-4 w-12" /> 1758 + </div> 1759 + } 1760 + > 1761 + <div class="text-gray-400 text-sm font-mono inline-flex flex-wrap gap-4 mt-auto"> 1762 + <Show when={resolvedLanguage()}> 1763 + <div class="flex gap-2 items-center text-sm"> 1764 + <span 1765 + class="rounded-full inline-block shrink-0" 1766 + style={{ 'background-color': langColor(), width: '10px', height: '10px' }} 1767 + /> 1768 + <span>{resolvedLanguage()}</span> 1769 + </div> 1770 + </Show> 1771 + <Show when={stars() !== undefined && stars()! > 0}> 1772 + <div class="flex gap-1 items-center text-sm"> 1773 + <Star class="w-3 h-3 fill-current" /> 1774 + <span>{stars()}</span> 1775 + </div> 1776 + </Show> 1777 + <Show when={forks() !== undefined && forks()! > 0}> 1778 + <div class="flex gap-1 items-center text-sm"> 1779 + <GitFork class="w-3 h-3" /> 1780 + <span>{forks()}</span> 1781 + </div> 1782 + </Show> 1783 + <Show when={openIssues() !== undefined && openIssues()! > 0}> 1784 + <div class="flex gap-1 items-center text-sm"> 1785 + <CircleDot class="w-3 h-3" /> 1786 + <span>{openIssues()}</span> 1787 + </div> 1788 + </Show> 1789 + <Show when={openPRs() !== undefined && openPRs()! > 0}> 1790 + <div class="flex gap-1 items-center text-sm"> 1791 + <GitPullRequest class="w-3 h-3" /> 1792 + <span>{openPRs()}</span> 1793 + </div> 1794 + </Show> 1795 + </div> 1796 + </Show> 1797 + ); 1798 + }; 1799 + 1800 + export const RepoCard: Component<RepoCardProps> = (props) => { 1801 + const linkHref = () => props.href || `/${props.owner}/${props.name}`; 1802 + 1803 + return ( 1724 1804 <div 1725 1805 class={clsx( 1726 1806 props.compact ? 'focus-within:bg-gray-100 dark:focus-within:bg-gray-800/80' : 'min-h-32', ··· 1756 1836 </div> 1757 1837 </Show> 1758 1838 1759 - <Show 1760 - when={!isMetricsLoading()} 1761 - fallback={ 1762 - <div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto"> 1763 - <SkeletonBlock class="h-4 w-16" /> 1764 - <SkeletonBlock class="h-4 w-12" /> 1765 - <SkeletonBlock class="h-4 w-12" /> 1766 - <SkeletonBlock class="h-4 w-12" /> 1767 - </div> 1768 - } 1769 - > 1770 - <div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto"> 1771 - <Show when={resolvedLanguage()}> 1772 - <div class="flex gap-2 items-center text-sm"> 1773 - <span 1774 - class="rounded-full inline-block" 1775 - style={{ 'background-color': langColor(), width: '10px', height: '10px' }} 1776 - /> 1777 - <span>{resolvedLanguage()}</span> 1778 - </div> 1779 - </Show> 1780 - <Show when={stars() !== undefined && stars()! > 0}> 1781 - <div class="flex gap-1 items-center text-sm"> 1782 - <Star class="w-3 h-3 fill-current" /> 1783 - <span>{stars()}</span> 1784 - </div> 1785 - </Show> 1786 - <Show when={forks() !== undefined && forks()! > 0}> 1787 - <div class="flex gap-1 items-center text-sm"> 1788 - <GitFork class="w-3 h-3" /> 1789 - <span>{forks()}</span> 1790 - </div> 1791 - </Show> 1792 - <Show when={openIssues() !== undefined && openIssues()! > 0}> 1793 - <div class="flex gap-1 items-center text-sm"> 1794 - <CircleDot class="w-3 h-3" /> 1795 - <span>{openIssues()}</span> 1796 - </div> 1797 - </Show> 1798 - <Show when={openPRs() !== undefined && openPRs()! > 0}> 1799 - <div class="flex gap-1 items-center text-sm"> 1800 - <GitPullRequest class="w-3 h-3" /> 1801 - <span>{openPRs()}</span> 1802 - </div> 1803 - </Show> 1804 - </div> 1805 - </Show> 1839 + <RepoStatsList {...props} /> 1806 1840 </div> 1807 1841 ); 1808 1842 };
+21 -31
src/pages/search.tsx
··· 17 17 import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js'; 18 18 19 19 import { ErrorState, LoadingState, PaginationControls } from '../components/common'; 20 + import { RepoStatsList } from '../components/repo'; 20 21 import { getIssueRecord } from '../lib/api/issues'; 21 22 import { getPullRecord } from '../lib/api/pulls'; 22 23 import { getRepoByDid } from '../lib/api/repos'; ··· 109 110 return `${normalized.slice(0, max - 1).trimEnd()}...`; 110 111 }; 111 112 112 - const collectionLabel = (nsid: SearchCollection): string => { 113 - if (nsid === 'sh.tangled.repo') return 'repo'; 114 - if (nsid === 'sh.tangled.repo.issue') return 'issue'; 115 - if (nsid === 'sh.tangled.repo.pull') return 'pull'; 116 - if (nsid === 'sh.tangled.repo.issue.comment') return 'issue comment'; 117 - if (nsid === 'sh.tangled.repo.pull.comment') return 'pull comment'; 118 - if (nsid === 'sh.tangled.actor.profile') return 'profile'; 119 - if (nsid === 'sh.tangled.label.definition') return 'label'; 120 - return 'string'; 121 - }; 122 - 123 113 const SearchResultIcon: Component<{ hit: SearchHit }> = (props) => ( 124 114 <Switch fallback={<FileText class="w-4 h-4 mr-1.5 shrink-0" />}> 125 115 <Match when={props.hit.nsid === 'sh.tangled.repo'}> ··· 210 200 }); 211 201 const parentQuery = createQuery(() => ({ 212 202 queryKey: ['search-parent', props.hit.nsid, parentUri()], 203 + enabled: Boolean(parentUri()), 213 204 queryFn: async () => isIssueComment() 214 - ? { kind: 'issue' as const, record: await getIssueRecord(parentUri()) } 215 - : { kind: 'pull' as const, record: await getPullRecord(parentUri()) }, 205 + ? { kind: 'issue' as const, record: await getIssueRecord(parentUri()!) } 206 + : { kind: 'pull' as const, record: await getPullRecord(parentUri()!) }, 216 207 staleTime: 300_000, 217 208 })); 218 209 219 210 return ( 220 211 <Show when={parentQuery.data} fallback={<span>comment</span>}> 221 212 {(parent) => { 222 - const repoDid = createMemo(() => parent().kind === 'issue' 223 - ? (parent().record.value as { repo: Did }).repo 224 - : (parent().record.value as { target: { repo: Did } }).target.repo); 225 - return ( 226 - <RepoThreadLink kind={parent().kind} repoDid={repoDid()} threadRef={parent().record.uri}> 227 - comment on {parent().kind} 228 - </RepoThreadLink> 229 - ); 230 - }} 231 - </Show> 213 + const repoDid = createMemo(() => parent().kind === 'issue' 214 + ? (parent().record.value as { repo: Did }).repo 215 + : (parent().record.value as { target: { repo: Did } }).target.repo); 216 + return ( 217 + <RepoThreadLink kind={parent().kind} repoDid={repoDid()} threadRef={parent().record.rkey}> 218 + comment on {parent().kind} 219 + </RepoThreadLink> 220 + ); 221 + }} 222 + </Show> 232 223 ); 233 224 }; 234 225 ··· 297 288 </A> 298 289 </Match> 299 290 <Match when={props.hit.nsid === 'sh.tangled.repo.issue'}> 300 - <RepoThreadLink kind="issue" repoDid={(props.hit.value as { repo: Did }).repo} threadRef={props.hit.uri}> 291 + <RepoThreadLink kind="issue" repoDid={(props.hit.value as { repo: Did }).repo} threadRef={props.hit.rkey}> 301 292 {title()} 302 293 </RepoThreadLink> 303 294 </Match> 304 295 <Match when={props.hit.nsid === 'sh.tangled.repo.pull'}> 305 - <RepoThreadLink kind="pull" repoDid={(props.hit.value as { target: { repo: Did } }).target.repo} threadRef={props.hit.uri}> 296 + <RepoThreadLink kind="pull" repoDid={(props.hit.value as { target: { repo: Did } }).target.repo} threadRef={props.hit.rkey}> 306 297 {title()} 307 298 </RepoThreadLink> 308 299 </Match> ··· 347 338 </Show> 348 339 349 340 <div class="text-gray-400 text-sm font-mono inline-flex flex-wrap gap-4 mt-auto pt-4"> 350 - <div class="flex gap-2 items-center text-sm"> 351 - <span class="untangled-search-type-dot" /> 352 - <span>{collectionLabel(props.hit.nsid)}</span> 353 - </div> 354 341 <div class="flex gap-1 items-center text-sm"> 355 342 <UserRound class="w-3 h-3" /> 356 - <span>{props.hit.author.handle}</span> 343 + <a href={`/${props.hit.author.did}`}>{props.hit.author.handle}</a> 357 344 </div> 358 345 <ResultRepoContext hit={props.hit} /> 359 346 <Show when={createdAt()}> 360 347 <div class="flex gap-1 items-center text-sm"> 361 348 <span>{formatRelativeTime(createdAt())}</span> 362 349 </div> 350 + </Show> 351 + <Show when={props.hit.nsid === 'sh.tangled.repo' && (props.hit.value as { repoDid?: string }).repoDid}> 352 + {(repoDid) => <RepoStatsList repoDid={repoDid() as Did} />} 363 353 </Show> 364 354 </div> 365 355 ··· 497 487 498 488 <div class="untangled-search-grid px-2"> 499 489 <div class="untangled-search-main"> 500 - <form onSubmit={onSubmit} class="flex items-center gap-2"> 490 + <form onSubmit={onSubmit} class="sticky top-0 z-10 py-3 bg-slate-100 dark:bg-gray-900 flex items-center gap-2"> 501 491 <div class="flex relative w-full"> 502 492 <div class="flex-1 flex relative"> 503 493 <input