improve search page results, show repo metrics for repo results
27 unmodified lines2829303132333334353637381615 unmodified linesExpand all165416551656165716571658165816591660166116621663166416651666166716681660166916701662166316711672167316741675167616771678167916801681168216831684168516861687168816721689169016911692169316941695169616971698169917001701170216791703170417051706170717081709168617101711171217131714171517161693171717181719172017219 unmodified lines1731173217331734173517361737171317381739171617171718171917201740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180727 unmodified lines1835183618371838175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051839184018411842184327 unmodified linesimport { marked } from 'marked';import { A } from '@solidjs/router';import { For, Show, createMemo, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js';import type { RepoContext, TreeEntry } from '../lib/api/repos';import type { Did } from '@atcute/lexicons/syntax';import { createQuery } from '@tanstack/solid-query';import { getRepo, getRepoLanguages } from '../lib/api/repos';import { getRepo, getRepoLanguages, getRepoForkCount, getRepoByDid } from '../lib/api/repos';import { getRepoStarCount } from '../lib/api/stars';import { getRepoIssueCount } from '../lib/api/issues';import { getRepoPullCount } from '../lib/api/pulls';import { formatRelativeTime, languageColor } from '../lib/repo-utils';1615 unmodified linesExpand all noBorder?: boolean; showOwner?: boolean;}
export const RepoCard: Component<RepoCardProps> = (props) => { const linkHref = () => props.href || `/${props.owner}/${props.name}`;export interface RepoStatsListProps { repoDid?: string; owner?: string; name?: string; language?: string; stars?: number; forks?: number; openIssues?: number; openPRs?: number;}
// Define queries for statisticsexport const RepoStatsList: Component<RepoStatsListProps> = (props) => { const repoContextQuery = createQuery(() => ({ queryKey: ['repo-context', props.owner, props.name], queryFn: () => getRepo(props.owner, props.name), queryKey: props.repoDid ? ['repo-by-did', props.repoDid] : ['repo-context', props.owner, props.name], queryFn: async () => { if (props.repoDid) { return getRepoByDid(props.repoDid as Did); } else if (props.owner && props.name) { return getRepo(props.owner, props.name); } return null; }, enabled: Boolean(props.repoDid || (props.owner && props.name)), staleTime: 300_000, }));
const repoContext = () => repoContextQuery.data;
const starCountQuery = createQuery(() => ({ queryKey: ['repo-stars-count', repoContext()?.repoDid], queryFn: () => getRepoStarCount(repoContext()!), enabled: !!repoContext(), enabled: !!repoContext() && props.stars === undefined, staleTime: 300_000, }));
const forkCountQuery = createQuery(() => ({ queryKey: ['repo-fork-count', repoContext()?.repoDid], queryFn: () => getRepoForkCount(repoContext()!), enabled: !!repoContext() && props.forks === undefined, staleTime: 300_000, }));
const issueCountQuery = createQuery(() => ({ queryKey: ['repo-issues-count', repoContext()?.repoDid], queryFn: () => getRepoIssueCount(repoContext()!), enabled: !!repoContext(), enabled: !!repoContext() && props.openIssues === undefined, staleTime: 300_000, }));
const pullCountQuery = createQuery(() => ({ queryKey: ['repo-pulls-count', repoContext()?.repoDid], queryFn: () => getRepoPullCount(repoContext()!), enabled: !!repoContext(), enabled: !!repoContext() && props.openPRs === undefined, staleTime: 300_000, }));
const languagesQuery = createQuery(() => ({ queryKey: ['repo-languages', repoContext()?.repoDid], queryFn: () => getRepoLanguages(repoContext()!, 'HEAD'), enabled: !!repoContext(), enabled: !!repoContext() && props.language === undefined, staleTime: 300_000, }));
const resolvedLanguage = () => {9 unmodified lines return lang ? languageColor(lang) : undefined; };
const stars = () => props.stars !== undefined ? props.stars : starCountQuery.data; const forks = () => props.forks !== undefined ? props.forks : forkCountQuery.data; const openIssues = () => props.openIssues !== undefined ? props.openIssues : issueCountQuery.data; const openPRs = () => props.openPRs !== undefined ? props.openPRs : pullCountQuery.data; const forks = () => props.forks;
const isMetricsLoading = () => { const langLoading = props.language === undefined && (languagesQuery.isLoading || repoContextQuery.isLoading); const starsLoading = props.stars === undefined && (starCountQuery.isLoading || repoContextQuery.isLoading); const issuesLoading = props.openIssues === undefined && (issueCountQuery.isLoading || repoContextQuery.isLoading); const prsLoading = props.openPRs === undefined && (pullCountQuery.isLoading || repoContextQuery.isLoading); return langLoading || starsLoading || issuesLoading || prsLoading; const contextLoading = repoContextQuery.isLoading && (props.repoDid !== undefined || (props.owner !== undefined && props.name !== undefined)); const langLoading = props.language === undefined && (languagesQuery.isLoading || contextLoading); const starsLoading = props.stars === undefined && (starCountQuery.isLoading || contextLoading); const forksLoading = props.forks === undefined && (forkCountQuery.isLoading || contextLoading); const issuesLoading = props.openIssues === undefined && (issueCountQuery.isLoading || contextLoading); const prsLoading = props.openPRs === undefined && (pullCountQuery.isLoading || contextLoading); return langLoading || starsLoading || forksLoading || issuesLoading || prsLoading; };
return ( <Show when={!isMetricsLoading()} fallback={ <div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto"> <SkeletonBlock class="h-4 w-16" /> <SkeletonBlock class="h-4 w-12" /> <SkeletonBlock class="h-4 w-12" /> <SkeletonBlock class="h-4 w-12" /> </div> } > <div class="text-gray-400 text-sm font-mono inline-flex flex-wrap gap-4 mt-auto"> <Show when={resolvedLanguage()}> <div class="flex gap-2 items-center text-sm"> <span class="rounded-full inline-block shrink-0" style={{ 'background-color': langColor(), width: '10px', height: '10px' }} /> <span>{resolvedLanguage()}</span> </div> </Show> <Show when={stars() !== undefined && stars()! > 0}> <div class="flex gap-1 items-center text-sm"> <Star class="w-3 h-3 fill-current" /> <span>{stars()}</span> </div> </Show> <Show when={forks() !== undefined && forks()! > 0}> <div class="flex gap-1 items-center text-sm"> <GitFork class="w-3 h-3" /> <span>{forks()}</span> </div> </Show> <Show when={openIssues() !== undefined && openIssues()! > 0}> <div class="flex gap-1 items-center text-sm"> <CircleDot class="w-3 h-3" /> <span>{openIssues()}</span> </div> </Show> <Show when={openPRs() !== undefined && openPRs()! > 0}> <div class="flex gap-1 items-center text-sm"> <GitPullRequest class="w-3 h-3" /> <span>{openPRs()}</span> </div> </Show> </div> </Show> );};
export const RepoCard: Component<RepoCardProps> = (props) => { const linkHref = () => props.href || `/${props.owner}/${props.name}`;
return ( <div class={clsx( props.compact ? 'focus-within:bg-gray-100 dark:focus-within:bg-gray-800/80' : 'min-h-32', 'py-4 px-6 gap-1 flex flex-col drop-shadow-sm bg-white dark:bg-gray-800',27 unmodified lines {props.description} </div> </Show>
<Show when={!isMetricsLoading()} fallback={ <div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto"> <SkeletonBlock class="h-4 w-16" /> <SkeletonBlock class="h-4 w-12" /> <SkeletonBlock class="h-4 w-12" /> <SkeletonBlock class="h-4 w-12" /> </div> } > <div class="text-gray-400 text-sm font-mono inline-flex gap-4 mt-auto"> <Show when={resolvedLanguage()}> <div class="flex gap-2 items-center text-sm"> <span class="rounded-full inline-block" style={{ 'background-color': langColor(), width: '10px', height: '10px' }} /> <span>{resolvedLanguage()}</span> </div> </Show> <Show when={stars() !== undefined && stars()! > 0}> <div class="flex gap-1 items-center text-sm"> <Star class="w-3 h-3 fill-current" /> <span>{stars()}</span> </div> </Show> <Show when={forks() !== undefined && forks()! > 0}> <div class="flex gap-1 items-center text-sm"> <GitFork class="w-3 h-3" /> <span>{forks()}</span> </div> </Show> <Show when={openIssues() !== undefined && openIssues()! > 0}> <div class="flex gap-1 items-center text-sm"> <CircleDot class="w-3 h-3" /> <span>{openIssues()}</span> </div> </Show> <Show when={openPRs() !== undefined && openPRs()! > 0}> <div class="flex gap-1 items-center text-sm"> <GitPullRequest class="w-3 h-3" /> <span>{openPRs()}</span> </div> </Show> </div> </Show> <RepoStatsList {...props} /> </div> );};
15 unmodified lines16171819202122232484 unmodified lines10911011111211211311411511611711811912012112211311411511682 unmodified lines19920020120220320421421520520620720820921021121222222322422522622722822923023121321421521621721821922022122222322422522660 unmodified lines28728828929030029129229329429530529629729829930036 unmodified lines337338339340350351352353341342356343344345346347348349350351352353354355356357128 unmodified linesExpand all48648748848950049049149249349494 unmodified lines15 unmodified linesimport type { Did, ResourceUri } from '@atcute/lexicons/syntax';import { For, Match, Show, Switch, createEffect, createMemo, createSignal, type Component, type JSX } from 'solid-js';
import { ErrorState, LoadingState, PaginationControls } from '../components/common';import { RepoStatsList } from '../components/repo';import { getIssueRecord } from '../lib/api/issues';import { getPullRecord } from '../lib/api/pulls';import { getRepoByDid } from '../lib/api/repos';import {84 unmodified lines } return `${normalized.slice(0, max - 1).trimEnd()}...`;};
const collectionLabel = (nsid: SearchCollection): string => { if (nsid === 'sh.tangled.repo') return 'repo'; if (nsid === 'sh.tangled.repo.issue') return 'issue'; if (nsid === 'sh.tangled.repo.pull') return 'pull'; if (nsid === 'sh.tangled.repo.issue.comment') return 'issue comment'; if (nsid === 'sh.tangled.repo.pull.comment') return 'pull comment'; if (nsid === 'sh.tangled.actor.profile') return 'profile'; if (nsid === 'sh.tangled.label.definition') return 'label'; return 'string';};
const SearchResultIcon: Component<{ hit: SearchHit }> = (props) => ( <Switch fallback={<FileText class="w-4 h-4 mr-1.5 shrink-0" />}> <Match when={props.hit.nsid === 'sh.tangled.repo'}> <Show when={(props.hit.value as { source?: string }).source} fallback={<BookMarked class="w-4 h-4 mr-1.5 shrink-0" />}>82 unmodified lines return (props.hit.value as { pull: ResourceUri }).pull; }); const parentQuery = createQuery(() => ({ queryKey: ['search-parent', props.hit.nsid, parentUri()], enabled: Boolean(parentUri()), queryFn: async () => isIssueComment() ? { kind: 'issue' as const, record: await getIssueRecord(parentUri()) } : { kind: 'pull' as const, record: await getPullRecord(parentUri()) }, ? { kind: 'issue' as const, record: await getIssueRecord(parentUri()!) } : { kind: 'pull' as const, record: await getPullRecord(parentUri()!) }, staleTime: 300_000, }));
return ( <Show when={parentQuery.data} fallback={<span>comment</span>}> {(parent) => { const repoDid = createMemo(() => parent().kind === 'issue' ? (parent().record.value as { repo: Did }).repo : (parent().record.value as { target: { repo: Did } }).target.repo); return ( <RepoThreadLink kind={parent().kind} repoDid={repoDid()} threadRef={parent().record.uri}> comment on {parent().kind} </RepoThreadLink> ); }} </Show> const repoDid = createMemo(() => parent().kind === 'issue' ? (parent().record.value as { repo: Did }).repo : (parent().record.value as { target: { repo: Did } }).target.repo); return ( <RepoThreadLink kind={parent().kind} repoDid={repoDid()} threadRef={parent().record.rkey}> comment on {parent().kind} </RepoThreadLink> ); }} </Show> );};
const ResultRepoContext: Component<{ hit: SearchHit }> = (props) => {60 unmodified lines {props.hit.author.handle}/{repoName()} </A> </Match> <Match when={props.hit.nsid === 'sh.tangled.repo.issue'}> <RepoThreadLink kind="issue" repoDid={(props.hit.value as { repo: Did }).repo} threadRef={props.hit.uri}> <RepoThreadLink kind="issue" repoDid={(props.hit.value as { repo: Did }).repo} threadRef={props.hit.rkey}> {title()} </RepoThreadLink> </Match> <Match when={props.hit.nsid === 'sh.tangled.repo.pull'}> <RepoThreadLink kind="pull" repoDid={(props.hit.value as { target: { repo: Did } }).target.repo} threadRef={props.hit.uri}> <RepoThreadLink kind="pull" repoDid={(props.hit.value as { target: { repo: Did } }).target.repo} threadRef={props.hit.rkey}> {title()} </RepoThreadLink> </Match> <Match when={props.hit.nsid === 'sh.tangled.actor.profile'}>36 unmodified lines <div class="untangled-search-description text-gray-600 dark:text-gray-300 text-sm">{body()}</div> </Show>
<div class="text-gray-400 text-sm font-mono inline-flex flex-wrap gap-4 mt-auto pt-4"> <div class="flex gap-2 items-center text-sm"> <span class="untangled-search-type-dot" /> <span>{collectionLabel(props.hit.nsid)}</span> </div> <div class="flex gap-1 items-center text-sm"> <UserRound class="w-3 h-3" /> <span>{props.hit.author.handle}</span> <a href={`/${props.hit.author.did}`}>{props.hit.author.handle}</a> </div> <ResultRepoContext hit={props.hit} /> <Show when={createdAt()}> <div class="flex gap-1 items-center text-sm"> <span>{formatRelativeTime(createdAt())}</span> </div> </Show> <Show when={props.hit.nsid === 'sh.tangled.repo' && (props.hit.value as { repoDid?: string }).repoDid}> {(repoDid) => <RepoStatsList repoDid={repoDid() as Did} />} </Show> </div>
<Show when={topics().length > 0}> <div class="mt-2 flex flex-wrap gap-2">128 unmodified linesExpand all <h1 class="text-2xl font-bold mb-4 px-2">search</h1>
<div class="untangled-search-grid px-2"> <div class="untangled-search-main"> <form onSubmit={onSubmit} class="flex items-center gap-2"> <form onSubmit={onSubmit} class="sticky top-0 z-10 py-3 bg-slate-100 dark:bg-gray-900 flex items-center gap-2"> <div class="flex relative w-full"> <div class="flex-1 flex relative"> <input id="search-q"94 unmodified lines