Polish inputs and make re-usable components

This commit is contained in:
2025-10-29 23:26:40 -07:00
parent 8e86b65a45
commit 0d404a8d84
6 changed files with 461 additions and 280 deletions
+178 -50
View File
@@ -9,6 +9,7 @@
export let loading = false;
export let hasResults = false;
export let oncompare: (() => void) | undefined = undefined;
export let currentPlayer: BeatLeaderPlayerProfile | null = null;
let initialized = false;
@@ -17,13 +18,36 @@
let playerAProfile: BeatLeaderPlayerProfile | null = null;
let playerBProfile: BeatLeaderPlayerProfile | null = null;
// Preview profiles (loaded as user types)
let previewAProfile: BeatLeaderPlayerProfile | null = null;
let previewBProfile: BeatLeaderPlayerProfile | null = null;
let loadingPreviewA = false;
let loadingPreviewB = false;
// Load from URL params on mount
onMount(() => {
if (browser) {
const sp = new URLSearchParams(location.search);
playerA = sp.get('a') ?? playerA;
playerB = sp.get('b') ?? playerB;
const urlA = sp.get('a');
const urlB = sp.get('b');
// Prefill playerA with current player if not in URL
if (!urlA && !playerA && currentPlayer?.id) {
playerA = currentPlayer.id;
} else {
playerA = urlA ?? playerA;
}
playerB = urlB ?? playerB;
initialized = true;
// Load initial previews if IDs are already present
if (playerA.trim()) {
void loadPreviewProfile('A', playerA.trim());
}
if (playerB.trim()) {
void loadPreviewProfile('B', playerB.trim());
}
}
});
@@ -67,6 +91,88 @@
playerBProfile = pb;
}
// Debounced preview loading
let previewDebounceTimerA: ReturnType<typeof setTimeout> | null = null;
let previewDebounceTimerB: ReturnType<typeof setTimeout> | null = null;
async function loadPreviewProfile(player: 'A' | 'B', id: string): Promise<void> {
if (!id || id.trim().length < 3) {
if (player === 'A') previewAProfile = null;
else previewBProfile = null;
return;
}
const trimmed = id.trim();
if (player === 'A') {
loadingPreviewA = true;
} else {
loadingPreviewB = true;
}
try {
const profile = await fetchPlayerProfile(trimmed);
// Only update if this is still the current player ID
const currentId = player === 'A' ? playerA.trim() : playerB.trim();
if (currentId === trimmed) {
if (player === 'A') {
previewAProfile = profile;
} else {
previewBProfile = profile;
}
}
} catch {
// Silently fail - don't show errors for preview loading
const currentId = player === 'A' ? playerA.trim() : playerB.trim();
if (currentId === trimmed) {
if (player === 'A') {
previewAProfile = null;
} else {
previewBProfile = null;
}
}
} finally {
if (player === 'A') {
loadingPreviewA = false;
} else {
loadingPreviewB = false;
}
}
}
// Watch for changes to playerA and debounce preview loading
$: if (initialized && browser) {
const trimmed = playerA.trim();
if (previewDebounceTimerA) clearTimeout(previewDebounceTimerA);
if (trimmed && trimmed.length >= 3) {
previewDebounceTimerA = setTimeout(() => {
void loadPreviewProfile('A', trimmed);
}, 800);
} else {
previewAProfile = null;
}
}
// Watch for changes to playerB and debounce preview loading
$: if (initialized && browser) {
const trimmed = playerB.trim();
if (previewDebounceTimerB) clearTimeout(previewDebounceTimerB);
if (trimmed && trimmed.length >= 3) {
previewDebounceTimerB = setTimeout(() => {
void loadPreviewProfile('B', trimmed);
}, 800);
} else {
previewBProfile = null;
}
}
// Computed: which profile to display (post-compare takes priority over preview)
$: displayProfileA = hasCompared ? playerAProfile : previewAProfile;
$: displayProfileB = hasCompared ? playerBProfile : previewBProfile;
$: showCardA = !!(displayProfileA || (loadingPreviewA && playerA.trim().length >= 3));
$: showCardB = !!(displayProfileB || (loadingPreviewB && playerB.trim().length >= 3));
async function handleSubmit(e: Event) {
e.preventDefault();
hasCompared = true;
@@ -90,31 +196,33 @@
required
/>
</label>
{#if $$slots['player-a-card']}
<slot name="player-a-card" />
{:else if showCardA}
<div class="player-card-wrapper">
{#if displayProfileA}
<PlayerCard
name={displayProfileA.name ?? 'Player A'}
avatar={displayProfileA.avatar ?? null}
country={displayProfileA.country ?? null}
rank={displayProfileA.rank ?? null}
showRank={typeof displayProfileA.rank === 'number'}
width="100%"
avatarSize={56}
techPp={displayProfileA.techPp}
accPp={displayProfileA.accPp}
passPp={displayProfileA.passPp}
playerId={displayProfileA.id ?? null}
gradientId="compare-player-a"
/>
{:else if loadingPreviewA}
<div class="loading-profile">Loading player...</div>
{:else if hasCompared}
<div class="empty-profile">Player A profile not found</div>
{/if}
</div>
{/if}
</div>
{#if $$slots['player-a-card']}
<slot name="player-a-card" />
{:else if hasCompared}
<div class="player-card-wrapper">
{#if playerAProfile}
<PlayerCard
name={playerAProfile.name ?? 'Player A'}
avatar={playerAProfile.avatar ?? null}
country={playerAProfile.country ?? null}
rank={playerAProfile.rank ?? null}
showRank={typeof playerAProfile.rank === 'number'}
width="100%"
avatarSize={56}
techPp={playerAProfile.techPp}
accPp={playerAProfile.accPp}
passPp={playerAProfile.passPp}
playerId={playerAProfile.id ?? null}
gradientId="compare-player-a"
/>
{:else}
<div class="empty-profile">Player A profile not found</div>
{/if}
</div>
{/if}
</div>
<div class="player-column">
@@ -127,31 +235,33 @@
required
/>
</label>
{#if $$slots['player-b-card']}
<slot name="player-b-card" />
{:else if showCardB}
<div class="player-card-wrapper">
{#if displayProfileB}
<PlayerCard
name={displayProfileB.name ?? 'Player B'}
avatar={displayProfileB.avatar ?? null}
country={displayProfileB.country ?? null}
rank={displayProfileB.rank ?? null}
showRank={typeof displayProfileB.rank === 'number'}
width="100%"
avatarSize={56}
techPp={displayProfileB.techPp}
accPp={displayProfileB.accPp}
passPp={displayProfileB.passPp}
playerId={displayProfileB.id ?? null}
gradientId="compare-player-b"
/>
{:else if loadingPreviewB}
<div class="loading-profile">Loading player...</div>
{:else if hasCompared}
<div class="empty-profile">Player B profile not found</div>
{/if}
</div>
{/if}
</div>
{#if $$slots['player-b-card']}
<slot name="player-b-card" />
{:else if hasCompared}
<div class="player-card-wrapper">
{#if playerBProfile}
<PlayerCard
name={playerBProfile.name ?? 'Player B'}
avatar={playerBProfile.avatar ?? null}
country={playerBProfile.country ?? null}
rank={playerBProfile.rank ?? null}
showRank={typeof playerBProfile.rank === 'number'}
width="100%"
avatarSize={56}
techPp={playerBProfile.techPp}
accPp={playerBProfile.accPp}
passPp={playerBProfile.passPp}
playerId={playerBProfile.id ?? null}
gradientId="compare-player-b"
/>
{:else}
<div class="empty-profile">Player B profile not found</div>
{/if}
</div>
{/if}
</div>
</div>
@@ -224,5 +334,23 @@
box-shadow: 0 0 20px rgba(34, 211, 238, 0.35), 0 0 0 2px rgba(34, 211, 238, 0.1);
color: rgba(255, 255, 255, 1);
}
.player-card-wrapper {
margin-top: 1rem;
}
.empty-profile {
padding: 2rem;
text-align: center;
color: rgba(148, 163, 184, 0.7);
font-size: 0.9rem;
}
.loading-profile {
padding: 2rem;
text-align: center;
color: rgba(148, 163, 184, 0.7);
font-size: 0.9rem;
}
</style>