app/routes/book.new.tsx
55 lines and 4 functions in DrDustinEdwards/carrel, at commit d5d5e82 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| meta | 17 to 19 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| loader | 21 to 24 | 2 | 67% | OkCRAP 2.2 | No data: mutation testing has not run on this repo yet |
| action | 26 to 33 | 4 | 0% | WatchCRAP 20 | No data: mutation testing has not run on this repo yet |
| NewBook | 35 to 63 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
Imports
It imports app/components/frame.tsx, app/lib/books.server.ts, app/lib/context.ts. No module of this repo imports it.
Source
// A new book is a project whose folder in the writing repository is named here. Nothing is written to
// Git until the first file is saved; the folder appears then.
import { Form, redirect, useNavigation } from "react-router";
import { Button } from "capsomer/react/button";
import { Field } from "capsomer/react/field";
import { Panel } from "capsomer/react/panel";
import { PageHead } from "capsomer/react/page-head";
import { RouterLink } from "~/components/frame";
import { createBook } from "~/lib/books.server";
import { getEnv, getViewer } from "~/lib/context";
import type { Route } from "./+types/book.new";
export function meta() {
return [{ title: "New book ยท Carrel" }];
}
export async function loader({ context }: Route.LoaderArgs) {
if (!getViewer(context).isOwner) throw new Response("Not found", { status: 404 });
return null;
}
export async function action({ request, context }: Route.ActionArgs) {
const form = await request.formData();
const result = await createBook(getEnv(context).DB, getViewer(context), {
name: String(form.get("name") ?? ""),
folder: String(form.get("folder") ?? "").trim(),
});
return result.ok ? redirect(`/b/${result.slug}`) : { error: result.error };
}
export default function NewBook({ actionData }: Route.ComponentProps) {
const busy = useNavigation().state !== "idle";
return (
<div className="app-page" data-narrow>
<PageHead renderLink={RouterLink} crumbs={[{ label: "Home", href: "/" }, { label: "New book" }]} title="New book" lead="A book is a folder in the writing repository. Nothing is written to Git until its first file is saved." />
<Panel title="The book">
<Form method="post" className="app-form">
<Field label="Name" required>
<input className="cap-input" name="name" required maxLength={120} autoComplete="off" />
</Field>
<Field
label="Folder in the writing repository"
required
help="Lower-case letters, digits and single hyphens, such as paluxy-portal. An existing folder is picked up at the first refresh."
error={actionData?.error}
announce
>
<input className="cap-input" name="folder" required pattern="[a-z0-9]+(-[a-z0-9]+)*" autoComplete="off" spellCheck={false} data-error-pattern-mismatch="Use lower-case letters, digits and single hyphens, such as paluxy-portal." />
</Field>
<div className="app-actions">
<Button type="submit" variant="primary" pending={busy}>
Create the book
</Button>
</div>
</Form>
</Panel>
</div>
);
}