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"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import SongPlayer from '$lib/components/SongPlayer.svelte'; import MapCard from '$lib/components/MapCard.svelte';
import MapActionButtons from '$lib/components/MapActionButtons.svelte';
import DifficultyLabel from '$lib/components/DifficultyLabel.svelte';
import SongCard from '$lib/components/SongCard.svelte';
type BeatLeaderScore = { type BeatLeaderScore = {
timeset?: string | number; 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} /> <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> </label>
</div> </div>
<div> <div class="flex items-center gap-3">
<button class="btn-neon" disabled={loading}> <button class="btn-neon" disabled={loading}>
{#if loading} {#if loading}
Loading... Loading...
@ -378,6 +375,9 @@
Compare Compare
{/if} {/if}
</button> </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> </div>
</form> </form>
@ -386,34 +386,32 @@
{/if} {/if}
{#if results.length > 0} {#if results.length > 0}
<div class="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"> <div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3 text-sm text-muted"> <div class="text-sm text-muted">
<span>{results.length} songs</span> {results.length} songs
<span>·</span> </div>
<label class="flex items-center gap-2">Sort <div class="flex items-center gap-4 text-sm text-muted flex-wrap justify-end">
<select class="rounded-md border border-white/10 bg-transparent px-2 py-1 text-sm" bind:value={sortBy}> <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="date">Date</option>
<option value="difficulty">Difficulty</option> <option value="difficulty">Difficulty</option>
</select> </select>
</label> </label>
<label class="flex items-center gap-2">Dir <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="desc">Desc</option>
<option value="asc">Asc</option> <option value="asc">Asc</option>
</select> </select>
</label> </label>
<label class="flex items-center gap-2">Page size <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={12}>12</option>
<option value={24}>24</option> <option value={24}>24</option>
<option value={36}>36</option> <option value={36}>36</option>
<option value={48}>48</option> <option value={48}>48</option>
</select> </select>
</label> </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>
</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"> <div class="mt-4 grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{#each pageItems as item} {#each pageItems as item}
<article class="card-surface overflow-hidden"> <article class="card-surface overflow-hidden">
<SongCard <MapCard
hash={item.hash} hash={item.hash}
coverURL={metaByHash[item.hash]?.coverURL} coverURL={metaByHash[item.hash]?.coverURL}
songName={metaByHash[item.hash]?.songName} songName={metaByHash[item.hash]?.songName}
mapper={metaByHash[item.hash]?.mapper} mapper={metaByHash[item.hash]?.mapper}
stars={starsByKey[`${item.hash}|${item.difficulties[0]?.name ?? 'ExpertPlus'}|${item.difficulties[0]?.characteristic ?? 'Standard'}`]?.stars} stars={starsByKey[`${item.hash}|${item.difficulties[0]?.name ?? 'ExpertPlus'}|${item.difficulties[0]?.characteristic ?? 'Standard'}`]?.stars}
timeset={item.timeset} timeset={item.timeset}
> diffName={item.difficulties[0]?.name ?? 'ExpertPlus'}
<div slot="difficulty" class="mt-2 flex items-center gap-2"> modeName={item.difficulties[0]?.characteristic ?? 'Standard'}
<DifficultyLabel leaderboardId={item.leaderboardId}
diffName={item.difficulties[0]?.name ?? 'ExpertPlus'} beatsaverKey={metaByHash[item.hash]?.key}
modeName={item.difficulties[0]?.characteristic ?? 'Standard'} playerWithDifficulty={true}
/> />
<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}
/>
</div>
</SongCard>
</article> </article>
{/each} {/each}
</div> </div>
@ -474,6 +458,47 @@
<style> <style>
.text-danger { color: #dc2626; } .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> </style>

View File

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