- PlayerSearch: typeahead with 200ms debounce, 48px touch targets, recently active when empty - BuyInFlow: 3-step wizard (search -> auto-seat preview -> confirm) with mini table diagram - BustOutFlow: minimal-tap flow (table grid -> seat tap -> verify -> hitman select) - PlayerDetail: full per-player tracking (status, chips, financials, history, undo buttons) - RebuyFlow: quick 2-tap flow with pre-selected player support - AddOnFlow: quick flow with mass add-on all option - Players page: Active/Busted/All tabs, DataTable with search, swipe actions, overlay flows - Layout: FAB actions wired to actual flows, clock pause/resume via API Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
332 lines
7.8 KiB
Svelte
332 lines
7.8 KiB
Svelte
<script lang="ts">
|
|
import { tournament } from '$lib/stores/tournament.svelte';
|
|
import type { Player } from '$lib/stores/tournament.svelte';
|
|
import { toast } from '$lib/stores/toast.svelte';
|
|
import { api } from '$lib/api';
|
|
import PlayerSearch from './PlayerSearch.svelte';
|
|
|
|
/**
|
|
* Add-On Flow — quick flow similar to Rebuy.
|
|
*
|
|
* Features:
|
|
* - Show only during add-on window
|
|
* - "Add-On All" option for break-time mass add-ons
|
|
* - Quick flow: select player + confirm
|
|
*/
|
|
|
|
interface Props {
|
|
/** Close callback. */
|
|
onclose: () => void;
|
|
/** Pre-selected player (from player detail). */
|
|
preselectedPlayer?: Player | null;
|
|
}
|
|
|
|
let { onclose, preselectedPlayer = null }: Props = $props();
|
|
|
|
type Step = 'select' | 'confirm';
|
|
let step = $state<Step>('select');
|
|
let selectedPlayer = $state<Player | null>(null);
|
|
|
|
// Initialize from preselectedPlayer prop
|
|
$effect(() => {
|
|
if (preselectedPlayer) {
|
|
selectedPlayer = preselectedPlayer;
|
|
step = 'confirm';
|
|
}
|
|
});
|
|
let submitting = $state(false);
|
|
|
|
// Add-on config (simplified — real config from tournament)
|
|
let addonAmount = 50;
|
|
let addonChips = 15000;
|
|
|
|
/** Eligible: active players only. */
|
|
function isEligible(player: Player): boolean {
|
|
return player.status === 'active';
|
|
}
|
|
|
|
function handleSelect(player: Player): void {
|
|
selectedPlayer = player;
|
|
step = 'confirm';
|
|
}
|
|
|
|
async function confirmAddon(): Promise<void> {
|
|
if (!selectedPlayer) return;
|
|
submitting = true;
|
|
|
|
try {
|
|
await api.post(`/tournaments/${tournament.id}/addon`, {
|
|
player_id: selectedPlayer.id
|
|
});
|
|
toast.success(`${selectedPlayer.name} add-on — +${addonChips.toLocaleString()} chips`);
|
|
onclose();
|
|
} catch (err) {
|
|
toast.error(`Add-on failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
}
|
|
|
|
async function addonAll(): Promise<void> {
|
|
submitting = true;
|
|
try {
|
|
const activePlayers = tournament.players.filter((p) => p.status === 'active');
|
|
let count = 0;
|
|
for (const player of activePlayers) {
|
|
await api.post(`/tournaments/${tournament.id}/addon`, {
|
|
player_id: player.id
|
|
});
|
|
count++;
|
|
}
|
|
toast.success(`Add-on applied to ${count} players`);
|
|
onclose();
|
|
} catch (err) {
|
|
toast.error(`Mass add-on failed: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
}
|
|
|
|
function goBack(): void {
|
|
if (step === 'confirm' && !preselectedPlayer) {
|
|
step = 'select';
|
|
selectedPlayer = null;
|
|
} else {
|
|
onclose();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="addon-flow">
|
|
<!-- Header -->
|
|
<div class="flow-header">
|
|
<button class="back-btn touch-target" onclick={goBack} aria-label="Go back">
|
|
<span aria-hidden="true">←</span>
|
|
</button>
|
|
<h2 class="flow-title">Add-On</h2>
|
|
<button class="close-btn touch-target" onclick={onclose} aria-label="Close">
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flow-content">
|
|
{#if step === 'select'}
|
|
<h3 class="step-title">Select Player</h3>
|
|
<PlayerSearch
|
|
players={tournament.players}
|
|
onselect={handleSelect}
|
|
filter={isEligible}
|
|
placeholder="Search eligible players..."
|
|
autofocus={true}
|
|
/>
|
|
|
|
<!-- Add-On All button -->
|
|
<div class="mass-addon">
|
|
<button
|
|
class="btn-addon-all touch-target"
|
|
onclick={addonAll}
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? 'Processing...' : 'Add-On All Active Players'}
|
|
</button>
|
|
<p class="mass-addon-hint">
|
|
Applies add-on to all {tournament.players.filter((p) => p.status === 'active').length} active players
|
|
</p>
|
|
</div>
|
|
{:else if step === 'confirm'}
|
|
<div class="confirm-card">
|
|
<h3 class="confirm-name">{selectedPlayer?.name}</h3>
|
|
<div class="confirm-details">
|
|
<div class="confirm-row">
|
|
<span class="confirm-label">Current Chips</span>
|
|
<span class="confirm-value chips">{selectedPlayer?.chips.toLocaleString()}</span>
|
|
</div>
|
|
<div class="confirm-row">
|
|
<span class="confirm-label">Add-On Amount</span>
|
|
<span class="confirm-value currency">{addonAmount.toLocaleString()}</span>
|
|
</div>
|
|
<div class="confirm-row">
|
|
<span class="confirm-label">Chips Added</span>
|
|
<span class="confirm-value chips">+{addonChips.toLocaleString()}</span>
|
|
</div>
|
|
<div class="confirm-row">
|
|
<span class="confirm-label">Add-Ons So Far</span>
|
|
<span class="confirm-value number">{selectedPlayer?.addons ?? 0}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
class="btn-confirm touch-target"
|
|
onclick={confirmAddon}
|
|
disabled={submitting}
|
|
>
|
|
{submitting ? 'Processing...' : 'Confirm Add-On'}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.addon-flow {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
background-color: var(--color-bg);
|
|
}
|
|
|
|
.flow-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--space-3) var(--space-4);
|
|
border-bottom: 1px solid var(--color-border);
|
|
background-color: var(--color-bg-elevated);
|
|
}
|
|
|
|
.flow-title {
|
|
font-size: var(--text-lg);
|
|
font-weight: 700;
|
|
color: var(--color-warning);
|
|
}
|
|
|
|
.back-btn, .close-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 40px;
|
|
height: 40px;
|
|
background: none;
|
|
border: none;
|
|
color: var(--color-text);
|
|
font-size: var(--text-xl);
|
|
cursor: pointer;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.back-btn:hover, .close-btn:hover {
|
|
background-color: var(--color-surface);
|
|
}
|
|
|
|
.flow-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--space-4);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-6);
|
|
}
|
|
|
|
.step-title {
|
|
font-size: var(--text-lg);
|
|
font-weight: 600;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
/* Mass add-on */
|
|
.mass-addon {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
align-items: center;
|
|
padding-top: var(--space-4);
|
|
border-top: 1px solid var(--color-border);
|
|
}
|
|
|
|
.btn-addon-all {
|
|
width: 100%;
|
|
padding: var(--space-3) var(--space-6);
|
|
font-size: var(--text-base);
|
|
font-weight: 600;
|
|
background-color: var(--color-warning);
|
|
color: var(--color-bg);
|
|
border: none;
|
|
border-radius: var(--radius-lg);
|
|
cursor: pointer;
|
|
transition: background-color var(--transition-fast), opacity var(--transition-fast);
|
|
}
|
|
|
|
.btn-addon-all:hover:not(:disabled) {
|
|
background-color: color-mix(in srgb, var(--color-warning) 85%, white);
|
|
}
|
|
|
|
.btn-addon-all:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.mass-addon-hint {
|
|
font-size: var(--text-xs);
|
|
color: var(--color-text-muted);
|
|
text-align: center;
|
|
}
|
|
|
|
/* Confirm card */
|
|
.confirm-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-4);
|
|
align-items: center;
|
|
padding-top: var(--space-4);
|
|
}
|
|
|
|
.confirm-name {
|
|
font-size: var(--text-2xl);
|
|
font-weight: 700;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.confirm-details {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background-color: var(--color-surface);
|
|
border-radius: var(--radius-lg);
|
|
border: 1px solid var(--color-border);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.confirm-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: var(--space-3) var(--space-4);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
.confirm-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.confirm-label {
|
|
font-size: var(--text-sm);
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
.confirm-value {
|
|
font-weight: 600;
|
|
color: var(--color-text);
|
|
}
|
|
|
|
.btn-confirm {
|
|
width: 100%;
|
|
padding: var(--space-4) var(--space-6);
|
|
font-size: var(--text-lg);
|
|
font-weight: 700;
|
|
background-color: var(--color-warning);
|
|
color: var(--color-bg);
|
|
border: none;
|
|
border-radius: var(--radius-lg);
|
|
cursor: pointer;
|
|
transition: background-color var(--transition-fast), opacity var(--transition-fast);
|
|
}
|
|
|
|
.btn-confirm:hover:not(:disabled) {
|
|
background-color: color-mix(in srgb, var(--color-warning) 85%, white);
|
|
}
|
|
|
|
.btn-confirm:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
</style>
|