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
130 lines
4.3 KiB
Svelte
130 lines
4.3 KiB
Svelte
<script lang="ts">
|
|
import { page } from '$app/state';
|
|
import { isSectionActive } from '$lib/navigation.svelte';
|
|
import { liked } from '$lib/player/liked.svelte';
|
|
import { createPlaylistModal } from '$lib/playlists.svelte';
|
|
import type { SessionUser } from '$lib/types';
|
|
import PlaylistArt from './ui/PlaylistArt.svelte';
|
|
import Home from '@lucide/svelte/icons/house';
|
|
import Compass from '@lucide/svelte/icons/compass';
|
|
import ListMusic from '@lucide/svelte/icons/list-music';
|
|
import Heart from '@lucide/svelte/icons/heart';
|
|
import Disc from '@lucide/svelte/icons/disc-2';
|
|
import Folder from '@lucide/svelte/icons/folder';
|
|
import Upload from '@lucide/svelte/icons/upload';
|
|
import Plus from '@lucide/svelte/icons/plus';
|
|
|
|
interface SidebarPlaylist {
|
|
id: string;
|
|
name: string;
|
|
coverTrackIds: (string | number)[];
|
|
updatedAt: string;
|
|
}
|
|
|
|
interface Props {
|
|
user: SessionUser | null;
|
|
playlists: SidebarPlaylist[];
|
|
}
|
|
|
|
let { user, playlists }: Props = $props();
|
|
|
|
const navLinks = $derived(
|
|
user
|
|
? [
|
|
{ href: '/', label: 'Inicio', icon: Home },
|
|
{ href: '/explore', label: 'Descubrir', icon: Compass },
|
|
{ href: '/playlists', label: 'Playlists', icon: ListMusic },
|
|
{ href: '/liked', label: 'Me gusta', icon: Heart, count: liked.count },
|
|
{ href: '/albums', label: 'Mis álbumes', icon: Disc },
|
|
{ href: '/library', label: 'Canciones subidas', icon: Folder },
|
|
{ href: '/upload', label: 'Subir música', icon: Upload }
|
|
]
|
|
: [{ href: '/explore', label: 'Descubrir', icon: Compass }]
|
|
);
|
|
|
|
function isActive(href: string) {
|
|
return isSectionActive(href, page.url.pathname, page.data.section);
|
|
}
|
|
</script>
|
|
|
|
<aside
|
|
class="hidden shrink-0 flex-col border-r border-hairline bg-[image:var(--mf-sidebar-bg)] pt-5.5 pr-3 pl-5 transition-[background] duration-[600ms] ease-out lg:flex"
|
|
style="width:var(--mf-sidebar-w)"
|
|
>
|
|
<a href="/" class="flex items-center gap-2.5 px-1.5 pb-6.5">
|
|
<span class="font-display text-[17.5px] font-semibold tracking-[-0.02em] text-fg">Musify</span>
|
|
</a>
|
|
|
|
<nav class="flex flex-col gap-0.5">
|
|
{#each navLinks as link (link.href)}
|
|
{@const active = isActive(link.href)}
|
|
<a
|
|
href={link.href}
|
|
aria-current={active ? 'page' : undefined}
|
|
class="flex items-center gap-3 rounded-[10px] px-3 py-2.25 text-[13.5px] font-medium transition-colors duration-150 {active
|
|
? 'bg-surface-hover text-fg'
|
|
: 'text-fg-2 hover:bg-hover hover:text-fg'}"
|
|
>
|
|
<link.icon class="h-[17px] w-[17px] shrink-0" strokeWidth={1.6} />
|
|
<span class="flex-1 truncate">{link.label}</span>
|
|
{#if link.count}
|
|
<span class="text-[11px] text-muted tabular-nums">{link.count}</span>
|
|
{/if}
|
|
</a>
|
|
{/each}
|
|
</nav>
|
|
|
|
{#if user}
|
|
<div class="mx-3 mt-6 mb-3.5 h-px bg-white/5"></div>
|
|
<div class="flex items-center justify-between px-3 pb-1.5">
|
|
<span class="text-[10.5px] font-semibold tracking-[0.13em] text-muted uppercase"
|
|
>Tus playlists</span
|
|
>
|
|
{#if playlists.length > 0}
|
|
<button
|
|
type="button"
|
|
onclick={() => createPlaylistModal.show()}
|
|
aria-label="Crear playlist"
|
|
class="text-base text-muted transition-colors hover:text-fg"
|
|
>
|
|
+
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
|
|
<div class="flex flex-1 flex-col gap-px overflow-y-auto pb-4.5">
|
|
{#each playlists as playlist (playlist.id)}
|
|
{@const active = page.url.pathname === `/playlists/${playlist.id}`}
|
|
<a
|
|
href="/playlists/{playlist.id}"
|
|
class="flex items-center gap-2.75 rounded-[10px] px-3 py-1.75 transition-colors duration-150 {active
|
|
? 'bg-surface-hover'
|
|
: 'hover:bg-hover'}"
|
|
>
|
|
<PlaylistArt
|
|
playlistId={playlist.id}
|
|
trackIds={playlist.coverTrackIds}
|
|
version={playlist.updatedAt}
|
|
size="small"
|
|
class="h-8.5 w-8.5 shrink-0 overflow-hidden rounded-lg opacity-90"
|
|
/>
|
|
<div class="min-w-0 flex-1">
|
|
<div class="truncate text-[13.5px] font-medium {active ? 'text-fg' : 'text-fg-2'}">
|
|
{playlist.name}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
{:else}
|
|
<button
|
|
type="button"
|
|
onclick={() => createPlaylistModal.show()}
|
|
class="flex items-center gap-2 rounded-[10px] px-3 py-2 text-[12px] text-muted transition-colors hover:bg-hover hover:text-fg-2"
|
|
>
|
|
<Plus class="h-3.5 w-3.5" strokeWidth={1.8} />
|
|
Crear tu primera lista
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</aside>
|