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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
128 lines · 4.5 KB Raw History
import { useQueryClient } from '@tanstack/react-query';
import { useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { Input } from '#/components/atoms/input.jsx';
import { Label } from '#/components/atoms/label.jsx';
import { MarkdownField } from '#/components/molecules/markdown-field.jsx';
import { PageHead } from '#/components/molecules/page-head.jsx';
import { PageNotice } from '#/components/molecules/page-notice.jsx';
import { Screen } from '#/components/molecules/screen.jsx';
import { repoRecord } from '#/lib/git.js';
import { rememberFiledIssue } from '#/lib/issues.js';
import { useNavigate } from '#/lib/navigation.jsx';
import { publishIssue } from '#/lib/publish.js';
import { useSession } from '#/lib/session.jsx';

/**
 * Filing an issue, on a page of its own.
 *
 * An issue is somebody's account of what went wrong, and it is markdown
 * wherever it is shown, so this gives it the room to be written and the
 * preview to be checked before it is published under their own name.
 */
export function NewIssuePage({ repo, space }) {
  const record = repoRecord(repo, space);
  const { session } = useSession();
  const queryClient = useQueryClient();
  const navigate = useNavigate();
  const [title, setTitle] = useState('');
  const [body, setBody] = useState('');
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState('');

  const at = `/${encodeURIComponent(repo)}/issues`;

  if (!record) {
    return <PageNotice>No such repository.</PageNotice>;
  }

  const file = async (event) => {
    event.preventDefault();
    setBusy(true);
    setError('');
    try {
      const written = await publishIssue(session, {
        repo,
        title: title.trim(),
        body: body.trim(),
      });
      // The filer sees their own issue on the list at once, before the
      // server's read finds it: kept until that read lists the same uri.
      const mine = {
        uri: written.uri,
        cid: written.cid,
        by: { did: session.did, handle: session.handle },
        title: title.trim(),
        body: body.trim(),
        createdAt: new Date().toISOString(),
      };
      rememberFiledIssue(repo, mine);
      queryClient.setQueryData(
        ['issues', repo, space ?? ''],
        (/** @type {Array<Record<string, *>>|undefined} */ old) =>
          old ? [mine, ...old] : old,
      );
      queryClient.invalidateQueries({
        queryKey: ['issues', repo, space ?? ''],
      });
      navigate(at);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
      setBusy(false);
    }
  };

  return (
    <Screen
      bar={
        <PageHead rule title="New issue" back={{ href: at, label: 'Issues' }} />
      }
    >
      <div className="max-w-3xl px-(--gutter) py-5">
        {!session ? (
          <p className="text-[13.5px] text-muted-foreground">
            Sign in to file one. The record goes into your own repo, so filing
            needs an account rather than permission from this one.
          </p>
        ) : (
          <form onSubmit={file} className="flex flex-col gap-4">
            <div>
              <Label htmlFor="issue-title">Title</Label>
              <Input
                id="issue-title"
                value={title}
                onChange={(event) => setTitle(event.target.value)}
                placeholder="The issue in one line"
                maxLength={300}
                autoFocus
                className="h-10 text-[14px]"
                disabled={busy}
              />
            </div>
            <div>
              <Label htmlFor="issue-body">Description</Label>
              <div className="overflow-hidden rounded-lg border border-input bg-background focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/50">
                <MarkdownField
                  value={body}
                  onChange={setBody}
                  placeholder="What happened, and what you expected. Markdown, so a stack trace goes in a fenced block."
                  label="Description"
                  rows={12}
                  disabled={busy}
                />
              </div>
            </div>
            <div className="flex flex-wrap items-center gap-3">
              <Button type="submit" disabled={busy || !title.trim()}>
                {busy ? 'Filing…' : 'File issue'}
              </Button>
              {error && (
                <span className="text-[12.5px] text-destructive">{error}</span>
              )}
            </div>
          </form>
        )}
      </div>
    </Screen>
  );
}