chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { parseSpaceUri } from '@pdsjs/spaces/uri';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { ArrowRight, Boxes, Plus } from 'lucide-react';
import { useState } from 'react';
import { AppTile } from '#/components/atoms/app-tile.jsx';
import { Badge } from '#/components/atoms/badge.jsx';
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 { ActorAvatar } from '#/components/molecules/actor-avatar.jsx';
import { AtUri } from '#/components/molecules/at-uri.jsx';
import { EmptyState } from '#/components/molecules/empty-state.jsx';
import { Identity } from '#/components/molecules/identity.jsx';
import { TextField } from '#/components/molecules/text-field.jsx';
import { spaceParts } from '#/lib/format.js';
import { href, useNavigate } from '#/lib/navigation.jsx';
import { useSession } from '#/lib/session.jsx';
import { createSpace, listSpaces } from '#/lib/spaces.js';
/** One row in the list: what the space is, and who governs it. */
function SpaceRow({ uri, isOwner, did }) {
const { spaceDid, spaceType, skey } = spaceParts(uri);
return (
<Card className="flex gap-3.5 p-4">
{/* The space type is an NSID, and the domain it reverses to is the app
the space belongs to. Its icon says at a glance which app a space is
for, which the type alone takes a moment to read. */}
<AppTile nsid={spaceType} size={36} />
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-baseline justify-between gap-2">
<Link
href={href('/space', { uri })}
className="font-medium font-mono text-[13px] no-underline hover:underline"
>
{spaceType}
</Link>
{isOwner && <Badge variant="scope">you host this</Badge>}
</div>
<div className="mt-1 text-muted-foreground text-xs">{skey}</div>
<div className="mt-2 flex min-w-0 items-center gap-1.5 text-faint text-xs">
{spaceDid === did ? (
'Under your own authority'
) : (
<>
Under
<ActorAvatar did={spaceDid} size={16} />
<Identity did={spaceDid} inline />
</>
)}
</div>
<AtUri uri={uri} className="mt-2 text-[11px]" />
</div>
</Card>
);
}
/**
* The way into a space this list does not name.
*
* `listSpaces` reports the spaces this account has written to, which is what a
* PDS tracks. A space somebody has admitted this account to but which it has
* never written in appears nowhere, and its URI is the only way to reach it.
*/
function OpenByUri() {
const navigate = useNavigate();
const [uri, setUri] = useState('');
const [error, setError] = useState(null);
function open(event) {
event.preventDefault();
const trimmed = uri.trim();
try {
parseSpaceUri(trimmed);
} catch (failure) {
setError(failure.message);
return;
}
navigate(href('/space', { uri: trimmed }));
}
return (
<form onSubmit={open} className="flex flex-col gap-2">
<div className="flex gap-2">
<input
value={uri}
onChange={(event) => {
setUri(event.target.value);
setError(null);
}}
placeholder="at://did:plc:…/space/com.example.forum/general"
spellCheck="false"
autoCapitalize="none"
autoCorrect="off"
aria-label="Open a space by URI"
className="h-9 min-w-0 flex-1 rounded-lg border border-input bg-transparent px-3 font-mono text-[12.5px] text-foreground outline-none placeholder:text-faint focus-visible:border-ring"
/>
<Button
size="sm"
type="submit"
variant="outline"
disabled={!uri.trim()}
>
<ArrowRight />
Open
</Button>
</div>
{error && <Notice variant="error">{error}</Notice>}
</form>
);
}
/** The form for a space anchored on this account. */
function NewSpace({ onCreated }) {
const { session } = useSession();
const [open, setOpen] = useState(false);
const [type, setType] = useState('');
const [skey, setSkey] = useState('');
const create = useMutation({
mutationFn: () => createSpace(session, { type, skey: skey || undefined }),
onSuccess: () => {
setOpen(false);
setType('');
setSkey('');
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 space
</span>
</Button>
);
}
return (
<Card className="flex flex-col gap-3 p-4">
<TextField
name="type"
label="Space type"
placeholder="com.example.forum"
hint="An NSID. It names the data model, and every space of one type shares it."
value={type}
onChange={(event) => setType(event.target.value)}
/>
<TextField
name="skey"
label="Key"
placeholder="left blank for a timestamp"
value={skey}
onChange={(event) => setSkey(event.target.value)}
/>
{create.error && <Notice variant="error">{create.error.message}</Notice>}
<div className="flex gap-2">
<Button
onClick={() => create.mutate()}
disabled={!type.includes('.') || create.isPending}
>
{create.isPending ? 'Creating…' : 'Create'}
</Button>
<Button variant="ghost" onClick={() => setOpen(false)}>
Cancel
</Button>
</div>
</Card>
);
}
export function SpacesPage() {
const { session, writable } = useSession();
const client = useQueryClient();
const { data, error, isPending } = useQuery({
queryKey: ['spaces', session.did],
queryFn: () => listSpaces(session),
});
if (isPending) {
return (
<div className="flex flex-col gap-3">
{[0, 1, 2].map((row) => (
<Skeleton key={row} className="h-24" />
))}
</div>
);
}
if (error) {
return (
<Notice variant="error">
{error.name === 'MethodNotImplemented'
? 'This PDS does not serve permissioned spaces.'
: error.message}
</Notice>
);
}
const spaces = data.spaces || [];
return (
<div className="flex flex-col gap-3">
<p className="text-muted-foreground text-sm leading-relaxed">
The spaces your repo holds data in. A PDS tracks the spaces its own user
has written to, so a space you have been admitted to but never written
in is not listed here.
</p>
<OpenByUri />
{spaces.length === 0 ? (
<EmptyState
icon={<Boxes />}
title="No spaces"
description="This account has written nothing into a permissioned space. Open one above by its URI."
/>
) : (
spaces.map((space) => (
<SpaceRow key={space.uri} {...space} did={session.did} />
))
)}
{writable && (
<NewSpace
onCreated={() => client.invalidateQueries({ queryKey: ['spaces'] })}
/>
)}
</div>
);
}