mirror of
https://github.com/Xevion/banner.git
synced 2026-01-31 14:23:36 -06:00
refactor: extract theme toggle styles to CSS and improve timeout handling
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user