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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
122 lines · 3.8 KB Raw History
import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query';
import { FileIcon } 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 { FileCard } from '#/components/molecules/file-card.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { FilesHeader } from '#/components/organisms/files-header.jsx';
import { ImageGrid } from '#/components/organisms/image-grid.jsx';
import { get } from '#/lib/api.js';
import { queryKeys } from '#/lib/query.js';
import { cn } from '#/lib/utils.js';

function buildQuery(filters) {
  const query = new URLSearchParams();
  if (filters.kind) query.set('kind', filters.kind);
  if (filters.app) query.set('app', filters.app);
  if (filters.unused) query.set('unused', '1');
  if (filters.sort === 'largest') query.set('sort', 'largest');
  const qs = query.toString();
  return qs ? `/blobs?${qs}` : '/blobs';
}

export function FilesPage({ filters, filesHref, recordHref, onNavigate }) {
  const query = buildQuery(filters);
  const {
    data,
    error,
    isPending,
    hasNextPage,
    fetchNextPage,
    isFetchingNextPage,
    isPlaceholderData,
  } = useInfiniteQuery({
    queryKey: queryKeys.blobs(filters),
    queryFn: ({ pageParam }) =>
      get(
        pageParam
          ? `${query}${query.includes('?') ? '&' : '?'}cursor=${encodeURIComponent(pageParam)}`
          : query,
      ),
    initialPageParam: null,
    getNextPageParam: (last) => last.cursor ?? undefined,
    // Keep the current list (and header) in place while a filter change loads.
    placeholderData: keepPreviousData,
  });

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

  return (
    <div>
      <ErrorNotice error={error?.message} />

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

      {!isPending && !error && facets && facets.total.count === 0 && (
        <EmptyState
          icon={<FileIcon />}
          title="No files yet"
          description="When apps upload files, like the images in your posts, they will appear here."
        />
      )}

      {facets && facets.total.count > 0 && (
        <FilesHeader
          facets={facets}
          filters={filters}
          hrefFor={filesHref}
          onNavigate={onNavigate}
        />
      )}

      <div className={cn(isPlaceholderData && 'opacity-60')}>
        {!isPending && facets && facets.total.count > 0 && !blobs.length && (
          <EmptyState
            icon={<FileIcon />}
            title="No matching files"
            description="Nothing matches these filters. Clear one to widen the view."
          />
        )}

        {filters.kind === 'image' && blobBase ? (
          <ImageGrid
            blobs={blobs}
            blobBase={blobBase}
            recordHref={recordHref}
            onNavigate={onNavigate}
          />
        ) : (
          blobs.map((blob) => (
            <FileCard
              key={blob.cid}
              blob={blob}
              blobBase={blobBase}
              recordHref={recordHref}
              onNavigate={onNavigate}
            />
          ))
        )}
      </div>

      {hasNextPage && (
        <TextLink
          className={cn('mt-1', filters.kind === 'image' && 'mt-3')}
          onClick={() => fetchNextPage()}
          disabled={isFetchingNextPage}
        >
          {isFetchingNextPage ? 'Loading…' : 'Load more'}
        </TextLink>
      )}
    </div>
  );
}