From f4d8a26972728891de8bde4eeb94c80f027ce2d2 Mon Sep 17 00:00:00 2001 From: diogo464 Date: Mon, 11 Aug 2025 16:04:32 +0100 Subject: basic v0 ui working --- frontend/app/drive/[...path]/page.tsx | 58 -- frontend/app/drive/page.tsx | 33 -- frontend/app/globals.css | 122 +++- frontend/app/layout.tsx | 6 +- frontend/app/page.tsx | 6 +- frontend/components.json | 21 + frontend/components/AuthButton.tsx | 31 - frontend/components/FileTable.tsx | 179 ------ frontend/components/FileUpload.tsx | 262 --------- frontend/components/ui/badge.tsx | 46 ++ frontend/components/ui/button.tsx | 59 ++ frontend/components/ui/checkbox.tsx | 47 ++ frontend/components/ui/dialog.tsx | 143 +++++ frontend/components/ui/dropdown-menu.tsx | 257 ++++++++ frontend/components/ui/input.tsx | 21 + frontend/components/ui/label.tsx | 24 + frontend/components/ui/progress.tsx | 31 + frontend/components/ui/table.tsx | 116 ++++ frontend/components/ui/toast.tsx | 129 +++++ frontend/components/ui/toaster.tsx | 35 ++ frontend/file-drive.tsx | 936 ++++++++++++++++++++++++++++++ frontend/history-view.tsx | 230 ++++++++ frontend/hooks/use-toast.ts | 193 +++++++ frontend/lib/utils.ts | 29 +- frontend/package-lock.json | 965 ++++++++++++++++++++++++++++++- frontend/package.json | 22 +- 26 files changed, 3384 insertions(+), 617 deletions(-) delete mode 100644 frontend/app/drive/[...path]/page.tsx delete mode 100644 frontend/app/drive/page.tsx create mode 100644 frontend/components.json delete mode 100644 frontend/components/AuthButton.tsx delete mode 100644 frontend/components/FileTable.tsx delete mode 100644 frontend/components/FileUpload.tsx create mode 100644 frontend/components/ui/badge.tsx create mode 100644 frontend/components/ui/button.tsx create mode 100644 frontend/components/ui/checkbox.tsx create mode 100644 frontend/components/ui/dialog.tsx create mode 100644 frontend/components/ui/dropdown-menu.tsx create mode 100644 frontend/components/ui/input.tsx create mode 100644 frontend/components/ui/label.tsx create mode 100644 frontend/components/ui/progress.tsx create mode 100644 frontend/components/ui/table.tsx create mode 100644 frontend/components/ui/toast.tsx create mode 100644 frontend/components/ui/toaster.tsx create mode 100644 frontend/file-drive.tsx create mode 100644 frontend/history-view.tsx create mode 100644 frontend/hooks/use-toast.ts 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 @@ -import { Drive_ls } from "@/lib/drive_server" -import { Drive_parent } from "@/lib/drive_shared" -import Link from "next/link" -import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth"; -import FileUpload from "@/components/FileUpload" -import FileTable from "@/components/FileTable" - -interface DrivePageProps { - params: Promise<{ - path: string[] - }> -} - -export default async function DrivePage({ params }: DrivePageProps) { - // Await params as required by Next.js 15 - const { path } = await params - - const user = await Auth_get_user(); - - // Construct the full path from params - const fullPath = path ? `/${path.join('/')}` : "" - - const entries = await Drive_ls(fullPath, false) - - // Check if we have a parent directory - const parentDir = Drive_parent(fullPath) - const parentPath = path && path.length > 1 - ? `/drive/${path.slice(0, -1).join('/')}` - : path && path.length === 1 - ? '/drive' - : null - - const showParent = parentDir !== null && parentPath !== null - - return ( -
-
- -

FCTDrive

- - - - - {/* File Upload Component */} - {Auth_user_can_upload(user) && ( - - )} -
-
- ) -} 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 @@ -import { Drive_ls } from "@/lib/drive_server" -import { Auth_get_user, Auth_user_can_upload } from "@/lib/auth" -import Link from "next/link" -import FileUpload from "@/components/FileUpload" -import FileTable from "@/components/FileTable" - -export default async function DrivePage() { - const user = await Auth_get_user() - const entries = await Drive_ls("", false) - - return ( -
-
- -

FCTDrive

- - - - - {/* File Upload Component */} - {Auth_user_can_upload(user) && ( - - )} -
-
- ) -} \ 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 @@ @import "tailwindcss"; +@import "tw-animate-css"; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } 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 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; -import AuthButton from "@/components/AuthButton"; +import { Toaster } from "@/components/ui/toaster"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -28,10 +28,8 @@ export default function RootLayout({ -
- -
{children} + ); 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 @@ -import { redirect } from 'next/navigation' +import FileDrive from "../file-drive" -export default function Home() { - redirect('/drive') +export default function Page() { + return } 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 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ 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 @@ -import { Auth_get_user, Auth_tinyauth_public_endpoint } from '@/lib/auth' - -export default async function AuthButton() { - const user = await Auth_get_user() - const authEndpoint = Auth_tinyauth_public_endpoint() - - if (user.isLoggedIn) { - return ( -
- - {user.email} - - - Logout - -
- ) - } - - return ( - - Login - - ) -} \ 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 @@ -'use client' - -import { useState } from 'react' -import { DriveLsEntry } from '@/lib/drive_types' -import { Drive_basename } from '@/lib/drive_shared' -import { formatSize } from '@/lib/utils' -import Link from 'next/link' - -interface FileTableEntry extends DriveLsEntry { - isParent?: boolean - parentPath?: string -} - -interface FileTableProps { - entries: DriveLsEntry[] - currentPath?: string - showParent?: boolean - parentPath?: string - onSelectedFilesChange?: (selectedFiles: string[]) => void -} - -export default function FileTable({ - entries, - currentPath = '', - showParent = false, - parentPath, - onSelectedFilesChange -}: FileTableProps) { - const [selectedFiles, setSelectedFiles] = useState>(new Set()) - - // Create entries with optional parent directory at top - const allEntries: FileTableEntry[] = [] - if (showParent && parentPath !== undefined) { - allEntries.push({ - path: '(parent)', - type: 'dir' as const, - lastmod: 0, - blob: null, - size: null, - author: '', - isParent: true, - parentPath - }) - } - - // Sort entries: directories first, then files, both alphabetically - const sortedEntries = entries.sort((a, b) => { - // First sort by type (directories before files) - if (a.type !== b.type) { - return a.type === 'dir' ? -1 : 1 - } - // Then sort alphabetically by path - return a.path.localeCompare(b.path) - }) - - allEntries.push(...sortedEntries) - - const handleFileSelection = (filePath: string, isSelected: boolean) => { - const newSelectedFiles = new Set(selectedFiles) - if (isSelected) { - newSelectedFiles.add(filePath) - } else { - newSelectedFiles.delete(filePath) - } - setSelectedFiles(newSelectedFiles) - onSelectedFilesChange?.(Array.from(newSelectedFiles)) - } - - const handleSelectAll = (isSelected: boolean) => { - if (isSelected) { - // Select all files (not directories or parent) - const fileEntries = allEntries.filter(entry => - entry.type === 'file' && !entry.isParent - ) - const newSelectedFiles = new Set(fileEntries.map(entry => entry.path)) - setSelectedFiles(newSelectedFiles) - onSelectedFilesChange?.(Array.from(newSelectedFiles)) - } else { - setSelectedFiles(new Set()) - onSelectedFilesChange?.([]) - } - } - - const selectableFiles = allEntries.filter(entry => - entry.type === 'file' && !entry.isParent - ) - const allFilesSelected = selectableFiles.length > 0 && - selectableFiles.every(entry => selectedFiles.has(entry.path)) - - return ( -
-
- - - - - - - - - - - - {allEntries.map((entry) => ( - - - - - - - - ))} - -
- handleSelectAll(e.target.checked)} - className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" - disabled={selectableFiles.length === 0} - /> - - Name - - Size - - Author - - Modified -
- {entry.type === 'file' && !entry.isParent ? ( - handleFileSelection(entry.path, e.target.checked)} - className="rounded border-gray-300 text-blue-600 focus:ring-blue-500" - /> - ) : ( -
// Placeholder to maintain alignment - )} -
-
-
- {entry.type === 'dir' ? ( -
📁
- ) : ( -
📄
- )} -
- {entry.type === 'dir' ? ( - - {entry.isParent ? '(parent)' : Drive_basename(entry.path)} - - ) : ( - - {Drive_basename(entry.path)} - - )} -
-
- {formatSize(entry.size)} - - {entry.author} - - {entry.lastmod > 0 ? new Date(entry.lastmod * 1000).toLocaleString() : ''} -
-
-
- ) -} \ 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 @@ -'use client' - -import { useState, useRef } from 'react' -import { UPLOAD_MAX_FILES, UPLOAD_MAX_FILE_SIZE } from '@/lib/constants' - -// Client-side file validation function -function validateFile(file: File): { allowed: boolean; reason?: string } { - if (file.size > UPLOAD_MAX_FILE_SIZE) { - return { allowed: false, reason: `File size exceeds ${UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB limit` }; - } - - return { allowed: true }; -} - -interface FileUploadProps { - targetPath: string - onUploadComplete?: () => void -} - -interface UploadResult { - filename: string - success: boolean - message: string -} - -export default function FileUpload({ targetPath, onUploadComplete }: FileUploadProps) { - const [isDragOver, setIsDragOver] = useState(false) - const [isUploading, setIsUploading] = useState(false) - const [selectedFiles, setSelectedFiles] = useState([]) - const [uploadResults, setUploadResults] = useState([]) - const [showResults, setShowResults] = useState(false) - const fileInputRef = useRef(null) - - const handleFileSelect = (files: FileList) => { - const fileArray = Array.from(files) - - // Validate file count - if (fileArray.length > UPLOAD_MAX_FILES) { - alert(`Too many files selected. Maximum ${UPLOAD_MAX_FILES} files allowed.`) - return - } - - // Validate each file - const validFiles: File[] = [] - for (const file of fileArray) { - const validation = validateFile(file) - if (!validation.allowed) { - alert(`File '${file.name}': ${validation.reason}`) - continue - } - validFiles.push(file) - } - - setSelectedFiles(validFiles) - setUploadResults([]) - setShowResults(false) - } - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault() - setIsDragOver(true) - } - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault() - setIsDragOver(false) - } - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault() - setIsDragOver(false) - - if (e.dataTransfer.files) { - handleFileSelect(e.dataTransfer.files) - } - } - - const handleFileInputChange = (e: React.ChangeEvent) => { - if (e.target.files) { - handleFileSelect(e.target.files) - } - } - - const handleUpload = async () => { - if (selectedFiles.length === 0) return - - setIsUploading(true) - setUploadResults([]) - - try { - const formData = new FormData() - selectedFiles.forEach(file => { - formData.append('files', file) - }) - formData.append('targetPath', targetPath) - - const response = await fetch('/api/upload', { - method: 'POST', - body: formData, - }) - - const result = await response.json() - - if (response.ok) { - setUploadResults(result.results || []) - setShowResults(true) - setSelectedFiles([]) - - // Clear file input - if (fileInputRef.current) { - fileInputRef.current.value = '' - } - - // Refresh the page after successful upload - setTimeout(() => { - window.location.reload() - }, 1000) - } else { - alert(`Upload failed: ${result.error}`) - } - } catch (error) { - console.error('Upload error:', error) - alert('Upload failed: Network error') - } finally { - setIsUploading(false) - } - } - - const removeFile = (index: number) => { - setSelectedFiles(prev => prev.filter((_, i) => i !== index)) - } - - const clearResults = () => { - setShowResults(false) - setUploadResults([]) - } - - return ( -
-

Upload Files

- - {/* Upload Results */} - {showResults && uploadResults.length > 0 && ( -
-
-

Upload Results

- -
-
- {uploadResults.map((result, index) => ( -
- {result.success ? '✓' : '✗'} - {result.filename}: - {result.message} -
- ))} -
-
- )} - - {/* File Drop Zone */} -
-
-
📁
-
-

Drop files here or click to browse

-

- Maximum {UPLOAD_MAX_FILES} files, {UPLOAD_MAX_FILE_SIZE / (1024 * 1024)}MB each -

-
- - -
-
- - {/* Selected Files */} - {selectedFiles.length > 0 && ( -
-

- Selected Files ({selectedFiles.length}) -

-
- {selectedFiles.map((file, index) => ( -
-
- 📄 -
-
- {file.name} -
-
- {(file.size / 1024 / 1024).toFixed(2)} MB -
-
-
- -
- ))} -
- - -
- )} -
- ) -} \ 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 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +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 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +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 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { CheckIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + indeterminate?: boolean + } +>(({ className, indeterminate, ...props }, ref) => { + const checkboxRef = React.useRef(null) + + React.useImperativeHandle(ref, () => checkboxRef.current!) + + React.useEffect(() => { + if (checkboxRef.current) { + checkboxRef.current.indeterminate = indeterminate ?? false + } + }, [indeterminate]) + + return ( + + + + + + ) +}) + +Checkbox.displayName = "Checkbox" + +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 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + showCloseButton = true, + ...props +}: React.ComponentProps & { + showCloseButton?: boolean +}) { + return ( + + + + {children} + {showCloseButton && ( + + + Close + + )} + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} 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 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} 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 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +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 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +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 @@ +"use client" + +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +function Progress({ + className, + value, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +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 @@ +"use client" + +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Table({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ) +} + +function TableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ) +} + +function TableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + + ) +} + +function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", + className + )} + {...props} + /> + ) +} + +function TableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ) +} + +function TableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ) +} + +function TableCaption({ + className, + ...props +}: React.ComponentProps<"caption">) { + return ( +
+ ) +} + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} 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 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "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", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} \ 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 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/hooks/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} \ 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 @@ +"use client" + +import type React from "react" + +import { useState, useRef } from "react" +import { + ChevronRight, + ChevronDown, + File, + Folder, + Upload, + Trash2, + Move, + MoreHorizontal, + HardDrive, + Edit, + Link, + Info, + LogIn, + LogOut, + HistoryIcon, +} from "lucide-react" +import { Button } from "@/components/ui/button" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Progress } from "@/components/ui/progress" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu" +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Checkbox } from "@/components/ui/checkbox" +import { toast } from "@/hooks/use-toast" +import HistoryView from "./history-view" + +interface FileItem { + id: string + name: string + type: "file" | "folder" + size: number + modified: string + modifiedTime: string + modifiedBy: string + children?: FileItem[] +} + +const mockData: FileItem[] = [ + { + id: "1", + name: "Documents and Important Files", + type: "folder", + size: 25728640, + modified: "2024-01-15", + modifiedTime: "14:32", + modifiedBy: "John Smith", + children: [ + { + id: "1-1", + name: "Work Projects and Client Reports", + type: "folder", + size: 18388608, + modified: "2024-01-10", + modifiedTime: "09:15", + modifiedBy: "Sarah Johnson", + children: [ + { + id: "1-1-1", + name: "Client ABC Corporation", + type: "folder", + size: 12388608, + modified: "2024-01-09", + modifiedTime: "16:45", + modifiedBy: "Mike Davis", + children: [ + { + id: "1-1-1-1", + name: "Quarterly Financial Analysis and Market Research Report Q4 2023.pdf", + type: "file", + size: 4194304, + modified: "2024-01-09", + modifiedTime: "16:30", + modifiedBy: "Mike Davis", + }, + { + id: "1-1-1-2", + name: "Strategic Business Development Plan and Implementation Timeline.docx", + type: "file", + size: 2097152, + modified: "2024-01-08", + modifiedTime: "11:20", + modifiedBy: "Emily Chen", + }, + { + id: "1-1-1-3", + name: "Meeting Notes and Action Items", + type: "folder", + size: 6097152, + modified: "2024-01-07", + modifiedTime: "13:45", + modifiedBy: "Alex Rodriguez", + children: [ + { + id: "1-1-1-3-1", + name: "Weekly Status Meeting Notes January 2024 - Detailed Summary.txt", + type: "file", + size: 1048576, + modified: "2024-01-07", + modifiedTime: "13:30", + modifiedBy: "Alex Rodriguez", + }, + { + id: "1-1-1-3-2", + name: "Project Kickoff Meeting Transcript and Stakeholder Feedback.docx", + type: "file", + size: 3048576, + modified: "2024-01-06", + modifiedTime: "15:10", + modifiedBy: "Lisa Wang", + }, + { + id: "1-1-1-3-3", + name: "Budget Review and Resource Allocation Discussion Points.xlsx", + type: "file", + size: 2000000, + modified: "2024-01-05", + modifiedTime: "10:25", + modifiedBy: "David Brown", + }, + ], + }, + ], + }, + { + id: "1-1-2", + name: "Internal Company Documentation and Policies", + type: "folder", + size: 6000000, + modified: "2024-01-08", + modifiedTime: "08:30", + modifiedBy: "HR Department", + children: [ + { + id: "1-1-2-1", + name: "Employee Handbook 2024 - Complete Guide with Benefits and Procedures.pdf", + type: "file", + size: 3500000, + modified: "2024-01-08", + modifiedTime: "08:15", + modifiedBy: "HR Department", + }, + { + id: "1-1-2-2", + name: "IT Security Policies and Data Protection Guidelines - Updated Version.docx", + type: "file", + size: 2500000, + modified: "2024-01-07", + modifiedTime: "17:40", + modifiedBy: "IT Security Team", + }, + ], + }, + ], + }, + { + id: "1-2", + name: "Personal Documents and Certificates", + type: "folder", + size: 7340032, + modified: "2024-01-12", + modifiedTime: "12:15", + modifiedBy: "John Smith", + children: [ + { + id: "1-2-1", + name: "Professional Resume and Cover Letter Templates - Updated 2024.docx", + type: "file", + size: 1048576, + modified: "2024-01-12", + modifiedTime: "12:00", + modifiedBy: "John Smith", + }, + { + id: "1-2-2", + name: "Educational Certificates and Professional Qualifications Portfolio.pdf", + type: "file", + size: 6291456, + modified: "2024-01-11", + modifiedTime: "14:30", + modifiedBy: "John Smith", + }, + ], + }, + ], + }, + { + id: "2", + name: "Media Files and Creative Assets", + type: "folder", + size: 152428800, + modified: "2024-01-14", + modifiedTime: "16:20", + modifiedBy: "Creative Team", + children: [ + { + id: "2-1", + name: "Photography and Visual Content", + type: "folder", + size: 75428800, + modified: "2024-01-14", + modifiedTime: "16:15", + modifiedBy: "Creative Team", + children: [ + { + id: "2-1-1", + name: "Travel and Vacation Photos Collection", + type: "folder", + size: 45428800, + modified: "2024-01-14", + modifiedTime: "16:10", + modifiedBy: "John Smith", + children: [ + { + id: "2-1-1-1", + name: "Summer Vacation 2023 - Beach Resort and Mountain Hiking Adventure Photos.jpg", + type: "file", + size: 15145728, + modified: "2024-01-14", + modifiedTime: "16:05", + modifiedBy: "John Smith", + }, + { + id: "2-1-1-2", + name: "European City Tour - Architecture and Cultural Landmarks Photography Collection.jpg", + type: "file", + size: 18283072, + modified: "2024-01-13", + modifiedTime: "19:30", + modifiedBy: "John Smith", + }, + { + id: "2-1-1-3", + name: "Wildlife Photography Safari - African Animals and Natural Landscapes.jpg", + type: "file", + size: 12000000, + modified: "2024-01-12", + modifiedTime: "20:45", + modifiedBy: "John Smith", + }, + ], + }, + { + id: "2-1-2", + name: "Professional Headshots and Corporate Event Photography - High Resolution.png", + type: "file", + size: 30000000, + modified: "2024-01-13", + modifiedTime: "11:15", + modifiedBy: "Professional Photographer", + }, + ], + }, + { + id: "2-2", + name: "Video Content and Multimedia Projects", + type: "folder", + size: 77000000, + modified: "2024-01-13", + modifiedTime: "14:20", + modifiedBy: "Video Production Team", + children: [ + { + id: "2-2-1", + name: "Corporate Training Videos and Educational Content Series - Complete Collection.mp4", + type: "file", + size: 45000000, + modified: "2024-01-13", + modifiedTime: "14:15", + modifiedBy: "Video Production Team", + }, + { + id: "2-2-2", + name: "Product Demo and Marketing Presentation Video - 4K Quality.mov", + type: "file", + size: 32000000, + modified: "2024-01-12", + modifiedTime: "16:30", + modifiedBy: "Marketing Team", + }, + ], + }, + ], + }, + { + id: "3", + name: "Development Projects and Source Code Repository", + type: "folder", + size: 89715200, + modified: "2024-01-16", + modifiedTime: "18:45", + modifiedBy: "Development Team", + children: [ + { + id: "3-1", + name: "Web Applications and Frontend Projects", + type: "folder", + size: 45000000, + modified: "2024-01-16", + modifiedTime: "18:40", + modifiedBy: "Frontend Team", + children: [ + { + id: "3-1-1", + name: "E-commerce Platform with React and Node.js - Full Stack Implementation.zip", + type: "file", + size: 25000000, + modified: "2024-01-16", + modifiedTime: "18:35", + modifiedBy: "Lead Developer", + }, + { + id: "3-1-2", + name: "Dashboard Analytics Tool with Real-time Data Visualization Components.zip", + type: "file", + size: 20000000, + modified: "2024-01-15", + modifiedTime: "22:10", + modifiedBy: "Data Team", + }, + ], + }, + { + id: "3-2", + name: "Mobile App Development and Cross-Platform Solutions.zip", + type: "file", + size: 44715200, + modified: "2024-01-14", + modifiedTime: "13:25", + modifiedBy: "Mobile Team", + }, + ], + }, + { + id: "4", + name: "Configuration Files and System Settings - Development Environment Setup.txt", + type: "file", + size: 4096, + modified: "2024-01-16", + modifiedTime: "09:30", + modifiedBy: "System Admin", + }, + { + id: "5", + name: "Database Backup and Migration Scripts - Production Environment.sql", + type: "file", + size: 8192, + modified: "2024-01-15", + modifiedTime: "23:45", + modifiedBy: "Database Admin", + }, +] + +function formatFileSize(bytes: number): string { + if (bytes === 0) return "0 Bytes" + const k = 1024 + const sizes = ["Bytes", "KB", "MB", "GB"] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + return Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i] +} + +function calculateTotalSize(items: FileItem[]): number { + return items.reduce((total, item) => { + if (item.type === "folder" && item.children) { + return total + calculateTotalSize(item.children) + } + return total + item.size + }, 0) +} + +export default function FileDrive() { + const [files, setFiles] = useState(mockData) + const [expandedFolders, setExpandedFolders] = useState>(new Set()) + const [selectedFiles, setSelectedFiles] = useState>(new Set()) + const [renameDialogOpen, setRenameDialogOpen] = useState(false) + const [infoDialogOpen, setInfoDialogOpen] = useState(false) + const [currentItem, setCurrentItem] = useState(null) + const [newName, setNewName] = useState("") + const fileInputRef = useRef(null) + + const [isLoggedIn, setIsLoggedIn] = useState(true) // Mock logged in state + const [currentView, setCurrentView] = useState<"drive" | "history">("drive") + const [uploadToFolder, setUploadToFolder] = useState(null) + + const maxStorage = 1073741824 // 1GB + const usedStorage = calculateTotalSize(files) + const storagePercentage = (usedStorage / maxStorage) * 100 + + const toggleFolder = (folderId: string) => { + const newExpanded = new Set(expandedFolders) + if (newExpanded.has(folderId)) { + newExpanded.delete(folderId) + } else { + newExpanded.add(folderId) + } + setExpandedFolders(newExpanded) + } + + const toggleFileSelection = (fileId: string) => { + const newSelected = new Set(selectedFiles) + if (newSelected.has(fileId)) { + newSelected.delete(fileId) + } else { + newSelected.add(fileId) + } + setSelectedFiles(newSelected) + } + + const selectAll = () => { + const getAllIds = (items: FileItem[]): string[] => { + const ids: string[] = [] + items.forEach((item) => { + ids.push(item.id) + if (item.children) { + ids.push(...getAllIds(item.children)) + } + }) + return ids + } + setSelectedFiles(new Set(getAllIds(files))) + } + + const deselectAll = () => { + setSelectedFiles(new Set()) + } + + const openRenameDialog = (item: FileItem) => { + setCurrentItem(item) + setNewName(item.name) + setRenameDialogOpen(true) + } + + const openInfoDialog = (item: FileItem) => { + setCurrentItem(item) + setInfoDialogOpen(true) + } + + const copyPermalink = (item: FileItem) => { + const permalink = `${window.location.origin}/drive/file/${item.id}` + navigator.clipboard.writeText(permalink).then(() => { + toast({ + title: "Link copied!", + description: "Permalink has been copied to clipboard", + }) + }) + } + + const handleRename = () => { + if (currentItem && newName.trim()) { + const renameInArray = (items: FileItem[]): FileItem[] => { + return items.map((item) => { + if (item.id === currentItem.id) { + return { ...item, name: newName.trim() } + } + if (item.children) { + return { ...item, children: renameInArray(item.children) } + } + return item + }) + } + setFiles(renameInArray(files)) + setRenameDialogOpen(false) + setCurrentItem(null) + setNewName("") + toast({ + title: "Renamed successfully", + description: `Item renamed to "${newName.trim()}"`, + }) + } + } + + const handleFileUpload = (event: React.ChangeEvent) => { + const uploadedFiles = event.target.files + if (uploadedFiles) { + const newFiles = Array.from(uploadedFiles).map((file, index) => ({ + id: `upload-${Date.now()}-${index}`, + name: file.name, + type: "file" as const, + size: file.size, + modified: new Date().toISOString().split("T")[0], + modifiedTime: new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }), + modifiedBy: "Current User", + })) + setFiles([...files, ...newFiles]) + } + } + + const deleteItems = (itemIds: string[]) => { + const deleteFromArray = (items: FileItem[]): FileItem[] => { + return items.filter((item) => { + if (itemIds.includes(item.id)) return false + if (item.children) { + item.children = deleteFromArray(item.children) + } + return true + }) + } + setFiles(deleteFromArray(files)) + // Remove deleted items from selection + const newSelected = new Set(selectedFiles) + itemIds.forEach((id) => newSelected.delete(id)) + setSelectedFiles(newSelected) + } + + const handleLogin = () => { + // Redirect to external auth page (configured via env var) + const authUrl = process.env.NEXT_PUBLIC_AUTH_URL || "/auth/login" + window.location.href = authUrl + } + + const handleLogout = () => { + // Handle logout (would typically clear tokens, etc.) + setIsLoggedIn(false) + // Could also redirect to logout endpoint + } + + const handleFolderUpload = (event: React.ChangeEvent, folderId: string) => { + const uploadedFiles = event.target.files + if (uploadedFiles) { + const newFiles = Array.from(uploadedFiles).map((file, index) => ({ + id: `upload-${Date.now()}-${index}`, + name: file.name, + type: "file" as const, + size: file.size, + modified: new Date().toISOString().split("T")[0], + modifiedTime: new Date().toLocaleTimeString("en-US", { hour12: false, hour: "2-digit", minute: "2-digit" }), + modifiedBy: "Current User", + })) + + // Add files to the specific folder + const addToFolder = (items: FileItem[]): FileItem[] => { + return items.map((item) => { + if (item.id === folderId && item.type === "folder") { + return { + ...item, + children: [...(item.children || []), ...newFiles], + size: item.size + newFiles.reduce((total, file) => total + file.size, 0), + } + } + if (item.children) { + return { ...item, children: addToFolder(item.children) } + } + return item + }) + } + + setFiles(addToFolder(files)) + toast({ + title: "Files uploaded successfully", + description: `${newFiles.length} file(s) uploaded to folder`, + }) + } + // Reset the input + event.target.value = "" + setUploadToFolder(null) + } + + const openFolderUpload = (folderId: string) => { + setUploadToFolder(folderId) + // Trigger file input click after state is set + setTimeout(() => { + const input = document.getElementById(`folder-upload-${folderId}`) as HTMLInputElement + input?.click() + }, 0) + } + + const renderFileRow = (item: FileItem, level = 0): React.ReactNode[] => { + const isExpanded = expandedFolders.has(item.id) + const isSelected = selectedFiles.has(item.id) + const rows: React.ReactNode[] = [] + + rows.push( + + + toggleFileSelection(item.id)} /> + + +
+ {item.type === "folder" && ( + + )} + {item.type === "folder" ? ( + + ) : ( + + )} + {item.name} +
+
+ {formatFileSize(item.size)} + {item.modified} + + + + + + + {item.type === "folder" && ( + <> + openFolderUpload(item.id)}> + + Upload to Folder + + + + )} + openRenameDialog(item)}> + + Rename + + copyPermalink(item)}> + + Copy Permalink + + openInfoDialog(item)}> + + Info + + + { + if (selectedFiles.size > 0) { + console.log("Moving selected files to:", item.type === "folder" ? item.id : "parent of " + item.id) + setSelectedFiles(new Set()) + } + }} + disabled={selectedFiles.size === 0} + className={selectedFiles.size === 0 ? "opacity-50 cursor-not-allowed" : ""} + > + + Move Here {selectedFiles.size > 0 && `(${selectedFiles.size})`} + + deleteItems([item.id])} className="text-red-600"> + + Delete + + + + +
, + ) + + if (item.type === "folder" && item.children && isExpanded) { + item.children.forEach((child) => { + rows.push(...renderFileRow(child, level + 1)) + }) + } + + return rows + } + + return ( +
+ {/* Header */} +
+
+
+ +

My Drive

+
+
+ + +
+
+
+ {currentView === "drive" && ( + + )} + {isLoggedIn ? ( + + ) : ( + + )} +
+
+ + {currentView === "drive" ? ( + <> + {/* Storage Info */} +
+
+ Storage Usage + + {formatFileSize(usedStorage)} of {formatFileSize(maxStorage)} used + +
+ +
+ {storagePercentage.toFixed(1)}% used + {formatFileSize(maxStorage - usedStorage)} available +
+
+ + {/* Bulk Actions */} + {selectedFiles.size > 0 && ( +
+
+ + {selectedFiles.size} item{selectedFiles.size !== 1 ? "s" : ""} selected + + +
+
+ +
+
+ )} + + {/* File Table */} +
+ + + + + 0 && + selectedFiles.size === + (() => { + const getAllIds = (items: FileItem[]): string[] => { + const ids: string[] = [] + items.forEach((item) => { + ids.push(item.id) + if (item.children) { + ids.push(...getAllIds(item.children)) + } + }) + return ids + } + return getAllIds(files).length + })() + } + indeterminate={ + selectedFiles.size > 0 && + selectedFiles.size < + (() => { + const getAllIds = (items: FileItem[]): string[] => { + const ids: string[] = [] + items.forEach((item) => { + ids.push(item.id) + if (item.children) { + ids.push(...getAllIds(item.children)) + } + }) + return ids + } + return getAllIds(files).length + })() + } + onCheckedChange={(checked) => { + if (checked) { + selectAll() + } else { + deselectAll() + } + }} + /> + + Name + Size + Modified + Actions + + + {files.flatMap((file) => renderFileRow(file))} +
+
+ + ) : ( + + )} + + {/* Rename Dialog */} + + + + Rename {currentItem?.type === "folder" ? "Folder" : "File"} + +
+
+ + setNewName(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + handleRename() + } + }} + placeholder="Enter new name" + /> +
+
+ + +
+
+
+
+ + {/* Info Dialog */} + + + + + {currentItem?.type === "folder" ? ( + + ) : ( + + )} + {currentItem?.type === "folder" ? "Folder" : "File"} Information + + + {currentItem && ( +
+
+
+ +

{currentItem.name}

+
+
+ +

{formatFileSize(currentItem.size)}

+
+
+ +

+ {currentItem.modified} at {currentItem.modifiedTime} +

+
+
+ +

{currentItem.modifiedBy}

+
+
+ +

{currentItem.type}

+
+
+ +

{currentItem.id}

+
+
+
+ +
+
+ )} +
+
+ + {/* Hidden file inputs for folder uploads */} + {(() => { + const getAllFolders = (items: FileItem[]): FileItem[] => { + const folders: FileItem[] = [] + items.forEach((item) => { + if (item.type === "folder") { + folders.push(item) + if (item.children) { + folders.push(...getAllFolders(item.children)) + } + } + }) + return folders + } + return getAllFolders(files).map((folder) => ( + handleFolderUpload(e, folder.id)} + /> + )) + })()} +
+ ) +} 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 @@ +"use client" + +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { Badge } from "@/components/ui/badge" +import { FileText, Folder, Trash2, Edit } from "lucide-react" + +interface HistoryEntry { + id: string + type: "file_create" | "file_remove" | "dir_create" | "rename" + fileName: string + userEmail: string + timestamp: string + details?: string +} + +const mockHistoryData: HistoryEntry[] = [ + { + id: "h1", + type: "file_create", + fileName: "Database Backup and Migration Scripts - Production Environment.sql", + userEmail: "admin@company.com", + timestamp: "2024-01-16T09:30:00Z", + }, + { + id: "h2", + type: "rename", + fileName: "Configuration Files and System Settings - Development Environment Setup.txt", + userEmail: "admin@company.com", + timestamp: "2024-01-16T09:25:00Z", + details: "Renamed from 'config.txt'", + }, + { + id: "h3", + type: "file_create", + fileName: "E-commerce Platform with React and Node.js - Full Stack Implementation.zip", + userEmail: "lead.dev@company.com", + timestamp: "2024-01-16T08:35:00Z", + }, + { + id: "h4", + type: "dir_create", + fileName: "Web Applications and Frontend Projects", + userEmail: "frontend.team@company.com", + timestamp: "2024-01-16T08:30:00Z", + }, + { + id: "h5", + type: "file_create", + fileName: "Dashboard Analytics Tool with Real-time Data Visualization Components.zip", + userEmail: "data.team@company.com", + timestamp: "2024-01-15T22:10:00Z", + }, + { + id: "h6", + type: "file_remove", + fileName: "old_backup_file.sql", + userEmail: "admin@company.com", + timestamp: "2024-01-15T20:45:00Z", + }, + { + id: "h7", + type: "rename", + fileName: "Mobile App Development and Cross-Platform Solutions.zip", + userEmail: "mobile.team@company.com", + timestamp: "2024-01-14T13:25:00Z", + details: "Renamed from 'mobile_app_v1.zip'", + }, + { + id: "h8", + type: "file_create", + fileName: "Corporate Training Videos and Educational Content Series - Complete Collection.mp4", + userEmail: "video.production@company.com", + timestamp: "2024-01-13T14:15:00Z", + }, + { + id: "h9", + type: "dir_create", + fileName: "Video Content and Multimedia Projects", + userEmail: "creative.team@company.com", + timestamp: "2024-01-13T14:00:00Z", + }, + { + id: "h10", + type: "file_remove", + fileName: "temp_presentation.pptx", + userEmail: "john.smith@company.com", + timestamp: "2024-01-13T11:30:00Z", + }, + { + id: "h11", + type: "file_create", + fileName: "Professional Headshots and Corporate Event Photography - High Resolution.png", + userEmail: "photographer@company.com", + timestamp: "2024-01-13T11:15:00Z", + }, + { + id: "h12", + type: "rename", + fileName: "Travel and Vacation Photos Collection", + userEmail: "john.smith@company.com", + timestamp: "2024-01-12T20:50:00Z", + details: "Renamed from 'Vacation Photos'", + }, + { + id: "h13", + type: "dir_create", + fileName: "Photography and Visual Content", + userEmail: "creative.team@company.com", + timestamp: "2024-01-12T16:00:00Z", + }, + { + id: "h14", + type: "file_create", + fileName: "Professional Resume and Cover Letter Templates - Updated 2024.docx", + userEmail: "john.smith@company.com", + timestamp: "2024-01-12T12:00:00Z", + }, + { + id: "h15", + type: "file_remove", + fileName: "draft_document.docx", + userEmail: "sarah.johnson@company.com", + timestamp: "2024-01-11T16:20:00Z", + }, +] + +function formatTimestamp(timestamp: string): string { + const date = new Date(timestamp) + return date.toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: true, + }) +} + +function getActionIcon(type: HistoryEntry["type"]) { + switch (type) { + case "file_create": + return + case "dir_create": + return + case "file_remove": + return + case "rename": + return + } +} + +function getActionBadge(type: HistoryEntry["type"]) { + switch (type) { + case "file_create": + return ( + + File Created + + ) + case "dir_create": + return ( + + Directory Created + + ) + case "file_remove": + return ( + + File Removed + + ) + case "rename": + return ( + + Renamed + + ) + } +} + +export default function HistoryView() { + return ( +
+ {/* History Header */} +
+
+

Activity History

+

Recent filesystem modifications and changes

+
+ {mockHistoryData.length} total entries +
+ + {/* History Table */} +
+ + + + Action + Type + File/Directory Name + User + Timestamp + Details + + + + {mockHistoryData.map((entry) => ( + + {getActionIcon(entry.type)} + {getActionBadge(entry.type)} + + {entry.fileName} + + + {entry.userEmail} + + + {formatTimestamp(entry.timestamp)} + + + {entry.details || "—"} + + + ))} + +
+
+
+ ) +} 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 @@ +"use client" + +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +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 @@ -/** - * Formats a size in bytes into a human-readable string - * @param bytes Size in bytes - * @returns Formatted size string (e.g., "1.5 KB", "2.3 MB", "1.2 GB") - */ -export function formatSize(bytes: number | null): string { - if (bytes === null || bytes === 0) { - return '-' - } +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" - const units = ['B', 'KB', 'MB', 'GB', 'TB'] - let size = bytes - let unitIndex = 0 - - while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024 - unitIndex++ - } - - // Format with appropriate decimal places - if (size < 10 && unitIndex > 0) { - return `${size.toFixed(1)} ${units[unitIndex]}` - } else { - return `${Math.round(size)} ${units[unitIndex]}` - } +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) } - 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 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-toast": "^1.2.14", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.539.0", "next": "15.4.6", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -21,6 +32,7 @@ "eslint": "^9", "eslint-config-next": "15.4.6", "tailwindcss": "^4", + "tw-animate-css": "^1.3.6", "typescript": "^5" } }, @@ -225,6 +237,44 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.3" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -966,6 +1016,724 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", + "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.14.tgz", + "integrity": "sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1311,7 +2079,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -1321,7 +2089,7 @@ "version": "19.1.7", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.7.tgz", "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -1947,6 +2715,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2308,12 +3088,33 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2385,7 +3186,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2520,6 +3321,12 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -3392,6 +4199,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4492,6 +5308,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "0.539.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz", + "integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", @@ -5096,6 +5921,75 @@ "dev": true, "license": "MIT" }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5706,6 +6600,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", @@ -5831,6 +6735,16 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.6.tgz", + "integrity": "sha512-9dy0R9UsYEGmgf26L8UcHiLmSFTHa9+D7+dAt/G/sF5dCnPePZbfgDYinc7/UzAM7g/baVrmS6m9yEpU46d+LA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6007,6 +6921,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "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 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-toast": "^1.2.14", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.539.0", + "next": "15.4.6", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.4.6" + "tailwind-merge": "^3.3.1" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.4.6", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "tw-animate-css": "^1.3.6", + "typescript": "^5" } } -- cgit