chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { Dialog } from '@base-ui/react/dialog';
import { useState } from 'react';
import { Button, buttonVariants } from '#/components/atoms/button.jsx';
import { Input } from '#/components/atoms/input.jsx';
import { Handle } from '#/components/molecules/handle.jsx';
import { account } from '#/lib/git.js';
import { beginSignIn } from '#/lib/oauth.js';
import { useSession } from '#/lib/session.jsx';
import { cn } from '#/lib/utils.js';
/**
* The visitor's session, in the header: a sign-in affordance, or who they
* are and the way out.
*
* The site's own account sits beside this and stays what it is, the identity
* being browsed. This is the identity doing the browsing, on whichever server
* holds it; signing in navigates to that server's consent page and back.
*
* The form asks in a dialog rather than in the header. Signing in is the one
* thing on this page that leaves it, so it is worth a moment's attention and
* room to say where the visitor is going and what fails when it does. The
* header has room for neither: a form there widened the bar as it opened and
* left an error to be read from a tooltip.
*/
export function SessionControls() {
const { session, endSession } = useSession();
const [open, setOpen] = useState(false);
const [identifier, setIdentifier] = useState('');
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
if (session) {
return (
<span className="flex items-center gap-1.5 text-[12.5px] text-faint">
<Handle who={session} className="max-w-40" />
<span aria-hidden="true">·</span>
<button
type="button"
onClick={endSession}
className="cursor-pointer text-key hover:underline"
>
sign out
</button>
</span>
);
}
const submit = async (event) => {
event.preventDefault();
setBusy(true);
setError('');
try {
await beginSignIn(identifier);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
setBusy(false);
}
};
return (
<Dialog.Root
open={open}
onOpenChange={(next) => {
setOpen(next);
// A dialog closed part way through is reopened from the start, not
// from whatever it was left saying.
if (!next) {
setError('');
setBusy(false);
}
}}
>
{/* The classes rather than the Button, the way a Link that should read
as one takes them: the trigger is base-ui's own element and wants its
own props on it. */}
<Dialog.Trigger
className={cn(
buttonVariants({ variant: 'outline', size: 'sm' }),
'cursor-pointer',
)}
>
Sign in
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Backdrop className="fixed inset-0 z-50 bg-background/70 backdrop-blur-[1px]" />
<Dialog.Popup
className={cn(
'-translate-x-1/2 -translate-y-1/2 fixed top-1/2 left-1/2 z-50',
'w-[min(26rem,calc(100vw-2rem))] rounded-2xl border border-border bg-card p-5 shadow-lg outline-none',
)}
>
<Dialog.Title className="font-semibold text-[15px]">
Sign in
</Dialog.Title>
<Dialog.Description className="mt-1.5 text-[12.5px] text-muted-foreground">
With your own atproto account, wherever it lives. Your server asks
you to consent and sends you back here.
</Dialog.Description>
<form onSubmit={submit} className="mt-4 flex flex-col gap-2.5">
<Input
// The dialog opens on this, so a visitor who came to sign in
// types without reaching for the mouse.
autoFocus
value={identifier}
onChange={(event) => setIdentifier(event.target.value)}
placeholder="your handle or DID"
autoComplete="username"
aria-label="Handle or DID"
aria-invalid={error ? true : undefined}
disabled={busy}
/>
{error && <p className="text-[12.5px] text-destructive">{error}</p>}
<div className="mt-1 flex items-center justify-end gap-2">
<Dialog.Close
className={cn(
buttonVariants({ variant: 'ghost', size: 'sm' }),
'cursor-pointer',
)}
>
Cancel
</Dialog.Close>
<Button
type="submit"
size="sm"
disabled={busy || !identifier.trim()}
>
{busy ? 'Taking you there…' : 'Continue'}
</Button>
</div>
</form>
{/* What signing in is for here, which is not obvious from a header
button on somebody else's repository. */}
<p className="mt-4 border-border border-t pt-3 text-[12px] text-faint">
Your words are written to your own repo, never to {account.handle}
's. This repository decides whose it shows.
</p>
</Dialog.Popup>
</Dialog.Portal>
</Dialog.Root>
);
}