Skip to content
88 linesCodeBlameRaw
1import { Form, Link, useLocation, useRouteLoaderData } from "react-router";
2
3import type { Membership, User } from "@g1t/contracts";
4
5import { withNext } from "../lib/next";
6import { useSignUpCopy } from "../lib/registration";
7import { missingKind, notFoundCopy } from "../lib/not-found";
8import { PageMain } from "./landmark";
9import { Pixel404 } from "./logo";
10import { ButtonLink, SubmitButton } from "./ui";
11
12/**
13 * The page for anything someone cannot see. It reads only the kind of thing
14 * the address names and who is signed in, so a private project and a
15 * missing one give the same page, word for word, and the same 404.
16 */
17export function NotFound({ data }: { data?: unknown }) {
18 const { pathname, search } = useLocation();
19 const root = useRouteLoaderData("root") as
20 | { user?: User | null; shell?: { workspace?: Membership | null; repo?: { namespace: string; name: string } | null } }
21 | undefined;
22 const user = root?.user ?? null;
23 const workspace = user ? (root?.shell?.workspace ?? null) : null;
24 // Something missing inside a project the viewer can see: back to it.
25 const project = root?.shell?.repo ?? null;
26 const inProject =
27 project != null && pathname.toLowerCase().startsWith(`/${project.namespace}/${project.name}/`.toLowerCase());
28 const kind = missingKind(data, pathname);
29 const copy = notFoundCopy(kind, user?.username);
30 const here = pathname + search;
31 const signUp = useSignUpCopy();
32 return (
33 <PageMain className="mx-auto flex max-w-lg flex-col items-center px-4 py-20 text-center sm:py-28">
34 <Pixel404 className="text-[3.5rem] sm:text-[4.5rem]" />
35 <h1 className="mt-10 text-balance text-2xl font-semibold tracking-tight">{copy.title}</h1>
36 <p className="mt-2 text-balance text-muted">{copy.body}</p>
37
38 <div className="mt-8 flex flex-wrap justify-center gap-2">
39 {copy.signIn ? (
40 <ButtonLink to={withNext("/login", here)}>Sign in</ButtonLink>
41 ) : kind === "person" ? (
42 <ButtonLink to="/search?type=people">Search people</ButtonLink>
43 ) : inProject ? (
44 <ButtonLink to={`/${project.namespace}/${project.name}`}>Go to {project.name}</ButtonLink>
45 ) : workspace ? (
46 <ButtonLink to={`/${workspace.slug}`}>Go to {workspace.name?.trim() || workspace.slug}</ButtonLink>
47 ) : user ? (
48 <ButtonLink to="/">Go to Mission control</ButtonLink>
49 ) : null}
50 <ButtonLink to="/explore" variant="quiet">
51 {user ? "Explore" : "Explore public projects"}
52 </ButtonLink>
53 </div>
54
55 <p className="mt-6 hidden text-sm text-faint sm:block">
56 Looking for something? Press{" "}
57 <kbd className="rounded bg-raised px-1.5 py-0.5 font-mono text-[0.6875rem] text-muted ring-1 ring-line">⌘K</kbd>{" "}
58 to search.
59 </p>
60
61 {copy.signIn && (
62 <p className="mt-3 text-xs text-faint">
63 New to g1t?{" "}
64 <Link to={withNext("/register", here)} className="text-muted underline-offset-4 hover:text-fg hover:underline">
65 {signUp.primary}
66 </Link>
67 </p>
68 )}
69
70 {copy.signedInAs && (
71 // Signing out, then in again, comes back here.
72 <Form
73 method="post"
74 action={`/logout?next=${encodeURIComponent(withNext("/login", here))}`}
75 className="mt-3 text-xs text-faint"
76 >
77 Signed in as <span className="font-mono text-muted">@{copy.signedInAs}</span> ·{" "}
78 <SubmitButton
79 pending="Signing out…"
80 className="inline-flex items-center gap-1 text-muted underline-offset-4 hover:text-fg hover:underline disabled:opacity-50"
81 >
82 Switch account
83 </SubmitButton>
84 </Form>
85 )}
86 </PageMain>
87 );
88}