Files
auditly/components/ui/ImageUpload.tsx

181 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;