Skip to the content

Capsid menu

app/routes/home.tsx

117 lines and 4 functions in DrDustinEdwards/carrel, at commit d5d5e82 on 2026-10-24. This file on GitHub.

Functions

The functions of home.tsx
FunctionLinesComplexityCoveredCRAPMutation
loader16 to 253100%OkCRAP 3No data: mutation testing has not run on this repo yet
Home57 to 127100%CriticalCRAP 110No data: mutation testing has not run on this repo yet
link59 to 5910%OkCRAP 2No data: mutation testing has not run on this repo yet
<anonymous:84>84 to 9550%WatchCRAP 30No 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>
  );
}