mnemo_cards/mnemo_cards_admin/src/components/BulkCardUpload.tsx

247 lines
7.6 KiB
TypeScript
Raw Normal View History

2025-12-14 19:21:36 +00:00
import { useState, useRef } from 'react'
import { Button } from './ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'
2025-12-14 19:34:43 +00:00
import { Upload, X, Check } from 'lucide-react'
2025-12-14 19:21:36 +00:00
interface UploadedImage {
id: string
file: File
preview: string
base64: string
}
interface BulkCardUploadProps {
onImagesUploaded: (images: UploadedImage[]) => void
onClose: () => void
}
export function BulkCardUpload({ onImagesUploaded, onClose }: BulkCardUploadProps) {
const fileInputRef = useRef<HTMLInputElement>(null)
const [uploadedImages, setUploadedImages] = useState<UploadedImage[]>([])
const [isDragging, setIsDragging] = useState(false)
const handleFileSelect = async (files: FileList) => {
const newImages: UploadedImage[] = []
for (let i = 0; i < files.length; i++) {
const file = files[i]
// Validate file type
if (!file.type.startsWith('image/')) {
continue
}
// Validate file size (max 5MB)
const fileSizeMB = file.size / (1024 * 1024)
if (fileSizeMB > 5) {
continue
}
try {
const base64 = await fileToBase64(file)
const preview = URL.createObjectURL(file)
newImages.push({
id: `${Date.now()}-${i}`,
file,
preview,
base64,
})
} catch (error) {
console.error('Error processing file:', error)
}
}
setUploadedImages((prev) => [...prev, ...newImages])
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (files && files.length > 0) {
handleFileSelect(files)
}
// Reset input value to allow selecting the same files again
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
const files = e.dataTransfer.files
if (files && files.length > 0) {
handleFileSelect(files)
}
}
const handleRemove = (id: string) => {
setUploadedImages((prev) => {
const image = prev.find((img) => img.id === id)
if (image) {
URL.revokeObjectURL(image.preview)
}
return prev.filter((img) => img.id !== id)
})
}
const handleClearAll = () => {
uploadedImages.forEach((img) => URL.revokeObjectURL(img.preview))
setUploadedImages([])
}
const handleContinue = () => {
if (uploadedImages.length > 0) {
onImagesUploaded(uploadedImages)
}
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold">Bulk Card Upload</h2>
<p className="text-sm text-muted-foreground">
Upload multiple images, then fill in card details one by one
</p>
</div>
<Button variant="outline" onClick={onClose}>
<X className="h-4 w-4 mr-2" />
Cancel
</Button>
</div>
<Card>
<CardHeader>
<CardTitle>Upload Images</CardTitle>
<CardDescription>
Select multiple image files or drag and drop them here
</CardDescription>
</CardHeader>
<CardContent>
<div
className={`border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors ${
isDragging
? 'border-primary bg-primary/5'
: 'border-muted-foreground/25 hover:border-muted-foreground/50'
}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept="image/*"
multiple
onChange={handleInputChange}
className="hidden"
/>
<div className="flex flex-col items-center justify-center space-y-4">
<Upload className="h-12 w-12 text-muted-foreground" />
<div className="text-lg">
<span className="text-primary font-medium">Click to upload</span> or drag and drop
</div>
<p className="text-sm text-muted-foreground">
PNG, JPG, GIF up to 5MB each. Multiple files supported.
</p>
</div>
</div>
</CardContent>
</Card>
{uploadedImages.length > 0 && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Uploaded Images ({uploadedImages.length})</CardTitle>
<CardDescription>
Review uploaded images and continue to fill in card details
</CardDescription>
</div>
<Button variant="outline" size="sm" onClick={handleClearAll}>
<X className="h-4 w-4 mr-2" />
Clear All
</Button>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4 max-h-[500px] overflow-y-auto">
{uploadedImages.map((image) => (
<div key={image.id} className="relative group">
<div className="relative aspect-square border rounded-lg overflow-hidden bg-muted">
<img
src={image.preview}
alt={image.file.name}
className="w-full h-full object-cover"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/50 transition-colors flex items-center justify-center">
<Button
variant="destructive"
size="sm"
className="opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
handleRemove(image.id)
}}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
<p className="text-xs text-muted-foreground mt-1 truncate" title={image.file.name}>
{image.file.name}
</p>
</div>
))}
</div>
</CardContent>
</Card>
)}
{uploadedImages.length > 0 && (
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleContinue}>
<Check className="h-4 w-4 mr-2" />
Continue to Fill Details ({uploadedImages.length} cards)
</Button>
</div>
)}
</div>
)
}
// Helper function to convert file to base64
function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const result = reader.result as string
// Remove data:image/...;base64, prefix
const base64 = result.split(',')[1]
resolve(base64)
}
reader.onerror = reject
reader.readAsDataURL(file)
})
}