Skip to content
101 linesCodeBlameRaw
1import type { DocRepoPage } from "@g1t/contracts";
2import { FolderGit2, GitPullRequestArrow, PencilLine, Trash2 } from "lucide-react";
3import { Link, data, useNavigate } from "react-router";
4
5import type { Route } from "./+types/repo-file";
6import { useDocsAction } from "../../../components/docs/actions";
7import { Crumbs } from "../../../components/docs/parts";
8import { Markdown } from "../../../components/markdown";
9import { ErrorText, TimeAgo } from "../../../components/ui";
10import { Hint } from "../../../components/ui/hint";
11import { page as pageMeta } from "../../../lib/meta";
12import { docs } from "../../../lib/services.server";
13import { requireUser, roleIn } from "../../../lib/session.server";
14
15export function meta({ loaderData: loaded, params, ...args }: Route.MetaArgs) {
16 const file = loaded?.found.file;
17 return pageMeta(args, { title: `${file?.title ?? "Docs"} · ${params.repoOwner}/${params.repoName} · ${params.owner} · g1t` });
18}
19
20export async function loader({ params, context, request }: Route.LoaderArgs): Promise<{ found: DocRepoPage }> {
21 const viewer = requireUser(context, request);
22 if (!roleIn(viewer, params.owner)) throw data(null, { status: 404 });
23 const repo = `${params.repoOwner}/${params.repoName}`;
24 const found = await docs.repoPage(params.owner.toLowerCase(), viewer, repo, params["*"] ?? "").catch(() => null);
25 if (!found?.ok) throw data(null, { status: 404 });
26 return { found: found.value };
27}
28
29/**
30 * One file of a project's docs, read-only: rendered as Code renders it,
31 * with its links and pictures pointing into the repository. Changes go
32 * through the repository: Edit in Code opens the file there.
33 */
34export default function RepoDocFile({ loaderData, params }: Route.ComponentProps) {
35 const slug = params.owner.toLowerCase();
36 const { space, file } = loaderData.found;
37 const navigate = useNavigate();
38 const { send, error } = useDocsAction(slug);
39 const [namespace, name] = space.repo.split("/") as [string, string];
40 const folder = file.path.includes("/") ? file.path.slice(0, file.path.lastIndexOf("/")) : "";
41 const encode = (path: string) => path.split("/").map(encodeURIComponent).join("/");
42 return (
43 <div className="-mt-6">
44 <div className="sticky top-(--topbar-h) z-20 -mx-4 flex h-12 items-center gap-3 border-b border-line bg-bg/85 px-4 backdrop-blur sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
45 <div className="min-w-0 grow">
46 <Crumbs items={[{ label: "Docs", to: `/${slug}/-/docs` }, { label: space.repo }, ...file.path.split("/").map((part) => ({ label: part }))]} />
47 </div>
48 <Link to={file.code_href} className="inline-flex h-8 items-center gap-1.5 rounded-md border border-line px-2.5 text-xs text-fg/85 hover:bg-raised">
49 <PencilLine size={13} /> Edit in Code
50 </Link>
51 {space.can_remove && (
52 <Hint label={`Stop showing ${space.repo}'s docs in Docs`}>
53 <button
54 type="button"
55 aria-label={`Stop showing ${space.repo}'s docs`}
56 onClick={async () => {
57 const done = await send("remove_repo_space", { id: space.id });
58 if (done.ok) navigate(`/${slug}/-/docs`);
59 }}
60 className="flex size-8 items-center justify-center rounded-md text-faint hover:bg-raised hover:text-danger"
61 >
62 <Trash2 size={15} />
63 </button>
64 </Hint>
65 )}
66 </div>
67 <article className="mx-auto max-w-3xl pt-10">
68 <p className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-faint">
69 <span className="inline-flex items-center gap-1.5">
70 <FolderGit2 size={13} /> {space.repo} · {space.default_branch}
71 </span>
72 <span className="font-mono">{file.path}</span>
73 {space.indexed_at && (
74 <span>
75 Read <TimeAgo at={space.indexed_at} />
76 </span>
77 )}
78 </p>
79 <p className="mt-3 flex items-start gap-2 rounded-lg border border-line bg-surface px-3 py-2 text-xs leading-relaxed text-muted">
80 <GitPullRequestArrow size={14} className="mt-px shrink-0 text-faint" aria-hidden="true" />
81 This page lives in the repository and changes through pull requests. Edit it in Code, or ask an agent to open a pull request for it.
82 </p>
83 {error && (
84 <div className="mt-3">
85 <ErrorText>{error}</ErrorText>
86 </div>
87 )}
88 <div className="docs-read mt-6">
89 <Markdown
90 source={file.markdown}
91 repo={{ namespace, name }}
92 // Relative links point into the repository at its default
93 // branch, and pictures at its files there.
94 base={`/${space.repo}/blob/${encodeURIComponent(space.default_branch)}${folder ? `/${encode(folder)}` : ""}`}
95 rawBase={`/${space.repo}/raw/${encodeURIComponent(space.commit ?? space.default_branch)}${folder ? `/${encode(folder)}` : ""}`}
96 />
97 </div>
98 </article>
99 </div>
100 );
101}