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 */} +
+ +
+