788 lines
32 KiB
TypeScript
Raw Normal View History

2025-04-12 12:33:37 +02:00
"use client";
2025-04-13 21:10:17 +02:00
import { AppSidebar } from "@/components/app-sidebar";
2025-04-12 12:33:37 +02:00
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
2025-04-12 12:33:37 +02:00
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
2025-04-14 15:35:32 +02:00
import {
Plus,
Link,
MonitorCog,
FileDigit,
Trash2,
LayoutGrid,
List,
Pencil,
Cpu,
Microchip,
MemoryStick,
HardDrive,
} from "lucide-react";
2025-04-12 12:33:37 +02:00
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/card";
2025-04-12 12:33:37 +02:00
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/pagination";
2025-04-12 12:33:37 +02:00
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
2025-04-13 21:10:17 +02:00
} from "@/components/ui/alert-dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
2025-04-12 12:33:37 +02:00
import {
2025-04-13 21:10:17 +02:00
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
2025-04-12 15:42:32 +02:00
import {
2025-04-13 21:10:17 +02:00
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
2025-04-12 15:42:32 +02:00
import Cookies from "js-cookie";
2025-04-12 12:33:37 +02:00
import { useState, useEffect } from "react";
2025-04-14 15:35:32 +02:00
import axios from "axios";
2025-04-13 21:10:17 +02:00
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
2025-04-12 12:33:37 +02:00
2025-04-13 21:10:17 +02:00
interface Server {
id: number;
name: string;
os?: string;
ip?: string;
url?: string;
cpu?: string;
gpu?: string;
ram?: string;
disk?: string;
}
interface GetServersResponse {
servers: Server[];
maxPage: number;
}
2025-04-12 21:00:18 +02:00
2025-04-13 21:10:17 +02:00
export default function Dashboard() {
const [name, setName] = useState<string>("");
const [os, setOs] = useState<string>("");
const [ip, setIp] = useState<string>("");
const [url, setUrl] = useState<string>("");
const [cpu, setCpu] = useState<string>("");
const [gpu, setGpu] = useState<string>("");
const [ram, setRam] = useState<string>("");
const [disk, setDisk] = useState<string>("");
2025-04-12 15:42:32 +02:00
2025-04-13 21:10:17 +02:00
const [currentPage, setCurrentPage] = useState<number>(1);
const [maxPage, setMaxPage] = useState<number>(1);
2025-04-14 21:26:12 +02:00
const [itemsPerPage, setItemsPerPage] = useState<number>(4);
2025-04-13 21:10:17 +02:00
const [servers, setServers] = useState<Server[]>([]);
const [isGridLayout, setIsGridLayout] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
2025-04-12 20:18:40 +02:00
const [editId, setEditId] = useState<number | null>(null);
2025-04-13 21:10:17 +02:00
const [editName, setEditName] = useState<string>("");
const [editOs, setEditOs] = useState<string>("");
const [editIp, setEditIp] = useState<string>("");
const [editUrl, setEditUrl] = useState<string>("");
const [editCpu, setEditCpu] = useState<string>("");
const [editGpu, setEditGpu] = useState<string>("");
const [editRam, setEditRam] = useState<string>("");
const [editDisk, setEditDisk] = useState<string>("");
2025-04-12 20:18:40 +02:00
2025-04-14 14:50:48 +02:00
const [searchTerm, setSearchTerm] = useState<string>("");
const [isSearching, setIsSearching] = useState<boolean>(false);
2025-04-12 15:42:32 +02:00
useEffect(() => {
2025-04-14 15:35:32 +02:00
const savedLayout = Cookies.get("layoutPreference-servers");
2025-04-14 21:26:12 +02:00
const layout_bool = savedLayout === "grid";
setIsGridLayout(layout_bool);
setItemsPerPage(layout_bool ? 6 : 4);
2025-04-12 15:42:32 +02:00
}, []);
const toggleLayout = () => {
const newLayout = !isGridLayout;
setIsGridLayout(newLayout);
2025-04-14 15:35:32 +02:00
Cookies.set("layoutPreference-servers", newLayout ? "grid" : "standard", {
2025-04-12 15:42:32 +02:00
expires: 365,
2025-04-14 15:35:32 +02:00
path: "/",
sameSite: "strict",
2025-04-12 15:42:32 +02:00
});
2025-04-14 21:26:12 +02:00
setItemsPerPage(newLayout ? 6 : 4);
2025-04-12 15:42:32 +02:00
};
2025-04-12 12:33:37 +02:00
const add = async () => {
try {
2025-04-14 15:35:32 +02:00
await axios.post("/api/servers/add", {
name,
os,
ip,
url,
cpu,
gpu,
ram,
disk,
2025-04-13 21:10:17 +02:00
});
2025-04-12 12:33:37 +02:00
getServers();
} catch (error: any) {
console.log(error.response.data);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 12:33:37 +02:00
const getServers = async () => {
try {
2025-04-12 17:40:37 +02:00
setLoading(true);
2025-04-14 15:35:32 +02:00
const response = await axios.post<GetServersResponse>(
"/api/servers/get",
{
page: currentPage,
2025-04-14 21:26:12 +02:00
ITEMS_PER_PAGE: itemsPerPage,
2025-04-14 15:35:32 +02:00
}
);
2025-04-12 12:33:37 +02:00
setServers(response.data.servers);
setMaxPage(response.data.maxPage);
2025-04-12 17:40:37 +02:00
setLoading(false);
2025-04-12 12:33:37 +02:00
} catch (error: any) {
console.log(error.response);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 12:33:37 +02:00
useEffect(() => {
getServers();
2025-04-14 21:26:12 +02:00
}, [currentPage, itemsPerPage]);
2025-04-12 12:33:37 +02:00
const handlePrevious = () => {
2025-04-14 15:35:32 +02:00
setCurrentPage((prev) => Math.max(1, prev - 1));
};
2025-04-12 12:33:37 +02:00
const handleNext = () => {
2025-04-14 15:35:32 +02:00
setCurrentPage((prev) => Math.min(maxPage, prev + 1));
};
2025-04-12 12:33:37 +02:00
const deleteApplication = async (id: number) => {
try {
2025-04-14 15:35:32 +02:00
await axios.post("/api/servers/delete", { id });
2025-04-12 12:33:37 +02:00
getServers();
} catch (error: any) {
console.log(error.response.data);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 12:33:37 +02:00
2025-04-13 21:10:17 +02:00
const openEditDialog = (server: Server) => {
2025-04-12 20:18:40 +02:00
setEditId(server.id);
setEditName(server.name);
2025-04-13 21:10:17 +02:00
setEditOs(server.os || "");
setEditIp(server.ip || "");
setEditUrl(server.url || "");
setEditCpu(server.cpu || "");
setEditGpu(server.gpu || "");
setEditRam(server.ram || "");
setEditDisk(server.disk || "");
2025-04-12 21:00:18 +02:00
};
2025-04-12 20:18:40 +02:00
const edit = async () => {
2025-04-13 21:10:17 +02:00
if (!editId) return;
2025-04-14 15:35:32 +02:00
2025-04-12 20:18:40 +02:00
try {
2025-04-14 15:35:32 +02:00
await axios.put("/api/servers/edit", {
2025-04-12 20:18:40 +02:00
id: editId,
name: editName,
os: editOs,
ip: editIp,
2025-04-12 21:00:18 +02:00
url: editUrl,
cpu: editCpu,
gpu: editGpu,
ram: editRam,
2025-04-14 15:35:32 +02:00
disk: editDisk,
2025-04-12 20:18:40 +02:00
});
getServers();
setEditId(null);
} catch (error: any) {
console.log(error.response.data);
}
2025-04-14 15:35:32 +02:00
};
2025-04-12 20:18:40 +02:00
2025-04-14 14:50:48 +02:00
const searchServers = async () => {
try {
setIsSearching(true);
const response = await axios.post<{ results: Server[] }>(
"/api/servers/search",
{ searchterm: searchTerm }
);
setServers(response.data.results);
setIsSearching(false);
} catch (error: any) {
console.error("Search error:", error.response?.data);
setIsSearching(false);
}
};
useEffect(() => {
const delayDebounce = setTimeout(() => {
if (searchTerm.trim() === "") {
getServers();
} else {
searchServers();
}
}, 300);
return () => clearTimeout(delayDebounce);
}, [searchTerm]);
2025-04-12 12:33:37 +02:00
return (
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
<div className="flex items-center gap-2 px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbPage>/</BreadcrumbPage>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
2025-04-12 13:21:03 +02:00
<BreadcrumbPage>My Infrastructure</BreadcrumbPage>
2025-04-12 12:33:37 +02:00
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Servers</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</div>
</header>
<div className="pl-4 pr-4">
2025-04-12 15:42:32 +02:00
<div className="flex justify-between items-center">
2025-04-12 12:33:37 +02:00
<span className="text-2xl font-semibold">Your Servers</span>
2025-04-12 15:42:32 +02:00
<div className="flex gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
2025-04-14 15:35:32 +02:00
<Button
variant="outline"
2025-04-12 15:42:32 +02:00
size="icon"
onClick={toggleLayout}
>
{isGridLayout ? (
<List className="h-4 w-4" />
) : (
<LayoutGrid className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
2025-04-14 15:35:32 +02:00
{isGridLayout
? "Switch to list view"
: "Switch to grid view"}
2025-04-12 15:42:32 +02:00
</TooltipContent>
</Tooltip>
</TooltipProvider>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" size="icon">
<Plus />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Add an server</AlertDialogTitle>
<AlertDialogDescription>
2025-04-14 15:35:32 +02:00
<Tabs defaultValue="general" className="w-full">
<TabsList className="w-full">
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="hardware">Hardware</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">Name</Label>
<Input
id="name"
type="text"
placeholder="e.g. Server1"
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="description">
Operating System{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Select onValueChange={(value) => setOs(value)}>
2025-04-12 21:00:18 +02:00
<SelectTrigger className="w-full">
2025-04-14 15:35:32 +02:00
<SelectValue placeholder="Select OS" />
2025-04-12 21:00:18 +02:00
</SelectTrigger>
<SelectContent>
2025-04-14 15:35:32 +02:00
<SelectItem value="Windows">
Windows
</SelectItem>
<SelectItem value="Linux">Linux</SelectItem>
<SelectItem value="MacOS">MacOS</SelectItem>
2025-04-12 21:00:18 +02:00
</SelectContent>
2025-04-14 15:35:32 +02:00
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="icon">
IP Adress{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="icon"
type="text"
placeholder="e.g. 192.168.100.2"
onChange={(e) => setIp(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<TooltipProvider>
2025-04-12 21:00:18 +02:00
<Tooltip>
2025-04-14 15:35:32 +02:00
<TooltipTrigger>
<Label htmlFor="publicURL">
Management URL{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
</TooltipTrigger>
<TooltipContent>
Link to a web interface (e.g. Proxmox or
Portainer) with which the server can be
managed
</TooltipContent>
2025-04-12 21:00:18 +02:00
</Tooltip>
2025-04-14 15:35:32 +02:00
</TooltipProvider>
<Input
id="publicURL"
type="text"
placeholder="e.g. https://proxmox.server1.com"
onChange={(e) => setUrl(e.target.value)}
/>
</div>
2025-04-12 21:00:18 +02:00
</div>
2025-04-14 15:35:32 +02:00
</TabsContent>
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
CPU{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. AMD Ryzen™ 7 7800X3D"
onChange={(e) => setCpu(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
GPU{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. AMD Radeon™ Graphics"
onChange={(e) => setGpu(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
RAM{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. 64GB DDR5"
onChange={(e) => setRam(e.target.value)}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">
Disk{" "}
<span className="text-stone-600">
(optional)
</span>
</Label>
<Input
id="name"
type="text"
placeholder="e.g. 2TB SSD"
onChange={(e) => setDisk(e.target.value)}
/>
</div>
2025-04-12 21:00:18 +02:00
</div>
2025-04-14 15:35:32 +02:00
</TabsContent>
</Tabs>
2025-04-12 15:42:32 +02:00
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={add}>Add</AlertDialogAction>
2025-04-12 15:42:32 +02:00
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
2025-04-12 12:33:37 +02:00
</div>
2025-04-14 14:50:48 +02:00
<div className="flex flex-col gap-2 mb-4 pt-2">
<Input
id="application-search"
placeholder="Type to search..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
2025-04-12 12:33:37 +02:00
<br />
2025-04-14 15:35:32 +02:00
{!loading ? (
<div
className={
isGridLayout
? "grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 gap-4"
: "space-y-4"
}
>
2025-04-12 17:40:37 +02:00
{servers.map((server) => (
2025-04-14 15:35:32 +02:00
<Card
key={server.id}
className={
isGridLayout
? "h-full flex flex-col justify-between"
: "w-full mb-4"
}
2025-04-12 17:40:37 +02:00
>
<CardHeader>
<div className="flex items-center justify-between w-full">
<div className="flex items-center">
2025-04-12 21:00:18 +02:00
<div className="ml-4">
2025-04-14 15:35:32 +02:00
<CardTitle className="text-2xl font-bold">
{server.name}
</CardTitle>
<CardDescription
className={`text-sm mt-1 grid gap-y-1 ${
isGridLayout
? "grid-cols-1"
: "grid-cols-2 gap-x-4"
}`}
>
2025-04-12 21:00:18 +02:00
<div className="flex items-center gap-2 text-foreground/80">
<MonitorCog className="h-4 w-4 text-muted-foreground" />
2025-04-14 15:35:32 +02:00
<span>
<b>OS:</b> {server.os || "-"}
</span>
2025-04-12 21:00:18 +02:00
</div>
<div className="flex items-center gap-2 text-foreground/80">
<FileDigit className="h-4 w-4 text-muted-foreground" />
2025-04-14 15:35:32 +02:00
<span>
2025-04-15 15:05:01 +02:00
<b>IP:</b> {server.ip || "Not set"}
2025-04-14 15:35:32 +02:00
</span>
2025-04-12 21:00:18 +02:00
</div>
<div className="col-span-full pt-2 pb-2">
<Separator />
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Cpu className="h-4 w-4 text-muted-foreground" />
2025-04-14 15:35:32 +02:00
<span>
<b>CPU:</b> {server.cpu || "-"}
</span>
2025-04-12 21:00:18 +02:00
</div>
<div className="flex items-center gap-2 text-foreground/80">
<Microchip className="h-4 w-4 text-muted-foreground" />
2025-04-14 15:35:32 +02:00
<span>
<b>GPU:</b> {server.gpu || "-"}
</span>
2025-04-12 21:00:18 +02:00
</div>
2025-04-12 17:40:37 +02:00
<div className="flex items-center gap-2 text-foreground/80">
2025-04-12 21:00:18 +02:00
<MemoryStick className="h-4 w-4 text-muted-foreground" />
2025-04-14 15:35:32 +02:00
<span>
<b>RAM:</b> {server.ram || "-"}
</span>
2025-04-12 17:40:37 +02:00
</div>
<div className="flex items-center gap-2 text-foreground/80">
2025-04-12 21:00:18 +02:00
<HardDrive className="h-4 w-4 text-muted-foreground" />
2025-04-14 15:35:32 +02:00
<span>
<b>Disk:</b> {server.disk || "-"}
</span>
2025-04-12 17:40:37 +02:00
</div>
2025-04-12 21:00:18 +02:00
</CardDescription>
2025-04-12 17:40:37 +02:00
</div>
2025-04-12 15:42:32 +02:00
</div>
2025-04-12 20:18:40 +02:00
<div className="flex flex-col items-end justify-start space-y-2 w-[405px]">
2025-04-12 17:40:37 +02:00
<div className="flex items-center gap-2 w-full">
2025-04-14 15:35:32 +02:00
<div className="flex flex-col space-y-2 flex-grow">
2025-04-12 17:40:37 +02:00
{server.url && (
2025-04-14 15:35:32 +02:00
<Button
variant="outline"
2025-04-12 17:40:37 +02:00
className="gap-2 w-full"
2025-04-14 15:35:32 +02:00
onClick={() =>
window.open(server.url, "_blank")
}
>
2025-04-12 17:40:37 +02:00
<Link className="h-4 w-4" />
Open Management URL
2025-04-12 20:18:40 +02:00
</Button>
2025-04-14 15:35:32 +02:00
)}
</div>
<div className="flex flex-col gap-2">
<Button
variant="destructive"
size="icon"
className="h-9 w-9"
onClick={() => deleteApplication(server.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
size="icon"
className="h-9 w-9"
onClick={() => openEditDialog(server)}
>
<Pencil className="h-4 w-4" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
Edit Server
</AlertDialogTitle>
<AlertDialogDescription>
<Tabs
defaultValue="general"
className="w-full"
>
<TabsList className="w-full">
<TabsTrigger value="general">
General
</TabsTrigger>
<TabsTrigger value="hardware">
Hardware
</TabsTrigger>
</TabsList>
<TabsContent value="general">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editOs">
Operating System
</Label>
<Select
value={editOs}
onValueChange={setEditOs}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select OS" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Windows">
Windows
</SelectItem>
<SelectItem value="Linux">
Linux
</SelectItem>
<SelectItem value="MacOS">
MacOS
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editIp">
IP Adress
</Label>
<Input
id="editIp"
type="text"
placeholder="e.g. 192.168.100.2"
value={editIp}
onChange={(e) =>
setEditIp(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editUrl">
Management URL
</Label>
<Input
id="editUrl"
type="text"
placeholder="e.g. https://proxmox.server1.com"
value={editUrl}
onChange={(e) =>
setEditUrl(e.target.value)
}
/>
</div>
</div>
</TabsContent>
2025-04-12 21:00:18 +02:00
2025-04-14 15:35:32 +02:00
<TabsContent value="hardware">
<div className="space-y-4 pt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editCpu">CPU</Label>
<Input
id="editCpu"
value={editCpu}
onChange={(e) =>
setEditCpu(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editGpu">GPU</Label>
<Input
id="editGpu"
value={editGpu}
onChange={(e) =>
setEditGpu(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editRam">RAM</Label>
<Input
id="editRam"
value={editRam}
onChange={(e) =>
setEditRam(e.target.value)
}
/>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="editDisk">
Disk
</Label>
<Input
id="editDisk"
value={editDisk}
onChange={(e) =>
setEditDisk(e.target.value)
}
/>
</div>
</div>
</TabsContent>
</Tabs>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Button onClick={edit}>Save</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
2025-04-12 17:40:37 +02:00
</div>
2025-04-12 15:42:32 +02:00
</div>
2025-04-12 12:33:37 +02:00
</div>
2025-04-12 17:40:37 +02:00
</CardHeader>
</Card>
))}
</div>
2025-04-14 15:35:32 +02:00
) : (
2025-04-12 17:40:37 +02:00
<div className="flex items-center justify-center">
2025-04-14 15:35:32 +02:00
<div className="inline-block" role="status" aria-label="loading">
<svg
className="w-6 h-6 stroke-white animate-spin "
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0_9023_61563)">
<path
d="M14.6437 2.05426C11.9803 1.2966 9.01686 1.64245 6.50315 3.25548C1.85499 6.23817 0.504864 12.4242 3.48756 17.0724C6.47025 21.7205 12.6563 23.0706 17.3044 20.088C20.4971 18.0393 22.1338 14.4793 21.8792 10.9444"
stroke="stroke-current"
stroke-width="1.4"
stroke-linecap="round"
className="my-path"
></path>
2025-04-12 17:40:37 +02:00
</g>
<defs>
2025-04-14 15:35:32 +02:00
<clipPath id="clip0_9023_61563">
<rect width="24" height="24" fill="white"></rect>
</clipPath>
2025-04-12 17:40:37 +02:00
</defs>
2025-04-14 15:35:32 +02:00
</svg>
<span className="sr-only">Loading...</span>
2025-04-12 17:40:37 +02:00
</div>
2025-04-14 15:35:32 +02:00
</div>
)}
2025-04-14 21:28:09 +02:00
<div className="pt-4 pb-4">
2025-04-12 15:42:32 +02:00
<Pagination>
<PaginationContent>
<PaginationItem>
2025-04-14 15:35:32 +02:00
<PaginationPrevious
2025-04-12 15:42:32 +02:00
onClick={handlePrevious}
isActive={currentPage > 1}
2025-04-14 21:36:15 +02:00
style={{ cursor: currentPage === 1 ? 'not-allowed' : 'pointer' }}
2025-04-12 15:42:32 +02:00
/>
</PaginationItem>
2025-04-14 15:35:32 +02:00
2025-04-12 15:42:32 +02:00
<PaginationItem>
<PaginationLink isActive>{currentPage}</PaginationLink>
</PaginationItem>
2025-04-12 12:33:37 +02:00
2025-04-12 15:42:32 +02:00
<PaginationItem>
2025-04-14 15:35:32 +02:00
<PaginationNext
2025-04-12 15:42:32 +02:00
onClick={handleNext}
isActive={currentPage < maxPage}
2025-04-14 21:36:15 +02:00
style={{ cursor: currentPage === maxPage ? 'not-allowed' : 'pointer' }}
2025-04-12 15:42:32 +02:00
/>
</PaginationItem>
</PaginationContent>
</Pagination>
</div>
2025-04-12 12:33:37 +02:00
</div>
</SidebarInset>
</SidebarProvider>
2025-04-14 15:35:32 +02:00
);
}