Make Discover and upload genres dynamic
CI/CD / BackEnd (push) Successful in 1m22s
CI/CD / WebPlayer (push) Successful in 38s
CI/CD / Deploy (push) Skipped
CI/CD / BackEnd (pull_request) Successful in 1m21s
CI/CD / WebPlayer (pull_request) Successful in 40s
CI/CD / Deploy (pull_request) Skipped
CI/CD / BackEnd (push) Successful in 1m22s
CI/CD / WebPlayer (push) Successful in 38s
CI/CD / Deploy (push) Skipped
CI/CD / BackEnd (pull_request) Successful in 1m21s
CI/CD / WebPlayer (pull_request) Successful in 40s
CI/CD / Deploy (pull_request) Skipped
Discover used a hardcoded list of genre tiles that searched track titles, and the upload form kept its own copy of the genre list and compatibility rules. Both now come from the backend. Backend: - GET /genres returns genres with at least one track and their counts. - GET /genres/available returns every assignable genre with the genres it cannot be combined with, so the rules live only in GenreCompatibility. - GET /tracks accepts ?genre= (case-insensitive, 400 on unknown values). web-player: - Discover tiles are built from /genres; each opens /explore?genre=X, a song list filtered by that tag. Unknown genres fall back to a label derived from their name. - The upload form loads /genres/available, disables incompatible genres as you pick, and the server validates before reserving upload URLs. - lib/data/trackGenres.ts is removed; its helpers merge into lib/data/genres.ts. - Regenerate the typed API client.
This commit is contained in:
Vendored
+401
-298
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,75 @@
|
||||
export const GENRE_INFO: Record<string, { label: string; tagline: string }> = {
|
||||
pop: { label: 'Pop', tagline: 'Éxitos que suenan en todas partes' },
|
||||
rock: { label: 'Rock', tagline: 'Guitarras, actitud y ruido' },
|
||||
reggaeton: { label: 'Reggaetón', tagline: 'El ritmo que no para' },
|
||||
'lo-fi': { label: 'Lo-fi', tagline: 'Para concentrarte o relajarte' },
|
||||
indie: { label: 'Indie', tagline: 'Voces fuera del radar' },
|
||||
salsa: { label: 'Salsa', tagline: 'Para mover el cuerpo' },
|
||||
'k-pop': { label: 'K-pop', tagline: 'Coreografías pegajosas' },
|
||||
electrónica: { label: 'Electrónica', tagline: 'Beats para perderte' },
|
||||
jazz: { label: 'Jazz', tagline: 'Improvisación y elegancia' },
|
||||
baladas: { label: 'Baladas', tagline: 'Para sentir con calma' },
|
||||
trap: { label: 'Trap', tagline: 'Autotune y bajos pesados' },
|
||||
cumbia: { label: 'Cumbia', tagline: 'El sabor de siempre' },
|
||||
'hip hop': { label: 'Hip hop', tagline: 'Rimas con actitud' },
|
||||
'música clásica': { label: 'Clásica', tagline: 'Siglos de composición' }
|
||||
const GENRE_INFO: Record<string, { label: string; tagline: string }> = {
|
||||
Pop: { label: 'Pop', tagline: 'Éxitos que suenan en todas partes' },
|
||||
Rock: { label: 'Rock', tagline: 'Guitarras, actitud y ruido' },
|
||||
HipHop: { label: 'Hip hop', tagline: 'Rimas con actitud' },
|
||||
RnB: { label: 'R&B', tagline: 'Ritmo suave y voces con alma' },
|
||||
Jazz: { label: 'Jazz', tagline: 'Improvisación y elegancia' },
|
||||
Blues: { label: 'Blues', tagline: 'Sentimiento en cada nota' },
|
||||
Classical: { label: 'Clásica', tagline: 'Siglos de composición' },
|
||||
Electronic: { label: 'Electrónica', tagline: 'Beats para perderte' },
|
||||
House: { label: 'House', tagline: 'Pista de baile sin fin' },
|
||||
Techno: { label: 'Techno', tagline: 'Pulso hipnótico' },
|
||||
Trance: { label: 'Trance', tagline: 'Melodías que te elevan' },
|
||||
Dubstep: { label: 'Dubstep', tagline: 'Bajos que hacen temblar' },
|
||||
DrumAndBass: { label: 'Drum and bass', tagline: 'Velocidad y graves' },
|
||||
Metal: { label: 'Metal', tagline: 'Riffs pesados' },
|
||||
Punk: { label: 'Punk', tagline: 'Rápido, crudo y directo' },
|
||||
Reggae: { label: 'Reggae', tagline: 'Buenas vibraciones' },
|
||||
Reggaeton: { label: 'Reggaetón', tagline: 'El ritmo que no para' },
|
||||
Country: { label: 'Country', tagline: 'Historias con guitarra' },
|
||||
Folk: { label: 'Folk', tagline: 'Raíces y acústico' },
|
||||
Indie: { label: 'Indie', tagline: 'Voces fuera del radar' },
|
||||
KPop: { label: 'K-pop', tagline: 'Coreografías pegajosas' },
|
||||
Latin: { label: 'Latina', tagline: 'Sabor y ritmo' },
|
||||
Soul: { label: 'Soul', tagline: 'Emoción en estado puro' },
|
||||
Funk: { label: 'Funk', tagline: 'Groove imposible de quedarse quieto' },
|
||||
Ambient: { label: 'Ambient', tagline: 'Paisajes sonoros' },
|
||||
Lofi: { label: 'Lo-fi', tagline: 'Para concentrarte o relajarte' }
|
||||
};
|
||||
|
||||
export const genreTiles = Object.entries(GENRE_INFO).map(([query, info], i, all) => ({
|
||||
query,
|
||||
hue: Math.round((i * 360) / all.length),
|
||||
...info
|
||||
}));
|
||||
const KNOWN_GENRES = Object.keys(GENRE_INFO);
|
||||
|
||||
function humanize(name: string): string {
|
||||
return name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
||||
}
|
||||
|
||||
export function genreInfo(genre: string) {
|
||||
return GENRE_INFO[genre] ?? { label: humanize(genre), tagline: '' };
|
||||
}
|
||||
|
||||
export function genreHue(genre: string): number {
|
||||
const known = KNOWN_GENRES.indexOf(genre);
|
||||
if (known >= 0) return Math.round((known * 360) / KNOWN_GENRES.length);
|
||||
let hash = 0;
|
||||
for (const char of genre) hash = (hash * 31 + char.charCodeAt(0)) % 360;
|
||||
return hash;
|
||||
}
|
||||
|
||||
export function genreTiles(genres: { genre: string }[]) {
|
||||
return genres.map(({ genre }) => ({
|
||||
genre,
|
||||
hue: genreHue(genre),
|
||||
...genreInfo(genre)
|
||||
}));
|
||||
}
|
||||
|
||||
export function genreOptions(genres: { genre: string; incompatibleWith: string[] }[]) {
|
||||
return genres.map(({ genre, incompatibleWith }) => ({
|
||||
genre,
|
||||
incompatibleWith,
|
||||
label: genreInfo(genre).label
|
||||
}));
|
||||
}
|
||||
|
||||
export function findConflict(
|
||||
tags: string[],
|
||||
options: { genre: string; incompatibleWith: string[] }[]
|
||||
): [string, string] | undefined {
|
||||
const byGenre = new Map(options.map((option) => [option.genre, option.incompatibleWith]));
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
for (let j = i + 1; j < tags.length; j++) {
|
||||
if (byGenre.get(tags[i])?.includes(tags[j])) return [tags[i], tags[j]];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
export const TRACK_GENRES = [
|
||||
{ value: 'Pop', label: 'Pop' },
|
||||
{ value: 'Rock', label: 'Rock' },
|
||||
{ value: 'HipHop', label: 'Hip-hop' },
|
||||
{ value: 'RnB', label: 'R&B' },
|
||||
{ value: 'Jazz', label: 'Jazz' },
|
||||
{ value: 'Blues', label: 'Blues' },
|
||||
{ value: 'Classical', label: 'Clásica' },
|
||||
{ value: 'Electronic', label: 'Electrónica' },
|
||||
{ value: 'House', label: 'House' },
|
||||
{ value: 'Techno', label: 'Techno' },
|
||||
{ value: 'Trance', label: 'Trance' },
|
||||
{ value: 'Dubstep', label: 'Dubstep' },
|
||||
{ value: 'DrumAndBass', label: 'Drum and bass' },
|
||||
{ value: 'Metal', label: 'Metal' },
|
||||
{ value: 'Punk', label: 'Punk' },
|
||||
{ value: 'Reggae', label: 'Reggae' },
|
||||
{ value: 'Reggaeton', label: 'Reggaetón' },
|
||||
{ value: 'Country', label: 'Country' },
|
||||
{ value: 'Folk', label: 'Folk' },
|
||||
{ value: 'Indie', label: 'Indie' },
|
||||
{ value: 'KPop', label: 'K-pop' },
|
||||
{ value: 'Latin', label: 'Latina' },
|
||||
{ value: 'Soul', label: 'Soul' },
|
||||
{ value: 'Funk', label: 'Funk' },
|
||||
{ value: 'Ambient', label: 'Ambient' },
|
||||
{ value: 'Lofi', label: 'Lo-fi' }
|
||||
] as const;
|
||||
|
||||
export type TrackGenre = (typeof TRACK_GENRES)[number]['value'];
|
||||
|
||||
const GENRE_VALUES: ReadonlySet<string> = new Set(TRACK_GENRES.map((genre) => genre.value));
|
||||
|
||||
const INTENSE: ReadonlySet<TrackGenre> = new Set(['Metal', 'Punk', 'Dubstep', 'DrumAndBass']);
|
||||
const MELLOW: ReadonlySet<TrackGenre> = new Set(['Classical', 'Ambient', 'Lofi']);
|
||||
|
||||
export function isTrackGenre(value: string): value is TrackGenre {
|
||||
return GENRE_VALUES.has(value);
|
||||
}
|
||||
|
||||
export function areCompatible(a: TrackGenre, b: TrackGenre): boolean {
|
||||
return !((INTENSE.has(a) && MELLOW.has(b)) || (INTENSE.has(b) && MELLOW.has(a)));
|
||||
}
|
||||
|
||||
export function genreLabel(value: TrackGenre): string {
|
||||
return TRACK_GENRES.find((genre) => genre.value === value)?.label ?? value;
|
||||
}
|
||||
|
||||
export function findConflict(tags: readonly TrackGenre[]): [TrackGenre, TrackGenre] | undefined {
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
for (let j = i + 1; j < tags.length; j++) {
|
||||
if (!areCompatible(tags[i], tags[j])) return [tags[i], tags[j]];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -68,6 +68,10 @@ export function searchHref(query: string): string {
|
||||
return query ? `/explore?q=${encodeURIComponent(query)}` : '/explore';
|
||||
}
|
||||
|
||||
export function genreHref(genre: string): string {
|
||||
return `/explore?genre=${encodeURIComponent(genre)}`;
|
||||
}
|
||||
|
||||
export interface AppNavLink {
|
||||
href: string;
|
||||
label: string;
|
||||
|
||||
@@ -9,13 +9,25 @@ import {
|
||||
|
||||
export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
||||
const query = url.searchParams.get('q')?.trim() ?? '';
|
||||
const genreParam = url.searchParams.get('genre')?.trim() || null;
|
||||
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
|
||||
|
||||
const user = locals.user;
|
||||
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||
|
||||
const genresRes = await api.GET('/genres');
|
||||
const genres = genresRes.data ?? [];
|
||||
const genre = genres.find((item) => item.genre === genreParam)?.genre ?? null;
|
||||
|
||||
const tracksPromise = api.GET('/tracks', {
|
||||
params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
|
||||
params: {
|
||||
query: {
|
||||
name: query || undefined,
|
||||
genre: genre ?? undefined,
|
||||
pageNumber: page,
|
||||
pageSize: PAGE_SIZE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const playlistsPromise = user
|
||||
@@ -47,6 +59,8 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
||||
|
||||
return {
|
||||
query,
|
||||
genre,
|
||||
genres,
|
||||
tracks,
|
||||
albums: (albumsRes?.data?.items ?? []).map((item) => item.album),
|
||||
users: usersRes?.data?.items ?? [],
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
import Chip from '$lib/components/ui/primitives/Chip.svelte';
|
||||
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
||||
import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
||||
import { searchHref } from '$lib/state/navigation.svelte';
|
||||
import { genreHref } from '$lib/state/navigation.svelte';
|
||||
import { createTrackMenu } from '$lib/state/menus.svelte';
|
||||
import ContextMenu, { contextMenuPosition } from '$lib/components/ui/overlay/ContextMenu.svelte';
|
||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||
import ListEnd from '@lucide/svelte/icons/list-end';
|
||||
import { appendUnique } from '$lib/data/collections';
|
||||
import { genreTiles } from '$lib/data/genres';
|
||||
import { genreInfo, genreTiles } from '$lib/data/genres';
|
||||
import {
|
||||
type SearchFilter,
|
||||
showGroup,
|
||||
@@ -58,7 +58,7 @@
|
||||
$effect(() => {
|
||||
trackMenu.close();
|
||||
albumMenu = null;
|
||||
sfilter = 'Todo';
|
||||
sfilter = data.genre ? 'Canciones' : 'Todo';
|
||||
localItems = data.tracks.items.map((item) => item.track);
|
||||
localPage = Number(data.tracks.pageNumber);
|
||||
hasMore = data.tracks.hasNextPage;
|
||||
@@ -73,6 +73,9 @@
|
||||
|
||||
const items = $derived(songRows.map((row) => row.track));
|
||||
|
||||
const tiles = $derived(genreTiles(data.genres));
|
||||
const activeGenre = $derived(data.genre ? genreInfo(data.genre) : null);
|
||||
|
||||
const albums = $derived(data.albums);
|
||||
const users = $derived(data.users);
|
||||
|
||||
@@ -169,6 +172,7 @@
|
||||
pageSize: String(EXPLORE_PAGE_SIZE)
|
||||
});
|
||||
if (data.query) params.set('name', data.query);
|
||||
if (data.genre) params.set('genre', data.genre);
|
||||
const res = await fetch(`/api/tracks?${params}`);
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
const next = (await res.json()) as typeof data.tracks;
|
||||
@@ -188,7 +192,9 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.query ? `Buscar «${data.query}»` : 'Descubrir'}</title>
|
||||
<title
|
||||
>{activeGenre ? activeGenre.label : data.query ? `Buscar «${data.query}»` : 'Descubrir'}</title
|
||||
>
|
||||
<meta
|
||||
name="description"
|
||||
content="Busca canciones, álbumes y personas, o descubre por género en Musify."
|
||||
@@ -196,7 +202,9 @@
|
||||
</svelte:head>
|
||||
|
||||
<Page>
|
||||
{#if !data.query}
|
||||
{#if activeGenre}
|
||||
<PageHeader eyebrow="Género" title={activeGenre.label} description={activeGenre.tagline} />
|
||||
{:else if !data.query}
|
||||
<PageHeader
|
||||
title="Descubrir"
|
||||
description="Encuentra tu próxima canción favorita explorando por género."
|
||||
@@ -300,31 +308,34 @@
|
||||
por playlist.
|
||||
</p>
|
||||
<div class="mt-5.5 flex flex-wrap justify-center gap-2">
|
||||
{#each genreTiles.slice(0, 4) as genre (genre.query)}
|
||||
<Chip href={searchHref(genre.query)}>{genre.label}</Chip>
|
||||
{#each tiles.slice(0, 4) as tile (tile.genre)}
|
||||
<Chip href={genreHref(tile.genre)}>{tile.label}</Chip>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !data.query}
|
||||
{#if !data.query && !data.genre}
|
||||
<div class="grid-wide">
|
||||
{#each genreTiles as genre, i (genre.query)}
|
||||
{#each tiles as tile, i (tile.genre)}
|
||||
<a
|
||||
href={searchHref(genre.query)}
|
||||
href={genreHref(tile.genre)}
|
||||
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-panel p-4 transition-[filter] genre-tile hover:brightness-110"
|
||||
style="--i:{i}; --tile-hue:{genre.hue}"
|
||||
style="--i:{i}; --tile-hue:{tile.hue}"
|
||||
>
|
||||
<div class="text-on-art">
|
||||
{genre.label}
|
||||
{tile.label}
|
||||
</div>
|
||||
<div class="text-xs text-on-art-2">{genre.tagline}</div>
|
||||
<div class="text-xs text-on-art-2">{tile.tagline}</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{#if tiles.length === 0}
|
||||
<EmptyState icon={Music} description="Todavía no hay géneros. Sube música para empezar." />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if data.query && !nothingFound}
|
||||
{#if (data.query || data.genre) && !nothingFound}
|
||||
<div class="mt-9 flex flex-col gap-9">
|
||||
{#if showGroup(sfilter, 'Canciones')}
|
||||
<div>
|
||||
@@ -364,7 +375,9 @@
|
||||
{:else}
|
||||
<EmptyState
|
||||
icon={Music}
|
||||
description="No hay canciones que coincidan con «{data.query}»."
|
||||
description={activeGenre
|
||||
? 'Todavía no hay canciones de este género.'
|
||||
: `No hay canciones que coincidan con «${data.query}».`}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -7,12 +7,18 @@ import {
|
||||
unwrapOrFail
|
||||
} from '$lib/server/api';
|
||||
import { putPresigned, extOf, contentTypeOf, AUDIO_TYPES, IMAGE_TYPES } from '$lib/server/upload';
|
||||
import { findConflict, genreLabel, isTrackGenre, type TrackGenre } from '$lib/data/trackGenres';
|
||||
import { findConflict, genreInfo } from '$lib/data/genres';
|
||||
import type { components } from '$lib/api/schema';
|
||||
|
||||
type Genre = components['schemas']['Genre'];
|
||||
|
||||
const MAX_TITLE = 100;
|
||||
|
||||
export const load: PageServerLoad = ({ locals, url }) => {
|
||||
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
||||
requireUser(locals, url);
|
||||
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||
const { data } = await api.GET('/genres/available');
|
||||
return { genres: data ?? [] };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
@@ -39,20 +45,23 @@ export const actions: Actions = {
|
||||
if (rawTags.length === 0) {
|
||||
return fail(400, { message: 'Elige al menos un género.' });
|
||||
}
|
||||
if (!rawTags.every(isTrackGenre)) {
|
||||
const api = createApiClient({ fetch, accessToken });
|
||||
const { data: available } = await api.GET('/genres/available');
|
||||
const options = available ?? [];
|
||||
const known = new Set<string>(options.map((option) => option.genre));
|
||||
if (!rawTags.every((tag) => known.has(tag))) {
|
||||
return fail(400, { message: 'Alguno de los géneros no es válido.' });
|
||||
}
|
||||
const tags: TrackGenre[] = [...new Set(rawTags)];
|
||||
const conflict = findConflict(tags);
|
||||
const tags = [...new Set(rawTags)] as Genre[];
|
||||
const conflict = findConflict(tags, options);
|
||||
if (conflict) {
|
||||
return fail(400, {
|
||||
message: `Los géneros ${genreLabel(conflict[0])} y ${genreLabel(conflict[1])} no se pueden combinar.`
|
||||
message: `Los géneros ${genreInfo(conflict[0]).label} y ${genreInfo(conflict[1]).label} no se pueden combinar.`
|
||||
});
|
||||
}
|
||||
|
||||
const audioContentType = contentTypeOf(audio, AUDIO_TYPES);
|
||||
const pictureContentType = contentTypeOf(cover, IMAGE_TYPES);
|
||||
const api = createApiClient({ fetch, accessToken });
|
||||
|
||||
const urlsResult = await api.POST('/tracks/upload-urls', {
|
||||
body: {
|
||||
|
||||
@@ -14,13 +14,15 @@
|
||||
import Checkbox from '$lib/components/ui/primitives/Checkbox.svelte';
|
||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
||||
import Chip from '$lib/components/ui/primitives/Chip.svelte';
|
||||
import { TRACK_GENRES, areCompatible, type TrackGenre } from '$lib/data/trackGenres';
|
||||
import { genreOptions } from '$lib/data/genres';
|
||||
|
||||
type Status = 'idle' | 'uploading' | 'done' | 'error';
|
||||
type StepState = 'idle' | 'active' | 'done';
|
||||
|
||||
const MAX_TITLE = 100;
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let title = $state('');
|
||||
let audioName = $state('');
|
||||
let audioSize = $state(0);
|
||||
@@ -31,7 +33,7 @@
|
||||
let acceptedTerms = $state(false);
|
||||
let errorMsg = $state('');
|
||||
let errorDetail = $state('');
|
||||
let tags = $state<TrackGenre[]>([]);
|
||||
let tags = $state<string[]>([]);
|
||||
let publishedTitle = $state('');
|
||||
let audioInput = $state<HTMLInputElement>();
|
||||
|
||||
@@ -41,6 +43,15 @@
|
||||
{ key: 'publish', label: 'Publicar', hint: 'Disponible en el catálogo.' }
|
||||
];
|
||||
|
||||
const options = $derived(genreOptions(data.genres));
|
||||
const blocked = $derived(
|
||||
new Set(
|
||||
options
|
||||
.filter((option) => tags.includes(option.genre))
|
||||
.flatMap((option) => option.incompatibleWith)
|
||||
)
|
||||
);
|
||||
|
||||
const busy = $derived(status === 'uploading');
|
||||
|
||||
const canPublish = $derived(
|
||||
@@ -85,14 +96,10 @@
|
||||
pickAudio(file);
|
||||
}
|
||||
|
||||
function toggleTag(tag: TrackGenre) {
|
||||
function toggleTag(tag: string) {
|
||||
tags = tags.includes(tag) ? tags.filter((current) => current !== tag) : [...tags, tag];
|
||||
}
|
||||
|
||||
function tagBlocked(tag: TrackGenre) {
|
||||
return !tags.includes(tag) && tags.some((current) => !areCompatible(current, tag));
|
||||
}
|
||||
|
||||
function reset() {
|
||||
title = '';
|
||||
audioName = '';
|
||||
@@ -254,13 +261,13 @@
|
||||
|
||||
<Field label="Géneros">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each TRACK_GENRES as genre (genre.value)}
|
||||
{#each options as option (option.genre)}
|
||||
<Chip
|
||||
selected={tags.includes(genre.value)}
|
||||
disabled={tagBlocked(genre.value)}
|
||||
onclick={() => toggleTag(genre.value)}
|
||||
selected={tags.includes(option.genre)}
|
||||
disabled={blocked.has(option.genre)}
|
||||
onclick={() => toggleTag(option.genre)}
|
||||
>
|
||||
{genre.label}
|
||||
{option.label}
|
||||
</Chip>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,13 @@ import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
||||
|
||||
export const GET: RequestHandler = async ({ url, locals, fetch }) => {
|
||||
const name = url.searchParams.get('name')?.trim() || undefined;
|
||||
const genre = url.searchParams.get('genre')?.trim() || undefined;
|
||||
const pageNumber = Math.max(1, Number(url.searchParams.get('pageNumber')) || 1);
|
||||
const pageSize = Math.max(1, Number(url.searchParams.get('pageSize')) || EXPLORE_PAGE_SIZE);
|
||||
|
||||
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||
const result = await api.GET('/tracks', {
|
||||
params: { query: { name, pageNumber, pageSize } }
|
||||
params: { query: { name, genre, pageNumber, pageSize } }
|
||||
});
|
||||
|
||||
return json(unwrapOrError(result, 'No se pudieron cargar las canciones.'));
|
||||
|
||||
Reference in New Issue
Block a user