737 lines
32 KiB
TypeScript
737 lines
32 KiB
TypeScript
'use client';
|
|
|
|
import { useState } 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 AddAlbumModalProps {
|
|
show: boolean;
|
|
onClose: () => void;
|
|
onAdd: (album: Album) => void;
|
|
}
|
|
|
|
export default function AddAlbumModal({ show, onClose, onAdd }: AddAlbumModalProps) {
|
|
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');
|
|
const [uploadMethod, setUploadMethod] = useState<'file' | 'url'>('file');
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const [songUploadMethod, setSongUploadMethod] = useState<{ [key: number]: { preview: 'file' | 'url', full: 'file' | 'url' } }>({});
|
|
const [uploadingSong, setUploadingSong] = useState<{ index: number, field: 'preview' | 'full' } | null>(null);
|
|
|
|
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 uploadFile = async (file: File) => {
|
|
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 handleCoverImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
await uploadFile(file);
|
|
};
|
|
|
|
const handleDragOver = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(true);
|
|
};
|
|
|
|
const handleDragLeave = (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(false);
|
|
};
|
|
|
|
const handleDrop = async (e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
setIsDragging(false);
|
|
|
|
const file = e.dataTransfer.files[0];
|
|
if (!file) return;
|
|
|
|
// Validate file type
|
|
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
|
|
if (!allowedTypes.includes(file.type)) {
|
|
setError('Invalid file type. Only images are allowed.');
|
|
return;
|
|
}
|
|
|
|
await uploadFile(file);
|
|
};
|
|
|
|
const uploadAudioFile = async (file: File, index: number, field: 'preview' | 'full') => {
|
|
setUploadingSong({ index, field });
|
|
setError('');
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('folder', field === 'preview' ? 'audio/previews' : 'audio/full');
|
|
|
|
const response = await fetch('/api/upload/audio', {
|
|
method: 'POST',
|
|
body: formData,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Upload failed');
|
|
}
|
|
|
|
const data = await response.json();
|
|
const updatedSongs = [...songs];
|
|
updatedSongs[index][field === 'preview' ? 'previewUrl' : 'fullUrl'] = data.url;
|
|
setSongs(updatedSongs);
|
|
} catch (err) {
|
|
setError(`Failed to upload ${field} audio file`);
|
|
} finally {
|
|
setUploadingSong(null);
|
|
}
|
|
};
|
|
|
|
const handleAudioFileChange = async (e: React.ChangeEvent<HTMLInputElement>, index: number, field: 'preview' | 'full') => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
await uploadAudioFile(file, index, field);
|
|
};
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// Create album
|
|
const albumId = title.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
|
|
const albumSongs: Song[] = songs.map((song, index) => ({
|
|
id: `${albumId}-${index + 1}`,
|
|
title: song.title,
|
|
duration: song.duration,
|
|
previewUrl: song.previewUrl || '/audio/preview-1.mp3',
|
|
fullUrl: song.fullUrl || '/audio/default-full.mp3',
|
|
}));
|
|
|
|
const newAlbum: Album = {
|
|
id: albumId,
|
|
title,
|
|
coverImage: coverImage || '/albums/default-cover.jpg',
|
|
year: yearNum,
|
|
genre,
|
|
description,
|
|
price: priceNum,
|
|
songs: albumSongs,
|
|
tag,
|
|
format,
|
|
bitrate,
|
|
};
|
|
|
|
onAdd(newAlbum);
|
|
handleClose();
|
|
};
|
|
|
|
const handleClose = () => {
|
|
setTitle('');
|
|
setYear('');
|
|
setGenre('');
|
|
setDescription('');
|
|
setPrice('');
|
|
setCoverImage('');
|
|
setCoverImageFile(null);
|
|
setSongs([{ id: '', title: '', duration: '', previewUrl: '', fullUrl: '' }]);
|
|
setError('');
|
|
setTag('Album');
|
|
setFormat('mp3');
|
|
setBitrate('320kbps');
|
|
onClose();
|
|
};
|
|
|
|
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-cyan/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-cyan to-accent-orange">
|
|
Add New 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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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>
|
|
|
|
{/* Method Tabs */}
|
|
<div className="flex gap-2 mb-3">
|
|
<button
|
|
type="button"
|
|
onClick={() => setUploadMethod('file')}
|
|
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
|
uploadMethod === 'file'
|
|
? 'bg-accent-cyan text-white'
|
|
: 'bg-white/5 text-gray-400 hover:bg-white/10'
|
|
} rounded-lg`}
|
|
>
|
|
<FaUpload className="inline mr-2" />
|
|
Upload File
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setUploadMethod('url')}
|
|
className={`px-4 py-2 text-sm font-medium transition-colors ${
|
|
uploadMethod === 'url'
|
|
? 'bg-accent-cyan text-white'
|
|
: 'bg-white/5 text-gray-400 hover:bg-white/10'
|
|
} rounded-lg`}
|
|
>
|
|
<FaImage className="inline mr-2" />
|
|
Enter URL
|
|
</button>
|
|
</div>
|
|
|
|
{uploadMethod === 'file' ? (
|
|
<>
|
|
{/* Drag & Drop Zone */}
|
|
<div
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
className={`relative border-2 border-dashed rounded-lg p-8 transition-all ${
|
|
isDragging
|
|
? 'border-accent-cyan bg-accent-cyan/10'
|
|
: 'border-white/20 hover:border-white/40'
|
|
}`}
|
|
>
|
|
{isUploading ? (
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-4 border-accent-cyan border-t-transparent mx-auto mb-4"></div>
|
|
<p className="text-gray-400">Uploading...</p>
|
|
</div>
|
|
) : coverImage ? (
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-20 h-20 rounded-lg overflow-hidden border-2 border-white/20 flex-shrink-0">
|
|
<img src={coverImage} alt="Cover preview" className="w-full h-full object-cover" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="text-accent-cyan font-medium mb-1">Image Uploaded Successfully</p>
|
|
<p className="text-xs text-gray-500 break-all">{coverImage}</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setCoverImage('')}
|
|
className="text-gray-400 hover:text-white transition-colors"
|
|
>
|
|
<FaTimes />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="text-center">
|
|
<FaUpload className="text-4xl text-gray-400 mx-auto mb-4" />
|
|
<p className="text-gray-300 mb-2">Drag & drop your image here</p>
|
|
<p className="text-sm text-gray-500 mb-4">or</p>
|
|
<label className="inline-block px-6 py-3 bg-white/5 hover:bg-white/10 border border-white/10 rounded-lg cursor-pointer transition-colors">
|
|
<span className="text-gray-300 font-medium">Browse Files</span>
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleCoverImageChange}
|
|
className="hidden"
|
|
disabled={isUploading}
|
|
/>
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Supported: JPG, PNG, WEBP, GIF (max 5MB)
|
|
</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
{/* URL Input */}
|
|
<div className="space-y-3">
|
|
<input
|
|
type="url"
|
|
value={coverImage}
|
|
onChange={(e) => setCoverImage(e.target.value)}
|
|
placeholder="https://example.com/album-cover.jpg"
|
|
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-lg focus:border-accent-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500"
|
|
/>
|
|
{coverImage && (
|
|
<div className="w-32 h-32 rounded-lg overflow-hidden border-2 border-white/20">
|
|
<img src={coverImage} alt="Cover preview" className="w-full h-full object-cover" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-2">
|
|
Enter the direct URL to the album cover image
|
|
</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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan/20 hover:bg-accent-cyan/30 text-accent-cyan 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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500 text-sm"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
{/* Full Song Audio */}
|
|
<div className="space-y-1">
|
|
<label className="text-xs text-gray-400">Full Song Audio</label>
|
|
<div className="flex gap-1 mb-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setSongUploadMethod({ ...songUploadMethod, [index]: { ...songUploadMethod[index], full: 'file' } })}
|
|
className={`px-2 py-1 text-xs font-medium transition-colors ${
|
|
songUploadMethod[index]?.full === 'file'
|
|
? 'bg-accent-cyan text-white'
|
|
: 'bg-white/5 text-gray-400'
|
|
} rounded`}
|
|
>
|
|
Upload
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setSongUploadMethod({ ...songUploadMethod, [index]: { ...songUploadMethod[index], full: 'url' } })}
|
|
className={`px-2 py-1 text-xs font-medium transition-colors ${
|
|
songUploadMethod[index]?.full === 'url'
|
|
? 'bg-accent-cyan text-white'
|
|
: 'bg-white/5 text-gray-400'
|
|
} rounded`}
|
|
>
|
|
URL
|
|
</button>
|
|
</div>
|
|
{songUploadMethod[index]?.full === 'file' ? (
|
|
<div className="relative">
|
|
{uploadingSong?.index === index && uploadingSong.field === 'full' ? (
|
|
<div className="px-3 py-6 bg-white/5 border border-white/10 rounded-lg text-center">
|
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-accent-cyan border-t-transparent mx-auto"></div>
|
|
<p className="text-xs text-gray-400 mt-2">Uploading...</p>
|
|
</div>
|
|
) : song.fullUrl ? (
|
|
<div className="px-3 py-2 bg-white/5 border border-white/10 rounded-lg flex items-center justify-between">
|
|
<span className="text-xs text-accent-cyan truncate">{song.fullUrl.split('/').pop()}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleSongChange(index, 'fullUrl', '')}
|
|
className="text-gray-400 hover:text-white"
|
|
>
|
|
<FaTimes className="text-xs" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<label className="block px-3 py-6 bg-white/5 border-2 border-dashed border-white/20 hover:border-accent-cyan rounded-lg cursor-pointer transition-colors text-center">
|
|
<FaUpload className="text-gray-400 mx-auto mb-1" />
|
|
<span className="text-xs text-gray-400">Click or drop audio file</span>
|
|
<input
|
|
type="file"
|
|
accept="audio/*"
|
|
onChange={(e) => handleAudioFileChange(e, index, 'full')}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<input
|
|
type="url"
|
|
value={song.fullUrl}
|
|
onChange={(e) => handleSongChange(index, 'fullUrl', e.target.value)}
|
|
placeholder="https://example.com/song.mp3"
|
|
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg focus:border-accent-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500 text-sm"
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Preview Audio */}
|
|
<div className="space-y-1">
|
|
<label className="text-xs text-gray-400">Preview (30s clip - optional)</label>
|
|
<div className="flex gap-1 mb-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setSongUploadMethod({ ...songUploadMethod, [index]: { ...songUploadMethod[index], preview: 'file' } })}
|
|
className={`px-2 py-1 text-xs font-medium transition-colors ${
|
|
songUploadMethod[index]?.preview === 'file'
|
|
? 'bg-accent-cyan text-white'
|
|
: 'bg-white/5 text-gray-400'
|
|
} rounded`}
|
|
>
|
|
Upload
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setSongUploadMethod({ ...songUploadMethod, [index]: { ...songUploadMethod[index], preview: 'url' } })}
|
|
className={`px-2 py-1 text-xs font-medium transition-colors ${
|
|
songUploadMethod[index]?.preview === 'url'
|
|
? 'bg-accent-cyan text-white'
|
|
: 'bg-white/5 text-gray-400'
|
|
} rounded`}
|
|
>
|
|
URL
|
|
</button>
|
|
</div>
|
|
{songUploadMethod[index]?.preview === 'file' ? (
|
|
<div className="relative">
|
|
{uploadingSong?.index === index && uploadingSong.field === 'preview' ? (
|
|
<div className="px-3 py-6 bg-white/5 border border-white/10 rounded-lg text-center">
|
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-accent-cyan border-t-transparent mx-auto"></div>
|
|
<p className="text-xs text-gray-400 mt-2">Uploading...</p>
|
|
</div>
|
|
) : song.previewUrl ? (
|
|
<div className="px-3 py-2 bg-white/5 border border-white/10 rounded-lg flex items-center justify-between">
|
|
<span className="text-xs text-accent-cyan truncate">{song.previewUrl.split('/').pop()}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleSongChange(index, 'previewUrl', '')}
|
|
className="text-gray-400 hover:text-white"
|
|
>
|
|
<FaTimes className="text-xs" />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<label className="block px-3 py-6 bg-white/5 border-2 border-dashed border-white/20 hover:border-accent-cyan rounded-lg cursor-pointer transition-colors text-center">
|
|
<FaUpload className="text-gray-400 mx-auto mb-1" />
|
|
<span className="text-xs text-gray-400">Click or drop audio file</span>
|
|
<input
|
|
type="file"
|
|
accept="audio/*"
|
|
onChange={(e) => handleAudioFileChange(e, index, 'preview')}
|
|
className="hidden"
|
|
/>
|
|
</label>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<input
|
|
type="url"
|
|
value={song.previewUrl}
|
|
onChange={(e) => handleSongChange(index, 'previewUrl', e.target.value)}
|
|
placeholder="https://example.com/preview.mp3"
|
|
className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg focus:border-accent-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500 text-sm"
|
|
/>
|
|
)}
|
|
</div>
|
|
</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-cyan to-accent-cyan/80 hover:from-accent-cyan/90 hover:to-accent-cyan/70 rounded-lg font-semibold text-white transition-all glow-cyan"
|
|
>
|
|
Add Album
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</motion.div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|