Feature: user upload artwork

This commit is contained in:
2025-10-25 02:22:39 -04:00
parent 28d0443f44
commit d376b48d5e
23 changed files with 1288 additions and 177 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -16,4 +16,4 @@ const Input = React.forwardRef<HTMLInputElement, React.InputHTMLAttributes<HTMLI
});
Input.displayName = 'Input';
export {Input};
export {Input};