add components for song metadata
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
export let diffName: string;
|
||||
export let modeName: string = 'Standard';
|
||||
|
||||
function difficultyToColor(name: string | undefined): string {
|
||||
const n = (name ?? 'ExpertPlus').toLowerCase();
|
||||
if (n === 'easy') return 'MediumSeaGreen';
|
||||
if (n === 'normal') return '#59b0f4';
|
||||
if (n === 'hard') return 'tomato';
|
||||
if (n === 'expert') return '#bf2a42';
|
||||
if (n === 'expertplus' || n === 'expert+' || n === 'ex+' ) return '#8f48db';
|
||||
return '#8f48db';
|
||||
}
|
||||
</script>
|
||||
|
||||
<span class="rounded bg-white/10 px-2 py-0.5 text-[11px]">
|
||||
{modeName} ·
|
||||
<span class="rounded px-1 ml-1" style="background-color: {difficultyToColor(diffName)}; color: #fff;">
|
||||
{diffName}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<script lang="ts">
|
||||
export let hash: string;
|
||||
export let leaderboardId: string | undefined = undefined;
|
||||
export let diffName: string = 'ExpertPlus';
|
||||
export let modeName: string = 'Standard';
|
||||
export let beatsaverKey: string | undefined = undefined;
|
||||
|
||||
// Toast notification state
|
||||
let toastMessage = '';
|
||||
let showToast = false;
|
||||
let toastTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Button feedback state
|
||||
let isLitUp = false;
|
||||
|
||||
function showToastMessage(message: string) {
|
||||
// Clear any existing toast
|
||||
if (toastTimeout) {
|
||||
clearTimeout(toastTimeout);
|
||||
}
|
||||
|
||||
toastMessage = message;
|
||||
showToast = true;
|
||||
|
||||
// Auto-hide toast after 3 seconds
|
||||
toastTimeout = setTimeout(() => {
|
||||
showToast = false;
|
||||
toastMessage = '';
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function lightUpButton() {
|
||||
isLitUp = true;
|
||||
// Remove the lighting effect after 1 second
|
||||
setTimeout(() => {
|
||||
isLitUp = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function copyBsrCommand() {
|
||||
if (!beatsaverKey) return;
|
||||
|
||||
try {
|
||||
const bsrCommand = `!bsr ${beatsaverKey}`;
|
||||
await navigator.clipboard.writeText(bsrCommand);
|
||||
|
||||
// Show success feedback with the actual command
|
||||
showToastMessage(`Copied "${bsrCommand}" to clipboard`);
|
||||
lightUpButton();
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err);
|
||||
showToastMessage('Failed to copy to clipboard');
|
||||
}
|
||||
}
|
||||
|
||||
$: beatLeaderUrl = leaderboardId
|
||||
? `https://beatleader.com/leaderboard/global/${leaderboardId}`
|
||||
: `https://beatleader.com/leaderboard/global/${hash}?diff=${encodeURIComponent(diffName)}&mode=${encodeURIComponent(modeName)}`;
|
||||
|
||||
$: beatSaverUrl = beatsaverKey
|
||||
? `https://beatsaver.com/maps/${beatsaverKey}`
|
||||
: `https://beatsaver.com/search/hash/${hash}`;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a
|
||||
class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20"
|
||||
href={beatLeaderUrl}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="Open in BeatLeader"
|
||||
>BL</a
|
||||
>
|
||||
<a
|
||||
class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20"
|
||||
href={beatSaverUrl}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="Open in BeatSaver"
|
||||
>BS</a
|
||||
>
|
||||
<button
|
||||
class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20 disabled:opacity-50"
|
||||
class:lit-up={isLitUp}
|
||||
on:click={copyBsrCommand}
|
||||
disabled={!beatsaverKey}
|
||||
title="!bsr"
|
||||
>!bsr</button>
|
||||
</div>
|
||||
|
||||
<!-- Toast Notification -->
|
||||
{#if showToast}
|
||||
<div class="toast-notification" role="status" aria-live="polite">
|
||||
{toastMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Toast notification styles */
|
||||
.toast-notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Button lighting effect */
|
||||
.lit-up {
|
||||
background: linear-gradient(45deg, #10b981, #059669);
|
||||
border-color: #10b981 !important;
|
||||
color: white !important;
|
||||
box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
|
||||
animation: pulse 0.6s ease-out;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(16, 185, 129, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(16, 185, 129, 0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
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;
|
||||
</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}
|
||||
<slot name="difficulty" />
|
||||
<slot name="content" />
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user