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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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: Verify — npx 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
| Agent | Tasks |
|---|---|
| Frontend Engineer | A1, A2, A3, A4, A5, B2, D1, D2, D3, E1, E2 |
| Backend Engineer | C1 |
| UI/UX Designer | B1 (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.