Files
esports/src/lib/components/match/MapHistory.svelte

148 lines
5.6 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
import { slide } from 'svelte/transition';
import { SvelteSet } from 'svelte/reactivity';
import type { MatchLive } from './live.svelte';
import { SIDE_COLORS, formatScore, modColor } from './theme';
import ChevronDown from '@lucide/svelte/icons/chevron-down';
const {
live,
isReferee,
embedded = false
}: { live: MatchLive; isReferee: boolean; embedded?: boolean } = $props();
const view = $derived(live.view);
const open = new SvelteSet<number>();
function name(playerId: string) {
for (const side of view.sides) {
const player = side.players.find((each) => each.id === playerId);
if (player) return player.osuName ?? player.name;
}
return 'Unknown';
}
</script>
{#if view.maps.length}
<section class={embedded ? 'p-2' : 'rounded-2xl border border-white/8 bg-zinc-950/60 p-3 sm:p-4'}>
<h2 class="mb-3 text-sm font-semibold tracking-wide text-zinc-200 uppercase">Maps played</h2>
<ol class="space-y-2">
{#each [...view.maps].reverse() as map (map.index)}
{@const slot = live.slot(map.slotId)}
{@const winner = live.side(map.winner)}
{@const red = map.totals[view.sides[0].id] ?? 0}
{@const blue = map.totals[view.sides[1].id] ?? 0}
{@const share = red + blue > 0 ? red / (red + blue) : 0.5}
<li
class="overflow-hidden rounded-xl border border-l-[3px] border-white/8 bg-white/[0.03]"
style="border-left-color:{slot ? modColor(slot.category) : '#71717a'}"
>
<button
type="button"
class="w-full p-3 text-left transition hover:bg-white/[0.04]"
aria-expanded={open.has(map.index)}
onclick={() => (open.has(map.index) ? open.delete(map.index) : open.add(map.index))}
>
<div class="flex min-w-0 items-center gap-2">
<span class="w-4 shrink-0 text-center text-[11px] text-zinc-500 tabular-nums"
>{map.index + 1}</span
>
<span
class="shrink-0 font-mono text-xs font-black"
style="color:{slot ? modColor(slot.category) : '#fff'}">{slot?.label ?? 'Map'}</span
>
<span class="min-w-0 flex-1 truncate text-sm font-medium"
>{slot?.title ?? `Beatmap #${slot?.beatmapId ?? ''}`}</span
>
<ChevronDown
class="size-4 shrink-0 text-zinc-500 transition {open.has(map.index)
? 'rotate-180'
: ''}"
/>
</div>
<div class="mt-2 flex items-center justify-between gap-2 pl-6 text-xs">
<span
class="min-w-0 truncate font-semibold"
style="color:{winner ? SIDE_COLORS[winner.color].text : '#a1a1aa'}"
>{winner?.name ?? 'Draw'}{winner ? ' won' : ''}</span
>
<span class="shrink-0 font-mono text-[11px] tabular-nums"
><span style="color:{SIDE_COLORS.red.text}"
>{formatScore(red, view.rules.winCondition)}</span
><span class="px-1 text-zinc-600">–</span><span
style="color:{SIDE_COLORS.blue.text}"
>{formatScore(blue, view.rules.winCondition)}</span
></span
>
</div>
<div class="mt-2 ml-6 flex h-1.5 overflow-hidden rounded-full bg-white/5">
<div style="width:{share * 100}%;background:{SIDE_COLORS.red.solid}"></div>
<div class="flex-1" style="background:{SIDE_COLORS.blue.solid}"></div>
</div>
</button>
{#if open.has(map.index)}
<div transition:slide={{ duration: 180 }} class="border-t border-white/5 px-3 py-2">
{#if map.scores.length}
<p class="mb-1 text-[10px] font-semibold tracking-wide text-zinc-500 uppercase">
Player scores
</p>
{#each map.scores as score (score.playerId)}
{@const side = live.side(score.side)}
<div class="flex items-center gap-2 border-t border-white/5 py-1.5 text-xs">
<span
class="size-2 shrink-0 rounded-full"
style="background:{side ? SIDE_COLORS[side.color].solid : '#71717a'}"
></span>
<div class="min-w-0 flex-1">
<a
href="/users/{score.userId}"
class="block truncate font-medium hover:underline">{name(score.playerId)}</a
><span class="text-[10px] text-zinc-500"
>{score.accuracy ? `${score.accuracy.toFixed(2)}%` : '–'} · {score.combo
? `${score.combo}x`
: '–'} · {score.mods.filter((mod) => mod !== 'NF').join('') ||
'NM'}{#if !score.passed}
· <span class="text-red-400">FAIL</span>{/if}</span
>
</div>
<span class="shrink-0 font-mono font-semibold tabular-nums"
>{score.score.toLocaleString('en-US')}</span
>
</div>
{/each}
{:else}
<p class="text-xs text-zinc-500">
No per-player scores were recorded for this map.
</p>
{/if}
{#if isReferee}
<div
class="mt-2 flex flex-wrap items-center gap-1.5 border-t border-white/5 pt-2 text-xs"
>
<span class="w-full text-zinc-500">Correct winner:</span>
{#each view.sides as side (side.id)}
<button
type="button"
class="rounded border border-white/10 px-2 py-0.5 hover:bg-white/10"
style="color:{SIDE_COLORS[side.color].text}"
onclick={() =>
live.send({ type: 'correct', mapIndex: map.index, winner: side.id })}
>{side.name}</button
>
{/each}
<button
type="button"
class="rounded border border-white/10 px-2 py-0.5 text-zinc-400 hover:bg-white/10"
onclick={() =>
live.send({ type: 'correct', mapIndex: map.index, winner: null })}
>Draw</button
>
</div>
{/if}
</div>
{/if}
</li>
{/each}
</ol>
</section>
{/if}