+
{!isPurchased && (
-
+
Preview
)}
-
+
{song.duration}
-
+
))}
diff --git a/app/api/s3/presigned-url/route.ts b/app/api/s3/presigned-url/route.ts
new file mode 100644
index 0000000..9137ded
--- /dev/null
+++ b/app/api/s3/presigned-url/route.ts
@@ -0,0 +1,69 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { getPresignedUrl, getMultiplePresignedUrls } from '@/lib/s3';
+
+/**
+ * Generate pre-signed URL(s) for S3 objects
+ * POST /api/s3/presigned-url
+ *
+ * Body:
+ * - Single key: { "key": "path/to/file.mp3", "expiresIn": 3600 }
+ * - Multiple keys: { "keys": ["path/file1.mp3", "path/file2.mp3"], "expiresIn": 3600 }
+ */
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { key, keys, expiresIn = 3600 } = body;
+
+ // Validate expiration time (max 7 days)
+ if (expiresIn > 604800) {
+ return NextResponse.json(
+ { error: 'expiresIn cannot exceed 7 days (604800 seconds)' },
+ { status: 400 }
+ );
+ }
+
+ // Handle single key
+ if (key) {
+ if (typeof key !== 'string' || !key.trim()) {
+ return NextResponse.json(
+ { error: 'Invalid key provided' },
+ { status: 400 }
+ );
+ }
+
+ const url = await getPresignedUrl(key, expiresIn);
+ return NextResponse.json({ url, expiresIn }, { status: 200 });
+ }
+
+ // Handle multiple keys
+ if (keys) {
+ if (!Array.isArray(keys) || keys.length === 0) {
+ return NextResponse.json(
+ { error: 'keys must be a non-empty array' },
+ { status: 400 }
+ );
+ }
+
+ if (keys.length > 100) {
+ return NextResponse.json(
+ { error: 'Cannot generate more than 100 URLs at once' },
+ { status: 400 }
+ );
+ }
+
+ const urls = await getMultiplePresignedUrls(keys, expiresIn);
+ return NextResponse.json({ urls, expiresIn }, { status: 200 });
+ }
+
+ return NextResponse.json(
+ { error: 'Either "key" or "keys" must be provided' },
+ { status: 400 }
+ );
+ } catch (error: any) {
+ console.error('Presigned URL generation error:', error);
+ return NextResponse.json(
+ { error: error.message || 'Failed to generate presigned URL' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/components/AddAlbumModal.tsx b/components/AddAlbumModal.tsx
index 0260562..8d3ea4f 100644
--- a/components/AddAlbumModal.tsx
+++ b/components/AddAlbumModal.tsx
@@ -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
) => {
- 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) => {
+ 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, 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
-
-
- 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"
- />
- 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 */}
+
+
Full Song Audio
+
+
+
+
+ {songUploadMethod[index]?.full === 'file' ? (
+
+ {uploadingSong?.index === index && uploadingSong.field === 'full' ? (
+
+ ) : song.fullUrl ? (
+
+ {song.fullUrl.split('/').pop()}
+
+
+ ) : (
+
+
+ Click or drop audio file
+ handleAudioFileChange(e, index, 'full')}
+ className="hidden"
+ />
+
+ )}
+
+ ) : (
+
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"
+ />
+ )}
+
+
+ {/* Preview Audio */}
+
+
Preview (30s clip - optional)
+
+
+
+
+ {songUploadMethod[index]?.preview === 'file' ? (
+
+ {uploadingSong?.index === index && uploadingSong.field === 'preview' ? (
+
+ ) : song.previewUrl ? (
+
+ {song.previewUrl.split('/').pop()}
+
+
+ ) : (
+
+
+ Click or drop audio file
+ handleAudioFileChange(e, index, 'preview')}
+ className="hidden"
+ />
+
+ )}
+
+ ) : (
+
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"
+ />
+ )}
+