add user profiles
3468 unmodified linesExpand all34693470347134723473347434753476347734783468 unmodified linesExpand all .untangled-home-promo-card-pink a { color: rgb(244 114 182); }}
.untangled-vouch-label { display: grid; grid-template-columns: auto 1fr auto;}
1223456221 unmodified linesExpand all228229230231232233234235236237238239240241242243244245246247248249250123 unmodified linesExpand allimport clsx from 'clsx';import { Bell, LogOut, RotateCcw, Save, Search, Settings } from 'lucide-solid';import { Bell, LogOut, RotateCcw, Save, Search, Settings, UserRound } from 'lucide-solid';import { A, useLocation, useNavigate } from '@solidjs/router';import { createQuery, useQueryClient } from '@tanstack/solid-query';import { For, Show, createEffect, createSignal, onCleanup, onMount, type Component, type JSX } from 'solid-js';
221 unmodified linesExpand all <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700"> <div class="font-medium truncate">{viewerQuery.data?.handle ?? auth.currentDid()}</div> <div class="text-gray-500 dark:text-gray-400 truncate">{auth.currentDid()}</div> </div> <Show when={viewerQuery.data?.handle}> {(handle) => ( <A href={`/${handle()}`} class="w-full px-4 py-3 flex items-center gap-2 text-left hover:bg-gray-50 hover:dark:bg-gray-700/50 text-black dark:text-white no-underline hover:no-underline border-b border-gray-200 dark:border-gray-700" onClick={(e) => { const details = e.currentTarget.closest('details'); if (details) details.open = false; }} > <UserRound class="size-4" /> profile </A> )} </Show> <button type="button" class="w-full px-4 py-3 flex items-center gap-2 text-left hover:bg-gray-50 hover:dark:bg-gray-700/50" onClick={() => void auth.signOut()}123 unmodified linesExpand all17 unmodified lines18192021222324252612 unmodified lines39404142434445464717 unmodified linesimport { HomePage } from './pages/home';import { NotFoundPage } from './pages/not-found';import { OAuthCallbackPage } from './pages/oauth-callback';import { SearchPage } from './pages/search';import { ProfilePage } from './pages/profile';
export const AppRoutes: Component = () => ( <Router root={RootShell}> <Route path="/" component={HomePage} />12 unmodified lines <Route path="/commit/:ref" component={CommitPage} /> <Route path="/blob/:ref/*path" component={BlobPage} /> <Route path="/tree/:ref/*path" component={RepoCodePage} /> </Route> <Route path="/:actor" component={ProfilePage} /> <Route path="*404" component={NotFoundPage} /> </Router>);
27 unmodified lines282930313233343536373839401583 unmodified linesExpand all16241625162616271628162916301631163216271633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699163216331634163516361700170117021703170417051640170617071708164317091710164517111712171317141715171617171718171917201654172117221723172417251726172716601728172917301731173216651666166716681733173417351736173717381739174017411742174317441745174617471748174917501673175117521753167616771754175517561679175717581759168216831760176117621685176317641765168816891766176717681691176917701771169416951772177317741697177517761777177817794 unmodified lines27 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 { createQuery } from '@tanstack/solid-query';import { getRepo, getRepoLanguages } 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';import { getTangledAppviewService } from '../lib/settings';import { Avatar, SkeletonBlock, buttonStyles, cardStyles } from './common';
1583 unmodified linesExpand all openPRs?: number; compact?: boolean; href?: string; noBorder?: boolean; showOwner?: boolean;}
export const RepoCard: Component<RepoCardProps> = (props) => { const linkHref = () => props.href || `/${props.owner}/${props.name}`; const langColor = () => props.language ? languageColor(props.language) : undefined;
// Define queries for statistics const repoContextQuery = createQuery(() => ({ queryKey: ['repo-context', props.owner, props.name], queryFn: () => getRepo(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(), staleTime: 300_000, }));
const issueCountQuery = createQuery(() => ({ queryKey: ['repo-issues-count', repoContext()?.repoDid], queryFn: () => getRepoIssueCount(repoContext()!), enabled: !!repoContext(), staleTime: 300_000, }));
const pullCountQuery = createQuery(() => ({ queryKey: ['repo-pulls-count', repoContext()?.repoDid], queryFn: () => getRepoPullCount(repoContext()!), enabled: !!repoContext(), staleTime: 300_000, }));
const languagesQuery = createQuery(() => ({ queryKey: ['repo-languages', repoContext()?.repoDid], queryFn: () => getRepoLanguages(repoContext()!, 'HEAD'), enabled: !!repoContext(), staleTime: 300_000, }));
const resolvedLanguage = () => { if (props.language !== undefined) return props.language; const list = languagesQuery.data?.languages; if (!list || list.length === 0) return undefined; const sorted = [...list].sort((a, b) => b.percentage - a.percentage); return sorted[0].name; };
const langColor = () => { const lang = resolvedLanguage(); return lang ? languageColor(lang) : undefined; };
const stars = () => props.stars !== undefined ? props.stars : starCountQuery.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; };
return ( <div class={clsx( 'flex flex-col gap-1 bg-white dark:bg-gray-800 drop-shadow-sm', props.compact ? 'p-3 md:p-4' : 'p-4 md:p-6', !props.noBorder && 'border border-gray-200 dark:border-gray-700 rounded-sm', !props.compact && 'min-h-[8rem]', props.compact && 'focus-within:bg-gray-100 dark:focus-within:bg-gray-800/80' 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', !props.noBorder && 'border border-gray-200 dark:border-gray-700 rounded-sm' )} > <div class="font-medium dark:text-white flex items-center justify-between"> <div class="flex items-center min-w-0 flex-1 mr-2 text-base"> <div class="flex items-center min-w-0 flex-1 mr-2"> <Show when={props.isFork} fallback={<BookMarked class="w-4 h-4 mr-1.5 shrink-0 text-gray-400" />} fallback={<BookMarked class="w-4 h-4 mr-1.5 shrink-0" />} > <GitFork class="w-4 h-4 mr-1.5 shrink-0 text-gray-400" /> <GitFork class="w-4 h-4 mr-1.5 shrink-0" /> </Show> <A href={linkHref()} class={clsx( 'truncate min-w-0 no-underline hover:underline text-black dark:text-white font-bold', props.compact && 'focus:outline-none' )} > <Show when={props.showOwner !== false} fallback={props.name}> {props.owner}/{props.name} {props.owner}/{props.name} </Show> </A> </div> </div>
<Show when={props.description}> <div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2 mt-1"> <div class="text-gray-600 dark:text-gray-300 text-sm line-clamp-2"> {props.description} </div> </Show>
<Show when={props.language || props.stars !== undefined || props.forks !== undefined || props.openIssues !== undefined || props.openPRs !== undefined}> <div class="text-gray-400 text-xs font-mono inline-flex gap-4 mt-auto pt-2"> <Show when={props.language}> <div class="flex gap-1.5 items-center"> <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>{props.language}</span> <span>{resolvedLanguage()}</span> </div> </Show> <Show when={props.stars !== undefined && props.stars > 0}> <div class="flex gap-1 items-center"> <Show when={stars() !== undefined && stars()! > 0}> <div class="flex gap-1 items-center text-sm"> <Star class="w-3 h-3 fill-current" /> <span>{props.stars}</span> <span>{stars()}</span> </div> </Show> <Show when={props.forks !== undefined && props.forks > 0}> <div class="flex gap-1 items-center"> <Show when={forks() !== undefined && forks()! > 0}> <div class="flex gap-1 items-center text-sm"> <GitFork class="w-3 h-3" /> <span>{props.forks}</span> <span>{forks()}</span> </div> </Show> <Show when={props.openIssues !== undefined && props.openIssues > 0}> <div class="flex gap-1 items-center"> <Show when={openIssues() !== undefined && openIssues()! > 0}> <div class="flex gap-1 items-center text-sm"> <CircleDot class="w-3 h-3" /> <span>{props.openIssues}</span> <span>{openIssues()}</span> </div> </Show> <Show when={props.openPRs !== undefined && props.openPRs > 0}> <div class="flex gap-1 items-center"> <Show when={openPRs() !== undefined && openPRs()! > 0}> <div class="flex gap-1 items-center text-sm"> <GitPullRequest class="w-3 h-3" /> <span>{props.openPRs}</span> <span>{openPRs()}</span> </div> </Show> </div> </Show>4 unmodified lines470 unmodified linesExpand all4714724734744754754764774784793 unmodified lines48348448548648748748848949049110 unmodified lines50250350450550650650750850951026 unmodified lines53753853954054154154254354454537 unmodified lines58358458558658758758858959059059159259359459459559659759859859960060160260260360460560658 unmodified lines66566666766866966967067167267167367467367567667767867945 unmodified lines72572672772872772973073173273373473573673773673873974074174258 unmodified lines470 unmodified linesExpand all </div>
<Show when={auth.currentDid()} fallback={<HomeFeedEmpty title="Sign in for repo activity" body="Issues, pull requests, and repositories from your account will appear here." />} fallback={<HomeFeedEmpty title="Sign in for repo activity" body="Issues, pull requests, and repos from your account will appear here." />} > <Show when={!ownReposQuery.isLoading} fallback={<HomeFeedLoading />}3 unmodified lines fallback={<ErrorState message={ownReposQuery.error instanceof Error ? ownReposQuery.error.message : 'Failed to load activity'} />} > <Show when={activityItems().length > 0} fallback={<HomeFeedEmpty title="No repo activity yet" body="New repository, issue, and pull request activity will land here." />} fallback={<HomeFeedEmpty title="No repo activity yet" body="New repo, issue, and pull request activity will land here." />} > <div class="untangled-home-activity-list"> <For each={activityItems()}> {(item, index) => <ActivityRow item={item} index={index()} />}10 unmodified lines <div class="flex flex-col gap-3 w-full"> <HomeSectionHeader icon={<BookMarked class="size-5 text-gray-500" />} title="recent" /> <Show when={recentRepos().length > 0} fallback={<HomeEmpty label="No recent repositories." />} fallback={<HomeEmpty label="No recent repos." />} > <div class="flex flex-col gap-3"> <For each={recentRepos().slice(0, 5)}> {(repo) => (26 unmodified lines {(item) => ( <A href={item.href} class="untangled-home-repo-row"> <div class="text-sm font-semibold text-black dark:text-white truncate"> {item.title}{' '} <span class="text-gray-500 dark:text-gray-400 font-normal">#{item.number}</span> <span class="text-gray-500 dark:text-gray-400 font-normal">#{item.uri.split('/').pop()}</span> </div> <div class="mt-2 flex items-center gap-2 text-xs text-gray-500 dark:text-gray-400 min-w-0"> <StateBadge state={item.state} kind={item.kind} /> <span class="truncate min-w-0">{item.author} · {item.repoLabel}</span>37 unmodified lines </div> </aside>
<section class="untangled-home-wide-section untangled-home-lower-left"> <HomeSectionHeader icon={<UserRound class="size-5" />} title="following activity" /> <HomeSectionHeader icon={<UserRound class="size-5" />} title="timeline" /> <Show when={auth.currentDid()} fallback={<HomeEmpty label="Sign in to see followed repository activity." />} fallback={<HomeEmpty label="Sign in to see followed repo activity." />} > <Show when={!followingQuery.isLoading} fallback={<LoadingState label="Loading followed repositories..." />} fallback={<LoadingState label="Loading followed repos..." />} > <Show when={!followingQuery.error} fallback={<ErrorState message={followingQuery.error instanceof Error ? followingQuery.error.message : 'Failed to load following activity'} />} fallback={<ErrorState message={followingQuery.error instanceof Error ? followingQuery.error.message : 'Failed to load timeline'} />} > <Show when={(followingQuery.data?.activity.length ?? 0) > 0} fallback={<HomeEmpty label="No followed repository activity yet." />} fallback={<HomeEmpty label="No followed repo activity yet." />} > <div class="untangled-home-activity-list"> <For each={followingQuery.data?.activity ?? []}> {(item, index) => <ActivityRow item={item} index={index()} />}58 unmodified lines } }; return ( <div class="flex items-center gap-3"> <A href={`/${suggestion.handle}`}> <Avatar did={suggestion.did} size="size-10" /> <Avatar did={suggestion.did} size="size-10" /> </A> <div class="flex flex-col min-w-0 flex-1"> <a href={`https://tangled.org/${suggestion.handle}`} target="_blank" rel="noopener noreferrer" class="text-sm font-medium dark:text-white hover:underline truncate"> <A href={`/${suggestion.handle}`} class="text-sm font-medium dark:text-white hover:underline truncate"> {suggestion.handle} </a> </A> <span class="text-xs text-gray-500 dark:text-gray-400 truncate"> </span> </div>45 unmodified linesconst HomeFeedLoading: Component = () => ( <div class="untangled-home-feed-empty"> <LoaderCircle class="size-8 animate-spin" /> <h3>Loading activity</h3> <p>Checking your repositories.</p> <p>Checking your repos.</p> </div>);
const ActivityRow: Component<{ item: HomeActivityItem; index: number }> = (props) => { const repoUrl = () => `/${props.item.repoLabel}`; const actorUrl = () => props.item.actorHandle ? `/${props.item.actorHandle}` : '#';
const actionText = () => { if (props.item.kind === 'repo') return 'published repository'; if (props.item.kind === 'repo') return 'published repo'; if (props.item.kind === 'issue') return 'created issue in'; if (props.item.kind === 'pull') return 'opened pull request in'; return 'updated'; };58 unmodified lines