podzahr/components/EditAlbumModal.tsx
nfel 9fd79a2d4e
main: fix build issue
Signed-off-by: nfel <nfilsaraee@gmail.com>
2025-12-30 02:07:26 +03:30

482 lines
19 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('');
const [tag, setTag] = useState<'Album' | 'EP' | 'Demo' | 'Deluxe' | 'Single'>('Album');
const [format, setFormat] = useState<'mp3' | 'm4a' | 'flac' | 'wav'>('mp3');
const [bitrate, setBitrate] = useState('320kbps');
// 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 })));
setTag(album.tag);
setFormat(album.format);
setBitrate(album.bitrate);
}
}, [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,
tag,
format,
bitrate,
};
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>
{/* Tag, Format, and Bitrate */}
<div className="grid grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Tag *
</label>
<select
value={tag}
onChange={(e) => setTag(e.target.value as any)}
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"
required
>
<option value="Album">Album</option>
<option value="EP">EP</option>
<option value="Demo">Demo</option>
<option value="Deluxe">Deluxe</option>
<option value="Single">Single</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Format *
</label>
<select
value={format}
onChange={(e) => setFormat(e.target.value as any)}
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"
required
>
<option value="mp3">MP3</option>
<option value="m4a">M4A</option>
<option value="flac">FLAC</option>
<option value="wav">WAV</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">
Bitrate *
</label>
<input
type="text"
value={bitrate}
onChange={(e) => setBitrate(e.target.value)}
placeholder="320kbps"
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>
{/* 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>
);
}