refactor song card component and match styling on both tools
This commit is contained in:
parent
c0a564393c
commit
84f10c13bc
98
src/lib/components/MapCard.svelte
Normal file
98
src/lib/components/MapCard.svelte
Normal 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>
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</SongCard>
|
||||
diffName={item.difficulties[0]?.name ?? 'ExpertPlus'}
|
||||
modeName={item.difficulties[0]?.characteristic ?? 'Standard'}
|
||||
leaderboardId={item.leaderboardId}
|
||||
beatsaverKey={metaByHash[item.hash]?.key}
|
||||
playerWithDifficulty={true}
|
||||
/>
|
||||
</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>
|
||||
|
||||
|
||||
|
||||
@ -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}
|
||||
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 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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user