From 44b555db10e65c542347592d7b4f0f9861c525a4 Mon Sep 17 00:00:00 2001 From: Mikkel Georgsen Date: Sun, 1 Mar 2026 08:23:09 +0100 Subject: [PATCH] feat(01-12): implement Players tab with buy-in, bust-out, rebuy, add-on flows - 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 --- frontend/src/lib/components/AddOnFlow.svelte | 332 +++++++++ .../src/lib/components/BustOutFlow.svelte | 629 +++++++++++++++++ frontend/src/lib/components/BuyInFlow.svelte | 643 ++++++++++++++++++ .../src/lib/components/PlayerDetail.svelte | 443 ++++++++++++ .../src/lib/components/PlayerSearch.svelte | 301 ++++++++ frontend/src/lib/components/RebuyFlow.svelte | 258 +++++++ frontend/src/routes/+layout.svelte | 70 +- frontend/src/routes/players/+page.svelte | 364 +++++++++- 8 files changed, 3017 insertions(+), 23 deletions(-) create mode 100644 frontend/src/lib/components/AddOnFlow.svelte create mode 100644 frontend/src/lib/components/BustOutFlow.svelte create mode 100644 frontend/src/lib/components/BuyInFlow.svelte create mode 100644 frontend/src/lib/components/PlayerDetail.svelte create mode 100644 frontend/src/lib/components/PlayerSearch.svelte create mode 100644 frontend/src/lib/components/RebuyFlow.svelte diff --git a/frontend/src/lib/components/AddOnFlow.svelte b/frontend/src/lib/components/AddOnFlow.svelte new file mode 100644 index 0000000..ac935ae --- /dev/null +++ b/frontend/src/lib/components/AddOnFlow.svelte @@ -0,0 +1,332 @@ + + +
+ +
+ +

Add-On

+ +
+ +
+ {#if step === 'select'} +

Select Player

+ + + +
+ +

+ Applies add-on to all {tournament.players.filter((p) => p.status === 'active').length} active players +

+
+ {:else if step === 'confirm'} +
+

{selectedPlayer?.name}

+
+
+ Current Chips + {selectedPlayer?.chips.toLocaleString()} +
+
+ Add-On Amount + {addonAmount.toLocaleString()} +
+
+ Chips Added + +{addonChips.toLocaleString()} +
+
+ Add-Ons So Far + {selectedPlayer?.addons ?? 0} +
+
+ + +
+ {/if} +
+
+ + diff --git a/frontend/src/lib/components/BustOutFlow.svelte b/frontend/src/lib/components/BustOutFlow.svelte new file mode 100644 index 0000000..bd1615b --- /dev/null +++ b/frontend/src/lib/components/BustOutFlow.svelte @@ -0,0 +1,629 @@ + + +
+ +
+ +

Bust Out

+ +
+ + +
+ {#if step === 'table'} + +

Select Table

+
+ {#each activeTables as table (table.id)} + + {/each} +
+ + {#if activeTables.length === 0} +

No active tables with players.

+ {/if} + + {:else if step === 'seat'} + +

Table {selectedTable?.number} — Tap the busted player

+ + {#if selectedTable} + {@const seatLayout = getSeatLayout(selectedTable)} +
+
+ Table {selectedTable.number} +
+
+ {#each seatLayout as { seat, player }} + {#if player} + + {:else} +
+ {seat} +
+ {/if} + {/each} +
+
+ {/if} + + {:else if step === 'verify'} + +
+
+ {playerInitials(selectedPlayer?.name ?? '?')} +
+

Bust {selectedPlayer?.name}?

+
+ Table {selectedTable?.number}, Seat {selectedPlayer?.seat} + {#if selectedPlayer?.chips} + {selectedPlayer.chips.toLocaleString()} chips + {/if} +
+ + +
+ + {:else if step === 'hitman'} + +

Who busted {selectedPlayer?.name}?

+ +
+ {#each hitmanCandidates as candidate (candidate.id)} + + {/each} +
+ +
+ {#if !showAllPlayers} + + {/if} + +
+ {/if} +
+
+ + diff --git a/frontend/src/lib/components/BuyInFlow.svelte b/frontend/src/lib/components/BuyInFlow.svelte new file mode 100644 index 0000000..7eb54d0 --- /dev/null +++ b/frontend/src/lib/components/BuyInFlow.svelte @@ -0,0 +1,643 @@ + + +
+ +
+ +

Buy In

+ +
+ + +
+
1
+
+
2
+
+
3
+
+ + +
+ {#if step === 'search'} + +
+

Select Player

+ p.status !== 'active'} + placeholder="Search by name..." + autofocus={true} + /> +
+ + {:else if step === 'seat'} + +
+

Seat Assignment

+ + {#if suggestedSeat && !overrideMode} +
+ Suggested Seat +
+ Table {suggestedSeat.table_number}, Seat {suggestedSeat.seat} +
+ + + {#if suggestedSeat} + {@const tableData = tournament.tables.find((t) => t.id === suggestedSeat!.table_id)} + {#if tableData} +
+
+ {#each getTablePlayers(tableData.id, tableData.seats) as seatData} +
+ {seatData.seat} +
+ {/each} +
+
+ {/if} + {/if} + +
+ + +
+
+ {:else if overrideMode} +
+

Select a different seat:

+ {#if availableSeats.length === 0} +

No available seats

+ {:else} + {#each availableSeats as seat} + + {/each} + {/if} + +
+ {:else} +
+

No tables available. Create a table first.

+ +
+ {/if} +
+ + {:else if step === 'confirm'} + +
+

Confirm Buy-In

+ +
+
+ Player + {selectedPlayer?.name} +
+
+ Table + Table {selectedSeat?.table_number} +
+
+ Seat + Seat {selectedSeat?.seat} +
+
+ Buy-In + {buyinAmount.toLocaleString()} +
+
+ + +
+ {/if} +
+
+ + diff --git a/frontend/src/lib/components/PlayerDetail.svelte b/frontend/src/lib/components/PlayerDetail.svelte new file mode 100644 index 0000000..228e2e6 --- /dev/null +++ b/frontend/src/lib/components/PlayerDetail.svelte @@ -0,0 +1,443 @@ + + +
+ +
+ +

{player.name}

+
+ {player.status} +
+
+ +
+ +
+
+
+ Status + {player.status} +
+
+ Location + {tableName}, {seatDisplay} +
+
+ Chips + {player.chips.toLocaleString()} +
+ {#if player.finish_position} +
+ Finish + {ordinal(player.finish_position)} place +
+ {/if} +
+
+ + +
+

Financial

+
+
+ Rebuys + {player.rebuys} +
+
+ Add-ons + {player.addons} +
+
+ Total Investment + {totalInvestment.toLocaleString()} +
+
+ Bounties Collected + {player.bounty} +
+
+
+ + + {#if isActive} +
+

Actions

+
+ {#if onrebuy} + + {/if} + {#if onaddon} + + {/if} + +
+
+ {:else if player.status === 'eliminated'} +
+

Actions

+
+ +
+
+ {/if} + + +
+

History

+ {#if playerHistory.length === 0} +

No actions recorded yet.

+ {:else} +
+ {#each playerHistory as entry (entry.id)} +
+ {formatTimestamp(entry.timestamp)} + {entry.message} +
+ {/each} +
+ {/if} +
+
+
+ + diff --git a/frontend/src/lib/components/PlayerSearch.svelte b/frontend/src/lib/components/PlayerSearch.svelte new file mode 100644 index 0000000..91f3ad5 --- /dev/null +++ b/frontend/src/lib/components/PlayerSearch.svelte @@ -0,0 +1,301 @@ + + + + + diff --git a/frontend/src/lib/components/RebuyFlow.svelte b/frontend/src/lib/components/RebuyFlow.svelte new file mode 100644 index 0000000..32473bc --- /dev/null +++ b/frontend/src/lib/components/RebuyFlow.svelte @@ -0,0 +1,258 @@ + + +
+ +
+ +

Rebuy

+ +
+ +
+ {#if step === 'select'} +

Select Player

+ + {:else if step === 'confirm'} +
+

{selectedPlayer?.name}

+
+
+ Current Chips + {selectedPlayer?.chips.toLocaleString()} +
+
+ Rebuy Amount + {rebuyAmount.toLocaleString()} +
+
+ Chips Added + +{rebuyChips.toLocaleString()} +
+
+ Rebuys So Far + {selectedPlayer?.rebuys ?? 0} +
+
+ + +
+ {/if} +
+
+ + diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 52b7c77..96d708c 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -10,7 +10,13 @@ import FAB from '$lib/components/FAB.svelte'; import Toast from '$lib/components/Toast.svelte'; import TournamentTabs from '$lib/components/TournamentTabs.svelte'; + import BuyInFlow from '$lib/components/BuyInFlow.svelte'; + import BustOutFlow from '$lib/components/BustOutFlow.svelte'; + import RebuyFlow from '$lib/components/RebuyFlow.svelte'; + import AddOnFlow from '$lib/components/AddOnFlow.svelte'; import { toast } from '$lib/stores/toast.svelte'; + import { tournament } from '$lib/stores/tournament.svelte'; + import { api } from '$lib/api'; let { children } = $props(); @@ -24,28 +30,50 @@ } }); + // Flow overlay state + let showBuyIn = $state(false); + let showBustOut = $state(false); + let showRebuy = $state(false); + let showAddon = $state(false); + /** Handle FAB action dispatches. */ function handleFABAction(actionId: string): void { switch (actionId) { case 'bust': - toast.info('Bust flow: coming in Plan N'); + showBustOut = true; break; case 'buyin': - toast.info('Buy-in flow: coming in Plan N'); + showBuyIn = true; break; case 'rebuy': - toast.info('Rebuy flow: coming in Plan N'); + showRebuy = true; break; case 'addon': - toast.info('Add-on flow: coming in Plan N'); + showAddon = true; break; case 'pause-resume': - toast.info('Pause/Resume: coming in Plan N'); + togglePauseResume(); break; default: console.warn(`Unknown FAB action: ${actionId}`); } } + + /** Toggle clock pause/resume. */ + async function togglePauseResume(): Promise { + if (!tournament.id || !tournament.clock) return; + try { + if (tournament.clock.is_paused) { + await api.post(`/tournaments/${tournament.id}/clock/resume`); + toast.success('Clock resumed'); + } else { + await api.post(`/tournaments/${tournament.id}/clock/pause`); + toast.success('Clock paused'); + } + } catch (err) { + toast.error(`Clock action failed: ${err instanceof Error ? err.message : 'Unknown error'}`); + } + } {#if isLoginPage} @@ -72,6 +100,31 @@ {/if} + +{#if showBuyIn} +
+ { showBuyIn = false; }} /> +
+{/if} + +{#if showBustOut} +
+ { showBustOut = false; }} /> +
+{/if} + +{#if showRebuy} +
+ { showRebuy = false; }} /> +
+{/if} + +{#if showAddon} +
+ { showAddon = false; }} /> +
+{/if} + @@ -107,4 +160,11 @@ min-height: 100dvh; color: var(--color-text-muted); } + + .flow-overlay { + position: fixed; + inset: 0; + z-index: 100; + background-color: var(--color-bg); + } diff --git a/frontend/src/routes/players/+page.svelte b/frontend/src/routes/players/+page.svelte index 325bd16..2db9c61 100644 --- a/frontend/src/routes/players/+page.svelte +++ b/frontend/src/routes/players/+page.svelte @@ -1,51 +1,379 @@ +
-

Players

-

Registered players and chip counts.

+ + +
+ + + +
+ + []} sortable={true} searchable={true} loading={false} - emptyMessage="No players registered yet" + {emptyMessage} rowKey={(item) => String(item['id'])} - swipeActions={[ - { id: 'bust', label: 'Bust', color: 'var(--color-error)', handler: () => {} }, - { id: 'rebuy', label: 'Rebuy', color: 'var(--color-primary)', handler: () => {} } - ]} + onrowclick={handleRowClick} + {swipeActions} />
+ +{#if showBuyIn} +
+ { showBuyIn = false; }} /> +
+{/if} + +{#if showBustOut} +
+ { showBustOut = false; }} /> +
+{/if} + +{#if showRebuy} +
+ { showRebuy = false; rebuyPlayer = null; }} + preselectedPlayer={rebuyPlayer} + /> +
+{/if} + +{#if showAddon} +
+ { showAddon = false; addonPlayer = null; }} + preselectedPlayer={addonPlayer} + /> +
+{/if} + +{#if selectedPlayer} +
+ { selectedPlayer = null; }} + onrebuy={handleRebuy} + onaddon={handleAddon} + /> +
+{/if} +