Skip to content
67 linesCodeBlameRaw
1import { FOLIO_KIND_NOUNS, isFolioKind, type FolioKind } from "@g1t/contracts";
2import { Form, data, redirect, useNavigation } from "react-router";
3
4import type { Route } from "./+types/new";
5import { FOLIO_KIND_UI, KindIcon } from "../../../components/folios/kinds";
6import { ErrorText } from "../../../components/ui";
7import { page } from "../../../lib/meta";
8import { folios } from "../../../lib/services.server";
9import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server";
10
11export function meta({ params, ...args }: Route.MetaArgs) {
12 return page(args, { title: `New ${isFolioKind(params.kind) ? FOLIO_KIND_NOUNS[params.kind] : "artifact"} · ${params.owner} · g1t` });
13}
14
15/** The kinds that can be made now; the rest aren't found until their phase ships. */
16function kindOf(value: string | undefined): FolioKind {
17 if (!isFolioKind(value) || !FOLIO_KIND_UI[value].ready) throw data(null, { status: 404 });
18 return value;
19}
20
21export async function loader({ params, context, request }: Route.LoaderArgs) {
22 const viewer = requireUser(context, request);
23 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
24 return { kind: kindOf(params.kind) };
25}
26
27/**
28 * Makes an artifact, then goes to it: `POST -/artifacts/new/<kind>` with
29 * optional `space`, `parent` and `template` (form fields, or the
30 * address's search). Only a post makes one, so a prefetched or repeated
31 * link never does; the form's button is busy until it lands.
32 */
33export async function action({ params, context, request }: Route.ActionArgs) {
34 assertSameOrigin(request);
35 const viewer = requireUser(context, request);
36 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
37 const kind = kindOf(params.kind);
38 const form = await request.formData().catch(() => new FormData());
39 const search = new URL(request.url).searchParams;
40 const field = (name: string) => String(form.get(name) ?? search.get(name) ?? "").trim() || null;
41 const made = await folios.create(params.owner.toLowerCase(), viewer, { kind, space_id: field("space"), parent_id: field("parent"), template_id: field("template") });
42 if (!made.ok) return { error: made.error.message };
43 throw redirect(made.value.path);
44}
45
46/** Opened as a link (from outside a page that posts): one button, so nothing is made by just visiting. */
47export default function NewFolio({ loaderData, actionData }: Route.ComponentProps) {
48 const { kind } = loaderData;
49 const busy = useNavigation().state !== "idle";
50 return (
51 <div className="mx-auto mt-10 flex max-w-md flex-col items-center text-center">
52 <KindIcon kind={kind} size={22} box={48} />
53 <h1 className="mt-4 text-xl font-semibold tracking-tight">New {FOLIO_KIND_NOUNS[kind]}</h1>
54 <p className="mt-1 text-sm text-muted">It starts in your Private, where only you can see it, unless you chose a space.</p>
55 {actionData?.error && (
56 <div className="mt-4">
57 <ErrorText>{actionData.error}</ErrorText>
58 </div>
59 )}
60 <Form method="post" className="mt-6">
61 <button type="submit" disabled={busy} className="inline-flex h-9 items-center rounded-md bg-accent px-4 text-sm font-medium text-bg hover:bg-accent-hover disabled:opacity-60">
62 {busy ? "Starting…" : `Start the ${FOLIO_KIND_NOUNS[kind]}`}
63 </button>
64 </Form>
65 </div>
66 );
67}