diff --git a/web/package.json b/web/package.json
index 0c963e0..00d2e57 100644
--- a/web/package.json
+++ b/web/package.json
@@ -16,6 +16,7 @@
"@tanstack/react-router-devtools": "^1.131.5",
"@tanstack/router-plugin": "^1.121.2",
"lucide-react": "^0.544.0",
+ "next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-timeago": "^8.3.0",
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
index bf7251f..f9cd448 100644
--- a/web/pnpm-lock.yaml
+++ b/web/pnpm-lock.yaml
@@ -26,6 +26,9 @@ importers:
lucide-react:
specifier: ^0.544.0
version: 0.544.0(react@19.1.1)
+ next-themes:
+ specifier: ^0.4.6
+ version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
react:
specifier: ^19.0.0
version: 19.1.1
@@ -1865,6 +1868,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ next-themes@0.4.6:
+ resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
node-releases@2.0.21:
resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
@@ -4133,6 +4142,11 @@ snapshots:
nanoid@3.3.11: {}
+ next-themes@0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
+ dependencies:
+ react: 19.1.1
+ react-dom: 19.1.1(react@19.1.1)
+
node-releases@2.0.21: {}
normalize-path@3.0.0: {}
diff --git a/web/src/App.css b/web/src/App.css
index 53e76a0..e1f490b 100644
--- a/web/src/App.css
+++ b/web/src/App.css
@@ -1,9 +1,10 @@
.App {
- background-color: #f8fafc;
min-height: 100vh;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ background-color: var(--color-background);
+ color: var(--color-text);
}
@keyframes pulse {
@@ -19,3 +20,16 @@
.animate-pulse {
animation: pulse 2s ease-in-out infinite;
}
+
+/* Screen reader only text */
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
diff --git a/web/src/components/ThemeToggle.tsx b/web/src/components/ThemeToggle.tsx
new file mode 100644
index 0000000..c9d7a20
--- /dev/null
+++ b/web/src/components/ThemeToggle.tsx
@@ -0,0 +1,60 @@
+import { useTheme } from "next-themes";
+import { Button } from "@radix-ui/themes";
+import { Sun, Moon, Monitor } from "lucide-react";
+import { useMemo } from "react";
+
+export function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+
+ const nextTheme = useMemo(() => {
+ switch (theme) {
+ case "light":
+ return "dark";
+ case "dark":
+ return "system";
+ case "system":
+ return "light";
+ default:
+ console.error(`Invalid theme: ${theme}`);
+ return "system";
+ }
+ }, [theme]);
+
+ const icon = useMemo(() => {
+ if (nextTheme === "system") {
+ return ;
+ }
+ return nextTheme === "dark" ? : ;
+ }, [nextTheme]);
+
+ return (
+
+ );
+}
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts
index 9ea8ba8..9a4096c 100644
--- a/web/src/lib/api.ts
+++ b/web/src/lib/api.ts
@@ -60,4 +60,4 @@ export class BannerApiClient {
}
// Export a default instance
-export const apiClient = new BannerApiClient();
+export const client = new BannerApiClient();
diff --git a/web/src/main.tsx b/web/src/main.tsx
index 1b09369..24d5cc8 100644
--- a/web/src/main.tsx
+++ b/web/src/main.tsx
@@ -1,42 +1,53 @@
-import { StrictMode } from 'react'
-import ReactDOM from 'react-dom/client'
-import { RouterProvider, createRouter } from '@tanstack/react-router'
+import { StrictMode } from "react";
+import ReactDOM from "react-dom/client";
+import { RouterProvider, createRouter } from "@tanstack/react-router";
+import { ThemeProvider } from "next-themes";
+import { Theme } from "@radix-ui/themes";
// Import the generated route tree
-import { routeTree } from './routeTree.gen'
+import { routeTree } from "./routeTree.gen";
-import './styles.css'
-import reportWebVitals from './reportWebVitals.ts'
+import "./styles.css";
+import reportWebVitals from "./reportWebVitals.ts";
// Create a new router instance
const router = createRouter({
routeTree,
context: {},
- defaultPreload: 'intent',
+ defaultPreload: "intent",
scrollRestoration: true,
defaultStructuralSharing: true,
defaultPreloadStaleTime: 0,
-})
+});
// Register the router instance for type safety
-declare module '@tanstack/react-router' {
+declare module "@tanstack/react-router" {
interface Register {
- router: typeof router
+ router: typeof router;
}
}
// Render the app
-const rootElement = document.getElementById('app')
+const rootElement = document.getElementById("app");
if (rootElement && !rootElement.innerHTML) {
- const root = ReactDOM.createRoot(rootElement)
+ const root = ReactDOM.createRoot(rootElement);
root.render(
-
- ,
- )
+
+
+
+
+
+
+ );
}
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
-reportWebVitals()
+reportWebVitals();
diff --git a/web/src/routes/__root.tsx b/web/src/routes/__root.tsx
index 051500d..17d5e61 100644
--- a/web/src/routes/__root.tsx
+++ b/web/src/routes/__root.tsx
@@ -3,22 +3,30 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { TanstackDevtools } from "@tanstack/react-devtools";
import { Theme } from "@radix-ui/themes";
import "@radix-ui/themes/styles.css";
+import { ThemeProvider } from "next-themes";
export const Route = createRootRoute({
component: () => (
-
-
- ,
- },
- ]}
- />
-
+
+
+
+ ,
+ },
+ ]}
+ />
+
+
),
});
diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx
index 948864b..c31418f 100644
--- a/web/src/routes/index.tsx
+++ b/web/src/routes/index.tsx
@@ -1,6 +1,6 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect } from "react";
-import { apiClient, type StatusResponse, type Status } from "../lib/api";
+import { client, type StatusResponse, type Status } from "../lib/api";
import { Card, Flex, Text, Tooltip, Skeleton } from "@radix-ui/themes";
import {
CheckCircle,
@@ -15,6 +15,7 @@ import {
WifiOff,
} from "lucide-react";
import TimeAgo from "react-timeago";
+import { ThemeToggle } from "../components/ThemeToggle";
import "../App.css";
export const Route = createFileRoute("/")({
@@ -211,7 +212,7 @@ function App() {
// Race between the API call and timeout
const statusData = await Promise.race([
- apiClient.getStatus(),
+ client.getStatus(),
timeoutPromise,
]);
@@ -262,6 +263,18 @@ function App() {
return (
+ {/* Theme Toggle - Fixed position in top right */}
+
+
+
+