Feature: user upload artwork
This commit is contained in:
@@ -8,114 +8,156 @@ import type {Artwork} from '@/types';
|
||||
interface AddArtworkDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onAdd: (artwork: Omit<Artwork, 'id'>) => void;
|
||||
onAdd: (artwork: Omit<Artwork, 'id'>, file: File) => Promise<Artwork>;
|
||||
}
|
||||
|
||||
export const AddArtworkDialog = ({open, onOpenChange, onAdd}: AddArtworkDialogProps) => {
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [imageUrl, setImageUrl] = useState('');
|
||||
const [year, setYear] = useState(new Date().getFullYear());
|
||||
const [medium, setMedium] = useState('');
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setPreviewUrl(result);
|
||||
setImageUrl(result); // Use data URL for local storage
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
const selectedFile = e.target.files?.[0];
|
||||
|
||||
if (!selectedFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'];
|
||||
if (!allowedTypes.includes(selectedFile.type)) {
|
||||
setError('Invalid file type. Please upload a JPEG, PNG, GIF, or WebP image.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (10MB)
|
||||
if (selectedFile.size > 10 * 1024 * 1024) {
|
||||
setError('File size too large. Maximum size is 10MB.');
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setFile(selectedFile);
|
||||
|
||||
// Create preview
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const result = reader.result as string;
|
||||
setPreviewUrl(result);
|
||||
};
|
||||
reader.readAsDataURL(selectedFile);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
|
||||
if (!title.trim()) {
|
||||
setError('Please enter a title');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
setError('Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await onAdd({title: title.trim(), imageUrl: ''}, file);
|
||||
|
||||
// Reset form
|
||||
setTitle('');
|
||||
setFile(null);
|
||||
setPreviewUrl('');
|
||||
setError(null);
|
||||
onOpenChange(false);
|
||||
} catch (err) {
|
||||
console.error('Error submitting artwork:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to upload artwork');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
onAdd({
|
||||
title,
|
||||
description,
|
||||
imageUrl: imageUrl || previewUrl
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
setImageUrl('');
|
||||
setYear(new Date().getFullYear());
|
||||
setMedium('');
|
||||
setPreviewUrl('');
|
||||
onOpenChange(false);
|
||||
const handleOpenChange = (newOpen: boolean) => {
|
||||
if (!isSubmitting) {
|
||||
if (!newOpen) {
|
||||
// Reset form when closing
|
||||
setTitle('');
|
||||
setFile(null);
|
||||
setPreviewUrl('');
|
||||
setError(null);
|
||||
}
|
||||
onOpenChange(newOpen);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="sm:max-w-[525px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add New Artwork</DialogTitle>
|
||||
<DialogDescription>Upload a new piece to your portfolio</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit} className="space-y-4 mt-4">
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="title" className="text-sm font-medium">
|
||||
Title *
|
||||
</label>
|
||||
<Input id="title" placeholder="Artwork title" value={title} onChange={e => setTitle(e.target.value)} required />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="description" className="text-sm font-medium">
|
||||
Description *
|
||||
</label>
|
||||
<Input id="description" placeholder="Describe your artwork" value={description} onChange={e => setDescription(e.target.value)} required />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="medium" className="text-sm font-medium">
|
||||
Medium *
|
||||
</label>
|
||||
<Input id="medium" placeholder="e.g., Oil on Canvas, Acrylic, Watercolor" value={medium} onChange={e => setMedium(e.target.value)} required />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="year" className="text-sm font-medium">
|
||||
Year *
|
||||
</label>
|
||||
<Input
|
||||
id="year"
|
||||
type="number"
|
||||
min="1900"
|
||||
max={new Date().getFullYear() + 1}
|
||||
value={year}
|
||||
onChange={e => setYear(parseInt(e.target.value))}
|
||||
id="title"
|
||||
placeholder="Artwork title"
|
||||
value={title}
|
||||
onChange={e => setTitle(e.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Image</label>
|
||||
<label className="text-sm font-medium">Image *</label>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input id="image-file" type="file" accept="image/*" onChange={handleFileChange} className="cursor-pointer" />
|
||||
<Input
|
||||
id="image-file"
|
||||
type="file"
|
||||
accept="image/jpeg,image/jpg,image/png,image/gif,image/webp"
|
||||
onChange={handleFileChange}
|
||||
className="cursor-pointer"
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
<Upload className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">or</div>
|
||||
<Input id="image-url" type="url" placeholder="https://example.com/image.jpg" value={imageUrl} onChange={e => setImageUrl(e.target.value)} />
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Supported formats: JPEG, PNG, GIF, WebP (max 10MB)
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(previewUrl || imageUrl) && (
|
||||
{previewUrl && (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Preview</label>
|
||||
<img src={previewUrl || imageUrl} alt="Preview" className="w-full h-48 object-cover rounded-md" />
|
||||
<img
|
||||
src={previewUrl}
|
||||
alt="Preview"
|
||||
className="w-full h-48 object-cover rounded-md border border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Add Artwork
|
||||
<Button type="submit" className="w-full" disabled={isSubmitting}>
|
||||
{isSubmitting ? 'Uploading...' : 'Add Artwork'}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
||||
@@ -18,7 +18,6 @@ export const ArtworkDetail = ({artwork, open, onOpenChange}: ArtworkDetailProps)
|
||||
</DialogHeader>
|
||||
<div className="mt-4">
|
||||
<img src={artwork.imageUrl} alt={artwork.title} className="w-full rounded-lg object-contain max-h-[60vh]" />
|
||||
<p className="mt-4 text-sm text-muted-foreground">{artwork.description}</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -16,4 +16,4 @@ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLI
|
||||
});
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export {Input};
|
||||
export {Input};
|
||||
|
||||
Reference in New Issue
Block a user