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"]
|
||||||
|
}
|