app/routes/project.new.tsx
72 lines and 5 functions in DrDustinEdwards/carrel, at commit d5d5e82 on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| newSourceFor | 21 to 25 | 2 | 0% | WatchCRAP 6 | No data: mutation testing has not run on this repo yet |
| meta | 30 to 32 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| loader | 34 to 39 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| action | 41 to 56 | 5 | 0% | WatchCRAP 30 | No data: mutation testing has not run on this repo yet |
| NewPost | 58 to 82 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
Imports
It imports app/components/frame.tsx, app/lib/content.server.ts, app/lib/context.ts, app/lib/projects.server.ts, app/lib/sites.server.ts. No module of this repo imports it.
Source
// A new post starts as a Carrel draft under a slug the site does not hold yet. Nothing reaches the
// site until the first Save, which sends it with expectedVersion null: the site refuses it if the
// slug was taken in the meantime.
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 { autosave, readDoc } from "~/lib/content.server";
import { getEnv, getViewer } from "~/lib/context";
import { requireSiteProject } from "~/lib/projects.server";
import { siteEntry, type SiteId } from "~/lib/sites.server";
import type { Route } from "./+types/project.new";
/** A site with a fixed set of items refuses a new one, so Carrel has no New post for it. */
function newSourceFor(site: SiteId) {
const newSource = siteEntry(site).newSource;
if (!newSource) throw new Response("This site has a fixed set of items; Carrel cannot add one.", { status: 404 });
return newSource;
}
/** Stricter than the contract's id: lower case, digits and single hyphens, as the site's slugs are. */
const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
export function meta() {
return [{ title: "New post ยท Carrel" }];
}
export async function loader({ params, context }: Route.LoaderArgs) {
const env = getEnv(context);
const project = await requireSiteProject(env.DB, getViewer(context), params.project, "edit");
newSourceFor(project.site);
return { project: { slug: project.slug, name: project.name } };
}
export async function action({ params, request, context }: Route.ActionArgs) {
const env = getEnv(context);
const viewer = getViewer(context);
const project = await requireSiteProject(env.DB, viewer, params.project, "edit");
const newSource = newSourceFor(project.site);
const slug = String((await request.formData()).get("slug") ?? "").trim();
if (!SLUG.test(slug) || slug.length > 120) {
return { error: "Use lower-case letters, digits and single hyphens, such as my-new-post." };
}
if (await readDoc(env, project, slug)) {
return { error: `The site already has a post at ${slug}. Open it from the list instead.` };
}
const today = new Date().toISOString().slice(0, 10);
await autosave(env.DB, project, viewer, slug, { source: newSource(slug, today), baseVersion: null });
return redirect(`/p/${project.slug}/e/${slug}`);
}
export default function NewPost({ loaderData, actionData }: Route.ComponentProps) {
const busy = useNavigation().state !== "idle";
return (
<div className="app-page" data-narrow>
<PageHead
renderLink={RouterLink}
crumbs={[{ label: loaderData.project.name, href: `/p/${loaderData.project.slug}` }, { label: "New post" }]}
title="New post"
lead="A new post starts as a draft in Carrel, under the address you give it."
/>
<Panel title="Address">
<Form method="post" className="app-form">
<Field label="Slug, the post's address on the site" required help="Lower-case letters, digits and single hyphens, such as my-new-post. The site gives it its address when it is published. It stays a draft in Carrel until you save it to the site." error={actionData?.error} announce>
<input className="cap-input" name="slug" 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 my-new-post." />
</Field>
<div className="app-actions">
<Button type="submit" variant="primary" pending={busy}>
Start writing
</Button>
</div>
</Form>
</Panel>
</div>
);
}