mirror of
https://github.com/Buriburizaem0n/nezha-dash-v1.git
synced 2026-05-06 05:48:41 +00:00
perf: use biome
This commit is contained in:
@@ -1,79 +1,109 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function AnimateCountClient({ count, className, minDigits }: { count: number; className?: string; minDigits?: number }) {
|
||||
const [previousCount, setPreviousCount] = useState(count)
|
||||
export function AnimateCountClient({
|
||||
count,
|
||||
className,
|
||||
minDigits,
|
||||
}: {
|
||||
count: number;
|
||||
className?: string;
|
||||
minDigits?: number;
|
||||
}) {
|
||||
const [previousCount, setPreviousCount] = useState(count);
|
||||
|
||||
useEffect(() => {
|
||||
if (count !== previousCount) {
|
||||
setTimeout(() => {
|
||||
setPreviousCount(count)
|
||||
}, 300)
|
||||
}
|
||||
}, [count])
|
||||
return (
|
||||
<AnimateCount
|
||||
key={count}
|
||||
preCount={previousCount}
|
||||
className={cn("inline-flex items-center leading-none", className)}
|
||||
minDigits={minDigits}
|
||||
data-issues-count-animation
|
||||
>
|
||||
{count}
|
||||
</AnimateCount>
|
||||
)
|
||||
useEffect(() => {
|
||||
if (count !== previousCount) {
|
||||
setTimeout(() => {
|
||||
setPreviousCount(count);
|
||||
}, 300);
|
||||
}
|
||||
}, [count, previousCount]);
|
||||
return (
|
||||
<AnimateCount
|
||||
key={count}
|
||||
preCount={previousCount}
|
||||
className={cn("inline-flex items-center leading-none", className)}
|
||||
minDigits={minDigits}
|
||||
data-issues-count-animation
|
||||
>
|
||||
{count}
|
||||
</AnimateCount>
|
||||
);
|
||||
}
|
||||
|
||||
export default AnimateCountClient
|
||||
export default AnimateCountClient;
|
||||
|
||||
export function AnimateCount({
|
||||
children: count,
|
||||
className,
|
||||
preCount,
|
||||
minDigits = 1,
|
||||
...props
|
||||
children: count,
|
||||
className,
|
||||
preCount,
|
||||
minDigits = 1,
|
||||
...props
|
||||
}: {
|
||||
children: number
|
||||
className?: string
|
||||
preCount?: number
|
||||
minDigits?: number
|
||||
children: number;
|
||||
className?: string;
|
||||
preCount?: number;
|
||||
minDigits?: number;
|
||||
}) {
|
||||
const currentDigits = count.toString().split("")
|
||||
const previousDigits = (preCount !== undefined ? preCount.toString() : count - 1 >= 0 ? (count - 1).toString() : "0").split("")
|
||||
const currentDigits = count.toString().split("");
|
||||
const previousDigits = (
|
||||
preCount !== undefined
|
||||
? preCount.toString()
|
||||
: count - 1 >= 0
|
||||
? (count - 1).toString()
|
||||
: "0"
|
||||
).split("");
|
||||
|
||||
// Ensure both numbers meet the minimum length requirement and maintain the same length for animation
|
||||
const maxLength = Math.max(previousDigits.length, currentDigits.length, minDigits)
|
||||
while (previousDigits.length < maxLength) {
|
||||
previousDigits.unshift("0")
|
||||
}
|
||||
while (currentDigits.length < maxLength) {
|
||||
currentDigits.unshift("0")
|
||||
}
|
||||
// Ensure both numbers meet the minimum length requirement and maintain the same length for animation
|
||||
const maxLength = Math.max(
|
||||
previousDigits.length,
|
||||
currentDigits.length,
|
||||
minDigits,
|
||||
);
|
||||
while (previousDigits.length < maxLength) {
|
||||
previousDigits.unshift("0");
|
||||
}
|
||||
while (currentDigits.length < maxLength) {
|
||||
currentDigits.unshift("0");
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...props} className={cn("flex h-[1em] items-center", className)}>
|
||||
{currentDigits.map((digit, index) => {
|
||||
const hasChanged = digit !== previousDigits[index]
|
||||
return (
|
||||
<div
|
||||
key={`${index}-${digit}`}
|
||||
className={cn("relative flex h-full min-w-[0.6em] items-center text-center", {
|
||||
"min-w-[0.2em]": digit === ".",
|
||||
})}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
data-issues-count-exit
|
||||
className={cn("absolute inset-0 flex items-center justify-center", hasChanged ? "animate" : "opacity-0")}
|
||||
>
|
||||
{previousDigits[index]}
|
||||
</div>
|
||||
<div data-issues-count-enter className={cn("absolute inset-0 flex items-center justify-center", hasChanged && "animate")}>
|
||||
{digit}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div {...props} className={cn("flex h-[1em] items-center", className)}>
|
||||
{currentDigits.map((digit, index) => {
|
||||
const hasChanged = digit !== previousDigits[index];
|
||||
return (
|
||||
<div
|
||||
key={`${index}-${digit}`}
|
||||
className={cn(
|
||||
"relative flex h-full min-w-[0.6em] items-center text-center",
|
||||
{
|
||||
"min-w-[0.2em]": digit === ".",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
data-issues-count-exit
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-center justify-center",
|
||||
hasChanged ? "animate" : "opacity-0",
|
||||
)}
|
||||
>
|
||||
{previousDigits[index]}
|
||||
</div>
|
||||
<div
|
||||
data-issues-count-enter
|
||||
className={cn(
|
||||
"absolute inset-0 flex items-center justify-center",
|
||||
hasChanged && "animate",
|
||||
)}
|
||||
>
|
||||
{digit}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,68 @@
|
||||
import { CycleTransferStats, NezhaServer } from "@/types/nezha-api"
|
||||
import React from "react"
|
||||
import type React from "react";
|
||||
import type { CycleTransferStats, NezhaServer } from "@/types/nezha-api";
|
||||
|
||||
import { CycleTransferStatsClient } from "./CycleTransferStatsClient"
|
||||
import { CycleTransferStatsClient } from "./CycleTransferStatsClient";
|
||||
|
||||
interface CycleTransferStatsProps {
|
||||
serverList: NezhaServer[]
|
||||
cycleStats: CycleTransferStats
|
||||
className?: string
|
||||
serverList: NezhaServer[];
|
||||
cycleStats: CycleTransferStats;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({ serverList, cycleStats, className }) => {
|
||||
if (serverList.length === 0) {
|
||||
return null
|
||||
}
|
||||
export const CycleTransferStatsCard: React.FC<CycleTransferStatsProps> = ({
|
||||
serverList,
|
||||
cycleStats,
|
||||
className,
|
||||
}) => {
|
||||
if (serverList.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverIdList = serverList.map((server) => server.id.toString())
|
||||
const serverIdList = serverList.map((server) => server.id.toString());
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
||||
if (!cycleData.server_name) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<section className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{Object.entries(cycleStats).map(([cycleId, cycleData]) => {
|
||||
if (!cycleData.server_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.entries(cycleData.server_name).map(([serverId, serverName]) => {
|
||||
const transfer = cycleData.transfer?.[serverId] || 0
|
||||
const nextUpdate = cycleData.next_update?.[serverId]
|
||||
return Object.entries(cycleData.server_name).map(
|
||||
([serverId, serverName]) => {
|
||||
const transfer = cycleData.transfer?.[serverId] || 0;
|
||||
const nextUpdate = cycleData.next_update?.[serverId];
|
||||
|
||||
if (!serverIdList.includes(serverId)) {
|
||||
return null
|
||||
}
|
||||
if (!serverIdList.includes(serverId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!transfer && !nextUpdate) {
|
||||
return null
|
||||
}
|
||||
if (!transfer && !nextUpdate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CycleTransferStatsClient
|
||||
key={`${cycleId}-${serverId}`}
|
||||
name={cycleData.name}
|
||||
from={cycleData.from}
|
||||
to={cycleData.to}
|
||||
max={cycleData.max}
|
||||
serverStats={[
|
||||
{
|
||||
serverId,
|
||||
serverName,
|
||||
transfer,
|
||||
nextUpdate: nextUpdate || "",
|
||||
},
|
||||
]}
|
||||
className={className}
|
||||
/>
|
||||
)
|
||||
})
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<CycleTransferStatsClient
|
||||
key={`${cycleId}-${serverId}`}
|
||||
name={cycleData.name}
|
||||
from={cycleData.from}
|
||||
to={cycleData.to}
|
||||
max={cycleData.max}
|
||||
serverStats={[
|
||||
{
|
||||
serverId,
|
||||
serverName,
|
||||
transfer,
|
||||
nextUpdate: nextUpdate || "",
|
||||
},
|
||||
]}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default CycleTransferStatsCard
|
||||
export default CycleTransferStatsCard;
|
||||
|
||||
@@ -1,79 +1,96 @@
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CycleTransferStatsClientProps {
|
||||
name: string
|
||||
from: string
|
||||
to: string
|
||||
max: number
|
||||
serverStats: Array<{
|
||||
serverId: string
|
||||
serverName: string
|
||||
transfer: number
|
||||
nextUpdate: string
|
||||
}>
|
||||
className?: string
|
||||
name: string;
|
||||
from: string;
|
||||
to: string;
|
||||
max: number;
|
||||
serverStats: Array<{
|
||||
serverId: string;
|
||||
serverName: string;
|
||||
transfer: number;
|
||||
nextUpdate: string;
|
||||
}>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CycleTransferStatsClient: React.FC<CycleTransferStatsClientProps> = ({ name, from, to, max, serverStats, className }) => {
|
||||
const { t } = useTranslation()
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-xs transition-all duration-200 dark:shadow-none",
|
||||
className,
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
|
||||
const progress = (transfer / max) * 100
|
||||
export const CycleTransferStatsClient: React.FC<
|
||||
CycleTransferStatsClientProps
|
||||
> = ({ name, from, to, max, serverStats, className }) => {
|
||||
const { t } = useTranslation();
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full bg-white px-4 py-3.5 rounded-lg border bg-card text-card-foreground hover:shadow-xs transition-all duration-200 dark:shadow-none",
|
||||
className,
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{serverStats.map(({ serverId, serverName, transfer, nextUpdate }) => {
|
||||
const progress = (transfer / max) * 100;
|
||||
|
||||
return (
|
||||
<div key={serverId} className="space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{serverName}</span>
|
||||
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">{name}</div>
|
||||
</div>
|
||||
return (
|
||||
<div key={serverId} className="space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{serverName}
|
||||
</span>
|
||||
<div className="bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 px-2 py-0.5 rounded text-xs font-medium">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Section */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">{formatBytes(transfer)}</span>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">/ {formatBytes(max)}</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">{progress.toFixed(1)}%</span>
|
||||
</div>
|
||||
{/* Progress Section */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-baseline gap-1">
|
||||
<span className="text-sm font-medium text-neutral-800 dark:text-neutral-200">
|
||||
{formatBytes(transfer)}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
/ {formatBytes(max)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-neutral-600 dark:text-neutral-300">
|
||||
{progress.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative h-1.5">
|
||||
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
|
||||
<div
|
||||
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative h-1.5">
|
||||
<div className="absolute inset-0 bg-neutral-100 dark:bg-neutral-800 rounded-full" />
|
||||
<div
|
||||
className="absolute inset-0 bg-emerald-500 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.min(progress, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||
<span>
|
||||
{new Date(from).toLocaleDateString()} - {new Date(to).toLocaleDateString()}
|
||||
</span>
|
||||
<span>
|
||||
{t("cycleTransfer.nextUpdate")}: {new Date(nextUpdate).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400">
|
||||
<span>
|
||||
{new Date(from).toLocaleDateString()} -{" "}
|
||||
{new Date(to).toLocaleDateString()}
|
||||
</span>
|
||||
<span>
|
||||
{t("cycleTransfer.nextUpdate")}:{" "}
|
||||
{new Date(nextUpdate).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CycleTransferStatsClient
|
||||
export default CycleTransferStatsClient;
|
||||
|
||||
+117
-107
@@ -1,119 +1,129 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { CommandDialog, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, CommandSeparator } from "@/components/ui/command"
|
||||
import { useCommand } from "@/hooks/use-command"
|
||||
import { useTheme } from "@/hooks/use-theme"
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
||||
import { formatNezhaInfo } from "@/lib/utils"
|
||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||
import { Home, Moon, Sun, SunMoon } from "lucide-react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Home, Moon, Sun, SunMoon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/components/ui/command";
|
||||
import { useCommand } from "@/hooks/use-command";
|
||||
import { useTheme } from "@/hooks/use-theme";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { formatNezhaInfo } from "@/lib/utils";
|
||||
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||
|
||||
export function DashCommand() {
|
||||
const { isOpen, closeCommand, toggleCommand } = useCommand()
|
||||
const [search, setSearch] = useState("")
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
const { setTheme } = useTheme()
|
||||
const { isOpen, closeCommand, toggleCommand } = useCommand();
|
||||
const [search, setSearch] = useState("");
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
const { lastMessage, connected } = useWebSocketContext()
|
||||
const { lastMessage, connected } = useWebSocketContext();
|
||||
|
||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||
const nezhaWsData = lastMessage
|
||||
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
toggleCommand()
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
toggleCommand();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [toggleCommand])
|
||||
document.addEventListener("keydown", down);
|
||||
return () => document.removeEventListener("keydown", down);
|
||||
}, [toggleCommand]);
|
||||
|
||||
if (!connected || !nezhaWsData) return null
|
||||
if (!connected || !nezhaWsData) return null;
|
||||
|
||||
const shortcuts = [
|
||||
{
|
||||
keywords: ["home", "homepage"],
|
||||
icon: <Home />,
|
||||
label: t("Home"),
|
||||
action: () => navigate("/"),
|
||||
},
|
||||
{
|
||||
keywords: ["light", "theme", "lightmode"],
|
||||
icon: <Sun />,
|
||||
label: t("ToggleLightMode"),
|
||||
action: () => setTheme("light"),
|
||||
},
|
||||
{
|
||||
keywords: ["dark", "theme", "darkmode"],
|
||||
icon: <Moon />,
|
||||
label: t("ToggleDarkMode"),
|
||||
action: () => setTheme("dark"),
|
||||
},
|
||||
{
|
||||
keywords: ["system", "theme", "systemmode"],
|
||||
icon: <SunMoon />,
|
||||
label: t("ToggleSystemMode"),
|
||||
action: () => setTheme("system"),
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
value: `${item.keywords.join(" ")} ${item.label}`,
|
||||
}))
|
||||
const shortcuts = [
|
||||
{
|
||||
keywords: ["home", "homepage"],
|
||||
icon: <Home />,
|
||||
label: t("Home"),
|
||||
action: () => navigate("/"),
|
||||
},
|
||||
{
|
||||
keywords: ["light", "theme", "lightmode"],
|
||||
icon: <Sun />,
|
||||
label: t("ToggleLightMode"),
|
||||
action: () => setTheme("light"),
|
||||
},
|
||||
{
|
||||
keywords: ["dark", "theme", "darkmode"],
|
||||
icon: <Moon />,
|
||||
label: t("ToggleDarkMode"),
|
||||
action: () => setTheme("dark"),
|
||||
},
|
||||
{
|
||||
keywords: ["system", "theme", "systemmode"],
|
||||
icon: <SunMoon />,
|
||||
label: t("ToggleSystemMode"),
|
||||
action: () => setTheme("system"),
|
||||
},
|
||||
].map((item) => ({
|
||||
...item,
|
||||
value: `${item.keywords.join(" ")} ${item.label}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandDialog open={isOpen} onOpenChange={closeCommand}>
|
||||
<CommandInput placeholder={t("TypeCommand")} value={search} onValueChange={setSearch} />
|
||||
<CommandList className="border-t">
|
||||
<CommandEmpty>{t("NoResults")}</CommandEmpty>
|
||||
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
|
||||
<>
|
||||
<CommandGroup heading={t("Servers")}>
|
||||
{nezhaWsData.servers.map((server) => (
|
||||
<CommandItem
|
||||
key={server.id}
|
||||
value={server.name}
|
||||
onSelect={() => {
|
||||
navigate(`/server/${server.id}`)
|
||||
closeCommand()
|
||||
}}
|
||||
>
|
||||
{formatNezhaInfo(nezhaWsData.now, server).online ? (
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
|
||||
) : (
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
|
||||
)}
|
||||
<span>{server.name}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
<CommandSeparator />
|
||||
return (
|
||||
<CommandDialog open={isOpen} onOpenChange={closeCommand}>
|
||||
<CommandInput
|
||||
placeholder={t("TypeCommand")}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
<CommandList className="border-t">
|
||||
<CommandEmpty>{t("NoResults")}</CommandEmpty>
|
||||
{nezhaWsData.servers && nezhaWsData.servers.length > 0 && (
|
||||
<CommandGroup heading={t("Servers")}>
|
||||
{nezhaWsData.servers.map((server) => (
|
||||
<CommandItem
|
||||
key={server.id}
|
||||
value={server.name}
|
||||
onSelect={() => {
|
||||
navigate(`/server/${server.id}`);
|
||||
closeCommand();
|
||||
}}
|
||||
>
|
||||
{formatNezhaInfo(nezhaWsData.now, server).online ? (
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center" />
|
||||
) : (
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center" />
|
||||
)}
|
||||
<span>{server.name}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
)}
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading={t("Shortcuts")}>
|
||||
{shortcuts.map((item) => (
|
||||
<CommandItem
|
||||
key={item.label}
|
||||
value={item.value}
|
||||
onSelect={() => {
|
||||
item.action()
|
||||
closeCommand()
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
)
|
||||
<CommandGroup heading={t("Shortcuts")}>
|
||||
{shortcuts.map((item) => (
|
||||
<CommandItem
|
||||
key={item.label}
|
||||
value={item.value}
|
||||
onSelect={() => {
|
||||
item.action();
|
||||
closeCommand();
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
<span>{item.label}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,41 @@
|
||||
import React from "react"
|
||||
import React from "react";
|
||||
|
||||
import ErrorPage from "../pages/ErrorPage"
|
||||
import ErrorPage from "../pages/ErrorPage";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error?: Error
|
||||
hasError: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
class ErrorBoundary extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
}
|
||||
}
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return {
|
||||
hasError: true,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return <ErrorPage code={500} message={this.state.error?.message || "应用程序发生错误"} />
|
||||
}
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<ErrorPage
|
||||
code={500}
|
||||
message={this.state.error?.message || "应用程序发生错误"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
export default ErrorBoundary;
|
||||
|
||||
+58
-47
@@ -1,52 +1,63 @@
|
||||
import { fetchSetting } from "@/lib/nezha-api"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchSetting } from "@/lib/nezha-api";
|
||||
|
||||
const Footer: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMac = /macintosh|mac os x/i.test(navigator.userAgent)
|
||||
const { t } = useTranslation();
|
||||
const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
|
||||
|
||||
const { data: settingData } = useQuery({
|
||||
queryKey: ["setting"],
|
||||
queryFn: () => fetchSetting(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
const { data: settingData } = useQuery({
|
||||
queryKey: ["setting"],
|
||||
queryFn: () => fetchSetting(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
|
||||
<section className="flex flex-col">
|
||||
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
|
||||
<div className="flex items-center gap-1">
|
||||
©2020-{new Date().getFullYear()}{" "}
|
||||
<a href={"https://github.com/naiba/nezha"} target="_blank">
|
||||
Nezha
|
||||
</a>
|
||||
<p>{settingData?.data?.version || ""}</p>
|
||||
</div>
|
||||
<div className="server-footer-theme flex flex-col items-center sm:items-end">
|
||||
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||
{isMac ? <span className="text-xs">⌘</span> : "Ctrl "}K
|
||||
</kbd>
|
||||
</p>
|
||||
<section>
|
||||
{t("footer.themeBy")}
|
||||
<a href={"https://github.com/hamster1963/nezha-dash"} target="_blank">
|
||||
nezha-dash
|
||||
</a>
|
||||
{import.meta.env.VITE_GIT_HASH && (
|
||||
<a href={"https://github.com/hamster1963/nezha-dash-v1/commit/" + import.meta.env.VITE_GIT_HASH} className="ml-1">
|
||||
({import.meta.env.VITE_GIT_HASH})
|
||||
</a>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<footer className="mx-auto w-full max-w-5xl px-4 lg:px-0 pb-4 server-footer">
|
||||
<section className="flex flex-col">
|
||||
<section className="mt-1 flex items-center sm:flex-row flex-col justify-between gap-2 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50 server-footer-name">
|
||||
<div className="flex items-center gap-1">
|
||||
©2020-{new Date().getFullYear()}{" "}
|
||||
<a
|
||||
href={"https://github.com/naiba/nezha"}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
Nezha
|
||||
</a>
|
||||
<p>{settingData?.data?.version || ""}</p>
|
||||
</div>
|
||||
<div className="server-footer-theme flex flex-col items-center sm:items-end">
|
||||
<p className="mt-1 text-[13px] font-light tracking-tight text-neutral-600/50 dark:text-neutral-300/50">
|
||||
<kbd className="pointer-events-none mx-1 inline-flex h-4 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100">
|
||||
{isMac ? <span className="text-xs">⌘</span> : "Ctrl "}K
|
||||
</kbd>
|
||||
</p>
|
||||
<section>
|
||||
{t("footer.themeBy")}
|
||||
<a
|
||||
href={"https://github.com/hamster1963/nezha-dash"}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
nezha-dash
|
||||
</a>
|
||||
{import.meta.env.VITE_GIT_HASH && (
|
||||
<a
|
||||
href={`https://github.com/hamster1963/nezha-dash-v1/commit/${import.meta.env.VITE_GIT_HASH}`}
|
||||
className="ml-1"
|
||||
>
|
||||
({import.meta.env.VITE_GIT_HASH})
|
||||
</a>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer
|
||||
export default Footer;
|
||||
|
||||
+210
-163
@@ -1,186 +1,233 @@
|
||||
import useTooltip from "@/hooks/use-tooltip"
|
||||
import { geoJsonString } from "@/lib/geo-json-string"
|
||||
import { countryCoordinates } from "@/lib/geo-limit"
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
||||
import { NezhaServer } from "@/types/nezha-api"
|
||||
import { geoEquirectangular, geoPath } from "d3-geo"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { geoEquirectangular, geoPath } from "d3-geo";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import useTooltip from "@/hooks/use-tooltip";
|
||||
import { geoJsonString } from "@/lib/geo-json-string";
|
||||
import { countryCoordinates } from "@/lib/geo-limit";
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||
import type { NezhaServer } from "@/types/nezha-api";
|
||||
|
||||
import MapTooltip from "./MapTooltip"
|
||||
import MapTooltip from "./MapTooltip";
|
||||
|
||||
export default function GlobalMap({ serverList, now }: { serverList: NezhaServer[]; now: number }) {
|
||||
const { t } = useTranslation()
|
||||
const countryList: string[] = []
|
||||
const serverCounts: { [key: string]: number } = {}
|
||||
export default function GlobalMap({
|
||||
serverList,
|
||||
now,
|
||||
}: {
|
||||
serverList: NezhaServer[];
|
||||
now: number;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const countryList: string[] = [];
|
||||
const serverCounts: { [key: string]: number } = {};
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
serverList.forEach((server) => {
|
||||
if (server.country_code) {
|
||||
const countryCode = server.country_code.toUpperCase()
|
||||
if (!countryList.includes(countryCode)) {
|
||||
countryList.push(countryCode)
|
||||
}
|
||||
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1
|
||||
}
|
||||
})
|
||||
serverList.forEach((server) => {
|
||||
if (server.country_code) {
|
||||
const countryCode = server.country_code.toUpperCase();
|
||||
if (!countryList.includes(countryCode)) {
|
||||
countryList.push(countryCode);
|
||||
}
|
||||
serverCounts[countryCode] = (serverCounts[countryCode] || 0) + 1;
|
||||
}
|
||||
});
|
||||
|
||||
const width = 900
|
||||
const height = 500
|
||||
const width = 900;
|
||||
const height = 500;
|
||||
|
||||
const geoJson = JSON.parse(geoJsonString)
|
||||
const filteredFeatures = geoJson.features.filter((feature: { properties: { iso_a3_eh: string } }) => feature.properties.iso_a3_eh !== "")
|
||||
const geoJson = JSON.parse(geoJsonString);
|
||||
const filteredFeatures = geoJson.features.filter(
|
||||
(feature: { properties: { iso_a3_eh: string } }) =>
|
||||
feature.properties.iso_a3_eh !== "",
|
||||
);
|
||||
|
||||
return (
|
||||
<section
|
||||
className={cn("flex flex-col gap-4 mt-8", {
|
||||
"bg-card/70 rounded-lg p-4": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<p className="text-sm font-medium opacity-40">
|
||||
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
|
||||
</p>
|
||||
<div className="w-full overflow-x-auto">
|
||||
<InteractiveMap
|
||||
countries={countryList}
|
||||
serverCounts={serverCounts}
|
||||
width={width}
|
||||
height={height}
|
||||
filteredFeatures={filteredFeatures}
|
||||
nezhaServerList={serverList}
|
||||
now={now}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
return (
|
||||
<section
|
||||
className={cn("flex flex-col gap-4 mt-8", {
|
||||
"bg-card/70 rounded-lg p-4": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<p className="text-sm font-medium opacity-40">
|
||||
{t("map.Distributions")} {countryList.length} {t("map.Regions")}
|
||||
</p>
|
||||
<div className="w-full overflow-x-auto">
|
||||
<InteractiveMap
|
||||
countries={countryList}
|
||||
serverCounts={serverCounts}
|
||||
width={width}
|
||||
height={height}
|
||||
filteredFeatures={filteredFeatures}
|
||||
nezhaServerList={serverList}
|
||||
now={now}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
interface InteractiveMapProps {
|
||||
countries: string[]
|
||||
serverCounts: { [key: string]: number }
|
||||
width: number
|
||||
height: number
|
||||
filteredFeatures: {
|
||||
type: "Feature"
|
||||
properties: {
|
||||
iso_a2_eh: string
|
||||
[key: string]: string
|
||||
}
|
||||
geometry: never
|
||||
}[]
|
||||
nezhaServerList: NezhaServer[]
|
||||
now: number
|
||||
countries: string[];
|
||||
serverCounts: { [key: string]: number };
|
||||
width: number;
|
||||
height: number;
|
||||
filteredFeatures: {
|
||||
type: "Feature";
|
||||
properties: {
|
||||
iso_a2_eh: string;
|
||||
[key: string]: string;
|
||||
};
|
||||
geometry: never;
|
||||
}[];
|
||||
nezhaServerList: NezhaServer[];
|
||||
now: number;
|
||||
}
|
||||
|
||||
export function InteractiveMap({ countries, serverCounts, width, height, filteredFeatures, nezhaServerList, now }: InteractiveMapProps) {
|
||||
const { setTooltipData } = useTooltip()
|
||||
export function InteractiveMap({
|
||||
countries,
|
||||
serverCounts,
|
||||
width,
|
||||
height,
|
||||
filteredFeatures,
|
||||
nezhaServerList,
|
||||
now,
|
||||
}: InteractiveMapProps) {
|
||||
const { setTooltipData } = useTooltip();
|
||||
|
||||
const projection = geoEquirectangular()
|
||||
.scale(140)
|
||||
.translate([width / 2, height / 2])
|
||||
.rotate([-12, 0, 0])
|
||||
const projection = geoEquirectangular()
|
||||
.scale(140)
|
||||
.translate([width / 2, height / 2])
|
||||
.rotate([-12, 0, 0]);
|
||||
|
||||
const path = geoPath().projection(projection)
|
||||
const path = geoPath().projection(projection);
|
||||
|
||||
return (
|
||||
<div className="relative w-full aspect-2/1" onMouseLeave={() => setTooltipData(null)}>
|
||||
<svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} xmlns="http://www.w3.org/2000/svg" className="w-full h-auto">
|
||||
<defs>
|
||||
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<g>
|
||||
{/* Background rect to handle mouse events in empty areas */}
|
||||
<rect x="0" y="0" width={width} height={height} fill="transparent" onMouseEnter={() => setTooltipData(null)} />
|
||||
{filteredFeatures.map((feature, index) => {
|
||||
const isHighlighted = countries.includes(feature.properties.iso_a2_eh)
|
||||
return (
|
||||
<div
|
||||
className="relative w-full aspect-2/1"
|
||||
onMouseLeave={() => setTooltipData(null)}
|
||||
>
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-full h-auto"
|
||||
>
|
||||
<defs>
|
||||
<pattern id="dots" width="2" height="2" patternUnits="userSpaceOnUse">
|
||||
<circle cx="1" cy="1" r="0.5" fill="currentColor" />
|
||||
</pattern>
|
||||
</defs>
|
||||
<g>
|
||||
{/* Background rect to handle mouse events in empty areas */}
|
||||
<rect
|
||||
x="0"
|
||||
y="0"
|
||||
width={width}
|
||||
height={height}
|
||||
fill="transparent"
|
||||
onMouseEnter={() => setTooltipData(null)}
|
||||
/>
|
||||
{filteredFeatures.map((feature, index) => {
|
||||
const isHighlighted = countries.includes(
|
||||
feature.properties.iso_a2_eh,
|
||||
);
|
||||
|
||||
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0
|
||||
const serverCount = serverCounts[feature.properties.iso_a2_eh] || 0;
|
||||
|
||||
return (
|
||||
<path
|
||||
key={index}
|
||||
d={path(feature) || ""}
|
||||
className={
|
||||
isHighlighted
|
||||
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
|
||||
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
if (!isHighlighted) {
|
||||
setTooltipData(null)
|
||||
return
|
||||
}
|
||||
if (path.centroid(feature)) {
|
||||
const countryCode = feature.properties.iso_a2_eh
|
||||
const countryServers = nezhaServerList
|
||||
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode)
|
||||
.map((server: NezhaServer) => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
status: formatNezhaInfo(now, server).online,
|
||||
}))
|
||||
setTooltipData({
|
||||
centroid: path.centroid(feature),
|
||||
country: feature.properties.name,
|
||||
count: serverCount,
|
||||
servers: countryServers,
|
||||
})
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
return (
|
||||
<path
|
||||
key={index}
|
||||
d={path(feature) || ""}
|
||||
className={
|
||||
isHighlighted
|
||||
? "fill-green-700 hover:fill-green-600 dark:fill-green-900 dark:hover:fill-green-700 transition-all cursor-pointer"
|
||||
: "fill-neutral-200/50 dark:fill-neutral-800 stroke-neutral-300/40 dark:stroke-neutral-700 stroke-[0.5]"
|
||||
}
|
||||
onMouseEnter={() => {
|
||||
if (!isHighlighted) {
|
||||
setTooltipData(null);
|
||||
return;
|
||||
}
|
||||
if (path.centroid(feature)) {
|
||||
const countryCode = feature.properties.iso_a2_eh;
|
||||
const countryServers = nezhaServerList
|
||||
.filter(
|
||||
(server: NezhaServer) =>
|
||||
server.country_code?.toUpperCase() === countryCode,
|
||||
)
|
||||
.map((server: NezhaServer) => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
status: formatNezhaInfo(now, server).online,
|
||||
}));
|
||||
setTooltipData({
|
||||
centroid: path.centroid(feature),
|
||||
country: feature.properties.name,
|
||||
count: serverCount,
|
||||
servers: countryServers,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
|
||||
{countries.map((countryCode) => {
|
||||
// 检查该国家是否已经在 filteredFeatures 中
|
||||
const isInFilteredFeatures = filteredFeatures.some((feature) => feature.properties.iso_a2_eh === countryCode)
|
||||
{/* 渲染不在 filteredFeatures 中的国家标记点 */}
|
||||
{countries.map((countryCode) => {
|
||||
// 检查该国家是否已经在 filteredFeatures 中
|
||||
const isInFilteredFeatures = filteredFeatures.some(
|
||||
(feature) => feature.properties.iso_a2_eh === countryCode,
|
||||
);
|
||||
|
||||
// 如果已经在 filteredFeatures 中,跳过
|
||||
if (isInFilteredFeatures) return null
|
||||
// 如果已经在 filteredFeatures 中,跳过
|
||||
if (isInFilteredFeatures) return null;
|
||||
|
||||
// 获取国家的经纬度
|
||||
const coords = countryCoordinates[countryCode]
|
||||
if (!coords) return null
|
||||
// 获取国家的经纬度
|
||||
const coords = countryCoordinates[countryCode];
|
||||
if (!coords) return null;
|
||||
|
||||
// 使用投影函数将经纬度转换为 SVG 坐标
|
||||
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0]
|
||||
const serverCount = serverCounts[countryCode] || 0
|
||||
// 使用投影函数将经纬度转换为 SVG 坐标
|
||||
const [x, y] = projection([coords.lng, coords.lat]) || [0, 0];
|
||||
const serverCount = serverCounts[countryCode] || 0;
|
||||
|
||||
return (
|
||||
<g
|
||||
key={countryCode}
|
||||
onMouseEnter={() => {
|
||||
const countryServers = nezhaServerList
|
||||
.filter((server: NezhaServer) => server.country_code?.toUpperCase() === countryCode.toUpperCase())
|
||||
.map((server: NezhaServer) => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
status: formatNezhaInfo(now, server).online,
|
||||
}))
|
||||
setTooltipData({
|
||||
centroid: [x, y],
|
||||
country: coords.name,
|
||||
count: serverCount,
|
||||
servers: countryServers,
|
||||
})
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
<MapTooltip />
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<g
|
||||
key={countryCode}
|
||||
onMouseEnter={() => {
|
||||
const countryServers = nezhaServerList
|
||||
.filter(
|
||||
(server: NezhaServer) =>
|
||||
server.country_code?.toUpperCase() ===
|
||||
countryCode.toUpperCase(),
|
||||
)
|
||||
.map((server: NezhaServer) => ({
|
||||
id: server.id,
|
||||
name: server.name,
|
||||
status: formatNezhaInfo(now, server).online,
|
||||
}));
|
||||
setTooltipData({
|
||||
centroid: [x, y],
|
||||
country: coords.name,
|
||||
count: serverCount,
|
||||
servers: countryServers,
|
||||
});
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r={4}
|
||||
className="fill-sky-700 stroke-white hover:fill-sky-600 dark:fill-sky-900 dark:hover:fill-sky-700 transition-all"
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
<MapTooltip />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,111 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { m } from "framer-motion"
|
||||
import { s } from "node_modules/framer-motion/dist/types.d-6pKw1mTI"
|
||||
import { createRef, useEffect, useRef } from "react"
|
||||
import { m } from "framer-motion";
|
||||
import { createRef, useEffect, useRef } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function GroupSwitch({
|
||||
tabs,
|
||||
currentTab,
|
||||
setCurrentTab,
|
||||
tabs,
|
||||
currentTab,
|
||||
setCurrentTab,
|
||||
}: {
|
||||
tabs: string[]
|
||||
currentTab: string
|
||||
setCurrentTab: (tab: string) => void
|
||||
tabs: string[];
|
||||
currentTab: string;
|
||||
setCurrentTab: (tab: string) => void;
|
||||
}) {
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()))
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const tagRefs = useRef(tabs.map(() => createRef<HTMLDivElement>()));
|
||||
|
||||
useEffect(() => {
|
||||
const container = scrollRef.current
|
||||
if (!container) return
|
||||
useEffect(() => {
|
||||
const container = scrollRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const isOverflowing = container.scrollWidth > container.clientWidth
|
||||
if (!isOverflowing) return
|
||||
const isOverflowing = container.scrollWidth > container.clientWidth;
|
||||
if (!isOverflowing) return;
|
||||
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault()
|
||||
container.scrollLeft += e.deltaY
|
||||
}
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
container.scrollLeft += e.deltaY;
|
||||
};
|
||||
|
||||
container.addEventListener("wheel", onWheel, { passive: false })
|
||||
container.addEventListener("wheel", onWheel, { passive: false });
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("wheel", onWheel)
|
||||
}
|
||||
}, [])
|
||||
return () => {
|
||||
container.removeEventListener("wheel", onWheel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tabs.length === 1 && tabs[0] === "All") {
|
||||
setCurrentTab("All")
|
||||
return
|
||||
}
|
||||
const savedGroup = sessionStorage.getItem("selectedGroup")
|
||||
if (savedGroup && tabs.includes(savedGroup)) {
|
||||
setCurrentTab(savedGroup)
|
||||
}
|
||||
}, [tabs, setCurrentTab])
|
||||
useEffect(() => {
|
||||
if (tabs.length === 1 && tabs[0] === "All") {
|
||||
setCurrentTab("All");
|
||||
return;
|
||||
}
|
||||
const savedGroup = sessionStorage.getItem("selectedGroup");
|
||||
if (savedGroup && tabs.includes(savedGroup)) {
|
||||
setCurrentTab(savedGroup);
|
||||
}
|
||||
}, [tabs, setCurrentTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)]
|
||||
useEffect(() => {
|
||||
const currentTagRef = tagRefs.current[tabs.indexOf(currentTab)];
|
||||
|
||||
if (currentTagRef && currentTagRef.current) {
|
||||
currentTagRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
})
|
||||
}
|
||||
}, [currentTab])
|
||||
if (currentTagRef?.current) {
|
||||
currentTagRef.current.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
}
|
||||
}, [currentTab, tabs.indexOf]);
|
||||
|
||||
if (tabs.length === 1 && tabs[0] === "All") {
|
||||
return null
|
||||
}
|
||||
if (tabs.length === 1 && tabs[0] === "All") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={scrollRef} className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]">
|
||||
<div
|
||||
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
||||
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
{tabs.map((tab: string, index: number) => (
|
||||
<div
|
||||
key={tab}
|
||||
ref={tagRefs.current[index]}
|
||||
onClick={() => setCurrentTab(tab)}
|
||||
className={cn(
|
||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
|
||||
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
||||
)}
|
||||
>
|
||||
{currentTab === tab && (
|
||||
<m.div
|
||||
layoutId="tab-switch"
|
||||
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||
style={{
|
||||
originY: "0px",
|
||||
borderRadius: 46,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-20 flex items-center gap-1">
|
||||
<p className="whitespace-nowrap">{tab}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
ref={scrollRef}
|
||||
className="scrollbar-hidden z-50 flex flex-col items-start overflow-x-scroll rounded-[50px]"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
|
||||
{
|
||||
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab: string, index: number) => (
|
||||
<div
|
||||
key={tab}
|
||||
ref={tagRefs.current[index]}
|
||||
onClick={() => setCurrentTab(tab)}
|
||||
className={cn(
|
||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
|
||||
currentTab === tab
|
||||
? "text-black dark:text-white"
|
||||
: "text-stone-400 dark:text-stone-500",
|
||||
)}
|
||||
>
|
||||
{currentTab === tab && (
|
||||
<m.div
|
||||
layoutId="tab-switch"
|
||||
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||
style={{
|
||||
originY: "0px",
|
||||
borderRadius: 46,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-20 flex items-center gap-1">
|
||||
<p className="whitespace-nowrap">{tab}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+312
-278
@@ -1,330 +1,364 @@
|
||||
import { ModeToggle } from "@/components/ThemeSwitcher"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useBackground } from "@/hooks/use-background"
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
||||
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { AnimatePresence, m } from "framer-motion"
|
||||
import { ImageMinus } from "lucide-react"
|
||||
import { DateTime } from "luxon"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { ImageMinus } from "lucide-react";
|
||||
import { DateTime } from "luxon";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ModeToggle } from "@/components/ThemeSwitcher";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useBackground } from "@/hooks/use-background";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { fetchLoginUser, fetchSetting } from "@/lib/nezha-api";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import AnimateCountClient from "./AnimatedCount"
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher"
|
||||
import { SearchButton } from "./SearchButton"
|
||||
import { Loader, LoadingSpinner } from "./loading/Loader"
|
||||
import { Button } from "./ui/button"
|
||||
import AnimateCountClient from "./AnimatedCount";
|
||||
import { LanguageSwitcher } from "./LanguageSwitcher";
|
||||
import { Loader, LoadingSpinner } from "./loading/Loader";
|
||||
import { SearchButton } from "./SearchButton";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
interface TimeState {
|
||||
hh: number
|
||||
mm: number
|
||||
ss: number
|
||||
hh: number;
|
||||
mm: number;
|
||||
ss: number;
|
||||
}
|
||||
|
||||
const useCurrentTime = () => {
|
||||
const [time, setTime] = useState<TimeState>({
|
||||
hh: DateTime.now().setLocale("en-US").hour,
|
||||
mm: DateTime.now().setLocale("en-US").minute,
|
||||
ss: DateTime.now().setLocale("en-US").second,
|
||||
})
|
||||
const [time, setTime] = useState<TimeState>({
|
||||
hh: DateTime.now().setLocale("en-US").hour,
|
||||
mm: DateTime.now().setLocale("en-US").minute,
|
||||
ss: DateTime.now().setLocale("en-US").second,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
const now = DateTime.now().setLocale("en-US")
|
||||
setTime({
|
||||
hh: now.hour,
|
||||
mm: now.minute,
|
||||
ss: now.second,
|
||||
})
|
||||
}, 1000)
|
||||
useEffect(() => {
|
||||
const intervalId = setInterval(() => {
|
||||
const now = DateTime.now().setLocale("en-US");
|
||||
setTime({
|
||||
hh: now.hour,
|
||||
mm: now.minute,
|
||||
ss: now.second,
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => clearInterval(intervalId)
|
||||
}, [])
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
return time
|
||||
}
|
||||
return time;
|
||||
};
|
||||
|
||||
function Header() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { backgroundImage, updateBackground } = useBackground()
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { backgroundImage, updateBackground } = useBackground();
|
||||
|
||||
const { data: settingData, isLoading } = useQuery({
|
||||
queryKey: ["setting"],
|
||||
queryFn: () => fetchSetting(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
})
|
||||
const { data: settingData, isLoading } = useQuery({
|
||||
queryKey: ["setting"],
|
||||
queryFn: () => fetchSetting(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
|
||||
const { lastMessage, connected } = useWebSocketContext()
|
||||
const { lastMessage, connected } = useWebSocketContext();
|
||||
|
||||
const onlineCount = connected ? (lastMessage ? JSON.parse(lastMessage.data).online || 0 : 0) : "..."
|
||||
const onlineCount = connected
|
||||
? lastMessage
|
||||
? JSON.parse(lastMessage.data).online || 0
|
||||
: 0
|
||||
: "...";
|
||||
|
||||
const siteName = settingData?.data?.config?.site_name
|
||||
const siteName = settingData?.data?.config?.site_name;
|
||||
|
||||
// @ts-expect-error CustomLogo is a global variable
|
||||
const customLogo = window.CustomLogo || "/apple-touch-icon.png"
|
||||
// @ts-expect-error CustomLogo is a global variable
|
||||
const customLogo = window.CustomLogo || "/apple-touch-icon.png";
|
||||
|
||||
// @ts-expect-error CustomDesc is a global variable
|
||||
const customDesc = window.CustomDesc || t("nezha")
|
||||
// @ts-expect-error CustomDesc is a global variable
|
||||
const customDesc = window.CustomDesc || t("nezha");
|
||||
|
||||
const customMobileBackgroundImage = window.CustomMobileBackgroundImage !== "" ? window.CustomMobileBackgroundImage : undefined
|
||||
const customMobileBackgroundImage =
|
||||
window.CustomMobileBackgroundImage !== ""
|
||||
? window.CustomMobileBackgroundImage
|
||||
: undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const link = document.querySelector("link[rel*='icon']") || document.createElement("link")
|
||||
// @ts-expect-error set link.type
|
||||
link.type = "image/x-icon"
|
||||
// @ts-expect-error set link.rel
|
||||
link.rel = "shortcut icon"
|
||||
// @ts-expect-error set link.href
|
||||
link.href = customLogo
|
||||
document.getElementsByTagName("head")[0].appendChild(link)
|
||||
}, [customLogo])
|
||||
useEffect(() => {
|
||||
const link =
|
||||
document.querySelector("link[rel*='icon']") ||
|
||||
document.createElement("link");
|
||||
// @ts-expect-error set link.type
|
||||
link.type = "image/x-icon";
|
||||
// @ts-expect-error set link.rel
|
||||
link.rel = "shortcut icon";
|
||||
// @ts-expect-error set link.href
|
||||
link.href = customLogo;
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}, [customLogo]);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = siteName || "哪吒监控 Nezha Monitoring"
|
||||
}, [siteName])
|
||||
useEffect(() => {
|
||||
document.title = siteName || "哪吒监控 Nezha Monitoring";
|
||||
}, [siteName]);
|
||||
|
||||
const handleBackgroundToggle = () => {
|
||||
if (window.CustomBackgroundImage) {
|
||||
// Store the current background image before removing it
|
||||
sessionStorage.setItem("savedBackgroundImage", window.CustomBackgroundImage)
|
||||
updateBackground(undefined)
|
||||
} else {
|
||||
// Restore the saved background image
|
||||
const savedImage = sessionStorage.getItem("savedBackgroundImage")
|
||||
if (savedImage) {
|
||||
updateBackground(savedImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleBackgroundToggle = () => {
|
||||
if (window.CustomBackgroundImage) {
|
||||
// Store the current background image before removing it
|
||||
sessionStorage.setItem(
|
||||
"savedBackgroundImage",
|
||||
window.CustomBackgroundImage,
|
||||
);
|
||||
updateBackground(undefined);
|
||||
} else {
|
||||
// Restore the saved background image
|
||||
const savedImage = sessionStorage.getItem("savedBackgroundImage");
|
||||
if (savedImage) {
|
||||
updateBackground(savedImage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const customBackgroundImage = backgroundImage
|
||||
const customBackgroundImage = backgroundImage;
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl">
|
||||
<section className="flex items-center justify-between header-top">
|
||||
<section
|
||||
onClick={() => {
|
||||
sessionStorage.removeItem("selectedGroup")
|
||||
navigate("/")
|
||||
}}
|
||||
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
|
||||
>
|
||||
<div className="mr-1 flex flex-row items-center justify-start header-logo">
|
||||
<img
|
||||
width={40}
|
||||
height={40}
|
||||
alt="apple-touch-icon"
|
||||
src={customLogo}
|
||||
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
|
||||
/>
|
||||
</div>
|
||||
{isLoading ? <Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" /> : siteName || "NEZHA"}
|
||||
<Separator orientation="vertical" className="mx-2 hidden h-4 w-px md:block" />
|
||||
<p className="hidden text-sm font-medium opacity-40 md:block">{customDesc}</p>
|
||||
</section>
|
||||
<section className="flex items-center gap-2 header-handles">
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
<Links />
|
||||
<DashboardLink />
|
||||
</div>
|
||||
<SearchButton />
|
||||
<LanguageSwitcher />
|
||||
<ModeToggle />
|
||||
{(customBackgroundImage || sessionStorage.getItem("savedBackgroundImage")) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBackgroundToggle}
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
"hidden sm:block": customMobileBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<ImageMinus className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
{connected ? onlineCount : <Loader visible={true} />}
|
||||
<p className="text-muted-foreground">{connected ? t("online") : t("offline")}</p>
|
||||
<span
|
||||
className={cn("h-2 w-2 rounded-full bg-green-500", {
|
||||
"bg-red-500": !connected,
|
||||
})}
|
||||
></span>
|
||||
</Button>
|
||||
</section>
|
||||
</section>
|
||||
<div className="w-full flex justify-between sm:hidden mt-1">
|
||||
<DashboardLink />
|
||||
<Links />
|
||||
</div>
|
||||
<Overview />
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl">
|
||||
<section className="flex items-center justify-between header-top">
|
||||
<section
|
||||
onClick={() => {
|
||||
sessionStorage.removeItem("selectedGroup");
|
||||
navigate("/");
|
||||
}}
|
||||
className="cursor-pointer flex items-center sm:text-base text-sm font-medium"
|
||||
>
|
||||
<div className="mr-1 flex flex-row items-center justify-start header-logo">
|
||||
<img
|
||||
width={40}
|
||||
height={40}
|
||||
alt="apple-touch-icon"
|
||||
src={customLogo}
|
||||
className="relative m-0! border-2 border-transparent h-6 w-6 object-cover object-top p-0!"
|
||||
/>
|
||||
</div>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-6 w-20 rounded-[5px] bg-muted-foreground/10 animate-none" />
|
||||
) : (
|
||||
siteName || "NEZHA"
|
||||
)}
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden h-4 w-px md:block"
|
||||
/>
|
||||
<p className="hidden text-sm font-medium opacity-40 md:block">
|
||||
{customDesc}
|
||||
</p>
|
||||
</section>
|
||||
<section className="flex items-center gap-2 header-handles">
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
<Links />
|
||||
<DashboardLink />
|
||||
</div>
|
||||
<SearchButton />
|
||||
<LanguageSwitcher />
|
||||
<ModeToggle />
|
||||
{(customBackgroundImage ||
|
||||
sessionStorage.getItem("savedBackgroundImage")) && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleBackgroundToggle}
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
"hidden sm:block": customMobileBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<ImageMinus className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn(
|
||||
"hover:bg-white dark:hover:bg-black cursor-default rounded-full flex items-center px-[9px] bg-white dark:bg-black",
|
||||
{
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{connected ? onlineCount : <Loader visible={true} />}
|
||||
<p className="text-muted-foreground">
|
||||
{connected ? t("online") : t("offline")}
|
||||
</p>
|
||||
<span
|
||||
className={cn("h-2 w-2 rounded-full bg-green-500", {
|
||||
"bg-red-500": !connected,
|
||||
})}
|
||||
></span>
|
||||
</Button>
|
||||
</section>
|
||||
</section>
|
||||
<div className="w-full flex justify-between sm:hidden mt-1">
|
||||
<DashboardLink />
|
||||
<Links />
|
||||
</div>
|
||||
<Overview />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type links = {
|
||||
link: string
|
||||
name: string
|
||||
}
|
||||
link: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
function Links() {
|
||||
// @ts-expect-error CustomLinks is a global variable
|
||||
const customLinks = window.CustomLinks as string
|
||||
// @ts-expect-error CustomLinks is a global variable
|
||||
const customLinks = window.CustomLinks as string;
|
||||
|
||||
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null
|
||||
const links: links[] | null = customLinks ? JSON.parse(customLinks) : null;
|
||||
|
||||
if (!links) return null
|
||||
if (!links) return null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-fit">
|
||||
{links.map((link, index) => {
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={link.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-fit">
|
||||
{links.map((link, index) => {
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={link.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function RefreshToast() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { needReconnect } = useWebSocketContext()
|
||||
const { needReconnect } = useWebSocketContext();
|
||||
|
||||
if (!needReconnect) {
|
||||
return null
|
||||
}
|
||||
if (!needReconnect) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (needReconnect) {
|
||||
sessionStorage.removeItem("needRefresh")
|
||||
setTimeout(() => {
|
||||
navigate(0)
|
||||
}, 1000)
|
||||
}
|
||||
if (needReconnect) {
|
||||
sessionStorage.removeItem("needRefresh");
|
||||
setTimeout(() => {
|
||||
navigate(0);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<m.div
|
||||
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
||||
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||
transition={{ type: "spring", duration: 0.8 }}
|
||||
className="fixed left-1/2 -translate-x-1/2 top-8 z-999 flex items-center justify-between gap-4 rounded-[50px] border border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
|
||||
>
|
||||
<section className="flex items-center gap-1.5">
|
||||
<LoadingSpinner />
|
||||
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
|
||||
</section>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
return (
|
||||
<AnimatePresence>
|
||||
<m.div
|
||||
initial={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
|
||||
exit={{ opacity: 0, filter: "blur(10px)", scale: 0.8 }}
|
||||
transition={{ type: "spring", duration: 0.8 }}
|
||||
className="fixed left-1/2 -translate-x-1/2 top-8 z-999 flex items-center justify-between gap-4 rounded-[50px] border border-solid bg-white px-2 py-1.5 shadow-xl shadow-black/5 dark:border-stone-700 dark:bg-stone-800 dark:shadow-none"
|
||||
>
|
||||
<section className="flex items-center gap-1.5">
|
||||
<LoadingSpinner />
|
||||
<p className="text-[12.5px] font-medium">{t("refreshing")}...</p>
|
||||
</section>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardLink() {
|
||||
const { t } = useTranslation()
|
||||
const { setNeedReconnect } = useWebSocketContext()
|
||||
const previousLoginState = useRef<boolean | null>(null)
|
||||
const {
|
||||
data: userData,
|
||||
isFetched,
|
||||
isLoadingError,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["login-user"],
|
||||
queryFn: () => fetchLoginUser(),
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchIntervalInBackground: true,
|
||||
refetchInterval: 1000 * 30,
|
||||
retry: 0,
|
||||
})
|
||||
const { t } = useTranslation();
|
||||
const { setNeedReconnect } = useWebSocketContext();
|
||||
const previousLoginState = useRef<boolean | null>(null);
|
||||
const {
|
||||
data: userData,
|
||||
isFetched,
|
||||
isLoadingError,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: ["login-user"],
|
||||
queryFn: () => fetchLoginUser(),
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchIntervalInBackground: true,
|
||||
refetchInterval: 1000 * 30,
|
||||
retry: 0,
|
||||
});
|
||||
|
||||
const isLogin = isError ? false : userData ? !!userData?.data?.id && !!document.cookie : false
|
||||
const isLogin = isError
|
||||
? false
|
||||
: userData
|
||||
? !!userData?.data?.id && !!document.cookie
|
||||
: false;
|
||||
|
||||
if (isLoadingError) {
|
||||
previousLoginState.current = isLogin
|
||||
}
|
||||
if (isLoadingError) {
|
||||
previousLoginState.current = isLogin;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refetch()
|
||||
}, [document.cookie])
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetched || isError) {
|
||||
// 只有当登录状态发生变化时才设置needReconnect
|
||||
if (previousLoginState.current !== null && previousLoginState.current !== isLogin) {
|
||||
setNeedReconnect(true)
|
||||
}
|
||||
previousLoginState.current = isLogin
|
||||
}
|
||||
}, [isLogin])
|
||||
useEffect(() => {
|
||||
if (isFetched || isError) {
|
||||
// 只有当登录状态发生变化时才设置needReconnect
|
||||
if (
|
||||
previousLoginState.current !== null &&
|
||||
previousLoginState.current !== isLogin
|
||||
) {
|
||||
setNeedReconnect(true);
|
||||
}
|
||||
previousLoginState.current = isLogin;
|
||||
}
|
||||
}, [isLogin, isError, isFetched, setNeedReconnect]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={"/dashboard"}
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||
>
|
||||
{!isLogin && t("login")}
|
||||
{isLogin && t("dashboard")}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={"/dashboard"}
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center text-nowrap gap-1 text-sm font-medium opacity-50 transition-opacity hover:opacity-100"
|
||||
>
|
||||
{!isLogin && t("login")}
|
||||
{isLogin && t("dashboard")}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Overview() {
|
||||
const { t } = useTranslation()
|
||||
const time = useCurrentTime()
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { t } = useTranslation();
|
||||
const time = useCurrentTime();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
|
||||
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
|
||||
{mounted ? (
|
||||
<div className="flex items-center font-medium text-sm">
|
||||
<AnimateCountClient count={time.hh} minDigits={2} />
|
||||
<span className="mb-px font-medium text-sm opacity-50">:</span>
|
||||
<AnimateCountClient count={time.mm} minDigits={2} />
|
||||
<span className="mb-px font-medium text-sm opacity-50">:</span>
|
||||
<span className="font-medium text-sm">
|
||||
<AnimateCountClient count={time.ss} minDigits={2} />
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
return (
|
||||
<section className={"mt-10 flex flex-col md:mt-16 header-timer"}>
|
||||
<p className="text-base font-semibold">👋 {t("overview")}</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<p className="text-sm font-medium opacity-50">{t("whereTheTimeIs")}</p>
|
||||
{mounted ? (
|
||||
<div className="flex items-center font-medium text-sm">
|
||||
<AnimateCountClient count={time.hh} minDigits={2} />
|
||||
<span className="mb-px font-medium text-sm opacity-50">:</span>
|
||||
<AnimateCountClient count={time.mm} minDigits={2} />
|
||||
<span className="mb-px font-medium text-sm opacity-50">:</span>
|
||||
<span className="font-medium text-sm">
|
||||
<AnimateCountClient count={time.ss} minDigits={2} />
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton className="h-[21px] w-16 animate-none rounded-[5px] bg-muted-foreground/10" />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
export default Header
|
||||
export default Header;
|
||||
|
||||
+27
-27
File diff suppressed because one or more lines are too long
@@ -1,68 +1,78 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { CheckCircleIcon, LanguageIcon } from "@heroicons/react/20/solid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function LanguageSwitcher() {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { t, i18n } = useTranslation();
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
const locale = i18n.languages[0]
|
||||
const locale = i18n.languages[0];
|
||||
|
||||
const handleSelect = (e: Event, newLocale: string) => {
|
||||
e.preventDefault() // 阻止默认的关闭行为
|
||||
i18n.changeLanguage(newLocale)
|
||||
}
|
||||
const handleSelect = (e: Event, newLocale: string) => {
|
||||
e.preventDefault(); // 阻止默认的关闭行为
|
||||
i18n.changeLanguage(newLocale);
|
||||
};
|
||||
|
||||
const localeItems = [
|
||||
{ name: t("language.zh-CN"), code: "zh-CN" },
|
||||
{ name: t("language.zh-TW"), code: "zh-TW" },
|
||||
{ name: t("language.en-US"), code: "en-US" },
|
||||
{ name: t("language.ru-RU"), code: "ru-RU" },
|
||||
{ name: t("language.es-ES"), code: "es-ES" },
|
||||
{ name: t("language.de-DE"), code: "de-DE" },
|
||||
{ name: t("language.ta-IN"), code: "ta-IN" },
|
||||
]
|
||||
const localeItems = [
|
||||
{ name: t("language.zh-CN"), code: "zh-CN" },
|
||||
{ name: t("language.zh-TW"), code: "zh-TW" },
|
||||
{ name: t("language.en-US"), code: "en-US" },
|
||||
{ name: t("language.ru-RU"), code: "ru-RU" },
|
||||
{ name: t("language.es-ES"), code: "es-ES" },
|
||||
{ name: t("language.de-DE"), code: "de-DE" },
|
||||
{ name: t("language.ta-IN"), code: "ta-IN" },
|
||||
];
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<LanguageIcon className="size-4" />
|
||||
<span className="sr-only">Change language</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||
{localeItems.map((item, index) => (
|
||||
<DropdownMenuItem
|
||||
key={item.code}
|
||||
onSelect={(e) => handleSelect(e, item.code)}
|
||||
className={cn(
|
||||
"text-xs",
|
||||
{
|
||||
"gap-3 bg-muted font-semibold": locale === item.code,
|
||||
},
|
||||
{
|
||||
"rounded-t-[5px]": index === localeItems.length - 1,
|
||||
"rounded-[5px]": index !== 0 && index !== localeItems.length - 1,
|
||||
"rounded-b-[5px]": index === 0,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name} {locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<LanguageIcon className="size-4" />
|
||||
<span className="sr-only">Change language</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="flex flex-col gap-0.5" align="end">
|
||||
{localeItems.map((item, index) => (
|
||||
<DropdownMenuItem
|
||||
key={item.code}
|
||||
onSelect={(e) => handleSelect(e, item.code)}
|
||||
className={cn(
|
||||
"text-xs",
|
||||
{
|
||||
"gap-3 bg-muted font-semibold": locale === item.code,
|
||||
},
|
||||
{
|
||||
"rounded-t-[5px]": index === localeItems.length - 1,
|
||||
"rounded-[5px]":
|
||||
index !== 0 && index !== localeItems.length - 1,
|
||||
"rounded-b-[5px]": index === 0,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{item.name}{" "}
|
||||
{locale === item.code && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +1,70 @@
|
||||
import useTooltip from "@/hooks/use-tooltip"
|
||||
import { AnimatePresence, m } from "framer-motion"
|
||||
import { memo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { AnimatePresence, m } from "framer-motion";
|
||||
import { memo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import useTooltip from "@/hooks/use-tooltip";
|
||||
|
||||
const MapTooltip = memo(function MapTooltip() {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { tooltipData } = useTooltip()
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { tooltipData } = useTooltip();
|
||||
|
||||
if (!tooltipData) return null
|
||||
if (!tooltipData) return null;
|
||||
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<m.div
|
||||
initial={{ opacity: 0, filter: "blur(10px)" }}
|
||||
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, filter: "blur(10px)" }}
|
||||
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
|
||||
key={tooltipData.country}
|
||||
style={{
|
||||
left: tooltipData.centroid[0],
|
||||
top: tooltipData.centroid[1],
|
||||
transform: "translate(20%, -50%)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">{tooltipData.country === "China" ? "Mainland China" : tooltipData.country}</p>
|
||||
<p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1">
|
||||
{tooltipData.count} {t("map.Servers")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="border-t dark:border-neutral-700 pt-1"
|
||||
style={{
|
||||
maxHeight: "200px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{tooltipData.servers.map((server) => (
|
||||
<button
|
||||
key={server.id}
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
|
||||
onClick={() => {
|
||||
sessionStorage.setItem("fromMainPage", "true")
|
||||
navigate(`/server/${server.id}`)
|
||||
}}
|
||||
>
|
||||
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`} />
|
||||
<span className="text-xs">{server.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
<m.div
|
||||
initial={{ opacity: 0, filter: "blur(10px)" }}
|
||||
animate={{ opacity: 1, filter: "blur(0px)" }}
|
||||
exit={{ opacity: 0, filter: "blur(10px)" }}
|
||||
className="absolute hidden lg:block bg-white dark:bg-neutral-800 px-2 py-1 rounded shadow-lg text-sm dark:border dark:border-neutral-700 z-50"
|
||||
key={tooltipData.country}
|
||||
style={{
|
||||
left: tooltipData.centroid[0],
|
||||
top: tooltipData.centroid[1],
|
||||
transform: "translate(20%, -50%)",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{tooltipData.country === "China"
|
||||
? "Mainland China"
|
||||
: tooltipData.country}
|
||||
</p>
|
||||
<p className="text-neutral-600 dark:text-neutral-400 text-xs font-light mb-1">
|
||||
{tooltipData.count} {t("map.Servers")}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="border-t dark:border-neutral-700 pt-1"
|
||||
style={{
|
||||
maxHeight: "200px",
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{tooltipData.servers.map((server) => (
|
||||
<button
|
||||
key={server.id}
|
||||
type="button"
|
||||
className="flex items-center gap-1.5 py-0.5 text-neutral-500 transition-colors hover:text-black dark:text-neutral-400 dark:hover:text-white"
|
||||
onClick={() => {
|
||||
sessionStorage.setItem("fromMainPage", "true");
|
||||
navigate(`/server/${server.id}`);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 shrink-0 rounded-full ${server.status ? "bg-green-500" : "bg-red-500"}`}
|
||||
/>
|
||||
<span className="text-xs">{server.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</m.div>
|
||||
</AnimatePresence>
|
||||
);
|
||||
});
|
||||
|
||||
export default MapTooltip
|
||||
export default MapTooltip;
|
||||
|
||||
+584
-482
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,23 @@
|
||||
import { Loader } from "@/components/loading/Loader"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Loader } from "@/components/loading/Loader";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function NetworkChartLoading() {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
|
||||
<CardTitle className="flex items-center gap-0.5 text-xl">
|
||||
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
|
||||
</CardTitle>
|
||||
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
|
||||
</div>
|
||||
<div className="hidden pr-4 pt-4 sm:block">
|
||||
<Loader visible={true} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6">
|
||||
<div className="aspect-auto h-[250px] w-full"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5">
|
||||
<CardTitle className="flex items-center gap-0.5 text-xl">
|
||||
<div className="aspect-auto h-[20px] w-24 bg-muted"></div>
|
||||
</CardTitle>
|
||||
<div className="mt-[2px] aspect-auto h-[14px] w-32 bg-muted"></div>
|
||||
</div>
|
||||
<div className="hidden pr-4 pt-4 sm:block">
|
||||
<Loader visible={true} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="px-2 sm:p-6">
|
||||
<div className="aspect-auto h-[250px] w-full"></div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
+86
-54
@@ -1,58 +1,90 @@
|
||||
import { PublicNoteData, cn } from "@/lib/utils"
|
||||
import { cn, type PublicNoteData } from "@/lib/utils";
|
||||
|
||||
export default function PlanInfo({ parsedData }: { parsedData: PublicNoteData }) {
|
||||
if (!parsedData || !parsedData.planDataMod) {
|
||||
return null
|
||||
}
|
||||
export default function PlanInfo({
|
||||
parsedData,
|
||||
}: {
|
||||
parsedData: PublicNoteData;
|
||||
}) {
|
||||
if (!parsedData || !parsedData.planDataMod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const extraList =
|
||||
parsedData.planDataMod.extra.split(",").length > 1
|
||||
? parsedData.planDataMod.extra.split(",")
|
||||
: parsedData.planDataMod.extra.split(",")[0] === ""
|
||||
? []
|
||||
: [parsedData.planDataMod.extra]
|
||||
const extraList =
|
||||
parsedData.planDataMod.extra.split(",").length > 1
|
||||
? parsedData.planDataMod.extra.split(",")
|
||||
: parsedData.planDataMod.extra.split(",")[0] === ""
|
||||
? []
|
||||
: [parsedData.planDataMod.extra];
|
||||
|
||||
return (
|
||||
<section className="flex gap-1 items-center flex-wrap mt-0.5">
|
||||
{parsedData.planDataMod.bandwidth !== "" && (
|
||||
<p className={cn("text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||
{parsedData.planDataMod.bandwidth}
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.trafficVol !== "" && (
|
||||
<p className={cn("text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||
{parsedData.planDataMod.trafficVol}
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.IPv4 === "1" && (
|
||||
<p
|
||||
className={cn("text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
|
||||
>
|
||||
IPv4
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.IPv6 === "1" && (
|
||||
<p className={cn("text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||
IPv6
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.networkRoute && (
|
||||
<p className={cn("text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}>
|
||||
{parsedData.planDataMod.networkRoute.split(",").map((route, index) => {
|
||||
return route + (index === parsedData.planDataMod!.networkRoute.split(",").length - 1 ? "" : "|")
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{extraList.map((extra, index) => {
|
||||
return (
|
||||
<p
|
||||
key={index}
|
||||
className={cn("text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]")}
|
||||
>
|
||||
{extra}
|
||||
</p>
|
||||
)
|
||||
})}
|
||||
</section>
|
||||
)
|
||||
return (
|
||||
<section className="flex gap-1 items-center flex-wrap mt-0.5">
|
||||
{parsedData.planDataMod.bandwidth !== "" && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[9px] bg-blue-600 dark:bg-blue-800 text-blue-200 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||
)}
|
||||
>
|
||||
{parsedData.planDataMod.bandwidth}
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.trafficVol !== "" && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[9px] bg-green-600 text-green-200 dark:bg-green-800 dark:text-green-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||
)}
|
||||
>
|
||||
{parsedData.planDataMod.trafficVol}
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.IPv4 === "1" && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[9px] bg-purple-600 text-purple-200 dark:bg-purple-800 dark:text-purple-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||
)}
|
||||
>
|
||||
IPv4
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.IPv6 === "1" && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[9px] bg-pink-600 text-pink-200 dark:bg-pink-800 dark:text-pink-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||
)}
|
||||
>
|
||||
IPv6
|
||||
</p>
|
||||
)}
|
||||
{parsedData.planDataMod.networkRoute && (
|
||||
<p
|
||||
className={cn(
|
||||
"text-[9px] bg-blue-600 text-blue-200 dark:bg-blue-800 dark:text-blue-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||
)}
|
||||
>
|
||||
{parsedData.planDataMod.networkRoute
|
||||
.split(",")
|
||||
.map((route, index) => {
|
||||
return (
|
||||
route +
|
||||
(index ===
|
||||
parsedData.planDataMod?.networkRoute.split(",").length - 1
|
||||
? ""
|
||||
: "|")
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{extraList.map((extra, index) => {
|
||||
return (
|
||||
<p
|
||||
key={index}
|
||||
className={cn(
|
||||
"text-[9px] bg-stone-600 text-stone-200 dark:bg-stone-800 dark:text-stone-300 w-fit rounded-[5px] px-[3px] py-[1.5px]",
|
||||
)}
|
||||
>
|
||||
{extra}
|
||||
</p>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Progress } from "./ui/progress"
|
||||
import { Progress } from "./ui/progress";
|
||||
|
||||
export default function RemainPercentBar({ value, className }: { value: number; className?: string }) {
|
||||
return (
|
||||
<Progress
|
||||
aria-label={"Server Usage Bar"}
|
||||
aria-labelledby={"Server Usage Bar"}
|
||||
value={value}
|
||||
indicatorClassName={value < 30 ? "bg-red-500" : value < 70 ? "bg-orange-400" : "bg-green-500"}
|
||||
className={cn("h-[3px] rounded-sm w-[70px]", className)}
|
||||
/>
|
||||
)
|
||||
export default function RemainPercentBar({
|
||||
value,
|
||||
className,
|
||||
}: {
|
||||
value: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<Progress
|
||||
aria-label={"Server Usage Bar"}
|
||||
aria-labelledby={"Server Usage Bar"}
|
||||
value={value}
|
||||
indicatorClassName={
|
||||
value < 30
|
||||
? "bg-red-500"
|
||||
: value < 70
|
||||
? "bg-orange-400"
|
||||
: "bg-green-500"
|
||||
}
|
||||
className={cn("h-[3px] rounded-sm w-[70px]", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useCommand } from "@/hooks/use-command"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||
import { useCommand } from "@/hooks/use-command";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Button } from "./ui/button"
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export function SearchButton() {
|
||||
const { openCommand } = useCommand()
|
||||
const { openCommand } = useCommand();
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
})}
|
||||
onClick={openCommand}
|
||||
title="Search"
|
||||
>
|
||||
<MagnifyingGlassIcon className="size-4" />
|
||||
<span className="sr-only">Search</span>
|
||||
</Button>
|
||||
)
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
})}
|
||||
onClick={openCommand}
|
||||
title="Search"
|
||||
>
|
||||
<MagnifyingGlassIcon className="size-4" />
|
||||
<span className="sr-only">Search</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
+271
-187
@@ -1,196 +1,280 @@
|
||||
import ServerFlag from "@/components/ServerFlag"
|
||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
|
||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
|
||||
import { NezhaServer } from "@/types/nezha-api"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ServerFlag from "@/components/ServerFlag";
|
||||
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import {
|
||||
GetFontLogoClass,
|
||||
GetOsName,
|
||||
MageMicrosoftWindows,
|
||||
} from "@/lib/logo-class";
|
||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
||||
import type { NezhaServer } from "@/types/nezha-api";
|
||||
import BillingInfo from "./billingInfo";
|
||||
import PlanInfo from "./PlanInfo";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Card } from "./ui/card";
|
||||
|
||||
import PlanInfo from "./PlanInfo"
|
||||
import BillingInfo from "./billingInfo"
|
||||
import { Badge } from "./ui/badge"
|
||||
import { Card } from "./ui/card"
|
||||
export default function ServerCard({
|
||||
now,
|
||||
serverInfo,
|
||||
}: {
|
||||
now: number;
|
||||
serverInfo: NezhaServer;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
name,
|
||||
country_code,
|
||||
online,
|
||||
cpu,
|
||||
up,
|
||||
down,
|
||||
mem,
|
||||
stg,
|
||||
net_in_transfer,
|
||||
net_out_transfer,
|
||||
public_note,
|
||||
platform,
|
||||
} = formatNezhaInfo(now, serverInfo);
|
||||
|
||||
export default function ServerCard({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { name, country_code, online, cpu, up, down, mem, stg, net_in_transfer, net_out_transfer, public_note, platform } = formatNezhaInfo(
|
||||
now,
|
||||
serverInfo,
|
||||
)
|
||||
const cardClick = () => {
|
||||
sessionStorage.setItem("fromMainPage", "true");
|
||||
navigate(`/server/${serverInfo.id}`);
|
||||
};
|
||||
|
||||
const cardClick = () => {
|
||||
sessionStorage.setItem("fromMainPage", "true")
|
||||
navigate(`/server/${serverInfo.id}`)
|
||||
}
|
||||
const showFlag = true;
|
||||
|
||||
const showFlag = true
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
// @ts-expect-error ShowNetTransfer is a global variable
|
||||
const showNetTransfer = window.ShowNetTransfer as boolean;
|
||||
|
||||
// @ts-expect-error ShowNetTransfer is a global variable
|
||||
const showNetTransfer = window.ShowNetTransfer as boolean
|
||||
// @ts-expect-error FixedTopServerName is a global variable
|
||||
const fixedTopServerName = window.FixedTopServerName as boolean;
|
||||
|
||||
// @ts-expect-error FixedTopServerName is a global variable
|
||||
const fixedTopServerName = window.FixedTopServerName as boolean
|
||||
const parsedData = parsePublicNote(public_note);
|
||||
|
||||
const parsedData = parsePublicNote(public_note)
|
||||
|
||||
return online ? (
|
||||
<Card
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||
{
|
||||
"flex-col": fixedTopServerName,
|
||||
"lg:flex-row": !fixedTopServerName,
|
||||
},
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2", {
|
||||
"lg:w-40": !fixedTopServerName,
|
||||
})}
|
||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
|
||||
<div
|
||||
className={cn("hidden lg:block", {
|
||||
"lg:hidden": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
className={cn("flex items-center gap-2 -mt-2 lg:hidden", {
|
||||
"lg:flex": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
<div className="flex flex-col lg:items-start items-center gap-2">
|
||||
<section
|
||||
className={cn("grid grid-cols-5 items-center gap-3", {
|
||||
"lg:grid-cols-6 lg:gap-4": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{fixedTopServerName && (
|
||||
<div className={"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"}>
|
||||
<div className="text-xs font-semibold">
|
||||
{platform.includes("Windows") ? (
|
||||
<MageMicrosoftWindows className="size-[10px]" />
|
||||
) : (
|
||||
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
|
||||
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{showNetTransfer && (
|
||||
<section className={"flex items-center w-full justify-between gap-1"}>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||
>
|
||||
{t("serverCard.upload")}:{formatBytes(net_out_transfer)}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||
>
|
||||
{t("serverCard.download")}:{formatBytes(net_in_transfer)}
|
||||
</Badge>
|
||||
</section>
|
||||
)}
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||
showNetTransfer ? "lg:min-h-[91px] min-h-[123px]" : "lg:min-h-[61px] min-h-[93px]",
|
||||
{
|
||||
"flex-col": fixedTopServerName,
|
||||
"lg:flex-row": !fixedTopServerName,
|
||||
},
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2", {
|
||||
"lg:w-40": !fixedTopServerName,
|
||||
})}
|
||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className={cn("break-normal font-bold tracking-tight max-w-[108px]", showFlag ? "text-xs" : "text-sm")}>{name}</p>
|
||||
<div
|
||||
className={cn("hidden lg:block", {
|
||||
"lg:hidden": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
className={cn("flex items-center gap-2 lg:hidden", {
|
||||
"lg:flex": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</Card>
|
||||
)
|
||||
return online ? (
|
||||
<Card
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||
{
|
||||
"flex-col": fixedTopServerName,
|
||||
"lg:flex-row": !fixedTopServerName,
|
||||
},
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2", {
|
||||
"lg:w-40": !fixedTopServerName,
|
||||
})}
|
||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||
)}
|
||||
>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p
|
||||
className={cn(
|
||||
"break-normal font-bold tracking-tight",
|
||||
showFlag ? "text-xs " : "text-sm",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
<div
|
||||
className={cn("hidden lg:block", {
|
||||
"lg:hidden": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && (
|
||||
<BillingInfo parsedData={parsedData} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
className={cn("flex items-center gap-2 -mt-2 lg:hidden", {
|
||||
"lg:flex": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
<div className="flex flex-col lg:items-start items-center gap-2">
|
||||
<section
|
||||
className={cn("grid grid-cols-5 items-center gap-3", {
|
||||
"lg:grid-cols-6 lg:gap-4": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{fixedTopServerName && (
|
||||
<div
|
||||
className={
|
||||
"hidden col-span-1 items-center lg:flex lg:flex-row gap-2"
|
||||
}
|
||||
>
|
||||
<div className="text-xs font-semibold">
|
||||
{platform.includes("Windows") ? (
|
||||
<MageMicrosoftWindows className="size-[10px]" />
|
||||
) : (
|
||||
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.system")}
|
||||
</p>
|
||||
<div className="flex items-center text-[10.5px] font-semibold">
|
||||
{platform.includes("Windows")
|
||||
? "Windows"
|
||||
: GetOsName(platform)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{cpu.toFixed(2)}%
|
||||
</div>
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.mem")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{mem.toFixed(2)}%
|
||||
</div>
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.stg")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{stg.toFixed(2)}%
|
||||
</div>
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.upload")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up >= 1024
|
||||
? `${(up / 1024).toFixed(2)}G/s`
|
||||
: up >= 1
|
||||
? `${up.toFixed(2)}M/s`
|
||||
: `${(up * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.download")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down >= 1024
|
||||
? `${(down / 1024).toFixed(2)}G/s`
|
||||
: down >= 1
|
||||
? `${down.toFixed(2)}M/s`
|
||||
: `${(down * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{showNetTransfer && (
|
||||
<section className={"flex items-center w-full justify-between gap-1"}>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] border-muted-50 shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||
>
|
||||
{t("serverCard.upload")}:{formatBytes(net_out_transfer)}
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="items-center flex-1 justify-center rounded-[8px] text-nowrap text-[11px] shadow-md shadow-neutral-200/30 dark:shadow-none"
|
||||
>
|
||||
{t("serverCard.download")}:{formatBytes(net_in_transfer)}
|
||||
</Badge>
|
||||
</section>
|
||||
)}
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<Card
|
||||
className={cn(
|
||||
"flex flex-col items-center justify-start gap-3 sm:gap-0 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors",
|
||||
showNetTransfer
|
||||
? "lg:min-h-[91px] min-h-[123px]"
|
||||
: "lg:min-h-[61px] min-h-[93px]",
|
||||
{
|
||||
"flex-col": fixedTopServerName,
|
||||
"lg:flex-row": !fixedTopServerName,
|
||||
},
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2", {
|
||||
"lg:w-40": !fixedTopServerName,
|
||||
})}
|
||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||
)}
|
||||
>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p
|
||||
className={cn(
|
||||
"break-normal font-bold tracking-tight max-w-[108px]",
|
||||
showFlag ? "text-xs" : "text-sm",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
<div
|
||||
className={cn("hidden lg:block", {
|
||||
"lg:hidden": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && (
|
||||
<BillingInfo parsedData={parsedData} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div
|
||||
className={cn("flex items-center gap-2 lg:hidden", {
|
||||
"lg:flex": fixedTopServerName,
|
||||
})}
|
||||
>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
+233
-136
@@ -1,143 +1,240 @@
|
||||
import ServerFlag from "@/components/ServerFlag"
|
||||
import ServerUsageBar from "@/components/ServerUsageBar"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { GetFontLogoClass, GetOsName, MageMicrosoftWindows } from "@/lib/logo-class"
|
||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils"
|
||||
import { NezhaServer } from "@/types/nezha-api"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import ServerFlag from "@/components/ServerFlag";
|
||||
import ServerUsageBar from "@/components/ServerUsageBar";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import {
|
||||
GetFontLogoClass,
|
||||
GetOsName,
|
||||
MageMicrosoftWindows,
|
||||
} from "@/lib/logo-class";
|
||||
import { cn, formatNezhaInfo, parsePublicNote } from "@/lib/utils";
|
||||
import type { NezhaServer } from "@/types/nezha-api";
|
||||
import BillingInfo from "./billingInfo";
|
||||
import PlanInfo from "./PlanInfo";
|
||||
import { Card } from "./ui/card";
|
||||
import { Separator } from "./ui/separator";
|
||||
|
||||
import PlanInfo from "./PlanInfo"
|
||||
import BillingInfo from "./billingInfo"
|
||||
import { Card } from "./ui/card"
|
||||
import { Separator } from "./ui/separator"
|
||||
export default function ServerCardInline({
|
||||
now,
|
||||
serverInfo,
|
||||
}: {
|
||||
now: number;
|
||||
serverInfo: NezhaServer;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
name,
|
||||
country_code,
|
||||
online,
|
||||
cpu,
|
||||
up,
|
||||
down,
|
||||
mem,
|
||||
stg,
|
||||
platform,
|
||||
uptime,
|
||||
net_in_transfer,
|
||||
net_out_transfer,
|
||||
public_note,
|
||||
} = formatNezhaInfo(now, serverInfo);
|
||||
|
||||
export default function ServerCardInline({ now, serverInfo }: { now: number; serverInfo: NezhaServer }) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { name, country_code, online, cpu, up, down, mem, stg, platform, uptime, net_in_transfer, net_out_transfer, public_note } = formatNezhaInfo(
|
||||
now,
|
||||
serverInfo,
|
||||
)
|
||||
const cardClick = () => {
|
||||
sessionStorage.setItem("fromMainPage", "true");
|
||||
navigate(`/server/${serverInfo.id}`);
|
||||
};
|
||||
|
||||
const cardClick = () => {
|
||||
sessionStorage.setItem("fromMainPage", "true")
|
||||
navigate(`/server/${serverInfo.id}`)
|
||||
}
|
||||
const showFlag = true;
|
||||
|
||||
const showFlag = true
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
const parsedData = parsePublicNote(public_note);
|
||||
|
||||
const parsedData = parsePublicNote(public_note)
|
||||
|
||||
return online ? (
|
||||
<section>
|
||||
<Card
|
||||
className={cn(
|
||||
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section className={cn("grid items-center gap-2 lg:w-36")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative w-28 flex flex-col">
|
||||
<p className={cn("break-normal font-bold tracking-tight", showFlag ? "text-xs " : "text-sm")}>{name}</p>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</section>
|
||||
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
|
||||
<div className={"items-center flex flex-row gap-2 whitespace-nowrap"}>
|
||||
<div className="text-xs font-semibold">
|
||||
{platform.includes("Windows") ? (
|
||||
<MageMicrosoftWindows className="size-[10px]" />
|
||||
) : (
|
||||
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.system")}</p>
|
||||
<div className="flex items-center text-[10.5px] font-semibold">{platform.includes("Windows") ? "Windows" : GetOsName(platform)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.uptime")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{uptime / 86400 >= 1
|
||||
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
|
||||
: `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{cpu.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.mem")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{mem.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.stg")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{stg.toFixed(2)}%</div>
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex w-16 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.upload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up >= 1024 ? `${(up / 1024).toFixed(2)}G/s` : up >= 1 ? `${up.toFixed(2)}M/s` : `${(up * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-16 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.download")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down >= 1024 ? `${(down / 1024).toFixed(2)}G/s` : down >= 1 ? `${down.toFixed(2)}M/s` : `${(down * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.totalUpload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{formatBytes(net_out_transfer)}</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{t("serverCard.totalDownload")}</p>
|
||||
<div className="flex items-center text-xs font-semibold">{formatBytes(net_in_transfer)}</div>
|
||||
</div>
|
||||
</section>
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
) : (
|
||||
<Card
|
||||
className={cn(
|
||||
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section className={cn("grid items-center gap-2 w-40")} style={{ gridTemplateColumns: "auto auto 1fr" }}>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||
<div className={cn("flex items-center justify-center", showFlag ? "min-w-[17px]" : "min-w-0")}>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p className={cn("break-normal font-bold w-28 tracking-tight", showFlag ? "text-xs" : "text-sm")}>{name}</p>
|
||||
{parsedData?.billingDataMod && <BillingInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</section>
|
||||
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</Card>
|
||||
)
|
||||
return online ? (
|
||||
<section>
|
||||
<Card
|
||||
className={cn(
|
||||
"flex items-center lg:flex-row justify-start gap-3 p-3 md:px-5 cursor-pointer hover:bg-accent/50 transition-colors min-w-[900px] w-full",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2 lg:w-36")}
|
||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-green-500 self-center"></span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||
)}
|
||||
>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative w-28 flex flex-col">
|
||||
<p
|
||||
className={cn(
|
||||
"break-normal font-bold tracking-tight",
|
||||
showFlag ? "text-xs " : "text-sm",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
{parsedData?.billingDataMod && (
|
||||
<BillingInfo parsedData={parsedData} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<Separator orientation="vertical" className="h-8 mx-0 ml-2" />
|
||||
<div className="flex flex-col gap-1">
|
||||
<section className={cn("grid grid-cols-9 items-center gap-3 flex-1")}>
|
||||
<div
|
||||
className={"items-center flex flex-row gap-2 whitespace-nowrap"}
|
||||
>
|
||||
<div className="text-xs font-semibold">
|
||||
{platform.includes("Windows") ? (
|
||||
<MageMicrosoftWindows className="size-[10px]" />
|
||||
) : (
|
||||
<p className={`fl-${GetFontLogoClass(platform)}`} />
|
||||
)}
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.system")}
|
||||
</p>
|
||||
<div className="flex items-center text-[10.5px] font-semibold">
|
||||
{platform.includes("Windows")
|
||||
? "Windows"
|
||||
: GetOsName(platform)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.uptime")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{uptime / 86400 >= 1
|
||||
? `${(uptime / 86400).toFixed(0)} ${t("serverCard.days")}`
|
||||
: `${(uptime / 3600).toFixed(0)} ${t("serverCard.hours")}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{cpu.toFixed(2)}%
|
||||
</div>
|
||||
<ServerUsageBar value={cpu} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.mem")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{mem.toFixed(2)}%
|
||||
</div>
|
||||
<ServerUsageBar value={mem} />
|
||||
</div>
|
||||
<div className={"flex w-14 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.stg")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{stg.toFixed(2)}%
|
||||
</div>
|
||||
<ServerUsageBar value={stg} />
|
||||
</div>
|
||||
<div className={"flex w-16 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.upload")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{up >= 1024
|
||||
? `${(up / 1024).toFixed(2)}G/s`
|
||||
: up >= 1
|
||||
? `${up.toFixed(2)}M/s`
|
||||
: `${(up * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-16 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.download")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{down >= 1024
|
||||
? `${(down / 1024).toFixed(2)}G/s`
|
||||
: down >= 1
|
||||
? `${down.toFixed(2)}M/s`
|
||||
: `${(down * 1024).toFixed(2)}K/s`}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.totalUpload")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{formatBytes(net_out_transfer)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={"flex w-20 flex-col"}>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverCard.totalDownload")}
|
||||
</p>
|
||||
<div className="flex items-center text-xs font-semibold">
|
||||
{formatBytes(net_in_transfer)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</div>
|
||||
</Card>
|
||||
</section>
|
||||
) : (
|
||||
<Card
|
||||
className={cn(
|
||||
"flex min-h-[61px] min-w-[900px] items-center justify-start p-3 md:px-5 flex-row cursor-pointer hover:bg-accent/50 transition-colors",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
onClick={cardClick}
|
||||
>
|
||||
<section
|
||||
className={cn("grid items-center gap-2 w-40")}
|
||||
style={{ gridTemplateColumns: "auto auto 1fr" }}
|
||||
>
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-red-500 self-center"></span>
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center",
|
||||
showFlag ? "min-w-[17px]" : "min-w-0",
|
||||
)}
|
||||
>
|
||||
{showFlag ? <ServerFlag country_code={country_code} /> : null}
|
||||
</div>
|
||||
<div className="relative flex flex-col">
|
||||
<p
|
||||
className={cn(
|
||||
"break-normal font-bold w-28 tracking-tight",
|
||||
showFlag ? "text-xs" : "text-sm",
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
{parsedData?.billingDataMod && (
|
||||
<BillingInfo parsedData={parsedData} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
<Separator orientation="vertical" className="h-8 ml-3 lg:ml-1 mr-3" />
|
||||
{parsedData?.planDataMod && <PlanInfo parsedData={parsedData} />}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
+1161
-873
File diff suppressed because it is too large
Load Diff
@@ -1,310 +1,377 @@
|
||||
import { BackIcon } from "@/components/Icon"
|
||||
import ServerFlag from "@/components/ServerFlag"
|
||||
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils"
|
||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||
import countries from "i18n-iso-countries"
|
||||
import enLocale from "i18n-iso-countries/langs/en.json"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import countries from "i18n-iso-countries";
|
||||
import enLocale from "i18n-iso-countries/langs/en.json";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { BackIcon } from "@/components/Icon";
|
||||
import { ServerDetailLoading } from "@/components/loading/ServerDetailLoading";
|
||||
import ServerFlag from "@/components/ServerFlag";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { cn, formatNezhaInfo } from "@/lib/utils";
|
||||
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./ui/accordion"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip"
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "./ui/accordion";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "./ui/tooltip";
|
||||
|
||||
export default function ServerDetailOverview({ server_id }: { server_id: string }) {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
export default function ServerDetailOverview({
|
||||
server_id,
|
||||
}: {
|
||||
server_id: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [hasHistory, setHasHistory] = useState(false)
|
||||
const [hasHistory, setHasHistory] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const previousPath = sessionStorage.getItem("fromMainPage")
|
||||
if (previousPath) {
|
||||
setHasHistory(true)
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
const previousPath = sessionStorage.getItem("fromMainPage");
|
||||
if (previousPath) {
|
||||
setHasHistory(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { lastMessage, connected } = useWebSocketContext()
|
||||
const { lastMessage, connected } = useWebSocketContext();
|
||||
|
||||
if (!connected && !lastMessage) {
|
||||
return <ServerDetailLoading />
|
||||
}
|
||||
if (!connected && !lastMessage) {
|
||||
return <ServerDetailLoading />;
|
||||
}
|
||||
|
||||
const linkClick = () => {
|
||||
if (hasHistory) {
|
||||
navigate(-1)
|
||||
} else {
|
||||
navigate("/")
|
||||
}
|
||||
}
|
||||
const linkClick = () => {
|
||||
if (hasHistory) {
|
||||
navigate(-1);
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
};
|
||||
|
||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||
const nezhaWsData = lastMessage
|
||||
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||
: null;
|
||||
|
||||
if (!nezhaWsData) {
|
||||
return <ServerDetailLoading />
|
||||
}
|
||||
if (!nezhaWsData) {
|
||||
return <ServerDetailLoading />;
|
||||
}
|
||||
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||
|
||||
if (!server) {
|
||||
return <ServerDetailLoading />
|
||||
}
|
||||
if (!server) {
|
||||
return <ServerDetailLoading />;
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
online,
|
||||
uptime,
|
||||
version,
|
||||
arch,
|
||||
mem_total,
|
||||
disk_total,
|
||||
country_code,
|
||||
platform,
|
||||
platform_version,
|
||||
cpu_info,
|
||||
gpu_info,
|
||||
load_1,
|
||||
load_5,
|
||||
load_15,
|
||||
net_out_transfer,
|
||||
net_in_transfer,
|
||||
last_active_time_string,
|
||||
boot_time_string,
|
||||
} = formatNezhaInfo(nezhaWsData.now, server)
|
||||
const {
|
||||
name,
|
||||
online,
|
||||
uptime,
|
||||
version,
|
||||
arch,
|
||||
mem_total,
|
||||
disk_total,
|
||||
country_code,
|
||||
platform,
|
||||
platform_version,
|
||||
cpu_info,
|
||||
gpu_info,
|
||||
load_1,
|
||||
load_5,
|
||||
load_15,
|
||||
net_out_transfer,
|
||||
net_in_transfer,
|
||||
last_active_time_string,
|
||||
boot_time_string,
|
||||
} = formatNezhaInfo(nezhaWsData.now, server);
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
countries.registerLocale(enLocale)
|
||||
countries.registerLocale(enLocale);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"bg-card/70 p-4 rounded-[10px]": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onClick={linkClick}
|
||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
|
||||
>
|
||||
<BackIcon />
|
||||
{name}
|
||||
</div>
|
||||
<section className="flex flex-wrap gap-2 mt-3">
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.status")}</p>
|
||||
<Badge
|
||||
className={cn("text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white", {
|
||||
" bg-green-800": online,
|
||||
" bg-red-600": !online,
|
||||
})}
|
||||
>
|
||||
{online ? t("serverDetail.online") : t("serverDetail.offline")}
|
||||
</Badge>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{online && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.uptime")}</p>
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{uptime / 86400 >= 1
|
||||
? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}`
|
||||
: `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{version && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.version")}</p>
|
||||
<div className="text-xs">{version} </div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{arch && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.arch")}</p>
|
||||
<div className="text-xs">{arch} </div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"bg-card/70 p-4 rounded-[10px]": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
onClick={linkClick}
|
||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-1 text-xl server-name"
|
||||
>
|
||||
<BackIcon />
|
||||
{name}
|
||||
</div>
|
||||
<section className="flex flex-wrap gap-2 mt-3">
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.status")}
|
||||
</p>
|
||||
<Badge
|
||||
className={cn(
|
||||
"text-[9px] rounded-[6px] w-fit px-1 py-0 -mt-[0.3px] dark:text-white",
|
||||
{
|
||||
" bg-green-800": online,
|
||||
" bg-red-600": !online,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{online ? t("serverDetail.online") : t("serverDetail.offline")}
|
||||
</Badge>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{online && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.uptime")}
|
||||
</p>
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{uptime / 86400 >= 1
|
||||
? `${Math.floor(uptime / 86400)} ${t("serverDetail.days")} ${Math.floor((uptime % 86400) / 3600)} ${t("serverDetail.hours")}`
|
||||
: `${Math.floor(uptime / 3600)} ${t("serverDetail.hours")}`}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{version && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.version")}
|
||||
</p>
|
||||
<div className="text-xs">{version} </div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{arch && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.arch")}
|
||||
</p>
|
||||
<div className="text-xs">{arch} </div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{mem_total ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.mem")}</p>
|
||||
<div className="text-xs">{formatBytes(mem_total)}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{mem_total ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.mem")}
|
||||
</p>
|
||||
<div className="text-xs">{formatBytes(mem_total)}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{disk_total ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.disk")}</p>
|
||||
<div className="text-xs">{formatBytes(disk_total)}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{disk_total ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.disk")}
|
||||
</p>
|
||||
<div className="text-xs">{formatBytes(disk_total)}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{country_code && (
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.region")}</p>
|
||||
<section className="flex items-start gap-1">
|
||||
<div className="text-xs text-start">{country_code?.toUpperCase()}</div>
|
||||
{country_code && <ServerFlag className="text-[11px] -mt-px" country_code={country_code} />}
|
||||
</section>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</section>
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
{platform && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.system")}</p>
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{platform} {platform_version ? " - " + platform_version : ""}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{cpu_info.length > 0 && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="text-xs"> {cpu_info.join(", ")}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{gpu_info.length > 0 && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{"GPU"}</p>
|
||||
<div className="text-xs">{gpu_info.join(", ")}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{"Load"}</p>
|
||||
<div className="text-xs">
|
||||
{load_1} / {load_5} / {load_15}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{net_out_transfer ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.upload")}</p>
|
||||
{net_out_transfer ? (
|
||||
<div className="text-xs"> {formatBytes(net_out_transfer)} </div>
|
||||
) : (
|
||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{net_in_transfer ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.download")}</p>
|
||||
{net_in_transfer ? (
|
||||
<div className="text-xs"> {formatBytes(net_in_transfer)} </div>
|
||||
) : (
|
||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</section>
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
{server?.state.temperatures && server?.state.temperatures.length > 0 && (
|
||||
<section className="flex flex-wrap gap-2 ml-1.5">
|
||||
<Accordion type="single" collapsible className="w-fit">
|
||||
<AccordionItem value="item-1" className="border-none">
|
||||
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">{t("serverDetail.temperature")}</AccordionTrigger>
|
||||
<AccordionContent className="pb-0">
|
||||
<section className="flex items-start flex-wrap gap-2">
|
||||
{server?.state.temperatures.map((item, index) => (
|
||||
<div className="text-xs flex items-center" key={index}>
|
||||
<p className="font-semibold">{item.Name}</p>: {item.Temperature.toFixed(2)} °C
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
{country_code && (
|
||||
<TooltipProvider delayDuration={100}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.region")}
|
||||
</p>
|
||||
<section className="flex items-start gap-1">
|
||||
<div className="text-xs text-start">
|
||||
{country_code?.toUpperCase()}
|
||||
</div>
|
||||
{country_code && (
|
||||
<ServerFlag
|
||||
className="text-[11px] -mt-px"
|
||||
country_code={country_code}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{countries.getName(country_code?.toUpperCase(), "en")}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</section>
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
{platform && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.system")}
|
||||
</p>
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{platform} {platform_version ? ` - ${platform_version}` : ""}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{cpu_info.length > 0 && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{"CPU"}</p>
|
||||
<div className="text-xs"> {cpu_info.join(", ")}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{gpu_info.length > 0 && (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{"GPU"}</p>
|
||||
<div className="text-xs">{gpu_info.join(", ")}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</section>
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{"Load"}</p>
|
||||
<div className="text-xs">
|
||||
{load_1} / {load_5} / {load_15}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{net_out_transfer ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.upload")}
|
||||
</p>
|
||||
{net_out_transfer ? (
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{formatBytes(net_out_transfer)}{" "}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
{net_in_transfer ? (
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.download")}
|
||||
</p>
|
||||
{net_in_transfer ? (
|
||||
<div className="text-xs">
|
||||
{" "}
|
||||
{formatBytes(net_in_transfer)}{" "}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs"> {t("serverDetail.unknown")}</div>
|
||||
)}
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
</section>
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
{server?.state.temperatures &&
|
||||
server?.state.temperatures.length > 0 && (
|
||||
<section className="flex flex-wrap gap-2 ml-1.5">
|
||||
<Accordion type="single" collapsible className="w-fit">
|
||||
<AccordionItem value="item-1" className="border-none">
|
||||
<AccordionTrigger className="text-xs py-0 text-muted-foreground font-normal">
|
||||
{t("serverDetail.temperature")}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pb-0">
|
||||
<section className="flex items-start flex-wrap gap-2">
|
||||
{server?.state.temperatures.map((item, index) => (
|
||||
<div className="text-xs flex items-center" key={index}>
|
||||
<p className="font-semibold">{item.Name}</p>:{" "}
|
||||
{item.Temperature.toFixed(2)} °C
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</section>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.bootTime")}</p>
|
||||
<div className="text-xs">{boot_time_string ? boot_time_string : "N/A"}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">{t("serverDetail.lastActive")}</p>
|
||||
<div className="text-xs">{last_active_time_string ? last_active_time_string : "N/A"}</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
<section className="flex flex-wrap gap-2 mt-1">
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.bootTime")}
|
||||
</p>
|
||||
<div className="text-xs">
|
||||
{boot_time_string ? boot_time_string : "N/A"}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="rounded-[10px] bg-transparent border-none shadow-none">
|
||||
<CardContent className="px-1.5 py-1">
|
||||
<section className="flex flex-col items-start gap-0.5">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("serverDetail.lastActive")}
|
||||
</p>
|
||||
<div className="text-xs">
|
||||
{last_active_time_string ? last_active_time_string : "N/A"}
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,96 +1,111 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context"
|
||||
import { formatNezhaInfo } from "@/lib/utils"
|
||||
import { NezhaWebsocketResponse } from "@/types/nezha-api"
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useWebSocketContext } from "@/hooks/use-websocket-context";
|
||||
import { formatNezhaInfo } from "@/lib/utils";
|
||||
import type { NezhaWebsocketResponse } from "@/types/nezha-api";
|
||||
|
||||
export default function ServerDetailSummary({ server_id }: { server_id: number }) {
|
||||
const { lastMessage, connected } = useWebSocketContext()
|
||||
export default function ServerDetailSummary({
|
||||
server_id,
|
||||
}: {
|
||||
server_id: number;
|
||||
}) {
|
||||
const { lastMessage, connected } = useWebSocketContext();
|
||||
|
||||
if (!connected && !lastMessage) {
|
||||
return null
|
||||
}
|
||||
if (!connected && !lastMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nezhaWsData = lastMessage ? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse) : null
|
||||
const nezhaWsData = lastMessage
|
||||
? (JSON.parse(lastMessage.data) as NezhaWebsocketResponse)
|
||||
: null;
|
||||
|
||||
if (!nezhaWsData) {
|
||||
return null
|
||||
}
|
||||
if (!nezhaWsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id))
|
||||
const server = nezhaWsData.servers.find((s) => s.id === Number(server_id));
|
||||
|
||||
if (!server) {
|
||||
return null
|
||||
}
|
||||
if (!server) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { cpu, mem, disk, up, down, tcp, udp, process } = formatNezhaInfo(nezhaWsData.now, server)
|
||||
const { cpu, mem, disk, up, down, tcp, udp, process } = formatNezhaInfo(
|
||||
nezhaWsData.now,
|
||||
server,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-4">
|
||||
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">CPU</span>
|
||||
<span className="font-medium text-[10px]">{cpu.toFixed(2)}%</span>
|
||||
</section>
|
||||
<UsageBar value={cpu} />
|
||||
</section>
|
||||
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Mem</span>
|
||||
<span className="font-medium text-[10px]">{mem.toFixed(2)}%</span>
|
||||
</section>
|
||||
<UsageBar value={mem} />
|
||||
</section>
|
||||
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Disk</span>
|
||||
<span className="font-medium text-[10px]">{disk.toFixed(2)}%</span>
|
||||
</section>
|
||||
<UsageBar value={disk} />
|
||||
</section>
|
||||
<section className="flex min-w-[85px] flex-col justify-center px-1.5 py-1">
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">Process</span>
|
||||
<span className="font-medium text-[10px]">{process}</span>
|
||||
</section>
|
||||
</section>
|
||||
<section className="flex min-w-[70px] flex-col justify-center gap-0.5 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">TCP</span>
|
||||
<span className="font-medium text-[10px]">{tcp}</span>
|
||||
</section>
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">UDP</span>
|
||||
<span className="font-medium text-[10px]">{udp}</span>
|
||||
</section>
|
||||
</section>
|
||||
<section className="flex min-w-[120px] flex-col justify-center gap-0.5 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">Upload</span>
|
||||
<span className="font-medium text-[10px]">{up.toFixed(2)}M/s</span>
|
||||
</section>
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">Download</span>
|
||||
<span className="font-medium text-[10px]">{down.toFixed(2)}M/s</span>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="mb-2 flex flex-wrap items-center gap-4">
|
||||
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">CPU</span>
|
||||
<span className="font-medium text-[10px]">{cpu.toFixed(2)}%</span>
|
||||
</section>
|
||||
<UsageBar value={cpu} />
|
||||
</section>
|
||||
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Mem</span>
|
||||
<span className="font-medium text-[10px]">{mem.toFixed(2)}%</span>
|
||||
</section>
|
||||
<UsageBar value={mem} />
|
||||
</section>
|
||||
<section className="flex w-24 flex-col justify-center gap-1 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between">
|
||||
<span className="text-[10px] text-muted-foreground">Disk</span>
|
||||
<span className="font-medium text-[10px]">{disk.toFixed(2)}%</span>
|
||||
</section>
|
||||
<UsageBar value={disk} />
|
||||
</section>
|
||||
<section className="flex min-w-[85px] flex-col justify-center px-1.5 py-1">
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">Process</span>
|
||||
<span className="font-medium text-[10px]">{process}</span>
|
||||
</section>
|
||||
</section>
|
||||
<section className="flex min-w-[70px] flex-col justify-center gap-0.5 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">TCP</span>
|
||||
<span className="font-medium text-[10px]">{tcp}</span>
|
||||
</section>
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">UDP</span>
|
||||
<span className="font-medium text-[10px]">{udp}</span>
|
||||
</section>
|
||||
</section>
|
||||
<section className="flex min-w-[120px] flex-col justify-center gap-0.5 px-1.5 py-1">
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">Upload</span>
|
||||
<span className="font-medium text-[10px]">{up.toFixed(2)}M/s</span>
|
||||
</section>
|
||||
<section className="flex items-center justify-between gap-4">
|
||||
<span className="text-[10px] text-muted-foreground">Download</span>
|
||||
<span className="font-medium text-[10px]">{down.toFixed(2)}M/s</span>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type UsageBarProps = {
|
||||
value: number
|
||||
}
|
||||
value: number;
|
||||
};
|
||||
|
||||
function UsageBar({ value }: UsageBarProps) {
|
||||
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 bg-stone-200 dark:bg-stone-800"}
|
||||
/>
|
||||
)
|
||||
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 bg-stone-200 dark:bg-stone-800"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,52 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode"
|
||||
import { useEffect, useState } from "react"
|
||||
import getUnicodeFlagIcon from "country-flag-icons/unicode";
|
||||
import { useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function ServerFlag({ country_code, className }: { country_code: string; className?: string }) {
|
||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false)
|
||||
export default function ServerFlag({
|
||||
country_code,
|
||||
className,
|
||||
}: {
|
||||
country_code: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [supportsEmojiFlags, setSupportsEmojiFlags] = useState(false);
|
||||
|
||||
// @ts-expect-error ForceUseSvgFlag is a global variable
|
||||
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean
|
||||
// @ts-expect-error ForceUseSvgFlag is a global variable
|
||||
const forceUseSvgFlag = window.ForceUseSvgFlag as boolean;
|
||||
|
||||
useEffect(() => {
|
||||
if (forceUseSvgFlag) {
|
||||
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
|
||||
setSupportsEmojiFlags(false)
|
||||
return
|
||||
}
|
||||
useEffect(() => {
|
||||
if (forceUseSvgFlag) {
|
||||
// 如果环境变量要求直接使用 SVG,则无需检查 Emoji 支持
|
||||
setSupportsEmojiFlags(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const checkEmojiSupport = () => {
|
||||
const canvas = document.createElement("canvas")
|
||||
const ctx = canvas.getContext("2d")
|
||||
const emojiFlag = "🇺🇸" // 使用美国国旗作为测试
|
||||
if (!ctx) return
|
||||
ctx.fillStyle = "#000"
|
||||
ctx.textBaseline = "top"
|
||||
ctx.font = "32px Arial"
|
||||
ctx.fillText(emojiFlag, 0, 0)
|
||||
const checkEmojiSupport = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const emojiFlag = "🇺🇸"; // 使用美国国旗作为测试
|
||||
if (!ctx) return;
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.textBaseline = "top";
|
||||
ctx.font = "32px Arial";
|
||||
ctx.fillText(emojiFlag, 0, 0);
|
||||
|
||||
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0
|
||||
setSupportsEmojiFlags(support)
|
||||
}
|
||||
const support = ctx.getImageData(16, 16, 1, 1).data[3] !== 0;
|
||||
setSupportsEmojiFlags(support);
|
||||
};
|
||||
|
||||
checkEmojiSupport()
|
||||
}, [])
|
||||
checkEmojiSupport();
|
||||
}, [forceUseSvgFlag]);
|
||||
|
||||
if (!country_code) return null
|
||||
if (!country_code) return null;
|
||||
|
||||
return (
|
||||
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
||||
{forceUseSvgFlag || !supportsEmojiFlags ? <span className={`fi fi-${country_code}`} /> : getUnicodeFlagIcon(country_code)}
|
||||
</span>
|
||||
)
|
||||
return (
|
||||
<span className={cn("text-[12px] text-muted-foreground", className)}>
|
||||
{forceUseSvgFlag || !supportsEmojiFlags ? (
|
||||
<span className={`fi fi-${country_code}`} />
|
||||
) : (
|
||||
getUnicodeFlagIcon(country_code)
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
+170
-143
@@ -1,146 +1,173 @@
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useStatus } from "@/hooks/use-status"
|
||||
import { formatBytes } from "@/lib/format"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/20/solid"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import {
|
||||
ArrowDownCircleIcon,
|
||||
ArrowUpCircleIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { useStatus } from "@/hooks/use-status";
|
||||
import { formatBytes } from "@/lib/format";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type ServerOverviewProps = {
|
||||
online: number
|
||||
offline: number
|
||||
total: number
|
||||
up: number
|
||||
down: number
|
||||
upSpeed: number
|
||||
downSpeed: number
|
||||
}
|
||||
|
||||
export default function ServerOverview({ online, offline, total, up, down, upSpeed, downSpeed }: ServerOverviewProps) {
|
||||
const { t } = useTranslation()
|
||||
const { status, setStatus } = useStatus()
|
||||
|
||||
// @ts-expect-error DisableAnimatedMan is a global variable
|
||||
const disableAnimatedMan = window.DisableAnimatedMan as boolean
|
||||
|
||||
// @ts-expect-error CustomIllustration is a global variable
|
||||
const customIllustration = window.CustomIllustration || "/animated-man.webp"
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4 server-overview">
|
||||
<Card
|
||||
onClick={() => {
|
||||
setStatus("all")
|
||||
}}
|
||||
className={cn("hover:border-blue-500 cursor-pointer transition-all", {
|
||||
"bg-card/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<CardContent className="flex h-full items-center px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.totalServers")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
|
||||
</span>
|
||||
<div className="text-lg font-semibold">{total}</div>
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => {
|
||||
setStatus("online")
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
{
|
||||
"ring-green-500 ring-2 border-transparent": status === "online",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex h-full items-center px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.onlineServers")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
|
||||
<div className="text-lg font-semibold">{online}</div>
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => {
|
||||
setStatus("offline")
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
{
|
||||
"ring-red-500 ring-2 border-transparent": status === "offline",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex h-full items-center px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.offlineServers")}</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
||||
</span>
|
||||
<div className="text-lg font-semibold">{offline}</div>
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
className={cn("hover:ring-purple-500 ring-1 ring-transparent transition-all", {
|
||||
"bg-card/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<CardContent className="flex h-full items-center relative px-6 py-3">
|
||||
<section className="flex flex-col gap-1 w-full">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<p className="text-sm font-medium md:text-base">{t("serverOverview.network")}</p>
|
||||
</div>
|
||||
<section className="flex items-start flex-row z-10 pr-0 gap-1">
|
||||
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">↑{formatBytes(up)}</p>
|
||||
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">↓{formatBytes(down)}</p>
|
||||
</section>
|
||||
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
|
||||
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||
<ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-px" />
|
||||
{formatBytes(upSpeed)}/s
|
||||
</p>
|
||||
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||
<ArrowDownCircleIcon className="size-3 mr-0.5" />
|
||||
{formatBytes(downSpeed)}/s
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
{!disableAnimatedMan && (
|
||||
<img
|
||||
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
|
||||
alt={"animated-man"}
|
||||
src={customIllustration}
|
||||
loading="eager"
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
online: number;
|
||||
offline: number;
|
||||
total: number;
|
||||
up: number;
|
||||
down: number;
|
||||
upSpeed: number;
|
||||
downSpeed: number;
|
||||
};
|
||||
|
||||
export default function ServerOverview({
|
||||
online,
|
||||
offline,
|
||||
total,
|
||||
up,
|
||||
down,
|
||||
upSpeed,
|
||||
downSpeed,
|
||||
}: ServerOverviewProps) {
|
||||
const { t } = useTranslation();
|
||||
const { status, setStatus } = useStatus();
|
||||
|
||||
// @ts-expect-error DisableAnimatedMan is a global variable
|
||||
const disableAnimatedMan = window.DisableAnimatedMan as boolean;
|
||||
|
||||
// @ts-expect-error CustomIllustration is a global variable
|
||||
const customIllustration = window.CustomIllustration || "/animated-man.webp";
|
||||
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<section className="grid grid-cols-2 gap-4 lg:grid-cols-4 server-overview">
|
||||
<Card
|
||||
onClick={() => {
|
||||
setStatus("all");
|
||||
}}
|
||||
className={cn("hover:border-blue-500 cursor-pointer transition-all", {
|
||||
"bg-card/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<CardContent className="flex h-full items-center px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{t("serverOverview.totalServers")}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-blue-500"></span>
|
||||
</span>
|
||||
<div className="text-lg font-semibold">{total}</div>
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => {
|
||||
setStatus("online");
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer hover:ring-green-500 ring-1 ring-transparent transition-all",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
{
|
||||
"ring-green-500 ring-2 border-transparent": status === "online",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex h-full items-center px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{t("serverOverview.onlineServers")}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-green-500 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-green-500"></span>
|
||||
</span>
|
||||
|
||||
<div className="text-lg font-semibold">{online}</div>
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
onClick={() => {
|
||||
setStatus("offline");
|
||||
}}
|
||||
className={cn(
|
||||
"cursor-pointer hover:ring-red-500 ring-1 ring-transparent transition-all",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
{
|
||||
"ring-red-500 ring-2 border-transparent": status === "offline",
|
||||
},
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex h-full items-center px-6 py-3">
|
||||
<section className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{t("serverOverview.offlineServers")}
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="relative flex h-2 w-2">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-500 opacity-75"></span>
|
||||
<span className="relative inline-flex h-2 w-2 rounded-full bg-red-500"></span>
|
||||
</span>
|
||||
<div className="text-lg font-semibold">{offline}</div>
|
||||
</div>
|
||||
</section>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card
|
||||
className={cn(
|
||||
"hover:ring-purple-500 ring-1 ring-transparent transition-all",
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<CardContent className="flex h-full items-center relative px-6 py-3">
|
||||
<section className="flex flex-col gap-1 w-full">
|
||||
<div className="flex items-center w-full justify-between">
|
||||
<p className="text-sm font-medium md:text-base">
|
||||
{t("serverOverview.network")}
|
||||
</p>
|
||||
</div>
|
||||
<section className="flex items-start flex-row z-10 pr-0 gap-1">
|
||||
<p className="sm:text-[12px] text-[10px] text-blue-800 dark:text-blue-400 text-nowrap font-medium">
|
||||
↑{formatBytes(up)}
|
||||
</p>
|
||||
<p className="sm:text-[12px] text-[10px] text-purple-800 dark:text-purple-400 text-nowrap font-medium">
|
||||
↓{formatBytes(down)}
|
||||
</p>
|
||||
</section>
|
||||
<section className="flex flex-col sm:flex-row -mr-1 sm:items-center items-start gap-1">
|
||||
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||
<ArrowUpCircleIcon className="size-3 mr-0.5 sm:mb-px" />
|
||||
{formatBytes(upSpeed)}/s
|
||||
</p>
|
||||
<p className="text-[11px] flex items-center text-nowrap font-semibold">
|
||||
<ArrowDownCircleIcon className="size-3 mr-0.5" />
|
||||
{formatBytes(downSpeed)}/s
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
{!disableAnimatedMan && (
|
||||
<img
|
||||
className="absolute right-3 top-[-85px] z-50 w-20 scale-90 group-hover:opacity-50 md:scale-100 transition-all"
|
||||
alt={"animated-man"}
|
||||
src={customIllustration}
|
||||
loading="eager"
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
|
||||
type ServerUsageBarProps = {
|
||||
value: number
|
||||
}
|
||||
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"}
|
||||
/>
|
||||
)
|
||||
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"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,100 @@
|
||||
import { fetchService } from "@/lib/nezha-api"
|
||||
import { NezhaServer, ServiceData } from "@/types/nezha-api"
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { fetchService } from "@/lib/nezha-api";
|
||||
import type { NezhaServer, ServiceData } from "@/types/nezha-api";
|
||||
|
||||
import { CycleTransferStatsCard } from "./CycleTransferStats"
|
||||
import ServiceTrackerClient from "./ServiceTrackerClient"
|
||||
import { Loader } from "./loading/Loader"
|
||||
import { CycleTransferStatsCard } from "./CycleTransferStats";
|
||||
import { Loader } from "./loading/Loader";
|
||||
import ServiceTrackerClient from "./ServiceTrackerClient";
|
||||
|
||||
export function ServiceTracker({ serverList }: { serverList: NezhaServer[] }) {
|
||||
const { t } = useTranslation()
|
||||
const { data: serviceData, isLoading } = useQuery({
|
||||
queryKey: ["service"],
|
||||
queryFn: () => fetchService(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchInterval: 10000,
|
||||
})
|
||||
const { t } = useTranslation();
|
||||
const { data: serviceData, isLoading } = useQuery({
|
||||
queryKey: ["service"],
|
||||
queryFn: () => fetchService(),
|
||||
refetchOnMount: true,
|
||||
refetchOnWindowFocus: true,
|
||||
refetchInterval: 10000,
|
||||
});
|
||||
|
||||
const processServiceData = (serviceData: ServiceData) => {
|
||||
const days = serviceData.up.map((up, index) => {
|
||||
const totalChecks = up + serviceData.down[index]
|
||||
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0
|
||||
return {
|
||||
completed: up > serviceData.down[index],
|
||||
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
||||
uptime: dailyUptime,
|
||||
delay: serviceData.delay[index] || 0,
|
||||
}
|
||||
})
|
||||
const processServiceData = (serviceData: ServiceData) => {
|
||||
const days = serviceData.up.map((up, index) => {
|
||||
const totalChecks = up + serviceData.down[index];
|
||||
const dailyUptime = totalChecks > 0 ? (up / totalChecks) * 100 : 0;
|
||||
return {
|
||||
completed: up > serviceData.down[index],
|
||||
date: new Date(Date.now() - (29 - index) * 24 * 60 * 60 * 1000),
|
||||
uptime: dailyUptime,
|
||||
delay: serviceData.delay[index] || 0,
|
||||
};
|
||||
});
|
||||
|
||||
const totalUp = serviceData.up.reduce((a, b) => a + b, 0)
|
||||
const totalChecks = serviceData.up.reduce((a, b) => a + b, 0) + serviceData.down.reduce((a, b) => a + b, 0)
|
||||
const uptime = (totalUp / totalChecks) * 100
|
||||
const totalUp = serviceData.up.reduce((a, b) => a + b, 0);
|
||||
const totalChecks =
|
||||
serviceData.up.reduce((a, b) => a + b, 0) +
|
||||
serviceData.down.reduce((a, b) => a + b, 0);
|
||||
const uptime = (totalUp / totalChecks) * 100;
|
||||
|
||||
const avgDelay = serviceData.delay.length > 0 ? serviceData.delay.reduce((a, b) => a + b, 0) / serviceData.delay.length : 0
|
||||
const avgDelay =
|
||||
serviceData.delay.length > 0
|
||||
? serviceData.delay.reduce((a, b) => a + b, 0) /
|
||||
serviceData.delay.length
|
||||
: 0;
|
||||
|
||||
return { days, uptime, avgDelay }
|
||||
}
|
||||
return { days, uptime, avgDelay };
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
||||
<Loader visible={true} />
|
||||
{t("serviceTracker.loading")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
||||
<Loader visible={true} />
|
||||
{t("serviceTracker.loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!serviceData?.data?.services && !serviceData?.data?.cycle_transfer_stats) {
|
||||
return (
|
||||
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
||||
<ExclamationTriangleIcon className="w-4 h-4" />
|
||||
{t("serviceTracker.noService")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
if (
|
||||
!serviceData?.data?.services &&
|
||||
!serviceData?.data?.cycle_transfer_stats
|
||||
) {
|
||||
return (
|
||||
<div className="mt-4 text-sm font-medium flex items-center gap-1">
|
||||
<ExclamationTriangleIcon className="w-4 h-4" />
|
||||
{t("serviceTracker.noService")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 w-full mx-auto ">
|
||||
{serviceData.data.cycle_transfer_stats && (
|
||||
<div>
|
||||
<CycleTransferStatsCard serverList={serverList} cycleStats={serviceData.data.cycle_transfer_stats} />
|
||||
</div>
|
||||
)}
|
||||
{serviceData.data.services && Object.keys(serviceData.data.services).length > 0 && (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
||||
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
||||
const { days, uptime, avgDelay } = processServiceData(data)
|
||||
return <ServiceTrackerClient key={name} days={days} title={data.service_name} uptime={uptime} avgDelay={avgDelay} />
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="mt-4 w-full mx-auto ">
|
||||
{serviceData.data.cycle_transfer_stats && (
|
||||
<div>
|
||||
<CycleTransferStatsCard
|
||||
serverList={serverList}
|
||||
cycleStats={serviceData.data.cycle_transfer_stats}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{serviceData.data.services &&
|
||||
Object.keys(serviceData.data.services).length > 0 && (
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 mt-4 gap-2 md:gap-4">
|
||||
{Object.entries(serviceData.data.services).map(([name, data]) => {
|
||||
const { days, uptime, avgDelay } = processServiceData(data);
|
||||
return (
|
||||
<ServiceTrackerClient
|
||||
key={name}
|
||||
days={days}
|
||||
title={data.service_name}
|
||||
uptime={uptime}
|
||||
avgDelay={avgDelay}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ServiceTracker
|
||||
export default ServiceTracker;
|
||||
|
||||
@@ -1,118 +1,166 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { cn } from "@/lib/utils"
|
||||
import React from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { Separator } from "./ui/separator"
|
||||
import { Separator } from "./ui/separator";
|
||||
|
||||
interface ServiceTrackerProps {
|
||||
days: Array<{
|
||||
completed: boolean
|
||||
date?: Date
|
||||
uptime: number
|
||||
delay: number
|
||||
}>
|
||||
className?: string
|
||||
title?: string
|
||||
uptime?: number
|
||||
avgDelay?: number
|
||||
days: Array<{
|
||||
completed: boolean;
|
||||
date?: Date;
|
||||
uptime: number;
|
||||
delay: number;
|
||||
}>;
|
||||
className?: string;
|
||||
title?: string;
|
||||
uptime?: number;
|
||||
avgDelay?: number;
|
||||
}
|
||||
|
||||
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({ days, className, title, uptime = 100, avgDelay = 0 }) => {
|
||||
const { t } = useTranslation()
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
export const ServiceTrackerClient: React.FC<ServiceTrackerProps> = ({
|
||||
days,
|
||||
className,
|
||||
title,
|
||||
uptime = 100,
|
||||
avgDelay = 0,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
const getUptimeColor = (uptime: number) => {
|
||||
if (uptime >= 99) return "text-emerald-500"
|
||||
if (uptime >= 95) return "text-amber-500"
|
||||
return "text-rose-500"
|
||||
}
|
||||
const getUptimeColor = (uptime: number) => {
|
||||
if (uptime >= 99) return "text-emerald-500";
|
||||
if (uptime >= 95) return "text-amber-500";
|
||||
return "text-rose-500";
|
||||
};
|
||||
|
||||
const getDelayColor = (delay: number) => {
|
||||
if (delay < 100) return "text-emerald-500"
|
||||
if (delay < 300) return "text-amber-500"
|
||||
return "text-rose-500"
|
||||
}
|
||||
const getDelayColor = (delay: number) => {
|
||||
if (delay < 100) return "text-emerald-500";
|
||||
if (delay < 300) return "text-amber-500";
|
||||
return "text-rose-500";
|
||||
};
|
||||
|
||||
const getStatusColor = (uptime: number) => {
|
||||
if (uptime >= 99) return "bg-emerald-500"
|
||||
if (uptime >= 95) return "bg-amber-500"
|
||||
return "bg-rose-500"
|
||||
}
|
||||
const getStatusColor = (uptime: number) => {
|
||||
if (uptime >= 99) return "bg-emerald-500";
|
||||
if (uptime >= 95) return "bg-amber-500";
|
||||
return "bg-rose-500";
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
||||
className,
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn("w-2.5 h-2.5 rounded-full transition-colors", getStatusColor(uptime))} />
|
||||
<span className="font-medium text-sm">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={cn("font-medium text-sm transition-colors", getDelayColor(avgDelay))}>{avgDelay.toFixed(0)}ms</span>
|
||||
<Separator className="h-4" orientation="vertical" />
|
||||
<span className={cn("font-medium text-sm transition-colors", getUptimeColor(uptime))}>
|
||||
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"w-full space-y-3 bg-white px-4 py-4 rounded-lg border bg-card text-card-foreground shadow-lg shadow-neutral-200/40 dark:shadow-none",
|
||||
className,
|
||||
{
|
||||
"bg-card/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={cn(
|
||||
"w-2.5 h-2.5 rounded-full transition-colors",
|
||||
getStatusColor(uptime),
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium text-sm">{title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-sm transition-colors",
|
||||
getDelayColor(avgDelay),
|
||||
)}
|
||||
>
|
||||
{avgDelay.toFixed(0)}ms
|
||||
</span>
|
||||
<Separator className="h-4" orientation="vertical" />
|
||||
<span
|
||||
className={cn(
|
||||
"font-medium text-sm transition-colors",
|
||||
getUptimeColor(uptime),
|
||||
)}
|
||||
>
|
||||
{uptime.toFixed(1)}% {t("serviceTracker.uptime")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
|
||||
{days.map((day, index) => (
|
||||
<TooltipProvider delayDuration={50} key={index}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
|
||||
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
|
||||
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_--theme(--color-white/10%)]",
|
||||
day.completed
|
||||
? "bg-linear-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_--theme(--color-green-600/30%)]"
|
||||
: "bg-linear-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_--theme(--color-red-600/30%)]",
|
||||
)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="p-0 overflow-hidden">
|
||||
<div className="px-3 py-2 bg-popover">
|
||||
<p className="font-medium text-sm mb-2">{day.date?.toLocaleDateString()}</p>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs text-muted-foreground">{t("serviceTracker.uptime")}:</span>
|
||||
<span className={cn("text-xs font-medium", day.uptime > 95 ? "text-green-500" : "text-red-500")}>{day.uptime.toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs text-muted-foreground">{t("serviceTracker.delay")}:</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
day.delay < 100 ? "text-green-500" : day.delay < 300 ? "text-yellow-500" : "text-red-500",
|
||||
)}
|
||||
>
|
||||
{day.delay.toFixed(0)}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-[3px] bg-muted/30 p-1 rounded-lg">
|
||||
{days.map((day, index) => (
|
||||
<TooltipProvider delayDuration={50} key={index}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex-1 h-7 rounded-[4px] transition-all duration-200 cursor-help",
|
||||
"before:absolute before:inset-0 before:rounded-[4px] before:opacity-0 hover:before:opacity-100 before:bg-white/10 before:transition-opacity",
|
||||
"after:absolute after:inset-0 after:rounded-[4px] after:shadow-[inset_0_1px_--theme(--color-white/10%)]",
|
||||
day.completed
|
||||
? "bg-linear-to-b from-green-500/90 to-green-600 shadow-[0_1px_2px_--theme(--color-green-600/30%)]"
|
||||
: "bg-linear-to-b from-red-500/80 to-red-600/90 shadow-[0_1px_2px_--theme(--color-red-600/30%)]",
|
||||
)}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="p-0 overflow-hidden">
|
||||
<div className="px-3 py-2 bg-popover">
|
||||
<p className="font-medium text-sm mb-2">
|
||||
{day.date?.toLocaleDateString()}
|
||||
</p>
|
||||
<div className="space-y-1.5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("serviceTracker.uptime")}:
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
day.uptime > 95 ? "text-green-500" : "text-red-500",
|
||||
)}
|
||||
>
|
||||
{day.uptime.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{t("serviceTracker.delay")}:
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
day.delay < 100
|
||||
? "text-green-500"
|
||||
: day.delay < 300
|
||||
? "text-yellow-500"
|
||||
: "text-red-500",
|
||||
)}
|
||||
>
|
||||
{day.delay.toFixed(0)}ms
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
|
||||
<span>30 {t("serviceTracker.daysAgo")}</span>
|
||||
<span>{t("serviceTracker.today")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="flex justify-between text-xs text-stone-500 dark:text-stone-400">
|
||||
<span>30 {t("serviceTracker.daysAgo")}</span>
|
||||
<span>{t("serviceTracker.today")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceTrackerClient
|
||||
export default ServiceTrackerClient;
|
||||
|
||||
@@ -1,42 +1,58 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { m } from "framer-motion"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { m } from "framer-motion";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function TabSwitch({ tabs, currentTab, setCurrentTab }: { tabs: string[]; currentTab: string; setCurrentTab: (tab: string) => void }) {
|
||||
const { t } = useTranslation()
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
return (
|
||||
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
|
||||
<div
|
||||
className={cn("flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800", {
|
||||
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
{tabs.map((tab: string) => (
|
||||
<div
|
||||
key={tab}
|
||||
onClick={() => setCurrentTab(tab)}
|
||||
className={cn(
|
||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
|
||||
currentTab === tab ? "text-black dark:text-white" : "text-stone-400 dark:text-stone-500",
|
||||
)}
|
||||
>
|
||||
{currentTab === tab && (
|
||||
<m.div
|
||||
layoutId="tab-switch-active"
|
||||
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||
style={{
|
||||
originY: "0px",
|
||||
borderRadius: 46,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-20 flex items-center gap-1">
|
||||
<p className="whitespace-nowrap">{t("tabSwitch." + tab)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
export default function TabSwitch({
|
||||
tabs,
|
||||
currentTab,
|
||||
setCurrentTab,
|
||||
}: {
|
||||
tabs: string[];
|
||||
currentTab: string;
|
||||
setCurrentTab: (tab: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
return (
|
||||
<div className="z-50 flex flex-col items-start rounded-[50px] server-info-tab">
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-1 rounded-[50px] bg-stone-100 p-[3px] dark:bg-stone-800",
|
||||
{
|
||||
"bg-stone-100/70 dark:bg-stone-800/70": customBackgroundImage,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{tabs.map((tab: string) => (
|
||||
<div
|
||||
key={tab}
|
||||
onClick={() => setCurrentTab(tab)}
|
||||
className={cn(
|
||||
"relative cursor-pointer rounded-3xl px-2.5 py-[8px] text-[13px] font-semibold transition-all duration-500",
|
||||
currentTab === tab
|
||||
? "text-black dark:text-white"
|
||||
: "text-stone-400 dark:text-stone-500",
|
||||
)}
|
||||
>
|
||||
{currentTab === tab && (
|
||||
<m.div
|
||||
layoutId="tab-switch-active"
|
||||
className="absolute inset-0 z-10 h-full w-full content-center bg-white shadow-lg shadow-black/5 dark:bg-stone-700 dark:shadow-white/5"
|
||||
style={{
|
||||
originY: "0px",
|
||||
borderRadius: 46,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-20 flex items-center gap-1">
|
||||
<p className="whitespace-nowrap">{t(`tabSwitch.${tab}`)}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +1,41 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "@/hooks/use-theme"
|
||||
import { useEffect } from "react"
|
||||
import { useEffect } from "react";
|
||||
import { useTheme } from "@/hooks/use-theme";
|
||||
|
||||
export function ThemeColorManager() {
|
||||
const { theme } = useTheme()
|
||||
const { theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const updateThemeColor = () => {
|
||||
const currentTheme = theme
|
||||
const meta = document.querySelector('meta[name="theme-color"]')
|
||||
useEffect(() => {
|
||||
const updateThemeColor = () => {
|
||||
const currentTheme = theme;
|
||||
const meta = document.querySelector('meta[name="theme-color"]');
|
||||
|
||||
if (!meta) {
|
||||
const newMeta = document.createElement("meta")
|
||||
newMeta.name = "theme-color"
|
||||
document.head.appendChild(newMeta)
|
||||
}
|
||||
if (!meta) {
|
||||
const newMeta = document.createElement("meta");
|
||||
newMeta.name = "theme-color";
|
||||
document.head.appendChild(newMeta);
|
||||
}
|
||||
|
||||
const themeColor =
|
||||
currentTheme === "dark"
|
||||
? "hsl(30 15% 8%)" // 深色模式背景色
|
||||
: "hsl(0 0% 98%)" // 浅色模式背景色
|
||||
const themeColor =
|
||||
currentTheme === "dark"
|
||||
? "hsl(30 15% 8%)" // 深色模式背景色
|
||||
: "hsl(0 0% 98%)"; // 浅色模式背景色
|
||||
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||
}
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
};
|
||||
|
||||
// Update on mount and theme change
|
||||
updateThemeColor()
|
||||
// Update on mount and theme change
|
||||
updateThemeColor();
|
||||
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
mediaQuery.addEventListener("change", updateThemeColor)
|
||||
// Listen for system theme changes
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mediaQuery.addEventListener("change", updateThemeColor);
|
||||
|
||||
return () => mediaQuery.removeEventListener("change", updateThemeColor)
|
||||
}, [theme])
|
||||
return () => mediaQuery.removeEventListener("change", updateThemeColor);
|
||||
}, [theme]);
|
||||
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,64 +1,81 @@
|
||||
import { ReactNode, createContext, useEffect, useState } from "react"
|
||||
import { createContext, type ReactNode, useEffect, useState } from "react";
|
||||
|
||||
export type Theme = "dark" | "light" | "system"
|
||||
export type Theme = "dark" | "light" | "system";
|
||||
|
||||
type ThemeProviderProps = {
|
||||
children: ReactNode
|
||||
defaultTheme?: Theme
|
||||
storageKey?: string
|
||||
}
|
||||
children: ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
};
|
||||
|
||||
type ThemeProviderState = {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
}
|
||||
theme: Theme;
|
||||
setTheme: (theme: Theme) => void;
|
||||
};
|
||||
|
||||
const initialState: ThemeProviderState = {
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
theme: "system",
|
||||
setTheme: () => null,
|
||||
};
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState);
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
storageKey = "vite-ui-theme",
|
||||
}: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(
|
||||
() => (localStorage.getItem(storageKey) as Theme) || "system",
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement;
|
||||
|
||||
root.classList.add("disable-transitions");
|
||||
root.classList.remove("light", "dark");
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
|
||||
.matches
|
||||
? "dark"
|
||||
: "light";
|
||||
|
||||
root.classList.add(systemTheme);
|
||||
const themeColor =
|
||||
systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
root.classList.remove("disable-transitions");
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
root.classList.add(theme);
|
||||
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)";
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", themeColor);
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
root.classList.remove("disable-transitions");
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timeoutId);
|
||||
}, [theme]);
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme);
|
||||
setTheme(theme);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProviderContext.Provider value={value}>
|
||||
{children}
|
||||
</ThemeProviderContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
|
||||
|
||||
export function ThemeProvider({ children, storageKey = "vite-ui-theme" }: ThemeProviderProps) {
|
||||
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem(storageKey) as Theme) || "system")
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
|
||||
root.classList.add("disable-transitions")
|
||||
root.classList.remove("light", "dark")
|
||||
|
||||
if (theme === "system") {
|
||||
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
|
||||
|
||||
root.classList.add(systemTheme)
|
||||
const themeColor = systemTheme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
root.classList.remove("disable-transitions")
|
||||
}, 0)
|
||||
return () => window.clearTimeout(timeoutId)
|
||||
}
|
||||
|
||||
root.classList.add(theme)
|
||||
const themeColor = theme === "dark" ? "hsl(30 15% 8%)" : "hsl(0 0% 98%)"
|
||||
document.querySelector('meta[name="theme-color"]')?.setAttribute("content", themeColor)
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
root.classList.remove("disable-transitions")
|
||||
}, 0)
|
||||
return () => window.clearTimeout(timeoutId)
|
||||
}, [theme])
|
||||
|
||||
const value = {
|
||||
theme,
|
||||
setTheme: (theme: Theme) => {
|
||||
localStorage.setItem(storageKey, theme)
|
||||
setTheme(theme)
|
||||
},
|
||||
}
|
||||
|
||||
return <ThemeProviderContext.Provider value={value}>{children}</ThemeProviderContext.Provider>
|
||||
}
|
||||
|
||||
export { ThemeProviderContext }
|
||||
export { ThemeProviderContext };
|
||||
|
||||
@@ -1,62 +1,76 @@
|
||||
import { Theme } from "@/components/ThemeProvider"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid"
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Theme } from "@/components/ThemeProvider";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { useTheme } from "../hooks/use-theme"
|
||||
import { useTheme } from "../hooks/use-theme";
|
||||
|
||||
export function ModeToggle() {
|
||||
const { t } = useTranslation()
|
||||
const { setTheme, theme } = useTheme()
|
||||
const { t } = useTranslation();
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
const customBackgroundImage = (window.CustomBackgroundImage as string) !== "" ? window.CustomBackgroundImage : undefined
|
||||
const customBackgroundImage =
|
||||
(window.CustomBackgroundImage as string) !== ""
|
||||
? window.CustomBackgroundImage
|
||||
: undefined;
|
||||
|
||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
||||
e.preventDefault()
|
||||
setTheme(newTheme)
|
||||
}
|
||||
const handleSelect = (e: Event, newTheme: Theme) => {
|
||||
e.preventDefault();
|
||||
setTheme(newTheme);
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<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 className="flex flex-col gap-0.5" align="end">
|
||||
<DropdownMenuItem
|
||||
className={cn("rounded-b-[5px] text-xs", { "gap-3 bg-muted font-semibold": theme === "light" })}
|
||||
onSelect={(e) => handleSelect(e, "light")}
|
||||
>
|
||||
{t("theme.light")}
|
||||
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn("rounded-[5px] text-xs", { "gap-3 bg-muted font-semibold": theme === "dark" })}
|
||||
onSelect={(e) => handleSelect(e, "dark")}
|
||||
>
|
||||
{t("theme.dark")}
|
||||
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn("rounded-t-[5px] text-xs", { "gap-3 bg-muted font-semibold": theme === "system" })}
|
||||
onSelect={(e) => handleSelect(e, "system")}
|
||||
>
|
||||
{t("theme.system")}
|
||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className={cn("rounded-full px-[9px] bg-white dark:bg-black", {
|
||||
"bg-white/70 dark:bg-black/70": customBackgroundImage,
|
||||
})}
|
||||
>
|
||||
<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 className="flex flex-col gap-0.5" align="end">
|
||||
<DropdownMenuItem
|
||||
className={cn("rounded-b-[5px] text-xs", {
|
||||
"gap-3 bg-muted font-semibold": theme === "light",
|
||||
})}
|
||||
onSelect={(e) => handleSelect(e, "light")}
|
||||
>
|
||||
{t("theme.light")}
|
||||
{theme === "light" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn("rounded-[5px] text-xs", {
|
||||
"gap-3 bg-muted font-semibold": theme === "dark",
|
||||
})}
|
||||
onSelect={(e) => handleSelect(e, "dark")}
|
||||
>
|
||||
{t("theme.dark")}
|
||||
{theme === "dark" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className={cn("rounded-t-[5px] text-xs", {
|
||||
"gap-3 bg-muted font-semibold": theme === "system",
|
||||
})}
|
||||
onSelect={(e) => handleSelect(e, "system")}
|
||||
>
|
||||
{t("theme.system")}
|
||||
{theme === "system" && <CheckCircleIcon className="size-4" />}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,101 @@
|
||||
import { PublicNoteData, cn, getDaysBetweenDatesWithAutoRenewal } from "@/lib/utils"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
cn,
|
||||
getDaysBetweenDatesWithAutoRenewal,
|
||||
type PublicNoteData,
|
||||
} from "@/lib/utils";
|
||||
|
||||
import RemainPercentBar from "./RemainPercentBar"
|
||||
import RemainPercentBar from "./RemainPercentBar";
|
||||
|
||||
export default function BillingInfo({ parsedData }: { parsedData: PublicNoteData }) {
|
||||
const { t } = useTranslation()
|
||||
if (!parsedData || !parsedData.billingDataMod) {
|
||||
return null
|
||||
}
|
||||
export default function BillingInfo({
|
||||
parsedData,
|
||||
}: {
|
||||
parsedData: PublicNoteData;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
if (!parsedData || !parsedData.billingDataMod) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let isNeverExpire = false
|
||||
let daysLeftObject = {
|
||||
days: 0,
|
||||
cycleLabel: "",
|
||||
remainingPercentage: 0,
|
||||
}
|
||||
let isNeverExpire = false;
|
||||
let daysLeftObject = {
|
||||
days: 0,
|
||||
cycleLabel: "",
|
||||
remainingPercentage: 0,
|
||||
};
|
||||
|
||||
if (parsedData?.billingDataMod?.endDate) {
|
||||
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
|
||||
isNeverExpire = true
|
||||
} else {
|
||||
try {
|
||||
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(parsedData.billingDataMod)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return (
|
||||
<div className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||
{t("billingInfo.remaining")}: {t("billingInfo.error")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsedData?.billingDataMod?.endDate) {
|
||||
if (parsedData.billingDataMod.endDate.startsWith("0000-00-00")) {
|
||||
isNeverExpire = true;
|
||||
} else {
|
||||
try {
|
||||
daysLeftObject = getDaysBetweenDatesWithAutoRenewal(
|
||||
parsedData.billingDataMod,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return (
|
||||
<div className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||
{t("billingInfo.remaining")}: {t("billingInfo.error")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return daysLeftObject.days >= 0 ? (
|
||||
<>
|
||||
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
|
||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
||||
</p>
|
||||
) : parsedData.billingDataMod.amount === "0" ? (
|
||||
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p>
|
||||
) : parsedData.billingDataMod.amount === "-1" ? (
|
||||
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p>
|
||||
) : null}
|
||||
<div className={cn("text-[10px] text-muted-foreground")}>
|
||||
{t("billingInfo.remaining")}: {isNeverExpire ? t("billingInfo.indefinite") : daysLeftObject.days + " " + t("billingInfo.days")}
|
||||
</div>
|
||||
{!isNeverExpire && <RemainPercentBar className="mt-0.5" value={daysLeftObject.remainingPercentage * 100} />}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{parsedData.billingDataMod.amount && parsedData.billingDataMod.amount !== "0" && parsedData.billingDataMod.amount !== "-1" ? (
|
||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/{parsedData.billingDataMod.cycle}
|
||||
</p>
|
||||
) : parsedData.billingDataMod.amount === "0" ? (
|
||||
<p className={cn("text-[10px] text-green-600 ")}>{t("billingInfo.free")}</p>
|
||||
) : parsedData.billingDataMod.amount === "-1" ? (
|
||||
<p className={cn("text-[10px] text-pink-600 ")}>{t("billingInfo.usage-baseed")}</p>
|
||||
) : null}
|
||||
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||
{t("billingInfo.expired")}: {daysLeftObject.days * -1} {t("billingInfo.days")}
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
return daysLeftObject.days >= 0 ? (
|
||||
<>
|
||||
{parsedData.billingDataMod.amount &&
|
||||
parsedData.billingDataMod.amount !== "0" &&
|
||||
parsedData.billingDataMod.amount !== "-1" ? (
|
||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
|
||||
{parsedData.billingDataMod.cycle}
|
||||
</p>
|
||||
) : parsedData.billingDataMod.amount === "0" ? (
|
||||
<p className={cn("text-[10px] text-green-600 ")}>
|
||||
{t("billingInfo.free")}
|
||||
</p>
|
||||
) : parsedData.billingDataMod.amount === "-1" ? (
|
||||
<p className={cn("text-[10px] text-pink-600 ")}>
|
||||
{t("billingInfo.usage-baseed")}
|
||||
</p>
|
||||
) : null}
|
||||
<div className={cn("text-[10px] text-muted-foreground")}>
|
||||
{t("billingInfo.remaining")}:{" "}
|
||||
{isNeverExpire
|
||||
? t("billingInfo.indefinite")
|
||||
: `${daysLeftObject.days} ${t("billingInfo.days")}`}
|
||||
</div>
|
||||
{!isNeverExpire && (
|
||||
<RemainPercentBar
|
||||
className="mt-0.5"
|
||||
value={daysLeftObject.remainingPercentage * 100}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{parsedData.billingDataMod.amount &&
|
||||
parsedData.billingDataMod.amount !== "0" &&
|
||||
parsedData.billingDataMod.amount !== "-1" ? (
|
||||
<p className={cn("text-[10px] text-muted-foreground ")}>
|
||||
{t("billingInfo.price")}: {parsedData.billingDataMod.amount}/
|
||||
{parsedData.billingDataMod.cycle}
|
||||
</p>
|
||||
) : parsedData.billingDataMod.amount === "0" ? (
|
||||
<p className={cn("text-[10px] text-green-600 ")}>
|
||||
{t("billingInfo.free")}
|
||||
</p>
|
||||
) : parsedData.billingDataMod.amount === "-1" ? (
|
||||
<p className={cn("text-[10px] text-pink-600 ")}>
|
||||
{t("billingInfo.usage-baseed")}
|
||||
</p>
|
||||
) : null}
|
||||
<p className={cn("text-[10px] text-muted-foreground text-red-600")}>
|
||||
{t("billingInfo.expired")}: {daysLeftObject.days * -1}{" "}
|
||||
{t("billingInfo.days")}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
const bars = Array(8).fill(0)
|
||||
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>
|
||||
)
|
||||
}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
export const LoadingSpinner = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={"size-4 animate-spin"}
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={"size-4 animate-spin"}
|
||||
>
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
|
||||
import { BackIcon } from "../Icon"
|
||||
import { BackIcon } from "../Icon";
|
||||
|
||||
export function ServerDetailChartLoading() {
|
||||
return (
|
||||
<div>
|
||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div>
|
||||
<section className="grid md:grid-cols-2 lg:grid-cols-3 grid-cols-1 gap-3">
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
<Skeleton className="h-[182px] w-full rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ServerDetailLoading() {
|
||||
const navigate = useNavigate()
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl px-0">
|
||||
<div
|
||||
onClick={() => {
|
||||
navigate("/")
|
||||
}}
|
||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
||||
>
|
||||
<BackIcon />
|
||||
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</div>
|
||||
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div className="mx-auto w-full max-w-5xl px-0">
|
||||
<div
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
}}
|
||||
className="flex flex-none cursor-pointer font-semibold leading-none items-center break-all tracking-tight gap-0.5 text-xl"
|
||||
>
|
||||
<BackIcon />
|
||||
<Skeleton className="h-[20px] w-24 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</div>
|
||||
<Skeleton className="flex flex-wrap gap-2 h-[81px] w-1/2 mt-3 rounded-[5px] bg-muted-foreground/10 animate-none"></Skeleton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { domMax as default } from "framer-motion"
|
||||
export { domMax as default } from "framer-motion";
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { LazyMotion } from "framer-motion"
|
||||
import { LazyMotion } from "framer-motion";
|
||||
|
||||
const loadFeatures = () => import("./framer-lazy-feature").then((res) => res.default)
|
||||
const loadFeatures = () =>
|
||||
import("./framer-lazy-feature").then((res) => res.default);
|
||||
|
||||
export const MotionProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<LazyMotion features={loadFeatures} strict key="framer">
|
||||
{children}
|
||||
</LazyMotion>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<LazyMotion features={loadFeatures} strict key="framer">
|
||||
{children}
|
||||
</LazyMotion>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,49 +1,55 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />)
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
AccordionItem.displayName = "AccordionItem";
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-start py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
));
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
));
|
||||
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
|
||||
@@ -1,89 +1,107 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
max: number
|
||||
value: number
|
||||
min: number
|
||||
className?: string
|
||||
primaryColor?: string
|
||||
max: number;
|
||||
value: number;
|
||||
min: number;
|
||||
className?: string;
|
||||
primaryColor?: string;
|
||||
}
|
||||
|
||||
export default function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, primaryColor, className }: Props) {
|
||||
const circumference = 2 * Math.PI * 45
|
||||
const percentPx = circumference / 100
|
||||
const currentPercent = ((value - min) / (max - min)) * 100
|
||||
export default function AnimatedCircularProgressBar({
|
||||
max = 100,
|
||||
min = 0,
|
||||
value = 0,
|
||||
primaryColor,
|
||||
className,
|
||||
}: Props) {
|
||||
const circumference = 2 * Math.PI * 45;
|
||||
const percentPx = circumference / 100;
|
||||
const currentPercent = ((value - min) / (max - min)) * 100;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative size-40 text-2xl font-semibold", className)}
|
||||
style={
|
||||
{
|
||||
"--circle-size": "100px",
|
||||
"--circumference": circumference,
|
||||
"--percent-to-px": `${percentPx}px`,
|
||||
"--gap-percent": "5",
|
||||
"--offset-factor": "0",
|
||||
"--transition-length": "1s",
|
||||
"--transition-step": "200ms",
|
||||
"--delay": "0s",
|
||||
"--percent-to-deg": "3.6deg",
|
||||
transform: "translateZ(0)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<svg fill="none" className="size-full" strokeWidth="2" viewBox="0 0 100 100">
|
||||
{currentPercent <= 90 && currentPercent >= 0 && (
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
strokeWidth="10"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="opacity-100 stroke-muted"
|
||||
style={
|
||||
{
|
||||
"--stroke-percent": 90 - currentPercent,
|
||||
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
|
||||
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||
transform: "rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
|
||||
transition: "all var(--transition-length) ease var(--delay)",
|
||||
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
strokeWidth="10"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={cn("opacity-100 stroke-current", {
|
||||
"stroke-(--stroke-primary-color)": primaryColor,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
"--stroke-primary-color": primaryColor,
|
||||
"--stroke-percent": currentPercent,
|
||||
strokeDasharray: "calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||
transition: "var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
|
||||
transitionProperty: "stroke-dasharray,transform",
|
||||
transform: "rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
|
||||
transformOrigin: "calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
data-current-value={currentPercent}
|
||||
className="duration-[var(--transition-length)] delay-[var(--delay)] absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
|
||||
>
|
||||
{currentPercent}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div
|
||||
className={cn("relative size-40 text-2xl font-semibold", className)}
|
||||
style={
|
||||
{
|
||||
"--circle-size": "100px",
|
||||
"--circumference": circumference,
|
||||
"--percent-to-px": `${percentPx}px`,
|
||||
"--gap-percent": "5",
|
||||
"--offset-factor": "0",
|
||||
"--transition-length": "1s",
|
||||
"--transition-step": "200ms",
|
||||
"--delay": "0s",
|
||||
"--percent-to-deg": "3.6deg",
|
||||
transform: "translateZ(0)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<svg
|
||||
fill="none"
|
||||
className="size-full"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 100 100"
|
||||
>
|
||||
{currentPercent <= 90 && currentPercent >= 0 && (
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
strokeWidth="10"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="opacity-100 stroke-muted"
|
||||
style={
|
||||
{
|
||||
"--stroke-percent": 90 - currentPercent,
|
||||
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
|
||||
strokeDasharray:
|
||||
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||
transform:
|
||||
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
|
||||
transition: "all var(--transition-length) ease var(--delay)",
|
||||
transformOrigin:
|
||||
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<circle
|
||||
cx="50"
|
||||
cy="50"
|
||||
r="45"
|
||||
strokeWidth="10"
|
||||
strokeDashoffset="0"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={cn("opacity-100 stroke-current", {
|
||||
"stroke-(--stroke-primary-color)": primaryColor,
|
||||
})}
|
||||
style={
|
||||
{
|
||||
"--stroke-primary-color": primaryColor,
|
||||
"--stroke-percent": currentPercent,
|
||||
strokeDasharray:
|
||||
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
|
||||
transition:
|
||||
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
|
||||
transitionProperty: "stroke-dasharray,transform",
|
||||
transform:
|
||||
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
|
||||
transformOrigin:
|
||||
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
data-current-value={currentPercent}
|
||||
className="duration-[var(--transition-length)] delay-[var(--delay)] absolute inset-0 m-auto size-fit ease-linear animate-in fade-in"
|
||||
>
|
||||
{currentPercent}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+28
-21
@@ -1,28 +1,35 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * 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-hidden 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",
|
||||
},
|
||||
},
|
||||
)
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden 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> {}
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@@ -1,42 +1,55 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
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 gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
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",
|
||||
},
|
||||
},
|
||||
)
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
{
|
||||
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
|
||||
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"
|
||||
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 }
|
||||
export { Button, buttonVariants };
|
||||
|
||||
+78
-31
@@ -1,38 +1,85 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
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 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 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 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 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 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"
|
||||
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, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
|
||||
+311
-226
@@ -1,277 +1,362 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
import * as RechartsPrimitive from "recharts"
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode
|
||||
icon?: React.ComponentType
|
||||
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
|
||||
}
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig
|
||||
}
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext)
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />")
|
||||
}
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config: ChartConfig
|
||||
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"]
|
||||
}
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId()
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
)
|
||||
})
|
||||
ChartContainer.displayName = "Chart"
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-hidden [&_.recharts-surface]:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
});
|
||||
ChartContainer.displayName = "Chart";
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color)
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme || config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null
|
||||
}
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
|
||||
return color ? ` --color-${key}: ${color};` : null
|
||||
})
|
||||
.join("\n")}
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean
|
||||
hideIndicator?: boolean
|
||||
indicator?: "line" | "dot" | "dashed"
|
||||
nameKey?: string
|
||||
labelKey?: string
|
||||
}
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart()
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const value = !labelKey && typeof label === "string" ? config[label as keyof typeof config]?.label || label : itemConfig?.label
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
|
||||
}
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
||||
}, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey])
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null
|
||||
}
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
const indicatorColor = color || item.payload.fill || item.color
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn("shrink-0 rounded-[2px] border-border bg-(--color-bg)", {
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
})}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className={cn("flex flex-1 justify-between leading-none", nestLabel ? "items-end" : "items-center")}>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
||||
</div>
|
||||
{item.value && <span className="font-mono font-medium tabular-nums text-foreground">{item.value.toLocaleString()}</span>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
)
|
||||
ChartTooltipContent.displayName = "ChartTooltip"
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-border bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartTooltipContent.displayName = "ChartTooltip";
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean
|
||||
nameKey?: string
|
||||
}
|
||||
>(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
|
||||
const { config } = useChart()
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null
|
||||
}
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={ref} className={cn("flex flex-wrap items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div key={item.value} className={cn("flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground")}>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
ChartLegendContent.displayName = "ChartLegend"
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartLegendContent.displayName = "ChartLegend";
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined
|
||||
}
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? payload.payload : undefined
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string
|
||||
} else if (payloadPayload && key in payloadPayload && typeof payloadPayload[key as keyof typeof payloadPayload] === "string") {
|
||||
configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string
|
||||
}
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config]
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle }
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||
import { Check } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Checkbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.Root>, React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
),
|
||||
)
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox }
|
||||
export { Checkbox };
|
||||
|
||||
+141
-89
@@ -1,107 +1,159 @@
|
||||
"use client"
|
||||
"use client";
|
||||
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog"
|
||||
import { Command as CommandPrimitive } from "cmdk"
|
||||
import { Search } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import { type DialogProps, DialogTitle } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Command = React.forwardRef<React.ElementRef<typeof CommandPrimitive>, React.ComponentPropsWithoutRef<typeof CommandPrimitive>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn("flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
Command.displayName = CommandPrimitive.displayName
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTitle />
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogTitle />
|
||||
<DialogContent className="overflow-hidden p-0 shadow-lg">
|
||||
<Command className="**:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 **:[[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Input>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div className="flex items-center bg-stone-100 dark:bg-stone-900 px-3" cmdk-input-wrapper="">
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
)
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className="flex items-center bg-stone-100 dark:bg-stone-900 px-3"
|
||||
cmdk-input-wrapper=""
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<React.ElementRef<typeof CommandPrimitive.List>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List ref={ref} className={cn("max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden", className)} {...props} />
|
||||
),
|
||||
)
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"max-h-[300px] mb-1 overflow-y-auto overflow-x-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Empty>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>>(
|
||||
(props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />,
|
||||
)
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Group>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => <CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />)
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<React.ElementRef<typeof CommandPrimitive.Item>, React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-hidden data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default gap-2 select-none items-center rounded-[8px] px-2 py-1.5 text-xs outline-hidden data-[disabled=true]:pointer-events-none data-[selected='true']:bg-stone-100 dark:data-[selected='true']:bg-stone-900 data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return <span className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)} {...props} />
|
||||
}
|
||||
CommandShortcut.displayName = "CommandShortcut"
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export { Command, CommandDialog, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem, CommandShortcut, CommandSeparator }
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
|
||||
+103
-60
@@ -1,76 +1,119 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
const DialogPortal = DialogPrimitive.Portal;
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
const DialogClose = DialogPrimitive.Close;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
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}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
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}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold leading-none tracking-tight", className)} {...props} />
|
||||
),
|
||||
)
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => <DialogPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />)
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export { Dialog, DialogPortal, DialogOverlay, DialogClose, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
|
||||
+165
-140
@@ -1,172 +1,197 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import { Check, ChevronRight, Circle } from "lucide-react"
|
||||
import * as React from "react"
|
||||
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 DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean
|
||||
}
|
||||
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-hidden 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
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden 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>
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-32 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
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-32 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>
|
||||
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-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl 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
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-2xl 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
|
||||
}
|
||||
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 justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default justify-between select-none items-center gap-2 rounded-[10px] px-2 py-1.5 text-sm outline-hidden transition-colors focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
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-hidden 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
|
||||
<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-hidden 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>
|
||||
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-hidden 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
|
||||
<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-hidden 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
|
||||
}
|
||||
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
|
||||
<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
|
||||
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"
|
||||
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,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
}
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
|
||||
+21
-18
@@ -1,21 +1,24 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
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 file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Input.displayName = "Input"
|
||||
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 file:text-foreground placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input }
|
||||
export { Input };
|
||||
|
||||
+19
-10
@@ -1,14 +1,23 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { type VariantProps, cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />)
|
||||
Label.displayName = LabelPrimitive.Root.displayName
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label }
|
||||
export { Label };
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-hidden 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}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-2xl outline-hidden 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}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent }
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
|
||||
@@ -1,20 +1,30 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
import * as React from "react"
|
||||
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
|
||||
}
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> & {
|
||||
indicatorClassName?: string;
|
||||
}
|
||||
>(({ 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 bg-primary transition-all", indicatorClassName)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
<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 bg-primary transition-all",
|
||||
indicatorClassName,
|
||||
)}
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
));
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName;
|
||||
|
||||
export { Progress }
|
||||
export { Progress };
|
||||
|
||||
+135
-104
@@ -1,126 +1,157 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
));
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton ref={ref} className={cn("flex cursor-default items-center justify-center py-1", className)} {...props}>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
));
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover 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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn("p-1", position === "popper" && "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)")}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-32 overflow-hidden rounded-md border bg-popover 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",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Label>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label ref={ref} className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} {...props} />
|
||||
),
|
||||
)
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<React.ElementRef<typeof SelectPrimitive.Item>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>>(
|
||||
({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden 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">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-hidden 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">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
),
|
||||
)
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => <SelectPrimitive.Separator ref={ref} className={cn("-mx-1 my-1 h-px bg-muted", className)} {...props} />)
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
};
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import * as React from "react";
|
||||
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-px w-full" : "h-full w-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
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-px w-full" : "h-full w-px",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator }
|
||||
export { Separator };
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return <div className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
|
||||
function Skeleton({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||
return (
|
||||
<div
|
||||
className={cn("animate-pulse rounded-md bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
export { Skeleton };
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Switch = React.forwardRef<React.ElementRef<typeof SwitchPrimitives.Root>, React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
),
|
||||
)
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-3 w-6 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-2 w-2 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch }
|
||||
export { Switch };
|
||||
|
||||
+107
-41
@@ -1,50 +1,116 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as React from "react"
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
||||
<tbody ref={ref} className={cn("[&_tr:last-child]:border-0", className)} {...props} />
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(({ className, ...props }, ref) => (
|
||||
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-medium last:[&>tr]:border-b-0", className)} {...props} />
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium last:[&>tr]:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(({ className, ...props }, ref) => (
|
||||
<tr ref={ref} className={cn("border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", className)} {...props} />
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn("h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(({ className, ...props }, ref) => (
|
||||
<td ref={ref} className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} {...props} />
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(({ className, ...props }, ref) => (
|
||||
<caption ref={ref} className={cn("mt-4 text-sm text-muted-foreground", className)} {...props} />
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
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-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg 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
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-[8px] border font-medium bg-popover px-1.5 py-0.5 text-xs text-popover-foreground shadow-lg 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, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
Reference in New Issue
Block a user