mirror of
https://github.com/Xevion/banner.git
synced 2025-12-06 05:14:26 -06:00
feat: dark mode with theme toggle button
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
"@tanstack/react-router-devtools": "^1.131.5",
|
"@tanstack/react-router-devtools": "^1.131.5",
|
||||||
"@tanstack/router-plugin": "^1.121.2",
|
"@tanstack/router-plugin": "^1.121.2",
|
||||||
"lucide-react": "^0.544.0",
|
"lucide-react": "^0.544.0",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-timeago": "^8.3.0",
|
"react-timeago": "^8.3.0",
|
||||||
|
|||||||
14
web/pnpm-lock.yaml
generated
14
web/pnpm-lock.yaml
generated
@@ -26,6 +26,9 @@ importers:
|
|||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.544.0
|
specifier: ^0.544.0
|
||||||
version: 0.544.0(react@19.1.1)
|
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:
|
react:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.1.1
|
version: 19.1.1
|
||||||
@@ -1865,6 +1868,12 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
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:
|
node-releases@2.0.21:
|
||||||
resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
|
resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==}
|
||||||
|
|
||||||
@@ -4133,6 +4142,11 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
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: {}
|
node-releases@2.0.21: {}
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
.App {
|
.App {
|
||||||
background-color: #f8fafc;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
font-family:
|
font-family:
|
||||||
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||||
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
@@ -19,3 +20,16 @@
|
|||||||
.animate-pulse {
|
.animate-pulse {
|
||||||
animation: pulse 2s ease-in-out infinite;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
60
web/src/components/ThemeToggle.tsx
Normal file
60
web/src/components/ThemeToggle.tsx
Normal file
@@ -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 <Monitor size={18} />;
|
||||||
|
}
|
||||||
|
return nextTheme === "dark" ? <Moon size={18} /> : <Sun size={18} />;
|
||||||
|
}, [nextTheme]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="3"
|
||||||
|
onClick={() => setTheme(nextTheme)}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
border: "none",
|
||||||
|
margin: "4px",
|
||||||
|
padding: "7px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "var(--gray-11)",
|
||||||
|
transition: "background-color 0.2s, color 0.2s",
|
||||||
|
transform: "scale(1.25)",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = "var(--gray-4)";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = "transparent";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -60,4 +60,4 @@ export class BannerApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Export a default instance
|
// Export a default instance
|
||||||
export const apiClient = new BannerApiClient();
|
export const client = new BannerApiClient();
|
||||||
|
|||||||
@@ -1,42 +1,53 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from "react";
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from "react-dom/client";
|
||||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
import { Theme } from "@radix-ui/themes";
|
||||||
|
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
import { routeTree } from './routeTree.gen'
|
import { routeTree } from "./routeTree.gen";
|
||||||
|
|
||||||
import './styles.css'
|
import "./styles.css";
|
||||||
import reportWebVitals from './reportWebVitals.ts'
|
import reportWebVitals from "./reportWebVitals.ts";
|
||||||
|
|
||||||
// Create a new router instance
|
// Create a new router instance
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
routeTree,
|
routeTree,
|
||||||
context: {},
|
context: {},
|
||||||
defaultPreload: 'intent',
|
defaultPreload: "intent",
|
||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
defaultStructuralSharing: true,
|
defaultStructuralSharing: true,
|
||||||
defaultPreloadStaleTime: 0,
|
defaultPreloadStaleTime: 0,
|
||||||
})
|
});
|
||||||
|
|
||||||
// Register the router instance for type safety
|
// Register the router instance for type safety
|
||||||
declare module '@tanstack/react-router' {
|
declare module "@tanstack/react-router" {
|
||||||
interface Register {
|
interface Register {
|
||||||
router: typeof router
|
router: typeof router;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render the app
|
// Render the app
|
||||||
const rootElement = document.getElementById('app')
|
const rootElement = document.getElementById("app");
|
||||||
if (rootElement && !rootElement.innerHTML) {
|
if (rootElement && !rootElement.innerHTML) {
|
||||||
const root = ReactDOM.createRoot(rootElement)
|
const root = ReactDOM.createRoot(rootElement);
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange={false}
|
||||||
|
>
|
||||||
|
<Theme>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</StrictMode>,
|
</Theme>
|
||||||
)
|
</ThemeProvider>
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you want to start measuring performance in your app, pass a function
|
// If you want to start measuring performance in your app, pass a function
|
||||||
// to log results (for example: reportWebVitals(console.log))
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
reportWebVitals()
|
reportWebVitals();
|
||||||
|
|||||||
@@ -3,10 +3,17 @@ import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
|
|||||||
import { TanstackDevtools } from "@tanstack/react-devtools";
|
import { TanstackDevtools } from "@tanstack/react-devtools";
|
||||||
import { Theme } from "@radix-ui/themes";
|
import { Theme } from "@radix-ui/themes";
|
||||||
import "@radix-ui/themes/styles.css";
|
import "@radix-ui/themes/styles.css";
|
||||||
|
import { ThemeProvider } from "next-themes";
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: () => (
|
component: () => (
|
||||||
<Theme appearance="light" accentColor="blue" grayColor="gray">
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
disableTransitionOnChange={false}
|
||||||
|
>
|
||||||
|
<Theme accentColor="blue" grayColor="gray">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<TanstackDevtools
|
<TanstackDevtools
|
||||||
config={{
|
config={{
|
||||||
@@ -20,5 +27,6 @@ export const Route = createRootRoute({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</Theme>
|
</Theme>
|
||||||
|
</ThemeProvider>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createFileRoute } from "@tanstack/react-router";
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
import { useState, useEffect } from "react";
|
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 { Card, Flex, Text, Tooltip, Skeleton } from "@radix-ui/themes";
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
WifiOff,
|
WifiOff,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import TimeAgo from "react-timeago";
|
import TimeAgo from "react-timeago";
|
||||||
|
import { ThemeToggle } from "../components/ThemeToggle";
|
||||||
import "../App.css";
|
import "../App.css";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
@@ -211,7 +212,7 @@ function App() {
|
|||||||
|
|
||||||
// Race between the API call and timeout
|
// Race between the API call and timeout
|
||||||
const statusData = await Promise.race([
|
const statusData = await Promise.race([
|
||||||
apiClient.getStatus(),
|
client.getStatus(),
|
||||||
timeoutPromise,
|
timeoutPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -262,6 +263,18 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
{/* Theme Toggle - Fixed position in top right */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "fixed",
|
||||||
|
top: "20px",
|
||||||
|
right: "20px",
|
||||||
|
zIndex: 1000,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
direction="column"
|
direction="column"
|
||||||
align="center"
|
align="center"
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
|
@import "@radix-ui/themes/styles.css";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
font-family:
|
||||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
|
||||||
sans-serif;
|
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
font-family:
|
||||||
monospace;
|
source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user