chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { Plus } 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 { JsonView } from '#/components/molecules/json-view.jsx';
import { RepoScope } from '#/components/molecules/repo-scope.jsx';
import { ago, rkeyTime } from '#/lib/format.js';
import { href } from '#/lib/navigation.jsx';
import { useSession } from '#/lib/session.jsx';
import { createRecord, listRecords } from '#/lib/spaces.js';
/** The starting document for a new record, so a writer edits rather than types. */
function template(collection) {
return JSON.stringify(
{ $type: collection, createdAt: new Date().toISOString() },
null,
2,
);
}
function NewRecord({ space, collection, onCreated }) {
const { session } = useSession();
const [open, setOpen] = useState(false);
const [text, setText] = useState(() => template(collection));
const [rkey, setRkey] = useState('');
const create = useMutation({
mutationFn: () =>
createRecord(session, {
space,
collection,
rkey: rkey || undefined,
record: JSON.parse(text),
}),
onSuccess: () => {
setOpen(false);
setText(template(collection));
setRkey('');
onCreated();
},
});
if (!open) {
return (
<Button variant="dashed" size="surface" onClick={() => setOpen(true)}>
<span className="flex items-center justify-center gap-2 py-3 text-sm">
<Plus className="size-4" />
New record
</span>
</Button>
);
}
return (
<Card className="flex flex-col gap-3 p-4">
<input
value={rkey}
onChange={(event) => setRkey(event.target.value)}
placeholder="record key, left blank for a timestamp"
className="h-9 rounded-lg border border-input bg-transparent px-3 font-mono text-[12.5px] text-foreground outline-none focus-visible:border-ring"
/>
<textarea
value={text}
onChange={(event) => setText(event.target.value)}
spellCheck="false"
rows={12}
className="rounded-lg border border-input bg-transparent p-3 font-mono text-[12.5px] text-foreground leading-[1.6] outline-none focus-visible:border-ring"
/>
{create.error && <Notice variant="error">{create.error.message}</Notice>}
<div className="flex gap-2">
<Button onClick={() => create.mutate()} disabled={create.isPending}>
{create.isPending ? 'Writing…' : 'Create'}
</Button>
<Button variant="ghost" onClick={() => setOpen(false)}>
Cancel
</Button>
</div>
</Card>
);
}
export function RecordsPage({ space, collection, repo }) {
const { session, writable } = useSession();
const client = useQueryClient();
const own = !repo || repo === session.did;
const { data, error, isPending } = useQuery({
queryKey: ['records', space, repo, collection],
queryFn: () =>
listRecords(session, { space, repo }, { collection, reverse: true }),
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 font-mono text-[16px] text-foreground">
{collection}
</h1>
</div>
<RepoScope space={space} repo={repo} />
{isPending && <Skeleton className="h-40" />}
{error && <Notice variant="error">{error.message}</Notice>}
{data?.records.map((record) => {
const written = rkeyTime(record.rkey);
return (
<Card key={record.rkey} className="p-4">
<div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
<Link
href={href('/record', {
uri: space,
collection,
rkey: record.rkey,
repo,
})}
className="font-mono text-[13px]"
>
{record.rkey}
</Link>
{written && (
<span className="text-faint text-xs">{ago(written)}</span>
)}
</div>
{record.value && <JsonView value={record.value} className="mt-3" />}
</Card>
);
})}
{data?.cursor && (
<Notice>
Showing the first {data.records.length} records. Older ones are behind
a cursor this page does not follow yet.
</Notice>
)}
{writable && own && (
<NewRecord
space={space}
collection={collection}
onCreated={() =>
client.invalidateQueries({ queryKey: ['records', space] })
}
/>
)}
</div>
);
}