mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 18:31:13 -05:00
feat: Add drag and drop to image uploader
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user