commit 3085a41b117e21e27ad397c5630f017b41011c9c Author: hamster1963 <1410514192@qq.com> Date: Sat Jul 27 02:17:07 2024 +0800 first commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..735293d --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +NezhaBaseUrl=http://0.0.0.0:8008 +NezhaAuth=5hAY3QX6Nl9B3UOQgB26KdsdS1dsdUdM \ No newline at end of file diff --git a/.github/shotOne.png b/.github/shotOne.png new file mode 100644 index 0000000..17a961a Binary files /dev/null and b/.github/shotOne.png differ diff --git a/.github/shotTwo.png b/.github/shotTwo.png new file mode 100644 index 0000000..e8f0d94 Binary files /dev/null and b/.github/shotTwo.png differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8ea49c --- /dev/null +++ b/.gitignore @@ -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 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..fda6d95 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +

HomeDash

+
+HomeDash 是一个基于 Next.js 和 Shadcn 的仪表盘 +
+Demo地址: https://dash.buycoffee.top + +![screen-shot-one](/.github/shotOne.png) +![screen-shot-two](/.github/shotTwo.png) + +
diff --git a/app/(main)/ClientComponents/LiveTag.tsx b/app/(main)/ClientComponents/LiveTag.tsx new file mode 100644 index 0000000..46cba27 --- /dev/null +++ b/app/(main)/ClientComponents/LiveTag.tsx @@ -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 ? ( + + Synced + + + ) : ( + + Static + + ); +} diff --git a/app/(main)/ClientComponents/ServerListClient.tsx b/app/(main)/ClientComponents/ServerListClient.tsx new file mode 100644 index 0000000..a8fe942 --- /dev/null +++ b/app/(main)/ClientComponents/ServerListClient.tsx @@ -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 ( +
+ {sortedResult.map( + (server: any) => ( + + ), + )} +
+ ); +} diff --git a/app/(main)/ClientComponents/ServerOverviewClient.tsx b/app/(main)/ClientComponents/ServerOverviewClient.tsx new file mode 100644 index 0000000..cedebd4 --- /dev/null +++ b/app/(main)/ClientComponents/ServerOverviewClient.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import blogMan from "@/public/blog-man.webp"; +import Image from "next/image"; +import useSWR from "swr"; +import { formatBytes, nezhaFetcher } from "@/lib/utils"; +import { Loader } from "@/components/loading/Loader"; + +export default function ServerOverviewClient() { + const { data } = useSWR('/api/server', nezhaFetcher); + + return ( +
+ + +
+

Total servers

+
+ + + + {data ?

{data?.result.length}

:
} +
+
+
+
+ + +
+

Online servers

+
+ + + + + {data ?

{data?.live_servers}

:
} +
+
+
+
+ + +
+

Offline servers

+
+ + + + + {data ?

{data?.offline_servers}

:
} +
+ + +
+
+
+ + +
+

Total bandwidth

+ {data ?

{formatBytes(data?.total_bandwidth)}

:
} +
+ {'Hamster1963'} +
+
+
+ ) +} \ No newline at end of file diff --git a/app/(main)/header.tsx b/app/(main)/header.tsx new file mode 100644 index 0000000..f062240 --- /dev/null +++ b/app/(main)/header.tsx @@ -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 ( +
+
+
+
+ apple-touch-icon +
+ HomeDash + +

+ Simple and beautiful dashboard +

+
+ {/* */} +
+ +
+ ); +} + +function Overview() { + const [mouted, setMounted] = useState(false); + useEffect(() => { + setMounted(true); + }, []); + const time = DateTime.TIME_SIMPLE; + time.hour12 = true; + + return ( +
+

👋 Overview

+
+

where the time is

+ {mouted && ( +

+ {DateTime.now().setLocale("en-US").toLocaleString(time)} +

+ )} +
+
+ ); +} + +export default Header; diff --git a/app/(main)/layout.tsx b/app/(main)/layout.tsx new file mode 100644 index 0000000..19b2d6a --- /dev/null +++ b/app/(main)/layout.tsx @@ -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 ( +
+
+
+ + {/*
+
+ ); +} diff --git a/app/(main)/nav.tsx b/app/(main)/nav.tsx new file mode 100644 index 0000000..3f8b907 --- /dev/null +++ b/app/(main)/nav.tsx @@ -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 ( +
+
+ {siteUrlList.map((site, index) => ( +
+ {index !== 0 && ( +

+ / +

+ )} + + {site.name} + +
+ ))} +
+
+ ); +} diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx new file mode 100644 index 0000000..a03186d --- /dev/null +++ b/app/(main)/page.tsx @@ -0,0 +1,15 @@ +import ServerList from "@/components/ServerList"; +import ServerOverview from "@/components/ServerOverview"; + + + +export default function Home() { + + return ( +
+ + +
+
+ ); +} diff --git a/app/android-chrome-192x192.png b/app/android-chrome-192x192.png new file mode 100644 index 0000000..6b354f0 Binary files /dev/null and b/app/android-chrome-192x192.png differ diff --git a/app/android-chrome-512x512.png b/app/android-chrome-512x512.png new file mode 100644 index 0000000..516d91b Binary files /dev/null and b/app/android-chrome-512x512.png differ diff --git a/app/api/server/route.ts b/app/api/server/route.ts new file mode 100644 index 0000000..ac94199 --- /dev/null +++ b/app/api/server/route.ts @@ -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 }) + } + +} \ No newline at end of file diff --git a/app/apple-touch-icon.png b/app/apple-touch-icon.png new file mode 100644 index 0000000..5e07a89 Binary files /dev/null and b/app/apple-touch-icon.png differ diff --git a/app/favicon-16x16.png b/app/favicon-16x16.png new file mode 100644 index 0000000..7d0e77e Binary files /dev/null and b/app/favicon-16x16.png differ diff --git a/app/favicon-32x32.png b/app/favicon-32x32.png new file mode 100644 index 0000000..9a7598e Binary files /dev/null and b/app/favicon-32x32.png differ diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..bb78ebd Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..da61a5f --- /dev/null +++ b/app/layout.tsx @@ -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 ( + + + + + {children} + + + + ); +} diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..3c78834 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,27 @@ +import Image from "next/image"; +import Link from "next/link"; + +export default function NotFoundPage() { + return ( +
+
+ TARDIS +
+

+ 404 Not Found +

+

TARDIS ERROR!

+ + Doctor? + +
+
+
+ ); +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..23316e6 Binary files /dev/null and b/bun.lockb differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..ef381e8 --- /dev/null +++ b/components.json @@ -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" + } +} diff --git a/components/BlurLayer.tsx b/components/BlurLayer.tsx new file mode 100644 index 0000000..745b752 --- /dev/null +++ b/components/BlurLayer.tsx @@ -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) => ( +
+ )); + + return ( +
+
{layers}
+
+ ); +}; + +export default BlurLayers; diff --git a/components/Icon.tsx b/components/Icon.tsx new file mode 100644 index 0000000..aa97669 --- /dev/null +++ b/components/Icon.tsx @@ -0,0 +1,7 @@ +export function GitHubIcon(props: React.ComponentPropsWithoutRef<"svg">) { + return ( + + + + ); +} diff --git a/components/ServerCard.tsx b/components/ServerCard.tsx new file mode 100644 index 0000000..c9757b6 --- /dev/null +++ b/components/ServerCard.tsx @@ -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" ? ( + + + + +
+

{name}

+ +
+
+ Online: {uptime.toFixed(0)} Days +
+
+
+
+

CPU

+
{cpu.toFixed(2)}%
+ +
+
+

Mem

+
{mem.toFixed(2)}%
+ +
+
+

STG

+
{stg.toFixed(2)}%
+ +
+
+

Upload

+
{up.toFixed(2)}Mb/s
+
+
+

Download

+
{down.toFixed(2)}Mb/s
+
+
+
+ ) : ( + + + + +
+

{name}

+ +
+
+ Offline +
+
+
+ ); +} diff --git a/components/ServerList.tsx b/components/ServerList.tsx new file mode 100644 index 0000000..db72679 --- /dev/null +++ b/components/ServerList.tsx @@ -0,0 +1,11 @@ +import React from "react"; + +import ServerListClient from "@/app/(main)/ClientComponents/ServerListClient"; + + + +export default async function ServerList() { + return ( + + ); +} diff --git a/components/ServerOverview.tsx b/components/ServerOverview.tsx new file mode 100644 index 0000000..d5523fa --- /dev/null +++ b/components/ServerOverview.tsx @@ -0,0 +1,12 @@ +import ServerOverviewClient from "@/app/(main)/ClientComponents/ServerOverviewClient"; + + + + +export default async function ServerOverview() { + return ( + + ) + + +} \ No newline at end of file diff --git a/components/ServerUsageBar.tsx b/components/ServerUsageBar.tsx new file mode 100644 index 0000000..8c5b72b --- /dev/null +++ b/components/ServerUsageBar.tsx @@ -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 ( + 90 + ? "bg-red-500" + : value > 70 + ? "bg-orange-400" + : "bg-green-500" + } + className={"h-[3px] rounded-sm"} + /> + ); +} diff --git a/components/SubscribeCard.tsx b/components/SubscribeCard.tsx new file mode 100644 index 0000000..b52d9fa --- /dev/null +++ b/components/SubscribeCard.tsx @@ -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 ( + + + + {provider} + + {nextBillDay !== -1 && ( +
+

+ {nextBillDay} +

+

|

+
+ )} +
+ +
+

{behavior}

+ +
+ +
+

{value}

+

+ {unit} +

+
+ +
+
+ ); +} + +export default SubscribeCard; diff --git a/components/client/NextToast.tsx b/components/client/NextToast.tsx new file mode 100644 index 0000000..6a94343 --- /dev/null +++ b/components/client/NextToast.tsx @@ -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 = { + light: "light", + dark: "dark", + system: "system", + }; + + // 使用类型断言确保theme是一个ThemeType + const selectedTheme = theme ? themeMap[theme as ThemeType] : "system"; + + return ( + + ); +} diff --git a/components/client/client-side-refresh.tsx b/components/client/client-side-refresh.tsx new file mode 100644 index 0000000..2a0cfd8 --- /dev/null +++ b/components/client/client-side-refresh.tsx @@ -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; +} diff --git a/components/client/theme-switcher.tsx b/components/client/theme-switcher.tsx new file mode 100644 index 0000000..95a59f6 --- /dev/null +++ b/components/client/theme-switcher.tsx @@ -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 ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/components/loading/Loader.tsx b/components/loading/Loader.tsx new file mode 100644 index 0000000..d8f2108 --- /dev/null +++ b/components/loading/Loader.tsx @@ -0,0 +1,13 @@ +const bars = Array(8).fill(0); + +export const Loader = ({ visible }: { visible: boolean }) => { + return ( +
+
+ {bars.map((_, i) => ( +
+ ))} +
+
+ ); +}; diff --git a/components/ui/animated-tooltip.tsx b/components/ui/animated-tooltip.tsx new file mode 100644 index 0000000..66e1d95 --- /dev/null +++ b/components/ui/animated-tooltip.tsx @@ -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) => ( +
+ + {item.name} + +
+ ))} + + ); +}; diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..b12af0a --- /dev/null +++ b/components/ui/badge.tsx @@ -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, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ); +} + +export { Badge, badgeVariants }; diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..856f6e5 --- /dev/null +++ b/components/ui/button.tsx @@ -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, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + }, +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..c5b32be --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,86 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +}; diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..b88da41 --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -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, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuPortal, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +}; diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..9d631e7 --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx new file mode 100644 index 0000000..3e4cf52 --- /dev/null +++ b/components/ui/navigation-menu.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + +)); +NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName; + +const NavigationMenuList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +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, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children}{" "} + +)); +NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName; + +const NavigationMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName; + +const NavigationMenuLink = NavigationMenuPrimitive.Link; + +const NavigationMenuViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ +
+)); +NavigationMenuViewport.displayName = + NavigationMenuPrimitive.Viewport.displayName; + +const NavigationMenuIndicator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +
+ +)); +NavigationMenuIndicator.displayName = + NavigationMenuPrimitive.Indicator.displayName; + +export { + NavigationMenu, + NavigationMenuContent, + NavigationMenuIndicator, + NavigationMenuItem, + NavigationMenuLink, + NavigationMenuList, + NavigationMenuTrigger, + navigationMenuTriggerStyle, + NavigationMenuViewport, +}; diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx new file mode 100644 index 0000000..7d7e9fa --- /dev/null +++ b/components/ui/progress.tsx @@ -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, + React.ComponentPropsWithoutRef & { + indicatorClassName?: string; // 添加一个新的可选属性来自定义Indicator的类名 + } +>(({ className, value, indicatorClassName, ...props }, ref) => ( + + + +)); +Progress.displayName = ProgressPrimitive.Root.displayName; + +export { Progress }; diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx new file mode 100644 index 0000000..9ac3b95 --- /dev/null +++ b/components/ui/separator.tsx @@ -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, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref, + ) => ( + + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..f075348 --- /dev/null +++ b/components/ui/sheet.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, 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, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = "SheetHeader"; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetFooter.displayName = "SheetFooter"; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetOverlay, + SheetPortal, + SheetTitle, + SheetTrigger, +}; diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..2cdf440 --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx new file mode 100644 index 0000000..383c2be --- /dev/null +++ b/components/ui/tooltip.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger }; diff --git a/lib/sseFetch.tsx b/lib/sseFetch.tsx new file mode 100644 index 0000000..7667e11 --- /dev/null +++ b/lib/sseFetch.tsx @@ -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( + url, + (key: string | URL, { next }: SWRSubscriptionOptions) => { + 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(); + }; + }); +} diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..4025a4e --- /dev/null +++ b/lib/utils.ts @@ -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; + }); + \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..9b4c182 --- /dev/null +++ b/next.config.mjs @@ -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)); diff --git a/package.json b/package.json new file mode 100644 index 0000000..5deab95 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 0000000..fa7765f --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,4 @@ +// prettier.config.js +module.exports = { + plugins: ["prettier-plugin-tailwindcss"], +}; diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000..6b354f0 Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000..516d91b Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..5e07a89 Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/blog-man.webp b/public/blog-man.webp new file mode 100644 index 0000000..d1dc301 Binary files /dev/null and b/public/blog-man.webp differ diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000..7d0e77e Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000..9a7598e Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..bb78ebd Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/hamster.webp b/public/hamster.webp new file mode 100644 index 0000000..75030b9 Binary files /dev/null and b/public/hamster.webp differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..e00a07d --- /dev/null +++ b/public/manifest.json @@ -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" +} diff --git a/public/swe-worker-5c72df51bb1f6ee0.js b/public/swe-worker-5c72df51bb1f6ee0.js new file mode 100644 index 0000000..36e6e59 --- /dev/null +++ b/public/swe-worker-5c72df51bb1f6ee0.js @@ -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(//g))/rel=['"]stylesheet['"]/.test(s)&&t.push(a.match(r).then(e=>e?Promise.resolve():a.add(r)));for(let[,a]of e.matchAll(//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()}}; \ No newline at end of file diff --git a/public/tardis.jpg b/public/tardis.jpg new file mode 100644 index 0000000..6f81c11 Binary files /dev/null and b/public/tardis.jpg differ diff --git a/styles/globals.css b/styles/globals.css new file mode 100644 index 0000000..a776841 --- /dev/null +++ b/styles/globals.css @@ -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; + } +} diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..0628323 --- /dev/null +++ b/tailwind.config.ts @@ -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; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..e7ff90f --- /dev/null +++ b/tsconfig.json @@ -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"] +}