diff --git a/AppImage/README.md b/AppImage/README.md
deleted file mode 100644
index b9a3f52..0000000
--- a/AppImage/README.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# ProxMenux Monitor
-
-A modern, responsive dashboard for monitoring Proxmox VE systems built with Next.js and React.
-
-## Features
-
-- **System Overview**: Real-time monitoring of CPU, memory, temperature, and active VMs/LXC containers
-- **Storage Management**: Visual representation of storage distribution and disk performance metrics
-- **Network Monitoring**: Network interface statistics and performance graphs
-- **Virtual Machines**: Comprehensive view of VMs and LXC containers with resource usage
-- **System Logs**: Real-time system log monitoring and filtering
-- **Dark/Light Theme**: Toggle between themes with Proxmox-inspired design
-- **Responsive Design**: Works seamlessly on desktop and mobile devices
-
-## Technology Stack
-
-- **Frontend**: Next.js 15, React 19, TypeScript
-- **Styling**: Tailwind CSS with custom Proxmox-inspired theme
-- **Charts**: Recharts for data visualization
-- **UI Components**: Radix UI primitives with shadcn/ui
-- **Backend**: Flask server for system data collection
-- **Packaging**: AppImage for easy distribution
-
-## Development
-
-\`\`\`bash
-# Install dependencies
-npm install
-
-# Start development server
-npm run dev
-
-# Build for production
-npm run build
-\`\`\`
-
-## Building AppImage
-
-\`\`\`bash
-# Make build script executable
-chmod +x scripts/build_appimage.sh
-
-# Build AppImage
-./scripts/build_appimage.sh
-\`\`\`
-
-## Translation Support
-
-The project includes a translation system for multi-language support:
-
-\`\`\`bash
-# Build translation AppImage
-chmod +x scripts/build_translate_appimage.sh
-./scripts/build_translate_appimage.sh
-\`\`\`
-
-## License
-
-MIT License - see LICENSE file for details.
diff --git a/AppImage/app/api/flask/route.ts b/AppImage/app/api/flask/route.ts
deleted file mode 100644
index a5264ec..0000000
--- a/AppImage/app/api/flask/route.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { type NextRequest, NextResponse } from "next/server"
-
-// This will be the bridge between Next.js and the Flask server
-// For now, we'll return mock data that simulates what the Flask server would provide
-
-export async function GET(request: NextRequest) {
- const { searchParams } = new URL(request.url)
- const endpoint = searchParams.get("endpoint")
-
- // Mock data that would come from the Flask server running on port 8008
- const mockData = {
- system: {
- cpu_usage: 67.3,
- memory_usage: 49.4,
- temperature: 52,
- uptime: "15d 7h 23m",
- load_average: [1.23, 1.45, 1.67],
- },
- storage: {
- total: 2000,
- used: 1250,
- available: 750,
- disks: [
- { name: "/dev/sda", type: "HDD", size: 1000, used: 650, health: "healthy", temp: 42 },
- { name: "/dev/sdb", type: "HDD", size: 1000, used: 480, health: "healthy", temp: 38 },
- { name: "/dev/sdc", type: "SSD", size: 500, used: 120, health: "healthy", temp: 35 },
- { name: "/dev/nvme0n1", type: "NVMe", size: 1000, used: 340, health: "warning", temp: 55 },
- ],
- },
- network: {
- interfaces: [
- { name: "vmbr0", type: "Bridge", status: "up", ip: "192.168.1.100/24", speed: "1000 Mbps" },
- { name: "enp1s0", type: "Physical", status: "up", ip: "192.168.1.101/24", speed: "1000 Mbps" },
- ],
- traffic: {
- incoming: 89,
- outgoing: 67,
- },
- },
- vms: [
- {
- id: 100,
- name: "web-server-01",
- status: "running",
- os: "Ubuntu 22.04",
- cpu: 4,
- memory: 8192,
- disk: 50,
- uptime: "15d 7h 23m",
- cpu_usage: 45,
- memory_usage: 62,
- disk_usage: 78,
- },
- ],
- }
-
- try {
- // In the real implementation, this would make a request to the Flask server
- // const response = await fetch(`http://localhost:8008/api/${endpoint}`)
- // const data = await response.json()
-
- // For now, return mock data based on the endpoint
- switch (endpoint) {
- case "system":
- return NextResponse.json(mockData.system)
- case "storage":
- return NextResponse.json(mockData.storage)
- case "network":
- return NextResponse.json(mockData.network)
- case "vms":
- return NextResponse.json(mockData.vms)
- default:
- return NextResponse.json(mockData)
- }
- } catch (error) {
- return NextResponse.json({ error: "Failed to fetch data from Flask server" }, { status: 500 })
- }
-}
diff --git a/AppImage/app/globals.css b/AppImage/app/globals.css
index 2cfb3a7..f08e712 100644
--- a/AppImage/app/globals.css
+++ b/AppImage/app/globals.css
@@ -4,7 +4,6 @@
@custom-variant dark (&:is(.dark *));
:root {
- /* Proxmox light theme colors */
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
@@ -32,10 +31,11 @@
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
+ --header-bg: oklch(1 0 0);
+ --header-foreground: oklch(0.145 0 0);
}
.dark {
- /* Proxmox dark theme with proper gray background (#2b2f36) */
--background: oklch(0.205 0.005 240); /* Proxmox dark gray #2b2f36 */
--foreground: oklch(0.985 0 0);
--card: oklch(0.235 0.005 240); /* Slightly lighter gray for cards #363c45 */
@@ -55,12 +55,15 @@
--border: oklch(0.335 0.005 240); /* More visible borders */
--input: oklch(0.285 0.005 240);
--ring: oklch(0.439 0 0);
- /* Updated chart colors to be more vibrant and visible in dark mode */
--chart-1: oklch(0.65 0.2 220); /* Bright Blue */
--chart-2: oklch(0.65 0.2 140); /* Bright Green */
--chart-3: oklch(0.7 0.2 50); /* Bright Yellow */
--chart-4: oklch(0.65 0.2 300); /* Bright Purple */
--chart-5: oklch(0.65 0.2 20); /* Bright Orange */
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
@@ -69,12 +72,13 @@
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.285 0.005 240);
--sidebar-ring: oklch(0.439 0 0);
- /* Header is black only in dark mode */
--header-bg: oklch(0 0 0);
--header-foreground: oklch(1 0 0);
}
@theme inline {
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
@@ -111,9 +115,6 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
- /* Custom header colors */
- --color-header-bg: var(--header-bg);
- --color-header-foreground: var(--header-foreground);
}
@layer base {
@@ -123,80 +124,78 @@
body {
@apply bg-background text-foreground;
}
-}
+ .header-bg {
+ background-color: var(--header-bg);
+ color: var(--header-foreground);
+ backdrop-filter: none; /* Remove any blur effects */
+ }
+ /* Custom scrollbar with better contrast */
+ ::-webkit-scrollbar {
+ width: 6px;
+ }
-/* Header styling that adapts to theme */
-.header-bg {
- background-color: var(--header-bg);
- color: var(--header-foreground);
-}
+ ::-webkit-scrollbar-track {
+ background: var(--background);
+ }
-/* Custom scrollbar with better contrast */
-::-webkit-scrollbar {
- width: 6px;
-}
+ ::-webkit-scrollbar-thumb {
+ background: var(--muted);
+ border-radius: 3px;
+ }
-::-webkit-scrollbar-track {
- background: var(--background);
-}
+ ::-webkit-scrollbar-thumb:hover {
+ background: var(--muted-foreground);
+ }
-::-webkit-scrollbar-thumb {
- background: var(--muted);
- border-radius: 3px;
-}
+ /* Better contrast for dark mode content */
+ .dark .metric-card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ }
-::-webkit-scrollbar-thumb:hover {
- background: var(--muted-foreground);
-}
+ .dark .metric-value {
+ color: var(--foreground);
+ font-weight: 600;
+ }
-/* Better contrast for dark mode content */
-.dark .metric-card {
- background: var(--card);
- border: 1px solid var(--border);
-}
+ .dark .metric-label {
+ color: var(--muted-foreground);
+ }
-.dark .metric-value {
- color: var(--foreground);
- font-weight: 600;
-}
+ /* Fix chart axis visibility in dark mode */
+ .dark .recharts-cartesian-axis-tick-value {
+ fill: var(--muted-foreground) !important;
+ }
-.dark .metric-label {
- color: var(--muted-foreground);
-}
+ .dark .recharts-text {
+ fill: var(--muted-foreground) !important;
+ }
-/* Fix chart axis visibility in dark mode */
-.dark .recharts-cartesian-axis-tick-value {
- fill: var(--muted-foreground) !important;
-}
+ /* Improve server info layout in header - clean design without transparency */
+ .server-info {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.25rem 0.75rem;
+ border-radius: 0.375rem;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ }
-.dark .recharts-text {
- fill: var(--muted-foreground) !important;
-}
+ .dark .server-info {
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ }
-/* Improve server info layout in header - clean design without transparency */
-.server-info {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.25rem 0.75rem;
- border-radius: 0.375rem;
- border: 1px solid rgba(255, 255, 255, 0.2);
-}
+ /* Better spacing for VM/LXC badges */
+ .vm-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ align-items: center;
+ }
-.dark .server-info {
- border: 1px solid rgba(255, 255, 255, 0.1);
-}
-
-/* Better spacing for VM/LXC badges */
-.vm-badges {
- display: flex;
- flex-wrap: wrap;
- gap: 0.25rem;
- align-items: center;
-}
-
-.vm-badge {
- font-size: 0.75rem;
- padding: 0.125rem 0.5rem;
- white-space: nowrap;
+ .vm-badge {
+ font-size: 0.75rem;
+ padding: 0.125rem 0.5rem;
+ white-space: nowrap;
+ }
}
diff --git a/AppImage/app/layout.tsx b/AppImage/app/layout.tsx
index 27a5ad7..f4009cc 100644
--- a/AppImage/app/layout.tsx
+++ b/AppImage/app/layout.tsx
@@ -9,21 +9,8 @@ import "./globals.css"
export const metadata: Metadata = {
title: "ProxMenux Monitor",
- description: "Proxmox System Dashboard and Monitor",
+ description: "Proxmox System Dashboard",
generator: "v0.app",
- manifest: "/manifest.json",
- icons: {
- icon: [
- { url: "/favicon-16x16.png", sizes: "16x16", type: "image/png" },
- { url: "/favicon-32x32.png", sizes: "32x32", type: "image/png" },
- ],
- apple: [{ url: "/apple-touch-icon.png", sizes: "180x180", type: "image/png" }],
- },
- viewport: "width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no",
- themeColor: [
- { media: "(prefers-color-scheme: light)", color: "#ffffff" },
- { media: "(prefers-color-scheme: dark)", color: "#1a1a1a" },
- ],
}
export default function RootLayout({
@@ -33,13 +20,13 @@ export default function RootLayout({
}>) {
return (
-
- Loading...}>
+
+
{children}
-
+
)
diff --git a/AppImage/app/page.tsx b/AppImage/app/page.tsx
index 47629db..c6d59e1 100644
--- a/AppImage/app/page.tsx
+++ b/AppImage/app/page.tsx
@@ -1,6 +1,6 @@
-import { ProxmoxDashboard } from "../components/proxmox-dashboard"
+import { ProxmoxDashboard } from "../AppImage/components/proxmox-dashboard"
-export default function Home() {
+export default function Page() {
return (
diff --git a/AppImage/components.json b/AppImage/components.json
new file mode 100644
index 0000000..4ee62ee
--- /dev/null
+++ b/AppImage/components.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "iconLibrary": "lucide"
+}
diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx
deleted file mode 100644
index 8d3a714..0000000
--- a/AppImage/components/network-metrics.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-"use client"
-
-import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
-import { Badge } from "./ui/badge"
-import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
-import { Wifi, Globe, Shield, Activity, Network, Router } from "lucide-react"
-
-const networkTraffic = [
- { time: "00:00", incoming: 45, outgoing: 32 },
- { time: "04:00", incoming: 52, outgoing: 28 },
- { time: "08:00", incoming: 78, outgoing: 65 },
- { time: "12:00", incoming: 65, outgoing: 45 },
- { time: "16:00", incoming: 82, outgoing: 58 },
- { time: "20:00", incoming: 58, outgoing: 42 },
- { time: "24:00", incoming: 43, outgoing: 35 },
-]
-
-const connectionData = [
- { time: "00:00", connections: 1250 },
- { time: "04:00", connections: 980 },
- { time: "08:00", connections: 1850 },
- { time: "12:00", connections: 1650 },
- { time: "16:00", connections: 2100 },
- { time: "20:00", connections: 1580 },
- { time: "24:00", connections: 1320 },
-]
-
-export function NetworkMetrics() {
- return (
-
- {/* Network Overview Cards */}
-
-
-
- Network Traffic
-
-
-
- 156 MB/s
-
- ↓ 89 MB/s
- ↑ 67 MB/s
-
- Peak: 245 MB/s at 16:30
-
-
-
-
-
- Active Connections
-
-
-
- 1,847
-
-
- Normal
-
-
-
- ↑ 12% from last hour
-
-
-
-
-
-
- Firewall Status
-
-
-
- Active
-
-
- Protected
-
-
- 247 blocked attempts today
-
-
-
-
-
-
-
- Latency
-
-
-
- 12ms
-
-
- Excellent
-
-
- Avg response time
-
-
-
-
- {/* Network Charts */}
-
-
-
-
-
- Network Traffic (24h)
-
-
-
-
-
-
-
-
- [`${value} MB/s`, name === "incoming" ? "Incoming" : "Outgoing"]}
- />
-
-
-
-
-
-
-
-
-
-
-
- Active Connections (24h)
-
-
-
-
-
-
-
-
- [`${value}`, "Connections"]}
- />
-
-
-
-
-
-
-
- {/* Network Interfaces */}
-
-
-
-
- Network Interfaces
-
-
-
-
- {[
- {
- name: "vmbr0",
- type: "Bridge",
- status: "up",
- ip: "192.168.1.100/24",
- speed: "1000 Mbps",
- rx: "2.3 GB",
- tx: "1.8 GB",
- },
- {
- name: "enp1s0",
- type: "Physical",
- status: "up",
- ip: "192.168.1.101/24",
- speed: "1000 Mbps",
- rx: "1.2 GB",
- tx: "890 MB",
- },
- {
- name: "vmbr1",
- type: "Bridge",
- status: "up",
- ip: "10.0.0.1/24",
- speed: "1000 Mbps",
- rx: "456 MB",
- tx: "234 MB",
- },
- {
- name: "tap101i0",
- type: "TAP",
- status: "up",
- ip: "10.0.0.101/24",
- speed: "1000 Mbps",
- rx: "123 MB",
- tx: "89 MB",
- },
- ].map((interface_, index) => (
-
-
-
-
-
{interface_.name}
-
- {interface_.type} • {interface_.speed}
-
-
-
-
-
-
-
IP Address
-
{interface_.ip}
-
-
-
-
RX / TX
-
- {interface_.rx} / {interface_.tx}
-
-
-
-
- {interface_.status.toUpperCase()}
-
-
-
- ))}
-
-
-
-
- )
-}
diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx
deleted file mode 100644
index 626a28a..0000000
--- a/AppImage/components/proxmox-dashboard.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-"use client"
-
-import { useState, useEffect } from "react"
-import { Badge } from "./ui/badge"
-import { Button } from "./ui/button"
-import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"
-import { SystemOverview } from "./system-overview"
-import { StorageMetrics } from "./storage-metrics"
-import { NetworkMetrics } from "./network-metrics"
-import { VirtualMachines } from "./virtual-machines"
-import { SystemLogs } from "./system-logs"
-import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Languages, Server } from "lucide-react"
-import Image from "next/image"
-import { ThemeToggle } from "./theme-toggle"
-
-interface SystemStatus {
- status: "healthy" | "warning" | "critical"
- uptime: string
- lastUpdate: string
- serverName: string
- nodeId: string
-}
-
-export function ProxmoxDashboard() {
- const [systemStatus, setSystemStatus] = useState({
- status: "healthy",
- uptime: "15d 7h 23m",
- lastUpdate: new Date().toLocaleTimeString(),
- serverName: "proxmox-01",
- nodeId: "pve-node-01",
- })
- const [isRefreshing, setIsRefreshing] = useState(false)
- const [isTranslating, setIsTranslating] = useState(false)
-
- useEffect(() => {
- const fetchServerInfo = async () => {
- try {
- const response = await fetch("/api/flask/system-info")
- if (response.ok) {
- const data = await response.json()
- setSystemStatus((prev) => ({
- ...prev,
- serverName: data.hostname || "proxmox-01",
- nodeId: data.node_id || "pve-node-01",
- }))
- }
- } catch (error) {
- console.log("[v0] Using default server name due to API error:", error)
- }
- }
-
- fetchServerInfo()
- }, [])
-
- const refreshData = async () => {
- setIsRefreshing(true)
- await new Promise((resolve) => setTimeout(resolve, 1000))
- setSystemStatus((prev) => ({
- ...prev,
- lastUpdate: new Date().toLocaleTimeString(),
- }))
- setIsRefreshing(false)
- }
-
- const translatePage = async () => {
- setIsTranslating(true)
- try {
- if ("translate" in document.documentElement.dataset) {
- const currentLang = document.documentElement.dataset.translate
- document.documentElement.dataset.translate = currentLang === "yes" ? "no" : "yes"
- } else {
- const googleTranslateScript = document.createElement("script")
- googleTranslateScript.src = "https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit"
- document.head.appendChild(googleTranslateScript)
-
- window.googleTranslateElementInit = () => {
- new window.google.translate.TranslateElement(
- {
- pageLanguage: "en",
- includedLanguages: "es,en,fr,de,it,pt,ru,zh,ja,ko",
- layout: window.google.translate.TranslateElement.InlineLayout.SIMPLE,
- },
- "google_translate_element",
- )
- }
- }
- } catch (error) {
- console.error("Translation error:", error)
- }
- setIsTranslating(false)
- }
-
- const getStatusIcon = () => {
- switch (systemStatus.status) {
- case "healthy":
- return
- case "warning":
- return
- case "critical":
- return
- }
- }
-
- const getStatusColor = () => {
- switch (systemStatus.status) {
- case "healthy":
- return "bg-green-500/10 text-green-500 border-green-500/20"
- case "warning":
- return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
- case "critical":
- return "bg-red-500/10 text-red-500 border-red-500/20"
- }
- }
-
- return (
-
-
-
-
-
-
-
-
-
-
-
ProxMenux Monitor
-
Proxmox System Dashboard
-
-
-
-
-
-
-
{systemStatus.nodeId}
-
-
-
-
-
-
-
- {getStatusIcon()}
- {systemStatus.status}
-
-
-
Uptime: {systemStatus.uptime}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Overview
-
-
- Storage
-
-
- Network
-
-
- Virtual Machines
-
-
- System Logs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )
-}
diff --git a/AppImage/components/storage-metrics.tsx b/AppImage/components/storage-metrics.tsx
deleted file mode 100644
index b22a03f..0000000
--- a/AppImage/components/storage-metrics.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-"use client"
-
-import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
-import { Progress } from "./ui/progress"
-import { Badge } from "./ui/badge"
-import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from "recharts"
-import { HardDrive, Database, Archive, AlertTriangle, CheckCircle, Activity } from "lucide-react"
-
-const storageData = [
- { name: "Used", value: 1250, color: "#3b82f6" }, // Blue
- { name: "Available", value: 750, color: "#10b981" }, // Green
-]
-
-const diskPerformance = [
- { disk: "sda", read: 45, write: 32, iops: 1250 },
- { disk: "sdb", read: 67, write: 28, iops: 980 },
- { disk: "sdc", read: 23, write: 45, iops: 1100 },
- { disk: "nvme0n1", read: 156, write: 89, iops: 3400 },
-]
-
-export function StorageMetrics() {
- return (
-
- {/* Storage Overview Cards */}
-
-
-
- Total Storage
-
-
-
- 2.0 TB
-
- 1.25 TB used • 750 GB available
-
-
-
-
-
- VM & LXC Storage
-
-
-
- 890 GB
-
- 71.2% of allocated space
-
-
-
-
-
-
-
- Backups
-
-
-
- 245 GB
-
-
- 12 Backups
-
-
- Last backup: 2h ago
-
-
-
-
-
-
-
- IOPS
-
-
-
- 6.7K
-
- Read: 4.2K
- Write: 2.5K
-
- Average operations/sec
-
-
-
-
- {/* Storage Distribution and Performance */}
-
-
-
-
-
- Storage Distribution
-
-
-
-
-
-
- Used Storage
- 1.25 TB (62.5%)
-
-
-
-
-
-
- Available Storage
- 750 GB (37.5%)
-
-
-
-
-
-
-
-
-
-
-
-
-
- Disk Performance
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* Disk Details */}
-
-
-
-
- Storage Devices
-
-
-
-
- {[
- { name: "/dev/sda", type: "HDD", size: "1TB", used: "650GB", health: "healthy", temp: "42°C" },
- { name: "/dev/sdb", type: "HDD", size: "1TB", used: "480GB", health: "healthy", temp: "38°C" },
- { name: "/dev/sdc", type: "SSD", size: "500GB", used: "120GB", health: "healthy", temp: "35°C" },
- { name: "/dev/nvme0n1", type: "NVMe", size: "1TB", used: "340GB", health: "warning", temp: "55°C" },
- ].map((disk, index) => (
-
-
-
-
-
{disk.name}
-
- {disk.type} • {disk.size}
-
-
-
-
-
-
-
- {disk.used} / {disk.size}
-
-
-
-
-
-
-
- {disk.health === "healthy" ? (
-
- ) : (
-
- )}
- {disk.health}
-
-
-
- ))}
-
-
-
-
- )
-}
diff --git a/AppImage/components/system-logs.tsx b/AppImage/components/system-logs.tsx
deleted file mode 100644
index 5ef052b..0000000
--- a/AppImage/components/system-logs.tsx
+++ /dev/null
@@ -1,275 +0,0 @@
-"use client"
-
-import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
-import { Badge } from "./ui/badge"
-import { Button } from "./ui/button"
-import { Input } from "./ui/input"
-import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"
-import { ScrollArea } from "./ui/scroll-area"
-import { FileText, Search, Download, AlertTriangle, Info, CheckCircle, XCircle } from "lucide-react"
-import { useState } from "react"
-
-const systemLogs = [
- {
- timestamp: "2024-01-15 14:32:15",
- level: "info",
- service: "pveproxy",
- message: "User root@pam authenticated successfully",
- source: "auth.log",
- },
- {
- timestamp: "2024-01-15 14:31:45",
- level: "warning",
- service: "pvedaemon",
- message: "VM 101 high memory usage detected (85%)",
- source: "syslog",
- },
- {
- timestamp: "2024-01-15 14:30:22",
- level: "error",
- service: "pve-cluster",
- message: "Failed to connect to cluster node pve-02",
- source: "cluster.log",
- },
- {
- timestamp: "2024-01-15 14:29:18",
- level: "info",
- service: "pvestatd",
- message: "Storage local: 1.25TB used, 750GB available",
- source: "syslog",
- },
- {
- timestamp: "2024-01-15 14:28:33",
- level: "info",
- service: "pve-firewall",
- message: "Blocked connection attempt from 192.168.1.50",
- source: "firewall.log",
- },
- {
- timestamp: "2024-01-15 14:27:45",
- level: "warning",
- service: "smartd",
- message: "SMART warning: /dev/nvme0n1 temperature high (55°C)",
- source: "smart.log",
- },
- {
- timestamp: "2024-01-15 14:26:12",
- level: "info",
- service: "pveproxy",
- message: "Started backup job for VM 100",
- source: "backup.log",
- },
- {
- timestamp: "2024-01-15 14:25:38",
- level: "error",
- service: "qemu-server",
- message: "VM 102 failed to start: insufficient memory",
- source: "qemu.log",
- },
- {
- timestamp: "2024-01-15 14:24:55",
- level: "info",
- service: "pvedaemon",
- message: "VM 103 migrated successfully to node pve-01",
- source: "migration.log",
- },
- {
- timestamp: "2024-01-15 14:23:17",
- level: "warning",
- service: "pve-ha-lrm",
- message: "Resource VM:104 state changed to error",
- source: "ha.log",
- },
-]
-
-export function SystemLogs() {
- const [searchTerm, setSearchTerm] = useState("")
- const [levelFilter, setLevelFilter] = useState("all")
- const [serviceFilter, setServiceFilter] = useState("all")
-
- const filteredLogs = systemLogs.filter((log) => {
- const matchesSearch =
- log.message.toLowerCase().includes(searchTerm.toLowerCase()) ||
- log.service.toLowerCase().includes(searchTerm.toLowerCase())
- const matchesLevel = levelFilter === "all" || log.level === levelFilter
- const matchesService = serviceFilter === "all" || log.service === serviceFilter
-
- return matchesSearch && matchesLevel && matchesService
- })
-
- const getLevelColor = (level: string) => {
- switch (level) {
- case "error":
- return "bg-red-500/10 text-red-500 border-red-500/20"
- case "warning":
- return "bg-yellow-500/10 text-yellow-500 border-yellow-500/20"
- case "info":
- return "bg-blue-500/10 text-blue-500 border-blue-500/20"
- default:
- return "bg-gray-500/10 text-gray-500 border-gray-500/20"
- }
- }
-
- const getLevelIcon = (level: string) => {
- switch (level) {
- case "error":
- return
- case "warning":
- return
- case "info":
- return
- default:
- return
- }
- }
-
- const logCounts = {
- total: systemLogs.length,
- error: systemLogs.filter((log) => log.level === "error").length,
- warning: systemLogs.filter((log) => log.level === "warning").length,
- info: systemLogs.filter((log) => log.level === "info").length,
- }
-
- const uniqueServices = [...new Set(systemLogs.map((log) => log.service))]
-
- return (
-
- {/* Log Statistics */}
-
-
-
- Total Logs
-
-
-
- {logCounts.total}
- Last 24 hours
-
-
-
-
-
- Errors
-
-
-
- {logCounts.error}
- Requires attention
-
-
-
-
-
- Warnings
-
-
-
- {logCounts.warning}
- Monitor closely
-
-
-
-
-
- Info
-
-
-
- {logCounts.info}
- Normal operations
-
-
-
-
- {/* Log Filters and Search */}
-
-
-
-
- System Logs
-
-
-
-
-
-
-
- setSearchTerm(e.target.value)}
- className="pl-10 bg-background border-border"
- />
-
-
-
-
-
-
-
-
-
-
-
-
- {filteredLogs.map((log, index) => (
-
-
-
- {getLevelIcon(log.level)}
- {log.level.toUpperCase()}
-
-
-
-
-
-
{log.service}
-
{log.timestamp}
-
-
{log.message}
-
Source: {log.source}
-
-
- ))}
-
- {filteredLogs.length === 0 && (
-
-
-
No logs found matching your criteria
-
- )}
-
-
-
-
-
- )
-}
diff --git a/AppImage/components/system-overview.tsx b/AppImage/components/system-overview.tsx
deleted file mode 100644
index 5c9b319..0000000
--- a/AppImage/components/system-overview.tsx
+++ /dev/null
@@ -1,249 +0,0 @@
-"use client"
-
-import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
-import { Progress } from "./ui/progress"
-import { Badge } from "./ui/badge"
-import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, AreaChart, Area } from "recharts"
-import { Cpu, MemoryStick, Thermometer, Users, Activity, Server, Zap } from "lucide-react"
-
-const cpuData = [
- { time: "00:00", value: 45 },
- { time: "04:00", value: 52 },
- { time: "08:00", value: 78 },
- { time: "12:00", value: 65 },
- { time: "16:00", value: 82 },
- { time: "20:00", value: 58 },
- { time: "24:00", value: 43 },
-]
-
-const memoryData = [
- { time: "00:00", used: 12.5, available: 19.5 },
- { time: "04:00", used: 14.2, available: 17.8 },
- { time: "08:00", used: 18.7, available: 13.3 },
- { time: "12:00", used: 16.3, available: 15.7 },
- { time: "16:00", used: 21.1, available: 10.9 },
- { time: "20:00", used: 15.8, available: 16.2 },
- { time: "24:00", used: 13.2, available: 18.8 },
-]
-
-export function SystemOverview() {
- return (
-
- {/* Key Metrics Cards */}
-
-
-
- CPU Usage
-
-
-
- 67.3%
-
-
- ↓ 2.1% from last hour
-
-
-
-
-
-
- Memory Usage
-
-
-
- 15.8 GB
-
-
- 49.4% of 32 GB • ↑ 1.2 GB
-
-
-
-
-
-
- Temperature
-
-
-
- 52°C
-
-
- Normal
-
-
- Max: 78°C • Avg: 48°C
-
-
-
-
-
- Active VMs & LXC
-
-
-
- 15
-
-
- 8 Running VMs
-
-
- 3 Running LXC
-
-
- 4 Stopped
-
-
- Total: 12 VMs • 6 LXC configured
-
-
-
-
- {/* Charts Section */}
-
-
-
-
-
- CPU Usage (24h)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Memory Usage (24h)
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {/* System Information */}
-
-
-
-
-
- System Information
-
-
-
-
- Hostname:
- proxmox-01
-
-
- Version:
- PVE 8.1.3
-
-
- Kernel:
- 6.5.11-7-pve
-
-
- Architecture:
- x86_64
-
-
-
-
-
-
-
-
- Active Sessions
-
-
-
-
- Web Console:
-
- 3 active
-
-
-
- SSH Sessions:
-
- 1 active
-
-
-
- API Calls:
- 247/hour
-
-
-
-
-
-
-
-
- Power & Performance
-
-
-
-
- Power State:
-
- Running
-
-
-
- Load Average:
- 1.23, 1.45, 1.67
-
-
- Boot Time:
- 2.3s
-
-
-
-
-
- )
-}
diff --git a/AppImage/components/theme-toggle.tsx b/AppImage/components/theme-toggle.tsx
deleted file mode 100644
index eb4c91c..0000000
--- a/AppImage/components/theme-toggle.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-"use client"
-import { Moon, Sun } from "lucide-react"
-import { useTheme } from "next-themes"
-
-import { Button } from "./ui/button"
-
-export function ThemeToggle() {
- const { theme, setTheme } = useTheme()
-
- return (
-
- )
-}
diff --git a/AppImage/components/ui/accordion.tsx b/AppImage/components/ui/accordion.tsx
new file mode 100644
index 0000000..e538a33
--- /dev/null
+++ b/AppImage/components/ui/accordion.tsx
@@ -0,0 +1,66 @@
+'use client'
+
+import * as React from 'react'
+import * as AccordionPrimitive from '@radix-ui/react-accordion'
+import { ChevronDownIcon } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function Accordion({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AccordionItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ svg]:rotate-180',
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+ )
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
diff --git a/AppImage/components/ui/alert-dialog.tsx b/AppImage/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..9704452
--- /dev/null
+++ b/AppImage/components/ui/alert-dialog.tsx
@@ -0,0 +1,157 @@
+'use client'
+
+import * as React from 'react'
+import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
+
+import { cn } from '@/lib/utils'
+import { buttonVariants } from '@/components/ui/button'
+
+function AlertDialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function AlertDialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ )
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AlertDialogCancel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ AlertDialog,
+ AlertDialogPortal,
+ AlertDialogOverlay,
+ AlertDialogTrigger,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogFooter,
+ AlertDialogTitle,
+ AlertDialogDescription,
+ AlertDialogAction,
+ AlertDialogCancel,
+}
diff --git a/AppImage/components/ui/alert.tsx b/AppImage/components/ui/alert.tsx
new file mode 100644
index 0000000..e6751ab
--- /dev/null
+++ b/AppImage/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+import * as React from 'react'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
+
+const alertVariants = cva(
+ 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current',
+ {
+ variants: {
+ variant: {
+ default: 'bg-card text-card-foreground',
+ destructive:
+ 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+)
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ )
+}
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/AppImage/components/ui/aspect-ratio.tsx b/AppImage/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..40bb120
--- /dev/null
+++ b/AppImage/components/ui/aspect-ratio.tsx
@@ -0,0 +1,11 @@
+'use client'
+
+import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio'
+
+function AspectRatio({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+export { AspectRatio }
diff --git a/AppImage/components/ui/avatar.tsx b/AppImage/components/ui/avatar.tsx
new file mode 100644
index 0000000..aa98465
--- /dev/null
+++ b/AppImage/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+'use client'
+
+import * as React from 'react'
+import * as AvatarPrimitive from '@radix-ui/react-avatar'
+
+import { cn } from '@/lib/utils'
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/AppImage/components/ui/badge.tsx b/AppImage/components/ui/badge.tsx
index 78b3863..fc4126b 100644
--- a/AppImage/components/ui/badge.tsx
+++ b/AppImage/components/ui/badge.tsx
@@ -1,28 +1,46 @@
-import type * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
const badgeVariants = cva(
- "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
+ 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden',
{
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",
+ default:
+ 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90',
+ secondary:
+ 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
+ destructive:
+ 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground',
},
},
defaultVariants: {
- variant: "default",
+ variant: 'default',
},
},
)
-export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'span'> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : 'span'
-function Badge({ className, variant, ...props }: BadgeProps) {
- return
+ return (
+
+ )
}
export { Badge, badgeVariants }
diff --git a/AppImage/components/ui/breadcrumb.tsx b/AppImage/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..1750ff2
--- /dev/null
+++ b/AppImage/components/ui/breadcrumb.tsx
@@ -0,0 +1,109 @@
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { ChevronRight, MoreHorizontal } from 'lucide-react'
+
+import { cn } from '@/lib/utils'
+
+function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) {
+ return
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbLink({
+ asChild,
+ className,
+ ...props
+}: React.ComponentProps<'a'> & {
+ asChild?: boolean
+}) {
+ const Comp = asChild ? Slot : 'a'
+
+ return (
+
+ )
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ )
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+ )
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+
+ More
+
+ )
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+}
diff --git a/AppImage/components/ui/button.tsx b/AppImage/components/ui/button.tsx
index 495dba8..815443b 100644
--- a/AppImage/components/ui/button.tsx
+++ b/AppImage/components/ui/button.tsx
@@ -1,46 +1,59 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
-import { cn } from "@/lib/utils"
+import * as React from 'react'
+import { Slot } from '@radix-ui/react-slot'
+import { cva, type VariantProps } from 'class-variance-authority'
+
+import { cn } from '@/lib/utils'
const buttonVariants = cva(
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
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",
+ default:
+ 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90',
+ destructive:
+ 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
+ outline:
+ 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
+ secondary:
+ 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80',
+ ghost:
+ 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
+ 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",
+ default: 'h-9 px-4 py-2 has-[>svg]:px-3',
+ sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
+ lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
+ icon: 'size-9',
},
},
defaultVariants: {
- variant: "default",
- size: "default",
+ variant: 'default',
+ size: 'default',
},
},
)
-export interface ButtonProps
- extends React.ButtonHTMLAttributes,
- VariantProps {
- asChild?: boolean
+function Button({
+ className,
+ variant,
+ size,
+ asChild = false,
+ ...props
+}: React.ComponentProps<'button'> &
+ VariantProps & {
+ asChild?: boolean
+ }) {
+ const Comp = asChild ? Slot : 'button'
+
+ return (
+
+ )
}
-const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
- return
- },
-)
-Button.displayName = "Button"
-
export { Button, buttonVariants }
diff --git a/AppImage/components/ui/calendar.tsx b/AppImage/components/ui/calendar.tsx
new file mode 100644
index 0000000..eaa373e
--- /dev/null
+++ b/AppImage/components/ui/calendar.tsx
@@ -0,0 +1,213 @@
+'use client'
+
+import * as React from 'react'
+import {
+ ChevronDownIcon,
+ ChevronLeftIcon,
+ ChevronRightIcon,
+} from 'lucide-react'
+import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker'
+
+import { cn } from '@/lib/utils'
+import { Button, buttonVariants } from '@/components/ui/button'
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = 'label',
+ buttonVariant = 'ghost',
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps['variant']
+}) {
+ const defaultClassNames = getDefaultClassNames()
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className,
+ )}
+ captionLayout={captionLayout}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString('default', { month: 'short' }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn('w-fit', defaultClassNames.root),
+ months: cn(
+ 'flex gap-4 flex-col md:flex-row relative',
+ defaultClassNames.months,
+ ),
+ month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
+ nav: cn(
+ 'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
+ defaultClassNames.nav,
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_previous,
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none',
+ defaultClassNames.button_next,
+ ),
+ month_caption: cn(
+ 'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
+ defaultClassNames.month_caption,
+ ),
+ dropdowns: cn(
+ 'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
+ defaultClassNames.dropdowns,
+ ),
+ dropdown_root: cn(
+ 'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
+ defaultClassNames.dropdown_root,
+ ),
+ dropdown: cn(
+ 'absolute bg-popover inset-0 opacity-0',
+ defaultClassNames.dropdown,
+ ),
+ caption_label: cn(
+ 'select-none font-medium',
+ captionLayout === 'label'
+ ? 'text-sm'
+ : 'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
+ defaultClassNames.caption_label,
+ ),
+ table: 'w-full border-collapse',
+ weekdays: cn('flex', defaultClassNames.weekdays),
+ weekday: cn(
+ 'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
+ defaultClassNames.weekday,
+ ),
+ week: cn('flex w-full mt-2', defaultClassNames.week),
+ week_number_header: cn(
+ 'select-none w-(--cell-size)',
+ defaultClassNames.week_number_header,
+ ),
+ week_number: cn(
+ 'text-[0.8rem] select-none text-muted-foreground',
+ defaultClassNames.week_number,
+ ),
+ day: cn(
+ 'relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none',
+ defaultClassNames.day,
+ ),
+ range_start: cn(
+ 'rounded-l-md bg-accent',
+ defaultClassNames.range_start,
+ ),
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
+ range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
+ today: cn(
+ 'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
+ defaultClassNames.today,
+ ),
+ outside: cn(
+ 'text-muted-foreground aria-selected:text-muted-foreground',
+ defaultClassNames.outside,
+ ),
+ disabled: cn(
+ 'text-muted-foreground opacity-50',
+ defaultClassNames.disabled,
+ ),
+ hidden: cn('invisible', defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ )
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === 'left') {
+ return (
+
+ )
+ }
+
+ if (orientation === 'right') {
+ return (
+
+ )
+ }
+
+ return (
+
+ )
+ },
+ DayButton: CalendarDayButton,
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ )
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ )
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ ...props
+}: React.ComponentProps) {
+ const defaultClassNames = getDefaultClassNames()
+
+ const ref = React.useRef(null)
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus()
+ }, [modifiers.focused])
+
+ return (
+