423 lines
16 KiB
TypeScript
423 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { FaTimes, FaPlus, FaTrash, FaUpload, FaImage } from 'react-icons/fa';
|
|
import { Album, Song } from '@/lib/types';
|
|
|
|
interface EditAlbumModalProps {
|
|
show: boolean;
|
|
album: Album | null;
|
|
onClose: () => void;
|
|
onUpdate: (albumId: string, album: Album) => void;
|
|
}
|
|
|
|
export default function EditAlbumModal({ show, album, onClose, onUpdate }: EditAlbumModalProps) {
|
|
const [title, setTitle] = useState('');
|
|
const [year, setYear] = useState('');
|
|
const [genre, setGenre] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [price, setPrice] = useState('');
|
|
const [coverImage, setCoverImage] = useState('');
|
|
const [coverImageFile, setCoverImageFile] = useState<File | null>(null);
|
|
const [isUploading, setIsUploading] = useState(false);
|
|
const [songs, setSongs] = useState<Song[]>([{ id: '', title: '', duration: '', previewUrl: '', fullUrl: '' }]);
|
|
const [error, setError] = useState('');
|
|
|
|
// Populate form when album changes
|
|
useEffect(() => {
|
|
if (album) {
|
|
setTitle(album.title);
|
|
setYear(album.year.toString());
|
|
setGenre(album.genre);
|
|
setDescription(album.description);
|
|
setPrice(album.price.toString());
|
|
setCoverImage(album.coverImage || '');
|
|
setSongs(album.songs.map((song) => ({ ...song })));
|
|
}
|
|
}, [album]);
|
|
|
|
const handleAddSong = () => {
|
|
setSongs([...songs, { id: '', title: '', duration: '', previewUrl: '', fullUrl: '' }]);
|
|
};
|
|
|
|
const handleRemoveSong = (index: number) => {
|
|
if (songs.length > 1) {
|
|
setSongs(songs.filter((_, i) => i !== index));
|
|
}
|
|
};
|
|
|
|
const handleSongChange = (index: number, field: 'title' | 'duration' | 'previewUrl' | 'fullUrl', value: string) => {
|
|
const updatedSongs = [...songs];
|
|
updatedSongs[index][field] = value;
|
|
setSongs(updatedSongs);
|
|
};
|
|
|
|
const handleCoverImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
setCoverImageFile(file);
|
|
setIsUploading(true);
|
|
setError('');
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('folder', 'album-covers');
|
|
|
|
const response = await fetch('/api/upload', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Upload failed');
|
|
}
|
|
|
|
const data = await response.json();
|
|
setCoverImage(data.url);
|
|
} catch (err) {
|
|
setError('Failed to upload cover image');
|
|
setCoverImageFile(null);
|
|
} finally {
|
|
setIsUploading(false);
|
|
}
|
|
};
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
if (!album) return;
|
|
|
|
// Validation
|
|
if (!title.trim()) {
|
|
setError('Title is required');
|
|
return;
|
|
}
|
|
|
|
const yearNum = parseInt(year);
|
|
if (!year || yearNum < 1900 || yearNum > new Date().getFullYear() + 1) {
|
|
setError('Please enter a valid year');
|
|
return;
|
|
}
|
|
|
|
if (!genre.trim()) {
|
|
setError('Genre is required');
|
|
return;
|
|
}
|
|
|
|
if (!description.trim()) {
|
|
setError('Description is required');
|
|
return;
|
|
}
|
|
|
|
const priceNum = parseFloat(price);
|
|
if (!price || priceNum <= 0) {
|
|
setError('Please enter a valid price');
|
|
return;
|
|
}
|
|
|
|
// Validate songs
|
|
for (let i = 0; i < songs.length; i++) {
|
|
if (!songs[i].title.trim()) {
|
|
setError(`Song ${i + 1} title is required`);
|
|
return;
|
|
}
|
|
if (!songs[i].duration.trim()) {
|
|
setError(`Song ${i + 1} duration is required`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Update album
|
|
const albumSongs: Song[] = songs.map((song, index) => ({
|
|
id: song.id || `${album.id}-${index + 1}`,
|
|
title: song.title,
|
|
duration: song.duration,
|
|
previewUrl: song.previewUrl || '/audio/preview-1.mp3',
|
|
fullUrl: song.fullUrl || '/audio/default-full.mp3',
|
|
}));
|
|
|
|
const updatedAlbum: Album = {
|
|
id: album.id,
|
|
title,
|
|
coverImage: coverImage || album.coverImage || '/albums/default-cover.jpg',
|
|
year: yearNum,
|
|
genre,
|
|
description,
|
|
price: priceNum,
|
|
songs: albumSongs,
|
|
};
|
|
|
|
onUpdate(album.id, updatedAlbum);
|
|
handleClose();
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setError('');
|
|
onClose();
|
|
};
|
|
|
|
if (!album) return null;
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{show && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
className="fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
|
onClick={handleClose}
|
|
>
|
|
<motion.div
|
|
initial={{ scale: 0.9, opacity: 0 }}
|
|
animate={{ scale: 1, opacity: 1 }}
|
|
exit={{ scale: 0.9, opacity: 0 }}
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="glass-effect rounded-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto p-8 border-2 border-accent-orange/30"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent-orange to-accent-cyan">
|
|
Edit Album
|
|
</h2>
|
|
<button
|
|
onClick={handleClose}
|
|
className="p-2 hover:bg-white/10 rounded-full transition-colors"
|
|
aria-label="Close"
|
|
>
|
|
<FaTimes className="text-xl text-gray-400" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Form */}
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
{/* Title */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Album Title *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={title}
|
|
onChange={(e) => setTitle(e.target.value)}
|
|
placeholder="Enter album title"
|
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Cover Image Upload */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Album Cover Image
|
|
</label>
|
|
<div className="flex items-center gap-4">
|
|
<label className="flex-1 cursor-pointer">
|
|
<div className="flex items-center justify-center gap-2 px-4 py-3 bg-white/5 border border-white/10 rounded-lg hover:bg-white/10 transition-colors">
|
|
{isUploading ? (
|
|
<>
|
|
<div className="animate-spin rounded-full h-4 w-4 border-2 border-accent-orange border-t-transparent"></div>
|
|
<span className="text-gray-400 text-sm">Uploading...</span>
|
|
</>
|
|
) : coverImage ? (
|
|
<>
|
|
<FaImage className="text-accent-orange" />
|
|
<span className="text-accent-orange text-sm">Change Image</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<FaUpload className="text-gray-400" />
|
|
<span className="text-gray-400 text-sm">Choose Image</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleCoverImageChange}
|
|
className="hidden"
|
|
disabled={isUploading}
|
|
/>
|
|
</label>
|
|
{coverImage && (
|
|
<div className="w-16 h-16 rounded-lg overflow-hidden border border-white/10">
|
|
<img src={coverImage} alt="Cover preview" className="w-full h-full object-cover" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Upload an album cover image (max 5MB, JPG/PNG/WEBP)
|
|
</p>
|
|
</div>
|
|
|
|
{/* Year and Genre */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Year *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
value={year}
|
|
onChange={(e) => setYear(e.target.value)}
|
|
placeholder="2024"
|
|
min="1900"
|
|
max={new Date().getFullYear() + 1}
|
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500"
|
|
required
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Genre *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={genre}
|
|
onChange={(e) => setGenre(e.target.value)}
|
|
placeholder="Progressive Rock"
|
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500"
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Description *
|
|
</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="Enter album description"
|
|
rows={3}
|
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500 resize-none"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Price */}
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Price ($) *
|
|
</label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
value={price}
|
|
onChange={(e) => setPrice(e.target.value)}
|
|
placeholder="9.99"
|
|
min="0.01"
|
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Songs */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="block text-sm font-medium text-gray-300">
|
|
Songs *
|
|
</label>
|
|
<button
|
|
type="button"
|
|
onClick={handleAddSong}
|
|
className="px-3 py-1 bg-accent-orange/20 hover:bg-accent-orange/30 text-accent-orange rounded-lg text-sm flex items-center gap-1 transition-colors"
|
|
>
|
|
<FaPlus className="text-xs" />
|
|
Add Song
|
|
</button>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{songs.map((song, index) => (
|
|
<div key={index} className="p-4 bg-white/5 rounded-lg border border-white/10 space-y-2">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<span className="text-sm font-medium text-gray-300">Song {index + 1}</span>
|
|
{songs.length > 1 && (
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveSong(index)}
|
|
className="p-1.5 bg-red-500/20 hover:bg-red-500/30 text-red-400 rounded-lg transition-colors"
|
|
aria-label="Remove song"
|
|
>
|
|
<FaTrash className="text-xs" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<input
|
|
type="text"
|
|
value={song.title}
|
|
onChange={(e) => handleSongChange(index, 'title', e.target.value)}
|
|
placeholder="Song title"
|
|
className="px-3 py-2 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500 text-sm"
|
|
required
|
|
/>
|
|
<input
|
|
type="text"
|
|
value={song.duration}
|
|
onChange={(e) => handleSongChange(index, 'duration', e.target.value)}
|
|
placeholder="3:45"
|
|
className="px-3 py-2 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500 text-sm"
|
|
required
|
|
/>
|
|
</div>
|
|
<input
|
|
type="url"
|
|
value={song.fullUrl}
|
|
onChange={(e) => handleSongChange(index, 'fullUrl', e.target.value)}
|
|
placeholder="Song URL (e.g., https://example.com/song.mp3)"
|
|
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500 text-sm"
|
|
/>
|
|
<input
|
|
type="url"
|
|
value={song.previewUrl}
|
|
onChange={(e) => handleSongChange(index, 'previewUrl', e.target.value)}
|
|
placeholder="Preview URL (optional - 30s clip)"
|
|
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg focus:border-accent-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500 text-sm"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Error Message */}
|
|
{error && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: -10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-300 text-sm"
|
|
>
|
|
{error}
|
|
</motion.div>
|
|
)}
|
|
|
|
{/* Submit Button */}
|
|
<div className="flex gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={handleClose}
|
|
className="flex-1 py-3 bg-white/10 hover:bg-white/20 rounded-lg font-semibold text-white transition-all"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="flex-1 py-3 bg-gradient-to-r from-accent-orange to-accent-orange/80 hover:from-accent-orange/90 hover:to-accent-orange/70 rounded-lg font-semibold text-white transition-all glow-orange"
|
|
>
|
|
Update Album
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|