update onboarding colors and add image upload

This commit is contained in:
Ra
2025-08-20 11:20:28 -07:00
parent 875280cdac
commit 9332a48542
12 changed files with 2078 additions and 426 deletions

View 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;