feat: init

This commit is contained in:
hamster1963 2024-11-23 16:34:34 +08:00
parent e5682aacbd
commit 65902d5385
11 changed files with 122 additions and 98 deletions

View File

@ -24,7 +24,7 @@ jobs:
- name: Build static export - name: Build static export
run: | run: |
bun run build --base=/dashboard bun run build
- name: Compress dist folder - name: Compress dist folder
run: zip -r dist.zip dist run: zip -r dist.zip dist

View File

@ -8,15 +8,10 @@ const App: React.FC = () => {
return ( return (
<Router basename={import.meta.env.BASE_URL}> <Router basename={import.meta.env.BASE_URL}>
<div className="flex min-h-screen w-full flex-col"> <div className="flex min-h-screen w-full flex-col">
<main className="flex min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 bg-background p-4 md:p-10 md:pt-8"> <main className="flex min-h-[calc(100vh-calc(var(--spacing)*16))] flex-1 flex-col gap-4 bg-background p-4 md:p-10 md:pt-8">
<Header /> <Header />
<Routes> <Routes>
<Route <Route path="/" element={<Server />} />
path="/"
element={
<Server />
}
/>
</Routes> </Routes>
<Footer /> <Footer />
</main> </main>

View File

@ -8,13 +8,10 @@ import { DateTime } from "luxon";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
function Header() { function Header() {
return ( return (
<div className="mx-auto w-full max-w-5xl"> <div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between"> <section className="flex items-center justify-between">
<section <section className="flex items-center text-base font-medium">
className="flex items-center text-base font-medium"
>
<div className="mr-1 flex flex-row items-center justify-start"> <div className="mr-1 flex flex-row items-center justify-start">
<img <img
width={40} width={40}
@ -45,7 +42,7 @@ function Header() {
// https://github.com/streamich/react-use/blob/master/src/useInterval.ts // https://github.com/streamich/react-use/blob/master/src/useInterval.ts
const useInterval = (callback: () => void, delay: number | null) => { const useInterval = (callback: () => void, delay: number | null) => {
const savedCallback = useRef<() => void>(() => { }); const savedCallback = useRef<() => void>(() => {});
useEffect(() => { useEffect(() => {
savedCallback.current = callback; savedCallback.current = callback;
}); });
@ -74,9 +71,7 @@ function Overview() {
<section className={"mt-10 flex flex-col md:mt-16"}> <section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">👋 Overview</p> <p className="text-base font-semibold">👋 Overview</p>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<p className="text-sm font-medium opacity-50"> <p className="text-sm font-medium opacity-50">where the time is</p>
where the time is
</p>
{mouted ? ( {mouted ? (
<p className="text-sm font-medium">{timeString}</p> <p className="text-sm font-medium">{timeString}</p>
) : ( ) : (

View File

@ -8,7 +8,6 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { Moon, Sun } from "lucide-react"; import { Moon, Sun } from "lucide-react";
import { Theme } from "@/components/ThemeProvider"; import { Theme } from "@/components/ThemeProvider";
import { useTheme } from "../hooks/use-theme"; import { useTheme } from "../hooks/use-theme";
@ -40,21 +39,18 @@ export function ModeToggle() {
onSelect={(e) => handleSelect(e, "light")} onSelect={(e) => handleSelect(e, "light")}
> >
Light Light
{theme === "light" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className={cn({ "gap-3 bg-muted": theme === "dark" })} className={cn({ "gap-3 bg-muted": theme === "dark" })}
onSelect={(e) => handleSelect(e, "dark")} onSelect={(e) => handleSelect(e, "dark")}
> >
Dark Dark
{theme === "dark" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className={cn({ "gap-3 bg-muted": theme === "system" })} className={cn({ "gap-3 bg-muted": theme === "system" })}
onSelect={(e) => handleSelect(e, "system")} onSelect={(e) => handleSelect(e, "system")}
> >
System System
{theme === "system" && <CheckCircleIcon className="size-4" />}
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>

View File

@ -1,7 +1,7 @@
import * as React from "react" import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator" import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
const Separator = React.forwardRef< const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>, React.ElementRef<typeof SeparatorPrimitive.Root>,
@ -9,7 +9,7 @@ const Separator = React.forwardRef<
>( >(
( (
{ className, orientation = "horizontal", decorative = true, ...props }, { className, orientation = "horizontal", decorative = true, ...props },
ref ref,
) => ( ) => (
<SeparatorPrimitive.Root <SeparatorPrimitive.Root
ref={ref} ref={ref}
@ -18,12 +18,12 @@ const Separator = React.forwardRef<
className={cn( className={cn(
"shrink-0 bg-border", "shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className className,
)} )}
{...props} {...props}
/> />
) ),
) );
Separator.displayName = SeparatorPrimitive.Root.displayName Separator.displayName = SeparatorPrimitive.Root.displayName;
export { Separator } export { Separator };

View File

@ -1,4 +1,4 @@
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils";
function Skeleton({ function Skeleton({
className, className,
@ -9,7 +9,7 @@ function Skeleton({
className={cn("animate-pulse rounded-md bg-muted", className)} className={cn("animate-pulse rounded-md bg-muted", className)}
{...props} {...props}
/> />
) );
} }
export { Skeleton } export { Skeleton };

View File

@ -1,89 +1,89 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from "react";
export interface WebSocketHook { export interface WebSocketHook {
socket: WebSocket | null socket: WebSocket | null;
connected: boolean connected: boolean;
onlineCount: number onlineCount: number;
message: string | null message: string | null;
sendMessage: (msg: string) => void sendMessage: (msg: string) => void;
} }
export default function useWebSocket(url: string): WebSocketHook { export default function useWebSocket(url: string): WebSocketHook {
const [socket, setSocket] = useState<WebSocket | null>(null) const [socket, setSocket] = useState<WebSocket | null>(null);
const [message, setMessage] = useState<string | null>(null) const [message, setMessage] = useState<string | null>(null);
const [connected, setConnected] = useState<boolean>(false) const [connected, setConnected] = useState<boolean>(false);
const [onlineCount, setOnlineCount] = useState<number>(0) const [onlineCount, setOnlineCount] = useState<number>(0);
const socketRef = useRef<WebSocket | null>(null) const socketRef = useRef<WebSocket | null>(null);
const reconnectAttempts = useRef<number>(0) const reconnectAttempts = useRef<number>(0);
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null) const reconnectTimeout = useRef<NodeJS.Timeout | null>(null);
const isUnmounted = useRef<boolean>(false) const isUnmounted = useRef<boolean>(false);
const connect = useCallback(() => { const connect = useCallback(() => {
if (isUnmounted.current) return if (isUnmounted.current) return;
const ws = new WebSocket(url) const ws = new WebSocket(url);
setSocket(ws) setSocket(ws);
socketRef.current = ws socketRef.current = ws;
ws.onopen = () => { ws.onopen = () => {
setConnected(true) setConnected(true);
reconnectAttempts.current = 0 reconnectAttempts.current = 0;
} };
ws.onmessage = (event: MessageEvent) => { ws.onmessage = (event: MessageEvent) => {
setMessage(event.data) setMessage(event.data);
const msgJson = JSON.parse(event.data) const msgJson = JSON.parse(event.data);
if (msgJson.type === 'live') { if (msgJson.type === "live") {
setOnlineCount(msgJson.data.count) setOnlineCount(msgJson.data.count);
} }
} };
ws.onerror = (error) => { ws.onerror = (error) => {
console.error('WebSocket Error:', error) console.error("WebSocket Error:", error);
} };
ws.onclose = () => { ws.onclose = () => {
setConnected(false) setConnected(false);
if (!isUnmounted.current) { if (!isUnmounted.current) {
// Attempt to reconnect // Attempt to reconnect
if (reconnectAttempts.current < 5) { if (reconnectAttempts.current < 5) {
const timeout = Math.pow(2, reconnectAttempts.current) * 1000 // Exponential backoff const timeout = Math.pow(2, reconnectAttempts.current) * 1000; // Exponential backoff
reconnectAttempts.current += 1 reconnectAttempts.current += 1;
reconnectTimeout.current = setTimeout(() => { reconnectTimeout.current = setTimeout(() => {
connect() connect();
}, timeout) }, timeout);
} else { } else {
console.warn('Max reconnect attempts reached.') console.warn("Max reconnect attempts reached.");
} }
} }
} };
}, [url]) }, [url]);
useEffect(() => { useEffect(() => {
connect() connect();
return () => { return () => {
isUnmounted.current = true isUnmounted.current = true;
if (socketRef.current) { if (socketRef.current) {
socketRef.current.close() socketRef.current.close();
} }
if (reconnectTimeout.current) { if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current) clearTimeout(reconnectTimeout.current);
} }
} };
}, [connect]) }, [connect]);
// Function to send messages // Function to send messages
const sendMessage = useCallback((msg: string) => { const sendMessage = useCallback((msg: string) => {
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) { if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
socketRef.current.send(msg) socketRef.current.send(msg);
} else { } else {
console.warn( console.warn(
'WebSocket is not open. Ready state:', "WebSocket is not open. Ready state:",
socketRef.current?.readyState socketRef.current?.readyState,
) );
} }
}, []) }, []);
return { socket, message, sendMessage, connected, onlineCount } return { socket, message, sendMessage, connected, onlineCount };
} }

View File

@ -1,26 +1,25 @@
import { createContext, useContext, ReactNode } from 'react' import { createContext, useContext, ReactNode } from "react";
import useWebSocket, { WebSocketHook } from './useWebsocket' import useWebSocket, { WebSocketHook } from "./useWebsocket";
interface WebSocketProviderProps { interface WebSocketProviderProps {
children: ReactNode children: ReactNode;
} }
const WebSocketContext = createContext<WebSocketHook | undefined>(undefined) const WebSocketContext = createContext<WebSocketHook | undefined>(undefined);
export const WebSocketProvider = ({ children }: WebSocketProviderProps) => { export const WebSocketProvider = ({ children }: WebSocketProviderProps) => {
const ws = useWebSocket('wss://dev-next.buycoffee.top:4433/api/v1/ws/server') const ws = useWebSocket("wss://dev-next.buycoffee.top:4433/api/v1/ws/server");
return ( return (
<WebSocketContext.Provider value={ws}>{children}</WebSocketContext.Provider> <WebSocketContext.Provider value={ws}>{children}</WebSocketContext.Provider>
) );
} };
export const useWebSocketContext = (): WebSocketHook => { export const useWebSocketContext = (): WebSocketHook => {
const context = useContext(WebSocketContext) const context = useContext(WebSocketContext);
if (!context) { if (!context) {
throw new Error( throw new Error(
'useWebSocketContext must be used within a WebSocketProvider' "useWebSocketContext must be used within a WebSocketProvider",
) );
} }
return context return context;
} };

View File

@ -12,14 +12,14 @@ const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<WebSocketProvider > <WebSocketProvider>
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<App /> <App />
<Toaster richColors position="top-right" /> <Toaster richColors position="top-right" />
<ReactQueryDevtools /> <ReactQueryDevtools />
</QueryClientProvider> </QueryClientProvider>
</WebSocketProvider> </WebSocketProvider>
</ThemeProvider> </ThemeProvider>
</React.StrictMode>, </React.StrictMode>,
); );

39
src/types/nezha-api.ts Normal file
View File

@ -0,0 +1,39 @@
export interface NezhaAPI {
id: number;
name: string;
host: NezhaAPIHost;
status: NezhaAPIStatus;
}
export interface NezhaAPIHost {
Platform: string;
PlatformVersion: string;
CPU: string[];
MemTotal: number;
DiskTotal: number;
SwapTotal: number;
Arch: string;
BootTime: number;
CountryCode: string;
Version: string;
}
export interface NezhaAPIStatus {
CPU: number;
MemUsed: number;
SwapUsed: number;
DiskUsed: number;
NetInTransfer: number;
NetOutTransfer: number;
NetInSpeed: number;
NetOutSpeed: number;
Uptime: number;
Load1: number;
Load5: number;
Load15: number;
TcpConnCount: number;
UdpConnCount: number;
ProcessCount: number;
Temperatures: number;
GPU: number;
}