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
run: |
bun run build --base=/dashboard
bun run build
- name: Compress dist folder
run: zip -r dist.zip dist

View File

@ -11,12 +11,7 @@ const App: React.FC = () => {
<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 />
<Routes>
<Route
path="/"
element={
<Server />
}
/>
<Route path="/" element={<Server />} />
</Routes>
<Footer />
</main>

View File

@ -8,13 +8,10 @@ import { DateTime } from "luxon";
import { useEffect, useRef, useState } from "react";
function Header() {
return (
<div className="mx-auto w-full max-w-5xl">
<section className="flex items-center justify-between">
<section
className="flex items-center text-base font-medium"
>
<section className="flex items-center text-base font-medium">
<div className="mr-1 flex flex-row items-center justify-start">
<img
width={40}
@ -74,9 +71,7 @@ function Overview() {
<section className={"mt-10 flex flex-col md:mt-16"}>
<p className="text-base font-semibold">👋 Overview</p>
<div className="flex items-center gap-1.5">
<p className="text-sm font-medium opacity-50">
where the time is
</p>
<p className="text-sm font-medium opacity-50">where the time is</p>
{mouted ? (
<p className="text-sm font-medium">{timeString}</p>
) : (

View File

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

View File

@ -1,7 +1,7 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import * as React from "react";
import * as SeparatorPrimitive from "@radix-ui/react-separator";
import { cn } from "@/lib/utils"
import { cn } from "@/lib/utils";
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
@ -9,7 +9,7 @@ const Separator = React.forwardRef<
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
ref,
) => (
<SeparatorPrimitive.Root
ref={ref}
@ -18,12 +18,12 @@ const Separator = React.forwardRef<
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
className,
)}
{...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({
className,
@ -9,7 +9,7 @@ function Skeleton({
className={cn("animate-pulse rounded-md bg-muted", className)}
{...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 {
socket: WebSocket | null
connected: boolean
onlineCount: number
message: string | null
sendMessage: (msg: string) => void
socket: WebSocket | null;
connected: boolean;
onlineCount: number;
message: string | null;
sendMessage: (msg: string) => void;
}
export default function useWebSocket(url: string): WebSocketHook {
const [socket, setSocket] = useState<WebSocket | null>(null)
const [message, setMessage] = useState<string | null>(null)
const [connected, setConnected] = useState<boolean>(false)
const [onlineCount, setOnlineCount] = useState<number>(0)
const socketRef = useRef<WebSocket | null>(null)
const reconnectAttempts = useRef<number>(0)
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null)
const isUnmounted = useRef<boolean>(false)
const [socket, setSocket] = useState<WebSocket | null>(null);
const [message, setMessage] = useState<string | null>(null);
const [connected, setConnected] = useState<boolean>(false);
const [onlineCount, setOnlineCount] = useState<number>(0);
const socketRef = useRef<WebSocket | null>(null);
const reconnectAttempts = useRef<number>(0);
const reconnectTimeout = useRef<NodeJS.Timeout | null>(null);
const isUnmounted = useRef<boolean>(false);
const connect = useCallback(() => {
if (isUnmounted.current) return
if (isUnmounted.current) return;
const ws = new WebSocket(url)
setSocket(ws)
socketRef.current = ws
const ws = new WebSocket(url);
setSocket(ws);
socketRef.current = ws;
ws.onopen = () => {
setConnected(true)
reconnectAttempts.current = 0
}
setConnected(true);
reconnectAttempts.current = 0;
};
ws.onmessage = (event: MessageEvent) => {
setMessage(event.data)
const msgJson = JSON.parse(event.data)
if (msgJson.type === 'live') {
setOnlineCount(msgJson.data.count)
}
setMessage(event.data);
const msgJson = JSON.parse(event.data);
if (msgJson.type === "live") {
setOnlineCount(msgJson.data.count);
}
};
ws.onerror = (error) => {
console.error('WebSocket Error:', error)
}
console.error("WebSocket Error:", error);
};
ws.onclose = () => {
setConnected(false)
setConnected(false);
if (!isUnmounted.current) {
// Attempt to reconnect
if (reconnectAttempts.current < 5) {
const timeout = Math.pow(2, reconnectAttempts.current) * 1000 // Exponential backoff
reconnectAttempts.current += 1
const timeout = Math.pow(2, reconnectAttempts.current) * 1000; // Exponential backoff
reconnectAttempts.current += 1;
reconnectTimeout.current = setTimeout(() => {
connect()
}, timeout)
connect();
}, timeout);
} else {
console.warn('Max reconnect attempts reached.')
console.warn("Max reconnect attempts reached.");
}
}
}
}, [url])
};
}, [url]);
useEffect(() => {
connect()
connect();
return () => {
isUnmounted.current = true
isUnmounted.current = true;
if (socketRef.current) {
socketRef.current.close()
socketRef.current.close();
}
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current)
clearTimeout(reconnectTimeout.current);
}
}
}, [connect])
};
}, [connect]);
// Function to send messages
const sendMessage = useCallback((msg: string) => {
if (socketRef.current && socketRef.current.readyState === WebSocket.OPEN) {
socketRef.current.send(msg)
socketRef.current.send(msg);
} else {
console.warn(
'WebSocket is not open. Ready state:',
socketRef.current?.readyState
)
"WebSocket is not open. Ready state:",
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 useWebSocket, { WebSocketHook } from './useWebsocket'
import { createContext, useContext, ReactNode } from "react";
import useWebSocket, { WebSocketHook } from "./useWebsocket";
interface WebSocketProviderProps {
children: ReactNode
children: ReactNode;
}
const WebSocketContext = createContext<WebSocketHook | undefined>(undefined)
const WebSocketContext = createContext<WebSocketHook | undefined>(undefined);
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 (
<WebSocketContext.Provider value={ws}>{children}</WebSocketContext.Provider>
)
}
);
};
export const useWebSocketContext = (): WebSocketHook => {
const context = useContext(WebSocketContext)
const context = useContext(WebSocketContext);
if (!context) {
throw new Error(
'useWebSocketContext must be used within a WebSocketProvider'
)
}
return context
"useWebSocketContext must be used within a WebSocketProvider",
);
}
return context;
};

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;
}