summaryrefslogtreecommitdiff
path: root/frontend
diff options
context:
space:
mode:
authordiogo464 <[email protected]>2025-08-11 16:04:32 +0100
committerdiogo464 <[email protected]>2025-08-11 16:04:32 +0100
commitf4d8a26972728891de8bde4eeb94c80f027ce2d2 (patch)
tree3c8b9c25c2a1e3fab7a86f51922c39eb2ed93697 /frontend
parent32b008a9c0c8e0130ab10bc96ffea9232f9cf95a (diff)
basic v0 ui working
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/drive/[...path]/page.tsx58
-rw-r--r--frontend/app/drive/page.tsx33
-rw-r--r--frontend/app/globals.css122
-rw-r--r--frontend/app/layout.tsx6
-rw-r--r--frontend/app/page.tsx6
-rw-r--r--frontend/components.json21
-rw-r--r--frontend/components/AuthButton.tsx31
-rw-r--r--frontend/components/FileTable.tsx179
-rw-r--r--frontend/components/FileUpload.tsx262
-rw-r--r--frontend/components/ui/badge.tsx46
-rw-r--r--frontend/components/ui/button.tsx59
-rw-r--r--frontend/components/ui/checkbox.tsx47
-rw-r--r--frontend/components/ui/dialog.tsx143
-rw-r--r--frontend/components/ui/dropdown-menu.tsx257
-rw-r--r--frontend/components/ui/input.tsx21
-rw-r--r--frontend/components/ui/label.tsx24
-rw-r--r--frontend/components/ui/progress.tsx31
-rw-r--r--frontend/components/ui/table.tsx116
-rw-r--r--frontend/components/ui/toast.tsx129
-rw-r--r--frontend/components/ui/toaster.tsx35
-rw-r--r--frontend/file-drive.tsx936
-rw-r--r--frontend/history-view.tsx230
-rw-r--r--frontend/hooks/use-toast.ts193
-rw-r--r--frontend/lib/utils.ts29
-rw-r--r--frontend/package-lock.json965
-rw-r--r--frontend/package.json22
26 files changed, 3384 insertions, 617 deletions
diff --git a/frontend/app/drive/[...path]/page.tsx b/frontend/app/drive/[...path]/page.tsx
deleted file mode 100644
index be11253..0000000
--- a/frontend/app/drive/[...path]/page.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
1import { Drive_ls } from "@/lib/drive_server"
2import { Drive_parent } from "@/lib/drive_shared"
3import Link from "next/link"
4import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth";
5import FileUpload from "@/components/FileUpload"
6import FileTable from "@/components/FileTable"
7
8interface DrivePageProps {
9 params: Promise<{
10 path: string[]
11 }>
12}
13
14export default async function DrivePage({ params }: DrivePageProps) {
15 // Await params as required by Next.js 15
16 const { path } = await params
17
18 const user = await Auth_get_user();
19
20 // Construct the full path from params
21 const fullPath = path ? `/${path.join('/')}` : ""
22
23 const entries = await Drive_ls(fullPath, false)
24
25 // Check if we have a parent directory
26 const parentDir = Drive_parent(fullPath)
27 const parentPath = path && path.length > 1
28 ? `/drive/${path.slice(0, -1).join('/')}`
29 : path && path.length === 1
30 ? '/drive'
31 : null
32
33 const showParent = parentDir !== null && parentPath !== null
34
35 return (
36 <div className="min-h-screen bg-background">
37 <div className="container mx-auto p-4">
38 <Link href="/drive" className="inline-block mb-6">
39 <h1 className="text-2xl font-bold text-foreground hover:text-blue-600 dark:hover:text-blue-400 transition-colors cursor-pointer">FCTDrive</h1>
40 </Link>
41
42 <FileTable
43 entries={entries}
44 currentPath={fullPath}
45 showParent={showParent}
46 parentPath={parentPath || undefined}
47 />
48
49 {/* File Upload Component */}
50 {Auth_user_can_upload(user) && (
51 <FileUpload
52 targetPath={fullPath}
53 />
54 )}
55 </div>
56 </div>
57 )
58}
diff --git a/frontend/app/drive/page.tsx b/frontend/app/drive/page.tsx
deleted file mode 100644
index 9253c3a..0000000
--- a/frontend/app/drive/page.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
1import { Drive_ls } from "@/lib/drive_server"
2import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth"
3import Link from "next/link"
4import FileUpload from "@/components/FileUpload"
5import FileTable from "@/components/FileTable"
6
7export default async function DrivePage() {
8 const user = await Auth_get_user()
9 const entries = await Drive_ls("", false)
10
11 return (
12 <div className="min-h-screen bg-background">
13 <div className="container mx-auto p-4">
14 <Link href="/drive" className="inline-block mb-6">
15 <h1 className="text-2xl font-bold text-foreground hover:text-blue-600 dark:hover:text-blue-400 transition-colors cursor-pointer">FCTDrive</h1>
16 </Link>
17
18 <FileTable
19 entries={entries}
20 currentPath=""
21 showParent={false}
22 />
23
24 {/* File Upload Component */}
25 {Auth_user_can_upload(user) && (
26 <FileUpload
27 targetPath=""
28 />
29 )}
30 </div>
31 </div>
32 )
33} \ No newline at end of file
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index a2dc41e..dc98be7 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -1,26 +1,122 @@
1@import "tailwindcss"; 1@import "tailwindcss";
2@import "tw-animate-css";
2 3
3:root { 4@custom-variant dark (&:is(.dark *));
4 --background: #ffffff;
5 --foreground: #171717;
6}
7 5
8@theme inline { 6@theme inline {
9 --color-background: var(--background); 7 --color-background: var(--background);
10 --color-foreground: var(--foreground); 8 --color-foreground: var(--foreground);
11 --font-sans: var(--font-geist-sans); 9 --font-sans: var(--font-geist-sans);
12 --font-mono: var(--font-geist-mono); 10 --font-mono: var(--font-geist-mono);
11 --color-sidebar-ring: var(--sidebar-ring);
12 --color-sidebar-border: var(--sidebar-border);
13 --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
14 --color-sidebar-accent: var(--sidebar-accent);
15 --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
16 --color-sidebar-primary: var(--sidebar-primary);
17 --color-sidebar-foreground: var(--sidebar-foreground);
18 --color-sidebar: var(--sidebar);
19 --color-chart-5: var(--chart-5);
20 --color-chart-4: var(--chart-4);
21 --color-chart-3: var(--chart-3);
22 --color-chart-2: var(--chart-2);
23 --color-chart-1: var(--chart-1);
24 --color-ring: var(--ring);
25 --color-input: var(--input);
26 --color-border: var(--border);
27 --color-destructive: var(--destructive);
28 --color-accent-foreground: var(--accent-foreground);
29 --color-accent: var(--accent);
30 --color-muted-foreground: var(--muted-foreground);
31 --color-muted: var(--muted);
32 --color-secondary-foreground: var(--secondary-foreground);
33 --color-secondary: var(--secondary);
34 --color-primary-foreground: var(--primary-foreground);
35 --color-primary: var(--primary);
36 --color-popover-foreground: var(--popover-foreground);
37 --color-popover: var(--popover);
38 --color-card-foreground: var(--card-foreground);
39 --color-card: var(--card);
40 --radius-sm: calc(var(--radius) - 4px);
41 --radius-md: calc(var(--radius) - 2px);
42 --radius-lg: var(--radius);
43 --radius-xl: calc(var(--radius) + 4px);
13} 44}
14 45
15@media (prefers-color-scheme: dark) { 46:root {
16 :root { 47 --radius: 0.625rem;
17 --background: #0a0a0a; 48 --background: oklch(1 0 0);
18 --foreground: #ededed; 49 --foreground: oklch(0.145 0 0);
19 } 50 --card: oklch(1 0 0);
51 --card-foreground: oklch(0.145 0 0);
52 --popover: oklch(1 0 0);
53 --popover-foreground: oklch(0.145 0 0);
54 --primary: oklch(0.205 0 0);
55 --primary-foreground: oklch(0.985 0 0);
56 --secondary: oklch(0.97 0 0);
57 --secondary-foreground: oklch(0.205 0 0);
58 --muted: oklch(0.97 0 0);
59 --muted-foreground: oklch(0.556 0 0);
60 --accent: oklch(0.97 0 0);
61 --accent-foreground: oklch(0.205 0 0);
62 --destructive: oklch(0.577 0.245 27.325);
63 --border: oklch(0.922 0 0);
64 --input: oklch(0.922 0 0);
65 --ring: oklch(0.708 0 0);
66 --chart-1: oklch(0.646 0.222 41.116);
67 --chart-2: oklch(0.6 0.118 184.704);
68 --chart-3: oklch(0.398 0.07 227.392);
69 --chart-4: oklch(0.828 0.189 84.429);
70 --chart-5: oklch(0.769 0.188 70.08);
71 --sidebar: oklch(0.985 0 0);
72 --sidebar-foreground: oklch(0.145 0 0);
73 --sidebar-primary: oklch(0.205 0 0);
74 --sidebar-primary-foreground: oklch(0.985 0 0);
75 --sidebar-accent: oklch(0.97 0 0);
76 --sidebar-accent-foreground: oklch(0.205 0 0);
77 --sidebar-border: oklch(0.922 0 0);
78 --sidebar-ring: oklch(0.708 0 0);
20} 79}
21 80
22body { 81.dark {
23 background: var(--background); 82 --background: oklch(0.145 0 0);
24 color: var(--foreground); 83 --foreground: oklch(0.985 0 0);
25 font-family: Arial, Helvetica, sans-serif; 84 --card: oklch(0.205 0 0);
85 --card-foreground: oklch(0.985 0 0);
86 --popover: oklch(0.205 0 0);
87 --popover-foreground: oklch(0.985 0 0);
88 --primary: oklch(0.922 0 0);
89 --primary-foreground: oklch(0.205 0 0);
90 --secondary: oklch(0.269 0 0);
91 --secondary-foreground: oklch(0.985 0 0);
92 --muted: oklch(0.269 0 0);
93 --muted-foreground: oklch(0.708 0 0);
94 --accent: oklch(0.269 0 0);
95 --accent-foreground: oklch(0.985 0 0);
96 --destructive: oklch(0.704 0.191 22.216);
97 --border: oklch(1 0 0 / 10%);
98 --input: oklch(1 0 0 / 15%);
99 --ring: oklch(0.556 0 0);
100 --chart-1: oklch(0.488 0.243 264.376);
101 --chart-2: oklch(0.696 0.17 162.48);
102 --chart-3: oklch(0.769 0.188 70.08);
103 --chart-4: oklch(0.627 0.265 303.9);
104 --chart-5: oklch(0.645 0.246 16.439);
105 --sidebar: oklch(0.205 0 0);
106 --sidebar-foreground: oklch(0.985 0 0);
107 --sidebar-primary: oklch(0.488 0.243 264.376);
108 --sidebar-primary-foreground: oklch(0.985 0 0);
109 --sidebar-accent: oklch(0.269 0 0);
110 --sidebar-accent-foreground: oklch(0.985 0 0);
111 --sidebar-border: oklch(1 0 0 / 10%);
112 --sidebar-ring: oklch(0.556 0 0);
113}
114
115@layer base {
116 * {
117 @apply border-border outline-ring/50;
118 }
119 body {
120 @apply bg-background text-foreground;
121 }
26} 122}
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 2e001e4..9cc7dac 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -1,7 +1,7 @@
1import type { Metadata } from "next"; 1import type { Metadata } from "next";
2import { Geist, Geist_Mono } from "next/font/google"; 2import { Geist, Geist_Mono } from "next/font/google";
3import "./globals.css"; 3import "./globals.css";
4import AuthButton from "@/components/AuthButton"; 4import { Toaster } from "@/components/ui/toaster";
5 5
6const geistSans = Geist({ 6const geistSans = Geist({
7 variable: "--font-geist-sans", 7 variable: "--font-geist-sans",
@@ -28,10 +28,8 @@ export default function RootLayout({
28 <body 28 <body
29 className={`${geistSans.variable} ${geistMono.variable} antialiased`} 29 className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30 > 30 >
31 <div className="absolute top-4 right-4 z-10">
32 <AuthButton />
33 </div>
34 {children} 31 {children}
32 <Toaster />
35 </body> 33 </body>
36 </html> 34 </html>
37 ); 35 );
diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx
index 3333985..e2b6a80 100644
--- a/frontend/app/page.tsx
+++ b/frontend/app/page.tsx
@@ -1,5 +1,5 @@
1import { redirect } from 'next/navigation' 1import FileDrive from "../file-drive"
2 2
3export default function Home() { 3export default function Page() {
4 redirect('/drive') 4 return <FileDrive />
5} 5}
diff --git a/frontend/components.json b/frontend/components.json
new file mode 100644
index 0000000..335484f
--- /dev/null
+++ b/frontend/components.json
@@ -0,0 +1,21 @@
1{
2 "$schema": "https://ui.shadcn.com/schema.json",
3 "style": "new-york",
4 "rsc": true,
5 "tsx": true,
6 "tailwind": {
7 "config": "",
8 "css": "app/globals.css",
9 "baseColor": "neutral",
10 "cssVariables": true,
11 "prefix": ""
12 },
13 "aliases": {
14 "components": "@/components",
15 "utils": "@/lib/utils",
16 "ui": "@/components/ui",
17 "lib": "@/lib",
18 "hooks": "@/hooks"
19 },
20 "iconLibrary": "lucide"
21} \ No newline at end of file
diff --git a/frontend/components/AuthButton.tsx b/frontend/components/AuthButton.tsx
deleted file mode 100644
index 05c493c..0000000
--- a/frontend/components/AuthButton.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
1import { Auth_get_user, Auth_tinyauth_public_endpoint } from '@/lib/auth'
2
3export default async function AuthButton() {
4 const user = await Auth_get_user()
5 const authEndpoint = Auth_tinyauth_public_endpoint()
6
7 if (user.isLoggedIn) {
8 return (
9 <div className="flex items-center gap-4">
10 <span className="text-sm text-gray-600 dark:text-gray-300">
11 {user.email}
12 </span>
13 <a
14 href={authEndpoint}
15 className="px-3 py-1 text-sm bg-red-600 hover:bg-red-700 text-white rounded transition-colors"
16 >
17 Logout
18 </a>
19 </div>
20 )
21 }
22
23 return (
24 <a
25 href={authEndpoint}
26 className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors"
27 >
28 Login
29 </a>
30 )
31} \ No newline at end of file
diff --git a/frontend/components/FileTable.tsx b/frontend/components/FileTable.tsx
deleted file mode 100644
index 97660f3..0000000
--- a/frontend/components/FileTable.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
1'use client'
2
3import { useState } from 'react'
4import { DriveLsEntry } from '@/lib/drive_types'
5import { Drive_basename } from '@/lib/drive_shared'
6import { formatSize } from '@/lib/utils'
7import Link from 'next/link'
8
9interface FileTableEntry extends DriveLsEntry {
10 isParent?: boolean
11 parentPath?: string
12}
13
14interface FileTableProps {
15 entries: DriveLsEntry[]
16 currentPath?: string
17 showParent?: boolean
18 parentPath?: string
19 onSelectedFilesChange?: (selectedFiles: string[]) => void
20}
21
22export default function FileTable({
23 entries,
24 currentPath = '',
25 showParent = false,
26 parentPath,
27 onSelectedFilesChange
28}: FileTableProps) {
29 const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set())
30
31 // Create entries with optional parent directory at top
32 const allEntries: FileTableEntry[] = []
33 if (showParent && parentPath !== undefined) {
34 allEntries.push({
35 path: '(parent)',
36 type: 'dir' as const,
37 lastmod: 0,
38 blob: null,
39 size: null,
40 author: '',
41 isParent: true,
42 parentPath
43 })
44 }
45
46 // Sort entries: directories first, then files, both alphabetically
47 const sortedEntries = entries.sort((a, b) => {
48 // First sort by type (directories before files)
49 if (a.type !== b.type) {
50 return a.type === 'dir' ? -1 : 1
51 }
52 // Then sort alphabetically by path
53 return a.path.localeCompare(b.path)
54 })
55
56 allEntries.push(...sortedEntries)
57
58 const handleFileSelection = (filePath: string, isSelected: boolean) => {
59 const newSelectedFiles = new Set(selectedFiles)
60 if (isSelected) {
61 newSelectedFiles.add(filePath)
62 } else {
63 newSelectedFiles.delete(filePath)
64 }
65 setSelectedFiles(newSelectedFiles)
66 onSelectedFilesChange?.(Array.from(newSelectedFiles))
67 }
68
69 const handleSelectAll = (isSelected: boolean) => {
70 if (isSelected) {
71 // Select all files (not directories or parent)
72 const fileEntries = allEntries.filter(entry =>
73 entry.type === 'file' && !entry.isParent
74 )
75 const newSelectedFiles = new Set(fileEntries.map(entry => entry.path))
76 setSelectedFiles(newSelectedFiles)
77 onSelectedFilesChange?.(Array.from(newSelectedFiles))
78 } else {
79 setSelectedFiles(new Set())
80 onSelectedFilesChange?.([])
81 }
82 }
83
84 const selectableFiles = allEntries.filter(entry =>
85 entry.type === 'file' && !entry.isParent
86 )
87 const allFilesSelected = selectableFiles.length > 0 &&
88 selectableFiles.every(entry => selectedFiles.has(entry.path))
89
90 return (
91 <div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
92 <div className="overflow-x-auto">
93 <table className="w-full">
94 <thead className="bg-gray-50 dark:bg-gray-700">
95 <tr>
96 <th className="px-4 py-3 text-left">
97 <input
98 type="checkbox"
99 checked={allFilesSelected}
100 onChange={(e) => handleSelectAll(e.target.checked)}
101 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
102 disabled={selectableFiles.length === 0}
103 />
104 </th>
105 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
106 Name
107 </th>
108 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
109 Size
110 </th>
111 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
112 Author
113 </th>
114 <th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
115 Modified
116 </th>
117 </tr>
118 </thead>
119 <tbody className="divide-y divide-gray-200 dark:divide-gray-600">
120 {allEntries.map((entry) => (
121 <tr
122 key={entry.path}
123 className="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
124 >
125 <td className="px-4 py-4 whitespace-nowrap">
126 {entry.type === 'file' && !entry.isParent ? (
127 <input
128 type="checkbox"
129 checked={selectedFiles.has(entry.path)}
130 onChange={(e) => handleFileSelection(entry.path, e.target.checked)}
131 className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
132 />
133 ) : (
134 <div className="w-4 h-4" /> // Placeholder to maintain alignment
135 )}
136 </td>
137 <td className="px-4 py-4 whitespace-nowrap">
138 <div className="flex items-center">
139 <div className="flex-shrink-0 h-5 w-5 mr-3">
140 {entry.type === 'dir' ? (
141 <div className="h-5 w-5 text-blue-500">📁</div>
142 ) : (
143 <div className="h-5 w-5 text-gray-400">📄</div>
144 )}
145 </div>
146 {entry.type === 'dir' ? (
147 <Link
148 href={entry.isParent ? entry.parentPath! : `/drive${entry.path}`}
149 className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
150 >
151 {entry.isParent ? '(parent)' : Drive_basename(entry.path)}
152 </Link>
153 ) : (
154 <Link
155 href={`/blob/${entry.blob}?filename=${encodeURIComponent(Drive_basename(entry.path))}`}
156 className="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
157 >
158 {Drive_basename(entry.path)}
159 </Link>
160 )}
161 </div>
162 </td>
163 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
164 {formatSize(entry.size)}
165 </td>
166 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
167 {entry.author}
168 </td>
169 <td className="px-4 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">
170 {entry.lastmod > 0 ? new Date(entry.lastmod * 1000).toLocaleString() : ''}
171 </td>
172 </tr>
173 ))}
174 </tbody>
175 </table>
176 </div>
177 </div>
178 )
179} \ No newline at end of file
diff --git a/frontend/components/FileUpload.tsx b/frontend/components/FileUpload.tsx
deleted file mode 100644
index 8fbb919..0000000
--- a/frontend/components/FileUpload.tsx
+++ /dev/null
@@ -1,262 +0,0 @@
1'use client'
2
3import { useState, useRef } from 'react'
4import { UPLOAD_MAX_FILES, UPLOAD_MAX_FILE_SIZE } from '@/lib/constants'
5
6// Client-side file validation function
7function validateFile(file: File): { allowed: boolean; reason?: string } {
8 if (file.size > UPLOAD_MAX_FILE_SIZE) {
9 return { allowed: false, reason: `File size exceeds ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB limit` };
10 }
11
12 return { allowed: true };
13}
14
15interface FileUploadProps {
16 targetPath: string
17 onUploadComplete?: () => void
18}
19
20interface UploadResult {
21 filename: string
22 success: boolean
23 message: string
24}
25
26export default function FileUpload({ targetPath, onUploadComplete }: FileUploadProps) {
27 const [isDragOver, setIsDragOver] = useState(false)
28 const [isUploading, setIsUploading] = useState(false)
29 const [selectedFiles, setSelectedFiles] = useState<File[]>([])
30 const [uploadResults, setUploadResults] = useState<UploadResult[]>([])
31 const [showResults, setShowResults] = useState(false)
32 const fileInputRef = useRef<HTMLInputElement>(null)
33
34 const handleFileSelect = (files: FileList) => {
35 const fileArray = Array.from(files)
36
37 // Validate file count
38 if (fileArray.length > UPLOAD_MAX_FILES) {
39 alert(`Too many files selected. Maximum ${UPLOAD_MAX_FILES} files allowed.`)
40 return
41 }
42
43 // Validate each file
44 const validFiles: File[] = []
45 for (const file of fileArray) {
46 const validation = validateFile(file)
47 if (!validation.allowed) {
48 alert(`File '${file.name}': ${validation.reason}`)
49 continue
50 }
51 validFiles.push(file)
52 }
53
54 setSelectedFiles(validFiles)
55 setUploadResults([])
56 setShowResults(false)
57 }
58
59 const handleDragOver = (e: React.DragEvent) => {
60 e.preventDefault()
61 setIsDragOver(true)
62 }
63
64 const handleDragLeave = (e: React.DragEvent) => {
65 e.preventDefault()
66 setIsDragOver(false)
67 }
68
69 const handleDrop = (e: React.DragEvent) => {
70 e.preventDefault()
71 setIsDragOver(false)
72
73 if (e.dataTransfer.files) {
74 handleFileSelect(e.dataTransfer.files)
75 }
76 }
77
78 const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
79 if (e.target.files) {
80 handleFileSelect(e.target.files)
81 }
82 }
83
84 const handleUpload = async () => {
85 if (selectedFiles.length === 0) return
86
87 setIsUploading(true)
88 setUploadResults([])
89
90 try {
91 const formData = new FormData()
92 selectedFiles.forEach(file => {
93 formData.append('files', file)
94 })
95 formData.append('targetPath', targetPath)
96
97 const response = await fetch('/api/upload', {
98 method: 'POST',
99 body: formData,
100 })
101
102 const result = await response.json()
103
104 if (response.ok) {
105 setUploadResults(result.results || [])
106 setShowResults(true)
107 setSelectedFiles([])
108
109 // Clear file input
110 if (fileInputRef.current) {
111 fileInputRef.current.value = ''
112 }
113
114 // Refresh the page after successful upload
115 setTimeout(() => {
116 window.location.reload()
117 }, 1000)
118 } else {
119 alert(`Upload failed: ${result.error}`)
120 }
121 } catch (error) {
122 console.error('Upload error:', error)
123 alert('Upload failed: Network error')
124 } finally {
125 setIsUploading(false)
126 }
127 }
128
129 const removeFile = (index: number) => {
130 setSelectedFiles(prev => prev.filter((_, i) => i !== index))
131 }
132
133 const clearResults = () => {
134 setShowResults(false)
135 setUploadResults([])
136 }
137
138 return (
139 <div className="mb-6 p-4 border border-gray-200 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800">
140 <h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">Upload Files</h2>
141
142 {/* Upload Results */}
143 {showResults && uploadResults.length > 0 && (
144 <div className="mb-4 p-3 bg-white dark:bg-gray-900 rounded border">
145 <div className="flex justify-between items-center mb-2">
146 <h3 className="font-medium text-gray-900 dark:text-gray-100">Upload Results</h3>
147 <button
148 onClick={clearResults}
149 className="text-sm text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
150 >
151 Clear
152 </button>
153 </div>
154 <div className="space-y-1">
155 {uploadResults.map((result, index) => (
156 <div
157 key={index}
158 className={`text-sm flex items-center gap-2 ${
159 result.success
160 ? 'text-green-600 dark:text-green-400'
161 : 'text-red-600 dark:text-red-400'
162 }`}
163 >
164 <span>{result.success ? '✓' : '✗'}</span>
165 <span className="font-medium">{result.filename}:</span>
166 <span>{result.message}</span>
167 </div>
168 ))}
169 </div>
170 </div>
171 )}
172
173 {/* File Drop Zone */}
174 <div
175 className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
176 isDragOver
177 ? 'border-blue-400 bg-blue-50 dark:bg-blue-900/20'
178 : 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500'
179 }`}
180 onDragOver={handleDragOver}
181 onDragLeave={handleDragLeave}
182 onDrop={handleDrop}
183 >
184 <div className="space-y-2">
185 <div className="text-4xl">📁</div>
186 <div className="text-gray-600 dark:text-gray-300">
187 <p className="font-medium">Drop files here or click to browse</p>
188 <p className="text-sm">
189 Maximum {UPLOAD_MAX_FILES} files, {UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB each
190 </p>
191 </div>
192 <input
193 ref={fileInputRef}
194 type="file"
195 multiple
196 className="hidden"
197 onChange={handleFileInputChange}
198 />
199 <button
200 type="button"
201 onClick={() => fileInputRef.current?.click()}
202 disabled={isUploading}
203 className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400 transition-colors"
204 >
205 Browse Files
206 </button>
207 </div>
208 </div>
209
210 {/* Selected Files */}
211 {selectedFiles.length > 0 && (
212 <div className="mt-4">
213 <h3 className="font-medium mb-2 text-gray-900 dark:text-gray-100">
214 Selected Files ({selectedFiles.length})
215 </h3>
216 <div className="space-y-2">
217 {selectedFiles.map((file, index) => (
218 <div
219 key={index}
220 className="flex items-center justify-between p-2 bg-white dark:bg-gray-900 rounded border"
221 >
222 <div className="flex items-center gap-2">
223 <span className="text-gray-400">📄</span>
224 <div>
225 <div className="text-sm font-medium text-gray-900 dark:text-gray-100">
226 {file.name}
227 </div>
228 <div className="text-xs text-gray-500 dark:text-gray-400">
229 {(file.size / 1024 / 1024).toFixed(2)} MB
230 </div>
231 </div>
232 </div>
233 <button
234 onClick={() => removeFile(index)}
235 disabled={isUploading}
236 className="text-red-500 hover:text-red-700 disabled:text-gray-400 text-sm"
237 >
238 Remove
239 </button>
240 </div>
241 ))}
242 </div>
243
244 <button
245 onClick={handleUpload}
246 disabled={isUploading || selectedFiles.length === 0}
247 className="mt-3 px-6 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:bg-gray-400 transition-colors flex items-center gap-2"
248 >
249 {isUploading ? (
250 <>
251 <div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent"></div>
252 Uploading...
253 </>
254 ) : (
255 `Upload ${selectedFiles.length} file${selectedFiles.length > 1 ? 's' : ''}`
256 )}
257 </button>
258 </div>
259 )}
260 </div>
261 )
262} \ No newline at end of file
diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx
new file mode 100644
index 0000000..0205413
--- /dev/null
+++ b/frontend/components/ui/badge.tsx
@@ -0,0 +1,46 @@
1import * as React from "react"
2import { Slot } from "@radix-ui/react-slot"
3import { cva, type VariantProps } from "class-variance-authority"
4
5import { cn } from "@/lib/utils"
6
7const badgeVariants = cva(
8 "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",
9 {
10 variants: {
11 variant: {
12 default:
13 "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 secondary:
15 "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 destructive:
17 "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",
18 outline:
19 "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 },
21 },
22 defaultVariants: {
23 variant: "default",
24 },
25 }
26)
27
28function Badge({
29 className,
30 variant,
31 asChild = false,
32 ...props
33}: React.ComponentProps<"span"> &
34 VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35 const Comp = asChild ? Slot : "span"
36
37 return (
38 <Comp
39 data-slot="badge"
40 className={cn(badgeVariants({ variant }), className)}
41 {...props}
42 />
43 )
44}
45
46export { Badge, badgeVariants }
diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx
new file mode 100644
index 0000000..a2df8dc
--- /dev/null
+++ b/frontend/components/ui/button.tsx
@@ -0,0 +1,59 @@
1import * as React from "react"
2import { Slot } from "@radix-ui/react-slot"
3import { cva, type VariantProps } from "class-variance-authority"
4
5import { cn } from "@/lib/utils"
6
7const buttonVariants = cva(
8 "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",
9 {
10 variants: {
11 variant: {
12 default:
13 "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 destructive:
15 "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",
16 outline:
17 "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 secondary:
19 "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 ghost:
21 "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 link: "text-primary underline-offset-4 hover:underline",
23 },
24 size: {
25 default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 icon: "size-9",
29 },
30 },
31 defaultVariants: {
32 variant: "default",
33 size: "default",
34 },
35 }
36)
37
38function Button({
39 className,
40 variant,
41 size,
42 asChild = false,
43 ...props
44}: React.ComponentProps<"button"> &
45 VariantProps<typeof buttonVariants> & {
46 asChild?: boolean
47 }) {
48 const Comp = asChild ? Slot : "button"
49
50 return (
51 <Comp
52 data-slot="button"
53 className={cn(buttonVariants({ variant, size, className }))}
54 {...props}
55 />
56 )
57}
58
59export { Button, buttonVariants }
diff --git a/frontend/components/ui/checkbox.tsx b/frontend/components/ui/checkbox.tsx
new file mode 100644
index 0000000..b0f1ccf
--- /dev/null
+++ b/frontend/components/ui/checkbox.tsx
@@ -0,0 +1,47 @@
1"use client"
2
3import * as React from "react"
4import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5import { CheckIcon } from "lucide-react"
6
7import { cn } from "@/lib/utils"
8
9const Checkbox = React.forwardRef<
10 React.ElementRef<typeof CheckboxPrimitive.Root>,
11 React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> & {
12 indeterminate?: boolean
13 }
14>(({ className, indeterminate, ...props }, ref) => {
15 const checkboxRef = React.useRef<HTMLButtonElement>(null)
16
17 React.useImperativeHandle(ref, () => checkboxRef.current!)
18
19 React.useEffect(() => {
20 if (checkboxRef.current) {
21 checkboxRef.current.indeterminate = indeterminate ?? false
22 }
23 }, [indeterminate])
24
25 return (
26 <CheckboxPrimitive.Root
27 ref={checkboxRef}
28 data-slot="checkbox"
29 className={cn(
30 "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
31 className
32 )}
33 {...props}
34 >
35 <CheckboxPrimitive.Indicator
36 data-slot="checkbox-indicator"
37 className="flex items-center justify-center text-current transition-none"
38 >
39 <CheckIcon className="size-3.5" />
40 </CheckboxPrimitive.Indicator>
41 </CheckboxPrimitive.Root>
42 )
43})
44
45Checkbox.displayName = "Checkbox"
46
47export { Checkbox }
diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx
new file mode 100644
index 0000000..d9ccec9
--- /dev/null
+++ b/frontend/components/ui/dialog.tsx
@@ -0,0 +1,143 @@
1"use client"
2
3import * as React from "react"
4import * as DialogPrimitive from "@radix-ui/react-dialog"
5import { XIcon } from "lucide-react"
6
7import { cn } from "@/lib/utils"
8
9function Dialog({
10 ...props
11}: React.ComponentProps<typeof DialogPrimitive.Root>) {
12 return <DialogPrimitive.Root data-slot="dialog" {...props} />
13}
14
15function DialogTrigger({
16 ...props
17}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18 return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
19}
20
21function DialogPortal({
22 ...props
23}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24 return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
25}
26
27function DialogClose({
28 ...props
29}: React.ComponentProps<typeof DialogPrimitive.Close>) {
30 return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
31}
32
33function DialogOverlay({
34 className,
35 ...props
36}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37 return (
38 <DialogPrimitive.Overlay
39 data-slot="dialog-overlay"
40 className={cn(
41 "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
42 className
43 )}
44 {...props}
45 />
46 )
47}
48
49function DialogContent({
50 className,
51 children,
52 showCloseButton = true,
53 ...props
54}: React.ComponentProps<typeof DialogPrimitive.Content> & {
55 showCloseButton?: boolean
56}) {
57 return (
58 <DialogPortal data-slot="dialog-portal">
59 <DialogOverlay />
60 <DialogPrimitive.Content
61 data-slot="dialog-content"
62 className={cn(
63 "bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
64 className
65 )}
66 {...props}
67 >
68 {children}
69 {showCloseButton && (
70 <DialogPrimitive.Close
71 data-slot="dialog-close"
72 className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
73 >
74 <XIcon />
75 <span className="sr-only">Close</span>
76 </DialogPrimitive.Close>
77 )}
78 </DialogPrimitive.Content>
79 </DialogPortal>
80 )
81}
82
83function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
84 return (
85 <div
86 data-slot="dialog-header"
87 className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
88 {...props}
89 />
90 )
91}
92
93function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
94 return (
95 <div
96 data-slot="dialog-footer"
97 className={cn(
98 "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
99 className
100 )}
101 {...props}
102 />
103 )
104}
105
106function DialogTitle({
107 className,
108 ...props
109}: React.ComponentProps<typeof DialogPrimitive.Title>) {
110 return (
111 <DialogPrimitive.Title
112 data-slot="dialog-title"
113 className={cn("text-lg leading-none font-semibold", className)}
114 {...props}
115 />
116 )
117}
118
119function DialogDescription({
120 className,
121 ...props
122}: React.ComponentProps<typeof DialogPrimitive.Description>) {
123 return (
124 <DialogPrimitive.Description
125 data-slot="dialog-description"
126 className={cn("text-muted-foreground text-sm", className)}
127 {...props}
128 />
129 )
130}
131
132export {
133 Dialog,
134 DialogClose,
135 DialogContent,
136 DialogDescription,
137 DialogFooter,
138 DialogHeader,
139 DialogOverlay,
140 DialogPortal,
141 DialogTitle,
142 DialogTrigger,
143}
diff --git a/frontend/components/ui/dropdown-menu.tsx b/frontend/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..ec51e9c
--- /dev/null
+++ b/frontend/components/ui/dropdown-menu.tsx
@@ -0,0 +1,257 @@
1"use client"
2
3import * as React from "react"
4import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
6
7import { cn } from "@/lib/utils"
8
9function DropdownMenu({
10 ...props
11}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12 return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
13}
14
15function DropdownMenuPortal({
16 ...props
17}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18 return (
19 <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20 )
21}
22
23function DropdownMenuTrigger({
24 ...props
25}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26 return (
27 <DropdownMenuPrimitive.Trigger
28 data-slot="dropdown-menu-trigger"
29 {...props}
30 />
31 )
32}
33
34function DropdownMenuContent({
35 className,
36 sideOffset = 4,
37 ...props
38}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
39 return (
40 <DropdownMenuPrimitive.Portal>
41 <DropdownMenuPrimitive.Content
42 data-slot="dropdown-menu-content"
43 sideOffset={sideOffset}
44 className={cn(
45 "bg-popover text-popover-foreground 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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
46 className
47 )}
48 {...props}
49 />
50 </DropdownMenuPrimitive.Portal>
51 )
52}
53
54function DropdownMenuGroup({
55 ...props
56}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
57 return (
58 <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
59 )
60}
61
62function DropdownMenuItem({
63 className,
64 inset,
65 variant = "default",
66 ...props
67}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
68 inset?: boolean
69 variant?: "default" | "destructive"
70}) {
71 return (
72 <DropdownMenuPrimitive.Item
73 data-slot="dropdown-menu-item"
74 data-inset={inset}
75 data-variant={variant}
76 className={cn(
77 "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
78 className
79 )}
80 {...props}
81 />
82 )
83}
84
85function DropdownMenuCheckboxItem({
86 className,
87 children,
88 checked,
89 ...props
90}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
91 return (
92 <DropdownMenuPrimitive.CheckboxItem
93 data-slot="dropdown-menu-checkbox-item"
94 className={cn(
95 "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96 className
97 )}
98 checked={checked}
99 {...props}
100 >
101 <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102 <DropdownMenuPrimitive.ItemIndicator>
103 <CheckIcon className="size-4" />
104 </DropdownMenuPrimitive.ItemIndicator>
105 </span>
106 {children}
107 </DropdownMenuPrimitive.CheckboxItem>
108 )
109}
110
111function DropdownMenuRadioGroup({
112 ...props
113}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
114 return (
115 <DropdownMenuPrimitive.RadioGroup
116 data-slot="dropdown-menu-radio-group"
117 {...props}
118 />
119 )
120}
121
122function DropdownMenuRadioItem({
123 className,
124 children,
125 ...props
126}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
127 return (
128 <DropdownMenuPrimitive.RadioItem
129 data-slot="dropdown-menu-radio-item"
130 className={cn(
131 "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132 className
133 )}
134 {...props}
135 >
136 <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137 <DropdownMenuPrimitive.ItemIndicator>
138 <CircleIcon className="size-2 fill-current" />
139 </DropdownMenuPrimitive.ItemIndicator>
140 </span>
141 {children}
142 </DropdownMenuPrimitive.RadioItem>
143 )
144}
145
146function DropdownMenuLabel({
147 className,
148 inset,
149 ...props
150}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
151 inset?: boolean
152}) {
153 return (
154 <DropdownMenuPrimitive.Label
155 data-slot="dropdown-menu-label"
156 data-inset={inset}
157 className={cn(
158 "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
159 className
160 )}
161 {...props}
162 />
163 )
164}
165
166function DropdownMenuSeparator({
167 className,
168 ...props
169}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
170 return (
171 <DropdownMenuPrimitive.Separator
172 data-slot="dropdown-menu-separator"
173 className={cn("bg-border -mx-1 my-1 h-px", className)}
174 {...props}
175 />
176 )
177}
178
179function DropdownMenuShortcut({
180 className,
181 ...props
182}: React.ComponentProps<"span">) {
183 return (
184 <span
185 data-slot="dropdown-menu-shortcut"
186 className={cn(
187 "text-muted-foreground ml-auto text-xs tracking-widest",
188 className
189 )}
190 {...props}
191 />
192 )
193}
194
195function DropdownMenuSub({
196 ...props
197}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198 return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
199}
200
201function DropdownMenuSubTrigger({
202 className,
203 inset,
204 children,
205 ...props
206}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
207 inset?: boolean
208}) {
209 return (
210 <DropdownMenuPrimitive.SubTrigger
211 data-slot="dropdown-menu-sub-trigger"
212 data-inset={inset}
213 className={cn(
214 "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
215 className
216 )}
217 {...props}
218 >
219 {children}
220 <ChevronRightIcon className="ml-auto size-4" />
221 </DropdownMenuPrimitive.SubTrigger>
222 )
223}
224
225function DropdownMenuSubContent({
226 className,
227 ...props
228}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
229 return (
230 <DropdownMenuPrimitive.SubContent
231 data-slot="dropdown-menu-sub-content"
232 className={cn(
233 "bg-popover text-popover-foreground 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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
234 className
235 )}
236 {...props}
237 />
238 )
239}
240
241export {
242 DropdownMenu,
243 DropdownMenuPortal,
244 DropdownMenuTrigger,
245 DropdownMenuContent,
246 DropdownMenuGroup,
247 DropdownMenuLabel,
248 DropdownMenuItem,
249 DropdownMenuCheckboxItem,
250 DropdownMenuRadioGroup,
251 DropdownMenuRadioItem,
252 DropdownMenuSeparator,
253 DropdownMenuShortcut,
254 DropdownMenuSub,
255 DropdownMenuSubTrigger,
256 DropdownMenuSubContent,
257}
diff --git a/frontend/components/ui/input.tsx b/frontend/components/ui/input.tsx
new file mode 100644
index 0000000..03295ca
--- /dev/null
+++ b/frontend/components/ui/input.tsx
@@ -0,0 +1,21 @@
1import * as React from "react"
2
3import { cn } from "@/lib/utils"
4
5function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 return (
7 <input
8 type={type}
9 data-slot="input"
10 className={cn(
11 "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12 "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13 "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14 className
15 )}
16 {...props}
17 />
18 )
19}
20
21export { Input }
diff --git a/frontend/components/ui/label.tsx b/frontend/components/ui/label.tsx
new file mode 100644
index 0000000..fb5fbc3
--- /dev/null
+++ b/frontend/components/ui/label.tsx
@@ -0,0 +1,24 @@
1"use client"
2
3import * as React from "react"
4import * as LabelPrimitive from "@radix-ui/react-label"
5
6import { cn } from "@/lib/utils"
7
8function Label({
9 className,
10 ...props
11}: React.ComponentProps<typeof LabelPrimitive.Root>) {
12 return (
13 <LabelPrimitive.Root
14 data-slot="label"
15 className={cn(
16 "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17 className
18 )}
19 {...props}
20 />
21 )
22}
23
24export { Label }
diff --git a/frontend/components/ui/progress.tsx b/frontend/components/ui/progress.tsx
new file mode 100644
index 0000000..e7a416c
--- /dev/null
+++ b/frontend/components/ui/progress.tsx
@@ -0,0 +1,31 @@
1"use client"
2
3import * as React from "react"
4import * as ProgressPrimitive from "@radix-ui/react-progress"
5
6import { cn } from "@/lib/utils"
7
8function Progress({
9 className,
10 value,
11 ...props
12}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
13 return (
14 <ProgressPrimitive.Root
15 data-slot="progress"
16 className={cn(
17 "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
18 className
19 )}
20 {...props}
21 >
22 <ProgressPrimitive.Indicator
23 data-slot="progress-indicator"
24 className="bg-primary h-full w-full flex-1 transition-all"
25 style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26 />
27 </ProgressPrimitive.Root>
28 )
29}
30
31export { Progress }
diff --git a/frontend/components/ui/table.tsx b/frontend/components/ui/table.tsx
new file mode 100644
index 0000000..51b74dd
--- /dev/null
+++ b/frontend/components/ui/table.tsx
@@ -0,0 +1,116 @@
1"use client"
2
3import * as React from "react"
4
5import { cn } from "@/lib/utils"
6
7function Table({ className, ...props }: React.ComponentProps<"table">) {
8 return (
9 <div
10 data-slot="table-container"
11 className="relative w-full overflow-x-auto"
12 >
13 <table
14 data-slot="table"
15 className={cn("w-full caption-bottom text-sm", className)}
16 {...props}
17 />
18 </div>
19 )
20}
21
22function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
23 return (
24 <thead
25 data-slot="table-header"
26 className={cn("[&_tr]:border-b", className)}
27 {...props}
28 />
29 )
30}
31
32function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
33 return (
34 <tbody
35 data-slot="table-body"
36 className={cn("[&_tr:last-child]:border-0", className)}
37 {...props}
38 />
39 )
40}
41
42function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
43 return (
44 <tfoot
45 data-slot="table-footer"
46 className={cn(
47 "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
48 className
49 )}
50 {...props}
51 />
52 )
53}
54
55function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 return (
57 <tr
58 data-slot="table-row"
59 className={cn(
60 "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
61 className
62 )}
63 {...props}
64 />
65 )
66}
67
68function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 return (
70 <th
71 data-slot="table-head"
72 className={cn(
73 "text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
74 className
75 )}
76 {...props}
77 />
78 )
79}
80
81function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 return (
83 <td
84 data-slot="table-cell"
85 className={cn(
86 "p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
87 className
88 )}
89 {...props}
90 />
91 )
92}
93
94function TableCaption({
95 className,
96 ...props
97}: React.ComponentProps<"caption">) {
98 return (
99 <caption
100 data-slot="table-caption"
101 className={cn("text-muted-foreground mt-4 text-sm", className)}
102 {...props}
103 />
104 )
105}
106
107export {
108 Table,
109 TableHeader,
110 TableBody,
111 TableFooter,
112 TableHead,
113 TableRow,
114 TableCell,
115 TableCaption,
116}
diff --git a/frontend/components/ui/toast.tsx b/frontend/components/ui/toast.tsx
new file mode 100644
index 0000000..6d2e12f
--- /dev/null
+++ b/frontend/components/ui/toast.tsx
@@ -0,0 +1,129 @@
1"use client"
2
3import * as React from "react"
4import * as ToastPrimitives from "@radix-ui/react-toast"
5import { cva, type VariantProps } from "class-variance-authority"
6import { X } from "lucide-react"
7
8import { cn } from "@/lib/utils"
9
10const ToastProvider = ToastPrimitives.Provider
11
12const ToastViewport = React.forwardRef<
13 React.ElementRef<typeof ToastPrimitives.Viewport>,
14 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
15>(({ className, ...props }, ref) => (
16 <ToastPrimitives.Viewport
17 ref={ref}
18 className={cn(
19 "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
20 className
21 )}
22 {...props}
23 />
24))
25ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26
27const toastVariants = cva(
28 "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 {
30 variants: {
31 variant: {
32 default: "border bg-background text-foreground",
33 destructive:
34 "destructive border-destructive bg-destructive text-destructive-foreground",
35 },
36 },
37 defaultVariants: {
38 variant: "default",
39 },
40 }
41)
42
43const Toast = React.forwardRef<
44 React.ElementRef<typeof ToastPrimitives.Root>,
45 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
46 VariantProps<typeof toastVariants>
47>(({ className, variant, ...props }, ref) => {
48 return (
49 <ToastPrimitives.Root
50 ref={ref}
51 className={cn(toastVariants({ variant }), className)}
52 {...props}
53 />
54 )
55})
56Toast.displayName = ToastPrimitives.Root.displayName
57
58const ToastAction = React.forwardRef<
59 React.ElementRef<typeof ToastPrimitives.Action>,
60 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
61>(({ className, ...props }, ref) => (
62 <ToastPrimitives.Action
63 ref={ref}
64 className={cn(
65 "inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
66 className
67 )}
68 {...props}
69 />
70))
71ToastAction.displayName = ToastPrimitives.Action.displayName
72
73const ToastClose = React.forwardRef<
74 React.ElementRef<typeof ToastPrimitives.Close>,
75 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
76>(({ className, ...props }, ref) => (
77 <ToastPrimitives.Close
78 ref={ref}
79 className={cn(
80 "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
81 className
82 )}
83 toast-close=""
84 {...props}
85 >
86 <X className="h-4 w-4" />
87 </ToastPrimitives.Close>
88))
89ToastClose.displayName = ToastPrimitives.Close.displayName
90
91const ToastTitle = React.forwardRef<
92 React.ElementRef<typeof ToastPrimitives.Title>,
93 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
94>(({ className, ...props }, ref) => (
95 <ToastPrimitives.Title
96 ref={ref}
97 className={cn("text-sm font-semibold", className)}
98 {...props}
99 />
100))
101ToastTitle.displayName = ToastPrimitives.Title.displayName
102
103const ToastDescription = React.forwardRef<
104 React.ElementRef<typeof ToastPrimitives.Description>,
105 React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
106>(({ className, ...props }, ref) => (
107 <ToastPrimitives.Description
108 ref={ref}
109 className={cn("text-sm opacity-90", className)}
110 {...props}
111 />
112))
113ToastDescription.displayName = ToastPrimitives.Description.displayName
114
115type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
116
117type ToastActionElement = React.ReactElement<typeof ToastAction>
118
119export {
120 type ToastProps,
121 type ToastActionElement,
122 ToastProvider,
123 ToastViewport,
124 Toast,
125 ToastTitle,
126 ToastDescription,
127 ToastClose,
128 ToastAction,
129} \ No newline at end of file
diff --git a/frontend/components/ui/toaster.tsx b/frontend/components/ui/toaster.tsx
new file mode 100644
index 0000000..b5b97f6
--- /dev/null
+++ b/frontend/components/ui/toaster.tsx
@@ -0,0 +1,35 @@
1"use client"
2
3import {
4 Toast,
5 ToastClose,
6 ToastDescription,
7 ToastProvider,
8 ToastTitle,
9 ToastViewport,
10} from "@/components/ui/toast"
11import { useToast } from "@/hooks/use-toast"
12
13export function Toaster() {
14 const { toasts } = useToast()
15
16 return (
17 <ToastProvider>
18 {toasts.map(function ({ id, title, description, action, ...props }) {
19 return (
20 <Toast key={id} {...props}>
21 <div className="grid gap-1">
22 {title && <ToastTitle>{title}</ToastTitle>}
23 {description && (
24 <ToastDescription>{description}</ToastDescription>
25 )}
26 </div>
27 {action}
28 <ToastClose />
29 </Toast>
30 )
31 })}
32 <ToastViewport />
33 </ToastProvider>
34 )
35} \ No newline at end of file
diff --git a/frontend/file-drive.tsx b/frontend/file-drive.tsx
new file mode 100644
index 0000000..b38548a
--- /dev/null
+++ b/frontend/file-drive.tsx
@@ -0,0 +1,936 @@
1"use client"
2
3import type React from "react"
4
5import { useState, useRef } from "react"
6import {
7 ChevronRight,
8 ChevronDown,
9 File,
10 Folder,
11 Upload,
12 Trash2,
13 Move,
14 MoreHorizontal,
15 HardDrive,
16 Edit,
17 Link,
18 Info,
19 LogIn,
20 LogOut,
21 HistoryIcon,
22} from "lucide-react"
23import { Button } from "@/components/ui/button"
24import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
25import { Progress } from "@/components/ui/progress"
26import {
27 DropdownMenu,
28 DropdownMenuContent,
29 DropdownMenuItem,
30 DropdownMenuTrigger,
31 DropdownMenuSeparator,
32} from "@/components/ui/dropdown-menu"
33import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
34import { Input } from "@/components/ui/input"
35import { Label } from "@/components/ui/label"
36import { Checkbox } from "@/components/ui/checkbox"
37import { toast } from "@/hooks/use-toast"
38import HistoryView from "./history-view"
39
40interface FileItem {
41 id: string
42 name: string
43 type: "file" | "folder"
44 size: number
45 modified: string
46 modifiedTime: string
47 modifiedBy: string
48 children?: FileItem[]
49}
50
51const mockData: FileItem[] = [
52 {
53 id: "1",
54 name: "Documents and Important Files",
55 type: "folder",
56 size: 25728640,
57 modified: "2024-01-15",
58 modifiedTime: "14:32",
59 modifiedBy: "John Smith",
60 children: [
61 {
62 id: "1-1",
63 name: "Work Projects and Client Reports",
64 type: "folder",
65 size: 18388608,
66 modified: "2024-01-10",
67 modifiedTime: "09:15",
68 modifiedBy: "Sarah Johnson",
69 children: [
70 {
71 id: "1-1-1",
72 name: "Client ABC Corporation",
73 type: "folder",
74 size: 12388608,
75 modified: "2024-01-09",
76 modifiedTime: "16:45",
77 modifiedBy: "Mike Davis",
78 children: [
79 {
80 id: "1-1-1-1",
81 name: "Quarterly Financial Analysis and Market Research Report Q4 2023.pdf",
82 type: "file",
83 size: 4194304,
84 modified: "2024-01-09",
85 modifiedTime: "16:30",
86 modifiedBy: "Mike Davis",
87 },
88 {
89 id: "1-1-1-2",
90 name: "Strategic Business Development Plan and Implementation Timeline.docx",
91 type: "file",
92 size: 2097152,
93 modified: "2024-01-08",
94 modifiedTime: "11:20",
95 modifiedBy: "Emily Chen",
96 },
97 {
98 id: "1-1-1-3",
99 name: "Meeting Notes and Action Items",
100 type: "folder",
101 size: 6097152,
102 modified: "2024-01-07",
103 modifiedTime: "13:45",
104 modifiedBy: "Alex Rodriguez",
105 children: [
106 {
107 id: "1-1-1-3-1",
108 name: "Weekly Status Meeting Notes January 2024 - Detailed Summary.txt",
109 type: "file",
110 size: 1048576,
111 modified: "2024-01-07",
112 modifiedTime: "13:30",
113 modifiedBy: "Alex Rodriguez",
114 },
115 {
116 id: "1-1-1-3-2",
117 name: "Project Kickoff Meeting Transcript and Stakeholder Feedback.docx",
118 type: "file",
119 size: 3048576,
120 modified: "2024-01-06",
121 modifiedTime: "15:10",
122 modifiedBy: "Lisa Wang",
123 },
124 {
125 id: "1-1-1-3-3",
126 name: "Budget Review and Resource Allocation Discussion Points.xlsx",
127 type: "file",
128 size: 2000000,
129 modified: "2024-01-05",
130 modifiedTime: "10:25",
131 modifiedBy: "David Brown",
132 },
133 ],
134 },
135 ],
136 },
137 {
138 id: "1-1-2",
139 name: "Internal Company Documentation and Policies",
140 type: "folder",
141 size: 6000000,
142 modified: "2024-01-08",
143 modifiedTime: "08:30",
144 modifiedBy: "HR Department",
145 children: [
146 {
147 id: "1-1-2-1",
148 name: "Employee Handbook 2024 - Complete Guide with Benefits and Procedures.pdf",
149 type: "file",
150 size: 3500000,
151 modified: "2024-01-08",
152 modifiedTime: "08:15",
153 modifiedBy: "HR Department",
154 },
155 {
156 id: "1-1-2-2",
157 name: "IT Security Policies and Data Protection Guidelines - Updated Version.docx",
158 type: "file",
159 size: 2500000,
160 modified: "2024-01-07",
161 modifiedTime: "17:40",
162 modifiedBy: "IT Security Team",
163 },
164 ],
165 },
166 ],
167 },
168 {
169 id: "1-2",
170 name: "Personal Documents and Certificates",
171 type: "folder",
172 size: 7340032,
173 modified: "2024-01-12",
174 modifiedTime: "12:15",
175 modifiedBy: "John Smith",
176 children: [
177 {
178 id: "1-2-1",
179 name: "Professional Resume and Cover Letter Templates - Updated 2024.docx",
180 type: "file",
181 size: 1048576,
182 modified: "2024-01-12",
183 modifiedTime: "12:00",
184 modifiedBy: "John Smith",
185 },
186 {
187 id: "1-2-2",
188 name: "Educational Certificates and Professional Qualifications Portfolio.pdf",
189 type: "file",
190 size: 6291456,
191 modified: "2024-01-11",
192 modifiedTime: "14:30",
193 modifiedBy: "John Smith",
194 },
195 ],
196 },
197 ],
198 },
199 {
200 id: "2",
201 name: "Media Files and Creative Assets",
202 type: "folder",
203 size: 152428800,
204 modified: "2024-01-14",
205 modifiedTime: "16:20",
206 modifiedBy: "Creative Team",
207 children: [
208 {
209 id: "2-1",
210 name: "Photography and Visual Content",
211 type: "folder",
212 size: 75428800,
213 modified: "2024-01-14",
214 modifiedTime: "16:15",
215 modifiedBy: "Creative Team",
216 children: [
217 {
218 id: "2-1-1",
219 name: "Travel and Vacation Photos Collection",
220 type: "folder",
221 size: 45428800,
222 modified: "2024-01-14",
223 modifiedTime: "16:10",
224 modifiedBy: "John Smith",
225 children: [
226 {
227 id: "2-1-1-1",
228 name: "Summer Vacation 2023 - Beach Resort and Mountain Hiking Adventure Photos.jpg",
229 type: "file",
230 size: 15145728,
231 modified: "2024-01-14",
232 modifiedTime: "16:05",
233 modifiedBy: "John Smith",
234 },
235 {
236 id: "2-1-1-2",
237 name: "European City Tour - Architecture and Cultural Landmarks Photography Collection.jpg",
238 type: "file",
239 size: 18283072,
240 modified: "2024-01-13",
241 modifiedTime: "19:30",
242 modifiedBy: "John Smith",
243 },
244 {
245 id: "2-1-1-3",
246 name: "Wildlife Photography Safari - African Animals and Natural Landscapes.jpg",
247 type: "file",
248 size: 12000000,
249 modified: "2024-01-12",
250 modifiedTime: "20:45",
251 modifiedBy: "John Smith",
252 },
253 ],
254 },
255 {
256 id: "2-1-2",
257 name: "Professional Headshots and Corporate Event Photography - High Resolution.png",
258 type: "file",
259 size: 30000000,
260 modified: "2024-01-13",
261 modifiedTime: "11:15",
262 modifiedBy: "Professional Photographer",
263 },
264 ],
265 },
266 {
267 id: "2-2",
268 name: "Video Content and Multimedia Projects",
269 type: "folder",
270 size: 77000000,
271 modified: "2024-01-13",
272 modifiedTime: "14:20",
273 modifiedBy: "Video Production Team",
274 children: [
275 {
276 id: "2-2-1",
277 name: "Corporate Training Videos and Educational Content Series - Complete Collection.mp4",
278 type: "file",
279 size: 45000000,
280 modified: "2024-01-13",
281 modifiedTime: "14:15",
282 modifiedBy: "Video Production Team",
283 },
284 {
285 id: "2-2-2",
286 name: "Product Demo and Marketing Presentation Video - 4K Quality.mov",
287 type: "file",
288 size: 32000000,
289 modified: "2024-01-12",
290 modifiedTime: "16:30",
291 modifiedBy: "Marketing Team",
292 },
293 ],
294 },
295 ],
296 },
297 {
298 id: "3",
299 name: "Development Projects and Source Code Repository",
300 type: "folder",
301 size: 89715200,
302 modified: "2024-01-16",
303 modifiedTime: "18:45",
304 modifiedBy: "Development Team",
305 children: [
306 {
307 id: "3-1",
308 name: "Web Applications and Frontend Projects",
309 type: "folder",
310 size: 45000000,
311 modified: "2024-01-16",
312 modifiedTime: "18:40",
313 modifiedBy: "Frontend Team",
314 children: [
315 {
316 id: "3-1-1",
317 name: "E-commerce Platform with React and Node.js - Full Stack Implementation.zip",
318 type: "file",
319 size: 25000000,
320 modified: "2024-01-16",
321 modifiedTime: "18:35",
322 modifiedBy: "Lead Developer",
323 },
324 {
325 id: "3-1-2",
326 name: "Dashboard Analytics Tool with Real-time Data Visualization Components.zip",
327 type: "file",
328 size: 20000000,
329 modified: "2024-01-15",
330 modifiedTime: "22:10",
331 modifiedBy: "Data Team",
332 },
333 ],
334 },
335 {
336 id: "3-2",
337 name: "Mobile App Development and Cross-Platform Solutions.zip",
338 type: "file",
339 size: 44715200,
340 modified: "2024-01-14",
341 modifiedTime: "13:25",
342 modifiedBy: "Mobile Team",
343 },
344 ],
345 },
346 {
347 id: "4",
348 name: "Configuration Files and System Settings - Development Environment Setup.txt",
349 type: "file",
350 size: 4096,
351 modified: "2024-01-16",
352 modifiedTime: "09:30",
353 modifiedBy: "System Admin",
354 },
355 {
356 id: "5",
357 name: "Database Backup and Migration Scripts - Production Environment.sql",
358 type: "file",
359 size: 8192,
360 modified: "2024-01-15",
361 modifiedTime: "23:45",
362 modifiedBy: "Database Admin",
363 },
364]
365
366function formatFileSize(bytes: number): string {
367 if (bytes === 0) return "0 Bytes"
368 const k = 1024
369 const sizes = ["Bytes", "KB", "MB", "GB"]
370 const i = Math.floor(Math.log(bytes) / Math.log(k))
371 return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
372}
373
374function calculateTotalSize(items: FileItem[]): number {
375 return items.reduce((total, item) => {
376 if (item.type === "folder" && item.children) {
377 return total + calculateTotalSize(item.children)
378 }
379 return total + item.size
380 }, 0)
381}
382
383export default function FileDrive() {
384 const [files, setFiles] = useState<FileItem[]>(mockData)
385 const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set())
386 const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set())
387 const [renameDialogOpen, setRenameDialogOpen] = useState(false)
388 const [infoDialogOpen, setInfoDialogOpen] = useState(false)
389 const [currentItem, setCurrentItem] = useState<FileItem | null>(null)
390 const [newName, setNewName] = useState("")
391 const fileInputRef = useRef<HTMLInputElement>(null)
392
393 const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state
394 const [currentView, setCurrentView] = useState<"drive" | "history">("drive")
395 const [uploadToFolder, setUploadToFolder] = useState<string | null>(null)
396
397 const maxStorage = 1073741824 // 1GB
398 const usedStorage = calculateTotalSize(files)
399 const storagePercentage = (usedStorage / maxStorage) * 100
400
401 const toggleFolder = (folderId: string) => {
402 const newExpanded = new Set(expandedFolders)
403 if (newExpanded.has(folderId)) {
404 newExpanded.delete(folderId)
405 } else {
406 newExpanded.add(folderId)
407 }
408 setExpandedFolders(newExpanded)
409 }
410
411 const toggleFileSelection = (fileId: string) => {
412 const newSelected = new Set(selectedFiles)
413 if (newSelected.has(fileId)) {
414 newSelected.delete(fileId)
415 } else {
416 newSelected.add(fileId)
417 }
418 setSelectedFiles(newSelected)
419 }
420
421 const selectAll = () => {
422 const getAllIds = (items: FileItem[]): string[] => {
423 const ids: string[] = []
424 items.forEach((item) => {
425 ids.push(item.id)
426 if (item.children) {
427 ids.push(...getAllIds(item.children))
428 }
429 })
430 return ids
431 }
432 setSelectedFiles(new Set(getAllIds(files)))
433 }
434
435 const deselectAll = () => {
436 setSelectedFiles(new Set())
437 }
438
439 const openRenameDialog = (item: FileItem) => {
440 setCurrentItem(item)
441 setNewName(item.name)
442 setRenameDialogOpen(true)
443 }
444
445 const openInfoDialog = (item: FileItem) => {
446 setCurrentItem(item)
447 setInfoDialogOpen(true)
448 }
449
450 const copyPermalink = (item: FileItem) => {
451 const permalink = `${window.location.origin}/drive/file/${item.id}`
452 navigator.clipboard.writeText(permalink).then(() => {
453 toast({
454 title: "Link copied!",
455 description: "Permalink has been copied to clipboard",
456 })
457 })
458 }
459
460 const handleRename = () => {
461 if (currentItem && newName.trim()) {
462 const renameInArray = (items: FileItem[]): FileItem[] => {
463 return items.map((item) => {
464 if (item.id === currentItem.id) {
465 return { ...item, name: newName.trim() }
466 }
467 if (item.children) {
468 return { ...item, children: renameInArray(item.children) }
469 }
470 return item
471 })
472 }
473 setFiles(renameInArray(files))
474 setRenameDialogOpen(false)
475 setCurrentItem(null)
476 setNewName("")
477 toast({
478 title: "Renamed successfully",
479 description: `Item renamed to "${newName.trim()}"`,
480 })
481 }
482 }
483
484 const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
485 const uploadedFiles = event.target.files
486 if (uploadedFiles) {
487 const newFiles = Array.from(uploadedFiles).map((file, index) => ({
488 id: `upload-${Date.now()}-${index}`,
489 name: file.name,
490 type: "file" as const,
491 size: file.size,
492 modified: new Date().toISOString().split("T")[0],
493 modifiedTime: new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }),
494 modifiedBy: "Current User",
495 }))
496 setFiles([...files, ...newFiles])
497 }
498 }
499
500 const deleteItems = (itemIds: string[]) => {
501 const deleteFromArray = (items: FileItem[]): FileItem[] => {
502 return items.filter((item) => {
503 if (itemIds.includes(item.id)) return false
504 if (item.children) {
505 item.children = deleteFromArray(item.children)
506 }
507 return true
508 })
509 }
510 setFiles(deleteFromArray(files))
511 // Remove deleted items from selection
512 const newSelected = new Set(selectedFiles)
513 itemIds.forEach((id) => newSelected.delete(id))
514 setSelectedFiles(newSelected)
515 }
516
517 const handleLogin = () => {
518 // Redirect to external auth page (configured via env var)
519 const authUrl = process.env.NEXT_PUBLIC_AUTH_URL || "/auth/login"
520 window.location.href = authUrl
521 }
522
523 const handleLogout = () => {
524 // Handle logout (would typically clear tokens, etc.)
525 setIsLoggedIn(false)
526 // Could also redirect to logout endpoint
527 }
528
529 const handleFolderUpload = (event: React.ChangeEvent<HTMLInputElement>, folderId: string) => {
530 const uploadedFiles = event.target.files
531 if (uploadedFiles) {
532 const newFiles = Array.from(uploadedFiles).map((file, index) => ({
533 id: `upload-${Date.now()}-${index}`,
534 name: file.name,
535 type: "file" as const,
536 size: file.size,
537 modified: new Date().toISOString().split("T")[0],
538 modifiedTime: new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }),
539 modifiedBy: "Current User",
540 }))
541
542 // Add files to the specific folder
543 const addToFolder = (items: FileItem[]): FileItem[] => {
544 return items.map((item) => {
545 if (item.id === folderId && item.type === "folder") {
546 return {
547 ...item,
548 children: [...(item.children || []), ...newFiles],
549 size: item.size + newFiles.reduce((total, file) => total + file.size, 0),
550 }
551 }
552 if (item.children) {
553 return { ...item, children: addToFolder(item.children) }
554 }
555 return item
556 })
557 }
558
559 setFiles(addToFolder(files))
560 toast({
561 title: "Files uploaded successfully",
562 description: `${newFiles.length} file(s) uploaded to folder`,
563 })
564 }
565 // Reset the input
566 event.target.value = ""
567 setUploadToFolder(null)
568 }
569
570 const openFolderUpload = (folderId: string) => {
571 setUploadToFolder(folderId)
572 // Trigger file input click after state is set
573 setTimeout(() => {
574 const input = document.getElementById(`folder-upload-${folderId}`) as HTMLInputElement
575 input?.click()
576 }, 0)
577 }
578
579 const renderFileRow = (item: FileItem, level = 0): React.ReactNode[] => {
580 const isExpanded = expandedFolders.has(item.id)
581 const isSelected = selectedFiles.has(item.id)
582 const rows: React.ReactNode[] = []
583
584 rows.push(
585 <TableRow key={item.id} className={`hover:bg-muted/50 ${isSelected ? "bg-muted/30" : ""}`}>
586 <TableCell className="w-[40px]">
587 <Checkbox checked={isSelected} onCheckedChange={() => toggleFileSelection(item.id)} />
588 </TableCell>
589 <TableCell className="font-medium">
590 <div className="flex items-center gap-2" style={{ paddingLeft: `${level * 20}px` }}>
591 {item.type === "folder" && (
592 <Button variant="ghost" size="sm" className="h-4 w-4 p-0" onClick={() => toggleFolder(item.id)}>
593 {isExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
594 </Button>
595 )}
596 {item.type === "folder" ? (
597 <Folder className="h-4 w-4 text-blue-500" />
598 ) : (
599 <File className="h-4 w-4 text-gray-500" />
600 )}
601 <span>{item.name}</span>
602 </div>
603 </TableCell>
604 <TableCell>{formatFileSize(item.size)}</TableCell>
605 <TableCell>{item.modified}</TableCell>
606 <TableCell>
607 <DropdownMenu>
608 <DropdownMenuTrigger asChild>
609 <Button variant="ghost" size="sm" className="h-8 w-8 p-0">
610 <MoreHorizontal className="h-4 w-4" />
611 </Button>
612 </DropdownMenuTrigger>
613 <DropdownMenuContent align="end">
614 {item.type === "folder" && (
615 <>
616 <DropdownMenuItem onClick={() => openFolderUpload(item.id)}>
617 <Upload className="mr-2 h-4 w-4" />
618 Upload to Folder
619 </DropdownMenuItem>
620 <DropdownMenuSeparator />
621 </>
622 )}
623 <DropdownMenuItem onClick={() => openRenameDialog(item)}>
624 <Edit className="mr-2 h-4 w-4" />
625 Rename
626 </DropdownMenuItem>
627 <DropdownMenuItem onClick={() => copyPermalink(item)}>
628 <Link className="mr-2 h-4 w-4" />
629 Copy Permalink
630 </DropdownMenuItem>
631 <DropdownMenuItem onClick={() => openInfoDialog(item)}>
632 <Info className="mr-2 h-4 w-4" />
633 Info
634 </DropdownMenuItem>
635 <DropdownMenuSeparator />
636 <DropdownMenuItem
637 onClick={() => {
638 if (selectedFiles.size > 0) {
639 console.log("Moving selected files to:", item.type === "folder" ? item.id : "parent of " + item.id)
640 setSelectedFiles(new Set())
641 }
642 }}
643 disabled={selectedFiles.size === 0}
644 className={selectedFiles.size === 0 ? "opacity-50 cursor-not-allowed" : ""}
645 >
646 <Move className="mr-2 h-4 w-4" />
647 Move Here {selectedFiles.size > 0 && `(${selectedFiles.size})`}
648 </DropdownMenuItem>
649 <DropdownMenuItem onClick={() => deleteItems([item.id])} className="text-red-600">
650 <Trash2 className="mr-2 h-4 w-4" />
651 Delete
652 </DropdownMenuItem>
653 </DropdownMenuContent>
654 </DropdownMenu>
655 </TableCell>
656 </TableRow>,
657 )
658
659 if (item.type === "folder" && item.children && isExpanded) {
660 item.children.forEach((child) => {
661 rows.push(...renderFileRow(child, level + 1))
662 })
663 }
664
665 return rows
666 }
667
668 return (
669 <div className="container mx-auto p-6 space-y-6">
670 {/* Header */}
671 <div className="flex items-center justify-between">
672 <div className="flex items-center gap-4">
673 <div className="flex items-center gap-2">
674 <HardDrive className="h-6 w-6" />
675 <h1 className="text-2xl font-bold">My Drive</h1>
676 </div>
677 <div className="flex items-center gap-2">
678 <Button
679 variant={currentView === "drive" ? "default" : "outline"}
680 size="sm"
681 onClick={() => setCurrentView("drive")}
682 >
683 <HardDrive className="mr-2 h-4 w-4" />
684 Drive
685 </Button>
686 <Button
687 variant={currentView === "history" ? "default" : "outline"}
688 size="sm"
689 onClick={() => setCurrentView("history")}
690 >
691 <HistoryIcon className="mr-2 h-4 w-4" />
692 History
693 </Button>
694 </div>
695 </div>
696 <div className="flex items-center gap-2">
697 {currentView === "drive" && (
698 <Button onClick={() => fileInputRef.current?.click()}>
699 <Upload className="mr-2 h-4 w-4" />
700 Upload Files
701 </Button>
702 )}
703 {isLoggedIn ? (
704 <Button variant="outline" onClick={handleLogout}>
705 <LogOut className="mr-2 h-4 w-4" />
706 Logout
707 </Button>
708 ) : (
709 <Button onClick={handleLogin}>
710 <LogIn className="mr-2 h-4 w-4" />
711 Login
712 </Button>
713 )}
714 </div>
715 </div>
716
717 {currentView === "drive" ? (
718 <>
719 {/* Storage Info */}
720 <div className="bg-card rounded-lg border p-4">
721 <div className="flex items-center justify-between mb-2">
722 <span className="text-sm font-medium">Storage Usage</span>
723 <span className="text-sm text-muted-foreground">
724 {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used
725 </span>
726 </div>
727 <Progress value={storagePercentage} className="h-2" />
728 <div className="flex justify-between text-xs text-muted-foreground mt-1">
729 <span>{storagePercentage.toFixed(1)}% used</span>
730 <span>{formatFileSize(maxStorage - usedStorage)} available</span>
731 </div>
732 </div>
733
734 {/* Bulk Actions */}
735 {selectedFiles.size > 0 && (
736 <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 flex items-center justify-between">
737 <div className="flex items-center gap-4">
738 <span className="text-sm font-medium text-blue-900">
739 {selectedFiles.size} item{selectedFiles.size !== 1 ? "s" : ""} selected
740 </span>
741 <Button variant="outline" size="sm" onClick={deselectAll}>
742 Deselect All
743 </Button>
744 </div>
745 <div className="flex items-center gap-2">
746 <Button
747 variant="outline"
748 size="sm"
749 onClick={() => deleteItems(Array.from(selectedFiles))}
750 className="text-red-600 hover:text-red-700"
751 >
752 <Trash2 className="mr-2 h-4 w-4" />
753 Delete Selected
754 </Button>
755 </div>
756 </div>
757 )}
758
759 {/* File Table */}
760 <div className="border rounded-lg">
761 <Table>
762 <TableHeader>
763 <TableRow>
764 <TableHead className="w-[40px]">
765 <Checkbox
766 checked={
767 selectedFiles.size > 0 &&
768 selectedFiles.size ===
769 (() => {
770 const getAllIds = (items: FileItem[]): string[] => {
771 const ids: string[] = []
772 items.forEach((item) => {
773 ids.push(item.id)
774 if (item.children) {
775 ids.push(...getAllIds(item.children))
776 }
777 })
778 return ids
779 }
780 return getAllIds(files).length
781 })()
782 }
783 indeterminate={
784 selectedFiles.size > 0 &&
785 selectedFiles.size <
786 (() => {
787 const getAllIds = (items: FileItem[]): string[] => {
788 const ids: string[] = []
789 items.forEach((item) => {
790 ids.push(item.id)
791 if (item.children) {
792 ids.push(...getAllIds(item.children))
793 }
794 })
795 return ids
796 }
797 return getAllIds(files).length
798 })()
799 }
800 onCheckedChange={(checked) => {
801 if (checked) {
802 selectAll()
803 } else {
804 deselectAll()
805 }
806 }}
807 />
808 </TableHead>
809 <TableHead>Name</TableHead>
810 <TableHead>Size</TableHead>
811 <TableHead>Modified</TableHead>
812 <TableHead className="w-[50px]">Actions</TableHead>
813 </TableRow>
814 </TableHeader>
815 <TableBody>{files.flatMap((file) => renderFileRow(file))}</TableBody>
816 </Table>
817 </div>
818 </>
819 ) : (
820 <HistoryView />
821 )}
822
823 {/* Rename Dialog */}
824 <Dialog open={renameDialogOpen} onOpenChange={setRenameDialogOpen}>
825 <DialogContent>
826 <DialogHeader>
827 <DialogTitle>Rename {currentItem?.type === "folder" ? "Folder" : "File"}</DialogTitle>
828 </DialogHeader>
829 <div className="space-y-4">
830 <div>
831 <Label htmlFor="newName">New Name</Label>
832 <Input
833 id="newName"
834 value={newName}
835 onChange={(e) => setNewName(e.target.value)}
836 onKeyDown={(e) => {
837 if (e.key === "Enter") {
838 handleRename()
839 }
840 }}
841 placeholder="Enter new name"
842 />
843 </div>
844 <div className="flex justify-end gap-2">
845 <Button variant="outline" onClick={() => setRenameDialogOpen(false)}>
846 Cancel
847 </Button>
848 <Button onClick={handleRename} disabled={!newName.trim()}>
849 Rename
850 </Button>
851 </div>
852 </div>
853 </DialogContent>
854 </Dialog>
855
856 {/* Info Dialog */}
857 <Dialog open={infoDialogOpen} onOpenChange={setInfoDialogOpen}>
858 <DialogContent className="max-w-md">
859 <DialogHeader>
860 <DialogTitle className="flex items-center gap-2">
861 {currentItem?.type === "folder" ? (
862 <Folder className="h-5 w-5 text-blue-500" />
863 ) : (
864 <File className="h-5 w-5 text-gray-500" />
865 )}
866 {currentItem?.type === "folder" ? "Folder" : "File"} Information
867 </DialogTitle>
868 </DialogHeader>
869 {currentItem && (
870 <div className="space-y-4">
871 <div className="space-y-3">
872 <div>
873 <Label className="text-sm font-medium text-muted-foreground">Name</Label>
874 <p className="text-sm break-words">{currentItem.name}</p>
875 </div>
876 <div>
877 <Label className="text-sm font-medium text-muted-foreground">Size</Label>
878 <p className="text-sm">{formatFileSize(currentItem.size)}</p>
879 </div>
880 <div>
881 <Label className="text-sm font-medium text-muted-foreground">Modified</Label>
882 <p className="text-sm">
883 {currentItem.modified} at {currentItem.modifiedTime}
884 </p>
885 </div>
886 <div>
887 <Label className="text-sm font-medium text-muted-foreground">Modified By</Label>
888 <p className="text-sm">{currentItem.modifiedBy}</p>
889 </div>
890 <div>
891 <Label className="text-sm font-medium text-muted-foreground">Type</Label>
892 <p className="text-sm capitalize">{currentItem.type}</p>
893 </div>
894 <div>
895 <Label className="text-sm font-medium text-muted-foreground">ID</Label>
896 <p className="text-sm font-mono text-xs">{currentItem.id}</p>
897 </div>
898 </div>
899 <div className="flex justify-end">
900 <Button variant="outline" onClick={() => setInfoDialogOpen(false)}>
901 Close
902 </Button>
903 </div>
904 </div>
905 )}
906 </DialogContent>
907 </Dialog>
908 <input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileUpload} />
909 {/* Hidden file inputs for folder uploads */}
910 {(() => {
911 const getAllFolders = (items: FileItem[]): FileItem[] => {
912 const folders: FileItem[] = []
913 items.forEach((item) => {
914 if (item.type === "folder") {
915 folders.push(item)
916 if (item.children) {
917 folders.push(...getAllFolders(item.children))
918 }
919 }
920 })
921 return folders
922 }
923 return getAllFolders(files).map((folder) => (
924 <input
925 key={folder.id}
926 id={`folder-upload-${folder.id}`}
927 type="file"
928 multiple
929 className="hidden"
930 onChange={(e) => handleFolderUpload(e, folder.id)}
931 />
932 ))
933 })()}
934 </div>
935 )
936}
diff --git a/frontend/history-view.tsx b/frontend/history-view.tsx
new file mode 100644
index 0000000..8cec793
--- /dev/null
+++ b/frontend/history-view.tsx
@@ -0,0 +1,230 @@
1"use client"
2
3import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
4import { Badge } from "@/components/ui/badge"
5import { FileText, Folder, Trash2, Edit } from "lucide-react"
6
7interface HistoryEntry {
8 id: string
9 type: "file_create" | "file_remove" | "dir_create" | "rename"
10 fileName: string
11 userEmail: string
12 timestamp: string
13 details?: string
14}
15
16const mockHistoryData: HistoryEntry[] = [
17 {
18 id: "h1",
19 type: "file_create",
20 fileName: "Database Backup and Migration Scripts - Production Environment.sql",
21 userEmail: "[email protected]",
22 timestamp: "2024-01-16T09:30:00Z",
23 },
24 {
25 id: "h2",
26 type: "rename",
27 fileName: "Configuration Files and System Settings - Development Environment Setup.txt",
28 userEmail: "[email protected]",
29 timestamp: "2024-01-16T09:25:00Z",
30 details: "Renamed from 'config.txt'",
31 },
32 {
33 id: "h3",
34 type: "file_create",
35 fileName: "E-commerce Platform with React and Node.js - Full Stack Implementation.zip",
36 userEmail: "[email protected]",
37 timestamp: "2024-01-16T08:35:00Z",
38 },
39 {
40 id: "h4",
41 type: "dir_create",
42 fileName: "Web Applications and Frontend Projects",
43 userEmail: "[email protected]",
44 timestamp: "2024-01-16T08:30:00Z",
45 },
46 {
47 id: "h5",
48 type: "file_create",
49 fileName: "Dashboard Analytics Tool with Real-time Data Visualization Components.zip",
50 userEmail: "[email protected]",
51 timestamp: "2024-01-15T22:10:00Z",
52 },
53 {
54 id: "h6",
55 type: "file_remove",
56 fileName: "old_backup_file.sql",
57 userEmail: "[email protected]",
58 timestamp: "2024-01-15T20:45:00Z",
59 },
60 {
61 id: "h7",
62 type: "rename",
63 fileName: "Mobile App Development and Cross-Platform Solutions.zip",
64 userEmail: "[email protected]",
65 timestamp: "2024-01-14T13:25:00Z",
66 details: "Renamed from 'mobile_app_v1.zip'",
67 },
68 {
69 id: "h8",
70 type: "file_create",
71 fileName: "Corporate Training Videos and Educational Content Series - Complete Collection.mp4",
72 userEmail: "[email protected]",
73 timestamp: "2024-01-13T14:15:00Z",
74 },
75 {
76 id: "h9",
77 type: "dir_create",
78 fileName: "Video Content and Multimedia Projects",
79 userEmail: "[email protected]",
80 timestamp: "2024-01-13T14:00:00Z",
81 },
82 {
83 id: "h10",
84 type: "file_remove",
85 fileName: "temp_presentation.pptx",
86 userEmail: "[email protected]",
87 timestamp: "2024-01-13T11:30:00Z",
88 },
89 {
90 id: "h11",
91 type: "file_create",
92 fileName: "Professional Headshots and Corporate Event Photography - High Resolution.png",
93 userEmail: "[email protected]",
94 timestamp: "2024-01-13T11:15:00Z",
95 },
96 {
97 id: "h12",
98 type: "rename",
99 fileName: "Travel and Vacation Photos Collection",
100 userEmail: "[email protected]",
101 timestamp: "2024-01-12T20:50:00Z",
102 details: "Renamed from 'Vacation Photos'",
103 },
104 {
105 id: "h13",
106 type: "dir_create",
107 fileName: "Photography and Visual Content",
108 userEmail: "[email protected]",
109 timestamp: "2024-01-12T16:00:00Z",
110 },
111 {
112 id: "h14",
113 type: "file_create",
114 fileName: "Professional Resume and Cover Letter Templates - Updated 2024.docx",
115 userEmail: "[email protected]",
116 timestamp: "2024-01-12T12:00:00Z",
117 },
118 {
119 id: "h15",
120 type: "file_remove",
121 fileName: "draft_document.docx",
122 userEmail: "[email protected]",
123 timestamp: "2024-01-11T16:20:00Z",
124 },
125]
126
127function formatTimestamp(timestamp: string): string {
128 const date = new Date(timestamp)
129 return date.toLocaleString("en-US", {
130 year: "numeric",
131 month: "short",
132 day: "numeric",
133 hour: "2-digit",
134 minute: "2-digit",
135 hour12: true,
136 })
137}
138
139function getActionIcon(type: HistoryEntry["type"]) {
140 switch (type) {
141 case "file_create":
142 return <FileText className="h-4 w-4 text-green-600" />
143 case "dir_create":
144 return <Folder className="h-4 w-4 text-blue-600" />
145 case "file_remove":
146 return <Trash2 className="h-4 w-4 text-red-600" />
147 case "rename":
148 return <Edit className="h-4 w-4 text-orange-600" />
149 }
150}
151
152function getActionBadge(type: HistoryEntry["type"]) {
153 switch (type) {
154 case "file_create":
155 return (
156 <Badge variant="outline" className="text-green-700 border-green-300 bg-green-50">
157 File Created
158 </Badge>
159 )
160 case "dir_create":
161 return (
162 <Badge variant="outline" className="text-blue-700 border-blue-300 bg-blue-50">
163 Directory Created
164 </Badge>
165 )
166 case "file_remove":
167 return (
168 <Badge variant="outline" className="text-red-700 border-red-300 bg-red-50">
169 File Removed
170 </Badge>
171 )
172 case "rename":
173 return (
174 <Badge variant="outline" className="text-orange-700 border-orange-300 bg-orange-50">
175 Renamed
176 </Badge>
177 )
178 }
179}
180
181export default function HistoryView() {
182 return (
183 <div className="space-y-6">
184 {/* History Header */}
185 <div className="flex items-center justify-between">
186 <div>
187 <h2 className="text-xl font-semibold">Activity History</h2>
188 <p className="text-sm text-muted-foreground">Recent filesystem modifications and changes</p>
189 </div>
190 <Badge variant="secondary">{mockHistoryData.length} total entries</Badge>
191 </div>
192
193 {/* History Table */}
194 <div className="border rounded-lg">
195 <Table>
196 <TableHeader>
197 <TableRow>
198 <TableHead className="w-[50px]">Action</TableHead>
199 <TableHead>Type</TableHead>
200 <TableHead>File/Directory Name</TableHead>
201 <TableHead>User</TableHead>
202 <TableHead>Timestamp</TableHead>
203 <TableHead>Details</TableHead>
204 </TableRow>
205 </TableHeader>
206 <TableBody>
207 {mockHistoryData.map((entry) => (
208 <TableRow key={entry.id} className="hover:bg-muted/50">
209 <TableCell>{getActionIcon(entry.type)}</TableCell>
210 <TableCell>{getActionBadge(entry.type)}</TableCell>
211 <TableCell className="font-medium">
212 <span className="break-words">{entry.fileName}</span>
213 </TableCell>
214 <TableCell>
215 <span className="text-sm font-mono">{entry.userEmail}</span>
216 </TableCell>
217 <TableCell>
218 <span className="text-sm">{formatTimestamp(entry.timestamp)}</span>
219 </TableCell>
220 <TableCell>
221 <span className="text-sm text-muted-foreground">{entry.details || "—"}</span>
222 </TableCell>
223 </TableRow>
224 ))}
225 </TableBody>
226 </Table>
227 </div>
228 </div>
229 )
230}
diff --git a/frontend/hooks/use-toast.ts b/frontend/hooks/use-toast.ts
new file mode 100644
index 0000000..87e7062
--- /dev/null
+++ b/frontend/hooks/use-toast.ts
@@ -0,0 +1,193 @@
1"use client"
2
3import * as React from "react"
4
5import type {
6 ToastActionElement,
7 ToastProps,
8} from "@/components/ui/toast"
9
10const TOAST_LIMIT = 1
11const TOAST_REMOVE_DELAY = 1000000
12
13type ToasterToast = ToastProps & {
14 id: string
15 title?: React.ReactNode
16 description?: React.ReactNode
17 action?: ToastActionElement
18}
19
20const actionTypes = {
21 ADD_TOAST: "ADD_TOAST",
22 UPDATE_TOAST: "UPDATE_TOAST",
23 DISMISS_TOAST: "DISMISS_TOAST",
24 REMOVE_TOAST: "REMOVE_TOAST",
25} as const
26
27let count = 0
28
29function genId() {
30 count = (count + 1) % Number.MAX_SAFE_INTEGER
31 return count.toString()
32}
33
34type ActionType = typeof actionTypes
35
36type Action =
37 | {
38 type: ActionType["ADD_TOAST"]
39 toast: ToasterToast
40 }
41 | {
42 type: ActionType["UPDATE_TOAST"]
43 toast: Partial<ToasterToast>
44 }
45 | {
46 type: ActionType["DISMISS_TOAST"]
47 toastId?: ToasterToast["id"]
48 }
49 | {
50 type: ActionType["REMOVE_TOAST"]
51 toastId?: ToasterToast["id"]
52 }
53
54interface State {
55 toasts: ToasterToast[]
56}
57
58const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
59
60const addToRemoveQueue = (toastId: string) => {
61 if (toastTimeouts.has(toastId)) {
62 return
63 }
64
65 const timeout = setTimeout(() => {
66 toastTimeouts.delete(toastId)
67 dispatch({
68 type: "REMOVE_TOAST",
69 toastId: toastId,
70 })
71 }, TOAST_REMOVE_DELAY)
72
73 toastTimeouts.set(toastId, timeout)
74}
75
76export const reducer = (state: State, action: Action): State => {
77 switch (action.type) {
78 case "ADD_TOAST":
79 return {
80 ...state,
81 toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
82 }
83
84 case "UPDATE_TOAST":
85 return {
86 ...state,
87 toasts: state.toasts.map((t) =>
88 t.id === action.toast.id ? { ...t, ...action.toast } : t
89 ),
90 }
91
92 case "DISMISS_TOAST": {
93 const { toastId } = action
94
95 // ! Side effects ! - This could be extracted into a dismissToast() action,
96 // but I'll keep it here for simplicity
97 if (toastId) {
98 addToRemoveQueue(toastId)
99 } else {
100 state.toasts.forEach((toast) => {
101 addToRemoveQueue(toast.id)
102 })
103 }
104
105 return {
106 ...state,
107 toasts: state.toasts.map((t) =>
108 t.id === toastId || toastId === undefined
109 ? {
110 ...t,
111 open: false,
112 }
113 : t
114 ),
115 }
116 }
117 case "REMOVE_TOAST":
118 if (action.toastId === undefined) {
119 return {
120 ...state,
121 toasts: [],
122 }
123 }
124 return {
125 ...state,
126 toasts: state.toasts.filter((t) => t.id !== action.toastId),
127 }
128 }
129}
130
131const listeners: Array<(state: State) => void> = []
132
133let memoryState: State = { toasts: [] }
134
135function dispatch(action: Action) {
136 memoryState = reducer(memoryState, action)
137 listeners.forEach((listener) => {
138 listener(memoryState)
139 })
140}
141
142type Toast = Omit<ToasterToast, "id">
143
144function toast({ ...props }: Toast) {
145 const id = genId()
146
147 const update = (props: ToasterToast) =>
148 dispatch({
149 type: "UPDATE_TOAST",
150 toast: { ...props, id },
151 })
152 const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
153
154 dispatch({
155 type: "ADD_TOAST",
156 toast: {
157 ...props,
158 id,
159 open: true,
160 onOpenChange: (open) => {
161 if (!open) dismiss()
162 },
163 },
164 })
165
166 return {
167 id: id,
168 dismiss,
169 update,
170 }
171}
172
173function useToast() {
174 const [state, setState] = React.useState<State>(memoryState)
175
176 React.useEffect(() => {
177 listeners.push(setState)
178 return () => {
179 const index = listeners.indexOf(setState)
180 if (index > -1) {
181 listeners.splice(index, 1)
182 }
183 }
184 }, [state])
185
186 return {
187 ...state,
188 toast,
189 dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
190 }
191}
192
193export { useToast, toast } \ No newline at end of file
diff --git a/frontend/lib/utils.ts b/frontend/lib/utils.ts
index 857d8d9..bd0c391 100644
--- a/frontend/lib/utils.ts
+++ b/frontend/lib/utils.ts
@@ -1,27 +1,6 @@
1/** 1import { clsx, type ClassValue } from "clsx"
2 * Formats a size in bytes into a human-readable string 2import { twMerge } from "tailwind-merge"
3 * @param bytes Size in bytes
4 * @returns Formatted size string (e.g., "1.5 KB", "2.3 MB", "1.2 GB")
5 */
6export function formatSize(bytes: number | null): string {
7 if (bytes === null || bytes === 0) {
8 return '-'
9 }
10 3
11 const units = ['B', 'KB', 'MB', 'GB', 'TB'] 4export function cn(...inputs: ClassValue[]) {
12 let size = bytes 5 return twMerge(clsx(inputs))
13 let unitIndex = 0
14
15 while (size >= 1024 && unitIndex < units.length - 1) {
16 size /= 1024
17 unitIndex++
18 }
19
20 // Format with appropriate decimal places
21 if (size < 10 && unitIndex > 0) {
22 return `${size.toFixed(1)} ${units[unitIndex]}`
23 } else {
24 return `${Math.round(size)} ${units[unitIndex]}`
25 }
26} 6}
27
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 9f0d213..2a93c83 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,9 +8,20 @@
8 "name": "frontend", 8 "name": "frontend",
9 "version": "0.1.0", 9 "version": "0.1.0",
10 "dependencies": { 10 "dependencies": {
11 "@radix-ui/react-checkbox": "^1.3.2",
12 "@radix-ui/react-dialog": "^1.1.14",
13 "@radix-ui/react-dropdown-menu": "^2.1.15",
14 "@radix-ui/react-label": "^2.1.7",
15 "@radix-ui/react-progress": "^1.1.7",
16 "@radix-ui/react-slot": "^1.2.3",
17 "@radix-ui/react-toast": "^1.2.14",
18 "class-variance-authority": "^0.7.1",
19 "clsx": "^2.1.1",
20 "lucide-react": "^0.539.0",
11 "next": "15.4.6", 21 "next": "15.4.6",
12 "react": "19.1.0", 22 "react": "19.1.0",
13 "react-dom": "19.1.0" 23 "react-dom": "19.1.0",
24 "tailwind-merge": "^3.3.1"
14 }, 25 },
15 "devDependencies": { 26 "devDependencies": {
16 "@eslint/eslintrc": "^3", 27 "@eslint/eslintrc": "^3",
@@ -21,6 +32,7 @@
21 "eslint": "^9", 32 "eslint": "^9",
22 "eslint-config-next": "15.4.6", 33 "eslint-config-next": "15.4.6",
23 "tailwindcss": "^4", 34 "tailwindcss": "^4",
35 "tw-animate-css": "^1.3.6",
24 "typescript": "^5" 36 "typescript": "^5"
25 } 37 }
26 }, 38 },
@@ -225,6 +237,44 @@
225 "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 237 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
226 } 238 }
227 }, 239 },
240 "node_modules/@floating-ui/core": {
241 "version": "1.7.3",
242 "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
243 "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
244 "license": "MIT",
245 "dependencies": {
246 "@floating-ui/utils": "^0.2.10"
247 }
248 },
249 "node_modules/@floating-ui/dom": {
250 "version": "1.7.3",
251 "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz",
252 "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==",
253 "license": "MIT",
254 "dependencies": {
255 "@floating-ui/core": "^1.7.3",
256 "@floating-ui/utils": "^0.2.10"
257 }
258 },
259 "node_modules/@floating-ui/react-dom": {
260 "version": "2.1.5",
261 "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz",
262 "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==",
263 "license": "MIT",
264 "dependencies": {
265 "@floating-ui/dom": "^1.7.3"
266 },
267 "peerDependencies": {
268 "react": ">=16.8.0",
269 "react-dom": ">=16.8.0"
270 }
271 },
272 "node_modules/@floating-ui/utils": {
273 "version": "0.2.10",
274 "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
275 "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
276 "license": "MIT"
277 },
228 "node_modules/@humanfs/core": { 278 "node_modules/@humanfs/core": {
229 "version": "0.19.1", 279 "version": "0.19.1",
230 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 280 "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -966,6 +1016,724 @@
966 "node": ">=12.4.0" 1016 "node": ">=12.4.0"
967 } 1017 }
968 }, 1018 },
1019 "node_modules/@radix-ui/primitive": {
1020 "version": "1.1.2",
1021 "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
1022 "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
1023 "license": "MIT"
1024 },
1025 "node_modules/@radix-ui/react-arrow": {
1026 "version": "1.1.7",
1027 "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
1028 "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
1029 "license": "MIT",
1030 "dependencies": {
1031 "@radix-ui/react-primitive": "2.1.3"
1032 },
1033 "peerDependencies": {
1034 "@types/react": "*",
1035 "@types/react-dom": "*",
1036 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1037 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1038 },
1039 "peerDependenciesMeta": {
1040 "@types/react": {
1041 "optional": true
1042 },
1043 "@types/react-dom": {
1044 "optional": true
1045 }
1046 }
1047 },
1048 "node_modules/@radix-ui/react-checkbox": {
1049 "version": "1.3.2",
1050 "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz",
1051 "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==",
1052 "license": "MIT",
1053 "dependencies": {
1054 "@radix-ui/primitive": "1.1.2",
1055 "@radix-ui/react-compose-refs": "1.1.2",
1056 "@radix-ui/react-context": "1.1.2",
1057 "@radix-ui/react-presence": "1.1.4",
1058 "@radix-ui/react-primitive": "2.1.3",
1059 "@radix-ui/react-use-controllable-state": "1.2.2",
1060 "@radix-ui/react-use-previous": "1.1.1",
1061 "@radix-ui/react-use-size": "1.1.1"
1062 },
1063 "peerDependencies": {
1064 "@types/react": "*",
1065 "@types/react-dom": "*",
1066 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1067 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1068 },
1069 "peerDependenciesMeta": {
1070 "@types/react": {
1071 "optional": true
1072 },
1073 "@types/react-dom": {
1074 "optional": true
1075 }
1076 }
1077 },
1078 "node_modules/@radix-ui/react-collection": {
1079 "version": "1.1.7",
1080 "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
1081 "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
1082 "license": "MIT",
1083 "dependencies": {
1084 "@radix-ui/react-compose-refs": "1.1.2",
1085 "@radix-ui/react-context": "1.1.2",
1086 "@radix-ui/react-primitive": "2.1.3",
1087 "@radix-ui/react-slot": "1.2.3"
1088 },
1089 "peerDependencies": {
1090 "@types/react": "*",
1091 "@types/react-dom": "*",
1092 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1093 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1094 },
1095 "peerDependenciesMeta": {
1096 "@types/react": {
1097 "optional": true
1098 },
1099 "@types/react-dom": {
1100 "optional": true
1101 }
1102 }
1103 },
1104 "node_modules/@radix-ui/react-compose-refs": {
1105 "version": "1.1.2",
1106 "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
1107 "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
1108 "license": "MIT",
1109 "peerDependencies": {
1110 "@types/react": "*",
1111 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1112 },
1113 "peerDependenciesMeta": {
1114 "@types/react": {
1115 "optional": true
1116 }
1117 }
1118 },
1119 "node_modules/@radix-ui/react-context": {
1120 "version": "1.1.2",
1121 "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
1122 "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
1123 "license": "MIT",
1124 "peerDependencies": {
1125 "@types/react": "*",
1126 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1127 },
1128 "peerDependenciesMeta": {
1129 "@types/react": {
1130 "optional": true
1131 }
1132 }
1133 },
1134 "node_modules/@radix-ui/react-dialog": {
1135 "version": "1.1.14",
1136 "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz",
1137 "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==",
1138 "license": "MIT",
1139 "dependencies": {
1140 "@radix-ui/primitive": "1.1.2",
1141 "@radix-ui/react-compose-refs": "1.1.2",
1142 "@radix-ui/react-context": "1.1.2",
1143 "@radix-ui/react-dismissable-layer": "1.1.10",
1144 "@radix-ui/react-focus-guards": "1.1.2",
1145 "@radix-ui/react-focus-scope": "1.1.7",
1146 "@radix-ui/react-id": "1.1.1",
1147 "@radix-ui/react-portal": "1.1.9",
1148 "@radix-ui/react-presence": "1.1.4",
1149 "@radix-ui/react-primitive": "2.1.3",
1150 "@radix-ui/react-slot": "1.2.3",
1151 "@radix-ui/react-use-controllable-state": "1.2.2",
1152 "aria-hidden": "^1.2.4",
1153 "react-remove-scroll": "^2.6.3"
1154 },
1155 "peerDependencies": {
1156 "@types/react": "*",
1157 "@types/react-dom": "*",
1158 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1159 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1160 },
1161 "peerDependenciesMeta": {
1162 "@types/react": {
1163 "optional": true
1164 },
1165 "@types/react-dom": {
1166 "optional": true
1167 }
1168 }
1169 },
1170 "node_modules/@radix-ui/react-direction": {
1171 "version": "1.1.1",
1172 "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
1173 "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
1174 "license": "MIT",
1175 "peerDependencies": {
1176 "@types/react": "*",
1177 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1178 },
1179 "peerDependenciesMeta": {
1180 "@types/react": {
1181 "optional": true
1182 }
1183 }
1184 },
1185 "node_modules/@radix-ui/react-dismissable-layer": {
1186 "version": "1.1.10",
1187 "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz",
1188 "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==",
1189 "license": "MIT",
1190 "dependencies": {
1191 "@radix-ui/primitive": "1.1.2",
1192 "@radix-ui/react-compose-refs": "1.1.2",
1193 "@radix-ui/react-primitive": "2.1.3",
1194 "@radix-ui/react-use-callback-ref": "1.1.1",
1195 "@radix-ui/react-use-escape-keydown": "1.1.1"
1196 },
1197 "peerDependencies": {
1198 "@types/react": "*",
1199 "@types/react-dom": "*",
1200 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1201 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1202 },
1203 "peerDependenciesMeta": {
1204 "@types/react": {
1205 "optional": true
1206 },
1207 "@types/react-dom": {
1208 "optional": true
1209 }
1210 }
1211 },
1212 "node_modules/@radix-ui/react-dropdown-menu": {
1213 "version": "2.1.15",
1214 "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz",
1215 "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==",
1216 "license": "MIT",
1217 "dependencies": {
1218 "@radix-ui/primitive": "1.1.2",
1219 "@radix-ui/react-compose-refs": "1.1.2",
1220 "@radix-ui/react-context": "1.1.2",
1221 "@radix-ui/react-id": "1.1.1",
1222 "@radix-ui/react-menu": "2.1.15",
1223 "@radix-ui/react-primitive": "2.1.3",
1224 "@radix-ui/react-use-controllable-state": "1.2.2"
1225 },
1226 "peerDependencies": {
1227 "@types/react": "*",
1228 "@types/react-dom": "*",
1229 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1230 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1231 },
1232 "peerDependenciesMeta": {
1233 "@types/react": {
1234 "optional": true
1235 },
1236 "@types/react-dom": {
1237 "optional": true
1238 }
1239 }
1240 },
1241 "node_modules/@radix-ui/react-focus-guards": {
1242 "version": "1.1.2",
1243 "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
1244 "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==",
1245 "license": "MIT",
1246 "peerDependencies": {
1247 "@types/react": "*",
1248 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1249 },
1250 "peerDependenciesMeta": {
1251 "@types/react": {
1252 "optional": true
1253 }
1254 }
1255 },
1256 "node_modules/@radix-ui/react-focus-scope": {
1257 "version": "1.1.7",
1258 "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
1259 "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
1260 "license": "MIT",
1261 "dependencies": {
1262 "@radix-ui/react-compose-refs": "1.1.2",
1263 "@radix-ui/react-primitive": "2.1.3",
1264 "@radix-ui/react-use-callback-ref": "1.1.1"
1265 },
1266 "peerDependencies": {
1267 "@types/react": "*",
1268 "@types/react-dom": "*",
1269 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1270 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1271 },
1272 "peerDependenciesMeta": {
1273 "@types/react": {
1274 "optional": true
1275 },
1276 "@types/react-dom": {
1277 "optional": true
1278 }
1279 }
1280 },
1281 "node_modules/@radix-ui/react-id": {
1282 "version": "1.1.1",
1283 "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
1284 "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
1285 "license": "MIT",
1286 "dependencies": {
1287 "@radix-ui/react-use-layout-effect": "1.1.1"
1288 },
1289 "peerDependencies": {
1290 "@types/react": "*",
1291 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1292 },
1293 "peerDependenciesMeta": {
1294 "@types/react": {
1295 "optional": true
1296 }
1297 }
1298 },
1299 "node_modules/@radix-ui/react-label": {
1300 "version": "2.1.7",
1301 "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz",
1302 "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==",
1303 "license": "MIT",
1304 "dependencies": {
1305 "@radix-ui/react-primitive": "2.1.3"
1306 },
1307 "peerDependencies": {
1308 "@types/react": "*",
1309 "@types/react-dom": "*",
1310 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1311 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1312 },
1313 "peerDependenciesMeta": {
1314 "@types/react": {
1315 "optional": true
1316 },
1317 "@types/react-dom": {
1318 "optional": true
1319 }
1320 }
1321 },
1322 "node_modules/@radix-ui/react-menu": {
1323 "version": "2.1.15",
1324 "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz",
1325 "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==",
1326 "license": "MIT",
1327 "dependencies": {
1328 "@radix-ui/primitive": "1.1.2",
1329 "@radix-ui/react-collection": "1.1.7",
1330 "@radix-ui/react-compose-refs": "1.1.2",
1331 "@radix-ui/react-context": "1.1.2",
1332 "@radix-ui/react-direction": "1.1.1",
1333 "@radix-ui/react-dismissable-layer": "1.1.10",
1334 "@radix-ui/react-focus-guards": "1.1.2",
1335 "@radix-ui/react-focus-scope": "1.1.7",
1336 "@radix-ui/react-id": "1.1.1",
1337 "@radix-ui/react-popper": "1.2.7",
1338 "@radix-ui/react-portal": "1.1.9",
1339 "@radix-ui/react-presence": "1.1.4",
1340 "@radix-ui/react-primitive": "2.1.3",
1341 "@radix-ui/react-roving-focus": "1.1.10",
1342 "@radix-ui/react-slot": "1.2.3",
1343 "@radix-ui/react-use-callback-ref": "1.1.1",
1344 "aria-hidden": "^1.2.4",
1345 "react-remove-scroll": "^2.6.3"
1346 },
1347 "peerDependencies": {
1348 "@types/react": "*",
1349 "@types/react-dom": "*",
1350 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1351 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1352 },
1353 "peerDependenciesMeta": {
1354 "@types/react": {
1355 "optional": true
1356 },
1357 "@types/react-dom": {
1358 "optional": true
1359 }
1360 }
1361 },
1362 "node_modules/@radix-ui/react-popper": {
1363 "version": "1.2.7",
1364 "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz",
1365 "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==",
1366 "license": "MIT",
1367 "dependencies": {
1368 "@floating-ui/react-dom": "^2.0.0",
1369 "@radix-ui/react-arrow": "1.1.7",
1370 "@radix-ui/react-compose-refs": "1.1.2",
1371 "@radix-ui/react-context": "1.1.2",
1372 "@radix-ui/react-primitive": "2.1.3",
1373 "@radix-ui/react-use-callback-ref": "1.1.1",
1374 "@radix-ui/react-use-layout-effect": "1.1.1",
1375 "@radix-ui/react-use-rect": "1.1.1",
1376 "@radix-ui/react-use-size": "1.1.1",
1377 "@radix-ui/rect": "1.1.1"
1378 },
1379 "peerDependencies": {
1380 "@types/react": "*",
1381 "@types/react-dom": "*",
1382 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1383 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1384 },
1385 "peerDependenciesMeta": {
1386 "@types/react": {
1387 "optional": true
1388 },
1389 "@types/react-dom": {
1390 "optional": true
1391 }
1392 }
1393 },
1394 "node_modules/@radix-ui/react-portal": {
1395 "version": "1.1.9",
1396 "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
1397 "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
1398 "license": "MIT",
1399 "dependencies": {
1400 "@radix-ui/react-primitive": "2.1.3",
1401 "@radix-ui/react-use-layout-effect": "1.1.1"
1402 },
1403 "peerDependencies": {
1404 "@types/react": "*",
1405 "@types/react-dom": "*",
1406 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1407 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1408 },
1409 "peerDependenciesMeta": {
1410 "@types/react": {
1411 "optional": true
1412 },
1413 "@types/react-dom": {
1414 "optional": true
1415 }
1416 }
1417 },
1418 "node_modules/@radix-ui/react-presence": {
1419 "version": "1.1.4",
1420 "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
1421 "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
1422 "license": "MIT",
1423 "dependencies": {
1424 "@radix-ui/react-compose-refs": "1.1.2",
1425 "@radix-ui/react-use-layout-effect": "1.1.1"
1426 },
1427 "peerDependencies": {
1428 "@types/react": "*",
1429 "@types/react-dom": "*",
1430 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1431 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1432 },
1433 "peerDependenciesMeta": {
1434 "@types/react": {
1435 "optional": true
1436 },
1437 "@types/react-dom": {
1438 "optional": true
1439 }
1440 }
1441 },
1442 "node_modules/@radix-ui/react-primitive": {
1443 "version": "2.1.3",
1444 "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
1445 "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
1446 "license": "MIT",
1447 "dependencies": {
1448 "@radix-ui/react-slot": "1.2.3"
1449 },
1450 "peerDependencies": {
1451 "@types/react": "*",
1452 "@types/react-dom": "*",
1453 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1454 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1455 },
1456 "peerDependenciesMeta": {
1457 "@types/react": {
1458 "optional": true
1459 },
1460 "@types/react-dom": {
1461 "optional": true
1462 }
1463 }
1464 },
1465 "node_modules/@radix-ui/react-progress": {
1466 "version": "1.1.7",
1467 "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
1468 "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
1469 "license": "MIT",
1470 "dependencies": {
1471 "@radix-ui/react-context": "1.1.2",
1472 "@radix-ui/react-primitive": "2.1.3"
1473 },
1474 "peerDependencies": {
1475 "@types/react": "*",
1476 "@types/react-dom": "*",
1477 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1478 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1479 },
1480 "peerDependenciesMeta": {
1481 "@types/react": {
1482 "optional": true
1483 },
1484 "@types/react-dom": {
1485 "optional": true
1486 }
1487 }
1488 },
1489 "node_modules/@radix-ui/react-roving-focus": {
1490 "version": "1.1.10",
1491 "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
1492 "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==",
1493 "license": "MIT",
1494 "dependencies": {
1495 "@radix-ui/primitive": "1.1.2",
1496 "@radix-ui/react-collection": "1.1.7",
1497 "@radix-ui/react-compose-refs": "1.1.2",
1498 "@radix-ui/react-context": "1.1.2",
1499 "@radix-ui/react-direction": "1.1.1",
1500 "@radix-ui/react-id": "1.1.1",
1501 "@radix-ui/react-primitive": "2.1.3",
1502 "@radix-ui/react-use-callback-ref": "1.1.1",
1503 "@radix-ui/react-use-controllable-state": "1.2.2"
1504 },
1505 "peerDependencies": {
1506 "@types/react": "*",
1507 "@types/react-dom": "*",
1508 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1509 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1510 },
1511 "peerDependenciesMeta": {
1512 "@types/react": {
1513 "optional": true
1514 },
1515 "@types/react-dom": {
1516 "optional": true
1517 }
1518 }
1519 },
1520 "node_modules/@radix-ui/react-slot": {
1521 "version": "1.2.3",
1522 "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
1523 "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
1524 "license": "MIT",
1525 "dependencies": {
1526 "@radix-ui/react-compose-refs": "1.1.2"
1527 },
1528 "peerDependencies": {
1529 "@types/react": "*",
1530 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1531 },
1532 "peerDependenciesMeta": {
1533 "@types/react": {
1534 "optional": true
1535 }
1536 }
1537 },
1538 "node_modules/@radix-ui/react-toast": {
1539 "version": "1.2.14",
1540 "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz",
1541 "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==",
1542 "license": "MIT",
1543 "dependencies": {
1544 "@radix-ui/primitive": "1.1.2",
1545 "@radix-ui/react-collection": "1.1.7",
1546 "@radix-ui/react-compose-refs": "1.1.2",
1547 "@radix-ui/react-context": "1.1.2",
1548 "@radix-ui/react-dismissable-layer": "1.1.10",
1549 "@radix-ui/react-portal": "1.1.9",
1550 "@radix-ui/react-presence": "1.1.4",
1551 "@radix-ui/react-primitive": "2.1.3",
1552 "@radix-ui/react-use-callback-ref": "1.1.1",
1553 "@radix-ui/react-use-controllable-state": "1.2.2",
1554 "@radix-ui/react-use-layout-effect": "1.1.1",
1555 "@radix-ui/react-visually-hidden": "1.2.3"
1556 },
1557 "peerDependencies": {
1558 "@types/react": "*",
1559 "@types/react-dom": "*",
1560 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1561 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1562 },
1563 "peerDependenciesMeta": {
1564 "@types/react": {
1565 "optional": true
1566 },
1567 "@types/react-dom": {
1568 "optional": true
1569 }
1570 }
1571 },
1572 "node_modules/@radix-ui/react-use-callback-ref": {
1573 "version": "1.1.1",
1574 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
1575 "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
1576 "license": "MIT",
1577 "peerDependencies": {
1578 "@types/react": "*",
1579 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1580 },
1581 "peerDependenciesMeta": {
1582 "@types/react": {
1583 "optional": true
1584 }
1585 }
1586 },
1587 "node_modules/@radix-ui/react-use-controllable-state": {
1588 "version": "1.2.2",
1589 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
1590 "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
1591 "license": "MIT",
1592 "dependencies": {
1593 "@radix-ui/react-use-effect-event": "0.0.2",
1594 "@radix-ui/react-use-layout-effect": "1.1.1"
1595 },
1596 "peerDependencies": {
1597 "@types/react": "*",
1598 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1599 },
1600 "peerDependenciesMeta": {
1601 "@types/react": {
1602 "optional": true
1603 }
1604 }
1605 },
1606 "node_modules/@radix-ui/react-use-effect-event": {
1607 "version": "0.0.2",
1608 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
1609 "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
1610 "license": "MIT",
1611 "dependencies": {
1612 "@radix-ui/react-use-layout-effect": "1.1.1"
1613 },
1614 "peerDependencies": {
1615 "@types/react": "*",
1616 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1617 },
1618 "peerDependenciesMeta": {
1619 "@types/react": {
1620 "optional": true
1621 }
1622 }
1623 },
1624 "node_modules/@radix-ui/react-use-escape-keydown": {
1625 "version": "1.1.1",
1626 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
1627 "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
1628 "license": "MIT",
1629 "dependencies": {
1630 "@radix-ui/react-use-callback-ref": "1.1.1"
1631 },
1632 "peerDependencies": {
1633 "@types/react": "*",
1634 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1635 },
1636 "peerDependenciesMeta": {
1637 "@types/react": {
1638 "optional": true
1639 }
1640 }
1641 },
1642 "node_modules/@radix-ui/react-use-layout-effect": {
1643 "version": "1.1.1",
1644 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
1645 "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
1646 "license": "MIT",
1647 "peerDependencies": {
1648 "@types/react": "*",
1649 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1650 },
1651 "peerDependenciesMeta": {
1652 "@types/react": {
1653 "optional": true
1654 }
1655 }
1656 },
1657 "node_modules/@radix-ui/react-use-previous": {
1658 "version": "1.1.1",
1659 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
1660 "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
1661 "license": "MIT",
1662 "peerDependencies": {
1663 "@types/react": "*",
1664 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1665 },
1666 "peerDependenciesMeta": {
1667 "@types/react": {
1668 "optional": true
1669 }
1670 }
1671 },
1672 "node_modules/@radix-ui/react-use-rect": {
1673 "version": "1.1.1",
1674 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
1675 "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
1676 "license": "MIT",
1677 "dependencies": {
1678 "@radix-ui/rect": "1.1.1"
1679 },
1680 "peerDependencies": {
1681 "@types/react": "*",
1682 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1683 },
1684 "peerDependenciesMeta": {
1685 "@types/react": {
1686 "optional": true
1687 }
1688 }
1689 },
1690 "node_modules/@radix-ui/react-use-size": {
1691 "version": "1.1.1",
1692 "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
1693 "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
1694 "license": "MIT",
1695 "dependencies": {
1696 "@radix-ui/react-use-layout-effect": "1.1.1"
1697 },
1698 "peerDependencies": {
1699 "@types/react": "*",
1700 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1701 },
1702 "peerDependenciesMeta": {
1703 "@types/react": {
1704 "optional": true
1705 }
1706 }
1707 },
1708 "node_modules/@radix-ui/react-visually-hidden": {
1709 "version": "1.2.3",
1710 "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
1711 "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
1712 "license": "MIT",
1713 "dependencies": {
1714 "@radix-ui/react-primitive": "2.1.3"
1715 },
1716 "peerDependencies": {
1717 "@types/react": "*",
1718 "@types/react-dom": "*",
1719 "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
1720 "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
1721 },
1722 "peerDependenciesMeta": {
1723 "@types/react": {
1724 "optional": true
1725 },
1726 "@types/react-dom": {
1727 "optional": true
1728 }
1729 }
1730 },
1731 "node_modules/@radix-ui/rect": {
1732 "version": "1.1.1",
1733 "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
1734 "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
1735 "license": "MIT"
1736 },
969 "node_modules/@rtsao/scc": { 1737 "node_modules/@rtsao/scc": {
970 "version": "1.1.0", 1738 "version": "1.1.0",
971 "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", 1739 "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1311,7 +2079,7 @@
1311 "version": "19.1.9", 2079 "version": "19.1.9",
1312 "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", 2080 "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
1313 "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", 2081 "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
1314 "dev": true, 2082 "devOptional": true,
1315 "license": "MIT", 2083 "license": "MIT",
1316 "dependencies": { 2084 "dependencies": {
1317 "csstype": "^3.0.2" 2085 "csstype": "^3.0.2"
@@ -1321,7 +2089,7 @@
1321 "version": "19.1.7", 2089 "version": "19.1.7",
1322 "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", 2090 "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz",
1323 "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", 2091 "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==",
1324 "dev": true, 2092 "devOptional": true,
1325 "license": "MIT", 2093 "license": "MIT",
1326 "peerDependencies": { 2094 "peerDependencies": {
1327 "@types/react": "^19.0.0" 2095 "@types/react": "^19.0.0"
@@ -1947,6 +2715,18 @@
1947 "dev": true, 2715 "dev": true,
1948 "license": "Python-2.0" 2716 "license": "Python-2.0"
1949 }, 2717 },
2718 "node_modules/aria-hidden": {
2719 "version": "1.2.6",
2720 "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
2721 "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
2722 "license": "MIT",
2723 "dependencies": {
2724 "tslib": "^2.0.0"
2725 },
2726 "engines": {
2727 "node": ">=10"
2728 }
2729 },
1950 "node_modules/aria-query": { 2730 "node_modules/aria-query": {
1951 "version": "5.3.2", 2731 "version": "5.3.2",
1952 "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", 2732 "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -2308,12 +3088,33 @@
2308 "node": ">=18" 3088 "node": ">=18"
2309 } 3089 }
2310 }, 3090 },
3091 "node_modules/class-variance-authority": {
3092 "version": "0.7.1",
3093 "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
3094 "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
3095 "license": "Apache-2.0",
3096 "dependencies": {
3097 "clsx": "^2.1.1"
3098 },
3099 "funding": {
3100 "url": "https://polar.sh/cva"
3101 }
3102 },
2311 "node_modules/client-only": { 3103 "node_modules/client-only": {
2312 "version": "0.0.1", 3104 "version": "0.0.1",
2313 "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 3105 "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
2314 "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", 3106 "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
2315 "license": "MIT" 3107 "license": "MIT"
2316 }, 3108 },
3109 "node_modules/clsx": {
3110 "version": "2.1.1",
3111 "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
3112 "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
3113 "license": "MIT",
3114 "engines": {
3115 "node": ">=6"
3116 }
3117 },
2317 "node_modules/color": { 3118 "node_modules/color": {
2318 "version": "4.2.3", 3119 "version": "4.2.3",
2319 "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 3120 "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -2385,7 +3186,7 @@
2385 "version": "3.1.3", 3186 "version": "3.1.3",
2386 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 3187 "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
2387 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 3188 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
2388 "dev": true, 3189 "devOptional": true,
2389 "license": "MIT" 3190 "license": "MIT"
2390 }, 3191 },
2391 "node_modules/damerau-levenshtein": { 3192 "node_modules/damerau-levenshtein": {
@@ -2520,6 +3321,12 @@
2520 "node": ">=8" 3321 "node": ">=8"
2521 } 3322 }
2522 }, 3323 },
3324 "node_modules/detect-node-es": {
3325 "version": "1.1.0",
3326 "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
3327 "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
3328 "license": "MIT"
3329 },
2523 "node_modules/doctrine": { 3330 "node_modules/doctrine": {
2524 "version": "2.1.0", 3331 "version": "2.1.0",
2525 "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", 3332 "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
@@ -3392,6 +4199,15 @@
3392 "url": "https://github.com/sponsors/ljharb" 4199 "url": "https://github.com/sponsors/ljharb"
3393 } 4200 }
3394 }, 4201 },
4202 "node_modules/get-nonce": {
4203 "version": "1.0.1",
4204 "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
4205 "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
4206 "license": "MIT",
4207 "engines": {
4208 "node": ">=6"
4209 }
4210 },
3395 "node_modules/get-proto": { 4211 "node_modules/get-proto": {
3396 "version": "1.0.1", 4212 "version": "1.0.1",
3397 "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 4213 "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -4492,6 +5308,15 @@
4492 "loose-envify": "cli.js" 5308 "loose-envify": "cli.js"
4493 } 5309 }
4494 }, 5310 },
5311 "node_modules/lucide-react": {
5312 "version": "0.539.0",
5313 "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz",
5314 "integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==",
5315 "license": "ISC",
5316 "peerDependencies": {
5317 "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5318 }
5319 },
4495 "node_modules/magic-string": { 5320 "node_modules/magic-string": {
4496 "version": "0.30.17", 5321 "version": "0.30.17",
4497 "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 5322 "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
@@ -5096,6 +5921,75 @@
5096 "dev": true, 5921 "dev": true,
5097 "license": "MIT" 5922 "license": "MIT"
5098 }, 5923 },
5924 "node_modules/react-remove-scroll": {
5925 "version": "2.7.1",
5926 "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz",
5927 "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==",
5928 "license": "MIT",
5929 "dependencies": {
5930 "react-remove-scroll-bar": "^2.3.7",
5931 "react-style-singleton": "^2.2.3",
5932 "tslib": "^2.1.0",
5933 "use-callback-ref": "^1.3.3",
5934 "use-sidecar": "^1.1.3"
5935 },
5936 "engines": {
5937 "node": ">=10"
5938 },
5939 "peerDependencies": {
5940 "@types/react": "*",
5941 "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
5942 },
5943 "peerDependenciesMeta": {
5944 "@types/react": {
5945 "optional": true
5946 }
5947 }
5948 },
5949 "node_modules/react-remove-scroll-bar": {
5950 "version": "2.3.8",
5951 "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
5952 "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
5953 "license": "MIT",
5954 "dependencies": {
5955 "react-style-singleton": "^2.2.2",
5956 "tslib": "^2.0.0"
5957 },
5958 "engines": {
5959 "node": ">=10"
5960 },
5961 "peerDependencies": {
5962 "@types/react": "*",
5963 "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
5964 },
5965 "peerDependenciesMeta": {
5966 "@types/react": {
5967 "optional": true
5968 }
5969 }
5970 },
5971 "node_modules/react-style-singleton": {
5972 "version": "2.2.3",
5973 "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
5974 "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
5975 "license": "MIT",
5976 "dependencies": {
5977 "get-nonce": "^1.0.0",
5978 "tslib": "^2.0.0"
5979 },
5980 "engines": {
5981 "node": ">=10"
5982 },
5983 "peerDependencies": {
5984 "@types/react": "*",
5985 "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
5986 },
5987 "peerDependenciesMeta": {
5988 "@types/react": {
5989 "optional": true
5990 }
5991 }
5992 },
5099 "node_modules/reflect.getprototypeof": { 5993 "node_modules/reflect.getprototypeof": {
5100 "version": "1.0.10", 5994 "version": "1.0.10",
5101 "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", 5995 "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5706,6 +6600,16 @@
5706 "url": "https://github.com/sponsors/ljharb" 6600 "url": "https://github.com/sponsors/ljharb"
5707 } 6601 }
5708 }, 6602 },
6603 "node_modules/tailwind-merge": {
6604 "version": "3.3.1",
6605 "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
6606 "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
6607 "license": "MIT",
6608 "funding": {
6609 "type": "github",
6610 "url": "https://github.com/sponsors/dcastil"
6611 }
6612 },
5709 "node_modules/tailwindcss": { 6613 "node_modules/tailwindcss": {
5710 "version": "4.1.11", 6614 "version": "4.1.11",
5711 "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", 6615 "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz",
@@ -5831,6 +6735,16 @@
5831 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 6735 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
5832 "license": "0BSD" 6736 "license": "0BSD"
5833 }, 6737 },
6738 "node_modules/tw-animate-css": {
6739 "version": "1.3.6",
6740 "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz",
6741 "integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==",
6742 "dev": true,
6743 "license": "MIT",
6744 "funding": {
6745 "url": "https://github.com/sponsors/Wombosvideo"
6746 }
6747 },
5834 "node_modules/type-check": { 6748 "node_modules/type-check": {
5835 "version": "0.4.0", 6749 "version": "0.4.0",
5836 "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 6750 "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -6007,6 +6921,49 @@
6007 "punycode": "^2.1.0" 6921 "punycode": "^2.1.0"
6008 } 6922 }
6009 }, 6923 },
6924 "node_modules/use-callback-ref": {
6925 "version": "1.3.3",
6926 "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
6927 "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
6928 "license": "MIT",
6929 "dependencies": {
6930 "tslib": "^2.0.0"
6931 },
6932 "engines": {
6933 "node": ">=10"
6934 },
6935 "peerDependencies": {
6936 "@types/react": "*",
6937 "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
6938 },
6939 "peerDependenciesMeta": {
6940 "@types/react": {
6941 "optional": true
6942 }
6943 }
6944 },
6945 "node_modules/use-sidecar": {
6946 "version": "1.1.3",
6947 "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
6948 "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
6949 "license": "MIT",
6950 "dependencies": {
6951 "detect-node-es": "^1.1.0",
6952 "tslib": "^2.0.0"
6953 },
6954 "engines": {
6955 "node": ">=10"
6956 },
6957 "peerDependencies": {
6958 "@types/react": "*",
6959 "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
6960 },
6961 "peerDependenciesMeta": {
6962 "@types/react": {
6963 "optional": true
6964 }
6965 }
6966 },
6010 "node_modules/which": { 6967 "node_modules/which": {
6011 "version": "2.0.2", 6968 "version": "2.0.2",
6012 "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 6969 "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 0852c78..d7922cf 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -9,19 +9,31 @@
9 "lint": "next lint" 9 "lint": "next lint"
10 }, 10 },
11 "dependencies": { 11 "dependencies": {
12 "@radix-ui/react-checkbox": "^1.3.2",
13 "@radix-ui/react-dialog": "^1.1.14",
14 "@radix-ui/react-dropdown-menu": "^2.1.15",
15 "@radix-ui/react-label": "^2.1.7",
16 "@radix-ui/react-progress": "^1.1.7",
17 "@radix-ui/react-slot": "^1.2.3",
18 "@radix-ui/react-toast": "^1.2.14",
19 "class-variance-authority": "^0.7.1",
20 "clsx": "^2.1.1",
21 "lucide-react": "^0.539.0",
22 "next": "15.4.6",
12 "react": "19.1.0", 23 "react": "19.1.0",
13 "react-dom": "19.1.0", 24 "react-dom": "19.1.0",
14 "next": "15.4.6" 25 "tailwind-merge": "^3.3.1"
15 }, 26 },
16 "devDependencies": { 27 "devDependencies": {
17 "typescript": "^5", 28 "@eslint/eslintrc": "^3",
29 "@tailwindcss/postcss": "^4",
18 "@types/node": "^20", 30 "@types/node": "^20",
19 "@types/react": "^19", 31 "@types/react": "^19",
20 "@types/react-dom": "^19", 32 "@types/react-dom": "^19",
21 "@tailwindcss/postcss": "^4",
22 "tailwindcss": "^4",
23 "eslint": "^9", 33 "eslint": "^9",
24 "eslint-config-next": "15.4.6", 34 "eslint-config-next": "15.4.6",
25 "@eslint/eslintrc": "^3" 35 "tailwindcss": "^4",
36 "tw-animate-css": "^1.3.6",
37 "typescript": "^5"
26 } 38 }
27} 39}