mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 13:48:31 +00:00
feat: add button for toggle theme
This commit is contained in:
+20
@@ -5,7 +5,27 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="Upwatch — fast, dependable uptime monitoring from Cloudflare's edge." />
|
||||
<meta name="robots" content="noindex,nofollow" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<script>
|
||||
(() => {
|
||||
try {
|
||||
const storedTheme = localStorage.getItem('upwatch-theme');
|
||||
const theme =
|
||||
storedTheme === 'light' || storedTheme === 'dark'
|
||||
? storedTheme
|
||||
: matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
const root = document.documentElement;
|
||||
root.classList.toggle('dark', theme === 'dark');
|
||||
root.style.colorScheme = theme;
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', theme === 'dark' ? '#171717' : '#ffffff');
|
||||
} catch {
|
||||
// Keep the default light theme when browser storage is unavailable.
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Activity, LogOut, Menu, Settings, Zap, type LucideIcon } from 'lucide-r
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ThemeToggle } from './ThemeToggle';
|
||||
import { navigate, usePathname } from '../lib/router';
|
||||
import { useLogoutMutation } from '../queries/auth';
|
||||
|
||||
@@ -54,6 +55,7 @@ export function AppHeader({ context }: { context?: string }) {
|
||||
<TooltipProvider>
|
||||
<nav className="nav-actions" aria-label="Primary navigation">
|
||||
{context && <span className="header-context">{context}</span>}
|
||||
<ThemeToggle className="app-nav-icon" />
|
||||
{items.map((item) => {
|
||||
const ItemIcon = item.icon;
|
||||
const isCurrent = item.href === pathname;
|
||||
@@ -97,6 +99,7 @@ export function AppHeader({ context }: { context?: string }) {
|
||||
{context && <SheetDescription>{context}</SheetDescription>}
|
||||
</SheetHeader>
|
||||
<nav className="app-nav-mobile" aria-label="Primary navigation">
|
||||
<ThemeToggle className="app-nav-mobile-item" showLabel />
|
||||
{items.map((item) => {
|
||||
const ItemIcon = item.icon;
|
||||
const isCurrent = item.href === pathname;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useTheme } from '../lib/theme';
|
||||
|
||||
type ThemeToggleProps = {
|
||||
className?: string;
|
||||
showLabel?: boolean;
|
||||
};
|
||||
|
||||
export function ThemeToggle({ className = '', showLabel = false }: ThemeToggleProps) {
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const isDark = theme === 'dark';
|
||||
const label = isDark ? 'Switch to light mode' : 'Switch to dark mode';
|
||||
const Icon = isDark ? Sun : Moon;
|
||||
|
||||
if (showLabel) {
|
||||
return (
|
||||
<Button variant="unstyled" className={className} type="button" onClick={toggleTheme} aria-label={label}>
|
||||
<Icon aria-hidden="true" />
|
||||
<span>{label}</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className={className} type="button" onClick={toggleTheme} aria-label={label}>
|
||||
<Icon aria-hidden="true" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent sideOffset={6}>{label}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
export type Theme = 'light' | 'dark';
|
||||
|
||||
const THEME_STORAGE_KEY = 'upwatch-theme';
|
||||
const listeners = new Set<() => void>();
|
||||
let currentTheme: Theme = 'light';
|
||||
|
||||
function getPreferredTheme(): Theme {
|
||||
try {
|
||||
const storedTheme = window.localStorage.getItem(THEME_STORAGE_KEY);
|
||||
if (storedTheme === 'light' || storedTheme === 'dark') return storedTheme;
|
||||
} catch {
|
||||
// Storage can be unavailable in privacy or sandboxed contexts.
|
||||
}
|
||||
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
function applyTheme(theme: Theme) {
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
document.documentElement.style.colorScheme = theme;
|
||||
document.querySelector<HTMLMetaElement>('meta[name="theme-color"]')?.setAttribute('content', theme === 'dark' ? '#171717' : '#ffffff');
|
||||
}
|
||||
|
||||
export function initializeTheme() {
|
||||
currentTheme = getPreferredTheme();
|
||||
applyTheme(currentTheme);
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void) {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
function getThemeSnapshot() {
|
||||
return currentTheme;
|
||||
}
|
||||
|
||||
export function useTheme() {
|
||||
const theme = useSyncExternalStore(subscribe, getThemeSnapshot, () => 'light');
|
||||
function toggleTheme() {
|
||||
const nextTheme = theme === 'dark' ? 'light' : 'dark';
|
||||
currentTheme = nextTheme;
|
||||
applyTheme(nextTheme);
|
||||
listeners.forEach((listener) => listener());
|
||||
try {
|
||||
window.localStorage.setItem(THEME_STORAGE_KEY, nextTheme);
|
||||
} catch {
|
||||
// The active theme still works for this page when persistence is unavailable.
|
||||
}
|
||||
}
|
||||
|
||||
return { theme, toggleTheme };
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import { queryClient } from './lib/query-client';
|
||||
import { initializeTheme } from './lib/theme';
|
||||
import './styles.css';
|
||||
|
||||
const root = document.getElementById('root');
|
||||
@@ -11,6 +12,8 @@ if (!root) {
|
||||
throw new Error('Root element was not found');
|
||||
}
|
||||
|
||||
initializeTheme();
|
||||
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ArrowLeft, TriangleAlert, Zap } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { IncidentTimeline } from '../components/IncidentTimeline';
|
||||
import { ThemeToggle } from '../components/ThemeToggle';
|
||||
import { navigate } from '../lib/router';
|
||||
import { useSeo } from '../lib/seo';
|
||||
import { usePublicIncidentQuery } from '../queries/status';
|
||||
@@ -32,10 +33,13 @@ export function IncidentDetailPage({ id }: { id: number }) {
|
||||
<a className="brand" href="/" aria-label="Upwatch public status">
|
||||
<Zap className="brand-mark" fill="currentColor" /> <span>upwatch</span>
|
||||
</a>
|
||||
<div className="status-header-actions">
|
||||
<ThemeToggle className="app-nav-icon" />
|
||||
<Button variant="unstyled" className="status-header-action" onClick={() => navigate('/')}>
|
||||
Status page
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main className="status-main incident-detail-page">
|
||||
<button className="incident-back" type="button" onClick={() => navigate('/')}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Zap } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ThemeToggle } from '../components/ThemeToggle';
|
||||
import { navigate } from '../lib/router';
|
||||
import { useSeo } from '../lib/seo';
|
||||
import { useLoginMutation, useSessionQuery } from '../queries/auth';
|
||||
@@ -26,6 +27,7 @@ export function LoginPage() {
|
||||
|
||||
return (
|
||||
<main className="auth-page">
|
||||
<ThemeToggle className="auth-theme-toggle app-nav-icon" />
|
||||
<div className="auth-brand" aria-label="Upwatch">
|
||||
<Zap className="auth-brand-mark" fill="currentColor" />
|
||||
<span>upwatch</span>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Empty, EmptyContent, EmptyDescription, EmptyMedia, EmptyTitle } from '@
|
||||
import type { PublicOverallStatus, PublicServiceStatus } from '../api/status';
|
||||
import { SiteIcon } from '../components/SiteIcon';
|
||||
import { StatusHistoryBar } from '../components/StatusHistoryBar';
|
||||
import { ThemeToggle } from '../components/ThemeToggle';
|
||||
import { formatDate, formatDuration } from '../lib/format';
|
||||
import { navigate } from '../lib/router';
|
||||
import { useSeo } from '../lib/seo';
|
||||
@@ -96,6 +97,8 @@ export function StatusPage() {
|
||||
<Zap className="brand-mark" fill="currentColor" />
|
||||
<span>upwatch</span>
|
||||
</a>
|
||||
<div className="status-header-actions">
|
||||
<ThemeToggle className="app-nav-icon" />
|
||||
<Button
|
||||
variant="unstyled"
|
||||
className="status-header-action"
|
||||
@@ -105,6 +108,7 @@ export function StatusPage() {
|
||||
{sessionQuery.data?.authenticated ? 'Dashboard' : 'Sign in'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="status-main">
|
||||
|
||||
@@ -873,6 +873,7 @@ button {
|
||||
}
|
||||
|
||||
.auth-page {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
justify-items: center;
|
||||
@@ -880,6 +881,12 @@ button {
|
||||
padding: 32px 24px 24px;
|
||||
background: radial-gradient(circle at 50% 38%, rgb(62 207 142 / 0.08), transparent 34%), linear-gradient(#fff, #fcfcfc);
|
||||
}
|
||||
.auth-theme-toggle {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 28px;
|
||||
right: 28px;
|
||||
}
|
||||
.auth-page::before {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
@@ -2102,6 +2109,11 @@ button {
|
||||
.status-header-inner {
|
||||
width: min(960px, calc(100% - 48px));
|
||||
}
|
||||
.status-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.status-header-action {
|
||||
min-height: 34px;
|
||||
padding: 6px 13px;
|
||||
|
||||
Reference in New Issue
Block a user