~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
143 lines · 5.1 KB Raw History
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>
  );
}