113 lines
4.9 KiB
TypeScript
113 lines
4.9 KiB
TypeScript
import React, { useState, useCallback } from 'react';
|
|
import { UploadedImage } from '../types';
|
|
import { UploadIcon } from './icons/UploadIcon';
|
|
import { ArrowRightIcon } from './icons/ArrowRightIcon';
|
|
|
|
interface ImageUploaderProps {
|
|
onImagesUploaded: (images: UploadedImage[]) => void;
|
|
}
|
|
|
|
const ImageUploader: React.FC<ImageUploaderProps> = ({ onImagesUploaded }) => {
|
|
const [images, setImages] = useState<UploadedImage[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
setError(null);
|
|
if (event.target.files) {
|
|
const files = Array.from(event.target.files);
|
|
if (files.length + images.length > 5) {
|
|
setError("You can upload a maximum of 5 images.");
|
|
return;
|
|
}
|
|
// Fix: Explicitly type `file` as `File` to resolve type inference issues.
|
|
const newImages: UploadedImage[] = files.map((file: File) => ({
|
|
file,
|
|
previewUrl: URL.createObjectURL(file),
|
|
subjectDescription: '',
|
|
}));
|
|
setImages(prev => [...prev, ...newImages]);
|
|
}
|
|
};
|
|
|
|
const handleDescriptionChange = (index: number, value: string) => {
|
|
setImages(prev => {
|
|
const updated = [...prev];
|
|
updated[index].subjectDescription = value;
|
|
return updated;
|
|
});
|
|
};
|
|
|
|
const removeImage = (indexToRemove: number) => {
|
|
setImages(prev => prev.filter((_, index) => index !== indexToRemove));
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
if (images.length < 2) {
|
|
setError('Please upload at least 2 images.');
|
|
return;
|
|
}
|
|
if (images.some(img => img.subjectDescription.trim() === '')) {
|
|
setError('Please describe the main subject in each image.');
|
|
return;
|
|
}
|
|
onImagesUploaded(images);
|
|
};
|
|
|
|
return (
|
|
<div className="w-full">
|
|
<div className="text-center mb-6">
|
|
<h2 className="text-2xl font-bold text-gray-100">Upload Your Source Images</h2>
|
|
<p className="text-gray-400">Upload 2 or more images. Then, briefly describe the person you want to feature from each.</p>
|
|
</div>
|
|
|
|
<div className="mb-6">
|
|
<label htmlFor="file-upload" className="relative cursor-pointer bg-gray-700 hover:bg-gray-600 text-purple-300 font-semibold py-3 px-5 rounded-lg border border-dashed border-gray-500 flex flex-col items-center justify-center transition-colors duration-300 h-48">
|
|
<UploadIcon className="w-12 h-12 mb-2 text-gray-400"/>
|
|
<span className="text-lg">Click to upload images</span>
|
|
<span className="text-sm text-gray-500">PNG, JPG, WEBP up to 10MB</span>
|
|
<input id="file-upload" name="file-upload" type="file" multiple accept="image/png, image/jpeg, image/webp" className="sr-only" onChange={handleFileChange} />
|
|
</label>
|
|
</div>
|
|
|
|
{images.length > 0 && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
|
|
{images.map((image, index) => (
|
|
<div key={index} className="bg-gray-700/50 rounded-lg p-3 relative group">
|
|
<img src={image.previewUrl} alt={`preview ${index}`} className="w-full h-40 object-cover rounded-md mb-3" />
|
|
<textarea
|
|
value={image.subjectDescription}
|
|
onChange={(e) => handleDescriptionChange(index, e.target.value)}
|
|
placeholder={`e.g., The man with glasses`}
|
|
className="w-full bg-gray-800 border border-gray-600 rounded-md p-2 text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500 transition"
|
|
rows={2}
|
|
/>
|
|
<button
|
|
onClick={() => removeImage(index)}
|
|
className="absolute top-1 right-1 bg-black/50 text-white rounded-full p-1 opacity-0 group-hover:opacity-100 transition-opacity"
|
|
aria-label="Remove image"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{error && <p className="text-red-400 text-center my-4">{error}</p>}
|
|
|
|
<div className="text-center mt-4">
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={images.length < 2 || images.some(i => !i.subjectDescription)}
|
|
className="bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white font-bold py-3 px-8 rounded-lg transition-all duration-300 text-lg flex items-center gap-2 mx-auto"
|
|
>
|
|
Next: Customize Prompt <ArrowRightIcon className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ImageUploader; |