chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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>
);
}