Design Prot Merge #1
@@ -69,7 +69,7 @@
|
|||||||
? 'text-fg'
|
? 'text-fg'
|
||||||
: 'text-fg-3 hover:bg-hover hover:text-fg'}"
|
: 'text-fg-3 hover:bg-hover hover:text-fg'}"
|
||||||
>
|
>
|
||||||
<link.icon class="h-6 w-6 shrink-0" style={active ? 'color:var(--mf-accent-60)' : ''} />
|
<link.icon class="h-6 w-6 shrink-0 {active ? 'text-accent-soft' : ''}" />
|
||||||
{link.label}
|
{link.label}
|
||||||
</a>
|
</a>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@@ -87,10 +87,7 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
style="top:{pillTop}px; height:{pillHeight}px; opacity:{pillVisible ? 1 : 0}"
|
style="top:{pillTop}px; height:{pillHeight}px; opacity:{pillVisible ? 1 : 0}"
|
||||||
>
|
>
|
||||||
<span
|
<span class="absolute inset-y-0 left-0 w-[3px] rounded-r-full bg-accent-soft"></span>
|
||||||
class="absolute inset-y-0 left-0 w-[3px] rounded-r-full"
|
|
||||||
style="background:var(--mf-accent-60)"
|
|
||||||
></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="relative z-10 flex flex-col gap-1">
|
<nav class="relative z-10 flex flex-col gap-1">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { fmtTime } from '$lib/theme/color';
|
import { fmtTime } from '$lib/format';
|
||||||
|
|
||||||
function seek(event: MouseEvent) {
|
function seek(event: MouseEvent) {
|
||||||
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
@@ -23,11 +23,11 @@
|
|||||||
style="width:{player.progressPercent}%"
|
style="width:{player.progressPercent}%"
|
||||||
></div>
|
></div>
|
||||||
<span
|
<span
|
||||||
class="pointer-events-none absolute top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 shadow-[0_1px_4px_rgba(0,0,0,0.5)] transition-opacity duration-75 group-hover:opacity-100"
|
class="pointer-events-none absolute top-1/2 h-3 w-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-on-art opacity-0 shadow-[0_1px_4px_rgba(0,0,0,0.5)] transition-opacity duration-75 group-hover:opacity-100"
|
||||||
style="left:{player.progressPercent}%"
|
style="left:{player.progressPercent}%"
|
||||||
></span>
|
></span>
|
||||||
</button>
|
</button>
|
||||||
<span class="text-xs text-(--mf-text-4) tabular-nums">
|
<span class="text-xs text-muted tabular-nums">
|
||||||
{fmtTime(player.current.duration)}
|
{fmtTime(player.current.duration)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
oninput={handleVolumeChange}
|
oninput={handleVolumeChange}
|
||||||
aria-label="Volumen"
|
aria-label="Volumen"
|
||||||
class="mf-volume"
|
class="mf-volume"
|
||||||
style="background:linear-gradient(to right, var(--mf-accent-60) {player.volume}%, var(--mf-track) {player.volume}%)"
|
style="background:linear-gradient(to right, var(--color-accent-soft) {player.volume}%, var(--color-track) {player.volume}%)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -37,14 +37,14 @@
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--mf-text);
|
background: var(--color-fg);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.mf-volume::-moz-range-thumb {
|
.mf-volume::-moz-range-thumb {
|
||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--mf-text);
|
background: var(--color-fg);
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
primary: 'bg-accent-soft font-semibold text-on-accent active:scale-[0.97]',
|
primary: 'bg-accent-soft font-semibold text-on-accent active:scale-[0.97]',
|
||||||
secondary: 'border border-line text-fg hover:bg-hover',
|
secondary: 'border border-line text-fg hover:bg-hover',
|
||||||
subtle: 'bg-surface-2 font-semibold text-fg hover:bg-surface-hover',
|
subtle: 'bg-surface-2 font-semibold text-fg hover:bg-surface-hover',
|
||||||
danger: 'bg-danger font-semibold text-white hover:brightness-110'
|
danger: 'bg-danger font-semibold text-on-accent hover:brightness-110'
|
||||||
};
|
};
|
||||||
|
|
||||||
const classes = $derived(
|
const classes = $derived(
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="animate-fade fixed inset-0 z-[60] grid place-items-center bg-black/70 p-4 backdrop-blur-sm"
|
class="animate-fade fixed inset-0 z-[60] grid place-items-center bg-scrim/70 p-4 backdrop-blur-sm"
|
||||||
role="presentation"
|
role="presentation"
|
||||||
onclick={onBackdropClick}
|
onclick={onBackdropClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute inset-0 flex items-end justify-center gap-[2.5px] bg-black/45 pb-2">
|
<div class="absolute inset-0 flex items-end justify-center gap-[2.5px] bg-scrim/45 pb-2">
|
||||||
{#each bars as bar, i (i)}
|
{#each bars as bar, i (i)}
|
||||||
<span
|
<span
|
||||||
class="w-[3px] origin-bottom rounded-full bg-white"
|
class="w-[3px] origin-bottom rounded-full bg-on-art"
|
||||||
style="height:14px; animation:equalize {bar.dur}s ease-in-out {bar.delay}s infinite; animation-play-state:{paused
|
style="height:14px; animation:equalize {bar.dur}s ease-in-out {bar.delay}s infinite; animation-play-state:{paused
|
||||||
? 'paused'
|
? 'paused'
|
||||||
: 'running'}"
|
: 'running'}"
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export function fmtTime(seconds: number): string {
|
||||||
|
const total = Math.max(0, Math.floor(seconds || 0));
|
||||||
|
const minutes = Math.floor(total / 60);
|
||||||
|
const rest = total % 60;
|
||||||
|
return `${minutes}:${rest < 10 ? '0' : ''}${rest}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateFormatter = new Intl.DateTimeFormat('es', { dateStyle: 'medium' });
|
||||||
|
|
||||||
|
export function fmtDate(value: string | number | Date): string {
|
||||||
|
return dateFormatter.format(new Date(value));
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ export interface ApiTrackLike {
|
|||||||
isExplicit?: boolean;
|
isExplicit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isYouTubeTrack(track: ApiTrackLike): boolean {
|
function isYouTubeTrack(track: ApiTrackLike): boolean {
|
||||||
return track.source === 'YouTube';
|
return track.source === 'YouTube';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { createApiClient } from '$lib/server/api';
|
import type { createApiClient } from '$lib/server/api';
|
||||||
import type { YouTubeSong } from '$lib/types';
|
import type { YouTubeSong } from '$lib/types';
|
||||||
|
|
||||||
export const YOUTUBE_FILLER_QUERIES = [
|
const YOUTUBE_FILLER_QUERIES = [
|
||||||
'pop',
|
'pop',
|
||||||
'rock',
|
'rock',
|
||||||
'reggaeton',
|
'reggaeton',
|
||||||
@@ -24,7 +24,7 @@ export interface YoutubeFiller {
|
|||||||
continuationToken: string;
|
continuationToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function pickRandomFillerQuery(): string {
|
function pickRandomFillerQuery(): string {
|
||||||
return YOUTUBE_FILLER_QUERIES[Math.floor(Math.random() * YOUTUBE_FILLER_QUERIES.length)];
|
return YOUTUBE_FILLER_QUERIES[Math.floor(Math.random() * YOUTUBE_FILLER_QUERIES.length)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,23 +15,6 @@ export function accentForHue(hue: number): string {
|
|||||||
return `oklch(${ACCENT_LIGHTNESS}% ${ACCENT_CHROMA} ${hue})`;
|
return `oklch(${ACCENT_LIGHTNESS}% ${ACCENT_CHROMA} ${hue})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function accentSoft(hue: number, pct: number): string {
|
|
||||||
return `color-mix(in srgb, ${accentForHue(hue)} ${pct}%, transparent)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function gradientForHue(hue: number): string {
|
export function gradientForHue(hue: number): string {
|
||||||
return `linear-gradient(135deg, oklch(70% 0.15 ${hue}), oklch(36% 0.13 ${hue + 28}))`;
|
return `linear-gradient(135deg, oklch(70% 0.15 ${hue}), oklch(36% 0.13 ${hue + 28}))`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fmtTime(seconds: number): string {
|
|
||||||
const total = Math.max(0, Math.floor(seconds || 0));
|
|
||||||
const minutes = Math.floor(total / 60);
|
|
||||||
const rest = total % 60;
|
|
||||||
return `${minutes}:${rest < 10 ? '0' : ''}${rest}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dateFormatter = new Intl.DateTimeFormat('es', { dateStyle: 'medium' });
|
|
||||||
|
|
||||||
export function fmtDate(value: string | number | Date): string {
|
|
||||||
return dateFormatter.format(new Date(value));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@
|
|||||||
--mf-track: rgba(255, 255, 255, 0.12);
|
--mf-track: rgba(255, 255, 255, 0.12);
|
||||||
|
|
||||||
--mf-accent: oklch(60% 0.17 145);
|
--mf-accent: oklch(60% 0.17 145);
|
||||||
--mf-accent-60: color-mix(in oklch, var(--mf-accent) 80%, transparent);
|
--mf-accent-soft: color-mix(in oklch, var(--mf-accent) 80%, transparent);
|
||||||
--mf-radius: 12px;
|
|
||||||
|
--mf-scrim: #000000;
|
||||||
|
--mf-on-art: #ffffff;
|
||||||
|
|
||||||
--mf-sidebar-w: 264px;
|
--mf-sidebar-w: 264px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function targetCoverSrc(target: TrackTarget): string | undefined {
|
|||||||
return target.kind === 'youtube' ? target.song.thumbnailUrl : undefined;
|
return target.kind === 'youtube' ? target.song.thumbnailUrl : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function queueIdForTarget(target: TrackTarget): string | number {
|
function queueIdForTarget(target: TrackTarget): string | number {
|
||||||
return target.kind === 'youtube' ? target.song.videoId : queueIdForTrack(target.track);
|
return target.kind === 'youtube' ? target.song.videoId : queueIdForTrack(target.track);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { createApiClient, unwrapOrError } from '$lib/server/api';
|
import { createApiClient, requireUser, unwrapOrError } from '$lib/server/api';
|
||||||
import { fetchYoutubeFiller } from '$lib/server/youtube';
|
import { fetchYoutubeFiller } from '$lib/server/youtube';
|
||||||
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
|
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
|
||||||
import { EXPLORE_PAGE_SIZE as PAGE_SIZE, YOUTUBE_FILLER_LIMIT } from '$lib/config';
|
import { EXPLORE_PAGE_SIZE as PAGE_SIZE, YOUTUBE_FILLER_LIMIT } from '$lib/config';
|
||||||
@@ -8,22 +8,20 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
|||||||
const query = url.searchParams.get('q')?.trim() ?? '';
|
const query = url.searchParams.get('q')?.trim() ?? '';
|
||||||
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
|
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
|
||||||
|
|
||||||
|
const user = requireUser(locals, url);
|
||||||
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||||
|
|
||||||
const tracksPromise = api.GET('/tracks', {
|
const tracksPromise = api.GET('/tracks', {
|
||||||
params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
|
params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchPromise =
|
const searchPromise = query
|
||||||
query && locals.accessToken
|
|
||||||
? api.GET('/youtube/search', { params: { query: { query } } })
|
? api.GET('/youtube/search', { params: { query: { query } } })
|
||||||
: Promise.resolve(null);
|
: Promise.resolve(null);
|
||||||
|
|
||||||
const playlistsPromise = locals.user
|
const playlistsPromise = api.GET('/playlists/users/{userId}', {
|
||||||
? api.GET('/playlists/users/{userId}', {
|
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
|
||||||
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 50 } }
|
});
|
||||||
})
|
|
||||||
: Promise.resolve(null);
|
|
||||||
|
|
||||||
const [tracksRes, searchRes, playlistsRes] = await Promise.all([
|
const [tracksRes, searchRes, playlistsRes] = await Promise.all([
|
||||||
tracksPromise,
|
tracksPromise,
|
||||||
@@ -41,8 +39,7 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
|||||||
youtubeFiller: query
|
youtubeFiller: query
|
||||||
? Promise.resolve(null)
|
? Promise.resolve(null)
|
||||||
: fetchYoutubeFiller(api, locals.accessToken, YOUTUBE_FILLER_LIMIT),
|
: fetchYoutubeFiller(api, locals.accessToken, YOUTUBE_FILLER_LIMIT),
|
||||||
playlists: playlistsRes?.data?.items ?? [],
|
playlists: playlistsRes?.data?.items ?? []
|
||||||
needsAuth: !locals.user
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -84,9 +84,7 @@
|
|||||||
|
|
||||||
const hasMore = $derived(localHasNext || ytContinuation !== '');
|
const hasMore = $derived(localHasNext || ytContinuation !== '');
|
||||||
|
|
||||||
const nothingFound = $derived(
|
const nothingFound = $derived(!!data.query && items.length === 0 && !data.ytError);
|
||||||
!!data.query && items.length === 0 && !data.needsAuth && !data.ytError
|
|
||||||
);
|
|
||||||
|
|
||||||
function buildHref(query: string) {
|
function buildHref(query: string) {
|
||||||
return query ? `/explore?q=${encodeURIComponent(query)}` : '/explore';
|
return query ? `/explore?q=${encodeURIComponent(query)}` : '/explore';
|
||||||
@@ -212,12 +210,7 @@
|
|||||||
<InfiniteScroll onLoadMore={loadMore} {hasMore} loading={loadingMore} />
|
<InfiniteScroll onLoadMore={loadMore} {hasMore} loading={loadingMore} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if data.needsAuth}
|
{#if data.ytError}
|
||||||
<EmptyState
|
|
||||||
icon={Music}
|
|
||||||
description="Inicia sesión para ver también resultados de YouTube Music."
|
|
||||||
/>
|
|
||||||
{:else if data.ytError}
|
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Music}
|
icon={Music}
|
||||||
description="YouTube Music no está disponible ahora mismo. Inténtalo de nuevo."
|
description="YouTube Music no está disponible ahora mismo. Inténtalo de nuevo."
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import Music from '@lucide/svelte/icons/music';
|
import Music from '@lucide/svelte/icons/music';
|
||||||
import { player, toQueueItems } from '$lib/player/player.svelte';
|
import { player, toQueueItems } from '$lib/player/player.svelte';
|
||||||
import { hueFor, fmtTime, fmtDate } from '$lib/theme/color';
|
import { hueFor } from '$lib/theme/color';
|
||||||
|
import { fmtTime, fmtDate } from '$lib/format';
|
||||||
import Page from '$lib/components/ui/Page.svelte';
|
import Page from '$lib/components/ui/Page.svelte';
|
||||||
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
||||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
youTubeThumbnailUrl,
|
youTubeThumbnailUrl,
|
||||||
type ApiTrackLike
|
type ApiTrackLike
|
||||||
} from '$lib/player/player.svelte';
|
} from '$lib/player/player.svelte';
|
||||||
import { hueFor, fmtTime, fmtDate } from '$lib/theme/color';
|
import { hueFor } from '$lib/theme/color';
|
||||||
|
import { fmtTime, fmtDate } from '$lib/format';
|
||||||
import Badge from '$lib/components/ui/Badge.svelte';
|
import Badge from '$lib/components/ui/Badge.svelte';
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
type ApiTrackLike,
|
type ApiTrackLike,
|
||||||
type QueueItem
|
type QueueItem
|
||||||
} from '$lib/player/player.svelte';
|
} from '$lib/player/player.svelte';
|
||||||
import { hueFor, fmtTime } from '$lib/theme/color';
|
import { hueFor } from '$lib/theme/color';
|
||||||
|
import { fmtTime } from '$lib/format';
|
||||||
import type { YouTubeSong } from '$lib/types';
|
import type { YouTubeSong } from '$lib/types';
|
||||||
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
import IconButton from '$lib/components/ui/IconButton.svelte';
|
||||||
|
|||||||
@@ -52,18 +52,19 @@
|
|||||||
if (t < 1) rafId = requestAnimationFrame(tick);
|
if (t < 1) rafId = requestAnimationFrame(tick);
|
||||||
};
|
};
|
||||||
rafId = requestAnimationFrame(tick);
|
rafId = requestAnimationFrame(tick);
|
||||||
|
return () => cancelAnimationFrame(rafId);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
<svelte:head><link rel="icon" href={favicon} /></svelte:head>
|
||||||
|
|
||||||
{#if isAuthPage}
|
{#if isAuthPage || !data.user}
|
||||||
<div class="min-h-screen bg-bg text-fg antialiased">
|
<div class="min-h-screen bg-bg text-fg antialiased">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex min-h-screen bg-bg text-fg antialiased">
|
<div class="flex min-h-screen bg-bg text-fg antialiased">
|
||||||
<Sidebar user={data.user!} />
|
<Sidebar user={data.user} />
|
||||||
<div class="flex min-w-0 flex-1 flex-col">
|
<div class="flex min-w-0 flex-1 flex-col">
|
||||||
<main class="flex-1 transition-[padding] {hasTrack ? 'pb-[92px]' : 'pb-0'}">
|
<main class="flex-1 transition-[padding] {hasTrack ? 'pb-[92px]' : 'pb-0'}">
|
||||||
{#key page.url.pathname}
|
{#key page.url.pathname}
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { createApiClient, playlistCoverTrackIds } from '$lib/server/api';
|
import { createApiClient, playlistCoverTrackIds, requireUser } from '$lib/server/api';
|
||||||
import { fetchYoutubeFiller, shuffle } from '$lib/server/youtube';
|
import { fetchYoutubeFiller, shuffle } from '$lib/server/youtube';
|
||||||
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
|
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
|
||||||
import { HOME_LATEST_PAGE_SIZE, YOUTUBE_FILLER_LIMIT } from '$lib/config';
|
import { HOME_LATEST_PAGE_SIZE, YOUTUBE_FILLER_LIMIT } from '$lib/config';
|
||||||
import { pickGreeting } from '$lib/server/greeting';
|
import { pickGreeting } from '$lib/server/greeting';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals, fetch }) => {
|
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
||||||
|
const user = requireUser(locals, url);
|
||||||
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
|
||||||
|
|
||||||
const latestPromise = api.GET('/tracks', {
|
const latestPromise = api.GET('/tracks', {
|
||||||
params: { query: { pageNumber: 1, pageSize: HOME_LATEST_PAGE_SIZE } }
|
params: { query: { pageNumber: 1, pageSize: HOME_LATEST_PAGE_SIZE } }
|
||||||
});
|
});
|
||||||
const playlistsPromise = locals.user
|
const playlistsPromise = api.GET('/playlists/users/{userId}', {
|
||||||
? api.GET('/playlists/users/{userId}', {
|
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 12 } }
|
||||||
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 12 } }
|
});
|
||||||
})
|
const recentlyPlayedPromise = api.GET('/users/{id}/listening-history', {
|
||||||
: Promise.resolve({ data: undefined });
|
params: { path: { id: user.sub } }
|
||||||
const recentlyPlayedPromise = locals.user
|
});
|
||||||
? api.GET('/users/{id}/listening-history', { params: { path: { id: locals.user.sub } } })
|
|
||||||
: Promise.resolve({ data: undefined });
|
|
||||||
|
|
||||||
const [latest, playlists, recentlyPlayed] = await Promise.all([
|
const [latest, playlists, recentlyPlayed] = await Promise.all([
|
||||||
latestPromise,
|
latestPromise,
|
||||||
|
|||||||
@@ -171,7 +171,7 @@
|
|||||||
<section class="relative overflow-hidden page-x pt-11 pb-16">
|
<section class="relative overflow-hidden page-x pt-11 pb-16">
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0 blur-[70px] saturate-150"
|
class="pointer-events-none absolute inset-0 blur-[70px] saturate-150"
|
||||||
style="background:linear-gradient(135deg, var(--mf-accent), color-mix(in oklch, var(--mf-accent), black 55%));animation:breathe 9s ease-in-out infinite"
|
style="background:linear-gradient(135deg, var(--color-accent), color-mix(in oklch, var(--color-accent), black 55%));animation:breathe 9s ease-in-out infinite"
|
||||||
></div>
|
></div>
|
||||||
<div class="animate-enter relative flex flex-col gap-2.5">
|
<div class="animate-enter relative flex flex-col gap-2.5">
|
||||||
<p class="tracking-[0.14em] text-fg-2 uppercase">{data.greeting}</p>
|
<p class="tracking-[0.14em] text-fg-2 uppercase">{data.greeting}</p>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
|
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-neutral-950: #08080a;
|
|
||||||
|
|
||||||
--color-bg: var(--mf-bg);
|
--color-bg: var(--mf-bg);
|
||||||
--color-elevated: var(--mf-elevated);
|
--color-elevated: var(--mf-elevated);
|
||||||
@@ -18,8 +17,10 @@
|
|||||||
--color-fg-3: var(--mf-text-3);
|
--color-fg-3: var(--mf-text-3);
|
||||||
--color-muted: var(--mf-text-4);
|
--color-muted: var(--mf-text-4);
|
||||||
--color-accent: var(--mf-accent);
|
--color-accent: var(--mf-accent);
|
||||||
--color-accent-soft: var(--mf-accent-60);
|
--color-accent-soft: var(--mf-accent-soft);
|
||||||
--color-on-accent: #ffffff;
|
--color-on-accent: var(--mf-on-art);
|
||||||
|
--color-scrim: var(--mf-scrim);
|
||||||
|
--color-on-art: var(--mf-on-art);
|
||||||
--color-danger: oklch(63.7% 0.237 25.331);
|
--color-danger: oklch(63.7% 0.237 25.331);
|
||||||
--color-danger-fg: oklch(88.5% 0.062 18.334);
|
--color-danger-fg: oklch(88.5% 0.062 18.334);
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div class="grid min-h-screen place-items-center bg-bg px-6 text-fg">
|
<div class="grid min-h-screen place-items-center bg-bg px-6 text-fg">
|
||||||
<div class="flex w-full max-w-[440px] flex-col items-center gap-9 text-center">
|
<div class="flex w-full max-w-110 flex-col items-center gap-9 text-center">
|
||||||
<div class="text-3xl font-extrabold tracking-tight">Musify</div>
|
<div class="text-3xl font-extrabold tracking-tight">Musify</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user