chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { useEffect, useRef, useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { Link } from '#/components/atoms/link.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { Skeleton } from '#/components/atoms/skeleton.jsx';
import { NavigationProvider, useLocation } from '#/lib/navigation.jsx';
import { completeSignIn } from '#/lib/oauth.js';
import { useSession } from '#/lib/session.jsx';
import { BlobsPage } from '#/pages/blobs.jsx';
import { CredentialPage } from '#/pages/credential.jsx';
import { OpsPage } from '#/pages/ops.jsx';
import { RecordPage } from '#/pages/record.jsx';
import { RecordsPage } from '#/pages/records.jsx';
import { SignInPage } from '#/pages/sign-in.jsx';
import { SpacePage } from '#/pages/space.jsx';
import { SpacesPage } from '#/pages/spaces.jsx';
/**
* Where the authorization server sends the browser back. The code is spent
* once, so the address bar is cleaned before anything else renders: a reload of
* this URL would otherwise present a code the server has already refused.
*/
function CallbackPage({ navigate }) {
const { setSession } = useSession();
const [error, setError] = useState(null);
// The exchange consumes the pending sign-in the moment it starts, so it
// must run once however many times the effect fires. StrictMode fires it
// twice in development, and a second run would find nothing pending and
// report a failure over a sign-in that succeeded. The ref survives the
// simulated remount; a cleanup-cancelled flag would not, and would also
// cancel the first run's redirect.
const ran = useRef(false);
useEffect(() => {
if (ran.current) return;
ran.current = true;
// Read from the address bar rather than from a prop: the code is spent once
// and this effect must not re-run, so it depends on nothing a render makes
// anew.
completeSignIn(new URLSearchParams(window.location.search))
.then(({ session }) => {
setSession(session);
navigate('/');
})
.catch((err) => setError(err.message));
}, [navigate, setSession]);
if (error) {
return (
<div className="mx-auto max-w-md pt-16">
<Notice variant="error">{error}</Notice>
<Link href="/" className="mt-4 inline-block text-[13px]">
Start again
</Link>
</div>
);
}
return <Skeleton className="mx-auto mt-16 h-24 max-w-md" />;
}
/**
* Pick the screen for a path, with the space it acts on read from the query.
*
* Each screen is keyed by what it shows. Both editors here hold a draft in
* state, and moving to another record on the same screen would otherwise open
* it with the last one's text still in the box.
*/
function Screen({ path, params }) {
const space = params.get('uri');
const repo = params.get('repo');
const collection = params.get('collection');
const rkey = params.get('rkey');
if (!space) return <SpacesPage />;
if (path === '/space') {
return <SpacePage key={`${space}/${repo}`} space={space} repo={repo} />;
}
if (path === '/ops') return <OpsPage space={space} repo={repo} />;
if (path === '/credential') return <CredentialPage space={space} />;
if (path === '/blobs') return <BlobsPage space={space} repo={repo} />;
if (path === '/records') {
return (
<RecordsPage
key={`${space}/${repo}/${collection}`}
space={space}
collection={collection}
repo={repo}
/>
);
}
if (path === '/record') {
return (
<RecordPage
key={`${space}/${repo}/${collection}/${rkey}`}
space={space}
collection={collection}
rkey={rkey}
repo={repo}
/>
);
}
return <SpacesPage />;
}
export function App() {
const { path, params, navigate } = useLocation();
const { session, endSession, writable } = useSession();
return (
<NavigationProvider navigate={navigate}>
<div className="mx-auto max-w-3xl px-5 pb-24">
{path === '/callback' ? (
<CallbackPage navigate={navigate} />
) : !session ? (
<SignInPage />
) : (
<>
<header className="flex flex-wrap items-baseline justify-between gap-3 pt-8 pb-6">
<Link
href="/"
className="font-semibold text-[22px] text-foreground tracking-tight no-underline hover:no-underline"
>
Spaces
</Link>
<span className="flex items-baseline gap-3">
<span className="text-muted-foreground text-[13px]">
{session.handle}
</span>
{!writable && (
<span className="text-faint text-xs">read only</span>
)}
<Button
variant="ghost"
size="xs"
onClick={() => {
endSession();
navigate('/');
}}
>
Sign out
</Button>
</span>
</header>
<Screen path={path} params={params} />
</>
)}
</div>
</NavigationProvider>
);
}