main: added presigned url + favicon

Signed-off-by: nfel <nfilsaraee@gmail.com>
This commit is contained in:
2026-01-01 19:06:11 +03:30
parent 9fd79a2d4e
commit 9478aa319f
10 changed files with 955 additions and 124 deletions
+24 -14
View File
@@ -147,11 +147,21 @@ export default function AlbumDetailPage() {
transition={{ duration: 0.6 }}
className="relative"
>
<div className="aspect-square overflow-hidden bg-paper-brown flex items-center justify-center border-4 border-paper-dark shadow-paper-lg">
<div className="absolute inset-0 cardboard-texture"></div>
<div className="relative z-10 text-6xl md:text-8xl font-bold text-paper-dark/20 p-8 text-center">
{album.title}
</div>
<div className="aspect-square overflow-hidden bg-paper-brown border-4 border-paper-dark shadow-paper-lg">
{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 relative">
<div className="absolute inset-0 cardboard-texture"></div>
<div className="relative z-10 text-6xl md:text-8xl font-bold text-paper-dark/20 p-8 text-center">
{album.title}
</div>
</div>
)}
</div>
{isPurchased && (
@@ -250,29 +260,29 @@ export default function AlbumDetailPage() {
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.4, delay: index * 0.05 }}
className="flex items-center justify-between p-4 hover:bg-paper-sand border-2 border-transparent hover:border-paper-brown transition-all group cursor-pointer"
className="flex items-center justify-between p-3 md:p-4 hover:bg-paper-sand border-2 border-transparent hover:border-paper-brown transition-all group cursor-pointer"
onClick={() => setCurrentAlbum(album)}
>
<div className="flex items-center gap-4 flex-1">
<span className="text-paper-gray font-mono text-sm w-8">
<div className="flex items-center gap-2 md:gap-4 flex-1 min-w-0">
<span className="text-paper-gray font-mono text-xs md:text-sm w-6 md:w-8 flex-shrink-0">
{String(index + 1).padStart(2, '0')}
</span>
<div className="flex-1">
<h3 className="text-paper-dark font-medium group-hover:font-bold transition-all">
<div className="flex-1 min-w-0">
<h3 className="text-paper-dark font-medium group-hover:font-bold transition-all truncate text-sm md:text-base">
{song.title}
</h3>
</div>
</div>
<div className="flex items-center gap-6">
<div className="flex items-center gap-2 md:gap-6 flex-shrink-0">
{!isPurchased && (
<span className="text-xs text-paper-dark px-2 py-1 bg-paper-brown/20 border border-paper-brown">
<span className="hidden sm:inline text-xs text-paper-dark px-2 py-1 bg-paper-brown/20 border border-paper-brown whitespace-nowrap">
Preview
</span>
)}
<span className="text-paper-brown text-sm font-mono">
<span className="text-paper-brown text-xs md:text-sm font-mono whitespace-nowrap">
{song.duration}
</span>
<FaPlay className="text-paper-dark opacity-0 group-hover:opacity-100 transition-opacity" />
<FaPlay className="text-paper-dark opacity-0 group-hover:opacity-100 transition-opacity text-xs md:text-base flex-shrink-0" />
</div>
</motion.div>
))}
+69
View File
@@ -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 }
);
}
}