Split the explore page into genre grid, top result and search results components
This commit is contained in:
@@ -48,3 +48,5 @@ export function createPagedList<T>(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PagedList<T> = ReturnType<typeof createPagedList<T>>;
|
||||||
|
|||||||
@@ -1,23 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Music from '@lucide/svelte/icons/music';
|
|
||||||
import { player } from '$lib/player/player.svelte';
|
|
||||||
import type { Paged, Track } from '$lib/types';
|
import type { Paged, Track } from '$lib/types';
|
||||||
import { fmtTime, fmtPlays, plural } from '$lib/utils/format';
|
import { plural } from '$lib/utils/format';
|
||||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||||
import Alert from '$lib/components/ui/primitives/Alert.svelte';
|
import Alert from '$lib/components/ui/primitives/Alert.svelte';
|
||||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
|
||||||
import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte';
|
|
||||||
import Artwork from '$lib/components/ui/media/Artwork.svelte';
|
|
||||||
import Avatar from '$lib/components/ui/media/Avatar.svelte';
|
|
||||||
import UserRow from '$lib/components/ui/media/UserRow.svelte';
|
|
||||||
import PlayButton from '$lib/components/ui/media/PlayButton.svelte';
|
|
||||||
import EqBars from '$lib/components/ui/media/EqBars.svelte';
|
|
||||||
import ListRow from '$lib/components/ui/media/ListRow.svelte';
|
|
||||||
import Chip from '$lib/components/ui/primitives/Chip.svelte';
|
import Chip from '$lib/components/ui/primitives/Chip.svelte';
|
||||||
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
|
||||||
import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
||||||
import { genreHref, playlistCover } from '$lib/utils/hrefs';
|
import { genreHref } from '$lib/utils/hrefs';
|
||||||
import { createMenu } from '$lib/state/menu.svelte';
|
import { createMenu } from '$lib/state/menu.svelte';
|
||||||
import { createPagedList } from '$lib/state/pagedList.svelte';
|
import { createPagedList } from '$lib/state/pagedList.svelte';
|
||||||
import TrackContextMenu from '$lib/components/music/TrackContextMenu.svelte';
|
import TrackContextMenu from '$lib/components/music/TrackContextMenu.svelte';
|
||||||
@@ -25,21 +14,14 @@
|
|||||||
import { genreInfo, genreTiles } from '$lib/data/genres';
|
import { genreInfo, genreTiles } from '$lib/data/genres';
|
||||||
import {
|
import {
|
||||||
type SearchFilter,
|
type SearchFilter,
|
||||||
showGroup,
|
|
||||||
capped,
|
|
||||||
matchPlaylists,
|
matchPlaylists,
|
||||||
SEARCH_LABELS,
|
|
||||||
searchTotal,
|
searchTotal,
|
||||||
searchChips,
|
searchChips,
|
||||||
findTopResult
|
findTopResult
|
||||||
} from '$lib/data/search';
|
} from '$lib/data/search';
|
||||||
|
import GenreGrid from './GenreGrid.svelte';
|
||||||
const TOP_RESULT_KIND_LABEL: Record<TopResult['kind'], string> = {
|
import TopResult from './TopResult.svelte';
|
||||||
track: 'Canción',
|
import SearchResults from './SearchResults.svelte';
|
||||||
album: 'Álbum',
|
|
||||||
playlist: 'Playlist',
|
|
||||||
user: 'Usuario'
|
|
||||||
};
|
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -96,18 +78,7 @@
|
|||||||
const chips = $derived(searchChips(counts));
|
const chips = $derived(searchChips(counts));
|
||||||
const totalHits = $derived(searchTotal(counts));
|
const totalHits = $derived(searchTotal(counts));
|
||||||
|
|
||||||
type TopResult = NonNullable<
|
const topResult = $derived(
|
||||||
ReturnType<
|
|
||||||
typeof findTopResult<
|
|
||||||
Track,
|
|
||||||
(typeof albums)[number],
|
|
||||||
(typeof playlistMatches)[number],
|
|
||||||
(typeof users)[number]
|
|
||||||
>
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
|
|
||||||
const topResult = $derived<TopResult | null>(
|
|
||||||
sfilter === 'all' && data.query
|
sfilter === 'all' && data.query
|
||||||
? findTopResult({
|
? findTopResult({
|
||||||
query: data.query,
|
query: data.query,
|
||||||
@@ -118,18 +89,6 @@
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
function playTopResult() {
|
|
||||||
if (topResult?.kind !== 'track') return;
|
|
||||||
player.playOrToggle([topResult.track], 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const topResultHref = $derived.by(() => {
|
|
||||||
if (!topResult || topResult.kind === 'track') return undefined;
|
|
||||||
if (topResult.kind === 'album') return `/albums/${topResult.album.id}`;
|
|
||||||
if (topResult.kind === 'playlist') return `/playlists/${topResult.playlist.id}`;
|
|
||||||
return `/user/${topResult.user.id}`;
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -174,62 +133,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if topResult}
|
{#if topResult}
|
||||||
{#snippet topResultBody()}
|
<TopResult result={topResult} />
|
||||||
{#if topResult.kind === 'track'}
|
|
||||||
<Artwork
|
|
||||||
trackIds={[topResult.track.id]}
|
|
||||||
size="xl"
|
|
||||||
alt={topResult.track.title}
|
|
||||||
class="shrink-0"
|
|
||||||
/>
|
|
||||||
{:else if topResult.kind === 'album'}
|
|
||||||
<Artwork trackIds={topResult.album.coverTrackIds} size="xl" class="shrink-0" />
|
|
||||||
{:else if topResult.kind === 'playlist'}
|
|
||||||
<Artwork
|
|
||||||
src={playlistCover(topResult.playlist, 'large')}
|
|
||||||
trackIds={topResult.playlist.coverTrackIds}
|
|
||||||
size="xl"
|
|
||||||
class="shrink-0"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<Avatar name={topResult.user.name} src={topResult.user.profilePictureUrl} size="lg" />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
|
||||||
<div class="text-eyebrow leading-none font-semibold text-fg-2">
|
|
||||||
Mejor resultado · {TOP_RESULT_KIND_LABEL[topResult.kind]}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="truncate font-display text-lg leading-none font-medium tracking-display text-fg"
|
|
||||||
>
|
|
||||||
{topResult.kind === 'track'
|
|
||||||
? topResult.track.title
|
|
||||||
: topResult.kind === 'album'
|
|
||||||
? topResult.album.title
|
|
||||||
: topResult.kind === 'playlist'
|
|
||||||
? topResult.playlist.name
|
|
||||||
: topResult.user.name}
|
|
||||||
</div>
|
|
||||||
{#if topResult.kind === 'track' && topResult.track.artist}
|
|
||||||
<div class="truncate text-sm leading-none text-fg-2">
|
|
||||||
{topResult.track.artist}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PlayButton as="span" size="lg" label="Reproducir" class="group-hover/top:brightness-110" />
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<svelte:element
|
|
||||||
this={topResult.kind === 'track' ? 'button' : 'a'}
|
|
||||||
type={topResult.kind === 'track' ? 'button' : undefined}
|
|
||||||
role={topResult.kind === 'track' ? 'button' : 'link'}
|
|
||||||
href={topResult.kind === 'track' ? undefined : topResultHref}
|
|
||||||
onclick={topResult.kind === 'track' ? playTopResult : undefined}
|
|
||||||
class="group/top animate-pop mt-6.5 flex w-full items-center gap-5 rounded-panel-lg bg-surface p-4.5 text-left transition hover:bg-surface-hover"
|
|
||||||
>
|
|
||||||
{@render topResultBody()}
|
|
||||||
</svelte:element>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -255,138 +159,23 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !data.query && !data.genre}
|
{#if !data.query && !data.genre}
|
||||||
<div class="grid-wide">
|
<GenreGrid {tiles} />
|
||||||
{#each tiles as tile, i (tile.genre)}
|
|
||||||
<a
|
|
||||||
href={genreHref(tile.genre)}
|
|
||||||
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-panel p-4 transition-[filter] genre-tile hover:brightness-110"
|
|
||||||
style="--i:{i}; --tile-hue:{tile.hue}"
|
|
||||||
>
|
|
||||||
<div class="text-on-art">
|
|
||||||
{tile.label}
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-on-art-2">{tile.tagline}</div>
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{#if tiles.length === 0}
|
|
||||||
<EmptyState icon={Music} description="Todavía no hay géneros. Sube música para empezar." />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if (data.query || data.genre) && !nothingFound}
|
{#if (data.query || data.genre) && !nothingFound}
|
||||||
<div class="mt-9 flex flex-col gap-9">
|
<SearchResults
|
||||||
{#if showGroup(sfilter, 'tracks')}
|
filter={sfilter}
|
||||||
<div>
|
{counts}
|
||||||
{#if items.length > 0}
|
query={data.query}
|
||||||
<SectionHeading title={SEARCH_LABELS.tracks} count={counts.tracks} />
|
inGenre={!!activeGenre}
|
||||||
<ul class="flex flex-col gap-1">
|
{tracksList}
|
||||||
{#each capped(sfilter, items) as track, i (track.id)}
|
{albums}
|
||||||
<li>
|
playlists={playlistMatches}
|
||||||
<ListRow
|
{users}
|
||||||
title={track.title}
|
{trackMenu}
|
||||||
subtitle={track.artist}
|
{albumMenu}
|
||||||
subtitleHref={track.ownerUserId}
|
|
||||||
explicit={track.explicit}
|
|
||||||
active={player.currentId === track.id}
|
|
||||||
size="lg"
|
|
||||||
trackId={track.id}
|
|
||||||
onclick={() => player.playOrToggle(items, i)}
|
|
||||||
oncontextmenu={(e) => trackMenu.open(e, track)}
|
|
||||||
>
|
|
||||||
{#snippet overlay()}
|
|
||||||
{#if player.currentId === track.id}
|
|
||||||
<EqBars overlay paused={!player.playing} />
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
{#snippet trailing()}
|
|
||||||
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
|
||||||
{fmtTime(track.duration)} · {fmtPlays(track.listensCount)}
|
|
||||||
</span>
|
|
||||||
{/snippet}
|
|
||||||
</ListRow>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{#if sfilter === 'tracks' && tracksList.hasMore}
|
|
||||||
<InfiniteScroll
|
|
||||||
onLoadMore={() => tracksList.loadMore()}
|
|
||||||
hasMore={tracksList.hasMore}
|
|
||||||
loading={tracksList.loading}
|
|
||||||
error={tracksList.error}
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
|
||||||
<EmptyState
|
|
||||||
icon={Music}
|
|
||||||
description={activeGenre
|
|
||||||
? 'Todavía no hay canciones de este género.'
|
|
||||||
: `No hay canciones que coincidan con «${data.query}».`}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if hasAlbums && showGroup(sfilter, 'albums')}
|
|
||||||
<div>
|
|
||||||
<SectionHeading title={SEARCH_LABELS.albums} count={counts.albums} />
|
|
||||||
<ul class="flex flex-col gap-1">
|
|
||||||
{#each capped(sfilter, albums) as album (album.id)}
|
|
||||||
<li>
|
|
||||||
<ListRow
|
|
||||||
title={album.title}
|
|
||||||
href="/albums/{album.id}"
|
|
||||||
size="lg"
|
|
||||||
trackIds={album.coverTrackIds}
|
|
||||||
oncontextmenu={(e) => albumMenu.open(e, album.id)}
|
|
||||||
>
|
|
||||||
{#snippet trailing()}
|
|
||||||
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
|
||||||
{plural(album.trackCount, 'canción', 'canciones')}
|
|
||||||
</span>
|
|
||||||
{/snippet}
|
|
||||||
</ListRow>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if hasUsers && showGroup(sfilter, 'users')}
|
|
||||||
<div>
|
|
||||||
<SectionHeading title={SEARCH_LABELS.users} count={counts.users} />
|
|
||||||
<ul class="flex flex-col gap-1">
|
|
||||||
{#each capped(sfilter, users) as u (u.id)}
|
|
||||||
<li>
|
|
||||||
<UserRow user={u} />
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if hasPlaylists && showGroup(sfilter, 'playlists')}
|
|
||||||
<div>
|
|
||||||
<SectionHeading title={SEARCH_LABELS.playlists} count={counts.playlists} />
|
|
||||||
<ul class="flex flex-col gap-1">
|
|
||||||
{#each capped(sfilter, playlistMatches) as playlist (playlist.id)}
|
|
||||||
<li>
|
|
||||||
<ListRow
|
|
||||||
title={playlist.name}
|
|
||||||
subtitle={playlist.description}
|
|
||||||
href="/playlists/{playlist.id}"
|
|
||||||
size="lg"
|
|
||||||
coverSrc={playlistCover(playlist, 'medium')}
|
|
||||||
trackIds={playlist.coverTrackIds}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
<TrackContextMenu menu={trackMenu} playlists={data.userPlaylists} />
|
<TrackContextMenu menu={trackMenu} playlists={data.userPlaylists} />
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Music from '@lucide/svelte/icons/music';
|
||||||
|
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||||
|
import { genreHref } from '$lib/utils/hrefs';
|
||||||
|
import type { genreTiles } from '$lib/data/genres';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tiles: ReturnType<typeof genreTiles>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { tiles }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="grid-wide">
|
||||||
|
{#each tiles as tile, i (tile.genre)}
|
||||||
|
<a
|
||||||
|
href={genreHref(tile.genre)}
|
||||||
|
class="animate-enter group relative flex h-28 flex-col gap-1 overflow-hidden rounded-panel p-4 transition-[filter] genre-tile hover:brightness-110"
|
||||||
|
style="--i:{i}; --tile-hue:{tile.hue}"
|
||||||
|
>
|
||||||
|
<div class="text-on-art">
|
||||||
|
{tile.label}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-on-art-2">{tile.tagline}</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if tiles.length === 0}
|
||||||
|
<EmptyState icon={Music} description="Todavía no hay géneros. Sube música para empezar." />
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Music from '@lucide/svelte/icons/music';
|
||||||
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import type { Album, Playlist, Track } from '$lib/types';
|
||||||
|
import { fmtTime, fmtPlays, plural } from '$lib/utils/format';
|
||||||
|
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||||
|
import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte';
|
||||||
|
import UserRow from '$lib/components/ui/media/UserRow.svelte';
|
||||||
|
import EqBars from '$lib/components/ui/media/EqBars.svelte';
|
||||||
|
import ListRow from '$lib/components/ui/media/ListRow.svelte';
|
||||||
|
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
||||||
|
import { playlistCover } from '$lib/utils/hrefs';
|
||||||
|
import type { Menu } from '$lib/state/menu.svelte';
|
||||||
|
import type { PagedList } from '$lib/state/pagedList.svelte';
|
||||||
|
import {
|
||||||
|
type SearchCounts,
|
||||||
|
type SearchFilter,
|
||||||
|
showGroup,
|
||||||
|
capped,
|
||||||
|
SEARCH_LABELS
|
||||||
|
} from '$lib/data/search';
|
||||||
|
|
||||||
|
interface ResultUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
profilePictureUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filter: SearchFilter;
|
||||||
|
counts: SearchCounts;
|
||||||
|
query: string;
|
||||||
|
inGenre: boolean;
|
||||||
|
tracksList: PagedList<Track>;
|
||||||
|
albums: Album[];
|
||||||
|
playlists: Playlist[];
|
||||||
|
users: ResultUser[];
|
||||||
|
trackMenu: Menu<Track>;
|
||||||
|
albumMenu: Menu<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
filter,
|
||||||
|
counts,
|
||||||
|
query,
|
||||||
|
inGenre,
|
||||||
|
tracksList,
|
||||||
|
albums,
|
||||||
|
playlists,
|
||||||
|
users,
|
||||||
|
trackMenu,
|
||||||
|
albumMenu
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const items = $derived(tracksList.items);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mt-9 flex flex-col gap-9">
|
||||||
|
{#if showGroup(filter, 'tracks')}
|
||||||
|
<div>
|
||||||
|
{#if items.length > 0}
|
||||||
|
<SectionHeading title={SEARCH_LABELS.tracks} count={counts.tracks} />
|
||||||
|
<ul class="flex flex-col gap-1">
|
||||||
|
{#each capped(filter, items) as track, i (track.id)}
|
||||||
|
<li>
|
||||||
|
<ListRow
|
||||||
|
title={track.title}
|
||||||
|
subtitle={track.artist}
|
||||||
|
subtitleHref={track.ownerUserId}
|
||||||
|
explicit={track.explicit}
|
||||||
|
active={player.currentId === track.id}
|
||||||
|
size="lg"
|
||||||
|
trackId={track.id}
|
||||||
|
onclick={() => player.playOrToggle(items, i)}
|
||||||
|
oncontextmenu={(e) => trackMenu.open(e, track)}
|
||||||
|
>
|
||||||
|
{#snippet overlay()}
|
||||||
|
{#if player.currentId === track.id}
|
||||||
|
<EqBars overlay paused={!player.playing} />
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
{#snippet trailing()}
|
||||||
|
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||||
|
{fmtTime(track.duration)} · {fmtPlays(track.listensCount)}
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</ListRow>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{#if filter === 'tracks' && tracksList.hasMore}
|
||||||
|
<InfiniteScroll
|
||||||
|
onLoadMore={() => tracksList.loadMore()}
|
||||||
|
hasMore={tracksList.hasMore}
|
||||||
|
loading={tracksList.loading}
|
||||||
|
error={tracksList.error}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<EmptyState
|
||||||
|
icon={Music}
|
||||||
|
description={inGenre
|
||||||
|
? 'Todavía no hay canciones de este género.'
|
||||||
|
: `No hay canciones que coincidan con «${query}».`}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if albums.length > 0 && showGroup(filter, 'albums')}
|
||||||
|
<div>
|
||||||
|
<SectionHeading title={SEARCH_LABELS.albums} count={counts.albums} />
|
||||||
|
<ul class="flex flex-col gap-1">
|
||||||
|
{#each capped(filter, albums) as album (album.id)}
|
||||||
|
<li>
|
||||||
|
<ListRow
|
||||||
|
title={album.title}
|
||||||
|
href="/albums/{album.id}"
|
||||||
|
size="lg"
|
||||||
|
trackIds={album.coverTrackIds}
|
||||||
|
oncontextmenu={(e) => albumMenu.open(e, album.id)}
|
||||||
|
>
|
||||||
|
{#snippet trailing()}
|
||||||
|
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||||
|
{plural(album.trackCount, 'canción', 'canciones')}
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
</ListRow>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if users.length > 0 && showGroup(filter, 'users')}
|
||||||
|
<div>
|
||||||
|
<SectionHeading title={SEARCH_LABELS.users} count={counts.users} />
|
||||||
|
<ul class="flex flex-col gap-1">
|
||||||
|
{#each capped(filter, users) as u (u.id)}
|
||||||
|
<li>
|
||||||
|
<UserRow user={u} />
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if playlists.length > 0 && showGroup(filter, 'playlists')}
|
||||||
|
<div>
|
||||||
|
<SectionHeading title={SEARCH_LABELS.playlists} count={counts.playlists} />
|
||||||
|
<ul class="flex flex-col gap-1">
|
||||||
|
{#each capped(filter, playlists) as playlist (playlist.id)}
|
||||||
|
<li>
|
||||||
|
<ListRow
|
||||||
|
title={playlist.name}
|
||||||
|
subtitle={playlist.description}
|
||||||
|
href="/playlists/{playlist.id}"
|
||||||
|
size="lg"
|
||||||
|
coverSrc={playlistCover(playlist, 'medium')}
|
||||||
|
trackIds={playlist.coverTrackIds}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import Artwork from '$lib/components/ui/media/Artwork.svelte';
|
||||||
|
import Avatar from '$lib/components/ui/media/Avatar.svelte';
|
||||||
|
import PlayButton from '$lib/components/ui/media/PlayButton.svelte';
|
||||||
|
import { playlistCover } from '$lib/utils/hrefs';
|
||||||
|
import type { Album, Playlist, Track } from '$lib/types';
|
||||||
|
import type { TopResult as TopResultData } from '$lib/data/search';
|
||||||
|
|
||||||
|
interface ResultUser {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
profilePictureUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
result: TopResultData<Track, Album, Playlist, ResultUser>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { result }: Props = $props();
|
||||||
|
|
||||||
|
const KIND_LABEL: Record<Props['result']['kind'], string> = {
|
||||||
|
track: 'Canción',
|
||||||
|
album: 'Álbum',
|
||||||
|
playlist: 'Playlist',
|
||||||
|
user: 'Usuario'
|
||||||
|
};
|
||||||
|
|
||||||
|
function play() {
|
||||||
|
if (result.kind !== 'track') return;
|
||||||
|
player.playOrToggle([result.track], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const href = $derived.by(() => {
|
||||||
|
if (result.kind === 'track') return undefined;
|
||||||
|
if (result.kind === 'album') return `/albums/${result.album.id}`;
|
||||||
|
if (result.kind === 'playlist') return `/playlists/${result.playlist.id}`;
|
||||||
|
return `/user/${result.user.id}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#snippet body()}
|
||||||
|
{#if result.kind === 'track'}
|
||||||
|
<Artwork trackIds={[result.track.id]} size="xl" alt={result.track.title} class="shrink-0" />
|
||||||
|
{:else if result.kind === 'album'}
|
||||||
|
<Artwork trackIds={result.album.coverTrackIds} size="xl" class="shrink-0" />
|
||||||
|
{:else if result.kind === 'playlist'}
|
||||||
|
<Artwork
|
||||||
|
src={playlistCover(result.playlist, 'large')}
|
||||||
|
trackIds={result.playlist.coverTrackIds}
|
||||||
|
size="xl"
|
||||||
|
class="shrink-0"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<Avatar name={result.user.name} src={result.user.profilePictureUrl} size="lg" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex h-22 min-w-0 flex-1 flex-col justify-center gap-3">
|
||||||
|
<div class="text-eyebrow leading-none font-semibold text-fg-2">
|
||||||
|
Mejor resultado · {KIND_LABEL[result.kind]}
|
||||||
|
</div>
|
||||||
|
<div class="truncate font-display text-lg leading-none font-medium tracking-display text-fg">
|
||||||
|
{result.kind === 'track'
|
||||||
|
? result.track.title
|
||||||
|
: result.kind === 'album'
|
||||||
|
? result.album.title
|
||||||
|
: result.kind === 'playlist'
|
||||||
|
? result.playlist.name
|
||||||
|
: result.user.name}
|
||||||
|
</div>
|
||||||
|
{#if result.kind === 'track' && result.track.artist}
|
||||||
|
<div class="truncate text-sm leading-none text-fg-2">
|
||||||
|
{result.track.artist}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PlayButton as="span" size="lg" label="Reproducir" class="group-hover/top:brightness-110" />
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<svelte:element
|
||||||
|
this={result.kind === 'track' ? 'button' : 'a'}
|
||||||
|
type={result.kind === 'track' ? 'button' : undefined}
|
||||||
|
role={result.kind === 'track' ? 'button' : 'link'}
|
||||||
|
{href}
|
||||||
|
onclick={result.kind === 'track' ? play : undefined}
|
||||||
|
class="group/top animate-pop mt-6.5 flex w-full items-center gap-5 rounded-panel-lg bg-surface p-4.5 text-left transition hover:bg-surface-hover"
|
||||||
|
>
|
||||||
|
{@render body()}
|
||||||
|
</svelte:element>
|
||||||
Reference in New Issue
Block a user