181 lines
5.9 KiB
TypeScript
181 lines
5.9 KiB
TypeScript
import React, { useState, useRef } from 'react';
|
||
import { StoredImage } from '../../services/imageStorageService';
|
||
|
||
interface ImageUploadProps {
|
||
onImageSelected: (file: File) => void;
|
||
onImageRemove?: () => void;
|
||
currentImage?: StoredImage | null;
|
||
loading?: boolean;
|
||
error?: string;
|
||
className?: string;
|
||
maxSizeMB?: number;
|
||
acceptedFormats?: string[];
|
||
size?: 'small' | 'medium' | 'large';
|
||
}
|
||
|
||
const ImageUpload: React.FC<ImageUploadProps> = ({
|
||
onImageSelected,
|
||
onImageRemove,
|
||
currentImage,
|
||
loading = false,
|
||
error,
|
||
className = '',
|
||
maxSizeMB = 10,
|
||
acceptedFormats = ['JPEG', 'PNG', 'GIF', 'WebP'],
|
||
size = 'medium'
|
||
}) => {
|
||
const [dragOver, setDragOver] = useState(false);
|
||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||
|
||
const sizeClasses = {
|
||
small: 'w-12 h-12',
|
||
medium: 'w-16 h-16',
|
||
large: 'w-24 h-24'
|
||
};
|
||
|
||
const handleFileSelect = (file: File) => {
|
||
// Basic validation
|
||
if (!file.type.startsWith('image/')) {
|
||
return;
|
||
}
|
||
|
||
if (file.size > maxSizeMB * 1024 * 1024) {
|
||
return;
|
||
}
|
||
|
||
onImageSelected(file);
|
||
};
|
||
|
||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
const file = e.target.files?.[0];
|
||
if (file) {
|
||
handleFileSelect(file);
|
||
}
|
||
// Reset input
|
||
if (fileInputRef.current) {
|
||
fileInputRef.current.value = '';
|
||
}
|
||
};
|
||
|
||
const handleDragOver = (e: React.DragEvent) => {
|
||
e.preventDefault();
|
||
setDragOver(true);
|
||
};
|
||
|
||
const handleDragLeave = (e: React.DragEvent) => {
|
||
e.preventDefault();
|
||
setDragOver(false);
|
||
};
|
||
|
||
const handleDrop = (e: React.DragEvent) => {
|
||
e.preventDefault();
|
||
setDragOver(false);
|
||
|
||
const file = e.dataTransfer.files[0];
|
||
if (file) {
|
||
handleFileSelect(file);
|
||
}
|
||
};
|
||
|
||
const handleClick = () => {
|
||
if (!loading) {
|
||
fileInputRef.current?.click();
|
||
}
|
||
};
|
||
|
||
const handleRemove = (e: React.MouseEvent) => {
|
||
e.stopPropagation();
|
||
if (onImageRemove) {
|
||
onImageRemove();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className={`relative ${className}`}>
|
||
<input
|
||
ref={fileInputRef}
|
||
type="file"
|
||
accept="image/*"
|
||
onChange={handleInputChange}
|
||
className="hidden"
|
||
disabled={loading}
|
||
/>
|
||
|
||
<div
|
||
className={`
|
||
${sizeClasses[size]} relative rounded-[250px] overflow-hidden
|
||
cursor-pointer transition-all duration-200
|
||
${dragOver ? 'ring-2 ring-Brand-Orange ring-opacity-50' : ''}
|
||
${loading ? 'opacity-50 cursor-not-allowed' : 'hover:opacity-80'}
|
||
`}
|
||
onDragOver={handleDragOver}
|
||
onDragLeave={handleDragLeave}
|
||
onDrop={handleDrop}
|
||
onClick={handleClick}
|
||
>
|
||
{currentImage ? (
|
||
<>
|
||
<img
|
||
src={currentImage.dataUrl}
|
||
alt="Uploaded"
|
||
className="w-full h-full object-cover"
|
||
/>
|
||
{!loading && onImageRemove && (
|
||
<button
|
||
onClick={handleRemove}
|
||
className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white rounded-full flex items-center justify-center text-xs hover:bg-red-600 transition-colors"
|
||
title="Remove image"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</>
|
||
) : (
|
||
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
|
||
{loading ? (
|
||
<div className="animate-spin w-6 h-6 border-2 border-Brand-Orange border-t-transparent rounded-full" />
|
||
) : (
|
||
<svg
|
||
width="24"
|
||
height="24"
|
||
viewBox="0 0 24 24"
|
||
fill="none"
|
||
xmlns="http://www.w3.org/2000/svg"
|
||
className="text-gray-400"
|
||
>
|
||
<path
|
||
d="M12 16L12 8M12 8L8 12M12 8L16 12"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
<path
|
||
d="M4 16V20C4 20.5523 4.44772 21 5 21H19C19.5523 21 20 20.5523 20 20V16"
|
||
stroke="currentColor"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
/>
|
||
</svg>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{dragOver && (
|
||
<div className="absolute inset-0 bg-Brand-Orange bg-opacity-20 flex items-center justify-center">
|
||
<span className="text-Brand-Orange text-xs font-medium">Drop image</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{error && (
|
||
<div className="absolute top-full left-0 mt-1 text-xs text-red-500 whitespace-nowrap">
|
||
{error}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ImageUpload; |