File Upload
Dropzones with previews, progress, and upload status.
Fromblocks.so
File Upload with Preview
"use client"
import { HelpCircle, Trash2, Upload } from "lucide-react"
import { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
import { cn } from "@/lib/utils"
export default function FileUpload01() {
const fileInputRef = useRef<HTMLInputElement>(null)
const [uploadedFiles, setUploadedFiles] = useState<File[]>([])
const [fileProgresses, setFileProgresses] = useState<Record<string, number>>(
{}
)
const handleFileSelect = (files: FileList | null) => {
if (!files) return
const newFiles = Array.from(files)
setUploadedFiles((prev) => [...prev, ...newFiles])
// Simulate upload progress for each file
newFiles.forEach((file) => {
let progress = 0
const interval = setInterval(() => {
progress += Math.random() * 10
if (progress >= 100) {
progress = 100
clearInterval(interval)
}
setFileProgresses((prev) => ({
...prev,
[file.name]: Math.min(progress, 100),
}))
}, 300)
})
}
const handleBoxClick = () => {
fileInputRef.current?.click()
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
handleFileSelect(e.dataTransfer.files)
}
const removeFile = (filename: string) => {
setUploadedFiles((prev) => prev.filter((file) => file.name !== filename))
setFileProgresses((prev) => {
const newProgresses = { ...prev }
delete newProgresses[filename]
return newProgresses
})
}
return (
<div className="flex items-center justify-center p-10">
<Card className="mx-auto w-full max-w-lg rounded-lg bg-background p-0 shadow-md">
<CardContent className="p-0">
<div className="p-6 pb-4">
<div className="flex items-start justify-between">
<div>
<h2 className="text-balance font-medium text-foreground text-lg">
Create a new project
</h2>
<p className="mt-1 text-pretty text-muted-foreground text-sm">
Drag and drop files to create a new project.
</p>
</div>
</div>
</div>
<div className="mt-2 px-6 pb-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="mb-2" htmlFor="projectName">
Project name
</Label>
<Input
defaultValue="Open Source Stripe"
id="projectName"
type="text"
/>
</div>
<div>
<Label className="mb-2" htmlFor="projectLead">
Project lead
</Label>
<Select
defaultValue="1"
items={{
"1": "Ephraim Duncan",
"2": "Lucas Smith",
"3": "Timur Ercan",
}}
>
<SelectTrigger className="w-full ps-2" id="projectLead">
<SelectValue placeholder="Select project lead" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="1">
<img
alt="Ephraim Duncan"
className="size-5 rounded"
height={20}
src="https://blocks.so/avatar-01.png"
width={20}
/>
<span className="truncate">Ephraim Duncan</span>
</SelectItem>
<SelectItem value="2">
<img
alt="Lucas Smith"
className="size-5 rounded"
height={20}
src="https://blocks.so/avatar-03.png"
width={20}
/>
<span className="truncate">Lucas Smith</span>
</SelectItem>
<SelectItem value="3">
<img
alt="Timur Ercan"
className="size-5 rounded"
height={20}
src="https://blocks.so/avatar-02.jpg"
width={20}
/>
<span className="truncate">Timur Ercan</span>
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="px-6">
<div
className="flex cursor-pointer flex-col items-center justify-center rounded-md border-2 border-border border-dashed p-8 text-center"
onClick={handleBoxClick}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<div className="mb-2 rounded-full bg-muted p-3">
<Upload className="h-5 w-5 text-muted-foreground" />
</div>
<p className="text-pretty font-medium text-foreground text-sm">
Upload a project image
</p>
<p className="mt-1 text-pretty text-muted-foreground text-sm">
or,{" "}
<label
className="cursor-pointer font-medium text-primary hover:text-primary/90"
htmlFor="fileUpload"
onClick={(e) => e.stopPropagation()}
>
click to browse
</label>{" "}
(4MB max)
</p>
<input
accept="image/*"
className="hidden"
id="fileUpload"
onChange={(e) => handleFileSelect(e.target.files)}
ref={fileInputRef}
type="file"
/>
</div>
</div>
<div
className={cn(
"space-y-3 px-6 pb-5",
uploadedFiles.length > 0 ? "mt-4" : ""
)}
>
{uploadedFiles.map((file, index) => {
const imageUrl = URL.createObjectURL(file)
return (
<div
className="flex flex-col rounded-lg border border-border p-2"
key={file.name + index}
onLoad={() => {
return () => URL.revokeObjectURL(imageUrl)
}}
>
<div className="flex items-center gap-2">
<div className="row-span-2 flex h-14 w-18 items-center justify-center self-start overflow-hidden rounded-sm bg-muted">
<img
alt={file.name}
className="h-full w-full object-cover"
src={imageUrl}
/>
</div>
<div className="flex-1 pr-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="max-w-[250px] truncate text-foreground text-sm">
{file.name}
</span>
<span className="whitespace-nowrap text-muted-foreground text-sm">
{Math.round(file.size / 1024)} KB
</span>
</div>
<Button
className="bg-transparent! hover:text-red-500"
onClick={() => removeFile(file.name)}
size="icon-sm"
variant="ghost"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
<div className="flex items-center gap-2">
<div className="h-2 flex-1 overflow-hidden rounded-full bg-muted">
<div
className="h-full bg-primary"
style={{
width: `${fileProgresses[file.name] || 0}%`,
}}
/>
</div>
<span className="whitespace-nowrap text-muted-foreground text-xs">
{Math.round(fileProgresses[file.name] || 0)}%
</span>
</div>
</div>
</div>
</div>
)
})}
</div>
<div className="flex items-center justify-between rounded-b-lg border-border border-t bg-muted px-6 py-3">
<TooltipProvider delay={0}>
<Tooltip>
<TooltipTrigger
render={
<Button
className="flex items-center text-muted-foreground hover:text-foreground"
size="sm"
variant="ghost"
/>
}
>
<HelpCircle className="mr-1 h-4 w-4" />
Need help?
</TooltipTrigger>
<TooltipContent className="border bg-background py-3 text-foreground">
<div className="space-y-1">
<p className="text-pretty font-medium text-[13px]">
Need assistance?
</p>
<p className="max-w-[200px] text-pretty text-muted-foreground text-xs dark:text-muted-background">
Upload project images by dragging and dropping files or
using the file browser. Supported formats: JPG, PNG, SVG.
Maximum file size: 4MB.
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<div className="flex gap-2">
<Button
className="h-9 px-4 font-medium text-sm"
variant="outline"
>
Cancel
</Button>
<Button className="h-9 px-4 font-medium text-sm">Continue</Button>
</div>
</div>
</CardContent>
</Card>
</div>
)
}npx shadcn@latest add https://blocks.so/r/file-upload-01.jsonFile Upload Simple
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
export default function FileUpload02() {
return (
<div className="flex items-center justify-center p-10">
<Card className="shadow-none">
<CardHeader>
<CardTitle>Set up your first workspace</CardTitle>
<CardDescription>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr.
</CardDescription>
</CardHeader>
<CardContent>
<form action="#" method="POST">
<div className="space-y-6">
<div className="space-y-2">
<Label htmlFor="workspace-name">
Workspace <span className="text-destructive">*</span>
</Label>
<Input
autoComplete="workspace-name"
id="workspace-name"
name="workspace-name"
placeholder="Workspace name"
required
type="text"
/>
</div>
<div className="space-y-2">
<Label htmlFor="file-1">
Upload file <span className="text-destructive">*</span>
</Label>
<Input
accept=".csv, .xlsx, .xls"
id="file-1"
name="file-1"
type="file"
/>
<p className="text-pretty text-muted-foreground text-sm">
You are only allowed to upload CSV, XLSX or XLS files.
</p>
</div>
</div>
<div className="mt-8 flex justify-end space-x-3">
<Button type="button" variant="outline">
Cancel
</Button>
<Button type="submit">Submit</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}npx shadcn@latest add https://blocks.so/r/file-upload-02.jsonFile Upload Multi-File Dropzone
"use client"
import { File, Trash } from "lucide-react"
import React from "react"
import { useDropzone } from "react-dropzone"
import { Button } from "@/components/ui/button"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
export default function FileUpload03() {
const [files, setFiles] = React.useState<File[]>([])
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop: (acceptedFiles) => setFiles(acceptedFiles),
})
const filesList = files.map((file) => (
<li className="relative" key={file.name}>
<Card className="relative p-4 shadow-none">
<div className="-translate-y-1/2 absolute top-1/2 right-4">
<Button
aria-label="Remove file"
onClick={() =>
setFiles((prevFiles) =>
prevFiles.filter((prevFile) => prevFile.name !== file.name)
)
}
size="icon"
type="button"
variant="ghost"
>
<Trash aria-hidden={true} className="h-5 w-5" />
</Button>
</div>
<CardContent className="flex items-center space-x-3 p-0">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted">
<File aria-hidden={true} className="h-5 w-5 text-foreground" />
</span>
<div>
<p className="text-pretty font-medium text-foreground">
{file.name}
</p>
<p className="mt-0.5 text-pretty text-muted-foreground text-sm">
{file.size} bytes
</p>
</div>
</CardContent>
</Card>
</li>
))
return (
<div className="flex items-center justify-center p-10">
<Card className="shadow-none sm:mx-auto sm:max-w-xl">
<CardHeader>
<CardTitle>Set up your first cloud storage</CardTitle>
<CardDescription>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr.
</CardDescription>
</CardHeader>
<CardContent>
<form action="#" method="post">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-6">
<div className="col-span-full sm:col-span-3">
<Label className="font-medium" htmlFor="bucket-name">
Bucket name
</Label>
<Input
className="mt-2"
id="bucket-name"
name="bucket-name"
placeholder="Bucket name"
type="text"
/>
</div>
<div className="col-span-full sm:col-span-3">
<Label className="font-medium" htmlFor="visibility">
Visibility
</Label>
<Select
defaultValue="private"
disabled
items={{ private: "Private", public: "Public" }}
>
<SelectTrigger
className="mt-2 w-full"
id="visibility"
name="visibility"
>
<SelectValue placeholder="Select visibility" />
</SelectTrigger>
<SelectContent>
<SelectItem value="private">Private</SelectItem>
<SelectItem value="public">Public</SelectItem>
</SelectContent>
</Select>
<p className="mt-2 text-pretty text-muted-foreground text-sm">
Only admins can change visibility.
</p>
</div>
<div className="col-span-full">
<Label className="font-medium" htmlFor="file-upload-2">
File(s) upload
</Label>
<div
{...getRootProps()}
className={cn(
isDragActive
? "border-primary bg-primary/10 ring-2 ring-primary/20"
: "border-border",
"mt-2 flex justify-center rounded-md border border-dashed px-6 py-20 transition-colors duration-200"
)}
>
<div>
<File
aria-hidden={true}
className="mx-auto h-12 w-12 text-muted-foreground/80"
/>
<div className="mt-4 flex text-muted-foreground">
<p>Drag and drop or</p>
<label
className="relative cursor-pointer rounded-sm pl-1 font-medium text-primary hover:text-primary/80 hover:underline hover:underline-offset-4"
htmlFor="file"
>
<span>choose file(s)</span>
<input
{...getInputProps()}
className="sr-only"
id="file-upload-2"
name="file-upload-2"
type="file"
/>
</label>
<p className="text-pretty pl-1">to upload</p>
</div>
</div>
</div>
<p className="mt-2 text-pretty text-muted-foreground text-sm leading-5 sm:flex sm:items-center sm:justify-between">
<span>All file types are allowed to upload.</span>
<span className="pl-1 sm:pl-0">Max. size per file: 50MB</span>
</p>
{filesList.length > 0 && (
<>
<h4 className="mt-6 text-balance font-medium text-foreground">
File(s) to upload
</h4>
<ul className="mt-4 space-y-4" role="list">
{filesList}
</ul>
</>
)}
</div>
</div>
<Separator className="my-6" />
<div className="flex items-center justify-end space-x-3">
<Button type="button" variant="outline">
Cancel
</Button>
<Button type="submit">Upload</Button>
</div>
</form>
</CardContent>
</Card>
</div>
)
}npx shadcn@latest add https://blocks.so/r/file-upload-03.jsonFile Upload Drag & Drop
"use client"
import { File, FileSpreadsheet, X } from "lucide-react"
import { type ChangeEvent, type DragEvent, useRef, useState } from "react"
import { toast } from "sonner"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
export default function FileUpload04() {
const [uploadState, setUploadState] = useState<{
file: File | null
progress: number
uploading: boolean
}>({
file: null,
progress: 0,
uploading: false,
})
const [showDummy, setShowDummy] = useState(true)
const fileInputRef = useRef<HTMLInputElement>(null)
const validFileTypes = [
"text/csv",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
]
const handleFile = (file: File | undefined) => {
if (!file) return
if (validFileTypes.includes(file.type)) {
setUploadState({ file, progress: 0, uploading: true })
const interval = setInterval(() => {
setUploadState((prev) => {
const newProgress = prev.progress + 5
if (newProgress >= 100) {
clearInterval(interval)
return { ...prev, progress: 100, uploading: false }
}
return { ...prev, progress: newProgress }
})
}, 200)
} else {
toast.error("Please upload a CSV, XLSX, or XLS file.", {
position: "bottom-right",
duration: 3000,
})
}
}
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
handleFile(event.target.files?.[0])
}
const handleDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault()
handleFile(event.dataTransfer.files?.[0])
}
const resetFile = () => {
setUploadState({ file: null, progress: 0, uploading: false })
if (fileInputRef.current) {
fileInputRef.current.value = ""
}
}
const getFileIcon = () => {
if (!uploadState.file) return <File />
const fileExt = uploadState.file.name.split(".").pop()?.toLowerCase() || ""
return ["csv", "xlsx", "xls"].includes(fileExt) ? (
<FileSpreadsheet className="h-5 w-5 text-foreground" />
) : (
<File className="h-5 w-5 text-foreground" />
)
}
const formatFileSize = (bytes: number) => {
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 / k ** i).toFixed(1)) + " " + sizes[i]
}
const { file, progress, uploading } = uploadState
return (
<div className="flex w-full max-w-lg items-center justify-center p-10">
<form className="w-full" onSubmit={(e) => e.preventDefault()}>
<h3 className="text-balance font-semibold text-foreground text-lg">
File Upload
</h3>
<div
className="mt-2 flex justify-center rounded-md border border-input border-dashed px-6 py-12"
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
>
<div>
<File
aria-hidden={true}
className="mx-auto h-12 w-12 text-muted-foreground"
/>
<div className="flex text-muted-foreground text-sm leading-6">
<p>Drag and drop or</p>
<label
className="relative cursor-pointer rounded-sm pl-1 font-medium text-primary hover:underline hover:underline-offset-4"
htmlFor="file-upload-03"
>
<span>choose file</span>
<input
accept=".csv, .xlsx, .xls"
className="sr-only"
id="file-upload-03"
name="file-upload-03"
onChange={handleFileChange}
ref={fileInputRef}
type="file"
/>
</label>
<p className="text-pretty pl-1">to upload</p>
</div>
</div>
</div>
<p className="mt-2 text-pretty text-muted-foreground text-xs leading-5 sm:flex sm:items-center sm:justify-between">
<span>Accepted file types: CSV, XLSX or XLS files.</span>
<span className="pl-1 sm:pl-0">Max. size: 10MB</span>
</p>
{!file && showDummy && (
<Card className="relative mt-8 gap-4 bg-muted p-4 shadow-none">
<Button
aria-label="Remove"
className="absolute top-1 right-1 text-muted-foreground hover:text-foreground"
onClick={() => setShowDummy(false)}
size="icon-sm"
type="button"
variant="ghost"
>
<X aria-hidden={true} className="h-5 w-5 shrink-0" />
</Button>
<div className="flex items-center space-x-2.5">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-sm bg-background shadow-sm ring-1 ring-border ring-inset">
<FileSpreadsheet
aria-hidden={true}
className="h-5 w-5 text-foreground"
/>
</span>
<div>
<p className="text-pretty font-medium text-foreground text-xs">
Revenue_Q1_2024.xlsx
</p>
<p className="mt-0.5 text-pretty text-muted-foreground text-xs">
3.1 MB
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Progress className="h-1.5" value={45} />
<span className="text-muted-foreground text-xs">45%</span>
</div>
</Card>
)}
{file && (
<Card className="relative mt-8 gap-4 bg-muted p-4 shadow-none">
<Button
aria-label="Remove"
className="absolute top-1 right-1 text-muted-foreground hover:text-foreground"
onClick={resetFile}
size="icon-sm"
type="button"
variant="ghost"
>
<X aria-hidden={true} className="h-5 w-5 shrink-0" />
</Button>
<div className="flex items-center space-x-2.5">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-sm bg-background shadow-sm ring-1 ring-border ring-inset">
{getFileIcon()}
</span>
<div>
<p className="text-pretty font-medium text-foreground text-xs">
{file?.name}
</p>
<p className="mt-0.5 text-pretty text-muted-foreground text-xs">
{file && formatFileSize(file.size)}
</p>
</div>
</div>
<div className="flex items-center space-x-3">
<Progress className="h-1.5" value={progress} />
<span className="text-muted-foreground text-xs">{progress}%</span>
</div>
</Card>
)}
<div className="mt-8 flex items-center justify-end space-x-3">
<Button
className="whitespace-nowrap"
disabled={!file}
onClick={resetFile}
type="button"
variant="outline"
>
Cancel
</Button>
<Button
className="whitespace-nowrap"
disabled={!file || uploading || progress < 100}
type="submit"
>
Upload
</Button>
</div>
</form>
</div>
)
}npx shadcn@latest add https://blocks.so/r/file-upload-04.jsonFile Upload with Progress
import { FileSpreadsheet, Upload, X } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
export default function FileUpload05() {
return (
<div className="flex w-full max-w-lg items-center justify-center p-10 sm:mx-auto sm:max-w-lg">
<form>
<h3 className="text-balance font-semibold text-foreground text-lg">
File Upload
</h3>
<div className="mt-4 flex justify-center space-x-4 rounded-md border border-input border-dashed px-6 py-10">
<div className="sm:flex sm:items-center sm:gap-x-3">
<Upload
aria-hidden={true}
className="mx-auto h-8 w-8 text-muted-foreground sm:mx-0 sm:h-6 sm:w-6"
/>
<div className="mt-4 flex text-foreground text-sm leading-6 sm:mt-0">
<p>Drag and drop or</p>
<Label
className="relative cursor-pointer rounded-sm pl-1 font-medium text-primary hover:underline hover:underline-offset-4"
htmlFor="file-upload-4"
>
<span>choose file</span>
<input
className="sr-only"
id="file-upload-4"
name="file-upload-4"
type="file"
/>
</Label>
<p className="text-pretty pl-1">to upload</p>
</div>
</div>
</div>
<p className="mt-2 flex items-center justify-between text-pretty text-muted-foreground text-xs leading-5">
Recommended max. size: 10 MB, Accepted file types: XLSX, XLS, CSV.
</p>
<div className="relative mt-8 rounded-lg bg-muted p-3">
<div className="absolute top-1 right-1">
<Button
aria-label="Remove"
className="rounded-sm p-2 text-muted-foreground hover:text-foreground"
size="sm"
type="button"
variant="ghost"
>
<X aria-hidden={true} className="size-4 shrink-0" />
</Button>
</div>
<div className="flex items-center space-x-2.5">
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-sm bg-background shadow-sm ring-1 ring-input ring-inset">
<FileSpreadsheet
aria-hidden={true}
className="size-5 text-foreground"
/>
</span>
<div className="w-full">
<p className="text-pretty font-medium text-foreground text-xs">
Revenue_Q1_2024.xlsx
</p>
<p className="mt-0.5 flex justify-between text-pretty text-muted-foreground text-xs">
<span>3.1 MB</span>
<span>Completed</span>
</p>
</div>
</div>
</div>
<div className="mt-8 flex items-center justify-end space-x-3">
<Button
className="whitespace-nowrap rounded-sm border border-input px-4 py-2 font-medium text-foreground text-sm shadow-sm hover:bg-accent hover:text-foreground"
type="button"
variant="outline"
>
Cancel
</Button>
<Button
className="whitespace-nowrap rounded-sm bg-primary px-4 py-2 font-medium text-primary-foreground text-sm shadow-sm hover:bg-primary/90"
type="submit"
variant="default"
>
Upload
</Button>
</div>
</form>
</div>
)
}npx shadcn@latest add https://blocks.so/r/file-upload-05.jsonFile Upload with Status Sections
"use client"
import { CheckCircle, FileText, Loader2, Upload, X } from "lucide-react"
import type React from "react"
import { useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import { Progress } from "@/components/ui/progress"
import { Separator } from "@/components/ui/separator"
interface UploadItem {
id: string
name: string
progress: number
status: "uploading" | "completed"
}
export default function FileUpload06() {
const [uploads, setUploads] = useState<UploadItem[]>([
{
id: "a1",
name: "design-mock-landing.png",
progress: 62,
status: "uploading",
},
{
id: "b2",
name: "team-headshot-2025-01-09.jpg",
progress: 28,
status: "uploading",
},
{
id: "c3",
name: "logo-v3-final.gif",
progress: 100,
status: "completed",
},
])
const filePickerRef = useRef<HTMLInputElement>(null)
const openFilePicker = () => {
filePickerRef.current?.click()
}
const onFileInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = event.target.files
if (selectedFiles) {
console.log("Files selected:", selectedFiles)
}
}
const onDragOver = (event: React.DragEvent) => {
event.preventDefault()
}
const onDropFiles = (event: React.DragEvent) => {
event.preventDefault()
const droppedFiles = event.dataTransfer.files
if (droppedFiles) {
console.log("Files dropped:", droppedFiles)
}
}
const removeUploadById = (id: string) => {
setUploads(uploads.filter((file) => file.id !== id))
}
const activeUploads = uploads.filter((file) => file.status === "uploading")
const completedUploads = uploads.filter((file) => file.status === "completed")
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-y-6">
<Card
className="group flex max-h-[200px] w-full cursor-pointer flex-col items-center justify-center gap-4 border-dashed py-8 text-sm shadow-none transition-colors hover:bg-muted/50"
onClick={openFilePicker}
onDragOver={onDragOver}
onDrop={onDropFiles}
>
<div className="grid space-y-3">
<div className="flex items-center gap-x-2 text-muted-foreground">
<Upload className="size-5" />
<div>
Drop files here or{" "}
<Button
className="h-auto p-0 font-normal text-primary"
onClick={openFilePicker}
variant="link"
>
browse files
</Button>{" "}
to add
</div>
</div>
</div>
<input
accept="image/png,image/jpeg,image/gif"
className="hidden"
multiple
onChange={onFileInputChange}
ref={filePickerRef}
type="file"
/>
<span className="mt-2 block text-base/6 text-muted-foreground group-disabled:opacity-50 sm:text-xs">
Supported: JPG, PNG, GIF (max 10 MB)
</span>
</Card>
<div className="flex flex-col gap-y-4">
{activeUploads.length > 0 && (
<div>
<h2 className="mb-4 flex items-center text-balance font-mono font-normal text-foreground text-lg uppercase sm:text-xs">
<Loader2 className="mr-1 size-4 animate-spin" />
Uploading
</h2>
<div className="-mt-2 divide-y">
{activeUploads.map((file) => (
<div className="group flex items-center py-4" key={file.id}>
<div className="mr-3 grid size-10 shrink-0 place-content-center rounded border bg-muted">
<FileText className="inline size-4 group-hover:hidden" />
<Button
aria-label="Cancel"
className="hidden size-4 h-auto p-0 group-hover:inline"
onClick={() => removeUploadById(file.id)}
size="icon"
variant="ghost"
>
<X className="size-4" />
</Button>
</div>
<div className="mb-1 flex w-full flex-col">
<div className="flex justify-between gap-2">
<span className="select-none text-base/6 text-foreground group-disabled:opacity-50 sm:text-sm/6">
{file.name}
</span>
<span className="text-muted-foreground text-sm tabular-nums">
{file.progress}%
</span>
</div>
<Progress
className="mt-1 h-2 min-w-64"
value={file.progress}
/>
</div>
</div>
))}
</div>
</div>
)}
{activeUploads.length > 0 && completedUploads.length > 0 && (
<Separator className="my-0" />
)}
{completedUploads.length > 0 && (
<div>
<h2 className="mb-4 flex items-center text-balance font-mono font-normal text-foreground text-lg uppercase sm:text-xs">
<CheckCircle className="mr-1 size-4" />
Finished
</h2>
<div className="-mt-2 divide-y">
{completedUploads.map((file) => (
<div className="group flex items-center py-4" key={file.id}>
<div className="mr-3 grid size-10 shrink-0 place-content-center rounded border bg-muted">
<FileText className="inline size-4 group-hover:hidden" />
<Button
aria-label="Remove"
className="hidden size-4 h-auto p-0 group-hover:inline"
onClick={() => removeUploadById(file.id)}
size="icon"
variant="ghost"
>
<X className="size-4" />
</Button>
</div>
<div className="mb-1 flex w-full flex-col">
<div className="flex justify-between gap-2">
<span className="select-none text-base/6 text-foreground group-disabled:opacity-50 sm:text-sm/6">
{file.name}
</span>
<span className="text-muted-foreground text-sm tabular-nums">
{file.progress}%
</span>
</div>
<Progress
className="mt-1 h-2 min-w-64"
value={file.progress}
/>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}npx shadcn@latest add https://blocks.so/r/file-upload-06.json