diff options
| author | diogo464 <[email protected]> | 2025-08-11 16:04:32 +0100 |
|---|---|---|
| committer | diogo464 <[email protected]> | 2025-08-11 16:04:32 +0100 |
| commit | f4d8a26972728891de8bde4eeb94c80f027ce2d2 (patch) | |
| tree | 3c8b9c25c2a1e3fab7a86f51922c39eb2ed93697 | |
| parent | 32b008a9c0c8e0130ab10bc96ffea9232f9cf95a (diff) | |
basic v0 ui working
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 @@ | |||
| 1 | import { Drive_ls } from "@/lib/drive_server" | ||
| 2 | import { Drive_parent } from "@/lib/drive_shared" | ||
| 3 | import Link from "next/link" | ||
| 4 | import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth"; | ||
| 5 | import FileUpload from "@/components/FileUpload" | ||
| 6 | import FileTable from "@/components/FileTable" | ||
| 7 | |||
| 8 | interface DrivePageProps { | ||
| 9 | params: Promise<{ | ||
| 10 | path: string[] | ||
| 11 | }> | ||
| 12 | } | ||
| 13 | |||
| 14 | export 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 @@ | |||
| 1 | import { Drive_ls } from "@/lib/drive_server" | ||
| 2 | import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth" | ||
| 3 | import Link from "next/link" | ||
| 4 | import FileUpload from "@/components/FileUpload" | ||
| 5 | import FileTable from "@/components/FileTable" | ||
| 6 | |||
| 7 | export 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 | ||
| 22 | body { | 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 @@ | |||
| 1 | import type { Metadata } from "next"; | 1 | import type { Metadata } from "next"; |
| 2 | import { Geist, Geist_Mono } from "next/font/google"; | 2 | import { Geist, Geist_Mono } from "next/font/google"; |
| 3 | import "./globals.css"; | 3 | import "./globals.css"; |
| 4 | import AuthButton from "@/components/AuthButton"; | 4 | import { Toaster } from "@/components/ui/toaster"; |
| 5 | 5 | ||
| 6 | const geistSans = Geist({ | 6 | const 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 @@ | |||
| 1 | import { redirect } from 'next/navigation' | 1 | import FileDrive from "../file-drive" |
| 2 | 2 | ||
| 3 | export default function Home() { | 3 | export 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 @@ | |||
| 1 | import { Auth_get_user, Auth_tinyauth_public_endpoint } from '@/lib/auth' | ||
| 2 | |||
| 3 | export 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 | |||
| 3 | import { useState } from 'react' | ||
| 4 | import { DriveLsEntry } from '@/lib/drive_types' | ||
| 5 | import { Drive_basename } from '@/lib/drive_shared' | ||
| 6 | import { formatSize } from '@/lib/utils' | ||
| 7 | import Link from 'next/link' | ||
| 8 | |||
| 9 | interface FileTableEntry extends DriveLsEntry { | ||
| 10 | isParent?: boolean | ||
| 11 | parentPath?: string | ||
| 12 | } | ||
| 13 | |||
| 14 | interface FileTableProps { | ||
| 15 | entries: DriveLsEntry[] | ||
| 16 | currentPath?: string | ||
| 17 | showParent?: boolean | ||
| 18 | parentPath?: string | ||
| 19 | onSelectedFilesChange?: (selectedFiles: string[]) => void | ||
| 20 | } | ||
| 21 | |||
| 22 | export 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 | |||
| 3 | import { useState, useRef } from 'react' | ||
| 4 | import { UPLOAD_MAX_FILES, UPLOAD_MAX_FILE_SIZE } from '@/lib/constants' | ||
| 5 | |||
| 6 | // Client-side file validation function | ||
| 7 | function 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 | |||
| 15 | interface FileUploadProps { | ||
| 16 | targetPath: string | ||
| 17 | onUploadComplete?: () => void | ||
| 18 | } | ||
| 19 | |||
| 20 | interface UploadResult { | ||
| 21 | filename: string | ||
| 22 | success: boolean | ||
| 23 | message: string | ||
| 24 | } | ||
| 25 | |||
| 26 | export 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 @@ | |||
| 1 | import * as React from "react" | ||
| 2 | import { Slot } from "@radix-ui/react-slot" | ||
| 3 | import { cva, type VariantProps } from "class-variance-authority" | ||
| 4 | |||
| 5 | import { cn } from "@/lib/utils" | ||
| 6 | |||
| 7 | const 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 | |||
| 28 | function 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 | |||
| 46 | export { 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 @@ | |||
| 1 | import * as React from "react" | ||
| 2 | import { Slot } from "@radix-ui/react-slot" | ||
| 3 | import { cva, type VariantProps } from "class-variance-authority" | ||
| 4 | |||
| 5 | import { cn } from "@/lib/utils" | ||
| 6 | |||
| 7 | const 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 | |||
| 38 | function 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 | |||
| 59 | export { 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 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" | ||
| 5 | import { CheckIcon } from "lucide-react" | ||
| 6 | |||
| 7 | import { cn } from "@/lib/utils" | ||
| 8 | |||
| 9 | const 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 | |||
| 45 | Checkbox.displayName = "Checkbox" | ||
| 46 | |||
| 47 | export { 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 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" | ||
| 5 | import { XIcon } from "lucide-react" | ||
| 6 | |||
| 7 | import { cn } from "@/lib/utils" | ||
| 8 | |||
| 9 | function Dialog({ | ||
| 10 | ...props | ||
| 11 | }: React.ComponentProps<typeof DialogPrimitive.Root>) { | ||
| 12 | return <DialogPrimitive.Root data-slot="dialog" {...props} /> | ||
| 13 | } | ||
| 14 | |||
| 15 | function DialogTrigger({ | ||
| 16 | ...props | ||
| 17 | }: React.ComponentProps<typeof DialogPrimitive.Trigger>) { | ||
| 18 | return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} /> | ||
| 19 | } | ||
| 20 | |||
| 21 | function DialogPortal({ | ||
| 22 | ...props | ||
| 23 | }: React.ComponentProps<typeof DialogPrimitive.Portal>) { | ||
| 24 | return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} /> | ||
| 25 | } | ||
| 26 | |||
| 27 | function DialogClose({ | ||
| 28 | ...props | ||
| 29 | }: React.ComponentProps<typeof DialogPrimitive.Close>) { | ||
| 30 | return <DialogPrimitive.Close data-slot="dialog-close" {...props} /> | ||
| 31 | } | ||
| 32 | |||
| 33 | function 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 | |||
| 49 | function 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 | |||
| 83 | function 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 | |||
| 93 | function 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 | |||
| 106 | function 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 | |||
| 119 | function 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 | |||
| 132 | export { | ||
| 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 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" | ||
| 5 | import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" | ||
| 6 | |||
| 7 | import { cn } from "@/lib/utils" | ||
| 8 | |||
| 9 | function DropdownMenu({ | ||
| 10 | ...props | ||
| 11 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) { | ||
| 12 | return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} /> | ||
| 13 | } | ||
| 14 | |||
| 15 | function DropdownMenuPortal({ | ||
| 16 | ...props | ||
| 17 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) { | ||
| 18 | return ( | ||
| 19 | <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} /> | ||
| 20 | ) | ||
| 21 | } | ||
| 22 | |||
| 23 | function 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 | |||
| 34 | function 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 | |||
| 54 | function DropdownMenuGroup({ | ||
| 55 | ...props | ||
| 56 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) { | ||
| 57 | return ( | ||
| 58 | <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} /> | ||
| 59 | ) | ||
| 60 | } | ||
| 61 | |||
| 62 | function 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 | |||
| 85 | function 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 | |||
| 111 | function 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 | |||
| 122 | function 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 | |||
| 146 | function 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 | |||
| 166 | function 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 | |||
| 179 | function 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 | |||
| 195 | function DropdownMenuSub({ | ||
| 196 | ...props | ||
| 197 | }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) { | ||
| 198 | return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} /> | ||
| 199 | } | ||
| 200 | |||
| 201 | function 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 | |||
| 225 | function 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 | |||
| 241 | export { | ||
| 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 @@ | |||
| 1 | import * as React from "react" | ||
| 2 | |||
| 3 | import { cn } from "@/lib/utils" | ||
| 4 | |||
| 5 | function 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 | |||
| 21 | export { 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 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as LabelPrimitive from "@radix-ui/react-label" | ||
| 5 | |||
| 6 | import { cn } from "@/lib/utils" | ||
| 7 | |||
| 8 | function 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 | |||
| 24 | export { 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 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as ProgressPrimitive from "@radix-ui/react-progress" | ||
| 5 | |||
| 6 | import { cn } from "@/lib/utils" | ||
| 7 | |||
| 8 | function 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 | |||
| 31 | export { 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 | |||
| 3 | import * as React from "react" | ||
| 4 | |||
| 5 | import { cn } from "@/lib/utils" | ||
| 6 | |||
| 7 | function 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 | |||
| 22 | function 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 | |||
| 32 | function 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 | |||
| 42 | function 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 | |||
| 55 | function 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 | |||
| 68 | function 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 | |||
| 81 | function 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 | |||
| 94 | function 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 | |||
| 107 | export { | ||
| 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 | |||
| 3 | import * as React from "react" | ||
| 4 | import * as ToastPrimitives from "@radix-ui/react-toast" | ||
| 5 | import { cva, type VariantProps } from "class-variance-authority" | ||
| 6 | import { X } from "lucide-react" | ||
| 7 | |||
| 8 | import { cn } from "@/lib/utils" | ||
| 9 | |||
| 10 | const ToastProvider = ToastPrimitives.Provider | ||
| 11 | |||
| 12 | const 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 | )) | ||
| 25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName | ||
| 26 | |||
| 27 | const 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 | |||
| 43 | const 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 | }) | ||
| 56 | Toast.displayName = ToastPrimitives.Root.displayName | ||
| 57 | |||
| 58 | const 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 | )) | ||
| 71 | ToastAction.displayName = ToastPrimitives.Action.displayName | ||
| 72 | |||
| 73 | const 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 | )) | ||
| 89 | ToastClose.displayName = ToastPrimitives.Close.displayName | ||
| 90 | |||
| 91 | const 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 | )) | ||
| 101 | ToastTitle.displayName = ToastPrimitives.Title.displayName | ||
| 102 | |||
| 103 | const 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 | )) | ||
| 113 | ToastDescription.displayName = ToastPrimitives.Description.displayName | ||
| 114 | |||
| 115 | type ToastProps = React.ComponentPropsWithoutRef<typeof Toast> | ||
| 116 | |||
| 117 | type ToastActionElement = React.ReactElement<typeof ToastAction> | ||
| 118 | |||
| 119 | export { | ||
| 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 | |||
| 3 | import { | ||
| 4 | Toast, | ||
| 5 | ToastClose, | ||
| 6 | ToastDescription, | ||
| 7 | ToastProvider, | ||
| 8 | ToastTitle, | ||
| 9 | ToastViewport, | ||
| 10 | } from "@/components/ui/toast" | ||
| 11 | import { useToast } from "@/hooks/use-toast" | ||
| 12 | |||
| 13 | export 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 | |||
| 3 | import type React from "react" | ||
| 4 | |||
| 5 | import { useState, useRef } from "react" | ||
| 6 | import { | ||
| 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" | ||
| 23 | import { Button } from "@/components/ui/button" | ||
| 24 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | ||
| 25 | import { Progress } from "@/components/ui/progress" | ||
| 26 | import { | ||
| 27 | DropdownMenu, | ||
| 28 | DropdownMenuContent, | ||
| 29 | DropdownMenuItem, | ||
| 30 | DropdownMenuTrigger, | ||
| 31 | DropdownMenuSeparator, | ||
| 32 | } from "@/components/ui/dropdown-menu" | ||
| 33 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" | ||
| 34 | import { Input } from "@/components/ui/input" | ||
| 35 | import { Label } from "@/components/ui/label" | ||
| 36 | import { Checkbox } from "@/components/ui/checkbox" | ||
| 37 | import { toast } from "@/hooks/use-toast" | ||
| 38 | import HistoryView from "./history-view" | ||
| 39 | |||
| 40 | interface 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 | |||
| 51 | const 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 | |||
| 366 | function 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 | |||
| 374 | function 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 | |||
| 383 | export 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 | |||
| 3 | import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" | ||
| 4 | import { Badge } from "@/components/ui/badge" | ||
| 5 | import { FileText, Folder, Trash2, Edit } from "lucide-react" | ||
| 6 | |||
| 7 | interface 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 | |||
| 16 | const 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 | |||
| 127 | function 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 | |||
| 139 | function 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 | |||
| 152 | function 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 | |||
| 181 | export 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 | |||
| 3 | import * as React from "react" | ||
| 4 | |||
| 5 | import type { | ||
| 6 | ToastActionElement, | ||
| 7 | ToastProps, | ||
| 8 | } from "@/components/ui/toast" | ||
| 9 | |||
| 10 | const TOAST_LIMIT = 1 | ||
| 11 | const TOAST_REMOVE_DELAY = 1000000 | ||
| 12 | |||
| 13 | type ToasterToast = ToastProps & { | ||
| 14 | id: string | ||
| 15 | title?: React.ReactNode | ||
| 16 | description?: React.ReactNode | ||
| 17 | action?: ToastActionElement | ||
| 18 | } | ||
| 19 | |||
| 20 | const 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 | |||
| 27 | let count = 0 | ||
| 28 | |||
| 29 | function genId() { | ||
| 30 | count = (count + 1) % Number.MAX_SAFE_INTEGER | ||
| 31 | return count.toString() | ||
| 32 | } | ||
| 33 | |||
| 34 | type ActionType = typeof actionTypes | ||
| 35 | |||
| 36 | type 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 | |||
| 54 | interface State { | ||
| 55 | toasts: ToasterToast[] | ||
| 56 | } | ||
| 57 | |||
| 58 | const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>() | ||
| 59 | |||
| 60 | const 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 | |||
| 76 | export 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 | |||
| 131 | const listeners: Array<(state: State) => void> = [] | ||
| 132 | |||
| 133 | let memoryState: State = { toasts: [] } | ||
| 134 | |||
| 135 | function dispatch(action: Action) { | ||
| 136 | memoryState = reducer(memoryState, action) | ||
| 137 | listeners.forEach((listener) => { | ||
| 138 | listener(memoryState) | ||
| 139 | }) | ||
| 140 | } | ||
| 141 | |||
| 142 | type Toast = Omit<ToasterToast, "id"> | ||
| 143 | |||
| 144 | function 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 | |||
| 173 | function 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 | |||
| 193 | export { 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 | /** | 1 | import { clsx, type ClassValue } from "clsx" |
| 2 | * Formats a size in bytes into a human-readable string | 2 | import { 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 | */ | ||
| 6 | export 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'] | 4 | export 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 | } |
