Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 1 | /** |
| 2 | * The site's footer: a few links worth having, who makes g1t, and a row of | |
| 3 | * the trust pages with the live status dot, the account, and the legal | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 4 | * line. Marketing and signed-out pages show it; the app's account menu |
| 5 | * has its own rows (components/shell.tsx). | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 6 | */ |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 7 | import { useEffect, useState } from "react"; |
| 8 | import { Link } from "react-router"; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 9 | |
| 10 | import type { User } from "@g1t/contracts"; | |
| 11 | ||
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 12 | import { useAsksFirst } from "./analytics-consent"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 13 | import { Mark } from "./logo"; |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 14 | import { choose } from "../lib/analytics-consent"; |
| 15 | import { COMPANY, MAKER_PRODUCTS, SOCIAL, copyright, listed } from "../lib/legal"; | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 16 | import { type OverallState, STATUS_JSON_URL, STATUS_URL, STATUS_WORDS, type StatusReport, dotClass } from "../lib/status"; |
| 17 | ||
| 18 | /** How long one fetched report is reused across the pages of one visit. */ | |
| 19 | const STATUS_REUSE_MS = 60_000; | |
| 20 | let lastStatus: { report: StatusReport; at: number } | null = null; | |
| 21 | let pendingStatus: Promise<StatusReport | null> | null = null; | |
| 22 | ||
| 23 | function fetchStatus(): Promise<StatusReport | null> { | |
| 24 | if (lastStatus && Date.now() - lastStatus.at < STATUS_REUSE_MS) return Promise.resolve(lastStatus.report); | |
| 25 | pendingStatus ??= fetch(STATUS_JSON_URL, { headers: { accept: "application/json" } }) | |
| 26 | .then((response) => (response.ok ? (response.json() as Promise<StatusReport>) : null)) | |
| 27 | .then((report) => { | |
| 28 | if (report?.overall) lastStatus = { report, at: Date.now() }; | |
| 29 | return report?.overall ? report : null; | |
| 30 | }) | |
| 31 | // The status page being unreachable only means no dot. | |
| 32 | .catch(() => null) | |
| 33 | .finally(() => { | |
| 34 | pendingStatus = null; | |
| 35 | }); | |
| 36 | return pendingStatus; | |
| 37 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 38 | |
| 39 | /** | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 40 | * The live status, from status.g1t.sh once the page is up (or `when` turns |
| 41 | * true), so no page waits on it. Kept a minute across pages. Null until it | |
| 42 | * arrives, or if it cannot. | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 43 | */ |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 44 | export function useSiteStatus(when = true): StatusReport | null { |
| 45 | const [report, setReport] = useState<StatusReport | null>(() => lastStatus?.report ?? null); | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 46 | useEffect(() => { |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 47 | if (!when) return; |
| 48 | let live = true; | |
| 49 | fetchStatus().then((fresh) => { | |
| 50 | if (live && fresh) setReport(fresh); | |
| 51 | }); | |
| 52 | return () => { | |
| 53 | live = false; | |
| 54 | }; | |
| 55 | }, [when]); | |
| 56 | return report; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 57 | } |
| 58 | ||
| 59 | /** The coloured dot, with words for screen readers. */ | |
| 60 | export function StatusDot({ state, className = "" }: { state: OverallState | null; className?: string }) { | |
| 61 | return ( | |
| 62 | <span className={`relative inline-flex size-2 shrink-0 ${className}`}> | |
| 63 | {state && state !== "up" && state !== "unknown" && ( | |
| 64 | <span aria-hidden className={`absolute inset-0 animate-ping rounded-full opacity-60 ${dotClass(state)}`} /> | |
| 65 | )} | |
| 66 | <span aria-hidden className={`relative inline-flex size-2 rounded-full ${dotClass(state)}`} /> | |
| 67 | <span className="sr-only">{state ? STATUS_WORDS[state] : "Checking status"}</span> | |
| 68 | </span> | |
| 69 | ); | |
| 70 | } | |
| 71 | ||
| 72 | /** Where the footer's grid links go. Plain links: some are files or other hosts. */ | |
| 73 | const FOOTER_LINKS: { title: string; links: [string, string][] }[] = [ | |
| 74 | { | |
| 75 | title: "Product", | |
| 76 | links: [ | |
| 77 | ["Explore", "/explore"], | |
| 78 | ["Pricing", "/pricing"], | |
| Chat and workspace agents: channels, DMs and named agents you talk to | 79 | ["Chat", "https://docs.g1t.sh/guides/chat/"], |
| 80 | ["Agents", "https://docs.g1t.sh/guides/agents/"], | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 81 | ["Working with g1t", "https://docs.g1t.sh/guides/working-with-g1t/"], |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 82 | ["Bring your own agent", "https://docs.g1t.sh/guides/bring-your-own-agent/"], |
| 83 | ["Integrations", "https://docs.g1t.sh/guides/integrations/"], | |
| 84 | ], | |
| 85 | }, | |
| 86 | { | |
| 87 | title: "Developers", | |
| 88 | links: [ | |
| 89 | ["Documentation", "https://docs.g1t.sh/"], | |
| 90 | ["API reference", "https://docs.g1t.sh/reference/api/"], | |
| 91 | ["MCP tools", "https://docs.g1t.sh/reference/mcp/"], | |
| 92 | ["llms.txt", "/llms.txt"], | |
| 93 | ], | |
| 94 | }, | |
| 95 | { | |
| 96 | title: "Company", | |
| 97 | links: [ | |
| 98 | ["Flagon, Inc.", COMPANY.url], | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 99 | ...SOCIAL.map((account): [string, string] => [account.label, account.url]), |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 100 | ["Support", "/support"], |
| 101 | ["Security", "/security"], | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 102 | ["Status", STATUS_URL], |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 103 | ], |
| 104 | }, | |
| 105 | { | |
| 106 | title: "Open source", | |
| 107 | links: [ | |
| 108 | ["Source on g1t", "/flagon-io/g1t"], | |
| 109 | ["MIT license", "/flagon-io/g1t/blob/main/LICENSE"], | |
| 110 | ["Run g1t yourself", "https://docs.g1t.sh/guides/self-hosting/"], | |
| 111 | ], | |
| 112 | }, | |
| 113 | ]; | |
| 114 | ||
| 115 | /** The row's own links, after Status. */ | |
| 116 | const ROW_LINKS: [string, string][] = [ | |
| 117 | ["Support", "/support"], | |
| 118 | ["Policies", "/policies"], | |
| 119 | ["Privacy", "/policies/privacy"], | |
| 120 | ["Security", "/security"], | |
| 121 | ]; | |
| 122 | ||
| 123 | /** | |
| 124 | * Flagon's mark, single-ink, as in Flagon's brand files | |
| 125 | * (flagon-mark-mono.svg): a flagon, its brew a tone of the text colour. | |
| 126 | */ | |
| 127 | function FlagonMark({ className = "" }: { className?: string }) { | |
| 128 | return ( | |
| 129 | <svg viewBox="11.3 12.3 41 41" fill="none" aria-hidden="true" className={className}> | |
| 130 | <defs> | |
| 131 | <clipPath id="g1t-footer-flagon-body"> | |
| 132 | <path d="M19.5 23 L34.5 23 L36.5 46 Q36.7 48 34.3 48 L19.7 48 Q17.3 48 17.5 46 Z" /> | |
| 133 | </clipPath> | |
| 134 | </defs> | |
| 135 | <g clipPath="url(#g1t-footer-flagon-body)"> | |
| 136 | <path | |
| 137 | d="M13 27 q3.75 -0.7 7.5 0 t7.5 0 t7.5 0 t7.5 0 t7.5 0 L51 52 L13 52 Z" | |
| 138 | fill="currentColor" | |
| 139 | fillOpacity="0.22" | |
| 140 | /> | |
| 141 | </g> | |
| 142 | <path | |
| 143 | d="M37 27 L45.5 27 L48.5 30 L48.5 36.5 L45.5 39.5 L38 39.5" | |
| 144 | stroke="currentColor" | |
| 145 | strokeWidth="3.1" | |
| 146 | strokeLinecap="round" | |
| 147 | strokeLinejoin="round" | |
| 148 | /> | |
| 149 | <path | |
| 150 | d="M17.5 21 L36.5 21 L38.6 47 Q39 50 36 50 L18 50 Q15 50 15.4 47 Z" | |
| 151 | stroke="currentColor" | |
| 152 | strokeWidth="3.1" | |
| 153 | strokeLinejoin="round" | |
| 154 | /> | |
| 155 | <path d="M17 21 Q16.3 16.3 20 15.6 L33 15.6 Q37.2 16.2 37 21 Z" stroke="currentColor" strokeWidth="3" strokeLinejoin="round" /> | |
| 156 | <path d="M36.7 17.1 L39.4 16.1" stroke="currentColor" strokeWidth="2.4" strokeLinejoin="round" /> | |
| 157 | <circle cx="40.3" cy="15.8" r="1.4" fill="currentColor" /> | |
| 158 | </svg> | |
| 159 | ); | |
| 160 | } | |
| 161 | ||
| 162 | /** "g1t is designed, built, and backed by Flagon, Inc., …" */ | |
| 163 | export function MakerLine() { | |
| 164 | const products = MAKER_PRODUCTS.map((product) => product.name); | |
| 165 | return ( | |
| 166 | <p className="text-sm leading-6 text-muted text-balance"> | |
| 167 | <Mark tight className="mr-1.5 inline-block h-[0.85em] w-auto align-baseline" /> | |
| 168 | <span className="font-semibold text-fg">g1t</span> is designed, built, and backed by{" "} | |
| 169 | <a href={COMPANY.url} className="whitespace-nowrap font-semibold text-fg hover:underline hover:underline-offset-4"> | |
| 170 | <FlagonMark className="mr-1 inline-block size-[0.95em] -translate-y-px align-middle" /> | |
| 171 | {COMPANY.name} | |
| 172 | </a> | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 173 | {products.length > 0 ? `, the people behind ${listed(products)}.` : `, ${COMPANY.about} founded by ${COMPANY.founder}.`} |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 174 | </p> |
| 175 | ); | |
| 176 | } | |
| 177 | ||
| 178 | const ROW_LINK = "rounded-sm transition-colors hover:text-fg"; | |
| 179 | ||
| 180 | /** | |
| 181 | * Status, the trust pages, the account and the legal line, separated by | |
| 182 | * dots that wrap away cleanly on a phone. | |
| 183 | */ | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 184 | export function LegalRow({ user }: { user: User | null | undefined }) { |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 185 | const status = useSiteStatus(); |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 186 | const asksFirst = useAsksFirst(); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 187 | const links = [ |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 188 | <a key="status" href={STATUS_URL} className={`inline-flex items-center gap-1.5 ${ROW_LINK}`}> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 189 | <StatusDot state={status?.overall.state ?? null} /> |
| 190 | Status | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 191 | </a>, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 192 | ...ROW_LINKS.map(([label, to]) => ( |
| 193 | <Link key={to} to={to} className={ROW_LINK}> | |
| 194 | {label} | |
| 195 | </Link> | |
| 196 | )), | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 197 | // Where the visitor was asked about the analytics cookie: ask again. |
| 198 | ...(asksFirst | |
| 199 | ? [ | |
| 200 | <button key="consent" type="button" onClick={() => choose(null)} className={ROW_LINK}> | |
| 201 | Cookie choices | |
| 202 | </button>, | |
| 203 | ] | |
| 204 | : []), | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 205 | ]; |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 206 | const account = [ |
| 207 | user ? ( | |
| 208 | <Link key="me" to={`/u/${user.username}`} className={`font-medium text-fg/90 ${ROW_LINK}`}> | |
| 209 | {user.username} | |
| 210 | </Link> | |
| 211 | ) : ( | |
| 212 | <Link key="login" to="/login" className={ROW_LINK}> | |
| 213 | Sign in | |
| 214 | </Link> | |
| 215 | ), | |
| 216 | ]; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 217 | const legal = [ |
| 218 | ...account, | |
| 219 | <span key="copyright" className="text-faint"> | |
| 220 | {copyright()} | |
| 221 | </span>, | |
| 222 | ]; | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 223 | const gap = "gap-x-3"; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 224 | const dot = ( |
| 225 | <span aria-hidden className="text-line-strong"> | |
| 226 | · | |
| 227 | </span> | |
| 228 | ); | |
| 229 | // Two runs, the links and the legal line, so that on a narrow screen the | |
| 230 | // second starts its own line rather than a line starting with a dot. | |
| 231 | const run = (items: React.ReactNode[]) => | |
| 232 | items.map((item, index) => ( | |
| 233 | <li key={index} className={`flex items-center ${gap}`}> | |
| 234 | {index > 0 && dot} | |
| 235 | {item} | |
| 236 | </li> | |
| 237 | )); | |
| 238 | return ( | |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 239 | <div className={`flex flex-wrap items-center justify-center gap-y-1.5 ${gap} text-[0.8125rem] text-muted`}> |
| 240 | <ul className={`flex flex-wrap items-center justify-center gap-y-1.5 ${gap}`}>{run(links)}</ul> | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 241 | {/* Between the two runs only while they share a line. */} |
| status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas | 242 | <span className="hidden md:inline">{dot}</span> |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 243 | <ul className={`flex flex-wrap items-center justify-center gap-y-1.5 ${gap}`}>{run(legal)}</ul> |
| 244 | </div> | |
| 245 | ); | |
| 246 | } | |
| 247 | ||
| 248 | export function SiteFooter({ user }: { user: User | null | undefined }) { | |
| 249 | return ( | |
| 250 | <footer className="mt-24 border-t border-line"> | |
| 251 | <nav aria-label="Footer" className="mx-auto grid max-w-6xl grid-cols-2 gap-x-8 gap-y-8 px-4 pt-12 pb-10 sm:grid-cols-4"> | |
| 252 | {FOOTER_LINKS.map((group) => ( | |
| 253 | <div key={group.title}> | |
| 254 | <p className="text-sm font-medium">{group.title}</p> | |
| 255 | <ul className="mt-3 space-y-2 text-sm text-muted"> | |
| 256 | {group.links.map(([label, to]) => ( | |
| 257 | <li key={to}> | |
| 258 | <a href={to} className="transition-colors hover:text-fg"> | |
| 259 | {label} | |
| 260 | </a> | |
| 261 | </li> | |
| 262 | ))} | |
| 263 | </ul> | |
| 264 | </div> | |
| 265 | ))} | |
| 266 | </nav> | |
| 267 | <div className="border-t border-line"> | |
| 268 | <div className="mx-auto max-w-3xl space-y-4 px-4 py-10 text-center"> | |
| 269 | <MakerLine /> | |
| 270 | <LegalRow user={user} /> | |
| 271 | </div> | |
| 272 | </div> | |
| 273 | </footer> | |
| 274 | ); | |
| 275 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.