Skip to main content

Bet Panel Phase 4 — Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Fix UX issues and redesign fixture page with match scoreboard, live cricket stats, and sport selector.

Architecture: Frontend-heavy with one backend enhancement (extend WebSocket cricket payload with batsmen/bowler data). All frontend work in Next.js 16 / React 19 with Zustand + Tailwind. Three parallel workstreams: UI/UX styling, frontend logic/wiring, backend data pipeline.

Tech Stack: Next.js 16, React 19, Zustand, Tailwind CSS v4, Socket.IO, Redis pub/sub

Worktree: bet-panel/ on branch feat/bet-panel


Workstream A: Quick Fixes (Sequential — do first)

Task A1: Auto-show bet panel on odds click

Files:

  • Modify: frontend/src/store/betPanelSettingsStore.ts
  • Modify: frontend/src/store/betSlipStore.ts

Step 1: Add ensurePanelVisible() to betPanelSettingsStore

In betPanelSettingsStore.ts, add to interface:

ensurePanelVisible: () => void;

Add to store body (after togglePanelVisible):

ensurePanelVisible: () => {
set({ panelVisible: true });
},

Step 2: Call ensurePanelVisible() in betSlipStore.addBet()

In betSlipStore.ts, add import at top:

import { useBetPanelSettingsStore } from './betPanelSettingsStore';

In addBet(), after the set() call at line 79 (before return 'added'), add:

useBetPanelSettingsStore.getState().ensurePanelVisible();

Step 3: Verifynpx tsc --noEmit in frontend/


Task A2: Remove gap between tabs and content

Files:

  • Modify: frontend/src/components/betting/FixtureTabBar.tsx

Step 1: Remove mb-4 from tab bar

Change line 23:

<div className="flex w-full mb-4">

to:

<div className="flex w-full">

Step 2: Verifynpx tsc --noEmit


Task A3: Better chevron styling on collapsible panels

Files:

  • Modify: frontend/src/components/cricket/TossAnalysisPanel.tsx

Note: SessionPanel does not have a collapsible header/chevron pattern, so only TossAnalysisPanel was updated.

Step 1: TossAnalysisPanel chevrons (lines 192-196)

Replace the chevron block:

{isExpanded ? (
<ChevronUp className="w-4 h-4 text-slate-400" />
) : (
<ChevronDown className="w-4 h-4 text-slate-400" />
)}

with:

<div className="w-8 h-8 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors">
<ChevronDown className={cn(
'w-5 h-5 text-slate-300 transition-transform duration-200',
isExpanded && 'rotate-180'
)} />
</div>

Remove ChevronUp from imports (keep ChevronDown only). Add cn import if not present.

Step 2: Verifynpx tsc --noEmit


Task A4: Larger back button in breadcrumbs

Files:

  • Modify: frontend/src/components/betting/FixtureBreadcrumb.tsx

Step 1: Enlarge the back button (line 24-30)

Replace:

<button
onClick={() => router.back()}
className="p-1 -ml-1 rounded-lg hover:bg-slate-800 transition-colors flex-shrink-0"
title="Go back"
>
<ArrowLeft className="w-4 h-4 text-slate-400" />
</button>

with:

<button
onClick={() => router.back()}
className="w-9 h-9 rounded-xl bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors flex-shrink-0"
title="Go back"
>
<ArrowLeft className="w-5 h-5 text-slate-300" />
</button>

Step 2: Verifynpx tsc --noEmit


Task A5: Remove compact match header from Market tab

Files:

  • Modify: frontend/src/app/fixture/[id]/page.tsx

The compact match header (lines 177-199) in the Market tab will be replaced by the new MatchScoreboard (Task B1). Remove it now.

Step 1: In the activeTab === 'market' block, remove the entire <div className="flex items-center justify-between py-3"> block and its children. Keep only <MarketsSection {...marketsProps} />.

The block becomes:

{activeTab === 'market' && (
<MarketsSection {...marketsProps} />
)}

Step 2: Remove unused imports: Play, SportScoreDisplay, formatVolume, useCurrency, AIFixtureButton (if only used there — check other tabs). Keep imports used by other tabs.

Step 3: Verifynpx tsc --noEmit


Workstream B: New Components (Can be parallelized)

Task B1: MatchScoreboard component

Files:

  • Create: frontend/src/components/betting/MatchScoreboard.tsx
  • Create: frontend/src/components/ui/CricketBatIcon.tsx
  • Modify: frontend/src/app/fixture/[id]/page.tsx

Step 1: Create CricketBatIcon SVG component

File: frontend/src/components/ui/CricketBatIcon.tsx

export function CricketBatIcon({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 19.5L2 22M4.5 19.5L6 18M4.5 19.5L3 18M6 18L14 10C14 10 16.5 7.5 19 5C21.5 2.5 20.5 2.5 19.5 3.5L12 11L6 18Z"
stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" fill="none"/>
<path d="M12 11L14 10L19.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
}

Step 2: Create MatchScoreboard component

File: frontend/src/components/betting/MatchScoreboard.tsx

'use client';

import { useState } from 'react';
import { ChevronDown, Play } from 'lucide-react';
import { cn } from '@/lib/utils';
import { TeamLogo } from '@/components/ui/TeamLogo';
import { CricketBatIcon } from '@/components/ui/CricketBatIcon';
import { AIFixtureButton } from '@/components/ai';
import type { Fixture } from '@/types/betting';
import type { CricketMatchContext } from '@/lib/cricketApi';

interface MatchScoreboardProps {
fixture: Fixture;
cricketContext?: CricketMatchContext | null;
}

export function MatchScoreboard({ fixture, cricketContext }: MatchScoreboardProps) {
const [isExpanded, setIsExpanded] = useState(true);
const isLive = fixture.status === 'live';
const isCricket = fixture.sportId === '27';

// Determine batting team for bat icon
const homeBatting = cricketContext?.battingTeam === fixture.homeTeam;
const awayBatting = cricketContext?.battingTeam === fixture.awayTeam;

// Cricket score from context
const score = cricketContext?.score; // e.g., "150/3"
const overs = cricketContext ? getCricketOvers(cricketContext) : null;

return (
<div className="bg-slate-800/50 border border-white/[0.08] rounded-xl overflow-hidden">
{/* Main scoreboard */}
<div className="p-4">
<div className="flex items-center justify-between mb-3">
{/* Home team */}
<div className="flex items-center gap-2.5 flex-1 min-w-0">
<TeamLogo teamName={fixture.homeTeam} size="sm" />
{isCricket && homeBatting && (
<CricketBatIcon className="w-4 h-4 text-amber-400 flex-shrink-0" />
)}
</div>

{/* Center: live badge + score */}
<div className="flex flex-col items-center px-4">
{isLive && (
<span className="inline-flex items-center gap-1 text-[10px] font-bold text-white bg-emerald-500 px-2 py-0.5 rounded-full mb-1.5 uppercase tracking-wide">
<Play className="w-2.5 h-2.5 fill-current" />
In Play
</span>
)}
{isCricket && score ? (
<div className="text-center">
<span className="text-xl font-bold text-white tabular-nums">{score}</span>
{overs && (
<div className="text-xs text-slate-400 mt-0.5">{overs} Ovs</div>
)}
</div>
) : (
<SportScore fixture={fixture} />
)}
</div>

{/* Away team */}
<div className="flex items-center gap-2.5 flex-1 min-w-0 justify-end">
{isCricket && awayBatting && (
<CricketBatIcon className="w-4 h-4 text-amber-400 flex-shrink-0" />
)}
<TeamLogo teamName={fixture.awayTeam} size="sm" />
</div>
</div>

{/* Team names row */}
<div className="flex items-center justify-between">
<span className="text-sm font-semibold text-white truncate flex-1">{fixture.homeTeam}</span>
<span className="text-xs text-slate-500 px-3">vs</span>
<span className="text-sm font-semibold text-white truncate flex-1 text-right">{fixture.awayTeam}</span>
</div>
</div>

{/* Collapsible toss/meta footer */}
{isCricket && cricketContext && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center justify-between px-4 py-2 border-t border-white/5 hover:bg-white/[0.02] transition-colors"
>
{isExpanded && (
<span className="text-xs text-slate-400 truncate">
{getTossLine(cricketContext)}
</span>
)}
<div className={cn(
'ml-auto transition-transform duration-200',
isExpanded && 'rotate-180'
)}>
<ChevronDown className="w-4 h-4 text-slate-500" />
</div>
</button>
)}

{/* AI button row */}
<div className="flex items-center justify-between px-4 py-2 border-t border-white/5">
{fixture.totalVolume && fixture.totalVolume > 0 ? (
<span className="text-xs text-emerald-400 tabular-nums">Vol: {formatCompact(fixture.totalVolume)}</span>
) : <span />}
<AIFixtureButton fixture={fixture} size="sm" />
</div>
</div>
);
}

// Helper: get cricket overs string from context
function getCricketOvers(ctx: CricketMatchContext): string | null {
// Parse score "150/3" to get info, but overs come from context
// The score string format is "runs/wickets", overs are in the context
// For now we derive from currentRate and score
return null; // Will be enhanced when backend sends overs
}

function getTossLine(ctx: CricketMatchContext): string {
return `${ctx.battingTeam} batting`;
}

function formatCompact(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return n.toString();
}

// Non-cricket score display
function SportScore({ fixture }: { fixture: Fixture }) {
if (fixture.homeScore !== undefined && fixture.awayScore !== undefined) {
return (
<span className="text-xl font-bold text-white tabular-nums">
{fixture.homeScore} - {fixture.awayScore}
</span>
);
}
return <span className="text-xs text-slate-500">vs</span>;
}

Step 3: Wire into fixture page

In fixture/[id]/page.tsx, import MatchScoreboard and place it between breadcrumb and tab bar:

import { MatchScoreboard } from '@/components/betting/MatchScoreboard';

// After FixtureBreadcrumb, before FixtureExposure:
<MatchScoreboard fixture={fixture} cricketContext={cricketContext} />

Step 4: Verifynpx tsc --noEmit


Task B2: SportSelector component

Files:

  • Create: frontend/src/components/betting/SportSelector.tsx
  • Modify: frontend/src/app/fixture/[id]/page.tsx

Step 1: Create SportSelector

File: frontend/src/components/betting/SportSelector.tsx

'use client';

import { useRouter } from 'next/navigation';
import { cn } from '@/lib/utils';
import { SPORT_INFO } from '@/lib/sportInfo';

// Sports to show in selector (priority order)
const SELECTOR_SPORTS = ['27', '10', '6', '1', '3', '4', '23', '12', '13'];

interface SportSelectorProps {
activeSportId?: string;
}

export function SportSelector({ activeSportId }: SportSelectorProps) {
const router = useRouter();

return (
<div className="flex gap-1.5 overflow-x-auto pb-1 scrollbar-hide mb-2">
{/* All Live pill */}
<button
onClick={() => router.push('/live')}
className={cn(
'h-8 text-sm rounded-lg px-3 whitespace-nowrap font-medium transition-colors flex-shrink-0',
!activeSportId
? 'bg-red-500 text-white'
: 'bg-slate-800 border border-white/10 text-slate-300 hover:bg-slate-700'
)}
>
All Live
</button>
{SELECTOR_SPORTS.map((sportId) => {
const info = SPORT_INFO[sportId];
if (!info) return null;
const isActive = activeSportId === sportId;
return (
<button
key={sportId}
onClick={() => router.push(`/live?sportId=${sportId}`)}
className={cn(
'h-8 text-sm rounded-lg px-3 whitespace-nowrap font-medium transition-colors flex-shrink-0',
isActive
? 'bg-red-500 text-white'
: 'bg-slate-800 border border-white/10 text-slate-300 hover:bg-slate-700'
)}
>
{info.name}
</button>
);
})}
</div>
);
}

Step 2: Wire into fixture page

In fixture/[id]/page.tsx, import and place ABOVE breadcrumb:

import { SportSelector } from '@/components/betting/SportSelector';

// Before FixtureBreadcrumb:
<SportSelector activeSportId={fixture.sportId} />

Step 3: Verifynpx tsc --noEmit


Workstream C: Backend — Extend Cricket Context (Independent)

Task C1: Add batsmen/bowler/partnership data to MatchContext

Files:

  • Modify: backend/src/services/cricket/ballByBallService.ts
  • Modify: backend/src/services/clientWs.ts

Step 1: Extend MatchContext interface (line 120)

Add new optional fields after partnership: string;:

  // Detailed player stats (for Match Info stats sub-tab)
currentBatsmen?: Array<{
playerName: string;
runs: number;
balls: number;
fours: number;
sixes: number;
strikeRate: number;
onStrike: boolean;
}>;
currentBowler?: {
playerName: string;
overs: number;
maidens: number;
runs: number;
wickets: number;
economy: number;
};
partnershipDetail?: {
runs: number;
balls: number;
};
lastWicket?: string;
overs?: string; // e.g., "15.2"

Step 2: Extract batsmen/bowler data in updateContextFromMatchState

After the existing score parsing (around line 1155+), add:

    // Extract current batsmen from live data
if (event.live?.batsmen && Array.isArray(event.live.batsmen)) {
context.currentBatsmen = event.live.batsmen.map((b: any) => ({
playerName: b.name || b.player_name || 'Unknown',
runs: b.runs || 0,
balls: b.balls || 0,
fours: b.fours || 0,
sixes: b.sixes || 0,
strikeRate: b.strike_rate || (b.balls > 0 ? (b.runs / b.balls) * 100 : 0),
onStrike: b.on_strike || false,
}));
}

// Extract current bowler
if (event.live?.bowler) {
const b = event.live.bowler;
context.currentBowler = {
playerName: b.name || b.player_name || 'Unknown',
overs: b.overs || 0,
maidens: b.maidens || 0,
runs: b.runs || 0,
wickets: b.wickets || 0,
economy: b.economy || (b.overs > 0 ? b.runs / b.overs : 0),
};
}

// Extract partnership detail
if (event.live?.partnership) {
const p = event.live.partnership;
context.partnershipDetail = {
runs: p.runs || 0,
balls: p.balls || 0,
};
context.partnership = `${p.runs} (${p.balls})`;
}

// Extract overs as string
if (score.overs && Array.isArray(score.overs)) {
const [completedOvers, ballsInOver] = score.overs;
context.overs = `${completedOvers}.${ballsInOver}`;
}

// Extract last wicket from fall of wickets
if (event.live?.last_wicket) {
const lw = event.live.last_wicket;
context.lastWicket = `${lw.player_name || 'Unknown'} ${lw.runs || 0}(${lw.balls || 0})`;
}

Step 3: Include new fields in publishContextUpdate (line 1017)

Add to the JSON.stringify payload:

currentBatsmen: context.currentBatsmen,
currentBowler: context.currentBowler,
partnershipDetail: context.partnershipDetail,
lastWicket: context.lastWicket,
overs: context.overs,

Step 4: Update clientWs.ts CricketMatchContext interface (line 113)

Add the same optional fields to the CricketMatchContext interface in clientWs.ts:

currentBatsmen?: Array<{
playerName: string;
runs: number;
balls: number;
fours: number;
sixes: number;
strikeRate: number;
onStrike: boolean;
}>;
currentBowler?: {
playerName: string;
overs: number;
maidens: number;
runs: number;
wickets: number;
economy: number;
};
partnershipDetail?: { runs: number; balls: number };
lastWicket?: string;
overs?: string;

Step 5: Verifynpx tsc --noEmit in backend/


Workstream D: Frontend — Cricket Match Info (Depends on C1 for types, but can start)

Task D1: Extend frontend CricketMatchContext type

Files:

  • Modify: frontend/src/lib/cricketApi.ts

Step 1: Add new optional fields to CricketMatchContext (after line 82)

  currentBatsmen?: Array<{
playerName: string;
runs: number;
balls: number;
fours: number;
sixes: number;
strikeRate: number;
onStrike: boolean;
}>;
currentBowler?: {
playerName: string;
overs: number;
maidens: number;
runs: number;
wickets: number;
economy: number;
};
partnershipDetail?: { runs: number; balls: number };
lastWicket?: string;
overs?: string;

Step 2: Verifynpx tsc --noEmit


Task D2: Delete BallByBallTab and update tab bar

Files:

  • Delete: frontend/src/components/betting/BallByBallTab.tsx
  • Modify: frontend/src/components/betting/FixtureTabBar.tsx
  • Modify: frontend/src/app/fixture/[id]/page.tsx

Step 1: Update FixtureTabBar

Change the type and remove ball-by-ball:

export type FixtureTab = 'market' | 'match-info';

// Remove ball-by-ball from tabs array:
const tabs: { key: FixtureTab; label: string; cricketOnly?: boolean }[] = [
{ key: 'market', label: 'Market' },
{ key: 'match-info', label: 'Match Info' },
];

// Remove the filter (no more cricketOnly tabs):
// const visibleTabs = tabs; (or just use tabs directly)

Remove isCricket from props if no longer needed.

Step 2: Update fixture page

Remove the ball-by-ball tab content block (lines 212-219):

{activeTab === 'ball-by-ball' && isCricket && (
<BallByBallTab ... />
)}

Remove BallByBallTab import.

Step 3: Delete BallByBallTab.tsx

rm frontend/src/components/betting/BallByBallTab.tsx

Step 4: Verifynpx tsc --noEmit


Task D3: Rewrite MatchInfoTab with 3 icon sub-tabs for live cricket

Files:

  • Modify: frontend/src/components/betting/MatchInfoTab.tsx

This is the largest task. The MatchInfoTab needs 3 icon sub-tabs when isCricket && isLive:

Sub-tab 1: Commentary (MessageSquare icon) — recent balls visualization + commentary Sub-tab 2: Stats (Users icon) — CRR, P'SHIP, RRR, batsmen/bowler tables Sub-tab 3: Scorecard (BarChart3 icon) — full scorecard (placeholder for now)

Step 1: Add cricketContext and cricketBalls props to MatchInfoTab

Update interface:

interface MatchInfoTabProps {
fixture: Fixture;
isCricket: boolean;
isLive: boolean;
cricketContext?: CricketMatchContext | null;
cricketBalls?: CricketBallUpdate[];
// ... existing market props
}

Step 2: Add sub-tab state and icon tabs for cricket

import { MessageSquare, Users, BarChart3 } from 'lucide-react';
import type { CricketMatchContext, CricketBallUpdate } from '@/lib/cricketApi';

type CricketSubTab = 'commentary' | 'stats' | 'scorecard';

// Inside component:
const [cricketSubTab, setCricketSubTab] = useState<CricketSubTab>('commentary');

const subTabs = [
{ key: 'commentary' as const, icon: MessageSquare },
{ key: 'stats' as const, icon: Users },
{ key: 'scorecard' as const, icon: BarChart3 },
];

Step 3: Build the Commentary sub-tab

Recent balls as circles:

  • Dot ball (0): bg-slate-700/50 text-slate-400
  • 1,2,3 runs: bg-slate-700/50 text-white
  • 4 runs: bg-emerald-500 text-white
  • 6 runs: bg-purple-500 text-white
  • Wicket (W): bg-red-500 text-white

Group by over with subtle vertical separator. Auto-scroll to latest. Commentary text below.

function RecentBalls({ balls }: { balls: CricketBallUpdate[] }) {
// Group balls by over
const grouped = groupBallsByOver(balls);

return (
<div className="flex gap-1 items-center overflow-x-auto pb-2 scrollbar-hide">
{grouped.map((overGroup, i) => (
<div key={i} className="flex items-center gap-0.5">
{i > 0 && <div className="w-px h-6 bg-white/10 mx-1 flex-shrink-0" />}
{overGroup.map((ball) => (
<BallCircle key={ball.ballId} ball={ball} />
))}
</div>
))}
</div>
);
}

function BallCircle({ ball }: { ball: CricketBallUpdate }) {
const { runs, outcome } = ball.template;
const isWicket = outcome === 'wicket' || !!ball.template.wicket;
const isFour = runs === 4 && !isWicket;
const isSix = runs === 6 && !isWicket;
const isDot = runs === 0 && !isWicket;

const bg = isWicket ? 'bg-red-500'
: isSix ? 'bg-purple-500'
: isFour ? 'bg-emerald-500'
: 'bg-slate-700/50';

const text = isWicket ? 'text-white'
: isDot ? 'text-slate-400'
: 'text-white';

return (
<div className={cn('w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold flex-shrink-0', bg, text)}>
{isWicket ? 'W' : runs}
</div>
);
}

Step 4: Build the Stats sub-tab

Stats row (4 columns), batsmen table, bowler table. Uses cricketContext.currentBatsmen, cricketContext.currentBowler, cricketContext.partnershipDetail, etc.

function CricketStats({ ctx }: { ctx: CricketMatchContext }) {
return (
<div className="space-y-4">
{/* Stats bar */}
<div className="grid grid-cols-4 gap-2">
<StatBox label="CRR" value={ctx.currentRate?.toFixed(2) || '-'} />
<StatBox label="P'SHIP" value={ctx.partnershipDetail ? `${ctx.partnershipDetail.runs}(${ctx.partnershipDetail.balls})` : ctx.partnership || '-'} />
<StatBox label="RRR" value={ctx.requiredRate?.toFixed(2) || '-'} />
<StatBox label="LAST WKT" value={ctx.lastWicket || '-'} />
</div>

{/* Batsmen table */}
{ctx.currentBatsmen && ctx.currentBatsmen.length > 0 && (
<div>
<table className="w-full text-xs">
<thead>
<tr className="text-slate-500 border-b border-white/5">
<th className="text-left py-1.5 font-medium">Batsmen</th>
<th className="text-right py-1.5 font-medium w-8">R</th>
<th className="text-right py-1.5 font-medium w-8">B</th>
<th className="text-right py-1.5 font-medium w-8">4s</th>
<th className="text-right py-1.5 font-medium w-8">6s</th>
<th className="text-right py-1.5 font-medium w-12">SR</th>
</tr>
</thead>
<tbody>
{ctx.currentBatsmen.map((bat) => (
<tr key={bat.playerName} className="border-b border-white/[0.03]">
<td className="py-1.5 text-white font-medium">
{bat.playerName}{bat.onStrike ? ' *' : ''}
</td>
<td className="text-right py-1.5 text-white font-bold">{bat.runs}</td>
<td className="text-right py-1.5 text-slate-400">{bat.balls}</td>
<td className="text-right py-1.5 text-slate-400">{bat.fours}</td>
<td className="text-right py-1.5 text-slate-400">{bat.sixes}</td>
<td className="text-right py-1.5 text-slate-400">{bat.strikeRate.toFixed(1)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}

{/* Bowler table */}
{ctx.currentBowler && (
<div>
<table className="w-full text-xs">
<thead>
<tr className="text-slate-500 border-b border-white/5">
<th className="text-left py-1.5 font-medium">Bowler</th>
<th className="text-right py-1.5 font-medium w-8">O</th>
<th className="text-right py-1.5 font-medium w-8">M</th>
<th className="text-right py-1.5 font-medium w-8">R</th>
<th className="text-right py-1.5 font-medium w-8">W</th>
<th className="text-right py-1.5 font-medium w-12">Eco</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-white/[0.03]">
<td className="py-1.5 text-white font-medium">{ctx.currentBowler.playerName}</td>
<td className="text-right py-1.5 text-slate-400">{ctx.currentBowler.overs}</td>
<td className="text-right py-1.5 text-slate-400">{ctx.currentBowler.maidens}</td>
<td className="text-right py-1.5 text-white font-bold">{ctx.currentBowler.runs}</td>
<td className="text-right py-1.5 text-emerald-400 font-bold">{ctx.currentBowler.wickets}</td>
<td className="text-right py-1.5 text-slate-400">{ctx.currentBowler.economy.toFixed(1)}</td>
</tr>
</tbody>
</table>
</div>
)}
</div>
);
}

function StatBox({ label, value }: { label: string; value: string }) {
return (
<div className="bg-slate-800/50 rounded-lg p-2 text-center">
<div className="text-xs text-slate-500 mb-0.5">{label}</div>
<div className="text-sm font-bold text-white truncate">{value}</div>
</div>
);
}

Step 5: Build Scorecard sub-tab

Full scorecard showing innings header (team, score, overs, run rate), current batsmen at crease, current bowler stats, extras breakdown, partnership details, fall of wickets summary, and 2nd innings target info panel. Uses CricketMatchContext.fallOfWickets[], extras, currentBatsmen, currentBowler, partnershipDetail.

Step 6: Assemble MatchInfoTab

Rewrite the component to: show sub-tab bar (icon only) for live cricket, render the appropriate sub-tab content. For non-cricket or pre-match, keep existing layout (no sub-tabs).

Step 7: Update fixture page to pass cricketContext and cricketBalls to MatchInfoTab:

<MatchInfoTab
isCricket={isCricket}
isLive={isLive}
cricketContext={cricketContext}
cricketBalls={cricketBalls}
{...marketsProps}
/>

Step 8: Verifynpx tsc --noEmit


Workstream E: Final Integration & Verification

Task E1: Wire everything together in fixture page

Files:

  • Modify: frontend/src/app/fixture/[id]/page.tsx

Final page structure should be:

SportSelector (above breadcrumbs)
FixtureBreadcrumb
MatchScoreboard (between breadcrumb and exposure)
FixtureExposure
FixtureTabBar (2 tabs: Market | Match Info, no gap)
Tab content (Market → MarketsSection, Match Info → rewritten MatchInfoTab)

Task E2: Full typecheck + lint

cd bet-panel/frontend && npx tsc --noEmit
cd bet-panel/frontend && npm run lint
cd bet-panel/backend && npx tsc --noEmit

Fix any errors.

Task E3: Commit

git add -A
git commit -m "feat(fixture): phase 4 — match scoreboard, cricket stats, sport selector, UX fixes"

Agent Assignment

AgentTasks
Frontend EngineerA1, A2, A3, A4, A5, B2, D1, D2, D3, E1, E2
Backend EngineerC1
UI/UX DesignerB1 (MatchScoreboard — make it world-class), review all styling

Note: Backend work (C1) is independent and can run in parallel with frontend work. The frontend can start D3 (MatchInfoTab rewrite) immediately since the new CricketMatchContext fields are optional — the UI renders gracefully when data isn't present yet.