~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
116 lines · 3.9 KB Raw History
import { useQuery } from '@tanstack/react-query';
import { FolderGit2Icon, LockIcon } from 'lucide-react';
import { Badge } from '#/components/atoms/badge.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { RowIcon } from '#/components/atoms/row-icon.jsx';
import { EmptyState } from '#/components/molecules/empty-state.jsx';
import { ListRow } from '#/components/molecules/list-row.jsx';
import { xrpcGet } from '#/lib/api.js';
import { bytes, timeAgo } from '#/lib/format.js';
import { queryKeys } from '#/lib/query.js';
import { NewRepoDialog } from '#/pages/git-new-repo.jsx';

/** One line summarizing a repository's shape. */
function repoSummary(repo) {
  if (repo.branches === 0 && repo.bundles === 0) return 'nothing pushed yet';
  const parts = [
    `${repo.branches} ${repo.branches === 1 ? 'branch' : 'branches'}`,
  ];
  if (repo.tags > 0) {
    parts.push(`${repo.tags} ${repo.tags === 1 ? 'tag' : 'tags'}`);
  }
  parts.push(bytes(repo.size));
  if (repo.updatedAt) parts.push(`pushed ${timeAgo(repo.updatedAt)}`);
  return parts.join(' · ');
}

/** The repository page URL, carrying the space for a private repository. */
export function repoHref(repo) {
  const base = `/account/git/${encodeURIComponent(repo.name)}`;
  return repo.space ? `${base}?space=${encodeURIComponent(repo.space)}` : base;
}

/**
 * @param {{data: Object, onNavigate: (href: string) => void}} props
 */
export function GitPage({ data: account, onNavigate }) {
  const { data, error } = useQuery({
    queryKey: queryKeys.gitRepos,
    queryFn: () => xrpcGet('dev.pdsjs.git.listRepos'),
  });

  if (error) {
    return <Notice variant="error">{error.message}</Notice>;
  }
  if (!data) return null;

  const newRepo = <NewRepoDialog data={account} onNavigate={onNavigate} />;

  if (!data.repos.length) {
    return (
      <div className="flex flex-col gap-3">
        <div className="flex justify-end">{newRepo}</div>
        <EmptyState
          icon={<FolderGit2Icon />}
          title="No repositories yet"
          description={
            <>
              Your account can host git repositories. Install the helper with{' '}
              <span className="font-mono text-[12px]">
                npm install -g @pdsjs/git
              </span>{' '}
              and push to{' '}
              <span className="font-mono text-[12px]">
                atproto://your.handle/repo-name
              </span>
              . History is stored in your repo as bundles, so it rides along in
              backups and migrates with your account.
            </>
          }
        />
      </div>
    );
  }

  return (
    <div className="flex flex-col gap-3">
      <div className="flex justify-end">{newRepo}</div>
      {data.repos.map((repo) => (
        <ListRow
          key={repo.space ? `${repo.space}#${repo.name}` : repo.name}
          className="px-5 py-4"
          media={
            <RowIcon>
              <FolderGit2Icon />
            </RowIcon>
          }
          title={
            <div className="flex min-w-0 items-center gap-2">
              <span className="min-w-0 truncate text-[16px]">{repo.name}</span>
              {repo.space && (
                <Badge variant="scope" className="shrink-0">
                  <LockIcon className="size-3" /> Private
                </Badge>
              )}
              {repo.defaultBranch && (
                <Badge variant="scope" className="shrink-0">
                  {repo.defaultBranch}
                </Badge>
              )}
            </div>
          }
          description={repo.description || repoSummary(repo)}
          actions={
            repo.description ? (
              <span className="shrink-0 text-[12px] text-muted-foreground">
                {repoSummary(repo)}
              </span>
            ) : undefined
          }
          href={repoHref(repo)}
          onNavigate={onNavigate}
        />
      ))}
    </div>
  );
}