first commit

This commit is contained in:
hamster1963 2024-07-27 02:17:07 +08:00
commit 3085a41b11
66 changed files with 2172 additions and 0 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
NezhaBaseUrl=http://0.0.0.0:8008
NezhaAuth=5hAY3QX6Nl9B3UOQgB26KdsdS1dsdUdM

BIN
.github/shotOne.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 KiB

BIN
.github/shotTwo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 KiB

48
.gitignore vendored Normal file
View 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
View 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>
![screen-shot-one](/.github/shotOne.png)
![screen-shot-two](/.github/shotTwo.png)
</div>

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

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

File diff suppressed because one or more lines are too long

62
app/(main)/header.tsx Normal file
View 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
View 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
View 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
View 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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

33
app/api/server/route.ts Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
app/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

BIN
app/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

BIN
app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

52
app/layout.tsx Normal file
View 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
View 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>
);
}

BIN
bun.lockb Executable file

Binary file not shown.

17
components.json Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,11 @@
import React from "react";
import ServerListClient from "@/app/(main)/ClientComponents/ServerListClient";
export default async function ServerList() {
return (
<ServerListClient />
);
}

View File

@ -0,0 +1,12 @@
import ServerOverviewClient from "@/app/(main)/ClientComponents/ServerOverviewClient";
export default async function ServerOverview() {
return (
<ServerOverviewClient />
)
}

View 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"}
/>
);
}

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

View 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"}
/>
);
}

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

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

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

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

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

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

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

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

View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

4
prettier.config.js Normal file
View File

@ -0,0 +1,4 @@
// prettier.config.js
module.exports = {
plugins: ["prettier-plugin-tailwindcss"],
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
public/blog-man.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
public/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

BIN
public/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/hamster.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

22
public/manifest.json Normal file
View 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"
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

191
styles/globals.css Normal file
View 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
View 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
View 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"]
}