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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
159 lines · 4.7 KB Raw History
import { useInfiniteQuery } from '@tanstack/react-query';
import { ArrowLeftIcon, FileQuestionIcon } from 'lucide-react';
import { Skeleton } from '#/components/atoms/skeleton.jsx';
import { EmptyState } from '#/components/molecules/empty-state.jsx';
import { ErrorNotice } from '#/components/molecules/error-notice.jsx';
import { RecordCard } from '#/components/molecules/record-card.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { get } from '#/lib/api.js';
import { dateTime } from '#/lib/format.js';
import { queryKeys } from '#/lib/query.js';

function buildQuery({ collection, space, rkey, seq }) {
  const base = `/records?collection=${encodeURIComponent(collection)}`;
  if (space) return `${base}&space=${encodeURIComponent(space)}`;
  if (rkey)
    return `${base}&rkey=${encodeURIComponent(rkey)}${
      seq ? `&seq=${encodeURIComponent(seq)}` : ''
    }`;
  return base;
}

/**
 * A History link opens a record as it stood at that change. This banner says so
 * and, when the live record still exists, bridges to it.
 */
function SnapshotBanner({ snapshot, currentHref, onNavigate }) {
  const when = snapshot.time ? dateTime(snapshot.time) : 'this change';
  const removed = snapshot.action === 'delete';
  const showBridge = !removed && snapshot.liveExists && !snapshot.liveCurrent;

  return (
    <div className="mb-4 rounded-2xl border border-border bg-muted px-4 py-3.5">
      <div className="text-[13px] text-foreground">
        {removed ? `Removed ${when}` : `Version from ${when}`}
      </div>
      <div className="mt-1 text-[12px] text-muted-foreground leading-relaxed">
        {removed
          ? 'This is the record as it was just before it was removed.'
          : snapshot.liveCurrent
            ? 'This is still the current version.'
            : snapshot.liveExists
              ? 'The record has changed since.'
              : 'This record has since been removed.'}
      </div>
      {showBridge && (
        <TextLink
          href={currentHref}
          onNavigate={onNavigate}
          className="mt-2 text-[12.5px]"
        >
          View current record &rarr;
        </TextLink>
      )}
    </div>
  );
}

export function RecordsPage({
  collection,
  space,
  rkey,
  seq,
  repoDid,
  recordHref,
  onNavigate,
  onBack,
}) {
  const query = buildQuery({ collection, space, rkey, seq });

  const {
    data,
    error,
    isPending,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: queryKeys.records({ collection, space, rkey, seq }),
    queryFn: ({ pageParam }) =>
      get(
        pageParam ? `${query}&cursor=${encodeURIComponent(pageParam)}` : query,
      ),
    initialPageParam: null,
    getNextPageParam: (last) => last.cursor ?? undefined,
  });

  const pages = data?.pages ?? [];
  const records = pages.flatMap((page) => page.records);
  const blobBase = pages[0]?.blobBase || null;
  const snapshot = pages[0]?.snapshot || null;

  return (
    <div>
      <TextLink onClick={onBack} className="mb-4 gap-1.5 text-sm">
        <ArrowLeftIcon className="size-4" />
        {space ? 'Spaces' : 'Back'}
      </TextLink>

      {snapshot && (
        <SnapshotBanner
          snapshot={snapshot}
          currentHref={recordHref(collection, rkey)}
          onNavigate={onNavigate}
        />
      )}

      <ErrorNotice error={error?.message} />

      {isPending && (
        <div className="space-y-3">
          {['a', 'b', 'c'].map((key) => (
            <Skeleton key={key} className="h-28 rounded-2xl" />
          ))}
        </div>
      )}

      {!isPending &&
        records.length === 0 &&
        (snapshot ? (
          <EmptyState
            icon={<FileQuestionIcon />}
            title="Content unavailable"
            description="The content of this change is no longer stored."
          />
        ) : (
          <EmptyState
            icon={<FileQuestionIcon />}
            title={rkey ? 'Record not found' : 'Nothing stored here'}
            description={
              rkey
                ? 'This record may have been deleted, or it never existed at this address.'
                : 'This collection holds no records yet.'
            }
          />
        ))}

      {records.map((record) => (
        <RecordCard
          key={record.uri}
          record={record}
          blobBase={blobBase}
          repoDid={repoDid}
          recordHref={recordHref}
          onNavigate={onNavigate}
        />
      ))}

      {hasNextPage && (
        <TextLink
          className="mt-1"
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading…' : 'Load more'}
        </TextLink>
      )}
    </div>
  );
}