add player card to compare player form
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
formatToolRequirementSummary,
|
||||
meetsToolRequirement,
|
||||
DEFAULT_ADMIN_RANK_FALLBACK,
|
||||
PLEB_BEATLEADER_ID,
|
||||
@@ -13,14 +12,12 @@
|
||||
|
||||
export let player: BeatLeaderPlayerProfile | null = null;
|
||||
export let requirement: ToolRequirement | null = null;
|
||||
export let customLockedMessage: string | null = null;
|
||||
export let showCurrentRank = true;
|
||||
export let adminRank: number | null = null;
|
||||
export let adminPlayer: BeatLeaderPlayerProfile | null = null;
|
||||
|
||||
$: requirementContext = { adminRank };
|
||||
$: hasAccess = meetsToolRequirement(player, requirement, requirementContext);
|
||||
$: summary = formatToolRequirementSummary(requirement, requirementContext);
|
||||
$: fallbackBaseline = DEFAULT_ADMIN_RANK_FALLBACK;
|
||||
$: resolvedBaseline =
|
||||
typeof adminRank === 'number' && Number.isFinite(adminRank) && adminRank > 0 ? adminRank : fallbackBaseline;
|
||||
@@ -29,11 +26,6 @@
|
||||
$: baselineCopy = resolvedBaseline
|
||||
? `players ranked better than ${plebName} (#${resolvedBaseline.toLocaleString()})`
|
||||
: `players ranked better than ${plebName}`;
|
||||
$: defaultLockedMessage = requirement?.requiresBetterRankThanAdmin
|
||||
? `You must be a BL Patreon supporter or ${baselineCopy} to use this tool.`
|
||||
: requirement?.lockedMessage ?? null;
|
||||
$: lockedMessage = customLockedMessage ?? defaultLockedMessage;
|
||||
$: showLockedMessage = lockedMessage && lockedMessage !== summary ? lockedMessage : null;
|
||||
$: playerRank = typeof player?.rank === 'number' ? player?.rank ?? null : null;
|
||||
$: playerRankDisplay = playerRank !== null ? `#${playerRank.toLocaleString()}` : null;
|
||||
</script>
|
||||
@@ -43,14 +35,8 @@
|
||||
{:else}
|
||||
<div class="mt-6 rounded-md border border-amber-500/40 bg-amber-500/10 p-4 text-sm text-amber-100 leading-relaxed">
|
||||
<p class="font-semibold text-amber-200">
|
||||
Tools are restricted to <a class="underline hover:text-white" href={BL_PATREON_URL} target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> (and {baselineCopy}).
|
||||
Auth Required: Tools are restricted to <a class="underline hover:text-white" href={BL_PATREON_URL} target="_blank" rel="noreferrer noopener">BeatLeader supporters</a> (and {baselineCopy}).
|
||||
</p>
|
||||
{#if summary}
|
||||
<p class="mt-1">{summary}</p>
|
||||
{/if}
|
||||
{#if showLockedMessage}
|
||||
<p class="mt-1">{showLockedMessage}</p>
|
||||
{/if}
|
||||
{#if plebProfile}
|
||||
<div class="pleb-card">
|
||||
<PlayerCard
|
||||
@@ -63,6 +49,7 @@
|
||||
techPp={plebProfile?.techPp}
|
||||
accPp={plebProfile?.accPp}
|
||||
passPp={plebProfile?.passPp}
|
||||
playerId={plebProfile?.id ?? PLEB_BEATLEADER_ID}
|
||||
gradientId="pleb-baseline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { dev } from '$app/environment';
|
||||
import beatleaderLogo from '$lib/assets/beatleader-logo.png';
|
||||
import { DEFAULT_ADMIN_RANK_FALLBACK, PLEB_BEATLEADER_ID } from '$lib/utils/plebsaber-utils';
|
||||
const links = [
|
||||
@@ -134,10 +133,11 @@
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-14 items-center justify-between">
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<span class="h-2 w-2 rounded-full bg-neon" style="box-shadow: 0 0 12px rgba(34,211,238,0.60);"></span>
|
||||
<span class="font-display text-lg tracking-widest">
|
||||
<span class="neon-text">PLEBSABER</span><span class="text-muted">.stream</span>
|
||||
<span class="neon-text">plebsaber</span><span class="text-muted">.stream</span>
|
||||
</span>
|
||||
<span class="h-2 w-2 rounded-full bg-neon-fuchsia" style="box-shadow: 0 0 12px rgba(255,0,229,0.60);"></span>
|
||||
<span class="h-2 w-2 rounded-full bg-neon" style="box-shadow: 0 0 12px rgba(34,211,238,0.60);"></span>
|
||||
</a>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-6">
|
||||
@@ -176,9 +176,7 @@
|
||||
{#if user?.id === PLEB_BEATLEADER_ID}
|
||||
<a href="/tools/stats" class="dropdown-item" on:click={closeMenu}>Stats</a>
|
||||
{/if}
|
||||
{#if dev}
|
||||
<a href="/testing" class="dropdown-item" on:click={closeMenu}>Testing</a>
|
||||
{/if}
|
||||
<a href="/player-info" class="dropdown-item" on:click={closeMenu}>Player Info</a>
|
||||
<a href={getProfileUrl(user.id)} class="dropdown-item dropdown-item--with-icon" target="_blank" rel="noreferrer noopener" on:click={closeMenu}>
|
||||
<span>Profile</span>
|
||||
<img src={beatleaderLogo} alt="BeatLeader" class="dropdown-icon" />
|
||||
@@ -222,9 +220,7 @@
|
||||
{#if user?.id === PLEB_BEATLEADER_ID}
|
||||
<a href="/tools/stats" class="dropdown-item" on:click={close}>Stats</a>
|
||||
{/if}
|
||||
{#if dev}
|
||||
<a href="/testing" class="dropdown-item" on:click={close}>Testing</a>
|
||||
{/if}
|
||||
<a href="/player-info" class="dropdown-item" on:click={close}>Player Info</a>
|
||||
<a href={getProfileUrl(user.id)} class="dropdown-item dropdown-item--with-icon" target="_blank" rel="noreferrer noopener" on:click={close}>
|
||||
<span>Profile</span>
|
||||
<img src={beatleaderLogo} alt="BeatLeader" class="dropdown-icon" />
|
||||
|
||||
@@ -13,61 +13,110 @@
|
||||
export let passPp: number | null | undefined = null;
|
||||
// gradientId must be unique per instance to avoid SVG gradient conflicts when multiple cards render on the same page
|
||||
export let gradientId: string = 'summary-triangle';
|
||||
export let playerId: string | null = null;
|
||||
|
||||
$: triangle = calculateSkillTriangle(techPp, accPp, passPp);
|
||||
$: triangleCorners = triangle?.corners;
|
||||
$: normalized = triangle?.normalized;
|
||||
$: hasTriangle = triangle !== null;
|
||||
$: profileUrl = playerId ? `https://beatleader.com/u/${encodeURIComponent(playerId)}` : null;
|
||||
|
||||
const gradientTechId = `${gradientId}-tech`;
|
||||
const gradientAccId = `${gradientId}-acc`;
|
||||
const gradientPassId = `${gradientId}-pass`;
|
||||
</script>
|
||||
|
||||
<div class="summary-header" style={`--header-width:${width}; --avatar-size:${avatarSize}px`}>
|
||||
<img src={avatar ?? ''} alt="Avatar" class:placeholder={!avatar} />
|
||||
<div class="summary-body">
|
||||
<div class="summary-name">{name}</div>
|
||||
<div class="summary-meta">
|
||||
{#if country}
|
||||
<span>{country}</span>
|
||||
{/if}
|
||||
{#if showRank && typeof rank === 'number'}
|
||||
<span>Rank: {rank}</span>
|
||||
{/if}
|
||||
{#if profileUrl}
|
||||
<a href={profileUrl} target="_blank" rel="noreferrer noopener" class="summary-header clickable" style={`--header-width:${width}; --avatar-size:${avatarSize}px`}>
|
||||
<img src={avatar ?? ''} alt="Avatar" class:placeholder={!avatar} />
|
||||
<div class="summary-body">
|
||||
<div class="summary-name">{name}</div>
|
||||
<div class="summary-meta">
|
||||
{#if country}
|
||||
<span>{country}</span>
|
||||
{/if}
|
||||
{#if showRank && typeof rank === 'number'}
|
||||
<span>Rank: {rank}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if hasTriangle}
|
||||
<div class="summary-triangle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" width="100%" height="100%" viewBox="0 0 100 86.6">
|
||||
<g transform="matrix(1 0 0 -1 0 86.6)">
|
||||
<defs>
|
||||
<linearGradient id={gradientTechId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.tech.x} y1={triangleCorners!.tech.y} x2={(triangleCorners!.acc.x + triangleCorners!.pass.x) / 2} y2={(triangleCorners!.acc.y + triangleCorners!.pass.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(255 0 120 / ${(normalized!.tech ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(255 0 120 / ${(normalized!.tech ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
<linearGradient id={gradientAccId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.acc.x} y1={triangleCorners!.acc.y} x2={(triangleCorners!.pass.x + triangleCorners!.tech.x) / 2} y2={(triangleCorners!.pass.y + triangleCorners!.tech.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(0 180 255 / ${(normalized!.acc ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(0 180 255 / ${(normalized!.acc ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
<linearGradient id={gradientPassId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.pass.x} y1={triangleCorners!.pass.y} x2={(triangleCorners!.tech.x + triangleCorners!.acc.x) / 2} y2={(triangleCorners!.tech.y + triangleCorners!.acc.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(0 255 180 / ${(normalized!.pass ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(0 255 180 / ${(normalized!.pass ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="#fff" stroke-width="0.5">
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientTechId})`} />
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientPassId})`} />
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientAccId})`} />
|
||||
</g>
|
||||
<g stroke="#fff" fill="none" stroke-width="2" stroke-dasharray="6 6">
|
||||
<path d="M 50,0 L 0,86.6 100,86.6 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<div class="summary-header" style={`--header-width:${width}; --avatar-size:${avatarSize}px`}>
|
||||
<img src={avatar ?? ''} alt="Avatar" class:placeholder={!avatar} />
|
||||
<div class="summary-body">
|
||||
<div class="summary-name">{name}</div>
|
||||
<div class="summary-meta">
|
||||
{#if country}
|
||||
<span>{country}</span>
|
||||
{/if}
|
||||
{#if showRank && typeof rank === 'number'}
|
||||
<span>Rank: {rank}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if hasTriangle}
|
||||
<div class="summary-triangle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" width="100%" height="100%" viewBox="0 0 100 86.6">
|
||||
<g transform="matrix(1 0 0 -1 0 86.6)">
|
||||
<defs>
|
||||
<linearGradient id={gradientTechId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.tech.x} y1={triangleCorners!.tech.y} x2={(triangleCorners!.acc.x + triangleCorners!.pass.x) / 2} y2={(triangleCorners!.acc.y + triangleCorners!.pass.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(255 0 120 / ${(normalized!.tech ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(255 0 120 / ${(normalized!.tech ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
<linearGradient id={gradientAccId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.acc.x} y1={triangleCorners!.acc.y} x2={(triangleCorners!.pass.x + triangleCorners!.tech.x) / 2} y2={(triangleCorners!.pass.y + triangleCorners!.tech.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(0 180 255 / ${(normalized!.acc ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(0 180 255 / ${(normalized!.acc ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
<linearGradient id={gradientPassId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.pass.x} y1={triangleCorners!.pass.y} x2={(triangleCorners!.tech.x + triangleCorners!.acc.x) / 2} y2={(triangleCorners!.tech.y + triangleCorners!.acc.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(0 255 180 / ${(normalized!.pass ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(0 255 180 / ${(normalized!.pass ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="#fff" stroke-width="0.5">
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientTechId})`} />
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientPassId})`} />
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientAccId})`} />
|
||||
</g>
|
||||
<g stroke="#fff" fill="none" stroke-width="2" stroke-dasharray="6 6">
|
||||
<path d="M 50,0 L 0,86.6 100,86.6 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if hasTriangle}
|
||||
<div class="summary-triangle">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.2" width="100%" height="100%" viewBox="0 0 100 86.6">
|
||||
<g transform="matrix(1 0 0 -1 0 86.6)">
|
||||
<defs>
|
||||
<linearGradient id={gradientTechId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.tech.x} y1={triangleCorners!.tech.y} x2={(triangleCorners!.acc.x + triangleCorners!.pass.x) / 2} y2={(triangleCorners!.acc.y + triangleCorners!.pass.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(255 0 120 / ${(normalized!.tech ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(255 0 120 / ${(normalized!.tech ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
<linearGradient id={gradientAccId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.acc.x} y1={triangleCorners!.acc.y} x2={(triangleCorners!.pass.x + triangleCorners!.tech.x) / 2} y2={(triangleCorners!.pass.y + triangleCorners!.tech.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(0 180 255 / ${(normalized!.acc ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(0 180 255 / ${(normalized!.acc ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
<linearGradient id={gradientPassId} gradientUnits="userSpaceOnUse" x1={triangleCorners!.pass.x} y1={triangleCorners!.pass.y} x2={(triangleCorners!.tech.x + triangleCorners!.acc.x) / 2} y2={(triangleCorners!.tech.y + triangleCorners!.acc.y) / 2}>
|
||||
<stop offset="0%" stop-color={`rgb(0 255 180 / ${(normalized!.pass ?? 0) * 100}%)`} />
|
||||
<stop offset="100%" stop-color={`rgb(0 255 180 / ${(normalized!.pass ?? 0) * 25}%)`} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g stroke="#fff" stroke-width="0.5">
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientTechId})`} />
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientPassId})`} />
|
||||
<path d={`M ${triangleCorners!.pass.x},${triangleCorners!.pass.y} L ${triangleCorners!.tech.x},${triangleCorners!.tech.y} ${triangleCorners!.acc.x},${triangleCorners!.acc.y} Z`} fill={`url(#${gradientAccId})`} />
|
||||
</g>
|
||||
<g stroke="#fff" fill="none" stroke-width="2" stroke-dasharray="6 6">
|
||||
<path d="M 50,0 L 0,86.6 100,86.6 Z" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.summary-header {
|
||||
@@ -78,6 +127,8 @@
|
||||
max-width: var(--header-width, 50%);
|
||||
min-width: 200px;
|
||||
flex: 0 0 var(--header-width, 50%);
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -127,5 +178,19 @@
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 4px 12px rgba(34, 211, 238, 0.25));
|
||||
}
|
||||
|
||||
.summary-header.clickable {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.summary-header.clickable:hover {
|
||||
transform: translateY(-2px);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.summary-header.clickable:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import PlayerCard from '$lib/components/PlayerCard.svelte';
|
||||
import type { BeatLeaderPlayerProfile } from '$lib/utils/plebsaber-utils';
|
||||
|
||||
export let playerA = '';
|
||||
export let playerB = '';
|
||||
@@ -10,6 +12,11 @@
|
||||
|
||||
let initialized = false;
|
||||
|
||||
// Local state for default player cards
|
||||
let hasCompared = false;
|
||||
let playerAProfile: BeatLeaderPlayerProfile | null = null;
|
||||
let playerBProfile: BeatLeaderPlayerProfile | null = null;
|
||||
|
||||
// Load from URL params on mount
|
||||
onMount(() => {
|
||||
if (browser) {
|
||||
@@ -38,34 +45,117 @@
|
||||
history.replaceState(null, '', url);
|
||||
}
|
||||
|
||||
function handleSubmit(e: Event) {
|
||||
async function fetchPlayerProfile(playerId: string): Promise<BeatLeaderPlayerProfile | null> {
|
||||
try {
|
||||
const res = await fetch(`https://api.beatleader.xyz/player/${encodeURIComponent(playerId)}`);
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as BeatLeaderPlayerProfile;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDefaultPlayerCards(a: string, b: string): Promise<void> {
|
||||
playerAProfile = null;
|
||||
playerBProfile = null;
|
||||
if (!a || !b) return;
|
||||
const [pa, pb] = await Promise.all([
|
||||
fetchPlayerProfile(a),
|
||||
fetchPlayerProfile(b)
|
||||
]);
|
||||
playerAProfile = pa;
|
||||
playerBProfile = pb;
|
||||
}
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
hasCompared = true;
|
||||
const a = playerA.trim();
|
||||
const b = playerB.trim();
|
||||
// Fire and forget: show default cards irrespective of parent page implementation
|
||||
void loadDefaultPlayerCards(a, b);
|
||||
oncompare?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="mt-6 grid gap-4 sm:grid-cols-3 items-end" on:submit={handleSubmit}>
|
||||
<div>
|
||||
<label class="block text-sm text-muted">Player A ID
|
||||
<input
|
||||
class="mt-1 w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none {hasResults ? 'border-white/10' : 'neon-input'}"
|
||||
bind:value={playerA}
|
||||
placeholder="7656119... or BL ID"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<form class="mt-6" on:submit={handleSubmit}>
|
||||
<div class="form-grid">
|
||||
<div class="player-column">
|
||||
<div class="input-tile">
|
||||
<label class="block text-sm text-muted">Player A ID
|
||||
<input
|
||||
class="mt-1 w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none {hasResults ? 'border-white/10' : 'neon-input'}"
|
||||
bind:value={playerA}
|
||||
placeholder="7656119... or BL ID"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</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">
|
||||
<div class="input-tile">
|
||||
<label class="block text-sm text-muted">Player B ID
|
||||
<input
|
||||
class="mt-1 w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none {hasResults ? 'border-white/10' : 'neon-input'}"
|
||||
bind:value={playerB}
|
||||
placeholder="7656119... or BL ID"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</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>
|
||||
<div>
|
||||
<label class="block text-sm text-muted">Player B ID
|
||||
<input
|
||||
class="mt-1 w-full rounded-md border bg-transparent px-3 py-2 text-sm outline-none {hasResults ? 'border-white/10' : 'neon-input'}"
|
||||
bind:value={playerB}
|
||||
placeholder="7656119... or BL ID"
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="btn-neon" type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
Loading...
|
||||
@@ -78,6 +168,44 @@
|
||||
</form>
|
||||
|
||||
<style>
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.player-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.input-tile {
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.25rem;
|
||||
background: linear-gradient(160deg, rgba(15, 23, 42, 0.6), rgba(8, 12, 24, 0.85));
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.input-tile:hover {
|
||||
border-color: rgba(148, 163, 184, 0.3);
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.neon-input {
|
||||
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));
|
||||
|
||||
@@ -103,7 +103,7 @@ const DEFAULT_PRIVATE_TOOL_REQUIREMENT: ToolRequirement = {
|
||||
};
|
||||
|
||||
export const TOOL_REQUIREMENTS = {
|
||||
'beatleader-compare': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
|
||||
'compare-histories': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
|
||||
'beatleader-headtohead': DEFAULT_PRIVATE_TOOL_REQUIREMENT,
|
||||
'beatleader-playlist-gap': DEFAULT_PRIVATE_TOOL_REQUIREMENT
|
||||
} as const satisfies Record<string, ToolRequirement>;
|
||||
|
||||
Reference in New Issue
Block a user