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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
110 lines · 3.4 KB Raw History
import { useQuery } from '@tanstack/react-query';
import { ImageOff } from 'lucide-react';
import { useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { Card } from '#/components/atoms/card.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { Skeleton } from '#/components/atoms/skeleton.jsx';
import { Copyable } from '#/components/molecules/copyable.jsx';
import { EmptyState } from '#/components/molecules/empty-state.jsx';
import { RepoScope } from '#/components/molecules/repo-scope.jsx';
import { fileSize } from '#/lib/format.js';
import { href } from '#/lib/navigation.jsx';
import { useSession } from '#/lib/session.jsx';
import { getBlob, listBlobs } from '#/lib/spaces.js';

/** One blob, fetched only when a reader asks to see it. */
function BlobRow({ space, repo, cid }) {
  const { session } = useSession();
  const [open, setOpen] = useState(false);

  const { data, error, isPending } = useQuery({
    queryKey: ['blob', space, repo, cid],
    queryFn: () => getBlob(session, { space, repo }, cid),
    enabled: open,
    retry: false,
  });

  return (
    <Card className="flex flex-col gap-3 p-3.5">
      <div className="flex items-center justify-between gap-3">
        <Copyable value={cid} className="text-[12px]">
          {cid.slice(0, 28)}…
        </Copyable>
        <Button variant="ghost" size="xs" onClick={() => setOpen(!open)}>
          {open ? 'Hide' : 'Open'}
        </Button>
      </div>
      {open && isPending && <Skeleton className="h-24" />}
      {open && error && <Notice variant="error">{error.message}</Notice>}
      {open && data && (
        <>
          {data.type.startsWith('image/') && (
            <img
              src={data.url}
              alt={cid}
              className="max-h-72 rounded-xl object-contain"
            />
          )}
          <a
            href={data.url}
            download={cid}
            className="text-[13px] text-key hover:underline"
          >
            {data.type || 'unknown type'} · {fileSize(data.size)}
          </a>
        </>
      )}
    </Card>
  );
}

export function BlobsPage({ space, repo }) {
  const { session } = useSession();

  const { data, error, isPending } = useQuery({
    queryKey: ['blobs', space, repo],
    queryFn: () => listBlobs(session, { space, repo }),
    retry: false,
  });

  return (
    <div className="flex flex-col gap-4">
      <div>
        <Link
          href={href('/space', { uri: space, repo })}
          className="text-[13px]"
        >
          ← Back to the space
        </Link>
        <h1 className="mt-2 font-semibold text-[16px] text-foreground">
          Blobs
        </h1>
      </div>

      <RepoScope space={space} repo={repo} />

      <p className="text-muted-foreground text-sm leading-relaxed">
        The blobs this repo's records in the space reference. Reachability is
        read from the records themselves, so a blob no record names is not
        listed and is not served.
      </p>

      {isPending && <Skeleton className="h-32" />}
      {error && <Notice variant="error">{error.message}</Notice>}

      {data?.cids.length === 0 && (
        <EmptyState
          icon={<ImageOff />}
          title="No blobs"
          description="No record in this space references one."
        />
      )}

      {data?.cids.map((cid) => (
        <BlobRow key={cid} space={space} repo={repo} cid={cid} />
      ))}
    </div>
  );
}