@devrongx/games 0.4.24 → 0.4.26
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
|
-
//
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
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,102 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
96
112
|
collapseInitializedRef.current = true;
|
|
97
113
|
}, [bets]);
|
|
98
114
|
|
|
99
|
-
//
|
|
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
|
+
// Category headers — only shown when no sort is active
|
|
177
|
+
const categoryForIndex = useMemo(() => {
|
|
178
|
+
const map = new Map<number, string>();
|
|
179
|
+
if (sortActive) return map;
|
|
180
|
+
let prev = "";
|
|
181
|
+
for (const mIdx of visibleMarketIndices) {
|
|
182
|
+
const cat = config.markets[mIdx].category;
|
|
183
|
+
if (cat !== prev) { map.set(mIdx, cat); prev = cat; }
|
|
184
|
+
}
|
|
185
|
+
return map;
|
|
186
|
+
}, [visibleMarketIndices, sortActive, config.markets]);
|
|
187
|
+
|
|
188
|
+
// ── Parlay state (unchanged) ─────────────────────────────────────────────
|
|
100
189
|
const [editingParlay, setEditingParlay] = useState<number | null>(null);
|
|
101
|
-
// Parlay info popup: which slot's breakdown is shown (null = none)
|
|
102
190
|
const [parlayInfo, setParlayInfo] = useState<number | null>(null);
|
|
103
|
-
|
|
104
191
|
const parlayColors = config.parlayConfig.colors;
|
|
105
|
-
|
|
106
|
-
// Derived from unified state
|
|
107
192
|
const parlayGroups = useMemo(() => deriveParlayGroups(bets), [bets]);
|
|
108
193
|
const marketToParlay = useMemo(() => deriveMarketToParlay(bets), [bets]);
|
|
109
194
|
const hasAnyParlays = Object.keys(parlayGroups).length > 0;
|
|
110
195
|
|
|
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
196
|
const toggleMarketInParlay = useCallback((mIdx: number) => {
|
|
114
197
|
if (editingParlay === null || !onBetsChange) return;
|
|
115
198
|
const slot = editingParlay;
|
|
116
199
|
const entry = bets[mIdx];
|
|
117
200
|
if (!entry) return;
|
|
118
|
-
|
|
119
|
-
// Block if market belongs to a different parlay
|
|
120
201
|
if (entry.parlaySlot !== null && entry.parlaySlot !== slot) return;
|
|
121
|
-
|
|
122
202
|
const next = { ...bets };
|
|
123
203
|
if (entry.parlaySlot === slot) {
|
|
124
|
-
// Remove from this parlay → individual
|
|
125
204
|
next[mIdx] = { ...entry, parlaySlot: null };
|
|
126
205
|
} else {
|
|
127
|
-
// Add to this parlay
|
|
128
206
|
next[mIdx] = { ...entry, parlaySlot: slot };
|
|
129
207
|
}
|
|
130
208
|
onBetsChange(next);
|
|
131
209
|
}, [editingParlay, bets, onBetsChange]);
|
|
132
210
|
|
|
133
|
-
// Clear a parlay slot — set all its markets back to individual
|
|
134
211
|
const clearParlay = useCallback((slot: number) => {
|
|
135
212
|
if (!onBetsChange) return;
|
|
136
213
|
const next = { ...bets };
|
|
@@ -143,21 +220,19 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
143
220
|
if (editingParlay === slot) setEditingParlay(null);
|
|
144
221
|
}, [bets, onBetsChange, editingParlay]);
|
|
145
222
|
|
|
146
|
-
//
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return set;
|
|
154
|
-
}, [config.markets]);
|
|
223
|
+
// ── Helper: is a market's options clickable? ─────────────────────────────
|
|
224
|
+
const isMarketInteractive = useCallback((mIdx: number) => {
|
|
225
|
+
if (!hasSubmitted) return true; // fresh state: all markets interactive
|
|
226
|
+
return editingMarkets.has(mIdx); // post-submit: only editing markets
|
|
227
|
+
}, [hasSubmitted, editingMarkets]);
|
|
228
|
+
|
|
229
|
+
// ── Render ───────────────────────────────────────────────────────────────
|
|
155
230
|
|
|
156
231
|
return (
|
|
157
232
|
<div className={`w-full flex flex-col gap-3 px-4 pb-28${marketsOnly ? "" : " relative"}`}>
|
|
158
233
|
{!marketsOnly && (
|
|
159
234
|
<>
|
|
160
|
-
{/* Match header
|
|
235
|
+
{/* Match header */}
|
|
161
236
|
<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
237
|
<div className="flex items-center gap-2 mb-1">
|
|
163
238
|
<span className="text-[14px] text-white font-bold tracking-wide" style={OUTFIT}>{config.teamB.shortName}</span>
|
|
@@ -167,440 +242,385 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
167
242
|
<span className="text-[9px] text-white/20 font-medium tracking-wider uppercase" style={OUTFIT}>{config.venue}</span>
|
|
168
243
|
</div>
|
|
169
244
|
|
|
170
|
-
{/* How to Play */}
|
|
171
|
-
<div className="pt-2">
|
|
245
|
+
{/* How to Play + Filter pills */}
|
|
246
|
+
<div className="pt-2 flex items-center gap-2 overflow-x-auto scrollbar-hide">
|
|
172
247
|
<button
|
|
173
248
|
onClick={() => goTo("intro")}
|
|
174
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-
|
|
249
|
+
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
250
|
>
|
|
176
|
-
<Play size={
|
|
251
|
+
<Play size={10} fill="#22E3E8" strokeWidth={0} className="text-[#22E3E8]" />
|
|
177
252
|
<span className="text-[10px] text-[#22E3E8] font-semibold tracking-wide" style={OUTFIT}>How to Play</span>
|
|
178
253
|
</button>
|
|
254
|
+
<div className="w-px h-4 bg-white/10 flex-shrink-0" />
|
|
255
|
+
<FilterPill label="Bets first" active={activeFilters.has("bets_first")} onClick={() => toggleFilter("bets_first")} />
|
|
256
|
+
<FilterPill label="Unanswered first" active={activeFilters.has("unanswered_first")} onClick={() => toggleFilter("unanswered_first")} />
|
|
257
|
+
<FilterPill label="Hide ignored" active={activeFilters.has("hide_ignored")} onClick={() => toggleFilter("hide_ignored")} />
|
|
258
|
+
<FilterPill label="Expand all" active={false} onClick={handleExpandAll} />
|
|
259
|
+
<FilterPill label="Collapse all" active={false} onClick={handleCollapseAll} />
|
|
179
260
|
</div>
|
|
180
261
|
</>
|
|
181
262
|
)}
|
|
182
263
|
|
|
183
|
-
{/*
|
|
264
|
+
{/* Sort indicator when category headers are hidden */}
|
|
265
|
+
{sortActive && !marketsOnly && (
|
|
266
|
+
<div className="px-1 pt-1">
|
|
267
|
+
<span className="text-[8px] text-white/30 uppercase tracking-widest font-bold" style={OUTFIT}>
|
|
268
|
+
Sorted by: {activeFilters.has("bets_first") ? "Bets placed first" : "Unanswered first"}
|
|
269
|
+
</span>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
|
|
273
|
+
{/* Markets */}
|
|
184
274
|
<div className="w-full">
|
|
185
275
|
<div className="flex flex-col gap-3">
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
276
|
+
{visibleMarketIndices.map((mIdx) => {
|
|
277
|
+
const market = config.markets[mIdx];
|
|
278
|
+
const selection = bets[mIdx];
|
|
279
|
+
const isPickerOpenForMarket = !collapsedMarkets.has(mIdx) && selection !== undefined;
|
|
280
|
+
const showCategoryHeader = categoryForIndex.has(mIdx);
|
|
281
|
+
const interactive = isMarketInteractive(mIdx);
|
|
282
|
+
|
|
283
|
+
const pickerAvailable = remainingBalance + (selection?.amount ?? 0);
|
|
284
|
+
const chipCount = Math.floor(pickerAvailable / config.parlayConfig.stakeIncrements);
|
|
285
|
+
const IconComponent = MARKET_ICONS[market.icon];
|
|
286
|
+
|
|
287
|
+
const isCollapsed = collapsedMarkets.has(mIdx);
|
|
288
|
+
const selectedOpt = selection ? market.options[selection.optionIdx] : null;
|
|
289
|
+
const betAmount = selection?.amount ?? 0;
|
|
290
|
+
const reward = selectedOpt && betAmount > 0 ? Math.round(betAmount * selectedOpt.odds) : 0;
|
|
291
|
+
|
|
292
|
+
// Per-market editing state
|
|
293
|
+
const isEditingThis = editingMarkets.has(mIdx);
|
|
294
|
+
const isDirty = dirtyMarkets.has(mIdx);
|
|
295
|
+
const hasSubmittedBet = !!submittedBets?.[mIdx];
|
|
296
|
+
|
|
297
|
+
return (
|
|
298
|
+
<div key={market.id}>
|
|
299
|
+
{/* Category header */}
|
|
300
|
+
{showCategoryHeader && (
|
|
301
|
+
<div className="px-1 pt-1 pb-1.5">
|
|
302
|
+
<span className="text-[8px] text-white/40 uppercase tracking-widest font-bold" style={OUTFIT}>
|
|
303
|
+
{config.categoryLabels[market.category] ?? market.category}
|
|
304
|
+
</span>
|
|
305
|
+
</div>
|
|
306
|
+
)}
|
|
207
307
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const betAmount = selection?.amount ?? 0;
|
|
213
|
-
const reward = selectedOpt && betAmount > 0 ? Math.round(betAmount * selectedOpt.odds) : 0;
|
|
308
|
+
{/* Question header */}
|
|
309
|
+
<div className="px-1 py-1.5 flex items-center gap-2 cursor-pointer" onClick={() => toggleCollapse(mIdx)}>
|
|
310
|
+
{IconComponent && <IconComponent size={14} className="text-white/30 flex-shrink-0" />}
|
|
311
|
+
<span className="text-[13px] text-white font-semibold leading-tight flex-1" style={OUTFIT}>{market.question}</span>
|
|
214
312
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<div className="
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
313
|
+
{/* Parlay color dot */}
|
|
314
|
+
{mIdx in marketToParlay && (
|
|
315
|
+
<div className="w-[8px] h-[8px] rounded-full flex-shrink-0" style={{ background: parlayColors[marketToParlay[mIdx]] }} />
|
|
316
|
+
)}
|
|
317
|
+
|
|
318
|
+
{/* Parlay edit toggle */}
|
|
319
|
+
{editingParlay !== null && selection && (() => {
|
|
320
|
+
const inCurrentParlay = bets[mIdx]?.parlaySlot === editingParlay;
|
|
321
|
+
const inOtherParlay = mIdx in marketToParlay && marketToParlay[mIdx] !== editingParlay;
|
|
322
|
+
if (inOtherParlay) return (
|
|
323
|
+
<div className="flex-shrink-0 w-[20px] h-[20px] rounded-full flex items-center justify-center opacity-40"
|
|
324
|
+
style={{ border: `1px dashed ${parlayColors[marketToParlay[mIdx]]}` }}>
|
|
325
|
+
<div className="w-[6px] h-[6px] rounded-full" style={{ background: parlayColors[marketToParlay[mIdx]] }} />
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
return (
|
|
329
|
+
<button
|
|
330
|
+
onClick={(e) => { e.stopPropagation(); toggleMarketInParlay(mIdx); }}
|
|
331
|
+
className="flex-shrink-0 w-[20px] h-[20px] rounded-full border flex items-center justify-center transition-all"
|
|
332
|
+
style={{ borderColor: parlayColors[editingParlay], background: inCurrentParlay ? parlayColors[editingParlay] : "transparent" }}>
|
|
333
|
+
{inCurrentParlay && (
|
|
334
|
+
<svg width="10" height="10" viewBox="0 0 16 16" fill="none">
|
|
335
|
+
<path d="M5 8l2 2 4-4" stroke="white" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
336
|
+
</svg>
|
|
337
|
+
)}
|
|
338
|
+
</button>
|
|
339
|
+
);
|
|
340
|
+
})()}
|
|
341
|
+
|
|
342
|
+
{/* Per-market action: edit / ignore / confirm+cancel */}
|
|
343
|
+
{hasSubmittedBet ? (
|
|
344
|
+
isEditingThis ? (
|
|
345
|
+
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
346
|
+
<button onClick={(e) => { e.stopPropagation(); onCancelMarketEdit?.(mIdx); }}
|
|
347
|
+
className="w-[22px] h-[22px] rounded-full flex items-center justify-center bg-white/[0.06]">
|
|
348
|
+
<X size={12} className="text-white/50" />
|
|
349
|
+
</button>
|
|
350
|
+
{isDirty && (
|
|
351
|
+
<button onClick={(e) => { e.stopPropagation(); onConfirmMarketEdit?.(mIdx); }}
|
|
352
|
+
className="w-[22px] h-[22px] rounded-full flex items-center justify-center"
|
|
353
|
+
style={{ background: "rgba(34,227,232,0.15)", border: "1px solid rgba(34,227,232,0.3)" }}>
|
|
354
|
+
{editSubmitting === mIdx
|
|
355
|
+
? <Loader2 size={12} className="animate-spin text-[#22E3E8]" />
|
|
356
|
+
: <Check size={12} className="text-[#22E3E8]" />}
|
|
253
357
|
</button>
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
358
|
+
)}
|
|
359
|
+
</div>
|
|
360
|
+
) : (
|
|
361
|
+
<button onClick={(e) => { e.stopPropagation(); onEditMarket?.(mIdx); }}
|
|
362
|
+
className="flex-shrink-0 w-[22px] h-[22px] rounded-full flex items-center justify-center bg-white/[0.04]">
|
|
363
|
+
<Pencil size={11} className="text-white/40" />
|
|
364
|
+
</button>
|
|
365
|
+
)
|
|
366
|
+
) : (
|
|
367
|
+
onToggleIgnore && (
|
|
368
|
+
<button onClick={(e) => { e.stopPropagation(); onToggleIgnore(mIdx); }}
|
|
369
|
+
className="flex-shrink-0 w-[22px] h-[22px] rounded-full flex items-center justify-center"
|
|
370
|
+
style={{ background: ignoredMarkets.has(mIdx) ? "rgba(34,227,232,0.1)" : "transparent" }}>
|
|
371
|
+
<EyeOff size={11} className={ignoredMarkets.has(mIdx) ? "text-[#22E3E8]" : "text-white/20"} />
|
|
372
|
+
</button>
|
|
373
|
+
)
|
|
374
|
+
)}
|
|
265
375
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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 />
|
|
376
|
+
{/* Chevron */}
|
|
377
|
+
<motion.div animate={{ rotate: isCollapsed ? 0 : 180 }} transition={{ duration: 0.25 }} className="flex-shrink-0">
|
|
378
|
+
<ChevronDown size={14} className="text-white/40" />
|
|
379
|
+
</motion.div>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
{/* Collapsed summary row */}
|
|
383
|
+
<AnimatePresence initial={false}>
|
|
384
|
+
{isCollapsed && (
|
|
385
|
+
<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" }}>
|
|
386
|
+
<div className="px-2 py-1.5 flex items-center justify-between">
|
|
387
|
+
<div className="flex items-center gap-1.5">
|
|
388
|
+
{selectedOpt ? (
|
|
389
|
+
<>
|
|
390
|
+
<SelectedCheck />
|
|
391
|
+
<span className="text-[10px] text-white font-semibold" style={OUTFIT}>{selectedOpt.label}</span>
|
|
392
|
+
</>
|
|
393
|
+
) : (
|
|
394
|
+
<span className="text-[10px] text-white/30 font-medium" style={OUTFIT}>
|
|
395
|
+
{ignoredMarkets.has(mIdx) ? "Ignored" : "No selection"}
|
|
396
|
+
</span>
|
|
361
397
|
)}
|
|
362
|
-
<span className="text-[10px] text-white font-semibold truncate" style={OUTFIT}>{opt.label}</span>
|
|
363
398
|
</div>
|
|
364
|
-
{
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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>
|
|
399
|
+
{selectedOpt && (
|
|
400
|
+
<div className="flex items-center gap-[3px]">
|
|
401
|
+
{betAmount > 0 ? (
|
|
402
|
+
<>
|
|
403
|
+
<PointsIcon size={7} />
|
|
404
|
+
<span className="text-[9px] text-white font-semibold" style={OUTFIT}>{betAmount}</span>
|
|
405
|
+
<span className="text-[8px] text-white/40" style={OUTFIT}>×</span>
|
|
406
|
+
<span className="text-[9px] text-white/60 font-semibold" style={OUTFIT}>{selectedOpt.odds}</span>
|
|
407
|
+
<span className="text-[8px] text-white/40" style={OUTFIT}>=</span>
|
|
408
|
+
<PointsIcon size={7} />
|
|
409
|
+
<span className="text-[9px] text-[#22E3E8] font-bold" style={OUTFIT}>{reward}</span>
|
|
410
|
+
</>
|
|
411
|
+
) : (
|
|
412
|
+
<span className="text-[9px] text-white/40 font-semibold" style={OUTFIT}>{selectedOpt.odds}x</span>
|
|
413
|
+
)}
|
|
385
414
|
</div>
|
|
386
415
|
)}
|
|
387
416
|
</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>
|
|
417
|
+
</motion.div>
|
|
418
|
+
)}
|
|
419
|
+
</AnimatePresence>
|
|
399
420
|
|
|
400
|
-
{/*
|
|
401
|
-
{
|
|
402
|
-
|
|
403
|
-
{
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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;
|
|
421
|
+
{/* Expanded view — options grid + picker */}
|
|
422
|
+
<AnimatePresence initial={false}>
|
|
423
|
+
{!isCollapsed && (
|
|
424
|
+
<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" }}>
|
|
425
|
+
<div className="grid grid-cols-2 gap-1.5 px-1 pt-0.5 pb-1">
|
|
426
|
+
{market.options.map((opt, j) => {
|
|
427
|
+
const isSelected = selection?.optionIdx === j;
|
|
428
|
+
const isPickerOpen = isPickerOpenForMarket && selection?.optionIdx === j;
|
|
429
|
+
const showPickerRow = isPickerOpenForMarket;
|
|
430
|
+
const displayReward = calcDisplayReward(opt, compoundMultiplier, isSelected, selectedCount, isSelected ? selection.amount : undefined);
|
|
431
|
+
const baseReward = optionReward(opt);
|
|
432
|
+
const optionClickable = interactive;
|
|
421
433
|
return (
|
|
422
|
-
<div
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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}
|
|
434
|
+
<div key={j} className="col-span-1 flex flex-col">
|
|
435
|
+
<div
|
|
436
|
+
onClick={() => { if (optionClickable) onOptionClick(mIdx, j); }}
|
|
437
|
+
className="flex items-center justify-between px-2 py-1.5 rounded-sm transition-all duration-300"
|
|
438
|
+
style={{
|
|
439
|
+
cursor: optionClickable ? "pointer" : "default",
|
|
440
|
+
opacity: optionClickable ? 1 : (isSelected ? 1 : 0.5),
|
|
441
|
+
background: isSelected ? "linear-gradient(135deg, #22E3E8, #9945FF)" : isPickerOpen ? "rgba(34,227,232,0.12)" : "transparent",
|
|
442
|
+
borderLeft: isSelected ? "1px solid transparent" : isPickerOpen ? "1px solid rgba(34,227,232,0.3)" : "1px solid rgba(255,255,255,0.12)",
|
|
443
|
+
borderBottom: isSelected ? "1px solid transparent" : isPickerOpen ? "1px solid rgba(34,227,232,0.3)" : "1px solid rgba(255,255,255,0.06)",
|
|
444
|
+
borderTop: "1px solid transparent",
|
|
445
|
+
borderRight: "1px solid transparent",
|
|
446
|
+
}}
|
|
446
447
|
>
|
|
447
|
-
|
|
448
|
-
|
|
448
|
+
<div className="flex items-center gap-1.5 min-w-0 flex-shrink">
|
|
449
|
+
{isSelected && <SelectedCheck />}
|
|
450
|
+
<span className="text-[10px] text-white font-semibold truncate" style={OUTFIT}>{opt.label}</span>
|
|
451
|
+
</div>
|
|
452
|
+
{showPickerRow ? (
|
|
453
|
+
<span className="text-[10px] font-bold text-[#22E3E8]" style={OUTFIT}>{opt.odds}x</span>
|
|
454
|
+
) : (
|
|
455
|
+
<div className="flex items-center gap-[3px] flex-shrink-0">
|
|
456
|
+
<span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.4)" }}>{opt.odds}x</span>
|
|
457
|
+
<span className="text-[7px] text-white/25" style={OUTFIT}>(</span>
|
|
458
|
+
<PointsIcon size={7} />
|
|
459
|
+
<span className="text-[8px] font-medium" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.35)" }}>{isSelected ? selection.amount : opt.entry}</span>
|
|
460
|
+
<span className="text-[8px]" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.2)" }}>{"\u2192"}</span>
|
|
461
|
+
<PointsIcon size={7} />
|
|
462
|
+
<span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: isSelected ? "white" : "#22E3E8" }}>{isSelected ? displayReward : baseReward}</span>
|
|
463
|
+
<span className="text-[7px] text-white/25" style={OUTFIT}>)</span>
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
</div>
|
|
449
467
|
</div>
|
|
450
468
|
);
|
|
451
469
|
})}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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>
|
|
470
|
+
|
|
471
|
+
{/* AI Insights */}
|
|
472
|
+
<div className="col-span-2 flex justify-end pr-4 mt-3">
|
|
473
|
+
<AiInsightButton question={market.question} options={market.options.map(o => o.label)} />
|
|
475
474
|
</div>
|
|
476
|
-
</div>
|
|
477
|
-
)}
|
|
478
|
-
</div>
|
|
479
|
-
)}
|
|
480
475
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
476
|
+
{/* Amount picker */}
|
|
477
|
+
{isPickerOpenForMarket && interactive && (
|
|
478
|
+
<div className="col-span-2 mt-0.5 relative">
|
|
479
|
+
{mIdx === 0 && showOnboarding && (
|
|
480
|
+
<div className="absolute inset-0 z-10 pointer-events-none rounded overflow-hidden">
|
|
481
|
+
<motion.div className="absolute inset-0"
|
|
482
|
+
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%)" }}
|
|
483
|
+
animate={{ x: ["-100%", "100%"] }}
|
|
484
|
+
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 1.5 }} />
|
|
485
|
+
</div>
|
|
486
|
+
)}
|
|
487
|
+
{chipCount > 0 ? (
|
|
488
|
+
<div className="flex items-center gap-1.5 overflow-x-auto scrollbar-hide py-1 px-0.5">
|
|
489
|
+
<span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
|
|
490
|
+
{Array.from({ length: chipCount }, (_, i) => (i + 1) * config.parlayConfig.stakeIncrements).map(amt => {
|
|
491
|
+
const isChipSelected = selection?.amount === amt;
|
|
492
|
+
return (
|
|
493
|
+
<div key={amt}
|
|
494
|
+
onClick={() => onAmountSelect(mIdx, selection!.optionIdx, amt)}
|
|
495
|
+
className="flex-shrink-0 flex items-center gap-0.5 px-2 py-[3px] rounded cursor-pointer transition-colors relative overflow-hidden"
|
|
496
|
+
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)" }}>
|
|
497
|
+
{isChipSelected && (
|
|
498
|
+
<div className="absolute inset-0 pointer-events-none animate-shine"
|
|
499
|
+
style={{ background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%)", backgroundSize: "200% 100%" }} />
|
|
500
|
+
)}
|
|
501
|
+
<Image src="/iamgame_square_logo.jpg" alt="" width={7} height={7} className="rounded-[1px] relative z-[1]" />
|
|
502
|
+
<span className={`text-[9px] font-bold relative z-[1] ${isChipSelected ? "text-black" : "text-[#22E3E8]"}`} style={OUTFIT}>{amt}</span>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
})}
|
|
506
|
+
</div>
|
|
507
|
+
) : (
|
|
508
|
+
<div className="relative py-1 px-0.5">
|
|
509
|
+
<div className="flex items-center gap-1.5 overflow-hidden" style={{ filter: "blur(3px)" }}>
|
|
510
|
+
<span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
|
|
511
|
+
{[50, 100, 150, 200, 250, 300, 350].map(amt => (
|
|
512
|
+
<div key={amt} className="flex-shrink-0 flex items-center gap-0.5 px-2 py-[3px] rounded"
|
|
513
|
+
style={{ background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.2)" }}>
|
|
514
|
+
<PointsIcon size={7} />
|
|
515
|
+
<span className="text-[9px] font-bold text-[#22E3E8]" style={OUTFIT}>{amt}</span>
|
|
516
|
+
</div>
|
|
517
|
+
))}
|
|
518
|
+
</div>
|
|
519
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
520
|
+
<p className="text-[9px] text-[#22E3E8] font-semibold flex items-center gap-1" style={OUTFIT}>
|
|
521
|
+
All {config.startingBalance.toLocaleString()} <PointsIcon size={9} /> points used. Reduce other bets to bet here.
|
|
522
|
+
</p>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
)}
|
|
526
|
+
</div>
|
|
527
|
+
)}
|
|
528
|
+
|
|
529
|
+
{/* Onboarding dialogue */}
|
|
530
|
+
<AnimatePresence>
|
|
531
|
+
{mIdx === 0 && showOnboarding && (
|
|
532
|
+
<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 }}
|
|
533
|
+
transition={{ type: "spring", stiffness: 300, damping: 28 }}>
|
|
534
|
+
<div className="rounded-md px-2.5 py-1.5" style={{ background: "#22E3E8" }}>
|
|
535
|
+
<p className="text-[10px] text-black font-semibold leading-relaxed" style={OUTFIT}>
|
|
536
|
+
{onboardingWords.map((word, i) => (
|
|
537
|
+
<motion.span key={i} className="inline" initial={{ opacity: 0, y: 6, filter: "blur(4px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
|
538
|
+
transition={{ duration: 0.35, delay: 0.4 + i * 0.045, ease: "easeOut" }}>{word}{" "}</motion.span>
|
|
539
|
+
))}
|
|
540
|
+
</p>
|
|
541
|
+
</div>
|
|
542
|
+
</motion.div>
|
|
543
|
+
)}
|
|
544
|
+
</AnimatePresence>
|
|
508
545
|
</div>
|
|
509
546
|
</motion.div>
|
|
510
547
|
)}
|
|
511
548
|
</AnimatePresence>
|
|
512
549
|
</div>
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
)}
|
|
516
|
-
</AnimatePresence>
|
|
517
|
-
</div>
|
|
518
|
-
);
|
|
519
|
-
})}
|
|
550
|
+
);
|
|
551
|
+
})}
|
|
520
552
|
</div>
|
|
521
553
|
</div>
|
|
522
554
|
|
|
523
555
|
{!marketsOnly && (
|
|
524
556
|
<>
|
|
525
|
-
{/* Sticky bottom summary bar */}
|
|
526
|
-
<div
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
>
|
|
557
|
+
{/* ── Sticky bottom summary bar ─────────────────────────────────────── */}
|
|
558
|
+
<div className="sticky bottom-0 z-10 -mx-4 px-4 pt-3 pb-5"
|
|
559
|
+
style={{ background: "linear-gradient(180deg, transparent 0%, #0a0a12 12%, #0a0a12 100%)" }}>
|
|
560
|
+
|
|
530
561
|
<div className="flex items-stretch justify-between gap-3">
|
|
531
|
-
{/* Left —
|
|
562
|
+
{/* Left — text area */}
|
|
532
563
|
<div className="flex flex-col gap-2 flex-1 min-w-0">
|
|
564
|
+
{/* Remaining balance */}
|
|
533
565
|
<AnimatePresence>
|
|
534
566
|
{remainingBalance > 0 && (
|
|
535
|
-
<motion.div
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
animate={{ scale: 1, opacity: 1 }}
|
|
539
|
-
exit={{ opacity: 0, height: 0 }}
|
|
540
|
-
transition={{ type: "spring", stiffness: 300, damping: 18, delay: 0.2 }}
|
|
541
|
-
>
|
|
567
|
+
<motion.div className="flex items-baseline gap-1"
|
|
568
|
+
initial={{ scale: 1.15, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ opacity: 0, height: 0 }}
|
|
569
|
+
transition={{ type: "spring", stiffness: 300, damping: 18, delay: 0.2 }}>
|
|
542
570
|
<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
571
|
<AnimatedNumber value={remainingBalance} className="text-white font-bold leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 16 : 24 }} />
|
|
544
572
|
<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
|
-
|
|
547
|
-
|
|
573
|
+
<span className="text-white font-medium leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 9 : 11 }}>points left to bet</span>
|
|
574
|
+
</motion.div>
|
|
575
|
+
)}
|
|
576
|
+
</AnimatePresence>
|
|
577
|
+
|
|
578
|
+
{/* Saved max outcome — only visible when editing creates a diff */}
|
|
579
|
+
<AnimatePresence>
|
|
580
|
+
{savedBetSummary && hasUnsavedEdits && savedBetSummary.totalEntry > 0 && (
|
|
581
|
+
<motion.div className="overflow-hidden" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }}
|
|
582
|
+
transition={{ height: { duration: 0.35, ease: "easeOut" }, opacity: { duration: 0.3, delay: 0.1 } }}>
|
|
583
|
+
<div className="flex items-baseline gap-1 flex-nowrap">
|
|
584
|
+
<span className="text-[11px] text-white/50 font-medium leading-none flex-shrink-0" style={OUTFIT}>Saved</span>
|
|
585
|
+
<Image src="/iamgame_square_logo.jpg" alt="" width={14} height={14} className="rounded-[2px] flex-shrink-0" />
|
|
586
|
+
<AnimatedNumber value={savedBetSummary.totalEntry} className="text-[16px] text-white/60 font-bold leading-none" style={OUTFIT} />
|
|
587
|
+
<span className="text-[16px] text-white/40 font-bold leading-none flex-shrink-0" style={OUTFIT}>→</span>
|
|
588
|
+
<Image src="/iamgame_square_logo.jpg" alt="" width={14} height={14} className="rounded-[2px] flex-shrink-0" />
|
|
589
|
+
<AnimatedNumber value={savedBetSummary.compoundedReward} className="text-[16px] text-[#22E3E8]/60 font-bold leading-none" style={OUTFIT} />
|
|
590
|
+
</div>
|
|
548
591
|
</motion.div>
|
|
549
592
|
)}
|
|
550
593
|
</AnimatePresence>
|
|
594
|
+
|
|
595
|
+
{/* Max outcome — always visible when there are bets */}
|
|
551
596
|
<AnimatePresence>
|
|
552
597
|
{totalEntry > 0 && (
|
|
553
|
-
<motion.div
|
|
554
|
-
|
|
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
|
-
>
|
|
598
|
+
<motion.div className="overflow-hidden" initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }}
|
|
599
|
+
transition={{ height: { duration: 0.35, ease: "easeOut" }, opacity: { duration: 0.3, delay: 0.1 } }}>
|
|
560
600
|
<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}>
|
|
601
|
+
<span className="text-[13px] text-white font-medium leading-none flex-shrink-0" style={OUTFIT}>
|
|
602
|
+
{hasSubmitted && hasUnsavedEdits ? "Pending" : "Max outcome"}
|
|
603
|
+
</span>
|
|
562
604
|
<Image src="/iamgame_square_logo.jpg" alt="" width={18} height={18} className="rounded-[2px] flex-shrink-0" />
|
|
563
605
|
<AnimatedNumber value={totalEntry} className="text-[22px] text-white font-bold leading-none" style={OUTFIT} />
|
|
564
606
|
<span className="text-[22px] text-white font-bold leading-none flex-shrink-0" style={OUTFIT}>→</span>
|
|
565
607
|
<Image src="/iamgame_square_logo.jpg" alt="" width={18} height={18} className="rounded-[2px] flex-shrink-0" />
|
|
566
608
|
<span className="relative inline-flex items-baseline overflow-visible">
|
|
567
|
-
{/* Shine sweep — clipped to text via mix-blend */}
|
|
568
609
|
<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%]"
|
|
610
|
+
<motion.span className="absolute inset-y-0 w-[40%]"
|
|
571
611
|
style={{ background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent)" }}
|
|
572
612
|
animate={{ left: ["-40%", "140%"] }}
|
|
573
|
-
transition={{ duration: 2, ease: "easeInOut", repeat: Infinity, repeatDelay: 3.5 }}
|
|
574
|
-
/>
|
|
613
|
+
transition={{ duration: 2, ease: "easeInOut", repeat: Infinity, repeatDelay: 3.5 }} />
|
|
575
614
|
</span>
|
|
576
|
-
{/* Floating particles — tiny, spread all over */}
|
|
577
615
|
{Array.from({ length: 12 }, (_, p) => {
|
|
578
616
|
const size = 1 + (p % 3) * 0.4;
|
|
579
617
|
const left = (p / 12) * 100 + (p % 3) * 3;
|
|
580
618
|
const top = 10 + (p * 7) % 80;
|
|
581
619
|
return (
|
|
582
|
-
<motion.span
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
-
/>
|
|
620
|
+
<motion.span key={p} className="absolute rounded-full pointer-events-none"
|
|
621
|
+
style={{ width: size, height: size, background: p % 3 === 0 ? "rgba(255,255,255,0.9)" : "#22E3E8", left: `${left}%`, top: `${top}%` }}
|
|
622
|
+
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] }}
|
|
623
|
+
transition={{ duration: 4 + (p % 4) * 0.8, repeat: Infinity, delay: (p % 8) * 0.7, ease: "easeInOut" }} />
|
|
604
624
|
);
|
|
605
625
|
})}
|
|
606
626
|
<AnimatedNumber value={compoundedReward} className="text-[22px] text-[#22E3E8] font-bold leading-none relative" style={OUTFIT} />
|
|
@@ -610,103 +630,67 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
610
630
|
)}
|
|
611
631
|
</AnimatePresence>
|
|
612
632
|
</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
633
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
/>
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
: <Play size={26} fill="white" strokeWidth={0} className="relative z-10" />
|
|
653
|
-
}
|
|
654
|
-
</motion.button>
|
|
655
|
-
);
|
|
656
|
-
})()}
|
|
634
|
+
{/* Right — action button */}
|
|
635
|
+
<AnimatePresence>
|
|
636
|
+
{buttonState === "fresh" && totalEntry > 0 && (
|
|
637
|
+
<motion.button
|
|
638
|
+
onClick={onSubmit ? onSubmit : undefined}
|
|
639
|
+
disabled={submitting}
|
|
640
|
+
className="relative w-[65px] min-h-[54px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center py-3"
|
|
641
|
+
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" }}
|
|
642
|
+
initial={{ opacity: 0, width: 0 }}
|
|
643
|
+
animate={{ opacity: 1, width: 65 }}
|
|
644
|
+
exit={{ opacity: 0, width: 0 }}
|
|
645
|
+
transition={{ width: { duration: 0.4, ease: "easeOut", delay: 0.8 }, opacity: { duration: 0.3, delay: 1 } }}>
|
|
646
|
+
<motion.div className="absolute inset-y-0 left-0"
|
|
647
|
+
animate={{ width: `${5 + (totalEntry / config.startingBalance) * 95}%` }}
|
|
648
|
+
transition={{ type: "spring", stiffness: 120, damping: 20 }}
|
|
649
|
+
style={{ background: "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)" }} />
|
|
650
|
+
{submitting
|
|
651
|
+
? <Loader2 size={22} strokeWidth={2.5} className="relative z-10 animate-spin text-white/70" />
|
|
652
|
+
: <Play size={26} fill="white" strokeWidth={0} className="relative z-10" />}
|
|
653
|
+
</motion.button>
|
|
654
|
+
)}
|
|
655
|
+
{buttonState === "submitted" && (
|
|
656
|
+
<motion.div
|
|
657
|
+
className="w-[54px] min-h-[54px] rounded-xl flex-shrink-0 flex items-center justify-center"
|
|
658
|
+
style={{ background: "rgba(74,222,128,0.10)" }}
|
|
659
|
+
initial={{ opacity: 0, scale: 0.8 }}
|
|
660
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
661
|
+
transition={{ type: "spring", stiffness: 300, damping: 20 }}>
|
|
662
|
+
<Check size={22} strokeWidth={2.5} className="text-white" />
|
|
663
|
+
</motion.div>
|
|
664
|
+
)}
|
|
657
665
|
</AnimatePresence>
|
|
658
666
|
</div>
|
|
659
667
|
|
|
660
|
-
{/* Risk
|
|
668
|
+
{/* Risk bars */}
|
|
661
669
|
<AnimatePresence>
|
|
662
670
|
{riskPercent > 0 && (
|
|
663
|
-
<motion.div
|
|
664
|
-
|
|
665
|
-
initial={{ height: 0 }}
|
|
666
|
-
animate={{ height: "auto" }}
|
|
667
|
-
exit={{ height: 0 }}
|
|
668
|
-
transition={{ height: { duration: 0.4, ease: "easeOut", delay: 1.8 } }}
|
|
669
|
-
>
|
|
671
|
+
<motion.div className="overflow-hidden" initial={{ height: 0 }} animate={{ height: "auto" }} exit={{ height: 0 }}
|
|
672
|
+
transition={{ height: { duration: 0.4, ease: "easeOut", delay: 1.8 } }}>
|
|
670
673
|
<div className="flex flex-col gap-2 mt-3">
|
|
671
|
-
{/*
|
|
672
|
-
|
|
673
|
-
className="flex items-center gap-2"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
674
|
+
{/* Saved risk bar (shown when submitted and unsaved edits exist) */}
|
|
675
|
+
{savedBetSummary && hasUnsavedEdits && savedBetSummary.riskPercent > 0 && (
|
|
676
|
+
<motion.div className="flex items-center gap-2" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.3 }}>
|
|
677
|
+
<span className="text-[8px] text-white/40 font-semibold flex-shrink-0 w-[38px]" style={OUTFIT}>Saved</span>
|
|
678
|
+
<div className="flex-1 h-[4px] rounded-full bg-white/[0.04] overflow-hidden">
|
|
679
|
+
<div className="h-full rounded-full" style={{ width: `${savedBetSummary.riskPercent}%`, background: "linear-gradient(90deg, #f59e0b, #ef4444)", opacity: 0.5, transition: "width 0.4s ease" }} />
|
|
680
|
+
</div>
|
|
681
|
+
<span className="text-[8px] text-white/40 font-bold flex-shrink-0" style={OUTFIT}>{savedBetSummary.riskPercent}%</span>
|
|
682
|
+
</motion.div>
|
|
683
|
+
)}
|
|
684
|
+
{/* Current risk bar */}
|
|
685
|
+
<motion.div className="flex items-center gap-2" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.4, delay: 2.2 }}>
|
|
686
|
+
<span className="text-[8px] text-white font-semibold flex-shrink-0 w-[38px]" style={OUTFIT}>
|
|
687
|
+
{savedBetSummary && hasUnsavedEdits ? "Now" : "Risk"}
|
|
688
|
+
</span>
|
|
679
689
|
<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
|
-
/>
|
|
690
|
+
<div className="h-full rounded-full" style={{ width: `${riskPercent}%`, background: "linear-gradient(90deg, #f59e0b, #ef4444)", transition: "width 0.4s ease" }} />
|
|
688
691
|
</div>
|
|
689
692
|
<span className="text-[8px] text-white font-bold flex-shrink-0" style={OUTFIT}>{riskPercent}%</span>
|
|
690
693
|
</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
694
|
</div>
|
|
711
695
|
</motion.div>
|
|
712
696
|
)}
|
|
@@ -725,7 +709,6 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
725
709
|
Every leg must hit. One miss and the entire combined bet is lost.
|
|
726
710
|
</p>
|
|
727
711
|
|
|
728
|
-
{/* Parlay slots — concentric circles */}
|
|
729
712
|
<div className="flex items-end gap-3">
|
|
730
713
|
{Array.from({ length: config.parlayConfig.maxSlots }, (_, slot) => {
|
|
731
714
|
const legs = parlayGroups[slot] ?? [];
|
|
@@ -738,58 +721,29 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
738
721
|
|
|
739
722
|
return (
|
|
740
723
|
<div key={slot} className="flex flex-col items-center gap-1">
|
|
741
|
-
{
|
|
742
|
-
|
|
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
|
-
>
|
|
724
|
+
<button onClick={() => setEditingParlay(isEditing ? null : slot)}
|
|
725
|
+
className="relative flex items-center justify-center transition-all" style={{ width: 36, height: 36 }}>
|
|
750
726
|
{legCount === 0 ? (
|
|
751
|
-
|
|
752
|
-
|
|
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
|
-
/>
|
|
727
|
+
<div className="w-[28px] h-[28px] rounded-full border border-dashed transition-all"
|
|
728
|
+
style={{ borderColor: isEditing ? color : "rgba(255,255,255,0.15)", boxShadow: isEditing ? `0 0 8px ${color}40` : "none" }} />
|
|
759
729
|
) : (
|
|
760
|
-
/* Filled concentric rings */
|
|
761
730
|
<>
|
|
762
731
|
{Array.from({ length: Math.min(legCount, 6) }, (_, ring) => {
|
|
763
732
|
const size = 12 + ring * 5;
|
|
764
733
|
const opacity = 1 - ring * 0.12;
|
|
765
734
|
return (
|
|
766
|
-
<motion.div
|
|
767
|
-
key={ring}
|
|
768
|
-
initial={{ scale: 0 }}
|
|
769
|
-
animate={{ scale: 1 }}
|
|
735
|
+
<motion.div key={ring} initial={{ scale: 0 }} animate={{ scale: 1 }}
|
|
770
736
|
transition={{ type: "spring", stiffness: 400, damping: 20, delay: ring * 0.05 }}
|
|
771
737
|
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
|
-
/>
|
|
738
|
+
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
739
|
);
|
|
782
740
|
})}
|
|
783
741
|
</>
|
|
784
742
|
)}
|
|
785
743
|
</button>
|
|
786
|
-
|
|
787
|
-
{/* Multiplier + info */}
|
|
788
744
|
{legCount >= 2 ? (
|
|
789
745
|
<div className="flex items-center gap-0.5">
|
|
790
|
-
<span className="text-[8px] font-bold text-white" style={OUTFIT}>
|
|
791
|
-
{multiplier}x
|
|
792
|
-
</span>
|
|
746
|
+
<span className="text-[8px] font-bold text-white" style={OUTFIT}>{multiplier}x</span>
|
|
793
747
|
<button onClick={() => setParlayInfo(parlayInfo === slot ? null : slot)}>
|
|
794
748
|
<Info size={8} className="text-white/40" />
|
|
795
749
|
</button>
|
|
@@ -797,22 +751,14 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
797
751
|
) : legCount === 1 ? (
|
|
798
752
|
<span className="text-[7px] text-white/30 font-medium" style={OUTFIT}>+1 more</span>
|
|
799
753
|
) : null}
|
|
800
|
-
|
|
801
|
-
{/* Info popup */}
|
|
802
754
|
<AnimatePresence>
|
|
803
755
|
{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 }}
|
|
756
|
+
<motion.div initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 4 }}
|
|
808
757
|
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
|
-
>
|
|
758
|
+
style={{ background: "#1a1a2e", border: `1px solid ${color}30` }}>
|
|
811
759
|
<div className="flex items-center justify-between mb-1.5">
|
|
812
760
|
<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>
|
|
761
|
+
<button onClick={() => setParlayInfo(null)}><X size={10} className="text-white/40" /></button>
|
|
816
762
|
</div>
|
|
817
763
|
{breakdown.map((leg, i) => (
|
|
818
764
|
<div key={i} className="flex items-center justify-between py-0.5">
|
|
@@ -828,20 +774,12 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
828
774
|
{totalStake > 0 && (
|
|
829
775
|
<div className="flex items-center justify-between mt-0.5">
|
|
830
776
|
<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>
|
|
777
|
+
<span className="text-[8px] text-white font-semibold" style={OUTFIT}>{totalStake} → {Math.round(totalStake * multiplier).toLocaleString()}</span>
|
|
834
778
|
</div>
|
|
835
779
|
)}
|
|
836
780
|
<p className="text-[6px] text-white/25 mt-1" style={OUTFIT}>All must hit to win</p>
|
|
837
|
-
{
|
|
838
|
-
|
|
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>
|
|
781
|
+
<button onClick={() => { clearParlay(slot); setParlayInfo(null); }}
|
|
782
|
+
className="mt-1.5 text-[7px] text-red-400/60 font-medium" style={OUTFIT}>Remove combined bet</button>
|
|
845
783
|
</motion.div>
|
|
846
784
|
)}
|
|
847
785
|
</AnimatePresence>
|
|
@@ -849,18 +787,13 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
849
787
|
);
|
|
850
788
|
})}
|
|
851
789
|
</div>
|
|
852
|
-
|
|
853
|
-
{/* Edit mode hint */}
|
|
854
790
|
{editingParlay !== null && (
|
|
855
791
|
<div className="flex items-center justify-between mt-2">
|
|
856
792
|
<p className="text-[8px] font-medium" style={{ ...OUTFIT, color: parlayColors[editingParlay] }}>
|
|
857
793
|
Tap questions above to add to this combined bet
|
|
858
794
|
</p>
|
|
859
|
-
<button
|
|
860
|
-
|
|
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
|
-
>
|
|
795
|
+
<button onClick={() => setEditingParlay(null)}
|
|
796
|
+
className="text-[8px] text-white/50 font-semibold px-2 py-0.5 rounded" style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)" }}>
|
|
864
797
|
Done
|
|
865
798
|
</button>
|
|
866
799
|
</div>
|
|
@@ -881,16 +814,8 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
881
814
|
<button
|
|
882
815
|
onClick={parlayEnabled ? () => setEditingParlay(0) : undefined}
|
|
883
816
|
className="relative w-[65px] min-h-[54px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center py-3"
|
|
884
|
-
style={{
|
|
885
|
-
|
|
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
|
-
/>
|
|
817
|
+
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 }}>
|
|
818
|
+
<div className="absolute inset-y-0 left-0 w-[30%]" style={{ background: "linear-gradient(135deg, #9945FF, #f83cc5, #22E3E8)" }} />
|
|
894
819
|
<Play size={26} fill="white" strokeWidth={0} className="relative z-10" />
|
|
895
820
|
</button>
|
|
896
821
|
</div>
|
|
@@ -916,25 +841,15 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
916
841
|
<span className="text-[8px] text-white/20 font-semibold tracking-widest" style={OUTFIT}>···</span>
|
|
917
842
|
</div>
|
|
918
843
|
)}
|
|
919
|
-
<LeaderboardRow
|
|
920
|
-
entry={entry}
|
|
921
|
-
rank={entry.rank ?? i + 1}
|
|
922
|
-
isLast={i === arr.length - 1}
|
|
923
|
-
/>
|
|
844
|
+
<LeaderboardRow entry={entry} rank={entry.rank ?? i + 1} isLast={i === arr.length - 1} />
|
|
924
845
|
</div>
|
|
925
846
|
))}
|
|
926
847
|
</div>
|
|
927
848
|
{onViewLeaderboard && leaderboardRows.length > 0 && (
|
|
928
|
-
<button
|
|
929
|
-
|
|
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>
|
|
849
|
+
<button onClick={onViewLeaderboard} className="mt-2 w-full text-center text-[9px] font-semibold"
|
|
850
|
+
style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}>See Full Leaderboard →</button>
|
|
935
851
|
)}
|
|
936
852
|
</div>
|
|
937
|
-
|
|
938
853
|
</>
|
|
939
854
|
)}
|
|
940
855
|
</div>
|