Skip to main content

Bet Panel Redesign — Implementation Plan

Branch: feat/bet-panel | Worktree: bet-panel/ Goal: Replace floating bet slip with a permanent right-side Bet Panel (BETSLIP + OPEN BETS tabs), matching 1exch.com UX with Apple 2026 glassmorphism design. Updated: 2026-02-12 — implemented. Drifts from original plan noted inline.


1. Architecture Overview

Current State (Problems)

  • BetSlip is a floating overlay (position: fixed, z-50) that can be opened/closed
  • Each page imports <BetSlip /> independently — no layout-level integration
  • Pages use max-w-4xl / max-w-7xl centered containers — no 3-column grid
  • Live page and fixture detail page have different content widths (inconsistent panel sizes)
  • No "Open Bets" tab — users must navigate to /my-bets page
  • No batch bet placement API
  • No Betfair tick/step size logic for odds +/- buttons
  • Clicking same odds button multiple times can add duplicate entries

Target State

Desktop (>=1024px):
┌─────────────────────────────────────────────────────────────────┐
│ Header (sticky, z-50) │
├──────────┬──────────────────────────────┬───────────────────────┤
│ LEFT │ │ BET PANEL (340px) │
│ PANEL │ Main Content Area │ ┌─────────────────┐ │
│ (empty) │ (fills remaining width) │ │BETSLIP│OPEN BETS│ │
│ │ │ ├─────────────────┤ │
│ Reserved│ - Live page fixtures │ │ONE CLICK: OFF │ │
│ for AI │ - Fixture detail markets │ │Your Bets: 2 🗑 │ │
│ features│ │ │[Bet Card 1] │ │
│ (future)│ Same width on ALL pages │ │[Bet Card 2] │ │
│ │ Content adapts to avail. │ │ (scrollable) │ │
│ │ space (fluid, no max-w) │ ├─────────────────┤ │
│ [AI Chat │ │ │Total Payout: XX │ │
│ Button] │ │ │Pot. Risk: XX │ │
│ (bottom │ │ │[Place Bet ⏱ 5s] │ │
│ left) │ │ └─────────────────┘ │
├──────────┴──────────────────────────────┴───────────────────────┤
│ (no bottom nav on desktop) │
└─────────────────────────────────────────────────────────────────┘

Tablet (768-1023px):
- Left panel hidden. Content + bet panel 2-column grid.
- Bet panel stays visible (like 1exch at 768px+).
- AI chat button overlays bottom-left.

Mobile (<768px):
- Single column. Bet panel = bottom sheet with floating trigger button.
- AI chat button bottom-left, bet panel trigger bottom-right.

Note: Left panel is empty in this PR — reserved for future AI features. Only center content + bet panel are implemented. AI chat button left in its current position (not moved to bottom-left — deferred).

Addition: The bet panel is hideable. A PanelRight toggle in the Header lets the user collapse/expand the panel on desktop. State persisted in betPanelSettingsStore.panelVisible.


2. Implementation Phases

Phase 0: Foundation — Layout Restructure

Goal: 3-column grid at layout level, consistent content panel sizing

0.1 Root Layout Grid

  • layout.tsx is a Next.js server component (exports metadata), so the CSS grid is in a separate MainLayout.tsx client component that wraps children.
  • Grid breakpoints:
    Desktop (>=1024px): grid-cols-[1fr_340px]
    Tablet (768-1023px): grid-cols-[1fr_300px] (narrower bet panel)
    Mobile (<768px): grid-cols-1 (bet panel is bottom sheet)
  • Grid columns are conditional on betPanelSettingsStore.panelVisible — when hidden, full-width single column.
  • Left panel for AI: NOT in this PR. Center content fills 1fr. AI features added later.
  • Bet Panel rendered ONCE at layout level — remove <BetSlip /> from all individual pages
  • Main content area: overflow-y-auto, fills available height
  • Bet Panel: sticky top-[64px] h-[calc(100vh-64px)] on desktop
  • AI chat button: left in current position (move to bottom-left deferred)

0.2 Page Layout Normalization

  • Live page (/live): Remove max-w-7xl, let content fill available grid cell
  • Fixture detail (/fixture/[id]): Remove max-w-4xl, use max-w-5xl or fluid
  • Both pages now have identical content width (the left grid column)
  • Content scrolls independently from bet panel

0.3 Header Adjustments

  • Mobile (md:hidden): Receipt icon button calling betSlipStore.toggleSlip(), red badge with bet count
  • Desktop (hidden md:flex): PanelRight icon button calling betPanelSettingsStore.togglePanelVisible(), highlighted blue when panel visible, bet count badge when panel hidden and bets exist
  • Header width spans full page (above grid)

Files created:

  • frontend/src/components/layout/MainLayout.tsx — client component wrapping CSS grid + BetPanel aside

Files modified:

  • frontend/src/app/layout.tsx — wraps children in <MainLayout>
  • frontend/src/app/live/page.tsx — remove max-w, remove BetSlip import
  • frontend/src/app/fixture/[id]/page.tsx — remove max-w (changed to max-w-5xl), remove BetSlip import
  • frontend/src/app/sports/page.tsx — remove BetSlip import
  • frontend/src/app/my-bets/page.tsx — remove BetSlip import
  • frontend/src/components/layout/Header.tsx — panel toggle buttons (mobile + desktop)
  • frontend/src/app/globals.css — bet panel animations (shake, flash, pulse, slide, thin scrollbar)

Phase 1: Bet Panel Component — BETSLIP Tab

Goal: Fully functional BETSLIP tab matching 1exch.com feature set

1.1 BetPanel Container Component

New: frontend/src/components/betting/BetPanel.tsx

  • Permanent sidebar on desktop (hidden md:flex), bottom sheet on mobile with drag-to-dismiss
  • Two tabs: BETSLIP | OPEN BETS — tab state in betSlipStore.activeTab
  • OneClickToggle rendered above tabs
  • Mobile: floating trigger button (bottom-right, z-40) + overlay bottom sheet (max-h-[80vh]) with drag handle
  • Auto-opens mobile sheet when bet added (useEffect on items.length)
  • Glassmorphism styling: backdrop-blur-xl bg-slate-900/80 border-l border-white/5

New: frontend/src/components/betting/BetSlipTab.tsx — extracted BETSLIP tab content (header with count + clear, scrollable card list, sticky footer)

1.2 ONE CLICK Betting Toggle

  • Positioned at top of bet panel, above tabs (like 1exch)
  • [OFF] ONE CLICK Betting [ⓘ]
  • OFF (default): Odds click adds to slip, user enters stake, clicks Place Bet
  • ON: Clicking odds immediately places bet at defaultStake (from settings)
  • Activation shows confirmation dialog: "Enable One Click? Bets placed instantly at {defaultStake} pts"
  • Setting persisted in hannibal-bet-panel-settings localStorage
  • Visual indicator: yellow/gold accent when ON

1.3 BETSLIP Tab Header

  • "Your Bets: {count}" label
  • Settings gear icon — opens settings dropdown
  • Delete all icon — clears all bets with confirmation
  • Settings options (Phase 2):
    • General: "Show bet info" toggle
    • Bets: "Consolidate" toggle, "Average Odds" toggle, "Order By Date" toggle

1.4 Individual Bet Card (BetSlipCard)

New: frontend/src/components/betting/BetSlipCard.tsx

Layout per 1exch reference:

┌─────────────────────────────────────┐
│ [LIVE 🟢] Match Name X │ ← Close button
│ │
│ Market Name (exchange) - Back/Lay │
│ [Logo] Selection Name [-] 5.6 [+]│ ← Odds with +/- buttons
│ │
│ Your Bet Min: 100 Max: 225,200 │ ← Betfair limits
│ Max. Mkt: 500,000+ │
│ │
│ ┌──────────────────┐ Liability: 2.00│ ← Stake input
│ │ Enter stake │ │
│ └──────────────────┘ │
│ [+500] [+2500] [+5000] [+12500] │ ← Quick stake buttons
│ [ Max ] │
└─────────────────────────────────────┘

Color coding:

  • Back bets: Light blue/cyan border-left accent
  • Lay bets: Light pink/rose border-left accent

Data displayed per card:

  • Match: homeTeam vs awayTeam (from fixture)
  • Live badge if fixture is live (green dot)
  • Market name: e.g., "Match Odds (exchange) - Lay"
  • Selection name: e.g., "Bayern Munich"
  • Current odds with +/- stepper buttons (Betfair tick increments)
  • Limits row: Your Bet min, Max (from depth size), Max. Mkt (totalMatched)
  • Stake input with liability display
  • Quick stake buttons: +10, +25, +50, +100, +500 (in points, configurable via settings)
    • ADD behavior (not replace!) — pressing +25 twice = 50
  • Max button: fills maximum available stake
  • Close (X) button per card

1.5 Betfair Tick Size Logic

New: frontend/src/utils/betfairTicks.ts

// Betfair price ladder increments
const TICK_RANGES = [
{ min: 1.01, max: 2, step: 0.01 },
{ min: 2, max: 3, step: 0.02 },
{ min: 3, max: 4, step: 0.05 },
{ min: 4, max: 6, step: 0.1 },
{ min: 6, max: 10, step: 0.2 },
{ min: 10, max: 20, step: 0.5 },
{ min: 20, max: 30, step: 1 },
{ min: 30, max: 50, step: 2 },
{ min: 50, max: 100, step: 5 },
{ min: 100, max: 1000, step: 10 },
];

export function getNextTick(price: number): number { ... }
export function getPrevTick(price: number): number { ... }
export function isValidTick(price: number): boolean { ... }
export function nearestValidTick(price: number): number { ... }

Functions:

  • getNextTick(price) — increment odds by one tick step
  • getPrevTick(price) — decrement odds by one tick step
  • isValidTick(price) — validate odds are on valid Betfair tick
  • nearestValidTick(price) — snap to nearest valid tick
┌─────────────────────────────────────┐
│ Total Payout: 478 │ ← Green text
│ Potential Risk: 35.00 │ ← White text
│ │
│ ┌──────────────────────────────────┐│
│ │ Place Bet 35.00 ⏱ 5s ││ ← Green button, countdown
│ │ Potential Payout 478 ││
│ └──────────────────────────────────┘│
└─────────────────────────────────────┘

Calculations:

  • Total Payout = Sum of (stake × odds) for back bets + Sum of (stake) for lay bets
  • Potential Risk = Sum of (stake) for back bets + Sum of (stake × (odds - 1)) for lay bets
  • Place Bet button shows total risk amount

5-Second Countdown Timer:

  • Shown on Place Bet button for live/in-play bets only
  • Timer format: Place Bet 35.00 ⏱ 5s
  • Purpose: Odds freshness indicator — odds change rapidly during live play
  • After 5s, timer resets or odds auto-refresh
  • For pre-match bets: no timer (odds are more stable)
  • Matches Betfair's betDelay concept (typically 2-5s for in-play markets)

1.7 Duplicate Prevention

  • betSlipStore.addBet() checks: same fixtureId + marketId + outcomeId + betType
  • If duplicate: highlight existing card with flash animation, do NOT add new one
  • If same match but different market/selection: allow (multiple bets on same match is fine)

1.8 Delete All Functionality

  • Delete (🗑) button in header clears ALL bets in BETSLIP tab
  • Double-tap confirm: first click shows "Confirm?" (auto-dismisses after 3s), second click clears + info toast
  • Does not affect Open Bets tab

1.9 Place All Bets

  • Validates ALL bets have valid stake (> 0)
  • Submits bets sequentially via existing /orders (punters) or /agent/bets (agents)
  • Per-bet toast on success/failure
  • On success: clears entire slip after 1.2s success animation. On all failure: shows error state for 2s.
  • Partial failure: clears all on any success (toast shows which failed). Full per-bet partial clear deferred — store lacks batch-remove.

Files created:

  • frontend/src/components/betting/BetPanel.tsx
  • frontend/src/components/betting/BetSlipTab.tsx
  • frontend/src/components/betting/BetSlipCard.tsx
  • frontend/src/components/betting/BetSlipFooter.tsx
  • frontend/src/components/betting/OneClickToggle.tsx
  • frontend/src/utils/betfairTicks.ts

Files modified:

  • frontend/src/store/betSlipStore.tsactiveTab, clearAll, addBet returns 'added'|'duplicate', flat display fields
  • frontend/src/types/betting.ts — extended BetSlipItem with flat display fields, Order with bookmaker
  • frontend/src/components/betting/ExchangeGrid.tsx — duplicate detection with shake + yellow ring flash
  • frontend/src/components/betting/FixtureCard.tsx — same duplicate detection

Files deleted:

  • frontend/src/components/betting/BetSlip.tsx — replaced by BetPanel
  • frontend/src/components/betting/OddsButton.tsx — unused standalone odds button (was already dead code)

Phase 2: OPEN BETS Tab

Goal: Show active orders with sub-tabs (Matched, Unmatched, Voided, Cashed Out)

2.1 Open Bets Store

New: frontend/src/store/openBetsStore.ts

interface OpenBetsState {
orders: Order[];
loading: boolean;
activeSubTab: 'matched' | 'unmatched' | 'voided' | 'cashed_out';
settings: {
showBetInfo: boolean;
consolidate: boolean;
averageOdds: boolean;
orderByDate: boolean;
};
}

2.2 Open Bets Sub-tabs

┌───────────────────────────────────┐
│ BETSLIP │ OPEN BETS │
├───────────────────────────────────┤
│ ⚙ Settings ▼ │
│ [Matched] [Unmatched] [Voided] │
│ [Cashed Out] │
├───────────────────────────────────┤
│ │
│ (Order cards or empty state) │
│ │
└───────────────────────────────────┘

Sub-tab filtering:

  • Matched: status === 'accepted'
  • Unmatched: status === 'pending' — includes Cancel button (Betfair only, not Pinnacle)
  • Voided: settlementOutcome === 'void'
  • Cashed Out: DEFERRED — omitted until cash-out feature is built. Show as disabled/grayed tab with tooltip "Coming soon".

2.3 Settings Panel (Collapsible)

  • General: "Show bet info" — toggle expanded/compact order cards
  • Bets: "Consolidate" — group same-selection bets
  • Bets: "Average Odds" — show weighted average odds for consolidated bets
  • Bets: "Order By Date" — sort newest first (checked) vs by match time

2.4 Order Card Component

New: frontend/src/components/betting/OpenBetCard.tsx

Shows: match name, market, selection, bet type, stake, odds, potential return, status badge, placed time.

Cancel Button (Unmatched tab only):

  • Red "Cancel" button on each pending order card
  • Betfair: calls POST /api/orders/:id/cancel — refunds stake (back) or liability (lay)
  • Pinnacle: Cancel button hidden — Pinnacle does not support order cancellation
  • Provider check: order.bookmaker !== 'pinnacle' to show/hide cancel
  • Two-click confirm: first click shows "Confirm?", second fires mutation. Loading spinner during cancel, error message on failure. Query cache invalidated on success.

2.5 Data Fetching

  • TanStack Query hook: useOpenBets(status?) — fetches GET /api/orders?status=pending,accepted
  • Poll every 10 seconds (refetchInterval), stale after 5s
  • useCancelOrder() mutation — invalidates query cache on success
  • WebSocket order:update event deferred — polling only for now

Files to create:

  • frontend/src/store/openBetsStore.ts
  • frontend/src/components/betting/OpenBetsTab.tsx
  • frontend/src/components/betting/OpenBetCard.tsx
  • frontend/src/components/betting/BetPanelSettings.tsx
  • frontend/src/hooks/useOpenBets.ts

Phase 3: Backend — Batch Orders + Tick Validation

Goal: Support "Place All" and validate tick sizes server-side

3.1 Batch Order Endpoint

New route: POST /api/orders/batch

// Request
{
orders: Array<{
fixtureId: string;
marketId: string;
outcomeId: string;
stake: number;
odds: number;
trueOdds?: number;
betType: 'back' | 'lay';
acceptBetterOdds: boolean;
sportId: number;
homeTeam?: string;
awayTeam?: string;
tournamentName?: string;
marketName?: string;
selectionName?: string;
}>
}

// Response
{
success: boolean;
data: {
submitted: number;
failed: number;
results: Array<{
index: number;
success: boolean;
order?: Order;
error?: string;
}>
}
}

Logic:

  • Validate ALL orders upfront (balance, limits, odds, tick validity)
  • Group by marketId for Betfair batch API
  • Best-effort: place as many as possible, return per-order status
  • Atomic balance check: total stake across all orders <= available balance
  • Each order creates its own Transaction record

3.2 Betfair Tick Validation (Backend)

New: backend/src/utils/betfairTicks.ts (same logic as frontend)

  • Validate odds are on valid tick before submitting to Betfair
  • Auto-snap to nearest valid tick with warning log
  • Return INVALID_ODDS error if odds are completely out of range

3.3 Market Limits Endpoint Enhancement

Enhance: GET /api/odds/:fixtureId response

Add to each outcome:

{
// existing fields...
limits: {
minStake: number; // Bookmaker minimum (0.10 for Betfair, 20 for Pinnacle)
maxBackStake: number | null; // First back depth entry size (null if no depth)
maxLayStake: number | null; // First lay depth entry size (null if no depth)
}
}

Files to create:

  • backend/src/utils/betfairTicks.ts

Files to modify:

  • backend/src/routes/orders.ts — add batch endpoint
  • backend/src/services/orderService.ts — batch placement logic
  • backend/src/routes/odds.ts — add limits to response

Phase 4: UI/UX Polish — Apple 2026 Glassmorphism

Goal: World-class visual design exceeding all competitors

4.1 Design System

  • Background: bg-slate-900/80 backdrop-blur-xl — frosted glass effect
  • Cards: bg-white/5 border border-white/10 rounded-xl — subtle glass cards
  • Back accent: border-l-2 border-sky-400/60 bg-sky-500/5
  • Lay accent: border-l-2 border-rose-400/60 bg-rose-500/5
  • Active tab: bg-white/10 text-white with subtle glow
  • Inactive tab: text-slate-400 hover:text-slate-200
  • Buttons: Rounded, subtle hover transitions (transition-all duration-200)
  • Inputs: bg-white/5 border-white/10 focus:border-sky-400/50 focus:ring-sky-400/20
  • Scrollbar: Thin custom scrollbar (scrollbar-thin scrollbar-track-transparent scrollbar-thumb-white/10)

4.2 Animations

  • Bet card add: Slide in from right + fade in (animate-slideInRight)
  • Bet card remove: Slide out left + fade out
  • Tab switch: Smooth underline slide (transition-[left,width])
  • Odds change flash: Brief background pulse (green up, red down)
  • Duplicate attempt: Shake + highlight existing card
  • Place bet: Button ripple effect + loading spinner
  • Success: Green checkmark animation + card slide out

4.3 Responsive Transitions (confirmed via Playwright on 1exch.com)

  • Desktop (>=1024px): 2-column grid, bet panel always visible at 340px
  • Tablet (768-1023px): 2-column grid, bet panel narrows to 300px but stays visible
  • Mobile (<768px): Single column, bet panel becomes bottom sheet with floating trigger
  • Breakpoint aligns with 1exch behavior: panel visible down to 768px
  • All transitions use transition-transform duration-300 ease-out
  • Floating trigger button: bottom-right corner, shows bet count badge
  • AI chat button: bottom-left corner (both mobile and desktop)

4.4 Empty States

  • BETSLIP empty: Illustration + "Add bets and start winning" message (like 1exch Image 6)
  • OPEN BETS empty: Sport-specific illustration + "You have no {Matched} Bets" message
  • Loading: Skeleton pulse animations matching card layout

3. Data Requirements from Betfair API

Currently Available (no backend changes needed)

DataSourceUsed In
Best back/lay oddsbackDepth[0].priceBet card odds display
Depth (3 levels)backDepth[], layDepth[]Depth ladder, max stake calc
Volume at pricebackDepth[0].size"Max" stake calculation
Total matchedtotalMatched on market"Max. Mkt" display
Market statusstatus fieldDisable betting on suspended
In-play flaginplay fieldLIVE badge on bet cards
Order statusesGET /api/ordersOpen Bets tab
Account balanceUser balancePointsBalance check before placement

Needs Backend Enhancement

DataGapSolution
Min stake per bookmakerHardcoded in backend onlyExpose in odds response
Max stake (depth-based)Available but not returnedSum best-price depth sizes
Betfair tick sizesNot implemented anywhereNew utility (frontend + backend)
Batch order placementNo endpointNew POST /api/orders/batch
Real-time order statusNo WebSocket eventAdd order:update event (Phase 2+)

4. State Management Changes

Modified: betSlipStore.ts

interface BetSlipState {
items: BetSlipItem[];
isOpen: boolean; // Only relevant for tablet/mobile
activeTab: 'betslip' | 'open_bets';
}

interface BetSlipItem {
id: string;
fixtureId: string;
marketId: string;
outcomeId: string;
betType: 'back' | 'lay';
odds: number;
trueOdds?: number;
stake: number;
// Display fields (captured at add time):
homeTeam: string;
awayTeam: string;
selectionName: string;
marketName: string;
tournamentName: string;
sportId: number;
isLive: boolean;
// Market data for limits:
minStake: number;
maxStake: number; // From depth size
maxMarket: number; // From totalMatched
}

Key changes:

  • Store flattened data (not full Fixture/Market/Outcome objects) — reduces serialization overhead
  • Add minStake, maxStake, maxMarket from odds API
  • addBet() returns 'added' | 'duplicate' for UI feedback
  • updateOdds() validates against Betfair tick sizes

New: openBetsStore.ts

interface OpenBetsState {
activeSubTab: 'matched' | 'unmatched' | 'voided';
settings: BetPanelSettings;
}

interface BetPanelSettings {
showBetInfo: boolean;
consolidate: boolean;
averageOdds: boolean;
orderByDate: boolean;
}

Orders fetched via TanStack Query (not stored in Zustand) — query key: ['orders', 'open'].

New: betPanelSettingsStore.ts

interface BetPanelSettingsState {
oneClickMode: boolean;
defaultStake: number; // Used for ONE CLICK betting (default: 10)
quickStakes: number[]; // [10, 25, 50, 100, 500]
acceptBetterOdds: boolean;
panelVisible: boolean; // Hide/show bet panel (default: true)
}

Persisted to localStorage as hannibal-bet-panel-settings with version migration. Follows existing Zustand + persist pattern (like hannibal-auth). panelVisible consumed by MainLayout.tsx (grid columns) and Header.tsx (toggle button).


5. Component Hierarchy

layout.tsx
├── Header
│ ├── Receipt toggle (mobile, md:hidden) — toggles betSlipStore.isOpen
│ └── PanelRight toggle (desktop, hidden md:flex) — toggles betPanelSettingsStore.panelVisible
├── MainLayout (client component — CSS grid wrapper)
│ ├── <main> (CSS grid left column, overflow-y-auto)
│ │ └── {children} — page content
│ └── <aside> (CSS grid right column, conditional on panelVisible)
│ └── BetPanel
│ ├── OneClickToggle (OFF/ON + inline default stake input)
│ ├── Tab bar: BETSLIP | OPEN BETS (animated underline, bet count badge)
│ ├── BetSlipTab (when BETSLIP active)
│ │ ├── Header (count + Clear button with double-tap confirm)
│ │ ├── BetSlipCard list (scrollable, .bet-panel-scroll)
│ │ │ └── BetSlipCard (per bet)
│ │ │ ├── MatchInfo (teams, live badge, close X)
│ │ │ ├── SelectionInfo (market, selection, odds +/-)
│ │ │ ├── LimitsRow (min, max, avail)
│ │ │ ├── StakeInput (input + liability)
│ │ │ └── QuickStakeButtons (+10, +25, +50, +100, +500) + Max
│ │ ├── Empty state (MousePointerClick icon)
│ │ └── BetSlipFooter (totals + Place Bet button + 5s live timer)
│ └── OpenBetsTab (when OPEN BETS active)
│ ├── Settings toggle + BetPanelSettings (collapsible)
│ ├── Sub-tabs: Matched | Unmatched | Voided | [Cashed Out: disabled]
│ ├── OpenBetCard list (scrollable)
│ │ └── OpenBetCard (per order + Cancel for unmatched non-Pinnacle)
│ ├── LoadingSkeleton
│ └── EmptyState (per sub-tab)
├── BetPanel mobile bottom sheet (md:hidden, overlay with drag-to-dismiss)
│ └── Same BetPanel content as above
├── Floating trigger button (md:hidden, bottom-right, shows bet count)
└── AiChatButton (current position, not moved)

TODO (future): Agent users place bets via POST /agent/bets. Bet panel currently for punters only. Add agent support when agent roles are fully implemented — will be managed by user role flag.


6. Implementation Order (Tracer Bullet)

Tracer Bullet: Single bet → Place → See in Open Bets

  1. Layout grid + bet panel container (skeleton with tabs)
  2. Single bet card with odds +/- (using tick logic)
  3. Stake input + footer with Place Bet button
  4. Place single bet through existing API
  5. Switch to Open Bets tab → see placed bet

Then expand: 6. Multiple bets + delete all + totals calculation 7. Duplicate prevention + flash animation 8. ONE CLICK betting toggle + confirmation dialog 9. 5s countdown timer for live bets 10. Batch placement API (backend) 11. Open Bets sub-tabs (Matched/Unmatched/Voided) 12. Cancel button for unmatched orders 13. Settings panel + localStorage persistence 14. Glassmorphism polish + animations 15. Mobile/tablet responsive behavior 16. Market limits in odds API response 17. AI chat button moved to bottom-left


7. Key Technical Decisions

Q1: Batch placement — atomic or best-effort?

Decision: Best-effort. Place as many as possible, return per-order status. Frontend shows green/red per bet card. Reason: One failed bet shouldn't block others.

Q2: Open Bets — polling or WebSocket?

Decision: Start with TanStack Query polling (10s interval). Add WebSocket order:update event later as enhancement. Reason: Polling is simpler, WebSocket event doesn't exist yet.

Q3: Bet panel width?

Decision: 340px fixed on desktop (1exch uses ~320px). Below 1024px, the panel becomes an overlay. Reason: 340px fits all content including +/- buttons and limits row without cramping.

Q4: Store architecture — BetSlipItem as flat data or nested objects?

Decision: Flat data. Capture display fields at addBet() time. Reason: Reduces serialization to localStorage, avoids stale nested object references, simpler component props.

Q5: Glassmorphism — how heavy?

Decision: Subtle. backdrop-blur-xl on panel background only. Cards use bg-white/5 — no blur per card (performance). Reason: Excessive blur causes GPU issues on lower-end devices.

Q6: ONE CLICK — safety mechanism?

Decision: Confirmation dialog on first enable. Shows: "Enable One Click? Bets placed instantly at {defaultStake} pts. You can change default stake in settings." Persisted in localStorage — remembers across sessions. Yellow/gold accent when active as constant visual reminder.

Q7: Quick stakes — ADD or REPLACE?

Decision: ADD behavior (like 1exch). Pressing +25 twice = 50. Current Hannibal uses REPLACE. This is a UX change. Reason: ADD is faster for live betting — users build up stake incrementally.

Q8: Bet panel visible at 768px?

Decision: YES (confirmed via Playwright on 1exch). Keep panel visible down to 768px. Only switch to bottom sheet below 768px. This matches 1exch exactly.


8. Files Inventory

New Files (14)

FilePurpose
frontend/src/components/layout/MainLayout.tsxClient component: CSS grid wrapper with conditional panel column
frontend/src/components/betting/BetPanel.tsxMain bet panel container (tabs, mobile bottom sheet)
frontend/src/components/betting/BetSlipTab.tsxBETSLIP tab content (header, card list, footer)
frontend/src/components/betting/BetSlipCard.tsxIndividual bet card (odds +/-, stake, quick stakes)
frontend/src/components/betting/BetSlipFooter.tsxTotals + Place Bet + 5s live timer
frontend/src/components/betting/OneClickToggle.tsxONE CLICK toggle + confirmation + inline stake input
frontend/src/components/betting/OpenBetsTab.tsxOPEN BETS tab (sub-tabs, settings, order cards)
frontend/src/components/betting/OpenBetCard.tsxOpen order card + Cancel for unmatched non-Pinnacle
frontend/src/components/betting/BetPanelSettings.tsxCollapsible settings toggles
frontend/src/utils/betfairTicks.tsBetfair odds tick logic (10 ranges, precision-safe)
frontend/src/hooks/useOpenBets.tsTanStack Query: useOpenBets + useCancelOrder
frontend/src/store/openBetsStore.tsOpen bets UI state (sub-tab, display settings)
frontend/src/store/betPanelSettingsStore.tsPersisted settings (ONE CLICK, quickStakes, panelVisible)
backend/src/utils/betfairTicks.tsServer-side tick validation + auto-snap

Modified Files (12)

FileChanges
frontend/src/app/layout.tsxWraps children in <MainLayout>
frontend/src/app/live/page.tsxRemove max-w, remove BetSlip import
frontend/src/app/fixture/[id]/page.tsxChange to max-w-5xl, remove BetSlip import
frontend/src/app/sports/page.tsxRemove BetSlip import
frontend/src/app/my-bets/page.tsxRemove BetSlip import
frontend/src/app/globals.cssBet panel animations + thin scrollbar
frontend/src/store/betSlipStore.tsactiveTab, clearAll, addBet returns added/duplicate, flat display fields
frontend/src/types/betting.tsBetSlipItem flat fields, Order.bookmaker
frontend/src/components/betting/ExchangeGrid.tsxDuplicate detection (shake + yellow flash)
frontend/src/components/betting/FixtureCard.tsxDuplicate detection (shake + yellow flash)
frontend/src/components/layout/Header.tsxPanel toggle buttons (mobile Receipt + desktop PanelRight)
backend/src/routes/orders.tsPOST /api/orders/batch endpoint
backend/src/services/orderService.tsplaceBatchOrders() method
backend/src/routes/odds.tsLimits (minStake, maxBackStake, maxLayStake) in odds response

Deleted Files (2)

FileReason
frontend/src/components/betting/BetSlip.tsxReplaced by BetPanel + BetSlipTab
frontend/src/components/betting/OddsButton.tsxDead code — unused standalone odds button

9. Resolved Questions

#QuestionDecisionRationale
1Quick stake values+10, +25, +50, +100, +500 (points)1 point = 1 USD. 1exch uses INR values (500/2500/5000/12500). Our USD-equivalent values are proportionally smaller. Configurable via settings.
2ONE CLICK bettingYES — implementUser confirmed. Toggle at top of panel. Confirmation dialog on first enable. Places bet at defaultStake on odds click.
35s countdown timerYES — for live bets onlyOdds freshness indicator. Matches Betfair's betDelay (2-5s for in-play). No timer for pre-match.
4Cashed Out sub-tabDEFERREDNo cash-out feature exists. Show tab as disabled/grayed with "Coming soon" tooltip.
5Agent betsDEFERREDPunters only for now. TODO added for future agent role support.
6Settings persistencelocalStorageIndustry standard. Key: hannibal-bet-panel-settings. Follows existing hannibal-{feature} pattern. Version migration included.
7Mobile bet panel triggerFloating button (bottom-right)Matches 1exch mobile pattern. Shows bet count badge. AI chat button goes bottom-left.
8Tick sizesHardcoded (frontend + backend)Betfair does NOT expose via API. Fixed price ladder for 20+ years. Same util on both sides.
9Cancel for unmatchedYES — Betfair onlyCancel button on unmatched order cards. Hidden for Pinnacle (no cancel support). Uses existing POST /api/orders/:id/cancel.
10Left panelEmpty — reserved for AINot implemented in this PR. AI chat button moves to bottom-left corner.

10. Remaining Open Items

  • One-click immediate placement: Toggle UI is built, but odds click in ExchangeGrid/FixtureCard does not yet check oneClickMode to place immediately — still adds to slip. Wire up the intercept.
  • Bet delay integration: Betfair's betDelay field exists but not surfaced. Could use to set the exact countdown timer per market (2-5s) instead of a static 5s.
  • WebSocket order:update event: Currently no real-time order status push. Open Bets tab relies on polling (10s). Backend enhancement needed later.
  • Error message mapping: Betfair error codes (INVALID_BET_SIZE, INSUFFICIENT_FUNDS, etc.) need user-friendly messages. Not blocking for MVP.
  • Partial failure in slip: On partial bet placement failure, entire slip is cleared (with toast showing which failed). Per-bet selective clearing needs a batch-remove action in betSlipStore.
  • AI chat button: Plan called for moving to bottom-left corner — deferred, left in current position.
  • Pinnacle no-cancel UX: BetSlip card doesn't show a "cannot cancel" indicator for Pinnacle bets (cancel button is just hidden in Open Bets).
  • Pinnacle $20 min stake warning: BetSlipCard doesn't yet show a warning when stake is below Pinnacle minimum for Pinnacle-routed bets.