chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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>
);
}