@devrongx/games 0.4.24 → 0.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,15 @@
4
4
  import { useState, useEffect, useMemo, useCallback, useRef } from "react";
5
5
  import Image from "next/image";
6
6
  import { motion, AnimatePresence, useSpring, useTransform, useMotionValue } from "framer-motion";
7
- import { ChevronDown, Info, X, Play, Pencil, Loader2 } from "lucide-react";
7
+ import { ChevronDown, Info, X, Play, Pencil, Loader2, Check, EyeOff } from "lucide-react";
8
8
  import { IBetSummary, ILeaderboardEntry, IChallengeConfig, IUserBets, deriveParlayGroups, deriveMarketToParlay, calcDisplayReward, optionReward, calcParlayMultiplier } from "./config";
9
- import { OUTFIT, MARKET_ICONS, PointsIcon, SelectedCheck, AiInsightButton } from "./constants";
9
+ import { OUTFIT, MARKET_ICONS, PointsIcon, SelectedCheck, AiInsightButton, FilterPill } from "./constants";
10
10
  import { useGamePopupStore } from "../../core/gamePopupStore";
11
11
  import { LeaderboardRow } from "./LeaderboardRow";
12
12
 
13
+ // ─── Shared empty set to avoid re-renders on optional props ──────────────────
14
+ const EMPTY_SET = new Set<number>();
15
+
13
16
  /** Animated number that counts up/down to the target value */
14
17
  const AnimatedNumber = ({ value, className, style }: { value: number; className?: string; style?: React.CSSProperties }) => {
15
18
  const motionVal = useMotionValue(value);
@@ -26,6 +29,8 @@ const AnimatedNumber = ({ value, className, style }: { value: number; className?
26
29
  return <span className={className} style={style}>{text}</span>;
27
30
  };
28
31
 
32
+ // ─── Props ───────────────────────────────────────────────────────────────────
33
+
29
34
  interface PreMatchGameProps {
30
35
  config: IChallengeConfig;
31
36
  bets: IUserBets;
@@ -34,48 +39,59 @@ interface PreMatchGameProps {
34
39
  onAmountSelect: (mIdx: number, oIdx: number, amount: number) => void;
35
40
  betSummary: IBetSummary;
36
41
  leaderboardRows: ILeaderboardEntry[];
37
- /** Render only the markets section (no header/balance/leaderboard) — used for card preview */
38
42
  marketsOnly?: boolean;
39
- /** Called when user taps the action button to submit/re-submit bets. */
40
43
  onSubmit?: () => Promise<void> | void;
41
- /** Snapshot of bets as last saved to the server — drives saved/changed button states. */
42
44
  submittedBets?: IUserBets | null;
43
- /** Called when user taps "See Full Leaderboard" */
44
45
  onViewLeaderboard?: () => void;
45
- /** v1: parlays are cosmetic, shows "Coming in v2". Default = false (hides section) */
46
46
  parlayEnabled?: boolean;
47
- /** Whether a submit is in progress */
48
47
  submitting?: boolean;
48
+ // Per-market editing
49
+ ignoredMarkets?: Set<number>;
50
+ onToggleIgnore?: (mIdx: number) => void;
51
+ editingMarkets?: Set<number>;
52
+ dirtyMarkets?: Set<number>;
53
+ onEditMarket?: (mIdx: number) => void;
54
+ onConfirmMarketEdit?: (mIdx: number) => void;
55
+ onCancelMarketEdit?: (mIdx: number) => void;
56
+ editSubmitting?: number | null;
57
+ savedBetSummary?: IBetSummary | null;
58
+ hasUnsavedEdits?: boolean;
49
59
  }
50
60
 
61
+ // ─── Component ───────────────────────────────────────────────────────────────
62
+
63
+ export const PreMatchGame = ({
64
+ config, bets, onBetsChange, onOptionClick, onAmountSelect, betSummary,
65
+ leaderboardRows, marketsOnly, onSubmit, submittedBets, onViewLeaderboard,
66
+ parlayEnabled = false, submitting = false,
67
+ ignoredMarkets: _ignoredMarkets, onToggleIgnore, editingMarkets: _editingMarkets,
68
+ dirtyMarkets: _dirtyMarkets, onEditMarket, onConfirmMarketEdit, onCancelMarketEdit,
69
+ editSubmitting, savedBetSummary, hasUnsavedEdits = false,
70
+ }: PreMatchGameProps) => {
71
+ const ignoredMarkets = _ignoredMarkets ?? EMPTY_SET;
72
+ const editingMarkets = _editingMarkets ?? EMPTY_SET;
73
+ const dirtyMarkets = _dirtyMarkets ?? EMPTY_SET;
51
74
 
52
- export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmountSelect, betSummary, leaderboardRows, marketsOnly, onSubmit, submittedBets, onViewLeaderboard, parlayEnabled = false, submitting = false }: PreMatchGameProps) => {
53
75
  const { selectedCount, compoundMultiplier, totalEntry, compoundedReward, remainingBalance, riskPercent } = betSummary;
54
76
 
55
- // Button state: fresh (no prior submit), saved (submitted, no changes), changed (submitted, user modified)
56
- const buttonState = useMemo<"fresh" | "saved" | "changed">(() => {
57
- if (!submittedBets) return "fresh";
58
- const subKeys = Object.keys(submittedBets);
59
- const curKeys = Object.keys(bets);
60
- if (subKeys.length !== curKeys.length) return "changed";
61
- for (const k of curKeys) {
62
- const cur = bets[Number(k)];
63
- const sub = submittedBets[Number(k)];
64
- if (!sub || cur.optionIdx !== sub.optionIdx || cur.amount !== sub.amount) return "changed";
65
- }
66
- return "saved";
67
- }, [bets, submittedBets]);
77
+ // Whether bets have been submitted to the server at least once
78
+ const hasSubmitted = !!submittedBets && Object.keys(submittedBets).length > 0;
79
+
80
+ // Button state for initial submit flow (fresh = never submitted)
81
+ const buttonState = useMemo<"fresh" | "submitted">(() => {
82
+ return hasSubmitted ? "submitted" : "fresh";
83
+ }, [hasSubmitted]);
68
84
 
69
85
  const goTo = useGamePopupStore(s => s.goTo);
70
86
 
71
- // Onboarding — visible whenever no points have been bet yet
87
+ // ── Onboarding — visible whenever no points have been bet yet ─────────────
72
88
  const showOnboarding = !marketsOnly && totalEntry === 0;
73
89
  const onboardingWords = useMemo(() =>
74
90
  `Select how many points you want to bet on each question. Spend all ${config.startingBalance.toLocaleString()} points across ${config.markets.length} questions.`.split(" "),
75
91
  [config.startingBalance, config.markets.length]
76
92
  );
77
93
 
78
- // Collapsed/expanded state per market
94
+ // ── Collapsed/expanded state per market ──────────────────────────────────
79
95
  const [collapsedMarkets, setCollapsedMarkets] = useState<Set<number>>(new Set());
80
96
  const toggleCollapse = useCallback((mIdx: number) => {
81
97
  setCollapsedMarkets(prev => {
@@ -86,7 +102,7 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
86
102
  });
87
103
  }, []);
88
104
 
89
- // One-time: when bets are restored with amounts already set, auto-collapse those markets
105
+ // One-time: when bets are restored with amounts, auto-collapse those markets
90
106
  const collapseInitializedRef = useRef(false);
91
107
  useEffect(() => {
92
108
  if (collapseInitializedRef.current) return;
@@ -96,41 +112,106 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
96
112
  collapseInitializedRef.current = true;
97
113
  }, [bets]);
98
114
 
99
- // Parlay edit mode: which slot is being edited (null = none)
115
+ // Auto-expand markets that enter editing state
116
+ useEffect(() => {
117
+ if (editingMarkets.size === 0) return;
118
+ setCollapsedMarkets(prev => {
119
+ let changed = false;
120
+ const next = new Set(prev);
121
+ for (const mIdx of editingMarkets) {
122
+ if (next.has(mIdx)) { next.delete(mIdx); changed = true; }
123
+ }
124
+ return changed ? next : prev;
125
+ });
126
+ }, [editingMarkets]);
127
+
128
+ // ── Filter pills state ───────────────────────────────────────────────────
129
+ const [activeFilters, setActiveFilters] = useState<Set<string>>(new Set());
130
+
131
+ const toggleFilter = useCallback((filter: string) => {
132
+ setActiveFilters(prev => {
133
+ const next = new Set(prev);
134
+ if (filter === "bets_first") {
135
+ next.delete("unanswered_first");
136
+ if (next.has("bets_first")) next.delete("bets_first"); else next.add("bets_first");
137
+ } else if (filter === "unanswered_first") {
138
+ next.delete("bets_first");
139
+ if (next.has("unanswered_first")) next.delete("unanswered_first"); else next.add("unanswered_first");
140
+ } else if (filter === "hide_ignored") {
141
+ if (next.has("hide_ignored")) next.delete("hide_ignored"); else next.add("hide_ignored");
142
+ }
143
+ return next;
144
+ });
145
+ }, []);
146
+
147
+ const handleExpandAll = useCallback(() => setCollapsedMarkets(new Set()), []);
148
+ const handleCollapseAll = useCallback(() => {
149
+ setCollapsedMarkets(new Set(config.markets.map((_, i) => i)));
150
+ }, [config.markets]);
151
+
152
+ // ── Computed: filtered/sorted market indices ─────────────────────────────
153
+ const sortActive = activeFilters.has("bets_first") || activeFilters.has("unanswered_first");
154
+
155
+ const visibleMarketIndices = useMemo(() => {
156
+ let indices = config.markets.map((_, i) => i);
157
+ if (activeFilters.has("hide_ignored")) {
158
+ indices = indices.filter(i => !ignoredMarkets.has(i));
159
+ }
160
+ if (activeFilters.has("bets_first")) {
161
+ indices.sort((a, b) => {
162
+ const aHas = (bets[a]?.amount ?? 0) > 0 ? 0 : 1;
163
+ const bHas = (bets[b]?.amount ?? 0) > 0 ? 0 : 1;
164
+ return aHas - bHas || a - b;
165
+ });
166
+ } else if (activeFilters.has("unanswered_first")) {
167
+ indices.sort((a, b) => {
168
+ const aAns = bets[a] !== undefined ? 1 : 0;
169
+ const bAns = bets[b] !== undefined ? 1 : 0;
170
+ return aAns - bAns || a - b;
171
+ });
172
+ }
173
+ return indices;
174
+ }, [config.markets, activeFilters, ignoredMarkets, bets]);
175
+
176
+ // Computed expand/collapse "active" states for the pills
177
+ const allExpanded = visibleMarketIndices.length > 0 && visibleMarketIndices.every(i => !collapsedMarkets.has(i));
178
+ const allCollapsed = visibleMarketIndices.length > 0 && visibleMarketIndices.every(i => collapsedMarkets.has(i));
179
+
180
+ // Category headers — only shown when no sort is active
181
+ const categoryForIndex = useMemo(() => {
182
+ const map = new Map<number, string>();
183
+ if (sortActive) return map;
184
+ let prev = "";
185
+ for (const mIdx of visibleMarketIndices) {
186
+ const cat = config.markets[mIdx].category;
187
+ if (cat !== prev) { map.set(mIdx, cat); prev = cat; }
188
+ }
189
+ return map;
190
+ }, [visibleMarketIndices, sortActive, config.markets]);
191
+
192
+ // ── Parlay state (unchanged) ─────────────────────────────────────────────
100
193
  const [editingParlay, setEditingParlay] = useState<number | null>(null);
101
- // Parlay info popup: which slot's breakdown is shown (null = none)
102
194
  const [parlayInfo, setParlayInfo] = useState<number | null>(null);
103
-
104
195
  const parlayColors = config.parlayConfig.colors;
105
-
106
- // Derived from unified state
107
196
  const parlayGroups = useMemo(() => deriveParlayGroups(bets), [bets]);
108
197
  const marketToParlay = useMemo(() => deriveMarketToParlay(bets), [bets]);
109
198
  const hasAnyParlays = Object.keys(parlayGroups).length > 0;
110
199
 
111
- // Toggle a market in/out of the currently editing parlay.
112
- // A market already in a DIFFERENT parlay cannot be toggled — UI prevents this.
113
200
  const toggleMarketInParlay = useCallback((mIdx: number) => {
114
201
  if (editingParlay === null || !onBetsChange) return;
115
202
  const slot = editingParlay;
116
203
  const entry = bets[mIdx];
117
204
  if (!entry) return;
118
-
119
- // Block if market belongs to a different parlay
120
205
  if (entry.parlaySlot !== null && entry.parlaySlot !== slot) return;
121
-
122
206
  const next = { ...bets };
123
207
  if (entry.parlaySlot === slot) {
124
- // Remove from this parlay → individual
125
208
  next[mIdx] = { ...entry, parlaySlot: null };
126
209
  } else {
127
- // Add to this parlay
128
210
  next[mIdx] = { ...entry, parlaySlot: slot };
129
211
  }
130
212
  onBetsChange(next);
131
213
  }, [editingParlay, bets, onBetsChange]);
132
214
 
133
- // Clear a parlay slot — set all its markets back to individual
134
215
  const clearParlay = useCallback((slot: number) => {
135
216
  if (!onBetsChange) return;
136
217
  const next = { ...bets };
@@ -143,21 +224,19 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
143
224
  if (editingParlay === slot) setEditingParlay(null);
144
225
  }, [bets, onBetsChange, editingParlay]);
145
226
 
146
- // Pre-compute which market indices start a new category section
147
- const categoryFirstIndices = useMemo(() => {
148
- const set = new Set<number>();
149
- let prev = "";
150
- config.markets.forEach((m, i) => {
151
- if (m.category !== prev) { set.add(i); prev = m.category; }
152
- });
153
- return set;
154
- }, [config.markets]);
227
+ // ── Helper: is a market's options clickable? ─────────────────────────────
228
+ const isMarketInteractive = useCallback((mIdx: number) => {
229
+ if (!hasSubmitted) return true; // fresh state: all markets interactive
230
+ return editingMarkets.has(mIdx); // post-submit: only editing markets
231
+ }, [hasSubmitted, editingMarkets]);
232
+
233
+ // ── Render ───────────────────────────────────────────────────────────────
155
234
 
156
235
  return (
157
236
  <div className={`w-full flex flex-col gap-3 px-4 pb-28${marketsOnly ? "" : " relative"}`}>
158
237
  {!marketsOnly && (
159
238
  <>
160
- {/* Match header — edge-to-edge, black bg */}
239
+ {/* Match header */}
161
240
  <div className="-mx-4 -mt-4 flex flex-col items-center justify-center pt-10 pb-8 bg-black border-b border-white/10">
162
241
  <div className="flex items-center gap-2 mb-1">
163
242
  <span className="text-[14px] text-white font-bold tracking-wide" style={OUTFIT}>{config.teamB.shortName}</span>
@@ -167,440 +246,394 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
167
246
  <span className="text-[9px] text-white/20 font-medium tracking-wider uppercase" style={OUTFIT}>{config.venue}</span>
168
247
  </div>
169
248
 
170
- {/* How to Play */}
171
- <div className="pt-2">
249
+ {/* How to Play + Filter pills */}
250
+ <div className="pt-2 flex items-center gap-2 overflow-x-auto scrollbar-hide">
172
251
  <button
173
252
  onClick={() => goTo("intro")}
174
- className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-[#22E3E8]/20 bg-[#22E3E8]/[0.06]"
253
+ className="flex-shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-full border border-[#22E3E8]/20 bg-[#22E3E8]/[0.06]"
175
254
  >
176
- <Play size={12} fill="#22E3E8" strokeWidth={0} className="text-[#22E3E8]" />
255
+ <Play size={10} fill="#22E3E8" strokeWidth={0} className="text-[#22E3E8]" />
177
256
  <span className="text-[10px] text-[#22E3E8] font-semibold tracking-wide" style={OUTFIT}>How to Play</span>
178
257
  </button>
258
+ <div className="w-px h-4 bg-white/10 flex-shrink-0" />
259
+ <FilterPill label="Bets first" active={activeFilters.has("bets_first")} onClick={() => toggleFilter("bets_first")} />
260
+ <FilterPill label="Unanswered first" active={activeFilters.has("unanswered_first")} onClick={() => toggleFilter("unanswered_first")} />
261
+ <FilterPill label="Hide ignored" active={activeFilters.has("hide_ignored")} onClick={() => toggleFilter("hide_ignored")} />
262
+ <FilterPill label="Expand all" active={allExpanded} onClick={handleExpandAll} />
263
+ <FilterPill label="Collapse all" active={allCollapsed} onClick={handleCollapseAll} />
179
264
  </div>
180
265
  </>
181
266
  )}
182
267
 
183
- {/* Markets grouped by category */}
268
+ {/* Sort indicator when category headers are hidden */}
269
+ {sortActive && !marketsOnly && (
270
+ <div className="px-1 pt-1">
271
+ <span className="text-[8px] text-white/30 uppercase tracking-widest font-bold" style={OUTFIT}>
272
+ Sorted by: {activeFilters.has("bets_first") ? "Bets placed first" : "Unanswered first"}
273
+ </span>
274
+ </div>
275
+ )}
276
+
277
+ {/* Markets */}
184
278
  <div className="w-full">
185
279
  <div className="flex flex-col gap-3">
186
- {config.markets.map((market, mIdx) => {
187
- const selection = bets[mIdx];
188
- const isPickerOpenForMarket = !collapsedMarkets.has(mIdx) && selection !== undefined;
189
- const showCategoryHeader = categoryFirstIndices.has(mIdx);
190
-
191
- // Available balance for picker: remaining + current bet on this market (it would be freed)
192
- const pickerAvailable = remainingBalance + (selection?.amount ?? 0);
193
- const chipCount = Math.floor(pickerAvailable / config.parlayConfig.stakeIncrements);
194
-
195
- const IconComponent = MARKET_ICONS[market.icon];
196
-
197
- return (
198
- <div key={market.id}>
199
- {/* Category header */}
200
- {showCategoryHeader && (
201
- <div className="px-1 pt-1 pb-1.5">
202
- <span className="text-[8px] text-white/40 uppercase tracking-widest font-bold" style={OUTFIT}>
203
- {config.categoryLabels[market.category] ?? market.category}
204
- </span>
205
- </div>
206
- )}
280
+ <AnimatePresence initial={false}>
281
+ {visibleMarketIndices.map((mIdx) => {
282
+ const market = config.markets[mIdx];
283
+ const selection = bets[mIdx];
284
+ const isPickerOpenForMarket = !collapsedMarkets.has(mIdx) && selection !== undefined;
285
+ const showCategoryHeader = categoryForIndex.has(mIdx);
286
+ const interactive = isMarketInteractive(mIdx);
287
+
288
+ const pickerAvailable = remainingBalance + (selection?.amount ?? 0);
289
+ const chipCount = Math.floor(pickerAvailable / config.parlayConfig.stakeIncrements);
290
+ const IconComponent = MARKET_ICONS[market.icon];
291
+
292
+ const isCollapsed = collapsedMarkets.has(mIdx);
293
+ const selectedOpt = selection ? market.options[selection.optionIdx] : null;
294
+ const betAmount = selection?.amount ?? 0;
295
+ const reward = selectedOpt && betAmount > 0 ? Math.round(betAmount * selectedOpt.odds) : 0;
296
+
297
+ // Per-market editing state
298
+ const isEditingThis = editingMarkets.has(mIdx);
299
+ const isDirty = dirtyMarkets.has(mIdx);
300
+ const hasSubmittedBet = !!submittedBets?.[mIdx];
301
+
302
+ return (
303
+ <motion.div
304
+ key={market.id}
305
+ layout="position"
306
+ initial={{ opacity: 0, height: 0 }}
307
+ animate={{ opacity: 1, height: "auto" }}
308
+ exit={{ opacity: 0, height: 0 }}
309
+ transition={{ layout: { type: "spring", stiffness: 300, damping: 30 }, opacity: { duration: 0.2 } }}
310
+ >
311
+ {/* Category header */}
312
+ {showCategoryHeader && (
313
+ <div className="px-1 pt-1 pb-1.5">
314
+ <span className="text-[8px] text-white/40 uppercase tracking-widest font-bold" style={OUTFIT}>
315
+ {config.categoryLabels[market.category] ?? market.category}
316
+ </span>
317
+ </div>
318
+ )}
207
319
 
208
- {/* Question header + collapse toggle */}
209
- {(() => {
210
- const isCollapsed = collapsedMarkets.has(mIdx);
211
- const selectedOpt = selection ? market.options[selection.optionIdx] : null;
212
- const betAmount = selection?.amount ?? 0;
213
- const reward = selectedOpt && betAmount > 0 ? Math.round(betAmount * selectedOpt.odds) : 0;
320
+ {/* Question header */}
321
+ <div className="px-1 py-1.5 flex items-center gap-2 cursor-pointer" onClick={() => toggleCollapse(mIdx)}>
322
+ {IconComponent && <IconComponent size={14} className="text-white/30 flex-shrink-0" />}
323
+ <span className="text-[13px] text-white font-semibold leading-tight flex-1" style={OUTFIT}>{market.question}</span>
214
324
 
215
- return (
216
- <>
217
- <div className="px-1 py-1.5 flex items-center gap-2 cursor-pointer" onClick={() => toggleCollapse(mIdx)}>
218
- {IconComponent && <IconComponent size={14} className="text-white/30 flex-shrink-0" />}
219
- <span className="text-[13px] text-white font-semibold leading-tight flex-1" style={OUTFIT}>{market.question}</span>
220
- {/* Parlay color dot */}
221
- {mIdx in marketToParlay && (
222
- <div
223
- className="w-[8px] h-[8px] rounded-full flex-shrink-0"
224
- style={{ background: parlayColors[marketToParlay[mIdx]] }}
225
- />
226
- )}
227
- {/* Edit mode: parlay toggle */}
228
- {editingParlay !== null && selection && (() => {
229
- const inCurrentParlay = bets[mIdx]?.parlaySlot === editingParlay;
230
- const inOtherParlay = mIdx in marketToParlay && marketToParlay[mIdx] !== editingParlay;
231
- if (inOtherParlay) return (
232
- <div
233
- className="flex-shrink-0 w-[20px] h-[20px] rounded-full flex items-center justify-center opacity-40"
234
- style={{ border: `1px dashed ${parlayColors[marketToParlay[mIdx]]}` }}
235
- >
236
- <div className="w-[6px] h-[6px] rounded-full" style={{ background: parlayColors[marketToParlay[mIdx]] }} />
237
- </div>
238
- );
239
- return (
240
- <button
241
- onClick={(e) => { e.stopPropagation(); toggleMarketInParlay(mIdx); }}
242
- className="flex-shrink-0 w-[20px] h-[20px] rounded-full border flex items-center justify-center transition-all"
243
- style={{
244
- borderColor: parlayColors[editingParlay],
245
- background: inCurrentParlay ? parlayColors[editingParlay] : "transparent",
246
- }}
247
- >
248
- {inCurrentParlay && (
249
- <svg width="10" height="10" viewBox="0 0 16 16" fill="none">
250
- <path d="M5 8l2 2 4-4" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
251
- </svg>
252
- )}
325
+ {/* Parlay color dot */}
326
+ {mIdx in marketToParlay && (
327
+ <div className="w-[8px] h-[8px] rounded-full flex-shrink-0" style={{ background: parlayColors[marketToParlay[mIdx]] }} />
328
+ )}
329
+
330
+ {/* Parlay edit toggle */}
331
+ {editingParlay !== null && selection && (() => {
332
+ const inCurrentParlay = bets[mIdx]?.parlaySlot === editingParlay;
333
+ const inOtherParlay = mIdx in marketToParlay && marketToParlay[mIdx] !== editingParlay;
334
+ if (inOtherParlay) return (
335
+ <div className="flex-shrink-0 w-[20px] h-[20px] rounded-full flex items-center justify-center opacity-40"
336
+ style={{ border: `1px dashed ${parlayColors[marketToParlay[mIdx]]}` }}>
337
+ <div className="w-[6px] h-[6px] rounded-full" style={{ background: parlayColors[marketToParlay[mIdx]] }} />
338
+ </div>
339
+ );
340
+ return (
341
+ <button
342
+ onClick={(e) => { e.stopPropagation(); toggleMarketInParlay(mIdx); }}
343
+ className="flex-shrink-0 w-[20px] h-[20px] rounded-full border flex items-center justify-center transition-all"
344
+ style={{ borderColor: parlayColors[editingParlay], background: inCurrentParlay ? parlayColors[editingParlay] : "transparent" }}>
345
+ {inCurrentParlay && (
346
+ <svg width="10" height="10" viewBox="0 0 16 16" fill="none">
347
+ <path d="M5 8l2 2 4-4" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
348
+ </svg>
349
+ )}
350
+ </button>
351
+ );
352
+ })()}
353
+
354
+ {/* Per-market action: edit / ignore / confirm+cancel */}
355
+ {hasSubmittedBet ? (
356
+ isEditingThis ? (
357
+ <div className="flex items-center gap-1.5 flex-shrink-0">
358
+ <button onClick={(e) => { e.stopPropagation(); onCancelMarketEdit?.(mIdx); }}
359
+ className="w-[22px] h-[22px] rounded-full flex items-center justify-center bg-white/[0.06]">
360
+ <X size={12} className="text-white/50" />
361
+ </button>
362
+ {isDirty && (
363
+ <button onClick={(e) => { e.stopPropagation(); onConfirmMarketEdit?.(mIdx); }}
364
+ className="w-[22px] h-[22px] rounded-full flex items-center justify-center"
365
+ style={{ background: "rgba(34,227,232,0.15)", border: "1px solid rgba(34,227,232,0.3)" }}>
366
+ {editSubmitting === mIdx
367
+ ? <Loader2 size={12} className="animate-spin text-[#22E3E8]" />
368
+ : <Check size={12} className="text-[#22E3E8]" />}
253
369
  </button>
254
- );
255
- })()}
256
- {/* Collapse/expand chevron */}
257
- <motion.div
258
- animate={{ rotate: isCollapsed ? 0 : 180 }}
259
- transition={{ duration: 0.25 }}
260
- className="flex-shrink-0"
261
- >
262
- <ChevronDown size={14} className="text-white/40" />
263
- </motion.div>
264
- </div>
370
+ )}
371
+ </div>
372
+ ) : (
373
+ <button onClick={(e) => { e.stopPropagation(); onEditMarket?.(mIdx); }}
374
+ className="flex-shrink-0 w-[22px] h-[22px] rounded-full flex items-center justify-center bg-white/[0.04]">
375
+ <Pencil size={11} className="text-white/40" />
376
+ </button>
377
+ )
378
+ ) : (
379
+ onToggleIgnore && (
380
+ <button onClick={(e) => { e.stopPropagation(); onToggleIgnore(mIdx); }}
381
+ className="flex-shrink-0 w-[22px] h-[22px] rounded-full flex items-center justify-center"
382
+ style={{ background: ignoredMarkets.has(mIdx) ? "rgba(34,227,232,0.1)" : "transparent" }}>
383
+ <EyeOff size={11} className={ignoredMarkets.has(mIdx) ? "text-[#22E3E8]" : "text-white/20"} />
384
+ </button>
385
+ )
386
+ )}
265
387
 
266
- {/* Collapsed summary row */}
267
- <AnimatePresence initial={false}>
268
- {isCollapsed && (
269
- <motion.div
270
- className="overflow-hidden"
271
- initial={{ height: 0, opacity: 0 }}
272
- animate={{ height: "auto", opacity: 1 }}
273
- exit={{ height: 0, opacity: 0 }}
274
- transition={{ duration: 0.25, ease: "easeInOut" }}
275
- >
276
- <div className="px-2 py-1.5 flex items-center justify-between">
277
- {/* Left selected option */}
278
- <div className="flex items-center gap-1.5">
279
- {selectedOpt ? (
280
- <>
281
- <SelectedCheck />
282
- <span className="text-[10px] text-white font-semibold" style={OUTFIT}>{selectedOpt.label}</span>
283
- </>
284
- ) : (
285
- <span className="text-[10px] text-white/30 font-medium" style={OUTFIT}>No selection</span>
286
- )}
287
- </div>
288
- {/* Right — bet summary */}
289
- {selectedOpt && (
290
- <div className="flex items-center gap-[3px]">
291
- {betAmount > 0 ? (
292
- <>
293
- <PointsIcon size={7} />
294
- <span className="text-[9px] text-white font-semibold" style={OUTFIT}>{betAmount}</span>
295
- <span className="text-[8px] text-white/40" style={OUTFIT}>×</span>
296
- <span className="text-[9px] text-white/60 font-semibold" style={OUTFIT}>{selectedOpt.odds}</span>
297
- <span className="text-[8px] text-white/40" style={OUTFIT}>=</span>
298
- <PointsIcon size={7} />
299
- <span className="text-[9px] text-[#22E3E8] font-bold" style={OUTFIT}>{reward}</span>
300
- </>
301
- ) : (
302
- <span className="text-[9px] text-white/40 font-semibold" style={OUTFIT}>{selectedOpt.odds}x</span>
303
- )}
304
- </div>
305
- )}
306
- </div>
307
- </motion.div>
308
- )}
309
- </AnimatePresence>
310
- </>
311
- );
312
- })()}
313
-
314
- {/* Expanded view — options grid + picker */}
315
- <AnimatePresence initial={false}>
316
- {!collapsedMarkets.has(mIdx) && (
317
- <motion.div
318
- className="overflow-hidden"
319
- initial={{ height: 0, opacity: 0 }}
320
- animate={{ height: "auto", opacity: 1 }}
321
- exit={{ height: 0, opacity: 0 }}
322
- transition={{ duration: 0.25, ease: "easeInOut" }}
323
- >
324
-
325
- {/* Options grid */}
326
- <div className="grid grid-cols-2 gap-1.5 px-1 pt-0.5 pb-1">
327
- {market.options.map((opt, j) => {
328
- const isSelected = selection?.optionIdx === j;
329
- const isPickerOpen = isPickerOpenForMarket && selection?.optionIdx === j;
330
- const showPickerRow = isPickerOpenForMarket;
331
- const displayReward = calcDisplayReward(opt, compoundMultiplier, isSelected, selectedCount, isSelected ? selection.amount : undefined);
332
- const baseReward = optionReward(opt);
333
- return (
334
- <div key={j} className="col-span-1 flex flex-col">
335
- <div
336
- onClick={() => onOptionClick(mIdx, j)}
337
- className="flex items-center justify-between px-2 py-1.5 rounded-sm transition-all duration-300 cursor-pointer"
338
- style={{
339
- background: isSelected
340
- ? "linear-gradient(135deg, #22E3E8, #9945FF)"
341
- : isPickerOpen
342
- ? "rgba(34,227,232,0.12)"
343
- : "transparent",
344
- borderLeft: isSelected
345
- ? "1px solid transparent"
346
- : isPickerOpen
347
- ? "1px solid rgba(34,227,232,0.3)"
348
- : "1px solid rgba(255,255,255,0.12)",
349
- borderBottom: isSelected
350
- ? "1px solid transparent"
351
- : isPickerOpen
352
- ? "1px solid rgba(34,227,232,0.3)"
353
- : "1px solid rgba(255,255,255,0.06)",
354
- borderTop: "1px solid transparent",
355
- borderRight: "1px solid transparent",
356
- }}
357
- >
358
- <div className="flex items-center gap-1.5 min-w-0 flex-shrink">
359
- {isSelected && (
360
- <SelectedCheck />
388
+ {/* Chevron */}
389
+ <motion.div animate={{ rotate: isCollapsed ? 0 : 180 }} transition={{ duration: 0.25 }} className="flex-shrink-0">
390
+ <ChevronDown size={14} className="text-white/40" />
391
+ </motion.div>
392
+ </div>
393
+
394
+ {/* Collapsed summary row */}
395
+ <AnimatePresence initial={false}>
396
+ {isCollapsed && (
397
+ <motion.div className="overflow-hidden" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: "easeInOut" }}>
398
+ <div className="px-2 py-1.5 flex items-center justify-between">
399
+ <div className="flex items-center gap-1.5">
400
+ {selectedOpt ? (
401
+ <>
402
+ <SelectedCheck />
403
+ <span className="text-[10px] text-white font-semibold" style={OUTFIT}>{selectedOpt.label}</span>
404
+ </>
405
+ ) : (
406
+ <span className="text-[10px] text-white/30 font-medium" style={OUTFIT}>
407
+ {ignoredMarkets.has(mIdx) ? "Ignored" : "No selection"}
408
+ </span>
361
409
  )}
362
- <span className="text-[10px] text-white font-semibold truncate" style={OUTFIT}>{opt.label}</span>
363
410
  </div>
364
- {/* When picker is open: only show multiplier in cyan */}
365
- {showPickerRow ? (
366
- <span className="text-[10px] font-bold text-[#22E3E8]" style={OUTFIT}>
367
- {opt.odds}x
368
- </span>
369
- ) : (
370
- <div className="flex items-center gap-[3px] flex-shrink-0">
371
- <span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.4)" }}>
372
- {opt.odds}x
373
- </span>
374
- <span className="text-[7px] text-white/25" style={OUTFIT}>(</span>
375
- <PointsIcon size={7} />
376
- <span className="text-[8px] font-medium" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.35)" }}>
377
- {isSelected ? selection.amount : opt.entry}
378
- </span>
379
- <span className="text-[8px]" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.2)" }}>{"\u2192"}</span>
380
- <PointsIcon size={7} />
381
- <span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: isSelected ? "white" : "#22E3E8" }}>
382
- {isSelected ? displayReward : baseReward}
383
- </span>
384
- <span className="text-[7px] text-white/25" style={OUTFIT}>)</span>
411
+ {selectedOpt && (
412
+ <div className="flex items-center gap-[3px]">
413
+ {betAmount > 0 ? (
414
+ <>
415
+ <PointsIcon size={7} />
416
+ <span className="text-[9px] text-white font-semibold" style={OUTFIT}>{betAmount}</span>
417
+ <span className="text-[8px] text-white/40" style={OUTFIT}>×</span>
418
+ <span className="text-[9px] text-white/60 font-semibold" style={OUTFIT}>{selectedOpt.odds}</span>
419
+ <span className="text-[8px] text-white/40" style={OUTFIT}>=</span>
420
+ <PointsIcon size={7} />
421
+ <span className="text-[9px] text-[#22E3E8] font-bold" style={OUTFIT}>{reward}</span>
422
+ </>
423
+ ) : (
424
+ <span className="text-[9px] text-white/40 font-semibold" style={OUTFIT}>{selectedOpt.odds}x</span>
425
+ )}
385
426
  </div>
386
427
  )}
387
428
  </div>
388
- </div>
389
- );
390
- })}
391
-
392
- {/* AI Insights button — right-aligned, below options */}
393
- <div className="col-span-2 flex justify-end pr-4 mt-3">
394
- <AiInsightButton
395
- question={market.question}
396
- options={market.options.map(o => o.label)}
397
- />
398
- </div>
429
+ </motion.div>
430
+ )}
431
+ </AnimatePresence>
399
432
 
400
- {/* Amount pickerinline chip carousel (always reserves height when picker is open) */}
401
- {isPickerOpenForMarket && (
402
- <div className="col-span-2 mt-0.5 relative">
403
- {/* Onboarding shine overlay on first market */}
404
- {mIdx === 0 && showOnboarding && (
405
- <div className="absolute inset-0 z-10 pointer-events-none rounded overflow-hidden">
406
- <motion.div
407
- className="absolute inset-0"
408
- style={{
409
- background: "linear-gradient(90deg, transparent 0%, rgba(34,227,232,0.25) 40%, rgba(255,255,255,0.35) 50%, rgba(34,227,232,0.25) 60%, transparent 100%)",
410
- }}
411
- animate={{ x: ["-100%", "100%"] }}
412
- transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 1.5 }}
413
- />
414
- </div>
415
- )}
416
- {chipCount > 0 ? (
417
- <div className="flex items-center gap-1.5 overflow-x-auto scrollbar-hide py-1 px-0.5">
418
- <span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
419
- {Array.from({ length: chipCount }, (_, i) => (i + 1) * config.parlayConfig.stakeIncrements).map(amt => {
420
- const isChipSelected = selection?.amount === amt;
433
+ {/* Expanded viewoptions grid + picker */}
434
+ <AnimatePresence initial={false}>
435
+ {!isCollapsed && (
436
+ <motion.div className="overflow-hidden" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.25, ease: "easeInOut" }}>
437
+ <div className="grid grid-cols-2 gap-1.5 px-1 pt-0.5 pb-1">
438
+ {market.options.map((opt, j) => {
439
+ const isSelected = selection?.optionIdx === j;
440
+ const isPickerOpen = isPickerOpenForMarket && selection?.optionIdx === j;
441
+ const showPickerRow = isPickerOpenForMarket;
442
+ const displayReward = calcDisplayReward(opt, compoundMultiplier, isSelected, selectedCount, isSelected ? selection.amount : undefined);
443
+ const baseReward = optionReward(opt);
444
+ const optionClickable = interactive;
421
445
  return (
422
- <div
423
- key={amt}
424
- onClick={() => onAmountSelect(mIdx, selection!.optionIdx, amt)}
425
- className="flex-shrink-0 flex items-center gap-0.5 px-2 py-[3px] rounded cursor-pointer transition-colors relative overflow-hidden"
426
- style={isChipSelected ? {
427
- background: "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)",
428
- } : {
429
- background: "rgba(34,227,232,0.08)",
430
- border: "1px solid rgba(34,227,232,0.2)",
431
- }}
432
- >
433
- {isChipSelected && (
434
- <div
435
- className="absolute inset-0 pointer-events-none animate-shine"
436
- style={{
437
- background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%)",
438
- backgroundSize: "200% 100%",
439
- }}
440
- />
441
- )}
442
- <Image src="/iamgame_square_logo.jpg" alt="" width={7} height={7} className="rounded-[1px] relative z-[1]" />
443
- <span
444
- className={`text-[9px] font-bold relative z-[1] ${isChipSelected ? "text-black" : "text-[#22E3E8]"}`}
445
- style={OUTFIT}
446
+ <div key={j} className="col-span-1 flex flex-col">
447
+ <div
448
+ onClick={() => { if (optionClickable) onOptionClick(mIdx, j); }}
449
+ className="flex items-center justify-between px-2 py-1.5 rounded-sm transition-all duration-300"
450
+ style={{
451
+ cursor: optionClickable ? "pointer" : "default",
452
+ opacity: optionClickable ? 1 : (isSelected ? 1 : 0.5),
453
+ background: isSelected ? "linear-gradient(135deg, #22E3E8, #9945FF)" : isPickerOpen ? "rgba(34,227,232,0.12)" : "transparent",
454
+ borderLeft: isSelected ? "1px solid transparent" : isPickerOpen ? "1px solid rgba(34,227,232,0.3)" : "1px solid rgba(255,255,255,0.12)",
455
+ borderBottom: isSelected ? "1px solid transparent" : isPickerOpen ? "1px solid rgba(34,227,232,0.3)" : "1px solid rgba(255,255,255,0.06)",
456
+ borderTop: "1px solid transparent",
457
+ borderRight: "1px solid transparent",
458
+ }}
446
459
  >
447
- {amt}
448
- </span>
460
+ <div className="flex items-center gap-1.5 min-w-0 flex-shrink">
461
+ {isSelected && <SelectedCheck />}
462
+ <span className="text-[10px] text-white font-semibold truncate" style={OUTFIT}>{opt.label}</span>
463
+ </div>
464
+ {showPickerRow ? (
465
+ <span className="text-[10px] font-bold text-[#22E3E8]" style={OUTFIT}>{opt.odds}x</span>
466
+ ) : (
467
+ <div className="flex items-center gap-[3px] flex-shrink-0">
468
+ <span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.4)" }}>{opt.odds}x</span>
469
+ <span className="text-[7px] text-white/25" style={OUTFIT}>(</span>
470
+ <PointsIcon size={7} />
471
+ <span className="text-[8px] font-medium" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.35)" }}>{isSelected ? selection.amount : opt.entry}</span>
472
+ <span className="text-[8px]" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.2)" }}>{"\u2192"}</span>
473
+ <PointsIcon size={7} />
474
+ <span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: isSelected ? "white" : "#22E3E8" }}>{isSelected ? displayReward : baseReward}</span>
475
+ <span className="text-[7px] text-white/25" style={OUTFIT}>)</span>
476
+ </div>
477
+ )}
478
+ </div>
449
479
  </div>
450
480
  );
451
481
  })}
452
- </div>
453
- ) : (
454
- /* All points spent blurred placeholder chips with overlay message */
455
- <div className="relative py-1 px-0.5">
456
- <div className="flex items-center gap-1.5 overflow-hidden" style={{ filter: "blur(3px)" }}>
457
- <span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
458
- {[50, 100, 150, 200, 250, 300, 350].map(amt => (
459
- <div
460
- key={amt}
461
- className="flex-shrink-0 flex items-center gap-0.5 px-2 py-[3px] rounded"
462
- style={{ background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.2)" }}
463
- >
464
- <PointsIcon size={7} />
465
- <span className="text-[9px] font-bold text-[#22E3E8]" style={OUTFIT}>{amt}</span>
466
- </div>
467
- ))}
468
- </div>
469
- <div className="absolute inset-0 flex items-center justify-center">
470
- <p className="text-[9px] text-[#22E3E8] font-semibold flex items-center gap-1" style={OUTFIT}>
471
- All {config.startingBalance.toLocaleString()}
472
- <PointsIcon size={9} />
473
- points used. Reduce other bets to bet here.
474
- </p>
482
+
483
+ {/* AI Insights */}
484
+ <div className="col-span-2 flex justify-end pr-4 mt-3">
485
+ <AiInsightButton question={market.question} options={market.options.map(o => o.label)} />
475
486
  </div>
476
- </div>
477
- )}
478
- </div>
479
- )}
480
487
 
481
- {/* Onboarding dialogue box — first market only, stays until user bets */}
482
- <AnimatePresence>
483
- {mIdx === 0 && showOnboarding && (
484
- <motion.div
485
- className="col-span-2 mt-1 mb-0.5"
486
- initial={{ opacity: 0, height: 0 }}
487
- animate={{ opacity: 1, height: "auto" }}
488
- exit={{ opacity: 0, height: 0 }}
489
- transition={{ type: "spring", stiffness: 300, damping: 28 }}
490
- >
491
- <div
492
- className="rounded-md px-2.5 py-1.5"
493
- style={{ background: "#22E3E8" }}
494
- >
495
- <p className="text-[10px] text-black font-semibold leading-relaxed" style={OUTFIT}>
496
- {onboardingWords.map((word, i) => (
497
- <motion.span
498
- key={i}
499
- className="inline"
500
- initial={{ opacity: 0, y: 6, filter: "blur(4px)" }}
501
- animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
502
- transition={{ duration: 0.35, delay: 0.4 + i * 0.045, ease: "easeOut" }}
503
- >
504
- {word}{" "}
505
- </motion.span>
506
- ))}
507
- </p>
488
+ {/* Amount picker */}
489
+ {isPickerOpenForMarket && interactive && (
490
+ <div className="col-span-2 mt-0.5 relative">
491
+ {mIdx === 0 && showOnboarding && (
492
+ <div className="absolute inset-0 z-10 pointer-events-none rounded overflow-hidden">
493
+ <motion.div className="absolute inset-0"
494
+ style={{ background: "linear-gradient(90deg, transparent 0%, rgba(34,227,232,0.25) 40%, rgba(255,255,255,0.35) 50%, rgba(34,227,232,0.25) 60%, transparent 100%)" }}
495
+ animate={{ x: ["-100%", "100%"] }}
496
+ transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 1.5 }} />
497
+ </div>
498
+ )}
499
+ {chipCount > 0 ? (
500
+ <div className="flex items-center gap-1.5 overflow-x-auto scrollbar-hide py-1 px-0.5">
501
+ <span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
502
+ {Array.from({ length: chipCount }, (_, i) => (i + 1) * config.parlayConfig.stakeIncrements).map(amt => {
503
+ const isChipSelected = selection?.amount === amt;
504
+ return (
505
+ <div key={amt}
506
+ onClick={() => onAmountSelect(mIdx, selection!.optionIdx, amt)}
507
+ className="flex-shrink-0 flex items-center gap-0.5 px-2 py-[3px] rounded cursor-pointer transition-colors relative overflow-hidden"
508
+ style={isChipSelected ? { background: "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)" } : { background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.2)" }}>
509
+ {isChipSelected && (
510
+ <div className="absolute inset-0 pointer-events-none animate-shine"
511
+ style={{ background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%)", backgroundSize: "200% 100%" }} />
512
+ )}
513
+ <Image src="/iamgame_square_logo.jpg" alt="" width={7} height={7} className="rounded-[1px] relative z-[1]" />
514
+ <span className={`text-[9px] font-bold relative z-[1] ${isChipSelected ? "text-black" : "text-[#22E3E8]"}`} style={OUTFIT}>{amt}</span>
515
+ </div>
516
+ );
517
+ })}
518
+ </div>
519
+ ) : (
520
+ <div className="relative py-1 px-0.5">
521
+ <div className="flex items-center gap-1.5 overflow-hidden" style={{ filter: "blur(3px)" }}>
522
+ <span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
523
+ {[50, 100, 150, 200, 250, 300, 350].map(amt => (
524
+ <div key={amt} className="flex-shrink-0 flex items-center gap-0.5 px-2 py-[3px] rounded"
525
+ style={{ background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.2)" }}>
526
+ <PointsIcon size={7} />
527
+ <span className="text-[9px] font-bold text-[#22E3E8]" style={OUTFIT}>{amt}</span>
528
+ </div>
529
+ ))}
530
+ </div>
531
+ <div className="absolute inset-0 flex items-center justify-center">
532
+ <p className="text-[9px] text-[#22E3E8] font-semibold flex items-center gap-1" style={OUTFIT}>
533
+ All {config.startingBalance.toLocaleString()} <PointsIcon size={9} /> points used. Reduce other bets to bet here.
534
+ </p>
535
+ </div>
536
+ </div>
537
+ )}
538
+ </div>
539
+ )}
540
+
541
+ {/* Onboarding dialogue */}
542
+ <AnimatePresence>
543
+ {mIdx === 0 && showOnboarding && (
544
+ <motion.div className="col-span-2 mt-1 mb-0.5" initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }}
545
+ transition={{ type: "spring", stiffness: 300, damping: 28 }}>
546
+ <div className="rounded-md px-2.5 py-1.5" style={{ background: "#22E3E8" }}>
547
+ <p className="text-[10px] text-black font-semibold leading-relaxed" style={OUTFIT}>
548
+ {onboardingWords.map((word, i) => (
549
+ <motion.span key={i} className="inline" initial={{ opacity: 0, y: 6, filter: "blur(4px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
550
+ transition={{ duration: 0.35, delay: 0.4 + i * 0.045, ease: "easeOut" }}>{word}{" "}</motion.span>
551
+ ))}
552
+ </p>
553
+ </div>
554
+ </motion.div>
555
+ )}
556
+ </AnimatePresence>
508
557
  </div>
509
558
  </motion.div>
510
559
  )}
511
560
  </AnimatePresence>
512
- </div>
513
-
514
- </motion.div>
515
- )}
516
- </AnimatePresence>
517
- </div>
518
- );
519
- })}
561
+ </motion.div>
562
+ );
563
+ })}
564
+ </AnimatePresence>
520
565
  </div>
521
566
  </div>
522
567
 
523
568
  {!marketsOnly && (
524
569
  <>
525
- {/* Sticky bottom summary bar */}
526
- <div
527
- className="sticky bottom-0 z-10 -mx-4 px-4 pt-3 pb-5"
528
- style={{ background: "linear-gradient(180deg, transparent 0%, #0a0a12 12%, #0a0a12 100%)" }}
529
- >
570
+ {/* ── Sticky bottom summary bar ─────────────────────────────────────── */}
571
+ <div className="sticky bottom-0 z-10 -mx-4 px-4 pt-3 pb-5"
572
+ style={{ background: "linear-gradient(180deg, transparent 0%, #0a0a12 12%, #0a0a12 100%)" }}>
573
+
530
574
  <div className="flex items-stretch justify-between gap-3">
531
- {/* Left — two text lines stacked */}
575
+ {/* Left — text area */}
532
576
  <div className="flex flex-col gap-2 flex-1 min-w-0">
577
+ {/* Remaining balance */}
533
578
  <AnimatePresence>
534
579
  {remainingBalance > 0 && (
535
- <motion.div
536
- className="flex items-baseline gap-1"
537
- initial={{ scale: 1.15, opacity: 0 }}
538
- animate={{ scale: 1, opacity: 1 }}
539
- exit={{ opacity: 0, height: 0 }}
540
- transition={{ type: "spring", stiffness: 300, damping: 18, delay: 0.2 }}
541
- >
580
+ <motion.div className="flex items-baseline gap-1"
581
+ initial={{ scale: 1.15, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ opacity: 0, height: 0 }}
582
+ transition={{ type: "spring", stiffness: 300, damping: 18, delay: 0.2 }}>
542
583
  <Image src="/iamgame_square_logo.jpg" alt="" width={totalEntry > 0 ? 14 : 20} height={totalEntry > 0 ? 14 : 20} className="rounded-[3px] transition-all duration-500" />
543
584
  <AnimatedNumber value={remainingBalance} className="text-white font-bold leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 16 : 24 }} />
544
585
  <span className="text-white font-bold leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 16 : 24 }}>/{config.startingBalance.toLocaleString()}</span>
545
- <span className="text-white font-medium leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 9 : 11 }}>
546
- points left to bet
547
- </span>
586
+ <span className="text-white font-medium leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 9 : 11 }}>points left to bet</span>
548
587
  </motion.div>
549
588
  )}
550
589
  </AnimatePresence>
590
+
591
+ {/* Saved max outcome */}
551
592
  <AnimatePresence>
552
- {totalEntry > 0 && (
553
- <motion.div
554
- className="overflow-hidden"
555
- initial={{ height: 0, opacity: 0 }}
556
- animate={{ height: "auto", opacity: 1 }}
557
- exit={{ height: 0, opacity: 0 }}
558
- transition={{ height: { duration: 0.35, ease: "easeOut" }, opacity: { duration: 0.3, delay: 0.1 } }}
559
- >
593
+ {savedBetSummary && savedBetSummary.totalEntry > 0 && (
594
+ <motion.div className="overflow-hidden" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }}
595
+ transition={{ height: { duration: 0.35, ease: "easeOut" }, opacity: { duration: 0.3, delay: 0.1 } }}>
560
596
  <div className="flex items-baseline gap-1 flex-nowrap">
561
- <span className="text-[13px] text-white font-medium leading-none flex-shrink-0" style={OUTFIT}>Max outcome</span>
597
+ <span className="text-[11px] text-white/50 font-medium leading-none flex-shrink-0" style={OUTFIT}>Saved</span>
598
+ <Image src="/iamgame_square_logo.jpg" alt="" width={14} height={14} className="rounded-[2px] flex-shrink-0" />
599
+ <AnimatedNumber value={savedBetSummary.totalEntry} className="text-[16px] text-white/60 font-bold leading-none" style={OUTFIT} />
600
+ <span className="text-[16px] text-white/40 font-bold leading-none flex-shrink-0" style={OUTFIT}>→</span>
601
+ <Image src="/iamgame_square_logo.jpg" alt="" width={14} height={14} className="rounded-[2px] flex-shrink-0" />
602
+ <AnimatedNumber value={savedBetSummary.compoundedReward} className="text-[16px] text-[#22E3E8]/60 font-bold leading-none" style={OUTFIT} />
603
+ </div>
604
+ </motion.div>
605
+ )}
606
+ </AnimatePresence>
607
+
608
+ {/* Current/unsaved max outcome */}
609
+ <AnimatePresence>
610
+ {totalEntry > 0 && (!hasSubmitted || hasUnsavedEdits) && (
611
+ <motion.div className="overflow-hidden" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }}
612
+ transition={{ height: { duration: 0.35, ease: "easeOut" }, opacity: { duration: 0.3, delay: 0.1 } }}>
613
+ <div className="flex items-baseline gap-1 flex-nowrap">
614
+ <span className="text-[13px] text-white font-medium leading-none flex-shrink-0" style={OUTFIT}>
615
+ {hasSubmitted ? "Pending" : "Max outcome"}
616
+ </span>
562
617
  <Image src="/iamgame_square_logo.jpg" alt="" width={18} height={18} className="rounded-[2px] flex-shrink-0" />
563
618
  <AnimatedNumber value={totalEntry} className="text-[22px] text-white font-bold leading-none" style={OUTFIT} />
564
619
  <span className="text-[22px] text-white font-bold leading-none flex-shrink-0" style={OUTFIT}>→</span>
565
620
  <Image src="/iamgame_square_logo.jpg" alt="" width={18} height={18} className="rounded-[2px] flex-shrink-0" />
566
621
  <span className="relative inline-flex items-baseline overflow-visible">
567
- {/* Shine sweep — clipped to text via mix-blend */}
568
622
  <span className="absolute inset-0 pointer-events-none overflow-hidden" style={{ WebkitMaskImage: "linear-gradient(90deg, transparent 0%, black 10%, black 90%, transparent 100%)" }} aria-hidden>
569
- <motion.span
570
- className="absolute inset-y-0 w-[40%]"
623
+ <motion.span className="absolute inset-y-0 w-[40%]"
571
624
  style={{ background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent)" }}
572
625
  animate={{ left: ["-40%", "140%"] }}
573
- transition={{ duration: 2, ease: "easeInOut", repeat: Infinity, repeatDelay: 3.5 }}
574
- />
626
+ transition={{ duration: 2, ease: "easeInOut", repeat: Infinity, repeatDelay: 3.5 }} />
575
627
  </span>
576
- {/* Floating particles — tiny, spread all over */}
577
628
  {Array.from({ length: 12 }, (_, p) => {
578
629
  const size = 1 + (p % 3) * 0.4;
579
630
  const left = (p / 12) * 100 + (p % 3) * 3;
580
631
  const top = 10 + (p * 7) % 80;
581
632
  return (
582
- <motion.span
583
- key={p}
584
- className="absolute rounded-full pointer-events-none"
585
- style={{
586
- width: size,
587
- height: size,
588
- background: p % 3 === 0 ? "rgba(255,255,255,0.9)" : "#22E3E8",
589
- left: `${left}%`,
590
- top: `${top}%`,
591
- }}
592
- animate={{
593
- y: [0, -4 - (p % 3) * 1.5, 0],
594
- x: [0, (p % 2 === 0 ? 1.5 : -1.5), 0],
595
- opacity: [0, 0.4, 1, 0.4, 0],
596
- }}
597
- transition={{
598
- duration: 4 + (p % 4) * 0.8,
599
- repeat: Infinity,
600
- delay: (p % 8) * 0.7,
601
- ease: "easeInOut",
602
- }}
603
- />
633
+ <motion.span key={p} className="absolute rounded-full pointer-events-none"
634
+ style={{ width: size, height: size, background: p % 3 === 0 ? "rgba(255,255,255,0.9)" : "#22E3E8", left: `${left}%`, top: `${top}%` }}
635
+ animate={{ y: [0, -4 - (p % 3) * 1.5, 0], x: [0, (p % 2 === 0 ? 1.5 : -1.5), 0], opacity: [0, 0.4, 1, 0.4, 0] }}
636
+ transition={{ duration: 4 + (p % 4) * 0.8, repeat: Infinity, delay: (p % 8) * 0.7, ease: "easeInOut" }} />
604
637
  );
605
638
  })}
606
639
  <AnimatedNumber value={compoundedReward} className="text-[22px] text-[#22E3E8] font-bold leading-none relative" style={OUTFIT} />
@@ -610,103 +643,69 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
610
643
  )}
611
644
  </AnimatePresence>
612
645
  </div>
613
- {/* Right — action button, hidden until points are bet */}
614
- <AnimatePresence>
615
- {totalEntry > 0 && (() => {
616
- const spentPercent = totalEntry / config.startingBalance;
617
- const fillPercent = buttonState === "saved" ? 100 : 5 + spentPercent * 95;
618
- const fillColor = "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)";
619
- const bgColor = "linear-gradient(135deg, rgba(34,227,232,0.2), rgba(153,69,255,0.2), rgba(248,60,197,0.2))";
620
646
 
621
- return (
622
- <motion.button
623
- onClick={onSubmit ? onSubmit : undefined}
624
- disabled={submitting}
625
- className="relative w-[65px] min-h-[54px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center py-3"
626
- style={{ background: bgColor, cursor: "pointer" }}
627
- initial={{ opacity: 0, width: 0 }}
628
- animate={{ opacity: 1, width: 65 }}
629
- exit={{ opacity: 0, width: 0 }}
630
- transition={{ width: { duration: 0.4, ease: "easeOut", delay: 0.8 }, opacity: { duration: 0.3, delay: 1 } }}
631
- >
632
- {/* Fill bar */}
633
- <motion.div
634
- className="absolute inset-y-0 left-0"
635
- animate={{ width: `${fillPercent}%` }}
636
- transition={{ type: "spring", stiffness: 120, damping: 20 }}
637
- style={{ background: fillColor }}
638
- />
639
- {/* Pulsing ring when there are unsaved changes */}
640
- {buttonState === "changed" && (
641
- <motion.div
642
- className="absolute inset-0 rounded-xl"
643
- animate={{ boxShadow: ["0 0 0px rgba(34,227,232,0)", "0 0 10px rgba(34,227,232,0.7)", "0 0 0px rgba(34,227,232,0)"] }}
644
- transition={{ duration: 1.4, repeat: Infinity, ease: "easeInOut" }}
645
- />
646
- )}
647
- {/* Icon */}
648
- {submitting
649
- ? <Loader2 size={22} strokeWidth={2.5} className="relative z-10 animate-spin text-white/70" />
650
- : buttonState === "saved"
651
- ? <Pencil size={20} strokeWidth={2} className="relative z-10 text-white/80" />
652
- : <Play size={26} fill="white" strokeWidth={0} className="relative z-10" />
653
- }
654
- </motion.button>
655
- );
656
- })()}
647
+ {/* Right — action button */}
648
+ <AnimatePresence>
649
+ {buttonState === "fresh" && totalEntry > 0 && (
650
+ <motion.button
651
+ onClick={onSubmit ? onSubmit : undefined}
652
+ disabled={submitting}
653
+ className="relative w-[65px] min-h-[54px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center py-3"
654
+ style={{ background: "linear-gradient(135deg, rgba(34,227,232,0.2), rgba(153,69,255,0.2), rgba(248,60,197,0.2))", cursor: "pointer" }}
655
+ initial={{ opacity: 0, width: 0 }}
656
+ animate={{ opacity: 1, width: 65 }}
657
+ exit={{ opacity: 0, width: 0 }}
658
+ transition={{ width: { duration: 0.4, ease: "easeOut", delay: 0.8 }, opacity: { duration: 0.3, delay: 1 } }}>
659
+ <motion.div className="absolute inset-y-0 left-0"
660
+ animate={{ width: `${5 + (totalEntry / config.startingBalance) * 95}%` }}
661
+ transition={{ type: "spring", stiffness: 120, damping: 20 }}
662
+ style={{ background: "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)" }} />
663
+ {submitting
664
+ ? <Loader2 size={22} strokeWidth={2.5} className="relative z-10 animate-spin text-white/70" />
665
+ : <Play size={26} fill="white" strokeWidth={0} className="relative z-10" />}
666
+ </motion.button>
667
+ )}
668
+ {buttonState === "submitted" && (
669
+ <motion.div
670
+ className="w-[54px] min-h-[54px] rounded-xl flex-shrink-0 flex items-center justify-center"
671
+ style={{ background: "rgba(34,197,94,0.12)", border: "1px solid rgba(34,197,94,0.3)" }}
672
+ initial={{ opacity: 0, scale: 0.8 }}
673
+ animate={{ opacity: 1, scale: 1 }}
674
+ transition={{ type: "spring", stiffness: 300, damping: 20 }}>
675
+ <div className="w-[28px] h-[28px] rounded-full flex items-center justify-center" style={{ background: "rgba(34,197,94,0.25)" }}>
676
+ <Check size={16} className="text-green-400" />
677
+ </div>
678
+ </motion.div>
679
+ )}
657
680
  </AnimatePresence>
658
681
  </div>
659
682
 
660
- {/* Risk bar + tip — hidden until risk > 0, staggered entrance */}
683
+ {/* Risk bars */}
661
684
  <AnimatePresence>
662
685
  {riskPercent > 0 && (
663
- <motion.div
664
- className="overflow-hidden"
665
- initial={{ height: 0 }}
666
- animate={{ height: "auto" }}
667
- exit={{ height: 0 }}
668
- transition={{ height: { duration: 0.4, ease: "easeOut", delay: 1.8 } }}
669
- >
686
+ <motion.div className="overflow-hidden" initial={{ height: 0 }} animate={{ height: "auto" }} exit={{ height: 0 }}
687
+ transition={{ height: { duration: 0.4, ease: "easeOut", delay: 1.8 } }}>
670
688
  <div className="flex flex-col gap-2 mt-3">
671
- {/* Risk factor bar */}
672
- <motion.div
673
- className="flex items-center gap-2"
674
- initial={{ opacity: 0 }}
675
- animate={{ opacity: 1 }}
676
- transition={{ duration: 0.4, delay: 2.2 }}
677
- >
678
- <span className="text-[8px] text-white font-semibold flex-shrink-0" style={OUTFIT}>Risk</span>
689
+ {/* Saved risk bar (shown when submitted and unsaved edits exist) */}
690
+ {savedBetSummary && hasUnsavedEdits && savedBetSummary.riskPercent > 0 && (
691
+ <motion.div className="flex items-center gap-2" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }}>
692
+ <span className="text-[8px] text-white/40 font-semibold flex-shrink-0 w-[38px]" style={OUTFIT}>Saved</span>
693
+ <div className="flex-1 h-[4px] rounded-full bg-white/[0.04] overflow-hidden">
694
+ <div className="h-full rounded-full" style={{ width: `${savedBetSummary.riskPercent}%`, background: "linear-gradient(90deg, #f59e0b, #ef4444)", opacity: 0.5, transition: "width 0.4s ease" }} />
695
+ </div>
696
+ <span className="text-[8px] text-white/40 font-bold flex-shrink-0" style={OUTFIT}>{savedBetSummary.riskPercent}%</span>
697
+ </motion.div>
698
+ )}
699
+ {/* Current risk bar */}
700
+ <motion.div className="flex items-center gap-2" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.4, delay: 2.2 }}>
701
+ <span className="text-[8px] text-white font-semibold flex-shrink-0 w-[38px]" style={OUTFIT}>
702
+ {savedBetSummary && hasUnsavedEdits ? "Now" : "Risk"}
703
+ </span>
679
704
  <div className="flex-1 h-[5px] rounded-full bg-white/[0.06] overflow-hidden">
680
- <div
681
- className="h-full rounded-full"
682
- style={{
683
- width: `${riskPercent}%`,
684
- background: "linear-gradient(90deg, #f59e0b, #ef4444)",
685
- transition: "width 0.4s ease",
686
- }}
687
- />
705
+ <div className="h-full rounded-full" style={{ width: `${riskPercent}%`, background: "linear-gradient(90deg, #f59e0b, #ef4444)", transition: "width 0.4s ease" }} />
688
706
  </div>
689
707
  <span className="text-[8px] text-white font-bold flex-shrink-0" style={OUTFIT}>{riskPercent}%</span>
690
708
  </motion.div>
691
- {/* Tip */}
692
- <motion.p
693
- className="text-[8px] text-white font-medium text-center mt-0.5"
694
- style={OUTFIT}
695
- initial={{ opacity: 0 }}
696
- animate={{ opacity: 1 }}
697
- transition={{ duration: 0.4, delay: 3 }}
698
- >
699
- {riskPercent > 80
700
- ? "Extreme variance — outcomes swing dramatically either way"
701
- : riskPercent > 60
702
- ? "High variance picks — significant upside and downside"
703
- : riskPercent > 40
704
- ? "Mixed variance — a blend of safe and risky selections"
705
- : riskPercent > 20
706
- ? "Mild variance — mostly conservative picks"
707
- : "Low variance — steady selections with modest returns"
708
- }
709
- </motion.p>
710
709
  </div>
711
710
  </motion.div>
712
711
  )}
@@ -725,7 +724,6 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
725
724
  Every leg must hit. One miss and the entire combined bet is lost.
726
725
  </p>
727
726
 
728
- {/* Parlay slots — concentric circles */}
729
727
  <div className="flex items-end gap-3">
730
728
  {Array.from({ length: config.parlayConfig.maxSlots }, (_, slot) => {
731
729
  const legs = parlayGroups[slot] ?? [];
@@ -738,58 +736,29 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
738
736
 
739
737
  return (
740
738
  <div key={slot} className="flex flex-col items-center gap-1">
741
- {/* Concentric circles */}
742
- <button
743
- onClick={() => setEditingParlay(isEditing ? null : slot)}
744
- className="relative flex items-center justify-center transition-all"
745
- style={{
746
- width: 36,
747
- height: 36,
748
- }}
749
- >
739
+ <button onClick={() => setEditingParlay(isEditing ? null : slot)}
740
+ className="relative flex items-center justify-center transition-all" style={{ width: 36, height: 36 }}>
750
741
  {legCount === 0 ? (
751
- /* Empty slot dashed outline */
752
- <div
753
- className="w-[28px] h-[28px] rounded-full border border-dashed transition-all"
754
- style={{
755
- borderColor: isEditing ? color : "rgba(255,255,255,0.15)",
756
- boxShadow: isEditing ? `0 0 8px ${color}40` : "none",
757
- }}
758
- />
742
+ <div className="w-[28px] h-[28px] rounded-full border border-dashed transition-all"
743
+ style={{ borderColor: isEditing ? color : "rgba(255,255,255,0.15)", boxShadow: isEditing ? `0 0 8px ${color}40` : "none" }} />
759
744
  ) : (
760
- /* Filled concentric rings */
761
745
  <>
762
746
  {Array.from({ length: Math.min(legCount, 6) }, (_, ring) => {
763
747
  const size = 12 + ring * 5;
764
748
  const opacity = 1 - ring * 0.12;
765
749
  return (
766
- <motion.div
767
- key={ring}
768
- initial={{ scale: 0 }}
769
- animate={{ scale: 1 }}
750
+ <motion.div key={ring} initial={{ scale: 0 }} animate={{ scale: 1 }}
770
751
  transition={{ type: "spring", stiffness: 400, damping: 20, delay: ring * 0.05 }}
771
752
  className="absolute rounded-full"
772
- style={{
773
- width: size,
774
- height: size,
775
- border: `2px solid ${color}`,
776
- opacity,
777
- background: ring === 0 ? `${color}30` : "transparent",
778
- boxShadow: isEditing ? `0 0 8px ${color}40` : "none",
779
- }}
780
- />
753
+ style={{ width: size, height: size, border: `2px solid ${color}`, opacity, background: ring === 0 ? `${color}30` : "transparent", boxShadow: isEditing ? `0 0 8px ${color}40` : "none" }} />
781
754
  );
782
755
  })}
783
756
  </>
784
757
  )}
785
758
  </button>
786
-
787
- {/* Multiplier + info */}
788
759
  {legCount >= 2 ? (
789
760
  <div className="flex items-center gap-0.5">
790
- <span className="text-[8px] font-bold text-white" style={OUTFIT}>
791
- {multiplier}x
792
- </span>
761
+ <span className="text-[8px] font-bold text-white" style={OUTFIT}>{multiplier}x</span>
793
762
  <button onClick={() => setParlayInfo(parlayInfo === slot ? null : slot)}>
794
763
  <Info size={8} className="text-white/40" />
795
764
  </button>
@@ -797,22 +766,14 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
797
766
  ) : legCount === 1 ? (
798
767
  <span className="text-[7px] text-white/30 font-medium" style={OUTFIT}>+1 more</span>
799
768
  ) : null}
800
-
801
- {/* Info popup */}
802
769
  <AnimatePresence>
803
770
  {parlayInfo === slot && legCount >= 2 && (
804
- <motion.div
805
- initial={{ opacity: 0, y: 4 }}
806
- animate={{ opacity: 1, y: 0 }}
807
- exit={{ opacity: 0, y: 4 }}
771
+ <motion.div initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 4 }}
808
772
  className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 w-[180px] rounded-lg p-2.5"
809
- style={{ background: "#1a1a2e", border: `1px solid ${color}30` }}
810
- >
773
+ style={{ background: "#1a1a2e", border: `1px solid ${color}30` }}>
811
774
  <div className="flex items-center justify-between mb-1.5">
812
775
  <span className="text-[8px] text-white font-bold" style={OUTFIT}>Combined Bet</span>
813
- <button onClick={() => setParlayInfo(null)}>
814
- <X size={10} className="text-white/40" />
815
- </button>
776
+ <button onClick={() => setParlayInfo(null)}><X size={10} className="text-white/40" /></button>
816
777
  </div>
817
778
  {breakdown.map((leg, i) => (
818
779
  <div key={i} className="flex items-center justify-between py-0.5">
@@ -828,20 +789,12 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
828
789
  {totalStake > 0 && (
829
790
  <div className="flex items-center justify-between mt-0.5">
830
791
  <span className="text-[7px] text-white/40 font-medium" style={OUTFIT}>Stake → Win</span>
831
- <span className="text-[8px] text-white font-semibold" style={OUTFIT}>
832
- {totalStake} → {Math.round(totalStake * multiplier).toLocaleString()}
833
- </span>
792
+ <span className="text-[8px] text-white font-semibold" style={OUTFIT}>{totalStake} → {Math.round(totalStake * multiplier).toLocaleString()}</span>
834
793
  </div>
835
794
  )}
836
795
  <p className="text-[6px] text-white/25 mt-1" style={OUTFIT}>All must hit to win</p>
837
- {/* Clear parlay */}
838
- <button
839
- onClick={() => { clearParlay(slot); setParlayInfo(null); }}
840
- className="mt-1.5 text-[7px] text-red-400/60 font-medium"
841
- style={OUTFIT}
842
- >
843
- Remove combined bet
844
- </button>
796
+ <button onClick={() => { clearParlay(slot); setParlayInfo(null); }}
797
+ className="mt-1.5 text-[7px] text-red-400/60 font-medium" style={OUTFIT}>Remove combined bet</button>
845
798
  </motion.div>
846
799
  )}
847
800
  </AnimatePresence>
@@ -849,18 +802,13 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
849
802
  );
850
803
  })}
851
804
  </div>
852
-
853
- {/* Edit mode hint */}
854
805
  {editingParlay !== null && (
855
806
  <div className="flex items-center justify-between mt-2">
856
807
  <p className="text-[8px] font-medium" style={{ ...OUTFIT, color: parlayColors[editingParlay] }}>
857
808
  Tap questions above to add to this combined bet
858
809
  </p>
859
- <button
860
- onClick={() => setEditingParlay(null)}
861
- className="text-[8px] text-white/50 font-semibold px-2 py-0.5 rounded"
862
- style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)" }}
863
- >
810
+ <button onClick={() => setEditingParlay(null)}
811
+ className="text-[8px] text-white/50 font-semibold px-2 py-0.5 rounded" style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)" }}>
864
812
  Done
865
813
  </button>
866
814
  </div>
@@ -881,16 +829,8 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
881
829
  <button
882
830
  onClick={parlayEnabled ? () => setEditingParlay(0) : undefined}
883
831
  className="relative w-[65px] min-h-[54px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center py-3"
884
- style={{
885
- background: "linear-gradient(135deg, rgba(153,69,255,0.2), rgba(248,60,197,0.2), rgba(34,227,232,0.2))",
886
- cursor: parlayEnabled ? "pointer" : "default",
887
- opacity: parlayEnabled ? 1 : 0.5,
888
- }}
889
- >
890
- <div
891
- className="absolute inset-y-0 left-0 w-[30%]"
892
- style={{ background: "linear-gradient(135deg, #9945FF, #f83cc5, #22E3E8)" }}
893
- />
832
+ style={{ background: "linear-gradient(135deg, rgba(153,69,255,0.2), rgba(248,60,197,0.2), rgba(34,227,232,0.2))", cursor: parlayEnabled ? "pointer" : "default", opacity: parlayEnabled ? 1 : 0.5 }}>
833
+ <div className="absolute inset-y-0 left-0 w-[30%]" style={{ background: "linear-gradient(135deg, #9945FF, #f83cc5, #22E3E8)" }} />
894
834
  <Play size={26} fill="white" strokeWidth={0} className="relative z-10" />
895
835
  </button>
896
836
  </div>
@@ -916,25 +856,15 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
916
856
  <span className="text-[8px] text-white/20 font-semibold tracking-widest" style={OUTFIT}>&middot;&middot;&middot;</span>
917
857
  </div>
918
858
  )}
919
- <LeaderboardRow
920
- entry={entry}
921
- rank={entry.rank ?? i + 1}
922
- isLast={i === arr.length - 1}
923
- />
859
+ <LeaderboardRow entry={entry} rank={entry.rank ?? i + 1} isLast={i === arr.length - 1} />
924
860
  </div>
925
861
  ))}
926
862
  </div>
927
863
  {onViewLeaderboard && leaderboardRows.length > 0 && (
928
- <button
929
- onClick={onViewLeaderboard}
930
- className="mt-2 w-full text-center text-[9px] font-semibold"
931
- style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}
932
- >
933
- See Full Leaderboard →
934
- </button>
864
+ <button onClick={onViewLeaderboard} className="mt-2 w-full text-center text-[9px] font-semibold"
865
+ style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}>See Full Leaderboard →</button>
935
866
  )}
936
867
  </div>
937
-
938
868
  </>
939
869
  )}
940
870
  </div>