chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useQuery } from '@tanstack/react-query';
import { GitBranchIcon, TagIcon } from 'lucide-react';
import { Skeleton } from '#/components/atoms/skeleton.jsx';
import { CodeMenu } from '#/components/molecules/code-menu.jsx';
import { DirTable } from '#/components/molecules/dir-table.jsx';
import { FileCommit } from '#/components/molecules/file-commit.jsx';
import { LatestCommit } from '#/components/molecules/latest-commit.jsx';
import { ListBox } from '#/components/molecules/list-box.jsx';
import { ListSkeleton } from '#/components/molecules/list-skeleton.jsx';
import { PageNotice } from '#/components/molecules/page-notice.jsx';
import { QuickSetup } from '#/components/molecules/quick-setup.jsx';
import { ReadingRepo } from '#/components/molecules/reading-repo.jsx';
import { ReadmePanel } from '#/components/molecules/readme-panel.jsx';
import { RefSelect } from '#/components/molecules/ref-select.jsx';
import { RepoAbout } from '#/components/molecules/repo-about.jsx';
import { RepoHeading } from '#/components/molecules/repo-heading.jsx';
import { Screen } from '#/components/molecules/screen.jsx';
import { SourceCrumbs } from '#/components/molecules/source-crumbs.jsx';
import { SourcePanes } from '#/components/molecules/source-panes.jsx';
import {
account,
branchRefs,
capabilities,
isEmpty,
readerFor,
repoRecord,
repoSize,
tagRefs,
} from '#/lib/git.js';
import { useNavigate } from '#/lib/navigation.jsx';
import { useDirectory, useFileCommit, useHistory } from '#/lib/source.js';
const README = /^readme(\.md|\.txt|\.markdown)?$/i;
export function TreePage({ repo, refName, path, space }) {
const navigate = useNavigate();
const record = repoRecord(repo, space);
// The listing the pane beside this one draws. Reading it here too costs
// nothing: one query key serves both.
const { data: entries, error } = useDirectory(repo, refName, path, space);
// What this directory says about itself. A directory with no README leaves
// the reading pane empty, which is what a checkout looks like.
const readme = (entries ?? []).find(
(entry) => entry.type === 'file' && README.test(entry.name),
);
const { data: readmeFile } = useQuery({
queryKey: ['readme', repo, refName, path, readme?.name, space ?? ''],
enabled: Boolean(readme),
queryFn: () =>
readerFor(repo, undefined, space).readFile(
account.did,
repo,
refName,
path ? `${path}/${readme.name}` : readme.name,
),
});
// The tip of the ref and how far its history runs. The gauges in the pane
// beside this one read the same answer.
const { data: history } = useHistory(repo, refName, space);
// The last commit to touch this directory, for the panel's header inside
// one. The walk runs behind the paint and the line fills in when it lands.
const { data: dirCommit, isPending: dirCommitPending } = useFileCommit(
repo,
refName,
path,
space,
);
if (!record) {
return <PageNotice>No such repository.</PageNotice>;
}
const branches = branchRefs(record).length;
const tags = tagRefs(record).length;
// What this view is looking at: which ref, and where inside it. The
// sections of the repository, and cloning it, are in the band above.
//
// Inside a directory the path leads, and the picker answers its right, the
// way a forge reads: the reader is navigating the path. At the root there
// is no path, so the picker stands first and the ref counts beside it.
const selectRef = (next) =>
navigate(`/${encodeURIComponent(repo)}/tree/${encodeURI(next)}`);
const refPicker = (
<RefSelect repo={record} value={refName} onSelect={selectRef} />
);
if (isEmpty(record)) {
return (
<Screen>
<div className="px-(--gutter) pt-5">
<RepoHeading repo={repo} space={space} />
</div>
<div className="px-(--gutter) py-6">
<QuickSetup record={record} space={space} />
</div>
</Screen>
);
}
const readmePanel = readme && (
<ReadmePanel
className="m-0 mt-4"
name={readme.name}
text={readmeFile?.text}
repo={repo}
refName={refName}
basePath={path}
space={space}
/>
);
// The root is where a repository is read about: its files in one panel
// with the latest commit on the panel's header, what it says about itself
// in the panel under that, and what it is beside them. Inside a directory
// the reader is navigating, and the panes take over.
if (!path) {
return (
<Screen>
<div className="px-(--gutter) pt-5">
<RepoHeading repo={repo} space={space} />
</div>
<div className="flex flex-col gap-6 px-(--gutter) py-4 lg:flex-row lg:items-start">
<div className="min-w-0 flex-1">
{/* What the panel below is looking at, and the one action a
repository has: the way to take it away. */}
<div className="mb-3 flex flex-wrap items-center gap-3">
{refPicker}
<span className="flex items-center gap-1.5 text-[13px] text-faint">
<GitBranchIcon className="size-3.5" />
<span className="font-semibold text-foreground">
{branches}
</span>
{branches === 1 ? 'branch' : 'branches'}
</span>
{tags > 0 && (
<span className="flex items-center gap-1.5 text-[13px] text-faint">
<TagIcon className="size-3.5" />
<span className="font-semibold text-foreground">{tags}</span>
{tags === 1 ? 'tag' : 'tags'}
</span>
)}
<span className="ml-auto">
<CodeMenu
repo={repo}
httpClone={capabilities.httpClone}
space={space}
/>
</span>
</div>
{error && <PageNotice tone="error">{error.message}</PageNotice>}
<ListBox className="m-0">
{history?.head ? (
<LatestCommit
repo={repo}
refName={refName}
commit={history.head}
count={history.count}
truncated={history.truncated}
space={space}
/>
) : (
<div className="flex min-h-[41px] items-center border-border border-b bg-muted px-4 py-2.5">
<Skeleton className="h-4 w-72 max-w-full" />
</div>
)}
{entries ? (
<DirTable
repo={repo}
refName={refName}
path={path}
entries={entries}
/>
) : (
!error && (
<>
<div className="empty:hidden border-border border-b p-4">
<ReadingRepo size={repoSize(record)} />
</div>
<ListSkeleton rows={5} />
</>
)
)}
</ListBox>
{readmePanel}
</div>
<aside className="w-full shrink-0 lg:w-72">
<RepoAbout
record={record}
repo={repo}
refName={refName}
space={space}
/>
</aside>
</div>
</Screen>
);
}
return (
<div className="flex min-h-0 flex-1 flex-col">
<SourcePanes
repo={repo}
record={record}
refName={refName}
onSelectRef={selectRef}
dirPath={path}
space={space}
>
<div className="px-(--gutter) pb-(--gutter)">
<SourceCrumbs
repo={repo}
refName={refName}
path={path}
actions={<span className="md:hidden">{refPicker}</span>}
/>
{error && <PageNotice tone="error">{error.message}</PageNotice>}
{!entries && !error && (
<>
<div className="empty:hidden pb-4">
<ReadingRepo size={repoSize(record)} />
</div>
<ListBox className="m-0">
<ListSkeleton rows={5} />
</ListBox>
</>
)}
{entries && (
<>
<ListBox className="m-0">
<FileCommit
repo={repo}
commit={dirCommit}
pending={dirCommitPending}
historyHref={`/${encodeURIComponent(repo)}/commits/${encodeURI(refName)}/${encodeURI(path)}`}
className="px-4"
/>
<DirTable
repo={repo}
refName={refName}
path={path}
entries={entries}
/>
</ListBox>
{readmePanel}
</>
)}
</div>
</SourcePanes>
</div>
);
}