app/routes/home.tsx
117 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 |
|---|---|---|---|---|---|
| loader | 16 to 25 | 3 | 100% | OkCRAP 3 | No data: mutation testing has not run on this repo yet |
| Home | 57 to 127 | 10 | 0% | CriticalCRAP 110 | No data: mutation testing has not run on this repo yet |
| link | 59 to 59 | 1 | 0% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| <anonymous:84> | 84 to 95 | 5 | 0% | WatchCRAP 30 | No data: mutation testing has not run on this repo yet |
Imports
It imports app/lib/context.ts, app/lib/google/oauth.server.ts, app/lib/people.server.ts. No module of this repo imports it.
Source
import type { ReactNode } from "react";
import { Link } from "react-router";
import { Banner } from "capsomer/react/banner";
import { Empty } from "capsomer/react/empty";
import { Panel } from "capsomer/react/panel";
import { Row, RowList } from "capsomer/react/row-list";
import { Pill, Status } from "capsomer/react/status";
import { PageHead } from "capsomer/react/page-head";
import { getEnv, getViewer } from "~/lib/context";
import { isConnected, oauthConnection } from "~/lib/google/oauth.server";
import { visibleProjects } from "~/lib/people.server";
import type { Route } from "./+types/home";
export async function loader({ request, context }: Route.LoaderArgs) {
const env = getEnv(context);
const viewer = getViewer(context);
const projects = await visibleProjects(env.DB, viewer);
// Google is the Owner's: the manuscripts and his drive.file grant.
const google = viewer.isOwner
? { configured: (await oauthConnection(env)).state === "configured", connected: await isConnected(env.DB, viewer) }
: null;
return { name: viewer.name || viewer.email, isOwner: viewer.isOwner, projects, google, googleMessage: new URL(request.url).searchParams.get("google") };
}
const KIND = { site: "Site", book: "Book" } as const;
const ROLE = { owner: "Owner", editor: "Editor", reader: "Reader" } as const;
const SiteGlyph = (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M8 2a6 6 0 1 0 0 12A6 6 0 0 0 8 2zM2 8h12M8 2c2 1.8 2 10.2 0 12M8 2c-2 1.8-2 10.2 0 12" />
</svg>
);
const DocGlyph = (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M4 2.5h5l3 3v8H4zM9 2.5v3h3M6 8.5h4M6 11h4" />
</svg>
);
const PostGlyph = (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M2.5 9.5V6.5l8-3.5v10l-8-3.5zM5 10.5l.8 3h2" />
</svg>
);
const LinkGlyph = (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M6.5 9.5a3 3 0 0 0 4 0l2-2a3 3 0 0 0-4-4l-.7.7M9.5 6.5a3 3 0 0 0-4 0l-2 2a3 3 0 0 0 4 4l.7-.7" />
</svg>
);
const BookGlyph = (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" d="M2.5 3.5c2-.8 4-.6 5.5.6 1.5-1.2 3.5-1.4 5.5-.6v9c-2-.8-4-.6-5.5.6-1.5-1.2-3.5-1.4-5.5-.6zM8 4.1v9" />
</svg>
);
export default function Home({ loaderData }: Route.ComponentProps) {
const { isOwner, projects, google, googleMessage } = loaderData;
const link = ({ href, children }: { href: string; children: ReactNode }) => <Link to={href}>{children}</Link>;
return (
<div className="app-page">
<PageHead
title="Carrel"
lead="Your sites and your books, in one place."
actions={
isOwner ? (
<Link to="/books/new" className="cap-btn" data-variant="primary">
New book
</Link>
) : null
}
/>
{googleMessage ? <Banner tone="info">{googleMessage}</Banner> : null}
<div className="app-split" data-aside={isOwner ? "" : undefined} data-even>
<Panel title="Projects" count={projects.length} flush>
{projects.length === 0 ? (
<Empty kind="nothing-yet" flush title="No projects yet">
A project appears here once it is shared with you.
</Empty>
) : (
<RowList label="Projects">
{projects.map((p) => (
<Row
key={p.id}
title={p.name}
href={p.kind ? `/${p.kind === "book" ? "b" : "p"}/${p.slug}` : undefined}
renderLink={link}
media={p.kind === "book" ? BookGlyph : SiteGlyph}
mediaVariant="icon"
detail={p.kind ? KIND[p.kind] : "Not set up for writing yet"}
meta={<Pill variant="outline">{ROLE[p.role]}</Pill>}
/>
))}
</RowList>
)}
</Panel>
{isOwner ? (
<Panel title="Owner tools" flush>
<RowList label="Owner tools">
<Row title="Manuscripts" href="/manuscripts" renderLink={link} media={DocGlyph} mediaVariant="icon" detail="The shared Drive folder, read-only." />
<Row title="Social" href="/social" renderLink={link} media={PostGlyph} mediaVariant="icon" detail="Posts announcing what went live." />
{google?.configured ? (
<Row
title="Send to Docs"
media={LinkGlyph}
mediaVariant="icon"
meta={google.connected ? <Status tone="ok">Connected</Status> : <Status tone="nodata">Not connected</Status>}
detail={google.connected ? "Connected with drive.file only." : "Connect Google to send a post to Docs."}
actions={
<a href="/auth/google/start" className="cap-btn">
{google.connected ? "Connect again" : "Connect Google"}
</a>
}
/>
) : (
<Row title="Send to Docs" media={LinkGlyph} mediaVariant="icon" meta={<Status tone="nodata">Not set up</Status>} detail="Send to Docs is not set up yet." />
)}
</RowList>
</Panel>
) : null}
</div>
</div>
);
}