feat: Add drag and drop to image uploader

This commit is contained in:
gpt-engineer-app[bot]
2025-10-01 19:24:45 +00:00
parent c582e6fc1b
commit 39119a013a

View File

@@ -1,6 +1,6 @@
import React, { useState, useRef } from 'react'; import React, { useState, useRef } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Upload, Star, CreditCard, Trash2 } from 'lucide-react'; import { Upload, Star, CreditCard, Trash2, ImagePlus } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
ContextMenu, ContextMenu,
@@ -9,6 +9,7 @@ import {
ContextMenuSeparator, ContextMenuSeparator,
ContextMenuTrigger, ContextMenuTrigger,
} from '@/components/ui/context-menu'; } from '@/components/ui/context-menu';
import { DragDropZone } from './DragDropZone';
export interface UploadedImage { export interface UploadedImage {
url: string; url: string;
@@ -39,12 +40,8 @@ export function EntityMultiImageUploader({
entityType = 'entity' entityType = 'entity'
}: EntityMultiImageUploaderProps) { }: EntityMultiImageUploaderProps) {
const maxImages = mode === 'create' ? 5 : 3; const maxImages = mode === 'create' ? 5 : 3;
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const handleFilesAdded = (files: File[]) => {
const currentCount = value.uploaded.length; const currentCount = value.uploaded.length;
const availableSlots = maxImages - currentCount; const availableSlots = maxImages - currentCount;
@@ -52,7 +49,7 @@ export function EntityMultiImageUploader({
return; return;
} }
const filesToAdd = Array.from(files).slice(0, availableSlots); const filesToAdd = files.slice(0, availableSlots);
const newImages: UploadedImage[] = filesToAdd.map(file => ({ const newImages: UploadedImage[] = filesToAdd.map(file => ({
url: URL.createObjectURL(file), url: URL.createObjectURL(file),
file, file,
@@ -81,11 +78,6 @@ export function EntityMultiImageUploader({
} }
onChange(updatedValue); onChange(updatedValue);
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}; };
const handleAssignRole = (index: number, role: 'banner' | 'card') => { const handleAssignRole = (index: number, role: 'banner' | 'card') => {
@@ -217,49 +209,65 @@ export function EntityMultiImageUploader({
return parts.join(' • '); return parts.join(' • ');
}; };
// Empty state: show large drag & drop zone
if (value.uploaded.length === 0) {
return (
<div className="space-y-4">
<DragDropZone
onFilesAdded={handleFilesAdded}
maxFiles={maxImages}
maxSizeMB={25}
allowedFileTypes={['image/*']}
/>
<div className="text-sm text-muted-foreground space-y-1">
<p> Right-click images to set as banner or card</p>
<p> Images will be uploaded when you submit the form</p>
</div>
</div>
);
}
// With images: show grid + compact upload area
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<input {/* Image Grid */}
ref={fileInputRef} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
type="file" {value.uploaded.map((image, index) => renderImageCard(image, index))}
accept="image/*" </div>
multiple={mode === 'create'}
onChange={handleFileSelect}
className="hidden"
/>
<Button {/* Compact Upload Area */}
type="button" {value.uploaded.length < maxImages && (
variant="outline" <DragDropZone
onClick={() => fileInputRef.current?.click()} onFilesAdded={handleFilesAdded}
disabled={value.uploaded.length >= maxImages} maxFiles={maxImages - value.uploaded.length}
> maxSizeMB={25}
<Upload className="mr-2 h-4 w-4" /> allowedFileTypes={['image/*']}
Select {mode === 'create' ? 'Images' : 'Image'} className="p-6"
{` (${value.uploaded.length}/${maxImages})`} >
</Button> <div className="text-center space-y-2">
<ImagePlus className="w-8 h-8 mx-auto text-muted-foreground" />
{value.uploaded.length > 0 && ( <p className="text-sm font-medium">Add More Images</p>
<div className="space-y-4"> <p className="text-xs text-muted-foreground">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> Drag & drop or click to browse ({value.uploaded.length}/{maxImages})
{value.uploaded.map((image, index) => renderImageCard(image, index))} </p>
</div> </div>
</DragDropZone>
<div className="space-y-2 text-sm text-muted-foreground">
<p className="font-medium">{getHelperText()}</p>
<div className="space-y-1 pl-4 border-l-2 border-border">
<p>
<strong>Banner:</strong> Main header image for the {entityType} detail page
{value.banner_assignment !== null && value.banner_assignment !== undefined && ` (Image ${value.banner_assignment + 1})`}
</p>
<p>
<strong>Card:</strong> Thumbnail in {entityType} listings and search results
{value.card_assignment !== null && value.card_assignment !== undefined && ` (Image ${value.card_assignment + 1})`}
</p>
</div>
</div>
</div>
)} )}
{/* Helper Text */}
<div className="space-y-2 text-sm text-muted-foreground">
<p className="font-medium">{getHelperText()}</p>
<div className="space-y-1 pl-4 border-l-2 border-border">
<p>
<strong>Banner:</strong> Main header image for the {entityType} detail page
{value.banner_assignment !== null && value.banner_assignment !== undefined && ` (Image ${value.banner_assignment + 1})`}
</p>
<p>
<strong>Card:</strong> Thumbnail in {entityType} listings and search results
{value.card_assignment !== null && value.card_assignment !== undefined && ` (Image ${value.card_assignment + 1})`}
</p>
</div>
</div>
</div> </div>
); );
} }