Add likes, follows and playlist visibility; redesign web-player UI

Backend:
- New Likes feature (toggle/get liked tracks) and endpoints
- User follow/unfollow, listening stats, public user profile
- Playlist visibility (public/private) support
- Various auth and DTO/response adjustments to support the above

Frontend (web-player):
- New design system pass: theme tokens/mode (light/dark), glass menu,
  tabs bar, player dock/extras/queue components, mix tiles, artist link
- New routes: Liked, public user profile (/u/[id]), likes API proxy
- Search refactor: shared SearchBox component used by TopBar and
  MobileHeader (search now reachable on mobile), live results as you
  type instead of requiring Enter, dropped the "Buscando…" quick-result
  preview
- Removed now-unused components (MobileNav, CoverBadge, MixBentoTile,
  PlayPauseOverlay, SuggestionsTable, userDirectory)
- Design handoff reference docs added under web-player/design_handoff_musify_player
  and web-player/docs/design-system-refactor-plan.md
This commit is contained in:
2026-09-14 23:42:45 +02:00
parent 389b3b9244
commit dc1ba89a6d
166 changed files with 14271 additions and 1982 deletions
+104 -21
View File
@@ -1,24 +1,107 @@
const GREETINGS_BY_HOUR: { until: number; pool: string[] }[] = [
{
until: 6,
pool: ['¿Aún despierto?', 'La noche también tiene banda sonora', 'Un ratito más, con música']
},
{
until: 12,
pool: ['Buenos días', 'Que empiece bien la mañana', 'Un nuevo día, una nueva lista']
},
{ until: 14, pool: ['Buenos días', 'El mediodía sabe mejor con música'] },
{
until: 19,
pool: ['Buenas tardes', 'La tarde pide su banda sonora', 'Un respiro, con algo de música']
},
{ until: 23, pool: ['Buenas noches', 'Hora de bajar el ritmo', 'Que la noche te acompañe'] },
{ until: 24, pool: ['¿Aún despierto?', 'La noche también tiene banda sonora'] }
interface SpecialDate {
month: number;
day: number;
message: string;
}
const SPECIAL_DATES: SpecialDate[] = [
{ month: 1, day: 1, message: 'Nuevo año, nueva música.' },
{ month: 2, day: 14, message: 'Hoy también hay canciones para eso.' },
{ month: 10, day: 31, message: 'Esta noche suena diferente.' },
{ month: 12, day: 24, message: 'Nochebuena. Que no pare la música.' },
{ month: 12, day: 25, message: 'Feliz Navidad. Pon tu banda sonora.' },
{ month: 12, day: 31, message: 'Último día del año. Haz que suene bien.' }
];
export function pickGreeting(hour: number): string {
const pool =
GREETINGS_BY_HOUR.find((slot) => hour < slot.until)?.pool ?? GREETINGS_BY_HOUR[0].pool;
const greeting = pool[Math.floor(Math.random() * pool.length)];
return /[.!?]$/.test(greeting) ? greeting : `${greeting}.`;
const BIRTHDAY_MESSAGE = 'Hoy toca celebrar. Tú eliges la música.';
const HOUR_SLOTS = [6, 9, 12, 15, 18, 21, 24];
const WEEKLY_SCHEDULE: string[][] = [
[
'Domingo de madrugada.',
'Buenos días. Tómate el domingo con calma.',
'Domingo. Empieza despacio.',
'Una mañana tranquila merece buena música.',
'Domingo por la tarde. Hora de bajar el ritmo.',
'La semana empieza a despedirse.',
'Una última canción antes de volver a empezar.'
],
[
'La semana acaba de empezar.',
'Buenos días. Empezamos otra semana.',
'Lunes. Dale una banda sonora al día.',
'Una pausa musical para este lunes.',
'El lunes todavía no ha terminado.',
'El lunes ya empieza a quedar atrás.',
'Sobrevive al lunes con una última canción.'
],
[
'La noche sigue sonando.',
'Buenos días. Martes en marcha.',
'La semana ya está cogiendo ritmo.',
'Martes al mediodía. ¿Qué escuchamos?',
'Una tarde cualquiera necesita música.',
'El día empieza a bajar el ritmo.',
'Un martes más llega a su fin.'
],
[
'Mitad de semana, mitad de noche.',
'Buenos días. Ya es miércoles.',
'Ya estamos en el ecuador de la semana.',
'Miércoles al mediodía. Un poco de música.',
'La semana ya va cuesta abajo.',
'Unas canciones para terminar el miércoles.',
'Mitad de semana superada.'
],
[
'Una noche más antes del fin de semana.',
'Buenos días. Ya casi estamos.',
'Jueves. El fin de semana se acerca.',
'Una pausa musical antes de seguir.',
'Queda poco para desconectar.',
'Ya se empieza a notar el viernes.',
'Una noche más y llega el fin de semana.'
],
[
'La semana ha terminado. La noche continúa.',
'Buenos días. Es viernes.',
'Viernes. Hoy suena diferente.',
'Ya huele a fin de semana.',
'Últimas horas de la semana.',
'Se acabó la semana. Dale al play.',
'Viernes por la noche. Tú eliges la banda sonora.'
],
[
'La noche es joven. Que siga sonando.',
'Buenos días. Hoy no hay prisa.',
'Sábado. Empieza el día a tu ritmo.',
'Mediodía de sábado. ¿Qué ponemos?',
'Una tarde de sábado pide música.',
'El sábado todavía tiene mucho por delante.',
'Esta noche puede sonar cualquier cosa.'
]
];
export interface Birthday {
month: number;
day: number;
}
/**
* `birthday` is optional because the user's date of birth isn't part of the
* session/profile data yet. Wire it up here once that field exists upstream.
*/
export function pickGreeting(date: Date, birthday?: Birthday | null): string {
const month = date.getMonth() + 1;
const day = date.getDate();
if (birthday && birthday.month === month && birthday.day === day) return BIRTHDAY_MESSAGE;
const special = SPECIAL_DATES.find((d) => d.month === month && d.day === day);
if (special) return special.message;
const hour = date.getHours();
const slot = HOUR_SLOTS.findIndex((until) => hour < until);
return WEEKLY_SCHEDULE[date.getDay()][slot === -1 ? 0 : slot];
}
@@ -1,24 +0,0 @@
export interface DirectoryUser {
id: string;
name: string;
handle: string;
}
const DIRECTORY: DirectoryUser[] = [
{ id: 'u1', name: 'Alex Rivera', handle: 'alexrivera' },
{ id: 'u2', name: 'Marina Cordero', handle: 'marinac' },
{ id: 'u3', name: 'Diego Fuentes', handle: 'dfuentes' },
{ id: 'u4', name: 'Lucía Marés', handle: 'luciamares' },
{ id: 'u5', name: 'Noah Bennett', handle: 'noahb' },
{ id: 'u6', name: 'Sara Ibáñez', handle: 'saraib' },
{ id: 'u7', name: 'Kairo', handle: 'kairo' },
{ id: 'u8', name: 'Valentina Ortiz', handle: 'valen' }
];
export function searchUsers(query: string, limit = 12): DirectoryUser[] {
const term = query.trim().toLowerCase();
if (!term) return [];
return DIRECTORY.filter(
(user) => user.name.toLowerCase().includes(term) || user.handle.toLowerCase().includes(term)
).slice(0, limit);
}
+1 -1
View File
@@ -1,7 +1,7 @@
import type { createApiClient } from '$lib/server/api';
import type { YouTubeAlbumResult, YouTubeSong } from '$lib/types';
const YOUTUBE_FILLER_QUERIES = [
export const YOUTUBE_FILLER_QUERIES = [
'pop',
'rock',
'reggaeton',