diff --git a/web-player/CLAUDE.md b/web-player/CLAUDE.md index 9a4dd35..a61cf1b 100644 --- a/web-player/CLAUDE.md +++ b/web-player/CLAUDE.md @@ -33,7 +33,7 @@ npm run lint:tokens # scripts/check-tokens.mjs, the design system guardrail ## Folder structure -- `src/lib/types.ts` holds the domain types (`Track`, `Album`, `Playlist`, `Mix`, `Paged`). Pages and the player only ever see these, never raw API DTOs. +- `src/lib/types.ts` holds the domain types (`Track`, `Album`, `Playlist`, `Mix`, `ListeningStats`, `Paged`). Pages and the player only ever see these, never raw API DTOs. - `src/lib/player/` is the player: `player.svelte.ts` (state and orchestration), `queue.ts` (pure queue operations), `stream.ts`, `mediaSession.ts`, `progressClock.ts`, `listenTracker.ts`, `recentlyPlayed.ts` (merges `player.recentlyPlayed` with the server history), and `actions.ts` (`playAllOrToggle`, `playShuffled`, `isQueueCurrent`). - `src/lib/state/` holds reactive rune state: `panels.svelte.ts` (queue panel, create playlist modal), `history.svelte.ts` (previous page for `BackLink`), `menu.svelte.ts` (`createMenu()`), `pagedList.svelte.ts` (`createPagedList`), `dialog.svelte.ts`, `liked.svelte.ts` (the user's liked tracks), `toggle.svelte.ts` (`createToggle`), and `scroll.ts` (`restoreScroll`). Nothing in `state/` imports from `components/`. - `src/lib/theme/` holds the theme: `accent.svelte.ts` (accent hue per track and the hue fade), `mode.svelte.ts`, `palette.ts`, `tokens.ts`, and `color.ts` (`ACCENT_HUE`). The player never imports from `theme/`: `(app)/+layout.svelte` follows `player.currentId` and calls `accent.follow`. @@ -51,7 +51,7 @@ npm run lint:tokens # scripts/check-tokens.mjs, the design system guardrail - `media/` for stateless music display components that receive props (Artwork, MediaCard, ListRow, PlayButton, and so on). - `forms/` for composite entity forms (AlbumForm, PlaylistForm, CoverForm, ImageDropzone). - `layout/` for page level scaffolding (Page, PageHeader, SectionHeading). -- The root `+layout.svelte` only holds the globals (fonts, theme, `DialogHost`). Everything the user sees after signing in lives under `src/routes/(app)/`, including Home (`(app)/+page.svelte`); `(app)/+layout.server.ts` guards the group and loads the shell data, and `(app)/+error.svelte` renders inside the shell. 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]/`, and its followers/following lists share one route, `user/[id]/[list=followList]/` (matcher in `src/params/followList.ts`). +- The root `+layout.svelte` only holds the globals (fonts, theme, `DialogHost`). Everything the user sees after signing in lives under `src/routes/(app)/`, including Home (`(app)/+page.svelte`); `(app)/+layout.server.ts` guards the group and loads the shell data, and `(app)/+error.svelte` renders inside the shell. Unknown URLs hit the catch-all `(app)/[...rest]/` (it throws a 404) so they also get the shell (unknown `/api/...` URLs hit `api/[...rest]/`, a JSON 404); the root `+error.svelte` only covers errors outside the group or in the group layout load. 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]/`, and its followers/following lists share one route, `user/[id]/[list=followList]/` (matcher in `src/params/followList.ts`). ## Naming conventions and glossary diff --git a/web-player/src/lib/server/mappers.ts b/web-player/src/lib/server/mappers.ts index b708925..d962197 100644 --- a/web-player/src/lib/server/mappers.ts +++ b/web-player/src/lib/server/mappers.ts @@ -1,5 +1,5 @@ import type { components } from '$lib/api/schema'; -import type { Album, LikedTrack, Mix, Paged, Playlist, Track } from '$lib/types'; +import type { Album, LikedTrack, ListeningStats, Mix, Paged, Playlist, Track } from '$lib/types'; type TrackDto = components['schemas']['TrackApplicationResponse']; type AlbumDto = components['schemas']['AlbumApplicationResponse']; @@ -84,12 +84,6 @@ export function toPlaylist(dto: PlaylistDto): Playlist { type ListeningStatsDto = components['schemas']['ListeningStatsResponse']; -export interface ListeningStats { - tracksThisWeek: number; - secondsThisWeek: number; - streakDays: number; -} - export function toCount(value: number | string | null | undefined): number { return Number(value ?? 0) || 0; } diff --git a/web-player/src/lib/types.ts b/web-player/src/lib/types.ts index fe43b2c..5eedb54 100644 --- a/web-player/src/lib/types.ts +++ b/web-player/src/lib/types.ts @@ -51,6 +51,12 @@ export interface Mix { tracks: Track[]; } +export interface ListeningStats { + tracksThisWeek: number; + secondsThisWeek: number; + streakDays: number; +} + export interface Paged { items: T[]; pageNumber: number; diff --git a/web-player/src/routes/(app)/[...rest]/+page.server.ts b/web-player/src/routes/(app)/[...rest]/+page.server.ts new file mode 100644 index 0000000..7e9c254 --- /dev/null +++ b/web-player/src/routes/(app)/[...rest]/+page.server.ts @@ -0,0 +1,6 @@ +import { error } from '@sveltejs/kit'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = () => { + error(404, 'Página no encontrada'); +}; diff --git a/web-player/src/routes/+error.svelte b/web-player/src/routes/+error.svelte index b068217..c424502 100644 --- a/web-player/src/routes/+error.svelte +++ b/web-player/src/routes/+error.svelte @@ -1,12 +1,18 @@ -
-
+
+

{page.status === 404 ? 'Página no encontrada' : 'Algo salió mal'}

- Volver al inicio +

+ {page.status === 404 + ? 'Comprueba el enlace o vuelve al inicio.' + : 'No se pudo cargar la página. Inténtalo de nuevo en unos momentos.'} +

+
diff --git a/web-player/src/routes/api/[...rest]/+server.ts b/web-player/src/routes/api/[...rest]/+server.ts new file mode 100644 index 0000000..71b324b --- /dev/null +++ b/web-player/src/routes/api/[...rest]/+server.ts @@ -0,0 +1,4 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const fallback: RequestHandler = () => json({ message: 'No encontrado.' }, { status: 404 });