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

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:
2026-09-23 20:05:30 +02:00
parent 8870c65c6f
commit b61ff1ebec
19 changed files with 906 additions and 414 deletions
+19 -12
View File
@@ -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>