import { useQuery } from '@tanstack/react-query';
import {
CircleDotIcon,
FolderGitIcon,
GitBranchIcon,
GitPullRequestIcon,
LockIcon,
PlusIcon,
TagIcon,
} from 'lucide-react';
import { useState } from 'react';
import { Badge } from '#/components/atoms/badge.jsx';
import { ButtonLink } from '#/components/atoms/button-link.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { AccountPanel } from '#/components/molecules/account-panel.jsx';
import { EmptyState } from '#/components/molecules/empty-state.jsx';
import { PageNotice } from '#/components/molecules/page-notice.jsx';
import { RepoActivity } from '#/components/molecules/repo-activity.jsx';
import { RepoListSkeleton } from '#/components/molecules/repo-list-skeleton.jsx';
import { SearchField } from '#/components/molecules/search-field.jsx';
import { bytes, timeAgo } from '#/lib/format.js';
import {
account,
branchRefs,
isEmpty,
loadRepoSummary,
loadRepos,
repoList,
repoSize,
reposKey,
tagRefs,
} from '#/lib/git.js';
import { COLUMN } from '#/lib/layout.js';
import { useSession } from '#/lib/session.jsx';
import { cn } from '#/lib/utils.js';
/* The separator belongs to the reading that follows it, so a row that wraps
never ends a line on a dot. */
const LEAD = "before:mr-2 before:text-faint before:content-['ยท']";
/** One reading: an icon and a number, with what it counts in the title. */
function Metric({ icon, value, unit, lead }) {
return (
{icon}
{value}
);
}
/** What this repository holds, in the order a reader asks for it. */
function readingsOf(repo) {
const branches = branchRefs(repo).length;
const tags = tagRefs(repo).length;
const readings = [
{
key: 'branches',
icon: ,
value: branches,
unit: branches === 1 ? 'branch' : 'branches',
},
];
if (tags > 0) {
readings.push({
key: 'tags',
icon: ,
value: tags,
unit: tags === 1 ? 'tag' : 'tags',
});
}
return readings;
}
/** The open pull request and issue counts, once the counts read lands. */
function RepoCounts({ counts }) {
if (!counts) return null;
const items = [
{
key: 'pulls',
icon: ,
value: counts.openPulls,
unit: counts.openPulls === 1 ? 'open pull request' : 'open pull requests',
},
{
key: 'issues',
icon: ,
value: counts.openIssues,
unit: counts.openIssues === 1 ? 'open issue' : 'open issues',
},
].filter((item) => item.value > 0);
return items.map((item) => (
));
}
export function ReposPage() {
const { session } = useSession();
const [filter, setFilter] = useState('');
// The account gate already loaded (or primed) the rows and pushes fresh
// ones into this key, so the page opens on content and never refetches
// on its own.
const { data, error } = useQuery({
queryKey: [reposKey()],
queryFn: loadRepos,
initialData: () => {
const list = repoList();
return list.length > 0 ? list : undefined;
},
staleTime: Number.POSITIVE_INFINITY,
});
// The counts are one read for the whole listing, behind the first paint:
// the cards draw from the rows already in hand, and the counts fill in when
// this lands. Each is a discovery pass on the server, so it is asked for
// here, where the counts show, and nowhere else.
const { data: summary } = useQuery({
queryKey: ['repo-summary'],
queryFn: loadRepoSummary,
staleTime: Number.POSITIVE_INFINITY,
});
// Creating writes the record into this account's own repo, so the button
// is the owner's. A visitor who is not signed in as the owner cannot.
const mine = Boolean(session && session.did === account.did);
const newButton = (
New repository
);
if (error) {
return {error.message};
}
if (!data) {
return ;
}
// The rows a reader typed for, by name or description. The filter is a
// field above the list the way a forge's is, and empty it shows everything.
const shown = filter
? data.filter((repo) =>
`${repo.name} ${repo.description ?? ''}`
.toLowerCase()
.includes(filter.toLowerCase()),
)
: data;
return (
{/* Whose repositories these are, once, beside the list of them. */}
{data.length > 0 && (
{mine && newButton}
)}
{data.length === 0 ? (
}
title="No repositories yet"
description="Create one, or push one with the git helper and it appears here."
action={mine && newButton}
/>
) : shown.length === 0 ? (
) : (