feat(frontend): add (app) route group and rework auth routing

Move authenticated pages under an (app) group with a Sidebar shell, add /auth/login and page-based /login, drop the old explore page and login +server endpoint, and add the server upload helper.
This commit is contained in:
2026-07-14 04:55:04 +02:00
parent 6cc41d1d6f
commit f155ccc912
19 changed files with 1584 additions and 31 deletions
@@ -0,0 +1,18 @@
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
import { createApiClient } from '$lib/server/api';
const PAGE_SIZE = 50;
export const load: PageServerLoad = async ({ locals, fetch }) => {
if (!locals.user) error(401, 'Inicia sesión.');
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const { data, error: err } = await api.GET('/tracks/users/{userId}', {
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: PAGE_SIZE } }
});
if (err || !data) error(502, 'No se pudo cargar tu biblioteca.');
return { tracks: data };
};
@@ -0,0 +1,96 @@
<script lang="ts">
import Upload from '@lucide/svelte/icons/upload';
import { player } from '$lib/player/player.svelte';
import { HUES } from '$lib/theme/color';
import Cover from '$lib/components/ui/Cover.svelte';
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
let { data } = $props();
const items = $derived(data.tracks.items);
const total = $derived(Number(data.tracks.totalItemCount));
const dateFormatter = new Intl.DateTimeFormat('es', { dateStyle: 'medium' });
function hueFor(id: string) {
let hash = 0;
for (let i = 0; i < id.length; i++) hash = (hash * 31 + id.charCodeAt(i)) >>> 0;
return HUES[hash % HUES.length];
}
function togglePlay(index: number) {
const track = items[index];
if (player.current.id === track.id) player.toggle();
else player.playQueue(items, index);
}
</script>
<svelte:head>
<title>Tu biblioteca · Musify</title>
<meta name="description" content="Todas tus canciones en Musify." />
</svelte:head>
<section class="px-8 py-11">
<div class="flex items-end justify-between gap-4">
<div>
<h1 class="font-display text-[28px] font-extrabold tracking-tight">Tu biblioteca</h1>
<p class="mt-1.5 text-sm text-[var(--mf-text-2)]">
{total}
{total === 1 ? 'canción' : 'canciones'} en tu biblioteca.
</p>
</div>
<a
href="/upload"
class="inline-flex items-center gap-2 rounded-lg bg-[var(--mf-accent)] px-5 py-2.5 text-sm font-semibold text-neutral-950 transition hover:brightness-110 hover:shadow-[0_8px_24px_-8px] active:scale-[0.97]"
>
<Upload class="h-4 w-4" />
Subir música
</a>
</div>
{#if items.length > 0}
<div class="mt-7 flex max-w-[900px] flex-col">
{#each items as track, i (track.id)}
<button
type="button"
onclick={() => togglePlay(i)}
class="animate-enter flex items-center gap-3.5 rounded-[10px] px-3 py-[11px] text-left transition duration-100 hover:bg-[var(--mf-surface-hover)] active:scale-[0.99]"
style="animation-delay:{i * 35}ms"
>
<Cover
trackId={track.id}
hue={hueFor(track.id)}
size="small"
alt={track.title}
class="h-[46px] w-[46px] flex-shrink-0 rounded-md"
>
{#if player.current.id === track.id}
<NowPlaying paused={!player.playing} />
{/if}
</Cover>
<div class="min-w-0 flex-1">
<div class="truncate text-[14.5px] font-semibold text-[var(--mf-text)]">
{track.title}
</div>
<div class="truncate text-[12.5px] text-[var(--mf-text-3)]">
{dateFormatter.format(new Date(track.createdAt))}
</div>
</div>
</button>
{/each}
</div>
{:else}
<div
class="mt-7 max-w-[560px] rounded-xl border border-[var(--mf-border)] bg-[var(--mf-surface)] px-6 py-10 text-center"
>
<p class="text-sm text-[var(--mf-text-2)]">Todavía no tienes canciones</p>
<a
href="/upload"
class="mt-4 inline-flex items-center gap-2 rounded-lg bg-[var(--mf-accent)] px-5 py-2.5 text-sm font-semibold text-neutral-950 transition hover:brightness-110 hover:shadow-[0_8px_24px_-8px] active:scale-[0.97]"
>
<Upload class="h-4 w-4" />
Sube la primera
</a>
</div>
{/if}
</section>