feat(frontend): redesign playlist pages with cover image support
Create and edit playlist modals now upload/preview a cover image (reusing the presigned-upload flow already used for tracks) and share identical layout and coloring. Adds a cover proxy route and wires PlaylistArt to render a playlist's real cover before falling back to the track mosaic/gradient. Also reworks the playlist detail page: full-width borderless "Añadir de tu biblioteca" table with listens count and click-to-play, drops the empty-playlist placeholder message, and restyles the playlists grid to match the rest of the app.
This commit is contained in:
@@ -1,9 +1,27 @@
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { error, fail, redirect } from '@sveltejs/kit';
|
||||
import { createApiClient } from '$lib/server/api';
|
||||
import { putPresigned } from '$lib/server/upload';
|
||||
|
||||
const MAX_NAME = 100;
|
||||
|
||||
const IMAGE_TYPES: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp'
|
||||
};
|
||||
|
||||
function extOf(name: string): string {
|
||||
const dot = name.lastIndexOf('.');
|
||||
return dot >= 0 ? name.slice(dot + 1).toLowerCase() : '';
|
||||
}
|
||||
|
||||
function contentTypeOf(file: File): string {
|
||||
if (file.type) return file.type;
|
||||
return IMAGE_TYPES[extOf(file.name)] ?? 'application/octet-stream';
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, fetch }) => {
|
||||
if (!locals.user) error(401, 'Inicia sesión.');
|
||||
|
||||
@@ -33,14 +51,46 @@ export const actions: Actions = {
|
||||
const form = await request.formData();
|
||||
const name = String(form.get('name') ?? '').trim();
|
||||
const description = String(form.get('description') ?? '').trim();
|
||||
const cover = form.get('cover');
|
||||
|
||||
if (name === '' || name.length > MAX_NAME) {
|
||||
return fail(400, { message: 'El nombre es obligatorio (máx. 100 caracteres).', name, description });
|
||||
}
|
||||
|
||||
const api = createApiClient({ fetch, accessToken: locals.accessToken });
|
||||
|
||||
let pictureIntentId: string | null = null;
|
||||
if (cover instanceof File && cover.size > 0) {
|
||||
const contentType = contentTypeOf(cover);
|
||||
const { data: upload, error: uploadError } = await api.POST('/playlists/upload-picture', {
|
||||
body: {
|
||||
fileType: extOf(cover.name) || 'jpg',
|
||||
contentType,
|
||||
expectedSizeBytes: cover.size
|
||||
}
|
||||
});
|
||||
|
||||
if (uploadError || !upload) {
|
||||
return fail(502, { message: 'No se pudo reservar la subida de la portada.', name, description });
|
||||
}
|
||||
|
||||
try {
|
||||
const coverBuffer = await cover.arrayBuffer();
|
||||
await putPresigned(upload.uploadUrl, Buffer.from(coverBuffer), upload.contentType);
|
||||
} catch (err) {
|
||||
return fail(502, {
|
||||
message: 'Falló la subida de la portada al almacenamiento.',
|
||||
name,
|
||||
description,
|
||||
detail: err instanceof Error ? err.message : undefined
|
||||
});
|
||||
}
|
||||
|
||||
pictureIntentId = upload.intentId;
|
||||
}
|
||||
|
||||
const { data, error: err } = await api.POST('/playlists', {
|
||||
body: { name, description, pictureIntentId: null }
|
||||
body: { name, description, pictureIntentId }
|
||||
});
|
||||
|
||||
if (err || !data) return fail(502, { message: 'No se pudo crear la playlist.', name, description });
|
||||
|
||||
Reference in New Issue
Block a user