refactor: extract theme toggle styles to CSS and improve timeout handling

This commit is contained in:
2026-01-28 19:47:24 -06:00
parent 7cc8267c2e
commit fa2fc45aa9
12 changed files with 88 additions and 126 deletions
+20
View File
@@ -20,6 +20,26 @@
animation: pulse 2s ease-in-out infinite;
}
/* Theme toggle button */
.theme-toggle {
cursor: pointer;
background-color: transparent;
border: none;
margin: 4px;
padding: 7px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-11);
transition: background-color 0.2s, color 0.2s;
transform: scale(1.25);
}
.theme-toggle:hover {
background-color: var(--gray-4);
}
/* Screen reader only text */
.sr-only {
position: absolute;
+3 -27
View File
@@ -1,6 +1,6 @@
import { useTheme } from "next-themes";
import { Button } from "@radix-ui/themes";
import { Sun, Moon, Monitor } from "lucide-react";
import { Monitor, Moon, Sun } from "lucide-react";
import { useTheme } from "next-themes";
import { useMemo } from "react";
export function ThemeToggle() {
@@ -28,31 +28,7 @@ export function ThemeToggle() {
}, [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";
}}
>
<Button variant="ghost" size="3" onClick={() => setTheme(nextTheme)} className="theme-toggle">
{icon}
<span className="sr-only">Toggle theme</span>
</Button>
+4 -4
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { BannerApiClient } from "./api";
// Mock fetch
@@ -31,12 +31,12 @@ describe("BannerApiClient", () => {
it("should fetch status data", async () => {
const mockStatus = {
status: "active",
status: "active" as const,
version: "0.3.4",
commit: "abc1234",
services: {
web: { name: "web", status: "active" },
database: { name: "database", status: "connected" },
web: { name: "web", status: "active" as const },
database: { name: "database", status: "connected" as const },
},
};
+29 -12
View File
@@ -1,21 +1,21 @@
import { Card, Flex, Skeleton, Text, Tooltip } from "@radix-ui/themes";
import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect } from "react";
import { client, type StatusResponse, type Status } from "../lib/api";
import { Card, Flex, Text, Tooltip, Skeleton } from "@radix-ui/themes";
import {
CheckCircle,
XCircle,
Clock,
Activity,
Bot,
CheckCircle,
Circle,
Clock,
Globe,
Hourglass,
Activity,
MessageCircle,
Circle,
WifiOff,
XCircle,
} from "lucide-react";
import { useEffect, useState } from "react";
import TimeAgo from "react-timeago";
import { ThemeToggle } from "../components/ThemeToggle";
import { type Status, type StatusResponse, client } from "../lib/api";
import "../App.css";
const REFRESH_INTERVAL = import.meta.env.DEV ? 3000 : 30000;
@@ -190,20 +190,29 @@ function App() {
const shouldShowLastFetch = hasResponse || hasError || hasTimeout;
useEffect(() => {
let timeoutId: NodeJS.Timeout;
let timeoutId: NodeJS.Timeout | null = null;
let requestTimeoutId: NodeJS.Timeout | null = null;
const fetchData = async () => {
try {
const startTime = Date.now();
// Create a timeout promise
// Create a timeout promise with cleanup tracking
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error("Request timeout")), REQUEST_TIMEOUT);
requestTimeoutId = setTimeout(() => {
reject(new Error("Request timeout"));
}, REQUEST_TIMEOUT);
});
// Race between the API call and timeout
const statusData = await Promise.race([client.getStatus(), timeoutPromise]);
// Clear the timeout if the request succeeded
if (requestTimeoutId) {
clearTimeout(requestTimeoutId);
requestTimeoutId = null;
}
const endTime = Date.now();
const responseTime = endTime - startTime;
@@ -214,9 +223,14 @@ function App() {
lastFetch: new Date(),
});
} catch (err) {
// Clear the timeout on error as well
if (requestTimeoutId) {
clearTimeout(requestTimeoutId);
requestTimeoutId = null;
}
const errorMessage = err instanceof Error ? err.message : "Failed to fetch data";
// Check if it's a timeout error
if (errorMessage === "Request timeout") {
setState({
mode: "timeout",
@@ -241,6 +255,9 @@ function App() {
if (timeoutId) {
clearTimeout(timeoutId);
}
if (requestTimeoutId) {
clearTimeout(requestTimeoutId);
}
};
}, []);