update onboarding colors and add image upload
This commit is contained in:
181
components/ui/ImageUpload.tsx
Normal file
181
components/ui/ImageUpload.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user