| 1 | /** |
| 2 | * What a not-found page says. A private project, issue or workspace and one |
| 3 | * that does not exist must look the same to someone who cannot see it, so |
| 4 | * the words depend only on what kind of thing the address names and on who |
| 5 | * is looking, never on whether it exists. |
| 6 | */ |
| 7 | |
| 8 | /** The kind of thing an address names. */ |
| 9 | export type MissingKind = "project" | "issue" | "pull" | "workspace" | "person" | "page"; |
| 10 | |
| 11 | const KINDS: readonly MissingKind[] = ["project", "issue", "pull", "workspace", "person", "page"]; |
| 12 | |
| 13 | /** Words for each kind, as a sentence uses them. */ |
| 14 | const NOUN: Record<MissingKind, string> = { |
| 15 | project: "project", |
| 16 | issue: "issue", |
| 17 | pull: "pull request", |
| 18 | workspace: "workspace", |
| 19 | person: "person", |
| 20 | page: "page", |
| 21 | }; |
| 22 | |
| 23 | /** Names that are pages of g1t itself, never a workspace. */ |
| 24 | const RESERVED = new Set([ |
| 25 | "settings", "explore", "search", "new", "u", "pricing", "avatars", "workspaces", "login", "logout", |
| 26 | "register", "verify", "forgot", "reset", "device", "oauth", "invite", |
| 27 | ]); |
| 28 | |
| 29 | /** |
| 30 | * The kind a loader said it could not find (`data({ kind }, { status: 404 })`), |
| 31 | * else the kind the address names. Only the shape of the address is used. |
| 32 | */ |
| 33 | export function missingKind(errorData: unknown, pathname: string): MissingKind { |
| 34 | const said = (errorData as { kind?: unknown } | null | undefined)?.kind; |
| 35 | if (typeof said === "string" && (KINDS as readonly string[]).includes(said)) return said as MissingKind; |
| 36 | const parts = pathname.split("/").filter(Boolean); |
| 37 | if (parts[0] === "u" && parts.length === 2) return "person"; |
| 38 | if (parts.length === 0 || RESERVED.has(parts[0]!)) return "page"; |
| 39 | if (parts.length === 1) return "workspace"; |
| 40 | if (parts[1] === "-") return "page"; |
| 41 | if (parts.length === 2) return "project"; |
| 42 | if (parts[2] === "issues" && parts.length === 4 && /^\d+$/.test(parts[3]!)) return "issue"; |
| 43 | if (parts[2] === "pull" && parts.length === 4 && /^\d+$/.test(parts[3]!)) return "pull"; |
| 44 | return "page"; |
| 45 | } |
| 46 | |
| 47 | export type NotFoundCopy = { |
| 48 | title: string; |
| 49 | body: string; |
| 50 | /** Who is signed in, for "Signed in as". Null when no one is. */ |
| 51 | signedInAs: string | null; |
| 52 | /** Offer to sign in and come back. */ |
| 53 | signIn: boolean; |
| 54 | }; |
| 55 | |
| 56 | /** |
| 57 | * What the page says to whoever is looking. Signed out, it says that the |
| 58 | * page may be private, and to sign in if it is yours; signed in, that it |
| 59 | * does not exist or they cannot reach it. Never which of the two. |
| 60 | */ |
| 61 | export function notFoundCopy(kind: MissingKind, username: string | null | undefined): NotFoundCopy { |
| 62 | // People are public: a name either belongs to someone or it does not. |
| 63 | if (kind === "person") { |
| 64 | return { title: "Nothing here", body: "No one on g1t goes by that name.", signedInAs: null, signIn: false }; |
| 65 | } |
| 66 | if (!username) { |
| 67 | return { |
| 68 | title: "Nothing here", |
| 69 | body: "This page doesn't exist, or it's private. Sign in if it's yours.", |
| 70 | signedInAs: null, |
| 71 | signIn: true, |
| 72 | }; |
| 73 | } |
| 74 | return { |
| 75 | title: "Nothing here", |
| 76 | body: "This page doesn't exist, or you don't have access to it.", |
| 77 | signedInAs: username, |
| 78 | signIn: false, |
| 79 | }; |
| 80 | } |