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/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 +++++ 14 files changed, 908 insertions(+), 472 deletions(-) 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 (limited to 'frontend/components') 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 -- cgit