1import React, { useEffect, useState } from "react";
2import ReactDOM from "react-dom/client";
3import { getConnection, getSession, signOut, type User } from "./client.ts";
4import { RepoIcon, ThemeToggle } from "./components.tsx";
5import { RepoPage } from "./pages/Repo.tsx";
6import { ReposPage } from "./pages/Repos.tsx";
7import { SignInPage } from "./pages/SignIn.tsx";
8import { Link, Router, segments, useRouter } from "./router.tsx";
9import "./styles.css";
10import { ThemeProvider } from "./theme.tsx";
11
12/**
13 * A GitHub-style browser for the deployed git-service — a plain React SPA
14 * (no framework router, no Effect in the bundle) that drives the service's
15 * REST API (`/api/v1`) with the Better Auth session cookie.
16 *
17 * Routes (GitHub-shaped):
18 *
19 * / → repository list
20 * /:owner/:repo → code view (tree @ default branch)
21 * /:owner/:repo/tree/:ref/*path → tree at path
22 * /:owner/:repo/blob/:ref/*path → file view
23 * /:owner/:repo/commits/:ref → commit log
24 * /:owner/:repo/commit/:oid → one commit: message + file diffs
25 * /:owner/:repo/pulls → pull requests (open/closed/merged)
26 * /:owner/:repo/pulls/:number → one pull request: merge box + diffs
27 * /:owner/:repo/settings → API keys / storage / danger zone
28 */
29const Header = ({
30 user,
31 onSignIn,
32 onSignOut,
33}: {
34 user: User | null;
35 onSignIn: () => void;
36 onSignOut: () => void;
37}) => (
38 <header className="border-b border-border-muted bg-canvas-subtle">
39 <div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3">
40 <Link to="/" className="flex items-center gap-2 font-semibold">
41 <RepoIcon className="size-5" />
42 git service
43 </Link>
44 <div className="flex items-center gap-3 text-xs text-fg-muted">
45 <ThemeToggle />
46 {user !== null ? (
47 <>
48 <span className="hidden sm:inline">{user.name || user.email}</span>
49 <button
50 type="button"
51 onClick={onSignOut}
52 className="cursor-pointer rounded-md border border-border-muted px-2 py-1 hover:text-danger"
53 >
54 Sign out
55 </button>
56 </>
57 ) : (
58 <button
59 type="button"
60 onClick={onSignIn}
61 className="cursor-pointer rounded-md border border-border-muted px-2 py-1 hover:text-accent"
62 >
63 Sign in
64 </button>
65 )}
66 </div>
67 </div>
68 </header>
69);
70
71const Routes = () => {
72 const { path } = useRouter();
73 const connection = getConnection();
74 // `undefined` while the session probe is in flight.
75 const [user, setUser] = useState<User | null | undefined>(undefined);
76 const [showSignIn, setShowSignIn] = useState(false);
77
78 useEffect(() => {
79 getSession(connection)
80 .then(setUser)
81 .catch(() => setUser(null));
82 }, [connection.url]);
83
84 if (user === undefined) return null;
85
86 // Browsing is ANONYMOUS by default — public repos need no account. The
87 // sign-in screen appears only when the user asks for it.
88 if (showSignIn) {
89 return (
90 <SignInPage
91 onSignedIn={(signedIn) => {
92 setUser(signedIn);
93 setShowSignIn(false);
94 }}
95 onCancel={() => setShowSignIn(false)}
96 />
97 );
98 }
99
100 const parts = segments(path);
101 return (
102 <>
103 <Header
104 user={user}
105 onSignIn={() => setShowSignIn(true)}
106 onSignOut={() => {
107 void signOut(connection).finally(() => setUser(null));
108 }}
109 />
110 <main className="mx-auto max-w-5xl px-4 py-6">
111 {parts.length === 0 ? (
112 <ReposPage connection={connection} user={user} />
113 ) : parts.length >= 2 ? (
114 <RepoPage
115 connection={connection}
116 user={user}
117 owner={parts[0]!}
118 name={parts[1]!.replace(/\.git$/, "")}
119 rest={parts.slice(2)}
120 />
121 ) : (
122 <div className="py-16 text-center text-fg-muted">Not found.</div>
123 )}
124 </main>
125 </>
126 );
127};
128
129ReactDOM.createRoot(document.getElementById("root")!).render(
130 <React.StrictMode>
131 <ThemeProvider>
132 <Router>
133 <Routes />
134 </Router>
135 </ThemeProvider>
136 </React.StrictMode>,
137);