head-to-head: add buttons

This commit is contained in:
pleb 2025-10-29 09:52:00 -07:00
parent f2380c2463
commit 0eb11db7d8
2 changed files with 134 additions and 8 deletions

View File

@ -88,7 +88,7 @@
// Toast notification state // Toast notification state
let toastMessage = ''; let toastMessage = '';
let showToast = false; let showToast = false;
let toastTimeout: number | null = null; let toastTimeout: ReturnType<typeof setTimeout> | null = null;
// Button feedback state - track which buttons are currently "lit up" // Button feedback state - track which buttons are currently "lit up"
let litButtons = new Set<string>(); let litButtons = new Set<string>();
@ -543,15 +543,15 @@
target="_blank" target="_blank"
rel="noopener" rel="noopener"
title="Open in BeatSaver" title="Open in BeatSaver"
>BSR</a >BS</a
> >
<button <button
class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20 disabled:opacity-50" class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20 disabled:opacity-50"
class:lit-up={litButtons.has(`bsr-${item.hash}`)} class:lit-up={litButtons.has(`bsr-${item.hash}`)}
on:click={() => { const key = metaByHash[item.hash]?.key; if (key) copyBsrCommand(key, item.hash); }} on:click={() => { const key = metaByHash[item.hash]?.key; if (key) copyBsrCommand(key, item.hash); }}
disabled={!metaByHash[item.hash]?.key} disabled={!metaByHash[item.hash]?.key}
title="Copy !bsr" title="!bsr"
>Copy !bsr</button> >!bsr</button>
</div> </div>
</div> </div>
</article> </article>

View File

@ -72,6 +72,55 @@
let metaByHash: Record<string, MapMeta> = {}; let metaByHash: Record<string, MapMeta> = {};
let loadingMeta = false; let loadingMeta = false;
// Toast notification state
let toastMessage = '';
let showToast = false;
let toastTimeout: ReturnType<typeof setTimeout> | null = null;
// Button feedback state - track which buttons are currently "lit up"
let litButtons = new Set<string>();
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(buttonId: string) {
litButtons.add(buttonId);
// Reassign here too to trigger reactivity immediately
litButtons = litButtons;
// Remove the lighting effect after 1 second
setTimeout(() => {
litButtons.delete(buttonId);
litButtons = litButtons; // Trigger reactivity
}, 1000);
}
async function copyBsrCommand(key: string, hash: string) {
try {
const bsrCommand = `!bsr ${key}`;
await navigator.clipboard.writeText(bsrCommand);
// Show success feedback with the actual command
showToastMessage(`Copied "${bsrCommand}" to clipboard`);
lightUpButton(`bsr-${hash}`);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
showToastMessage('Failed to copy to clipboard');
}
}
function normalizeAccuracy(value: number | undefined): number | null { function normalizeAccuracy(value: number | undefined): number | null {
if (value === undefined || value === null) return null; if (value === undefined || value === null) return null;
return value <= 1 ? value * 100 : value; return value <= 1 ? value * 100 : value;
@ -192,7 +241,8 @@
if (!bMap.has(key)) continue; if (!bMap.has(key)) continue;
const bScore = bMap.get(key)!; const bScore = bMap.get(key)!;
const [hashLower, diffName, modeName] = key.split('|'); const [hashLower, diffName, modeName] = key.split('|');
const hash = (aScore.leaderboard?.song?.hash ?? bScore.leaderboard?.song?.hash ?? hashLower).toString(); const rawHash = (aScore.leaderboard?.song?.hash ?? bScore.leaderboard?.song?.hash ?? hashLower);
const hash = String(rawHash).toLowerCase();
const tA = parseTimeset(aScore.timeset); const tA = parseTimeset(aScore.timeset);
const tB = parseTimeset(bScore.timeset); const tB = parseTimeset(bScore.timeset);
const accA = normalizeAccuracy((aScore.accuracy ?? aScore.acc) as number | undefined); const accA = normalizeAccuracy((aScore.accuracy ?? aScore.acc) as number | undefined);
@ -638,11 +688,32 @@
{/if} {/if}
</div> </div>
<div class="mt-3 flex items-center gap-2"> <div class="mt-3 flex items-center gap-2">
<div class="w-1/2"> <div class="w-1/2 flex flex-wrap gap-2">
<a class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20" href={item.leaderboardId <a
class="rounded-md border border-white/10 px-2 py-1 text-xs hover:border-white/20"
href={item.leaderboardId
? `https://beatleader.com/leaderboard/global/${item.leaderboardId}` ? `https://beatleader.com/leaderboard/global/${item.leaderboardId}`
: `https://beatleader.com/leaderboard/global/${item.hash}?diff=${encodeURIComponent(item.diffName)}&mode=${encodeURIComponent(item.modeName)}`} : `https://beatleader.com/leaderboard/global/${item.hash}?diff=${encodeURIComponent(item.diffName)}&mode=${encodeURIComponent(item.modeName)}`}
target="_blank" rel="noopener" title="Open in BeatLeader">BL</a> 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={metaByHash[item.hash]?.key ? `https://beatsaver.com/maps/${metaByHash[item.hash]?.key}` : `https://beatsaver.com/search/hash/${item.hash}`}
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={litButtons.has(`bsr-${item.hash}`)}
on:click={() => { const key = metaByHash[item.hash]?.key; if (key) copyBsrCommand(key, item.hash); }}
disabled={!metaByHash[item.hash]?.key}
title="!bsr"
>!bsr</button>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<SongPlayer hash={item.hash} preferBeatLeader={true} /> <SongPlayer hash={item.hash} preferBeatLeader={true} />
@ -663,6 +734,13 @@
{/if} {/if}
</section> </section>
<!-- Toast Notification -->
{#if showToast}
<div class="toast-notification" role="status" aria-live="polite">
{toastMessage}
</div>
{/if}
<style> <style>
.text-danger { color: #dc2626; } .text-danger { color: #dc2626; }
@ -814,6 +892,54 @@
background: #0f172a; background: #0f172a;
color: #fff; color: #fff;
} }
/* 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;
color: white;
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> </style>