theme switcher cleanup + stuff

This commit is contained in:
lda
2026-04-30 22:24:30 +07:00 Verified
parent 42af1899b4
commit 58511e0f00
4 changed files with 25 additions and 79 deletions
+4 -1
View File
@@ -1,3 +1,4 @@
import { useTheme } from "next-themes";
import { Toaster as Sonner, type ToasterProps } from "sonner"; import { Toaster as Sonner, type ToasterProps } from "sonner";
import { import {
CircleCheckIcon, CircleCheckIcon,
@@ -8,9 +9,11 @@ import {
} from "lucide-react"; } from "lucide-react";
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme();
return ( return (
<Sonner <Sonner
theme="system" theme={theme as ToasterProps["theme"]}
className="toaster group" className="toaster group"
icons={{ icons={{
success: <CircleCheckIcon className="size-4" />, success: <CircleCheckIcon className="size-4" />,
+8 -29
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { NavLink, Outlet, useLocation } from "react-router-dom"; import { NavLink, Outlet, useLocation } from "react-router-dom";
import { useTheme } from "next-themes";
import { import {
Moon, Moon,
Sun, Sun,
@@ -62,19 +63,6 @@ const navSections: NavSection[] = [
}, },
]; ];
type Theme = "light" | "dark";
const THEME_KEY = "wakey-ui-theme";
function resolveInitialTheme(): Theme {
const stored = window.localStorage.getItem(THEME_KEY);
if (stored === "light" || stored === "dark") return stored;
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}
function usePageTitle() { function usePageTitle() {
const { pathname } = useLocation(); const { pathname } = useLocation();
useEffect(() => { useEffect(() => {
@@ -89,19 +77,14 @@ function usePageTitle() {
} }
export function AppLayout() { export function AppLayout() {
const [theme, setTheme] = useState<Theme>(() => resolveInitialTheme()); const { resolvedTheme, setTheme } = useTheme();
const isDark = resolvedTheme === "dark";
const [collapsed, setCollapsed] = useState(() => { const [collapsed, setCollapsed] = useState(() => {
return window.localStorage.getItem(SIDEBAR_KEY) === "true"; return window.localStorage.getItem(SIDEBAR_KEY) === "true";
}); });
usePageTitle(); usePageTitle();
useEffect(() => {
const root = document.documentElement;
root.classList.toggle("dark", theme === "dark");
window.localStorage.setItem(THEME_KEY, theme);
}, [theme]);
useEffect(() => { useEffect(() => {
window.localStorage.setItem(SIDEBAR_KEY, String(collapsed)); window.localStorage.setItem(SIDEBAR_KEY, String(collapsed));
}, [collapsed]); }, [collapsed]);
@@ -180,27 +163,23 @@ export function AppLayout() {
variant="ghost" variant="ghost"
size="sm" size="sm"
className="sidebar-footer-btn" className="sidebar-footer-btn"
aria-label={ aria-label={isDark ? "Switch to light" : "Switch to dark"}
theme === "dark" ? "Switch to light" : "Switch to dark" onClick={() => setTheme(isDark ? "light" : "dark")}
}
onClick={() =>
setTheme((c) => (c === "dark" ? "light" : "dark"))
}
> >
{theme === "dark" ? ( {isDark ? (
<Sun className="size-4" aria-hidden /> <Sun className="size-4" aria-hidden />
) : ( ) : (
<Moon className="size-4" aria-hidden /> <Moon className="size-4" aria-hidden />
)} )}
{!collapsed && ( {!collapsed && (
<span className="sidebar-link-label"> <span className="sidebar-link-label">
{theme === "dark" ? "Light mode" : "Dark mode"} {isDark ? "Light mode" : "Dark mode"}
</span> </span>
)} )}
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="right"> <TooltipContent side="right">
{theme === "dark" ? "Switch to light" : "Switch to dark"} {isDark ? "Switch to light" : "Switch to dark"}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
+11 -3
View File
@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { ThemeProvider } from "next-themes";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { App } from "@/App"; import { App } from "@/App";
import "./styles.css"; import "./styles.css";
@@ -8,9 +9,16 @@ import "./styles.css";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter basename="/ui"> <BrowserRouter basename="/ui">
<TooltipProvider> <ThemeProvider
<App /> attribute="class"
</TooltipProvider> defaultTheme="system"
storageKey="wakey-ui-theme"
enableSystem
>
<TooltipProvider>
<App />
</TooltipProvider>
</ThemeProvider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode>, </React.StrictMode>,
); );
+2 -46
View File
@@ -1,12 +1,5 @@
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { import { Inbox, MoreHorizontal, RefreshCw, Search, Zap } from "lucide-react";
Check,
Inbox,
MoreHorizontal,
RefreshCw,
Search,
Zap,
} from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -74,14 +67,10 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [wakeBusyKey, setWakeBusyKey] = useState(""); const [wakeBusyKey, setWakeBusyKey] = useState("");
const [error, setError] = useState("");
const [status, setStatus] = useState("");
const [copied, setCopied] = useState("");
const [details, setDetails] = useState<FleetDevice | null>(null); const [details, setDetails] = useState<FleetDevice | null>(null);
async function loadFleet() { async function loadFleet() {
setLoading(true); setLoading(true);
setError("");
try { try {
const [nextDevices, nextKnownDevices] = await Promise.all([ const [nextDevices, nextKnownDevices] = await Promise.all([
fetchFleetDevices({ fetchFleetDevices({
@@ -96,7 +85,7 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
setDevices(nextDevices); setDevices(nextDevices);
setKnownDevices(nextKnownDevices); setKnownDevices(nextKnownDevices);
} catch (err) { } catch (err) {
setError(String(err)); toast.error("Failed to load fleet", { description: String(err) });
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -104,19 +93,15 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
async function refreshAllConnected() { async function refreshAllConnected() {
setRefreshing(true); setRefreshing(true);
setStatus("");
setError("");
try { try {
const result = await refreshFleetDevices(); const result = await refreshFleetDevices();
const failed = result.agents.filter((agent) => agent.status !== "ok"); const failed = result.agents.filter((agent) => agent.status !== "ok");
const msg = failed.length const msg = failed.length
? `Stored ${result.total_accepted} observations; ${failed.length} agent refresh failed` ? `Stored ${result.total_accepted} observations; ${failed.length} agent refresh failed`
: `Stored ${result.total_accepted} observations from ${result.agents.length} connected agents`; : `Stored ${result.total_accepted} observations from ${result.agents.length} connected agents`;
setStatus(msg);
toast.success(msg); toast.success(msg);
await loadFleet(); await loadFleet();
} catch (err) { } catch (err) {
setError(String(err));
toast.error("Fleet refresh failed", { description: String(err) }); toast.error("Fleet refresh failed", { description: String(err) });
} finally { } finally {
setRefreshing(false); setRefreshing(false);
@@ -126,8 +111,6 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
async function wakeDevice(device: FleetDevice, routeId?: string | null) { async function wakeDevice(device: FleetDevice, routeId?: string | null) {
if (!device.recommended_route && !routeId) return; if (!device.recommended_route && !routeId) return;
setWakeBusyKey(device.device_key); setWakeBusyKey(device.device_key);
setStatus("");
setError("");
try { try {
const result = await wakeFleetDevice({ const result = await wakeFleetDevice({
deviceKey: device.device_key, deviceKey: device.device_key,
@@ -135,11 +118,9 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
}); });
const msg = `Wake sent via ${agentLabel(result.route.agent_id, result.route.nickname)} (${result.route.mac ?? "no mac"})`; const msg = `Wake sent via ${agentLabel(result.route.agent_id, result.route.nickname)} (${result.route.mac ?? "no mac"})`;
toast.success(msg); toast.success(msg);
setStatus(msg);
if (onAfterWake) await onAfterWake(); if (onAfterWake) await onAfterWake();
await loadFleet(); await loadFleet();
} catch (err) { } catch (err) {
setError(String(err));
toast.error("Wake failed", { description: String(err) }); toast.error("Wake failed", { description: String(err) });
} finally { } finally {
setWakeBusyKey(""); setWakeBusyKey("");
@@ -151,7 +132,6 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
if (!text) return; if (!text) return;
try { try {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
setCopied(label);
toast.success(`Copied ${label}`); toast.success(`Copied ${label}`);
} catch { } catch {
const area = document.createElement("textarea"); const area = document.createElement("textarea");
@@ -162,7 +142,6 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
area.select(); area.select();
document.execCommand("copy"); document.execCommand("copy");
document.body.removeChild(area); document.body.removeChild(area);
setCopied(label);
toast.success(`Copied ${label}`); toast.success(`Copied ${label}`);
} }
} }
@@ -178,12 +157,6 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]); }, [query]);
useEffect(() => {
if (!copied) return;
const timer = window.setTimeout(() => setCopied(""), 1400);
return () => window.clearTimeout(timer);
}, [copied]);
const sortedDevices = useMemo(() => { const sortedDevices = useMemo(() => {
const rows = [...devices]; const rows = [...devices];
rows.sort((a, b) => { rows.sort((a, b) => {
@@ -316,23 +289,6 @@ export function DevicesPage({ agents, onAfterWake }: Props) {
/> />
</div> </div>
{status && (
<div className="rounded-md border bg-muted/50 px-3 py-2 text-sm">
{status}
</div>
)}
{error && (
<pre className="max-h-80 overflow-auto rounded-md border border-destructive/60 bg-destructive/10 p-3 text-xs text-destructive">
{error}
</pre>
)}
{copied && (
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Check className="size-4" aria-hidden />
Copied {copied}
</div>
)}
{/* Skeleton loading */} {/* Skeleton loading */}
{loading && !devices.length ? ( {loading && !devices.length ? (
<div className="grid gap-2"> <div className="grid gap-2">