| 1 | import type { DocRepoPage } from "@g1t/contracts"; |
| 2 | import { FolderGit2, GitPullRequestArrow, PencilLine, Trash2 } from "lucide-react"; |
| 3 | import { Link, data, useNavigate } from "react-router"; |
| 4 | |
| 5 | import type { Route } from "./+types/repo-file"; |
| 6 | import { useDocsAction } from "../../../components/docs/actions"; |
| 7 | import { Crumbs } from "../../../components/docs/parts"; |
| 8 | import { Markdown } from "../../../components/markdown"; |
| 9 | import { ErrorText, TimeAgo } from "../../../components/ui"; |
| 10 | import { Hint } from "../../../components/ui/hint"; |
| 11 | import { page as pageMeta } from "../../../lib/meta"; |
| 12 | import { docs } from "../../../lib/services.server"; |
| 13 | import { requireUser, roleIn } from "../../../lib/session.server"; |
| 14 | |
| 15 | export 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 | |
| 20 | export 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 | */ |
| 34 | export 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 | } |