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">
|
<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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user