pr_01m47d24b0e6n91zwymwxg0vpx/apps/web/app/root.tsx

228 lines7,134 bytesCodeBlame
1import { Plus, Search } from "lucide-react";
2import {
3 Form,
4 isRouteErrorResponse,
5 Link,
6 Links,
7 Meta,
8 NavLink,
9 Outlet,
10 Scripts,
11 ScrollRestoration,
12 useRouteLoaderData,
13} from "react-router";
14
15import type { Route } from "./+types/root";
16import "./app.css";
17import { Logo } from "./components/logo";
18import { Avatar, ButtonLink } from "./components/ui";
19import { getViewer, viewerMiddleware } from "./lib/session.server";
20
21export const links: Route.LinksFunction = () => [
22 { rel: "icon", type: "image/svg+xml", href: "/favicon.svg" },
23 { rel: "preconnect", href: "https://fonts.googleapis.com" },
24 {
25 rel: "preconnect",
26 href: "https://fonts.gstatic.com",
27 crossOrigin: "anonymous",
28 },
29 {
30 rel: "stylesheet",
31 href: "https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400..700&family=JetBrains+Mono:wght@400;500;600&display=swap",
32 },
33];
34
35export const middleware: Route.MiddlewareFunction[] = [viewerMiddleware];
36
37export function loader({ context }: Route.LoaderArgs) {
38 return { user: getViewer(context) };
39}
40
41function HeaderLink({ to, children }: { to: string; children: React.ReactNode }) {
42 return (
43 <NavLink
44 to={to}
45 className={({ isActive }) =>
46 `rounded-md px-2.5 py-1.5 text-sm transition-colors hover:bg-raised hover:text-fg ${
47 isActive ? "text-fg" : "text-muted"
48 }`
49 }
50 >
51 {children}
52 </NavLink>
53 );
54}
55
56function Header({ user }: { user: { username: string } | null | undefined }) {
57 return (
58 <header className="sticky top-0 z-40 border-b border-line bg-surface/85 backdrop-blur">
59 <div className="mx-auto flex h-14 max-w-6xl items-center gap-2 px-4">
60 <Link to="/" aria-label="g1t home" className="mr-2">
61 <Logo />
62 </Link>
63 <Form action="/search" role="search" className="relative hidden grow sm:block sm:max-w-xs">
64 <Search
65 size={14}
66 className="pointer-events-none absolute top-1/2 left-3 -translate-y-1/2 text-faint"
67 />
68 <input
69 name="q"
70 placeholder="Search repositories"
71 aria-label="Search repositories"
72 className="w-full rounded-md border border-line bg-bg py-1.5 pr-3 pl-8 text-sm outline-none transition-colors placeholder:text-faint hover:border-line-strong focus:border-accent-dim"
73 />
74 </Form>
75 <nav className="flex items-center gap-0.5">
76 <HeaderLink to="/explore">Explore</HeaderLink>
77 <HeaderLink to="/docs">Docs</HeaderLink>
78 </nav>
79 <div className="ml-auto flex items-center gap-2">
80 {user ? (
81 <>
82 <Link
83 to="/new"
84 aria-label="New repository"
85 className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg"
86 >
87 <Plus size={16} />
88 </Link>
89 <Link
90 to={`/${user.username}`}
91 className="flex items-center gap-2 rounded-md px-2 py-1 text-sm hover:bg-raised"
92 >
93 <Avatar name={user.username} size={22} />
94 <span className="hidden font-mono sm:inline">{user.username}</span>
95 </Link>
96 <HeaderLink to="/settings">Settings</HeaderLink>
97 <Form method="post" action="/logout">
98 <button
99 type="submit"
100 className="rounded-md px-2.5 py-1.5 text-sm text-muted hover:bg-raised hover:text-fg"
101 >
102 Sign out
103 </button>
104 </Form>
105 </>
106 ) : (
107 <>
108 <HeaderLink to="/login">Sign in</HeaderLink>
109 <ButtonLink to="/register">Sign up</ButtonLink>
110 </>
111 )}
112 </div>
113 </div>
114 </header>
115 );
116}
117
118const FOOTER_LINKS: { title: string; links: [string, string][] }[] = [
119 {
120 title: "Product",
121 links: [
122 ["Explore", "/explore"],
123 ["Sign up", "/register"],
124 ["Source", "/syntaqx/g1t"],
125 ],
126 },
127 {
128 title: "Docs",
129 links: [
130 ["Getting started", "/docs"],
131 ["Concepts", "/docs/concepts"],
132 ["Connect an agent", "/docs/agents"],
133 ["API", "/docs/api"],
134 ],
135 },
136];
137
138function Footer() {
139 return (
140 <footer className="mt-24 border-t border-line">
141 <div className="mx-auto flex max-w-6xl flex-wrap gap-x-20 gap-y-8 px-4 py-12">
142 <div className="grow">
143 <Logo />
144 <p className="mt-3 max-w-xs text-sm text-muted">
145 A git forge for agents. Open source under the MIT license, built on
146 Cloudflare Workers and Artifacts.
147 </p>
148 </div>
149 {FOOTER_LINKS.map((group) => (
150 <div key={group.title}>
151 <p className="text-sm font-medium">{group.title}</p>
152 <ul className="mt-3 space-y-2 text-sm text-muted">
153 {group.links.map(([label, to]) => (
154 <li key={to}>
155 <Link to={to} className="hover:text-fg">
156 {label}
157 </Link>
158 </li>
159 ))}
160 </ul>
161 </div>
162 ))}
163 </div>
164 </footer>
165 );
166}
167
168export function Layout({ children }: { children: React.ReactNode }) {
169 // Undefined when the root loader itself failed.
170 const user = useRouteLoaderData<typeof loader>("root")?.user;
171 return (
172 <html lang="en">
173 <head>
174 <meta charSet="utf-8" />
175 <meta name="viewport" content="width=device-width, initial-scale=1" />
176 <meta name="theme-color" content="#0e0d0a" />
177 <Meta />
178 <Links />
179 </head>
180 <body className="flex min-h-screen flex-col">
181 <Header user={user} />
182 <div className="grow">{children}</div>
183 <Footer />
184 <ScrollRestoration />
185 <Scripts />
186 </body>
187 </html>
188 );
189}
190
191export default function App() {
192 return <Outlet />;
193}
194
195export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
196 let title = "Something went wrong";
197 let details = "An unexpected error occurred. Try again in a moment.";
198 let stack: string | undefined;
199
200 if (isRouteErrorResponse(error)) {
201 if (error.status === 404) {
202 title = "Page not found";
203 details = "There is nothing at this address, or you do not have access to it.";
204 } else if (typeof error.data === "string" && error.data) {
205 details = error.data;
206 }
207 } else if (import.meta.env.DEV && error instanceof Error) {
208 details = error.message;
209 stack = error.stack;
210 }
211
212 return (
213 <main className="mx-auto max-w-xl px-4 py-32 text-center">
214 <h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
215 <p className="mt-3 text-muted">{details}</p>
216 <div className="mt-8">
217 <ButtonLink to="/" variant="quiet">
218 Back to home
219 </ButtonLink>
220 </div>
221 {stack && (
222 <pre className="mt-8 overflow-x-auto rounded-lg border border-line p-4 text-left text-xs">
223 <code>{stack}</code>
224 </pre>
225 )}
226 </main>
227 );
228}