main: added presigned url + favicon
Signed-off-by: nfel <nfilsaraee@gmail.com>
This commit is contained in:
+313
-49
@@ -25,6 +25,10 @@ export default function AddAlbumModal({ show, onClose, onAdd }: AddAlbumModalPro
|
||||
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: '' }]);
|
||||
@@ -42,10 +46,7 @@ export default function AddAlbumModal({ show, onClose, onAdd }: AddAlbumModalPro
|
||||
setSongs(updatedSongs);
|
||||
};
|
||||
|
||||
const handleCoverImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
setCoverImageFile(file);
|
||||
setIsUploading(true);
|
||||
setError('');
|
||||
@@ -74,6 +75,74 @@ export default function AddAlbumModal({ show, onClose, onAdd }: AddAlbumModalPro
|
||||
}
|
||||
};
|
||||
|
||||
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('');
|
||||
@@ -215,43 +284,114 @@ export default function AddAlbumModal({ show, onClose, onAdd }: AddAlbumModalPro
|
||||
<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">
|
||||
|
||||
{/* 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="animate-spin rounded-full h-4 w-4 border-2 border-accent-cyan border-t-transparent"></div>
|
||||
<span className="text-gray-400 text-sm">Uploading...</span>
|
||||
</>
|
||||
<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 ? (
|
||||
<>
|
||||
<FaImage className="text-accent-cyan" />
|
||||
<span className="text-accent-cyan text-sm">Image Uploaded</span>
|
||||
</>
|
||||
<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>
|
||||
) : (
|
||||
<>
|
||||
<FaUpload className="text-gray-400" />
|
||||
<span className="text-gray-400 text-sm">Choose Image</span>
|
||||
</>
|
||||
<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>
|
||||
<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" />
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Upload an album cover image (max 5MB, JPG/PNG/WEBP)
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Enter the direct URL to the album cover image
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Year and Genre */}
|
||||
@@ -417,20 +557,144 @@ export default function AddAlbumModal({ show, onClose, onAdd }: AddAlbumModalPro
|
||||
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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/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-cyan focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 text-white placeholder-gray-500 text-sm"
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
|
||||
@@ -47,10 +47,20 @@ export default function AlbumCard({ album, isPurchased, onPlay, onPurchase }: Al
|
||||
className="paper-card-light overflow-hidden group cursor-pointer hover:shadow-paper-lg transition-shadow"
|
||||
>
|
||||
{/* Album Cover */}
|
||||
<div className="relative aspect-square bg-paper-brown flex items-center justify-center overflow-hidden border-b-2 border-paper-gray">
|
||||
<div className="relative z-10 text-6xl font-bold text-paper-dark/20 p-8 text-center">
|
||||
{album.title}
|
||||
</div>
|
||||
<div className="relative aspect-square bg-paper-brown overflow-hidden border-b-2 border-paper-gray">
|
||||
{album.coverImage ? (
|
||||
<img
|
||||
src={album.coverImage}
|
||||
alt={album.title}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
<div className="relative z-10 text-6xl font-bold text-paper-dark/20 p-8 text-center">
|
||||
{album.title}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay on hover */}
|
||||
<div className="absolute inset-0 bg-paper-dark/80 opacity-0 group-hover:opacity-100 transition-all duration-300 flex items-center justify-center gap-3">
|
||||
|
||||
+313
-49
@@ -26,6 +26,10 @@ export default function EditAlbumModal({ show, album, onClose, onUpdate }: EditA
|
||||
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);
|
||||
|
||||
// Populate form when album changes
|
||||
useEffect(() => {
|
||||
@@ -59,10 +63,7 @@ export default function EditAlbumModal({ show, album, onClose, onUpdate }: EditA
|
||||
setSongs(updatedSongs);
|
||||
};
|
||||
|
||||
const handleCoverImageChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
setCoverImageFile(file);
|
||||
setIsUploading(true);
|
||||
setError('');
|
||||
@@ -91,6 +92,74 @@ export default function EditAlbumModal({ show, album, onClose, onUpdate }: EditA
|
||||
}
|
||||
};
|
||||
|
||||
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('');
|
||||
@@ -224,43 +293,114 @@ export default function EditAlbumModal({ show, album, onClose, onUpdate }: EditA
|
||||
<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">
|
||||
|
||||
{/* 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-orange 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-orange 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-orange bg-accent-orange/10'
|
||||
: 'border-white/20 hover:border-white/40'
|
||||
}`}
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-4 border-accent-orange border-t-transparent mx-auto mb-4"></div>
|
||||
<p className="text-gray-400">Uploading...</p>
|
||||
</div>
|
||||
) : coverImage ? (
|
||||
<>
|
||||
<FaImage className="text-accent-orange" />
|
||||
<span className="text-accent-orange text-sm">Change Image</span>
|
||||
</>
|
||||
<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-orange font-medium mb-1">Image Set</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>
|
||||
) : (
|
||||
<>
|
||||
<FaUpload className="text-gray-400" />
|
||||
<span className="text-gray-400 text-sm">Choose Image</span>
|
||||
</>
|
||||
<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>
|
||||
<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" />
|
||||
<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-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/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>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Upload an album cover image (max 5MB, JPG/PNG/WEBP)
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
Enter the direct URL to the album cover image
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Year and Genre */}
|
||||
@@ -426,20 +566,144 @@ export default function EditAlbumModal({ show, album, onClose, onUpdate }: EditA
|
||||
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"
|
||||
/>
|
||||
|
||||
{/* 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-orange 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-orange 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-orange 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-orange 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-orange 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-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/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-orange 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-orange 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-orange 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-orange 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-orange 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-orange focus:outline-none focus:ring-2 focus:ring-accent-orange/50 text-white placeholder-gray-500 text-sm"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -149,9 +149,9 @@ export default function MusicPlayer({ album, isPurchased, onClose, onPurchase }:
|
||||
<div className="absolute inset-0 bg-paper-brown"></div>
|
||||
<div className="absolute inset-0 cardboard-texture"></div>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-4xl md:text-5xl font-bold text-paper-dark/30 text-center px-6">
|
||||
{album.title}
|
||||
</div>
|
||||
{/* <div className="text-4xl md:text-5xl font-bold text-paper-dark/30 text-center px-6"> */}
|
||||
<img src={album.coverImage} alt={album.title} />
|
||||
{/* </div> */}
|
||||
</div>
|
||||
|
||||
{/* Preview Badge */}
|
||||
|
||||
Reference in New Issue
Block a user