/products (search), product pages with revisions, getting and staying on a revision, retracting; /products/:id/edit; /create and new revisions upload from the browser straight to the catalog with a token from the hub; /library; /catalogs (directory, registering with a once-shown key); /console/catalogs to approve or reject. Every page now sends a nonce-based CSP (proxy.ts), so product descriptions can't run scripts. buttonVariants now merges classes, so outline links get their border. Co-Authored-By: Claude Opus 5.5 <[email protected]>
80 lines
3.3 KiB
TypeScript
80 lines
3.3 KiB
TypeScript
"use client";
|
|
import { useActionState } from "react";
|
|
import { CopyButton } from "@/components/copy-button";
|
|
import { ErrorText, SubmitButton } from "@/components/forms";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field";
|
|
import { Input } from "@/components/ui/input";
|
|
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
|
|
import { registerCatalog } from "./actions";
|
|
|
|
export function RegisterForm() {
|
|
const [state, action] = useActionState(registerCatalog, null);
|
|
if (state?.registered) {
|
|
const { id, name, key } = state.registered;
|
|
return (
|
|
<Alert>
|
|
<AlertTitle>{name} is registered</AlertTitle>
|
|
<AlertDescription className="space-y-3">
|
|
<p>Put these in the catalog's settings. The key is shown only once. A moderator approves it next.</p>
|
|
<KeyLine label="CATALOG_ID" value={id} />
|
|
<KeyLine label="CATALOG_KEY" value={key} />
|
|
</AlertDescription>
|
|
</Alert>
|
|
);
|
|
}
|
|
return (
|
|
<form action={action}>
|
|
<FieldGroup>
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<Field>
|
|
<FieldLabel htmlFor="name">Name</FieldLabel>
|
|
<Input id="name" name="name" required maxLength={64} />
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel htmlFor="url">Address</FieldLabel>
|
|
<Input id="url" name="url" type="url" required maxLength={300} placeholder="https://" />
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel htmlFor="contact">Takedown contact</FieldLabel>
|
|
<Input id="contact" name="contact" required maxLength={200} placeholder="Email or URL" />
|
|
</Field>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<Field>
|
|
<FieldLabel htmlFor="visibility">Visibility</FieldLabel>
|
|
<NativeSelect id="visibility" name="visibility" defaultValue="public" className="w-full">
|
|
<NativeSelectOption value="public">Public</NativeSelectOption>
|
|
<NativeSelectOption value="private">Private</NativeSelectOption>
|
|
</NativeSelect>
|
|
</Field>
|
|
<Field>
|
|
<FieldLabel htmlFor="membership">Membership</FieldLabel>
|
|
<NativeSelect id="membership" name="membership" defaultValue="open" className="w-full">
|
|
<NativeSelectOption value="open">Open</NativeSelectOption>
|
|
<NativeSelectOption value="approval">By approval</NativeSelectOption>
|
|
<NativeSelectOption value="invite">Invite only</NativeSelectOption>
|
|
</NativeSelect>
|
|
</Field>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<SubmitButton pendingLabel="Registering…">Register</SubmitButton>
|
|
</div>
|
|
{state?.error && <ErrorText>{state.error}</ErrorText>}
|
|
</FieldGroup>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
function KeyLine({ label, value }: { label: string; value: string }) {
|
|
return (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="w-28 font-mono text-xs text-muted-foreground">{label}</span>
|
|
<code className="min-w-0 rounded-md border bg-muted px-2 py-1 font-mono text-xs break-all text-foreground">
|
|
{value}
|
|
</code>
|
|
<CopyButton text={value} />
|
|
</div>
|
|
);
|
|
}
|