refactor song card component and match styling on both tools

This commit is contained in:
pleb 2025-10-29 10:35:00 -07:00
parent c0a564393c
commit 84f10c13bc
3 changed files with 169 additions and 64 deletions

View File

@ -0,0 +1,98 @@
<script lang="ts">
import DifficultyLabel from './DifficultyLabel.svelte';
import MapActionButtons from './MapActionButtons.svelte';
import SongPlayer from './SongPlayer.svelte';
// Song metadata
export let hash: string;
export let coverURL: string | undefined = undefined;
export let songName: string | undefined = undefined;
export let mapper: string | undefined = undefined;
export let stars: number | undefined = undefined;
export let timeset: number | undefined = undefined;
// Difficulty info
export let diffName: string;
export let modeName: string = 'Standard';
// BeatLeader/BeatSaver links
export let leaderboardId: string | undefined = undefined;
export let beatsaverKey: string | undefined = undefined;
// Layout control
export let playerWithDifficulty: boolean = true; // if false, player goes with buttons
</script>
<div class="aspect-square bg-black/30">
{#if coverURL}
<img
src={coverURL}
alt={songName ?? hash}
loading="lazy"
class="h-full w-full object-cover"
/>
{:else}
<div class="h-full w-full flex items-center justify-center text-xs text-muted">No cover</div>
{/if}
</div>
<div class="p-3">
<div class="font-semibold truncate" title={songName ?? hash}>
{songName ?? hash}
</div>
{#if mapper}
<div class="mt-0.5 text-xs text-muted truncate flex items-center justify-between">
<span>
{mapper}
{#if stars !== undefined}
<span class="ml-3" title="BeatLeader star rating">{stars.toFixed(2)}</span>
{/if}
</span>
{#if timeset !== undefined}
<span class="text-[11px] ml-2">{new Date(timeset * 1000).toLocaleDateString()}</span>
{/if}
</div>
{/if}
{#if playerWithDifficulty}
<div class="mt-2 flex items-center gap-2">
<DifficultyLabel {diffName} {modeName} />
<div class="flex-1">
<SongPlayer {hash} preferBeatLeader={true} />
</div>
</div>
{:else}
<div class="mt-2">
<DifficultyLabel {diffName} {modeName} />
</div>
{/if}
<slot name="content" />
{#if playerWithDifficulty}
<div class="mt-3">
<MapActionButtons
{hash}
{leaderboardId}
{diffName}
{modeName}
{beatsaverKey}
/>
</div>
{:else}
<div class="mt-3 flex items-center gap-2">
<div class="w-1/2">
<MapActionButtons
{hash}
{leaderboardId}
{diffName}
{modeName}
{beatsaverKey}
/>
</div>
<div class="w-1/2">
<SongPlayer {hash} preferBeatLeader={true} />
</div>
</div>
{/if}
</div>

View File

@ -1,9 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import SongPlayer from '$lib/components/SongPlayer.svelte';
import MapActionButtons from '$lib/components/MapActionButtons.svelte';
import DifficultyLabel from '$lib/components/DifficultyLabel.svelte';
import SongCard from '$lib/components/SongCard.svelte';
import MapCard from '$lib/components/MapCard.svelte';
type BeatLeaderScore = {
timeset?: string | number;
@ -370,7 +367,7 @@
<input class="mt-1 w-full rounded-md border border-white/10 bg-transparent px-3 py-2 text-sm outline-none" type="number" min="0" max="120" bind:value={monthsB} />
</label>
</div>
<div>
<div class="flex items-center gap-3">
<button class="btn-neon" disabled={loading}>
{#if loading}
Loading...
@ -378,6 +375,9 @@
Compare
{/if}
</button>
{#if results.length > 0}
<button type="button" class="rounded-md border border-white/10 px-3 py-2 text-sm" on:click={downloadPlaylist}>Download .bplist</button>
{/if}
</div>
</form>
@ -386,34 +386,32 @@
{/if}
{#if results.length > 0}
<div class="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3 text-sm text-muted">
<span>{results.length} songs</span>
<span>·</span>
<label class="flex items-center gap-2">Sort
<select class="rounded-md border border-white/10 bg-transparent px-2 py-1 text-sm" bind:value={sortBy}>
<div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="text-sm text-muted">
{results.length} songs
</div>
<div class="flex items-center gap-4 text-sm text-muted flex-wrap justify-end">
<label class="flex items-center gap-3">
<span class="filter-label">Options:</span>
<select class="neon-select" bind:value={sortBy}>
<option value="date">Date</option>
<option value="difficulty">Difficulty</option>
</select>
</label>
<label class="flex items-center gap-2">Dir
<select class="rounded-md border border-white/10 bg-transparent px-2 py-1 text-sm" bind:value={sortDir}>
<select class="neon-select" bind:value={sortDir}>
<option value="desc">Desc</option>
<option value="asc">Asc</option>
</select>
</label>
<label class="flex items-center gap-2">Page size
<select class="rounded-md border border-white/10 bg-transparent px-2 py-1 text-sm" bind:value={pageSize}>
<select class="neon-select" bind:value={pageSize}>
<option value={12}>12</option>
<option value={24}>24</option>
<option value={36}>36</option>
<option value={48}>48</option>
</select>
</label>
</div>
<div class="flex items-center gap-3">
<button class="rounded-md border border-white/10 px-3 py-2 text-sm" on:click={downloadPlaylist}>Download .bplist</button>
</div>
</div>
@ -427,33 +425,19 @@
<div class="mt-4 grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{#each pageItems as item}
<article class="card-surface overflow-hidden">
<SongCard
<MapCard
hash={item.hash}
coverURL={metaByHash[item.hash]?.coverURL}
songName={metaByHash[item.hash]?.songName}
mapper={metaByHash[item.hash]?.mapper}
stars={starsByKey[`${item.hash}|${item.difficulties[0]?.name ?? 'ExpertPlus'}|${item.difficulties[0]?.characteristic ?? 'Standard'}`]?.stars}
timeset={item.timeset}
>
<div slot="difficulty" class="mt-2 flex items-center gap-2">
<DifficultyLabel
diffName={item.difficulties[0]?.name ?? 'ExpertPlus'}
modeName={item.difficulties[0]?.characteristic ?? 'Standard'}
/>
<div class="flex-1">
<SongPlayer hash={item.hash} preferBeatLeader={true} />
</div>
</div>
<div slot="actions" class="mt-3">
<MapActionButtons
hash={item.hash}
leaderboardId={item.leaderboardId}
diffName={item.difficulties[0]?.name ?? 'ExpertPlus'}
modeName={item.difficulties[0]?.characteristic ?? 'Standard'}
beatsaverKey={metaByHash[item.hash]?.key}
playerWithDifficulty={true}
/>
</div>
</SongCard>
</article>
{/each}
</div>
@ -474,6 +458,47 @@
<style>
.text-danger { color: #dc2626; }
/* Filter label styling */
.filter-label {
font-size: 1em;
letter-spacing: 0.05em;
font-weight: 700;
color: rgba(255, 0, 170, 0.95);
text-shadow: 0 0 8px rgba(255, 0, 170, 0.3);
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
font-family: var(--font-display);
line-height: 1;
}
/* Neon select dropdown */
.neon-select {
border-radius: 0.375rem;
border: 1px solid rgba(34, 211, 238, 0.3);
background: linear-gradient(180deg, rgba(15,23,42,0.9), rgba(11,15,23,0.95));
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
color: rgba(148, 163, 184, 1);
transition: all 0.2s ease;
box-shadow: 0 0 8px rgba(34, 211, 238, 0.15);
cursor: pointer;
}
.neon-select:hover {
border-color: rgba(34, 211, 238, 0.5);
color: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 16px rgba(34, 211, 238, 0.25);
}
.neon-select:focus {
outline: none;
border-color: rgba(34, 211, 238, 0.7);
box-shadow: 0 0 20px rgba(34, 211, 238, 0.35), 0 0 0 2px rgba(34, 211, 238, 0.1);
}
.neon-select option {
background: #0f172a;
color: #fff;
}
</style>

View File

@ -1,9 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import SongPlayer from '$lib/components/SongPlayer.svelte';
import MapActionButtons from '$lib/components/MapActionButtons.svelte';
import DifficultyLabel from '$lib/components/DifficultyLabel.svelte';
import SongCard from '$lib/components/SongCard.svelte';
import MapCard from '$lib/components/MapCard.svelte';
type BeatLeaderScore = {
timeset?: string | number;
@ -596,19 +593,18 @@
<div class="mt-4 grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{#each pageItems as item}
<article class="card-surface overflow-hidden">
<SongCard
<MapCard
hash={item.hash}
coverURL={metaByHash[item.hash]?.coverURL}
songName={metaByHash[item.hash]?.songName}
mapper={metaByHash[item.hash]?.mapper}
timeset={item.timeset}
>
<div slot="difficulty" class="mt-2">
<DifficultyLabel
diffName={item.diffName}
modeName={item.modeName}
/>
</div>
leaderboardId={item.leaderboardId}
beatsaverKey={metaByHash[item.hash]?.key}
playerWithDifficulty={false}
>
<div slot="content">
<div class="mt-3 grid grid-cols-2 gap-3 neon-surface">
<div class="player-card playerA {item.accA != null && item.accB != null && item.accA > item.accB ? 'winner' : ''}">
@ -638,21 +634,7 @@
{/if}
</div>
</div>
<div slot="actions" class="mt-3 flex items-center gap-2">
<div class="w-1/2">
<MapActionButtons
hash={item.hash}
leaderboardId={item.leaderboardId}
diffName={item.diffName}
modeName={item.modeName}
beatsaverKey={metaByHash[item.hash]?.key}
/>
</div>
<div class="w-1/2">
<SongPlayer hash={item.hash} preferBeatLeader={true} />
</div>
</div>
</SongCard>
</MapCard>
</article>
{/each}
</div>