first commit
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
NezhaBaseUrl=http://0.0.0.0:8008
|
||||
NezhaAuth=5hAY3QX6Nl9B3UOQgB26KdsdS1dsdUdM
|
BIN
.github/shotOne.png
vendored
Normal file
After Width: | Height: | Size: 517 KiB |
BIN
.github/shotTwo.png
vendored
Normal file
After Width: | Height: | Size: 493 KiB |
48
.gitignore
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# pwa
|
||||
/public/sw.js
|
||||
/public/sw.js.map
|
||||
/public/swe-worker-development.js
|
||||
/public/workbox*.js
|
||||
/public/workbox*.js.map
|
||||
|
||||
/.idea/
|
||||
|
||||
.env
|
||||
|
10
README.md
Normal file
@ -0,0 +1,10 @@
|
||||
<h1 align="center">HomeDash</h1>
|
||||
<div align="center">
|
||||
<strong>HomeDash 是一个基于 Next.js 和 Shadcn 的仪表盘</strong>
|
||||
<br>
|
||||
<strong>Demo地址: https://dash.buycoffee.top</strong>
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
43
app/(main)/ClientComponents/LiveTag.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { verifySSEConnection } from "@/lib/sseFetch";
|
||||
|
||||
export default function LiveTag() {
|
||||
const [connected, setConnected] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Store the promise in a variable
|
||||
const ssePromise = verifySSEConnection(
|
||||
"https://home.buycoffee.tech/v2/VerifySSEConnect",
|
||||
);
|
||||
setTimeout(() => {
|
||||
toast.promise(ssePromise, {
|
||||
loading: "Connecting to SSE...",
|
||||
success: "HomeDash SSE Connected",
|
||||
error: "Error connecting to SSE",
|
||||
});
|
||||
});
|
||||
// Handle promise resolution separately
|
||||
ssePromise
|
||||
.then(() => {
|
||||
setConnected(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setConnected(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return connected ? (
|
||||
<Badge className={"flex items-center justify-center gap-1 px-2"}>
|
||||
Synced
|
||||
<span className="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge className={"flex items-center justify-center gap-1 px-2"}>
|
||||
Static<span className="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
</Badge>
|
||||
);
|
||||
}
|
34
app/(main)/ClientComponents/ServerListClient.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import ServerCard from "@/components/ServerCard";
|
||||
import { nezhaFetcher } from "@/lib/utils";
|
||||
import useSWR from "swr";
|
||||
|
||||
export default function ServerListClient() {
|
||||
const { data } = useSWR('/api/server', nezhaFetcher, {
|
||||
refreshInterval: 2000,
|
||||
});
|
||||
if (!data) return null;
|
||||
const sortedResult = data.result.sort((a: any, b: any) => a.id - b.id);
|
||||
|
||||
return (
|
||||
<section className={"grid grid-cols-1 gap-2 md:grid-cols-2"}>
|
||||
{sortedResult.map(
|
||||
(server: any) => (
|
||||
<ServerCard
|
||||
key={server.id}
|
||||
id={server.id}
|
||||
cpu={server.status.CPU}
|
||||
name={server.name}
|
||||
up={server.status.NetOutSpeed / 1024 / 1024}
|
||||
down={server.status.NetInSpeed / 1024 / 1024}
|
||||
status={server.status.Uptime !== 0 ? "online" : "offline"}
|
||||
uptime={server.status.Uptime / 86400}
|
||||
mem={(server.status.MemUsed / server.host.MemTotal) * 100}
|
||||
stg={server.status.DiskUsed / server.host.DiskTotal}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
75
app/(main)/ClientComponents/ServerOverviewClient.tsx
Normal file
62
app/(main)/header.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
function Header() {
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl">
|
||||
<section className="flex items-center justify-between">
|
||||
<section className="text-md flex items-center font-medium">
|
||||
<div className="mr-1 flex flex-row items-center justify-start">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
unoptimized
|
||||
alt="apple-touch-icon"
|
||||
src={"/apple-touch-icon.png"}
|
||||
className="relative !m-0 h-6 w-6 border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
|
||||
/>
|
||||
</div>
|
||||
HomeDash
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden h-4 w-[1px] md:block"
|
||||
/>
|
||||
<p className="hidden text-sm font-medium opacity-40 md:block">
|
||||
Simple and beautiful dashboard
|
||||
</p>
|
||||
</section>
|
||||
{/* <LiveTag /> */}
|
||||
</section>
|
||||
<Overview />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Overview() {
|
||||
const [mouted, setMounted] = useState(false);
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
const time = DateTime.TIME_SIMPLE;
|
||||
time.hour12 = true;
|
||||
|
||||
return (
|
||||
<section className={"md:mt-16 mt-10 flex flex-col"}>
|
||||
<p className="text-md 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>
|
||||
{mouted && (
|
||||
<p className="opacity-1 text-sm font-medium">
|
||||
{DateTime.now().setLocale("en-US").toLocaleString(time)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
21
app/(main)/layout.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
|
||||
import Header from "@/app/(main)/header";
|
||||
import BlurLayers from "@/components/BlurLayer";
|
||||
|
||||
type DashboardProps = {
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function MainLayout({ children }: DashboardProps) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col">
|
||||
<main className="flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:p-10 md:pt-8">
|
||||
<Header />
|
||||
<BlurLayers />
|
||||
{/* <Nav /> */}
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
57
app/(main)/nav.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const siteUrlList = [
|
||||
{
|
||||
name: "Home",
|
||||
header: "👋 Overview",
|
||||
url: "/",
|
||||
},
|
||||
{
|
||||
name: "Service",
|
||||
header: "🎛️ Service",
|
||||
url: "/service",
|
||||
},
|
||||
];
|
||||
export default function Nav() {
|
||||
const nowPath = usePathname();
|
||||
return (
|
||||
<div className={"flex flex-col items-center justify-center"}>
|
||||
<div
|
||||
className={
|
||||
"fixed bottom-6 z-50 flex items-center gap-1 rounded-[50px] bg-stone-700 bg-opacity-80 px-2 py-1.5 backdrop-blur-lg"
|
||||
}
|
||||
>
|
||||
{siteUrlList.map((site, index) => (
|
||||
<div key={site.name} className={"flex items-center gap-1"}>
|
||||
{index !== 0 && (
|
||||
<p key={index} className={"pointer-events-none text-stone-500"}>
|
||||
/
|
||||
</p>
|
||||
)}
|
||||
<Link
|
||||
key={site.name}
|
||||
href={site.url}
|
||||
scroll={true}
|
||||
className={cn(
|
||||
"rounded-[50px] px-2.5 py-1.5 text-[16px] font-[500] text-stone-400 transition-colors sm:hover:text-white",
|
||||
clsx(
|
||||
nowPath === site.url &&
|
||||
"bg-stone-500 text-white dark:bg-stone-600",
|
||||
),
|
||||
)}
|
||||
>
|
||||
{site.name}
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
15
app/(main)/page.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import ServerList from "@/components/ServerList";
|
||||
import ServerOverview from "@/components/ServerOverview";
|
||||
|
||||
|
||||
|
||||
export default function Home() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto grid w-full max-w-5xl gap-4 md:gap-6">
|
||||
<ServerOverview />
|
||||
<ServerList />
|
||||
<div className={"mt-20 sm:mt-12"}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
BIN
app/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
app/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 10 KiB |
33
app/api/server/route.ts
Normal file
@ -0,0 +1,33 @@
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET(_: Request) {
|
||||
|
||||
try {
|
||||
const response = await fetch(process.env.NezhaBaseUrl+ '/api/v1/server/details',{
|
||||
headers: {
|
||||
'Authorization': process.env.NezhaAuth as string
|
||||
},
|
||||
next:{
|
||||
revalidate:1
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
data.live_servers = 0;
|
||||
data.offline_servers = 0;
|
||||
data.total_bandwidth = 0;
|
||||
|
||||
data.result.forEach((element: { status: { Uptime: number; NetOutTransfer: any; }; }) => {
|
||||
if (element.status.Uptime !== 0) {
|
||||
data.live_servers += 1;
|
||||
} else {
|
||||
data.offline_servers += 1;
|
||||
}
|
||||
data.total_bandwidth += element.status.NetOutTransfer;
|
||||
});
|
||||
return NextResponse.json(data, { status: 200 })
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: error }, { status: 200 })
|
||||
}
|
||||
|
||||
}
|
BIN
app/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
app/favicon-16x16.png
Normal file
After Width: | Height: | Size: 263 B |
BIN
app/favicon-32x32.png
Normal file
After Width: | Height: | Size: 324 B |
BIN
app/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
52
app/layout.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import "@/styles/globals.css";
|
||||
|
||||
import type { Metadata } from "next";
|
||||
import { Inter as FontSans } from "next/font/google";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import React from "react";
|
||||
|
||||
import NextThemeToaster from "@/components/client/NextToast";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const fontSans = FontSans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
manifest: "/manifest.json",
|
||||
title: "HomeDash",
|
||||
description: "A dashboard for nezha",
|
||||
appleWebApp: {
|
||||
capable: true,
|
||||
title: "HomeDash",
|
||||
statusBarStyle: "black-translucent",
|
||||
},
|
||||
};
|
||||
|
||||
interface RootLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<NextThemeToaster />
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
27
app/not-found.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<main className="relative h-screen w-full">
|
||||
<div className="absolute inset-0 m-4 flex items-center justify-center">
|
||||
<Image
|
||||
priority
|
||||
className="rounded-3xl object-cover"
|
||||
src="/tardis.jpg"
|
||||
fill={true}
|
||||
alt="TARDIS"
|
||||
/>
|
||||
<div className="text-container absolute right-4 p-4 md:right-20">
|
||||
<h1 className="text-2xl font-bold opacity-80 md:text-5xl">
|
||||
404 Not Found
|
||||
</h1>
|
||||
<p className="text-lg opacity-60 md:text-base">TARDIS ERROR!</p>
|
||||
<Link href={"/"} className="text-2xl opacity-80 md:text-3xl">
|
||||
Doctor?
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
17
components.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "stone",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
34
components/BlurLayer.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
const BlurLayers = () => {
|
||||
const computeLayerStyle = (index: number) => {
|
||||
const blurAmount = index * 3.7037;
|
||||
const maskStart = index * 10;
|
||||
let maskEnd = maskStart + 20;
|
||||
if (maskEnd > 100) {
|
||||
maskEnd = 100;
|
||||
}
|
||||
return {
|
||||
backdropFilter: `blur(${blurAmount}px)`,
|
||||
WebkitBackdropFilter: `blur(${blurAmount}px)`,
|
||||
zIndex: index + 1,
|
||||
maskImage: `linear-gradient(rgba(0, 0, 0, 0) ${maskStart}%, rgb(0, 0, 0) ${maskEnd}%)`,
|
||||
};
|
||||
};
|
||||
|
||||
// 根据层数动态生成层
|
||||
const layers = Array.from({ length: 5 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={"absolute inset-0 h-full w-full"}
|
||||
style={computeLayerStyle(index)}
|
||||
/>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={"fixed bottom-0 left-0 right-0 z-50 h-[140px]"}>
|
||||
<div className={"relative h-full"}>{layers}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlurLayers;
|
7
components/Icon.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 496 512" fill="white" {...props}>
|
||||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
94
components/ServerCard.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import React from "react";
|
||||
|
||||
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
type ServerCardProps = {
|
||||
id: number;
|
||||
status: string;
|
||||
name: string;
|
||||
uptime: number;
|
||||
cpu: number;
|
||||
mem: number;
|
||||
stg: number;
|
||||
up: number;
|
||||
down: number;
|
||||
};
|
||||
|
||||
export default function ServerCard({
|
||||
status,
|
||||
name,
|
||||
uptime,
|
||||
cpu,
|
||||
mem,
|
||||
stg,
|
||||
up,
|
||||
down,
|
||||
}: ServerCardProps) {
|
||||
return status === "online" ? (
|
||||
<Card
|
||||
className={
|
||||
"flex flex-col items-center justify-center gap-3 p-3 md:px-5 lg:flex-row"
|
||||
}
|
||||
>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<section className={"flex w-28 items-center justify-center gap-2"}>
|
||||
<p className="text-sm font-bold tracking-tight">{name}</p>
|
||||
<span className="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
</section>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Online: {uptime.toFixed(0)} Days</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<section className={"grid grid-cols-5 items-center gap-3"}>
|
||||
<div className={"flex flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">CPU</p>
|
||||
<div className="text-xs font-semibold">{cpu.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">Mem</p>
|
||||
<div className="text-xs font-semibold">{mem.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">STG</p>
|
||||
<div className="text-xs font-semibold">{stg.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">Upload</p>
|
||||
<div className="text-xs font-semibold">{up.toFixed(2)}Mb/s</div>
|
||||
</div>
|
||||
<div className={"flex flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">Download</p>
|
||||
<div className="text-xs font-semibold">{down.toFixed(2)}Mb/s</div>
|
||||
</div>
|
||||
</section>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
className={"flex flex-col h-[61px] items-center gap-3 p-3 md:px-6 lg:flex-row"}
|
||||
>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<section className={"flex w-28 items-center justify-center gap-2"}>
|
||||
<p className="text-sm font-bold tracking-tight">{name}</p>
|
||||
<span className="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
</section>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Offline</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Card>
|
||||
);
|
||||
}
|
11
components/ServerList.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
|
||||
import ServerListClient from "@/app/(main)/ClientComponents/ServerListClient";
|
||||
|
||||
|
||||
|
||||
export default async function ServerList() {
|
||||
return (
|
||||
<ServerListClient />
|
||||
);
|
||||
}
|
12
components/ServerOverview.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import ServerOverviewClient from "@/app/(main)/ClientComponents/ServerOverviewClient";
|
||||
|
||||
|
||||
|
||||
|
||||
export default async function ServerOverview() {
|
||||
return (
|
||||
<ServerOverviewClient />
|
||||
)
|
||||
|
||||
|
||||
}
|
25
components/ServerUsageBar.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
type ServerUsageBarProps = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
export default function ServerUsageBar({ value }: ServerUsageBarProps) {
|
||||
return (
|
||||
<Progress
|
||||
aria-label={"Server Usage Bar"}
|
||||
aria-labelledby={"Server Usage Bar"}
|
||||
value={value}
|
||||
indicatorClassName={
|
||||
value > 90
|
||||
? "bg-red-500"
|
||||
: value > 70
|
||||
? "bg-orange-400"
|
||||
: "bg-green-500"
|
||||
}
|
||||
className={"h-[3px] rounded-sm"}
|
||||
/>
|
||||
);
|
||||
}
|
67
components/SubscribeCard.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
|
||||
import { Loader } from "@/components/loading/Loader";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
type SubscribeCardProps = {
|
||||
provider: string;
|
||||
behavior: string;
|
||||
value: number;
|
||||
nextBillDay?: number;
|
||||
total: number;
|
||||
unit: string;
|
||||
colorClassName: string;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
function SubscribeCard({
|
||||
provider,
|
||||
behavior,
|
||||
value,
|
||||
total,
|
||||
unit,
|
||||
nextBillDay,
|
||||
colorClassName,
|
||||
isLoading,
|
||||
}: SubscribeCardProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-bold md:text-base">
|
||||
{provider}
|
||||
</CardTitle>
|
||||
{nextBillDay !== -1 && (
|
||||
<div className={"flex items-center gap-0.5"}>
|
||||
<p className={"text-[10px] font-semibold opacity-50"}>
|
||||
{nextBillDay}
|
||||
</p>
|
||||
<p className={"text-[10px] font-bold"}>|</p>
|
||||
</div>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={"flex items-center gap-0.5"}>
|
||||
<p className="text-[12px] text-muted-foreground">{behavior}</p>
|
||||
<Loader visible={isLoading} />
|
||||
</div>
|
||||
|
||||
<div className={"flex items-baseline gap-1"}>
|
||||
<p className={"mb-4 text-3xl font-semibold"}>{value}</p>
|
||||
<p className={"mb-4 text-sm font-light text-muted-foreground"}>
|
||||
{unit}
|
||||
</p>
|
||||
</div>
|
||||
<Progress
|
||||
aria-label={`Used ${unit}`}
|
||||
aria-labelledby={`${value} Used ${unit}`}
|
||||
value={(value / total) * 100}
|
||||
indicatorClassName={colorClassName}
|
||||
className={"h-[3px] rounded-sm"}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default SubscribeCard;
|
27
components/client/NextToast.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
import { useTheme } from "next-themes";
|
||||
import React from "react";
|
||||
import { Toaster } from "sonner";
|
||||
|
||||
type ThemeType = "light" | "dark" | "system"; // 声明 theme 的类型
|
||||
export default function NextThemeToaster() {
|
||||
const { theme } = useTheme();
|
||||
const themeMap: Record<ThemeType, string> = {
|
||||
light: "light",
|
||||
dark: "dark",
|
||||
system: "system",
|
||||
};
|
||||
|
||||
// 使用类型断言确保theme是一个ThemeType
|
||||
const selectedTheme = theme ? themeMap[theme as ThemeType] : "system";
|
||||
|
||||
return (
|
||||
<Toaster
|
||||
theme={selectedTheme as ThemeType}
|
||||
richColors={true}
|
||||
duration={2000}
|
||||
position="top-center"
|
||||
className={"mb-16 sm:mb-14"}
|
||||
/>
|
||||
);
|
||||
}
|
18
components/client/client-side-refresh.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
type ClientSideRefreshProps = {
|
||||
timeMs: number;
|
||||
};
|
||||
export default function ClientSideRefresh({ timeMs }: ClientSideRefreshProps) {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
router.refresh();
|
||||
}, timeMs);
|
||||
return () => clearInterval(interval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
return null;
|
||||
}
|
40
components/client/theme-switcher.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import * as React from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="rounded-full p-2">
|
||||
<Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
13
components/loading/Loader.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
const bars = Array(8).fill(0);
|
||||
|
||||
export const Loader = ({ visible }: { visible: boolean }) => {
|
||||
return (
|
||||
<div className="hamster-loading-wrapper" data-visible={visible}>
|
||||
<div className="hamster-spinner">
|
||||
{bars.map((_, i) => (
|
||||
<div className="hamster-loading-bar" key={`hamster-bar-${i}`} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
33
components/ui/animated-tooltip.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
export const AnimatedTooltip = ({
|
||||
items,
|
||||
}: {
|
||||
items: {
|
||||
id: number;
|
||||
name: string;
|
||||
designation: string;
|
||||
image: string;
|
||||
}[];
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{items.map((item) => (
|
||||
<div className="group relative -mr-4" key={item.name}>
|
||||
<Link href="https://buycoffee.top" target="_blank">
|
||||
<Image
|
||||
width={40}
|
||||
height={40}
|
||||
unoptimized
|
||||
src={item.image}
|
||||
alt={item.name}
|
||||
className="relative !m-0 h-6 w-6 rounded-full border-2 border-white object-cover object-top !p-0 transition duration-500 group-hover:z-30 group-hover:scale-105"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
36
components/ui/badge.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
56
components/ui/button.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
86
components/ui/card.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
200
components/ui/dropdown-menu.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
25
components/ui/input.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
128
components/ui/navigation-menu.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const NavigationMenu = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-10 flex max-w-max flex-1 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<NavigationMenuViewport />
|
||||
</NavigationMenuPrimitive.Root>
|
||||
));
|
||||
NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
|
||||
|
||||
const NavigationMenuList = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"group flex flex-1 list-none items-center justify-center space-x-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
|
||||
|
||||
const NavigationMenuItem = NavigationMenuPrimitive.Item;
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-10 w-max items-center justify-center rounded-md bg-transparent px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50",
|
||||
);
|
||||
|
||||
const NavigationMenuTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDown
|
||||
className="relative top-[1px] ml-1 h-3 w-3 transition duration-200 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
));
|
||||
NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
|
||||
|
||||
const NavigationMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
|
||||
|
||||
const NavigationMenuLink = NavigationMenuPrimitive.Link;
|
||||
|
||||
const NavigationMenuViewport = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className={cn("absolute left-0 top-full flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
className={cn(
|
||||
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
NavigationMenuViewport.displayName =
|
||||
NavigationMenuPrimitive.Viewport.displayName;
|
||||
|
||||
const NavigationMenuIndicator = React.forwardRef<
|
||||
React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
|
||||
React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
));
|
||||
NavigationMenuIndicator.displayName =
|
||||
NavigationMenuPrimitive.Indicator.displayName;
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
navigationMenuTriggerStyle,
|
||||
NavigationMenuViewport,
|
||||
};
|
33
components/ui/progress.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||
indicatorClassName?: string; // 添加一个新的可选属性来自定义Indicator的类名
|
||||
}
|
||||
>(({ className, value, indicatorClassName, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className={cn(
|
||||
"h-full w-full flex-1 transition-all",
|
||||
indicatorClassName || "bg-primary", // 使用提供的indicatorClassName或默认的bg-primary
|
||||
)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress };
|
31
components/ui/separator.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
140
components/ui/sheet.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
"use client";
|
||||
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Sheet = SheetPrimitive.Root;
|
||||
|
||||
const SheetTrigger = SheetPrimitive.Trigger;
|
||||
|
||||
const SheetClose = SheetPrimitive.Close;
|
||||
|
||||
const SheetPortal = SheetPrimitive.Portal;
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||
|
||||
const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = "right", className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
));
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetHeader.displayName = "SheetHeader";
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
SheetFooter.displayName = "SheetFooter";
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg font-semibold text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetOverlay,
|
||||
SheetPortal,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
};
|
15
components/ui/skeleton.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
30
components/ui/tooltip.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };
|
43
lib/sseFetch.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
import useSWRSubscription, {
|
||||
type SWRSubscriptionOptions,
|
||||
} from "swr/subscription";
|
||||
|
||||
type LooseObject = {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export function SSEDataFetch(url: string, fallbackData?: LooseObject): any {
|
||||
const { data } = useSWRSubscription<LooseObject>(
|
||||
url,
|
||||
(key: string | URL, { next }: SWRSubscriptionOptions<LooseObject>) => {
|
||||
const source = new EventSource(key);
|
||||
source.onmessage = (event) => {
|
||||
const parsedData = JSON.parse(event.data);
|
||||
next(null, parsedData);
|
||||
};
|
||||
source.onerror = () => next(new Error("EventSource error"));
|
||||
return () => source.close();
|
||||
},
|
||||
{
|
||||
fallbackData: fallbackData,
|
||||
},
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function verifySSEConnection(url: string): Promise<{ name: string }> {
|
||||
return new Promise<{ name: string }>((resolve, reject) => {
|
||||
const eventSource = new EventSource(url);
|
||||
|
||||
eventSource.onopen = () => {
|
||||
resolve({ name: "SSE Connected" });
|
||||
eventSource.close();
|
||||
};
|
||||
|
||||
eventSource.onerror = () => {
|
||||
reject("Failed to connect");
|
||||
eventSource.close();
|
||||
};
|
||||
});
|
||||
}
|
58
lib/utils.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { type ClassValue, clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatBytes(bytes: number, decimals: number = 2) {
|
||||
if (!+bytes) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
export function getDaysBetweenDates(date1: string, date2: string): number {
|
||||
const oneDay = 24 * 60 * 60 * 1000; // 一天的毫秒数
|
||||
const firstDate = new Date(date1);
|
||||
const secondDate = new Date(date2);
|
||||
|
||||
// 计算两个日期之间的天数差异
|
||||
return Math.round(
|
||||
Math.abs((firstDate.getTime() - secondDate.getTime()) / oneDay),
|
||||
);
|
||||
}
|
||||
|
||||
export const fetcher = (url: string) =>
|
||||
fetch(url)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => data.data)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
export const nezhaFetcher = (url: string) =>
|
||||
fetch(url)
|
||||
.then((res) => {
|
||||
if (!res.ok) {
|
||||
throw new Error(res.statusText);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then((data) => data)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
throw err;
|
||||
});
|
||||
|
23
next.config.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
import withPWAInit from "@ducanh2912/next-pwa";
|
||||
|
||||
import withBundleAnalyzer from "@next/bundle-analyzer";
|
||||
|
||||
const bundleAnalyzer = withBundleAnalyzer({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
const withPWA = withPWAInit({
|
||||
dest: "public",
|
||||
cacheOnFrontEndNav: true,
|
||||
aggressiveFrontEndNavCaching: true,
|
||||
reloadOnOnline: true,
|
||||
disable: false,
|
||||
workboxOptions: {
|
||||
disableDevLogs: true,
|
||||
},
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default bundleAnalyzer(withPWA(nextConfig));
|
57
package.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "homedash-shadcn",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3020",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ducanh2912/next-pwa": "^10.2.6",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.4",
|
||||
"@radix-ui/react-progress": "^1.0.3",
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.0.0",
|
||||
"lucide-react": "^0.414.0",
|
||||
"luxon": "^3.4.4",
|
||||
"next": "^14.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-intersection-observer": "^9.8.2",
|
||||
"react-wrap-balancer": "^1.1.0",
|
||||
"sharp": "^0.33.3",
|
||||
"sonner": "^1.4.41",
|
||||
"swr": "^2.2.6-beta.3",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-turbo": "^2.0.3",
|
||||
"eslint-plugin-unused-imports": "^4.0.0",
|
||||
"@next/bundle-analyzer": "^14.1.4",
|
||||
"@types/node": "^20.12.4",
|
||||
"@types/react": "^18.2.74",
|
||||
"@types/react-dom": "^18.2.24",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.4.0",
|
||||
"eslint-config-next": "^14.1.4",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.6.3",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.3"
|
||||
}
|
||||
}
|
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
4
prettier.config.js
Normal file
@ -0,0 +1,4 @@
|
||||
// prettier.config.js
|
||||
module.exports = {
|
||||
plugins: ["prettier-plugin-tailwindcss"],
|
||||
};
|
BIN
public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
public/blog-man.webp
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 263 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 324 B |
BIN
public/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
public/hamster.webp
Normal file
After Width: | Height: | Size: 12 KiB |
22
public/manifest.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "HomeDash",
|
||||
"short_name": "HomeDash PWA App",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#000000",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait"
|
||||
}
|
1
public/swe-worker-5c72df51bb1f6ee0.js
Normal file
@ -0,0 +1 @@
|
||||
self.onmessage=async e=>{switch(e.data.type){case"__START_URL_CACHE__":{let t=e.data.url,a=await fetch(t);if(!a.redirected)return(await caches.open("start-url")).put(t,a);return Promise.resolve()}case"__FRONTEND_NAV_CACHE__":{let t=e.data.url,a=await caches.open("pages");if(await a.match(t,{ignoreSearch:!0}))return;let s=await fetch(t);if(!s.ok)return;if(a.put(t,s.clone()),e.data.shouldCacheAggressively&&s.headers.get("Content-Type")?.includes("text/html"))try{let e=await s.text(),t=[],a=await caches.open("static-style-assets"),r=await caches.open("next-static-js-assets"),c=await caches.open("static-js-assets");for(let[s,r]of e.matchAll(/<link.*?href=['"](.*?)['"].*?>/g))/rel=['"]stylesheet['"]/.test(s)&&t.push(a.match(r).then(e=>e?Promise.resolve():a.add(r)));for(let[,a]of e.matchAll(/<script.*?src=['"](.*?)['"].*?>/g)){let e=/\/_next\/static.+\.js$/i.test(a)?r:c;t.push(e.match(a).then(t=>t?Promise.resolve():e.add(a)))}return await Promise.all(t)}catch{}return Promise.resolve()}default:return Promise.resolve()}};
|
BIN
public/tardis.jpg
Normal file
After Width: | Height: | Size: 571 KiB |
191
styles/globals.css
Normal file
@ -0,0 +1,191 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
--radius: 1rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
html {
|
||||
@apply scroll-smooth;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
/* font-feature-settings: "rlig" 1, "calt" 1; */
|
||||
font-synthesis-weight: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.step {
|
||||
counter-increment: step;
|
||||
}
|
||||
|
||||
.step:before {
|
||||
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
|
||||
@apply ml-[-50px] mt-[-4px];
|
||||
content: counter(step);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.container {
|
||||
@apply px-4;
|
||||
}
|
||||
}
|
||||
|
||||
::selection {
|
||||
@apply bg-stone-300 dark:bg-stone-800;
|
||||
}
|
||||
|
||||
.hamster-loading-wrapper {
|
||||
--size: 12px;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
inset: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.hamster-loading-wrapper[data-visible="false"] {
|
||||
transform-origin: center;
|
||||
animation: hamster-fade-out 0.2s ease forwards;
|
||||
}
|
||||
|
||||
.hamster-spinner {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
height: var(--size);
|
||||
width: var(--size);
|
||||
}
|
||||
|
||||
.hamster-loading-bar {
|
||||
--gray11: hsl(0, 0%, 43.5%);
|
||||
animation: hamster-spin 0.8s linear infinite;
|
||||
background: var(--gray11);
|
||||
border-radius: 6px;
|
||||
height: 13%;
|
||||
left: -10%;
|
||||
position: absolute;
|
||||
top: -3.9%;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(1) {
|
||||
animation-delay: -0.8s;
|
||||
transform: rotate(0deg) translate(120%);
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(2) {
|
||||
animation-delay: -0.7s;
|
||||
transform: rotate(45deg) translate(120%);
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(3) {
|
||||
animation-delay: -0.6s;
|
||||
transform: rotate(90deg) translate(120%);
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(4) {
|
||||
animation-delay: -0.5s;
|
||||
transform: rotate(135deg) translate(120%);
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(5) {
|
||||
animation-delay: -0.4s;
|
||||
transform: rotate(180deg) translate(120%);
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(6) {
|
||||
animation-delay: -0.3s;
|
||||
transform: rotate(225deg) translate(120%);
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(7) {
|
||||
animation-delay: -0.2s;
|
||||
transform: rotate(270deg) translate(120%);
|
||||
}
|
||||
|
||||
.hamster-loading-bar:nth-child(8) {
|
||||
animation-delay: -0.1s;
|
||||
transform: rotate(315deg) translate(120%);
|
||||
}
|
||||
|
||||
@keyframes hamster-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hamster-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hamster-spin {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.15;
|
||||
}
|
||||
}
|
84
tailwind.config.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
import { fontFamily } from "tailwindcss/defaultTheme";
|
||||
|
||||
const config = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./pages/**/*.{ts,tsx}",
|
||||
"./components/**/*.{ts,tsx}",
|
||||
"./app/**/*.{ts,tsx}",
|
||||
"./src/**/*.{ts,tsx}",
|
||||
],
|
||||
prefix: "",
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: "0" },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
} satisfies Config;
|
||||
|
||||
export default config;
|
26
tsconfig.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|