Polish inputs and make re-usable components
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -104,8 +104,8 @@ const DEFAULT_PRIVATE_TOOL_REQUIREMENT: ToolRequirement = {
|
||||
|
||||
export const TOOL_REQUIREMENTS = {
|
||||
'compare-histories': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
|
||||
'beatleader-headtohead': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
|
||||
'beatleader-playlist-gap': DEFAULT_PRIVATE_TOOL_REQUIREMENT
|
||||
'player-headtohead': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
|
||||
'player-playlist-gaps': DEFAULT_PRIVATE_TOOL_REQUIREMENT
|
||||
} as const satisfies Record<string, ToolRequirement>;
|
||||
|
||||
export type ToolKey = keyof typeof TOOL_REQUIREMENTS;
|
||||
@@ -320,6 +320,27 @@ export async function fetchAllRecentScoresAllDiffs(
|
||||
return Array.from(merged.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all scores for a player (no time limit) by paginating through the BeatLeader API
|
||||
* Useful for playlist gap analysis where we need to check all historical plays
|
||||
*/
|
||||
export async function fetchAllPlayerScores(playerId: string, maxPages = 200): Promise<BeatLeaderScore[]> {
|
||||
const pageSize = 100;
|
||||
let page = 1;
|
||||
const all: BeatLeaderScore[] = [];
|
||||
while (page <= maxPages) {
|
||||
const url = `/api/beatleader/player/${encodeURIComponent(playerId)}?scores=1&count=${pageSize}&page=${page}&sortBy=date&order=desc`;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error(`Failed to fetch scores for ${playerId}: ${res.status}`);
|
||||
const data = (await res.json()) as BeatLeaderScoresResponse;
|
||||
const batch = data.data ?? [];
|
||||
if (batch.length === 0) break;
|
||||
all.push(...batch);
|
||||
page += 1;
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 4. Data Processing Helpers
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user