Web Player Refactor #2

Merged
NotS1mp merged 50 commits from web-player-ref into master 2026-09-23 15:05:12 +00:00
3 changed files with 114 additions and 281 deletions
Showing only changes of commit a3ec9d19d3 - Show all commits
+36 -38
View File
@@ -1,22 +1,22 @@
# Musify — web-player # Musify: web-player
Musify's public web app (`web-player/` folder). For design and UI recipes, use the **`musify-web`** skill. Musify's public web app (`web-player/` folder). For design and UI recipes, use the **`musify-web`** skill.
## Stack ## Stack
SvelteKit 2 (`adapter-node`) · Svelte 5 **runes** · Tailwind 4 · TypeScript strict · Zitadel OIDC auth (`openid-client` + `jose`). SvelteKit 2 (`adapter-node`), Svelte 5 **runes**, Tailwind 4, TypeScript strict, Zitadel OIDC auth (`openid-client` + `jose`).
There is no `svelte.config.js` (config is inline in `vite.config.ts`) nor `tailwind.config.js` (config lives in `src/routes/layout.css`). There's no `svelte.config.js` (config is inline in `vite.config.ts`) and no `tailwind.config.js` (config lives in `src/routes/layout.css`).
## Commands ## Commands
```sh ```sh
npm run dev # dev server (port 5173) npm run dev # dev server (port 5173)
npm run build npm run build
npm run check # svelte-check — must exit 0 errors before closing a change npm run check # svelte-check, must exit 0 errors before closing a change
npm run format # prettier --write . npm run format # prettier --write .
npm run lint # prettier --check . && eslint . && npm run lint:tokens npm run lint # prettier --check . && eslint . && npm run lint:tokens
npm run lint:tokens # scripts/check-tokens.mjs — design-system guardrail (see below) npm run lint:tokens # scripts/check-tokens.mjs, the design system guardrail (see below)
``` ```
## Rules ## Rules
@@ -24,50 +24,48 @@ npm run lint:tokens # scripts/check-tokens.mjs — design-system guardrail (se
- **No comments** in code unless essential. - **No comments** in code unless essential.
- `npm run lint` and `npm run check` must exit clean before closing a change. - `npm run lint` and `npm run check` must exit clean before closing a change.
- **Always Svelte 5 runes:** `$props()` with `interface Props`, `$state`, `$derived`, `$effect`. No `export let` or `$:`. `{#each}` always keyed. - **Always Svelte 5 runes:** `$props()` with `interface Props`, `$state`, `$derived`, `$effect`. No `export let` or `$:`. `{#each}` always keyed.
- **Server-only code under `$lib/server/`**; never imported from client code. - **Server only code goes under `$lib/server/`**, and it's never imported from client code.
- SSR data via `load` in `+page.server.ts`/`+layout.server.ts` → `data` prop. - SSR data comes through `load` in `+page.server.ts`/`+layout.server.ts` and lands in the `data` prop.
- Links to `+server.ts` endpoints (`/auth/login`, `/auth/logout`) carry `data-sveltekit-reload`. - Links to `+server.ts` endpoints (`/auth/login`, `/auth/logout`) carry `data-sveltekit-reload`.
- Reuse design-system tokens/recipes; extract to `$lib/components/ui/` whatever repeats. - Reuse design system tokens and recipes before writing new CSS. If something repeats, extract it to `$lib/components/ui/`.
## Folder structure ## Folder structure
Full rationale and history in `docs/folder-structure-plan.md`. - `src/lib/state/` holds reactive rune based state (`*.svelte.ts` files that don't have a more specific home). Player state stays in `$lib/player/`, theme mode in `$lib/theme/`.
- `src/lib/data/` holds client side domain helpers: albums, collections, genres, mixes, recentlyPlayed, search.
- `src/lib/state/` — reactive rune-based state (`*.svelte.ts` with no other home; player state stays in `$lib/player/`, theme mode in `$lib/theme/`). - `src/lib/utils/` holds generic utilities with no domain knowledge, like `format.ts`.
- `src/lib/data/` — client-side domain/data helpers (albums, collections, genres, mixes, recentlyPlayed, search). - `src/lib/components/layout/` holds the app chrome: Sidebar, TopBar, TabsBar, MobileHeader.
- `src/lib/utils/` — generic, domain-agnostic utilities (e.g. `format.ts`). - `src/lib/components/player/` holds the player UI: PlayerBar, PlayerDock, Queue, TrackInfo, TransportControls.
- `src/lib/components/layout/` — app chrome (Sidebar, TopBar, TabsBar, MobileHeader). - `src/lib/components/ui/` holds the design system components, split into five categories:
- `src/lib/components/player/` — player UI (PlayerBar, PlayerDock, Queue, TrackInfo, TransportControls). - `primitives/` for generic controls with no music domain knowledge (Button, Input, Chip, Slider, and so on).
- `src/lib/components/ui/` — design-system components, split by category: - `overlay/` for anything that floats above the content (Modal, ContextMenu, AccountMenu, and so on).
- `primitives/` — generic controls with no music-domain knowledge (Button, Input, Chip, Slider, …). - `media/` for music domain display components (Artwork, TrackList, MediaCard, PlayButton, and so on).
- `overlay/` — anything that floats above content (Modal, ContextMenu, AccountMenu, …). - `forms/` for composite entity forms (AlbumForm, PlaylistForm, CoverForm, ImageDropzone).
- `media/` — music-domain display components (Artwork, TrackList, MediaCard, PlayButton, …). - `layout/` for page level scaffolding (Page, PageHeader, SectionHeading). This is different from `components/layout/` above, which is the global app chrome.
- `forms/` — composite entity forms (AlbumForm, PlaylistForm, CoverForm, ImageDropzone). - Auth routes live under `src/routes/auth/`: `+page.svelte` is the login screen, and `login/`, `logout/`, `callback/` are the OIDC endpoints. The public user profile route is `src/routes/(app)/user/[id]/`.
- `layout/` — page-level scaffolding (Page, PageHeader, SectionHeading) — distinct from `components/layout/`, which is the global app chrome.
- Auth routes live under `src/routes/auth/` (`+page.svelte` = login screen, `login/`, `logout/`, `callback/` = OIDC endpoints). Public user profile is `src/routes/(app)/user/[id]/`.
## Styles ## Styles
The design system lives in `src/routes/layout.css` (`@theme` tokens + `@utility` recipes) and `src/lib/theme/` (`theme.css` for static neutrals, `tokens.ts` for user-hue-dependent dynamic tokens). `scripts/check-tokens.mjs` (`npm run lint:tokens`, part of `lint`) fails if `src/**/*.svelte` reintroduces what the `docs/frontend-refactor-plan.md` (P0–P14) refactor removed — read it before adding an arbitrary value or a "stock" radius/size. The design system lives in `src/routes/layout.css` (`@theme` tokens plus `@utility` recipes) and in `src/lib/theme/` (`theme.css` for the static neutrals, `tokens.ts` for the dynamic tokens that depend on the user's hue). `scripts/check-tokens.mjs` (run as `npm run lint:tokens`, part of `lint`) fails if `src/**/*.svelte` brings back what the refactor in `docs/frontend-refactor-plan.md` (parts P0 through P14) removed, so read that doc before adding an arbitrary value or a stock radius or size.
- **Radii:** always `rounded-tag/thumb/control/art/art-lg/panel/panel-lg` (or `rounded-full` for pills/circles). Never `rounded-sm/md/lg/xl/2xl/3xl` or bare `rounded`. - **Radii:** always `rounded-tag/thumb/control/art/art-lg/panel/panel-lg` (or `rounded-full` for pills and circles). Never `rounded-sm/md/lg/xl/2xl/3xl` or a bare `rounded`.
- **Square sizes:** `size-*` instead of repeated `h-N w-N` (icons: `size-icon-xs/sm/md/lg/xl`; covers: `size-cover-xs…hero`). - **Square sizes:** use `size-*` instead of repeating `h-N w-N` (icons: `size-icon-xs/sm/md/lg/xl`; covers: `size-cover-xs` through `size-cover-hero`).
- **Typography:** recipes `text-display-1/2/3/4`, `text-eyebrow`, `text-body`, `text-count`; tracking via `tracking-display`/`tracking-eyebrow`, not a loose `tracking-[…]`. - **Typography:** use the recipes `text-display-1/2/3/4`, `text-eyebrow`, `text-body`, `text-count`, with tracking through `tracking-display`/`tracking-eyebrow` rather than a loose `tracking-[…]`.
- **Spacing:** Tailwind scale in `.5` steps (avoid quarters like `.25`/`.75`; only acceptable if the semantic token calls for it, e.g. `mb-4.5` in `SectionHeading`). - **Spacing:** stick to the Tailwind scale in `.5` steps and avoid quarters like `.25`/`.75`, unless the semantic token calls for it (for example `mb-4.5` in `SectionHeading`).
- **Colors:** always `--mf-*`/`text-*`/`bg-*`/`border-*` from `theme.css`/`tokens.ts`. Never literal hex or `rgba()` in a `.svelte` file (those literals only exist as the source of truth inside `theme.css`/`tokens.ts`). - **Colors:** always use `--mf-*`/`text-*`/`bg-*`/`border-*` from `theme.css`/`tokens.ts`. Never a literal hex or `rgba()` inside a `.svelte` file; those literals only belong in `theme.css`/`tokens.ts` as the source of truth.
- **`style="…"` and arbitrary `-[…]` values:** only the whitelist in section B.4 of `docs/frontend-refactor-plan.md` (stagger `--i`, tile hue, menu position, dynamic widths/heights via `var(--mf-*)`, `text-[clamp(…)]`, `grid-cols-[auto_1fr]`, `max-h-[85dvh]`, `vh` paddings for vertical centering). Everything else goes to a token. - **Inline `style="…"` and arbitrary `-[…]` values:** only the ones whitelisted in section B.4 of `docs/frontend-refactor-plan.md` (the stagger variable `--i`, tile hue, menu position, dynamic widths and heights through `var(--mf-*)`, `text-[clamp(…)]`, `grid-cols-[auto_1fr]`, `max-h-[85dvh]`, and `vh` paddings for vertical centering). Everything else should become a token.
- Base components to reuse before creating a new one: `Artwork`, `Avatar`, `ListRow`, `MediaIdentity`, `TrackList`, `MediaCard`, `PageHeader`, `ContextMenu`, `ConfirmDialog`, `PlayButton`, `Slider`, `Logo`, `Chip`, `SegmentedControl`, `CoverForm`. - Reach for these base components before creating a new one: `Artwork`, `Avatar`, `ListRow`, `MediaIdentity`, `TrackList`, `MediaCard`, `PageHeader`, `ContextMenu`, `ConfirmDialog`, `PlayButton`, `Slider`, `Logo`, `Chip`, `SegmentedControl`, `CoverForm`.
- Known, accepted gaps (documented in `scripts/check-tokens.mjs`): `strokeWidth={n}` on icons isn't unified (13 distinct values spread across almost every component; safely migrating it to `stroke-thin/regular/bold` recipes would require visually verifying that `lucide-svelte` respects `stroke-width` via CSS, so it was left out of that pass). - Known and accepted gap (documented in `scripts/check-tokens.mjs`): icon `strokeWidth={n}` isn't unified yet. There are 13 different values spread across almost every component, and migrating them to `stroke-thin/regular/bold` recipes safely would require visually confirming that `lucide-svelte` actually respects `stroke-width` through CSS, so that was left out of the last pass.
## Auth ## Auth
`hooks.server.ts` validates the encrypted session cookie (`mf_session`), refreshes the `access_token`, and populates `locals.user` / `locals.accessToken`. To protect a route, check `locals.user` in its `load` and redirect to `/auth?returnTo=…`. For the backend, send `locals.accessToken` as a Bearer token to `API_BASE_URL`. `hooks.server.ts` validates the encrypted session cookie (`mf_session`), refreshes the `access_token`, and fills `locals.user` and `locals.accessToken`. To protect a route, check `locals.user` in its `load` and redirect to `/auth?returnTo=…`. When talking to the backend, send `locals.accessToken` as a Bearer token to `API_BASE_URL`.
Environment variables in `.env` (see `.env.example`): `ZITADEL_*`, `AUTH_REDIRECT_URI`, `AUTH_POST_LOGOUT_URI`, `SESSION_SECRET`, `API_BASE_URL`. Environment variables live in `.env` (see `.env.example`): `ZITADEL_*`, `AUTH_REDIRECT_URI`, `AUTH_POST_LOGOUT_URI`, `SESSION_SECRET`, `API_BASE_URL`.
## Backend API ## Backend API
Typed client via `openapi-fetch` over `Musify.Api`'s OpenAPI spec. In a server `load`/action: Typed client via `openapi-fetch` over the `Musify.Api` OpenAPI spec. Inside a server `load` or action:
```ts ```ts
import { createApiClient } from '$lib/server/api'; import { createApiClient } from '$lib/server/api';
@@ -75,4 +73,4 @@ const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefine
const { data, error: err } = await api.GET('/tracks'); const { data, error: err } = await api.GET('/tracks');
``` ```
Returns `{ data, error }` (ErrorOr-style), never throws. Types live in `src/lib/api/schema.d.ts` and are regenerated with `npm run gen:api` (requires the dev API running at `API_BASE_URL`, which exposes `/openapi/v1.json` only in Development). It returns `{ data, error }` (ErrorOr style) and never throws. The types live in `src/lib/api/schema.d.ts` and get regenerated with `npm run gen:api` (this needs the dev API running at `API_BASE_URL`, which only exposes `/openapi/v1.json` in Development).
-133
View File
@@ -1,133 +0,0 @@
# Plan — Organización de carpetas (web-player)
> Plan de trabajo para agentes/IA. Cubre la **estructura de directorios**, no el sistema de
> diseño (para tokens/componentización visual ver `docs/frontend-refactor-plan.md`, P0–P14,
> que sigue siendo la referencia para radios/tamaños/tipografía/colores).
> Basado en una lectura completa de `web-player/src` (2026-09-22, rama `web-player-ref`).
## Cómo usar este documento
1. Cada fase (F1…F7) es independiente y es **un commit**. No mezcles fases.
2. Al mover archivos: actualizar todos los imports que los referencian (`grep` del nombre
viejo de ruta antes de dar la fase por cerrada).
3. Cierre de cada fase: `npm run check` y `npm run lint` **limpios** (desde `web-player/`).
4. Al terminar una fase, marca su casilla en la sección Registro.
Orden recomendado: `F1 → F2 → F3 → F4 → F6` (independientes entre sí, pueden ir en
cualquier orden). `F5` es opcional y requiere confirmación explícita del usuario antes de
empezarla (cambia URLs públicas / config de Zitadel).
---
## Diagnóstico
- `src/lib/` raíz tiene 12 archivos sueltos mezclando estado reactivo (runes), helpers de
dominio y utilidades genéricas.
- `src/lib/components/` raíz tiene 4 componentes de layout/chrome sueltos (ni `ui/` ni
`player/`).
- `src/lib/components/ui/` tiene 38 componentes en un único nivel, sin categorizar.
- `src/routes/` reparte auth en cuatro sitios distintos (`/login`, `/logout`, `/auth/login`,
`/auth/callback`).
---
## F1 — Agrupar estado reactivo
Crear `src/lib/state/` y mover:
- `menus.svelte.ts`
- `navigation.svelte.ts`
- `playlists.svelte.ts`
- `toggle.svelte.ts`
`lib/player/*.svelte.ts` y `lib/theme/mode.svelte.ts` se quedan donde están (ya tienen
dominio propio, no son huérfanos).
**Cierre:** imports actualizados, `npm run check` y `npm run lint` limpios.
## F2 — Agrupar helpers de dominio
Crear `src/lib/data/` y mover:
- `albums.ts`
- `collections.ts`
- `genres.ts`
- `mixes.ts`
- `recentlyPlayed.ts`
- `search.ts`
**Cierre:** igual que F1.
## F3 — Utilidades genéricas
- `format.ts` → `src/lib/utils/format.ts`.
- `types.ts` se queda en la raíz de `lib/` (si crece mucho, pasar a `src/lib/types/index.ts`
en una fase futura — no crear la carpeta por adelantado).
- `config.ts` se queda en la raíz de `lib/` (config global, un único archivo, no amerita
carpeta).
**Cierre:** igual que F1.
## F4 — Componentes de layout/chrome
Crear `src/lib/components/layout/` y mover:
- `MobileHeader.svelte`
- `Sidebar.svelte`
- `TabsBar.svelte`
- `TopBar.svelte`
**Cierre:** igual que F1.
## F5 — Consolidar rutas de auth y perfil de usuario
Unificar `/login`, `/logout`, `/auth/login`, `/auth/callback` bajo `/auth/`, y renombrar
`/u/[id]` a `/user/[id]`:
- `/auth` → page (login screen, antes `/login/+page.svelte`)
- `/auth/login` → server endpoint (ya existía)
- `/auth/logout` → server endpoint (antes `/logout/+server.ts`)
- `/auth/callback` → server endpoint (ya existía)
- `/user/[id]` → page (antes `/u/[id]`)
**Cambia URLs públicas.** Se verificó `.env.example`: `AUTH_REDIRECT_URI` ya apunta a
`/auth/callback` y `AUTH_POST_LOGOUT_URI` a `/`, ninguno de los dos cambia con esta fase,
así que no hace falta tocar la config de Zitadel.
## F6 — Categorizar `components/ui/`
38 componentes en un único nivel dificultan el escaneo visual y el autocompletado del
editor. Agrupar por categoría funcional (no por jerarquía atómica estricta — sería excesivo
para este tamaño):
| Carpeta | Componentes | Criterio |
| -------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| `primitives/` | Alert, BackLink, Button, Checkbox, Chip, EmptyState, Field, IconButton, InfiniteScroll, Input, Logo, SegmentedControl, Slider, Surface, Textarea | Piezas genéricas, sin conocimiento del dominio música |
| `overlay/` | AccountMenu, ConfirmDialog, ContextMenu, GlassMenu, MenuItem, Modal | Todo lo que flota encima del contenido |
| `media/` | ArtistLink, Artwork, Avatar, EqBars, ExplicitBadge, ListRow, MediaCard, MediaIdentity, PlayButton, TrackList | Específicos del dominio música/track |
| `forms/` | AlbumForm, CoverForm, ImageDropzone, PlaylistForm | Formularios compuestos de entidad |
| `layout/` | Page, PageHeader, SectionHeading | Andamiaje de página (distinto de `components/layout/` de F4, que es el chrome global) |
Sin barrels/`index.ts` — actualizar cada ruta de import directamente, no hace falta esa
indirección para esto.
Idealmente un sub-commit por categoría, pero en la práctica hay imports relativos
(`./X.svelte`) que cruzan categorías (p. ej. `AlbumForm` → `CoverForm`+`Field`+`Input`, que
caen en `forms/`+`primitives/`), así que mover una categoría a la vez deja `npm run check`
roto a mitad de camino. Se hizo en **un único commit** que mueve las 38 a la vez y reescribe
todos los imports (internos y externos) — sigue siendo revisable porque son solo
renames + cambios de ruta de import, sin lógica nueva.
**Cierre:** las 5 subcarpetas creadas, `ui/` raíz sin `.svelte` sueltos, imports
actualizados, `npm run check` y `npm run lint` limpios.
---
## Registro
- [x] F1 — estado reactivo → `lib/state/`
- [x] F2 — helpers de dominio → `lib/data/`
- [x] F3 — utilidades genéricas (`format.ts` → `lib/utils/`)
- [x] F4 — componentes de layout/chrome → `components/layout/`
- [x] F5 — consolidar rutas de auth bajo `/auth/` y `/u/[id]` → `/user/[id]`
- [x] F6 — categorizar `components/ui/` (primitives/overlay/media/forms/layout)
+78 -110
View File
@@ -1,160 +1,128 @@
# Musify — Guía de páginas # Musify, page guide
Documento vivo de qué debe contener cada página del frontend web. Sirve como mapa Living document describing what each page of the web frontend should contain. It works as a map for prioritizing features. The design system (tokens, recipes, components) lives in the `musify-web` skill; here we describe **content and structure**, not styling.
para priorizar features. El sistema de diseño (tokens, recetas, componentes) vive en
el skill `musify-web`; aquí describimos **contenido y estructura**, no estilos.
Leyenda de estado: ✅ hecho · 🟡 parcial / maqueta · ⬜ pendiente. Status legend: ✅ done · 🟡 partial / mockup · ⬜ pending.
> **Nota (rediseño app):** se está portando el prototipo `Musify.dc.html` (app: sidebar + > **Note (app redesign):** the `Musify.dc.html` prototype is being ported over (an app with a sidebar, views, and a persistent player with a dynamic accent per track). The marketing landing page was removed. The sections below still describe the older design and are pending an update to match the app views (Home, Search, Library, Upload, Import, Downloads, Playlists). The **player** is already built as components in `$lib/components/player/`, with its theme in `$lib/theme/` (`theme.css` for the palette, `color.ts` for the accent and gradient).
> vistas + player persistente con acento dinámico por pista). La landing de marketing se
> eliminó. Las secciones de abajo describen el diseño anterior y quedan pendientes de
> actualizar a las vistas de la app (Inicio, Buscar, Biblioteca, Subir, Importar, Descargas,
> Playlists). El **player** ya está hecho por componentes en `$lib/components/player/` con el
> tema en `$lib/theme/` (`theme.css` paleta + `color.ts` acento/gradiente).
## Inicio (app) `/` 🟡 ## Home (app) `/` 🟡
- **Hero**: saludo + "Tu música. Sin límites." con color-wash del hue de la pista actual. - **Hero**: a greeting plus "Your music. No limits." with a color wash taken from the current track's hue.
- **Acceso rápido = escuchado recientemente** (`player.recentlyPlayed`, hoy en cliente). - **Quick access, meaning recently played** (`player.recentlyPlayed`, client side for now). Empty state with an icon and an "Explore" CTA.
Estado vacío con icono + CTA «Explorar». - **Your playlists**: mosaic cards for the user's playlists (sample data for now). Has an empty state.
- **Tus playlists**: tarjetas mosaico de las playlists (hoy datos de muestra). Estado vacío.
### Backend pendiente (ASP.NET) para Inicio ### Pending backend work (ASP.NET) for Home
- **Escuchado recientemente**: registrar reproducciones por usuario (tabla de historial: - **Recently played**: track plays per user (a history table with `userId`, `trackId`, `playedAt`) and expose `GET /me/recently-played?limit=` (or `/users/{id}/recently-played`), returning tracks ordered by `playedAt` descending, with no duplicates. The frontend would load this in a `load` function and pass it to the store instead of using `recentlyPlayed`.
`userId`, `trackId`, `playedAt`) y exponer `GET /me/recently-played?limit=` (o - **Playlists by last listen**: add `lastPlayedAt` to the playlist (the schema currently only has `createdAt`/`updatedAt`) and allow sorting `GET /playlists/users/{userId}` by that field (for example `?sort=lastPlayed`), to show "your most recently played" playlists.
`/users/{id}/recently-played`) devolviendo pistas ordenadas por `playedAt` desc, sin - Tracks will need an **artist** and a **color/hue** (or a real cover to extract one from) for the artwork and the dynamic accent. Right now `TrackApplicationResponse` carries neither artist nor color.
duplicados. El front lo cargaría en un `load` y lo pasaría al store en vez de `recentlyPlayed`.
- **Playlists por última escucha**: añadir `lastPlayedAt` a la playlist (hoy el schema solo
tiene `createdAt`/`updatedAt`) y permitir ordenar `GET /playlists/users/{userId}` por ese
campo (p. ej. `?sort=lastPlayed`), para mostrar «tus últimas escuchadas».
- Las pistas necesitarán **artista** y un **color/hue** (o portada real de la que extraerlo)
para el arte y el acento dinámico; hoy `TrackApplicationResponse` no trae artista ni color.
## Estructura de rutas ## Route structure
Rutas agrupadas para separar lo público de lo autenticado (los grupos no cambian la URL): Routes are grouped to separate the public experience from the authenticated one (the groups don't change the URL):
``` ```
src/routes/ src/routes/
(marketing)/ Público, sin sesión obligatoria (marketing)/ Public, no session required
+page.svelte / Landing ✅ +page.svelte / Landing ✅
(app)/ Experiencia de la app (app)/ The app experience
explore/ /explore Explorar / buscar ✅ explore/ /explore Explore / search ✅
upload/ /upload Subir música 🟡 maqueta upload/ /upload Upload music 🟡 mockup
library/ /library Tu biblioteca ⬜ library/ /library Your library ⬜
playlists/ /playlists Tus playlists ⬜ playlists/ /playlists Your playlists ⬜
[id]/ /playlists/:id Detalle playlist ⬜ [id]/ /playlists/:id Playlist detail ⬜
auth auth/login auth/logout auth/callback Auth (página + endpoints) ✅ auth auth/login auth/logout auth/callback Auth (page + endpoints) ✅
api/tracks/[id]/stream Proxy de stream ✅ api/tracks/[id]/stream Stream proxy ✅
``` ```
Componentes globales (en todas las páginas): **Navbar**, **Footer** y **PlayerBar** Global components (present on every page): **Navbar**, **Footer**, and **PlayerBar** (the fixed playback bar, shown while something is playing).
(barra de reproducción fija, aparece al reproducir).
--- ---
## 1. Landing `/` ✅ (marketing) ## 1. Landing `/` ✅ (marketing)
Página de entrada para no autenticados. Vender el producto y llevar a registro/explorar. The entry page for logged out visitors. Its job is to sell the product and drive people to sign up or explore.
- **Hero**: badge de estado (beta), título, subtítulo, CTAs primario (Empezar gratis → - **Hero**: a status badge (beta), a title, a subtitle, a primary CTA (Start for free, leading to sign up) and a secondary one (Explore songs).
registro) y secundario (Explorar canciones). - **Features**: three cards covering listening, playlists, and uploading.
- **Features**: 3 cards (escuchar, playlists, subir). - **Suggested next steps**: a screenshots or demo section, social proof, a short FAQ.
- **Pendiente sugerido**: sección de capturas/demo, prueba social, FAQ corta.
## 2. Login / Auth ✅ (endpoints) ## 2. Login / Auth ✅ (endpoints)
`/auth` es la pantalla de login de Musify; sus botones llevan a `/auth/login`, que inicia `/auth` is Musify's login screen; its buttons lead to `/auth/login`, which kicks off the Zitadel OIDC flow and redirects. `?mode=register` starts on the sign up variant, and `?returnTo=` sends the user back to the route they came from once they're in. `/auth/logout` ends the session. `/auth/callback` exchanges the code and creates the encrypted `mf_session` cookie. The credentials UI itself is served by Zitadel, not by Musify.
el flujo OIDC de Zitadel y redirige. `?mode=register` arranca en registro; `?returnTo=`
vuelve a la ruta pedida tras entrar. `/auth/logout` cierra sesión. `/auth/callback`
intercambia el código y crea la cookie cifrada `mf_session`. La UI de credenciales la
sirve Zitadel, no Musify.
## 3. Explorar / Buscar `/explore` ✅ (app) ## 3. Explore / Search `/explore` ✅ (app)
Descubrimiento y búsqueda de canciones del catálogo. Discovery and search over the catalog.
- **Buscador**: input de texto (`?q=`) que filtra por nombre en el backend. - **Search box**: a text input (`?q=`) that filters by name on the backend.
- **Contador de resultados** y **grid de tracks** (2→4 columnas): portada, título, fecha, - **Results count** and a **track grid** (2 to 4 columns): cover, title, date, and a play/pause button on hover that queues and plays through the global player.
botón play/pausa en hover que encola y reproduce vía el player global. - **Pagination**: previous/next (`?page=`, 24 per page).
- **Paginación** anterior/siguiente (`?page=`, 24 por página). - **Empty state**: no results, or an empty catalog.
- **Estado vacío**: sin resultados / catálogo vacío. - **Suggested next steps**: filters (most recent, by user), loading skeletons, a real track cover (the listing API doesn't currently return the image key).
- **Pendiente sugerido**: filtros (más recientes, por usuario), skeletons de carga,
portada real del track (hoy la API de listado no devuelve la key de imagen).
## 4. Subir música `/upload` 🟡 maqueta (app) ## 4. Upload music `/upload` 🟡 mockup (app)
Alta de una canción nueva. Flujo real de backend (para cuando se cablee): Adding a new song. The real backend flow, for when it gets wired up:
1. `POST /tracks/upload-urls` → URLs presignadas para portada y audio + `intentId`s. 1. `POST /tracks/upload-urls` returns presigned URLs for cover and audio, plus `intentId`s.
2. `PUT` de cada archivo a su URL presignada (subida directa al almacenamiento). 2. `PUT` each file to its presigned URL (a direct upload to storage).
3. `POST /tracks` con `title`, `pictureIntentId`, `audioIntentId`. 3. `POST /tracks` with `title`, `pictureIntentId`, `audioIntentId`.
4. El backend procesa el audio (transcodifica a DASH) de forma asíncrona. 4. The backend processes the audio asynchronously (transcoding it to DASH).
Contenido de la página: What the page needs:
- **Zona de audio**: drag & drop o selector; muestra nombre y tamaño del archivo, validación - **Audio zone**: drag and drop or a file picker, showing the file's name and size, with type (audio) and size validation.
de tipo (audio) y tamaño. - **Cover**: an image picker with a square preview.
- **Portada**: selector de imagen con vista previa cuadrada. - **Metadata**: title (required), with room for future fields like a description.
- **Metadatos**: título (obligatorio). Espacio para futuros campos (descripción, etc.). - **Visible pipeline**: the Upload, Process, Publish steps and their status.
- **Pipeline visible**: pasos Subir → Procesar → Publicar con su estado. - **Progress bar** for the upload and the processing state.
- **Barra de progreso** de subida y estado de procesado. - **States**: empty, file selected, uploading, processing, published, error.
- **Estados**: vacío, archivo seleccionado, subiendo, procesando, publicado, error. - **Requires a session** (once wired up: redirect to `/auth?returnTo=/upload`).
- **Requiere sesión** (al cablear: redirigir a `/auth?returnTo=/upload`).
## 5. Tu biblioteca `/library` ⬜ (app) ## 5. Your library `/library` ⬜ (app)
Panel personal del usuario autenticado: lo suyo en un sitio. The authenticated user's personal panel, everything of theirs in one place.
- **Tabs / secciones**: «Tus canciones» (`GET /tracks/users/{userId}`) y «Tus playlists» - **Tabs / sections**: "Your songs" (`GET /tracks/users/{userId}`) and "Your playlists" (`GET /playlists/users/{userId}`).
(`GET /playlists/users/{userId}`). - **Your songs**: a list with play, plus management actions (delete via `DELETE /tracks/{id}`, add to a playlist).
- **Tus canciones**: lista con play, y acciones de gestión (borrar `DELETE /tracks/{id}`, - **Your playlists**: a grid of covers with a "Create playlist" button.
añadir a playlist). - **Empty state** with a CTA to upload or create.
- **Tus playlists**: grid de portadas + botón «Crear playlist». - **Requires a session.**
- **Estado vacío** con CTA a subir / crear.
- **Requiere sesión**.
## 6. Playlists `/playlists` ⬜ (app) ## 6. Playlists `/playlists` ⬜ (app)
Listado de las playlists del usuario. A listing of the user's playlists.
- **Grid** de playlists (portada, nombre, nº de tracks) → enlaza al detalle. - **Grid** of playlists (cover, name, track count), linking to the detail page.
- **Crear playlist**: modal/página con nombre, descripción y portada - **Create playlist**: a modal or page with a name, description, and cover (`POST /playlists`, cover via `POST /playlists/upload-picture`).
(`POST /playlists`, portada vía `POST /playlists/upload-picture`). - **Empty state** with a CTA.
- **Estado vacío** con CTA. - **Requires a session.**
- **Requiere sesión**.
## 7. Detalle de playlist `/playlists/[id]` ⬜ (app) ## 7. Playlist detail `/playlists/[id]` ⬜ (app)
- **Cabecera**: portada grande, nombre, descripción, nº de tracks, botón «Reproducir todo». - **Header**: a large cover, name, description, track count, and a "Play all" button.
- **Lista de tracks** (`GET /playlists/{id}/tracks`, paginada): índice, título, fecha, - **Track list** (`GET /playlists/{id}/tracks`, paginated): index, title, date, play, and remove from the playlist (`DELETE /playlists/{id}/tracks/{trackId}`).
play, quitar de la playlist (`DELETE /playlists/{id}/tracks/{trackId}`). - **Owner actions**: edit (`PUT`), delete (`DELETE`) the playlist, reorder (a future addition).
- **Acciones del dueño**: editar (`PUT`), borrar (`DELETE`) la playlist, reordenar (futuro). - **Add tracks**: from explore or the library (`POST /playlists/{id}/tracks/{trackId}`).
- **Añadir tracks**: desde explorar/biblioteca (`POST /playlists/{id}/tracks/{trackId}`).
## 8. Perfil de usuario `/user/[id]` ⬜ (futuro) ## 8. User profile `/user/[id]` ⬜ (future)
Vista pública de un usuario: avatar, nombre, sus canciones y playlists públicas A public view of a user: avatar, name, their public songs and playlists (`GET /users/{id}`, `GET /tracks/users/{id}`, `GET /playlists/users/{id}`).
(`GET /users/{id}`, `GET /tracks/users/{id}`, `GET /playlists/users/{id}`).
--- ---
## Player global (PlayerBar) ✅ ## Global player (PlayerBar) ✅
Barra fija inferior visible al reproducir. Info de la pista, transporte A fixed bottom bar, visible while something is playing. Shows track info, transport controls (previous, play/pause, next), a progress bar, and volume control. Queues lists from any grid (`player.playQueue`). Plays DASH through the `/api/tracks/:id/stream` proxy, which requests a manifest and a signed ticket from the backend.
(anterior/play-pausa/siguiente), barra de progreso y control de volumen. Encola listas
desde cualquier grid (`player.playQueue`). Reproduce DASH vía el proxy
`/api/tracks/:id/stream`, que pide manifiesto + ticket firmado al backend.
## Backend disponible (resumen) ## Available backend (summary)
| Recurso | Endpoints | | Resource | Endpoints |
| --------- | ----------------------------------------------------------------------- | | --------- | --------------------------------------------------------------------- |
| Tracks | listar/buscar, detalle, por usuario, stream, crear, borrar, upload-urls | | Tracks | list/search, detail, by user, stream, create, delete, upload-urls |
| Playlists | CRUD, tracks de la playlist, add/remove track, upload-picture | | Playlists | CRUD, playlist tracks, add/remove track, upload-picture |
| Users | listar/buscar, detalle, crear | | Users | list/search, detail, create |
Cliente tipado en `$lib/server/api.ts` (`openapi-fetch`, estilo ErrorOr). Consumir en Typed client in `$lib/server/api.ts` (`openapi-fetch`, ErrorOr style). Use it inside `load`/server actions, never from the client. Types live in `src/lib/api/schema.d.ts`.
`load`/actions server, nunca desde cliente. Tipos en `src/lib/api/schema.d.ts`.