@devrongx/games 0.4.23 → 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
|
-
//
|
|
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,40 +112,106 @@ 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
|
+
// 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]);
|
|
198
|
+
const hasAnyParlays = Object.keys(parlayGroups).length > 0;
|
|
109
199
|
|
|
110
|
-
// Toggle a market in/out of the currently editing parlay.
|
|
111
|
-
// A market already in a DIFFERENT parlay cannot be toggled — UI prevents this.
|
|
112
200
|
const toggleMarketInParlay = useCallback((mIdx: number) => {
|
|
113
201
|
if (editingParlay === null || !onBetsChange) return;
|
|
114
202
|
const slot = editingParlay;
|
|
115
203
|
const entry = bets[mIdx];
|
|
116
204
|
if (!entry) return;
|
|
117
|
-
|
|
118
|
-
// Block if market belongs to a different parlay
|
|
119
205
|
if (entry.parlaySlot !== null && entry.parlaySlot !== slot) return;
|
|
120
|
-
|
|
121
206
|
const next = { ...bets };
|
|
122
207
|
if (entry.parlaySlot === slot) {
|
|
123
|
-
// Remove from this parlay → individual
|
|
124
208
|
next[mIdx] = { ...entry, parlaySlot: null };
|
|
125
209
|
} else {
|
|
126
|
-
// Add to this parlay
|
|
127
210
|
next[mIdx] = { ...entry, parlaySlot: slot };
|
|
128
211
|
}
|
|
129
212
|
onBetsChange(next);
|
|
130
213
|
}, [editingParlay, bets, onBetsChange]);
|
|
131
214
|
|
|
132
|
-
// Clear a parlay slot — set all its markets back to individual
|
|
133
215
|
const clearParlay = useCallback((slot: number) => {
|
|
134
216
|
if (!onBetsChange) return;
|
|
135
217
|
const next = { ...bets };
|
|
@@ -142,21 +224,19 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
142
224
|
if (editingParlay === slot) setEditingParlay(null);
|
|
143
225
|
}, [bets, onBetsChange, editingParlay]);
|
|
144
226
|
|
|
145
|
-
//
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return set;
|
|
153
|
-
}, [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 ───────────────────────────────────────────────────────────────
|
|
154
234
|
|
|
155
235
|
return (
|
|
156
236
|
<div className={`w-full flex flex-col gap-3 px-4 pb-28${marketsOnly ? "" : " relative"}`}>
|
|
157
237
|
{!marketsOnly && (
|
|
158
238
|
<>
|
|
159
|
-
{/* Match header
|
|
239
|
+
{/* Match header */}
|
|
160
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">
|
|
161
241
|
<div className="flex items-center gap-2 mb-1">
|
|
162
242
|
<span className="text-[14px] text-white font-bold tracking-wide" style={OUTFIT}>{config.teamB.shortName}</span>
|
|
@@ -166,440 +246,394 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
166
246
|
<span className="text-[9px] text-white/20 font-medium tracking-wider uppercase" style={OUTFIT}>{config.venue}</span>
|
|
167
247
|
</div>
|
|
168
248
|
|
|
169
|
-
{/* How to Play */}
|
|
170
|
-
<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">
|
|
171
251
|
<button
|
|
172
252
|
onClick={() => goTo("intro")}
|
|
173
|
-
className="flex items-center gap-1.5 px-3 py-1.5 rounded-
|
|
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]"
|
|
174
254
|
>
|
|
175
|
-
<Play size={
|
|
255
|
+
<Play size={10} fill="#22E3E8" strokeWidth={0} className="text-[#22E3E8]" />
|
|
176
256
|
<span className="text-[10px] text-[#22E3E8] font-semibold tracking-wide" style={OUTFIT}>How to Play</span>
|
|
177
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} />
|
|
178
264
|
</div>
|
|
179
265
|
</>
|
|
180
266
|
)}
|
|
181
267
|
|
|
182
|
-
{/*
|
|
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 */}
|
|
183
278
|
<div className="w-full">
|
|
184
279
|
<div className="flex flex-col gap-3">
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
+
)}
|
|
206
319
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const betAmount = selection?.amount ?? 0;
|
|
212
|
-
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>
|
|
213
324
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<div className="
|
|
217
|
-
|
|
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
|
-
|
|
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]" />}
|
|
252
369
|
</button>
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
<
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
)}
|
|
264
387
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
</div>
|
|
287
|
-
{/* Right — bet summary */}
|
|
288
|
-
{selectedOpt && (
|
|
289
|
-
<div className="flex items-center gap-[3px]">
|
|
290
|
-
{betAmount > 0 ? (
|
|
291
|
-
<>
|
|
292
|
-
<PointsIcon size={7} />
|
|
293
|
-
<span className="text-[9px] text-white font-semibold" style={OUTFIT}>{betAmount}</span>
|
|
294
|
-
<span className="text-[8px] text-white/40" style={OUTFIT}>×</span>
|
|
295
|
-
<span className="text-[9px] text-white/60 font-semibold" style={OUTFIT}>{selectedOpt.odds}</span>
|
|
296
|
-
<span className="text-[8px] text-white/40" style={OUTFIT}>=</span>
|
|
297
|
-
<PointsIcon size={7} />
|
|
298
|
-
<span className="text-[9px] text-[#22E3E8] font-bold" style={OUTFIT}>{reward}</span>
|
|
299
|
-
</>
|
|
300
|
-
) : (
|
|
301
|
-
<span className="text-[9px] text-white/40 font-semibold" style={OUTFIT}>{selectedOpt.odds}x</span>
|
|
302
|
-
)}
|
|
303
|
-
</div>
|
|
304
|
-
)}
|
|
305
|
-
</div>
|
|
306
|
-
</motion.div>
|
|
307
|
-
)}
|
|
308
|
-
</AnimatePresence>
|
|
309
|
-
</>
|
|
310
|
-
);
|
|
311
|
-
})()}
|
|
312
|
-
|
|
313
|
-
{/* Expanded view — options grid + picker */}
|
|
314
|
-
<AnimatePresence initial={false}>
|
|
315
|
-
{!collapsedMarkets.has(mIdx) && (
|
|
316
|
-
<motion.div
|
|
317
|
-
className="overflow-hidden"
|
|
318
|
-
initial={{ height: 0, opacity: 0 }}
|
|
319
|
-
animate={{ height: "auto", opacity: 1 }}
|
|
320
|
-
exit={{ height: 0, opacity: 0 }}
|
|
321
|
-
transition={{ duration: 0.25, ease: "easeInOut" }}
|
|
322
|
-
>
|
|
323
|
-
|
|
324
|
-
{/* Options grid */}
|
|
325
|
-
<div className="grid grid-cols-2 gap-1.5 px-1 pt-0.5 pb-1">
|
|
326
|
-
{market.options.map((opt, j) => {
|
|
327
|
-
const isSelected = selection?.optionIdx === j;
|
|
328
|
-
const isPickerOpen = isPickerOpenForMarket && selection?.optionIdx === j;
|
|
329
|
-
const showPickerRow = isPickerOpenForMarket;
|
|
330
|
-
const displayReward = calcDisplayReward(opt, compoundMultiplier, isSelected, selectedCount, isSelected ? selection.amount : undefined);
|
|
331
|
-
const baseReward = optionReward(opt);
|
|
332
|
-
return (
|
|
333
|
-
<div key={j} className="col-span-1 flex flex-col">
|
|
334
|
-
<div
|
|
335
|
-
onClick={() => onOptionClick(mIdx, j)}
|
|
336
|
-
className="flex items-center justify-between px-2 py-1.5 rounded-sm transition-all duration-300 cursor-pointer"
|
|
337
|
-
style={{
|
|
338
|
-
background: isSelected
|
|
339
|
-
? "linear-gradient(135deg, #22E3E8, #9945FF)"
|
|
340
|
-
: isPickerOpen
|
|
341
|
-
? "rgba(34,227,232,0.12)"
|
|
342
|
-
: "transparent",
|
|
343
|
-
borderLeft: isSelected
|
|
344
|
-
? "1px solid transparent"
|
|
345
|
-
: isPickerOpen
|
|
346
|
-
? "1px solid rgba(34,227,232,0.3)"
|
|
347
|
-
: "1px solid rgba(255,255,255,0.12)",
|
|
348
|
-
borderBottom: isSelected
|
|
349
|
-
? "1px solid transparent"
|
|
350
|
-
: isPickerOpen
|
|
351
|
-
? "1px solid rgba(34,227,232,0.3)"
|
|
352
|
-
: "1px solid rgba(255,255,255,0.06)",
|
|
353
|
-
borderTop: "1px solid transparent",
|
|
354
|
-
borderRight: "1px solid transparent",
|
|
355
|
-
}}
|
|
356
|
-
>
|
|
357
|
-
<div className="flex items-center gap-1.5 min-w-0 flex-shrink">
|
|
358
|
-
{isSelected && (
|
|
359
|
-
<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>
|
|
360
409
|
)}
|
|
361
|
-
<span className="text-[10px] text-white font-semibold truncate" style={OUTFIT}>{opt.label}</span>
|
|
362
410
|
</div>
|
|
363
|
-
{
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
<span className="text-[8px]" style={{ ...OUTFIT, color: isSelected ? "white" : "rgba(255,255,255,0.2)" }}>{"\u2192"}</span>
|
|
379
|
-
<PointsIcon size={7} />
|
|
380
|
-
<span className="text-[8px] font-semibold" style={{ ...OUTFIT, color: isSelected ? "white" : "#22E3E8" }}>
|
|
381
|
-
{isSelected ? displayReward : baseReward}
|
|
382
|
-
</span>
|
|
383
|
-
<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
|
+
)}
|
|
384
426
|
</div>
|
|
385
427
|
)}
|
|
386
428
|
</div>
|
|
387
|
-
</div>
|
|
388
|
-
)
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
{/* AI Insights button — right-aligned, below options */}
|
|
392
|
-
<div className="col-span-2 flex justify-end pr-4 mt-3">
|
|
393
|
-
<AiInsightButton
|
|
394
|
-
question={market.question}
|
|
395
|
-
options={market.options.map(o => o.label)}
|
|
396
|
-
/>
|
|
397
|
-
</div>
|
|
429
|
+
</motion.div>
|
|
430
|
+
)}
|
|
431
|
+
</AnimatePresence>
|
|
398
432
|
|
|
399
|
-
{/*
|
|
400
|
-
{
|
|
401
|
-
|
|
402
|
-
{
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
transition={{ duration: 1.5, ease: "easeInOut", repeat: Infinity, repeatDelay: 1.5 }}
|
|
412
|
-
/>
|
|
413
|
-
</div>
|
|
414
|
-
)}
|
|
415
|
-
{chipCount > 0 ? (
|
|
416
|
-
<div className="flex items-center gap-1.5 overflow-x-auto scrollbar-hide py-1 px-0.5">
|
|
417
|
-
<span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
|
|
418
|
-
{Array.from({ length: chipCount }, (_, i) => (i + 1) * config.parlayConfig.stakeIncrements).map(amt => {
|
|
419
|
-
const isChipSelected = selection?.amount === amt;
|
|
433
|
+
{/* Expanded view — options 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;
|
|
420
445
|
return (
|
|
421
|
-
<div
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
className="absolute inset-0 pointer-events-none animate-shine"
|
|
435
|
-
style={{
|
|
436
|
-
background: "linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.3) 50%, transparent 100%)",
|
|
437
|
-
backgroundSize: "200% 100%",
|
|
438
|
-
}}
|
|
439
|
-
/>
|
|
440
|
-
)}
|
|
441
|
-
<Image src="/iamgame_square_logo.jpg" alt="" width={7} height={7} className="rounded-[1px] relative z-[1]" />
|
|
442
|
-
<span
|
|
443
|
-
className={`text-[9px] font-bold relative z-[1] ${isChipSelected ? "text-black" : "text-[#22E3E8]"}`}
|
|
444
|
-
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
|
+
}}
|
|
445
459
|
>
|
|
446
|
-
|
|
447
|
-
|
|
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>
|
|
448
479
|
</div>
|
|
449
480
|
);
|
|
450
481
|
})}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
<div className="flex items-center gap-1.5 overflow-hidden" style={{ filter: "blur(3px)" }}>
|
|
456
|
-
<span className="text-[8px] text-white/60 font-semibold flex-shrink-0" style={OUTFIT}>Bet:</span>
|
|
457
|
-
{[50, 100, 150, 200, 250, 300, 350].map(amt => (
|
|
458
|
-
<div
|
|
459
|
-
key={amt}
|
|
460
|
-
className="flex-shrink-0 flex items-center gap-0.5 px-2 py-[3px] rounded"
|
|
461
|
-
style={{ background: "rgba(34,227,232,0.08)", border: "1px solid rgba(34,227,232,0.2)" }}
|
|
462
|
-
>
|
|
463
|
-
<PointsIcon size={7} />
|
|
464
|
-
<span className="text-[9px] font-bold text-[#22E3E8]" style={OUTFIT}>{amt}</span>
|
|
465
|
-
</div>
|
|
466
|
-
))}
|
|
467
|
-
</div>
|
|
468
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
469
|
-
<p className="text-[9px] text-[#22E3E8] font-semibold flex items-center gap-1" style={OUTFIT}>
|
|
470
|
-
All {config.startingBalance.toLocaleString()}
|
|
471
|
-
<PointsIcon size={9} />
|
|
472
|
-
points used. Reduce other bets to bet here.
|
|
473
|
-
</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)} />
|
|
474
486
|
</div>
|
|
475
|
-
</div>
|
|
476
|
-
)}
|
|
477
|
-
</div>
|
|
478
|
-
)}
|
|
479
487
|
|
|
480
|
-
|
|
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
|
-
|
|
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>
|
|
507
557
|
</div>
|
|
508
558
|
</motion.div>
|
|
509
559
|
)}
|
|
510
560
|
</AnimatePresence>
|
|
511
|
-
</div>
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
</AnimatePresence>
|
|
516
|
-
</div>
|
|
517
|
-
);
|
|
518
|
-
})}
|
|
561
|
+
</motion.div>
|
|
562
|
+
);
|
|
563
|
+
})}
|
|
564
|
+
</AnimatePresence>
|
|
519
565
|
</div>
|
|
520
566
|
</div>
|
|
521
567
|
|
|
522
568
|
{!marketsOnly && (
|
|
523
569
|
<>
|
|
524
|
-
{/* Sticky bottom summary bar */}
|
|
525
|
-
<div
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
>
|
|
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
|
+
|
|
529
574
|
<div className="flex items-stretch justify-between gap-3">
|
|
530
|
-
{/* Left —
|
|
575
|
+
{/* Left — text area */}
|
|
531
576
|
<div className="flex flex-col gap-2 flex-1 min-w-0">
|
|
577
|
+
{/* Remaining balance */}
|
|
532
578
|
<AnimatePresence>
|
|
533
579
|
{remainingBalance > 0 && (
|
|
534
|
-
<motion.div
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
animate={{ scale: 1, opacity: 1 }}
|
|
538
|
-
exit={{ opacity: 0, height: 0 }}
|
|
539
|
-
transition={{ type: "spring", stiffness: 300, damping: 18, delay: 0.2 }}
|
|
540
|
-
>
|
|
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 }}>
|
|
541
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" />
|
|
542
584
|
<AnimatedNumber value={remainingBalance} className="text-white font-bold leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 16 : 24 }} />
|
|
543
585
|
<span className="text-white font-bold leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 16 : 24 }}>/{config.startingBalance.toLocaleString()}</span>
|
|
544
|
-
<span className="text-white font-medium leading-none transition-all duration-500" style={{ ...OUTFIT, fontSize: totalEntry > 0 ? 9 : 11 }}>
|
|
545
|
-
|
|
546
|
-
|
|
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>
|
|
587
|
+
</motion.div>
|
|
588
|
+
)}
|
|
589
|
+
</AnimatePresence>
|
|
590
|
+
|
|
591
|
+
{/* Saved max outcome */}
|
|
592
|
+
<AnimatePresence>
|
|
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 } }}>
|
|
596
|
+
<div className="flex items-baseline gap-1 flex-nowrap">
|
|
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>
|
|
547
604
|
</motion.div>
|
|
548
605
|
)}
|
|
549
606
|
</AnimatePresence>
|
|
607
|
+
|
|
608
|
+
{/* Current/unsaved max outcome */}
|
|
550
609
|
<AnimatePresence>
|
|
551
|
-
{totalEntry > 0 && (
|
|
552
|
-
<motion.div
|
|
553
|
-
|
|
554
|
-
initial={{ height: 0, opacity: 0 }}
|
|
555
|
-
animate={{ height: "auto", opacity: 1 }}
|
|
556
|
-
exit={{ height: 0, opacity: 0 }}
|
|
557
|
-
transition={{ height: { duration: 0.35, ease: "easeOut" }, opacity: { duration: 0.3, delay: 0.1 } }}
|
|
558
|
-
>
|
|
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 } }}>
|
|
559
613
|
<div className="flex items-baseline gap-1 flex-nowrap">
|
|
560
|
-
<span className="text-[13px] text-white font-medium leading-none flex-shrink-0" style={OUTFIT}>
|
|
614
|
+
<span className="text-[13px] text-white font-medium leading-none flex-shrink-0" style={OUTFIT}>
|
|
615
|
+
{hasSubmitted ? "Pending" : "Max outcome"}
|
|
616
|
+
</span>
|
|
561
617
|
<Image src="/iamgame_square_logo.jpg" alt="" width={18} height={18} className="rounded-[2px] flex-shrink-0" />
|
|
562
618
|
<AnimatedNumber value={totalEntry} className="text-[22px] text-white font-bold leading-none" style={OUTFIT} />
|
|
563
619
|
<span className="text-[22px] text-white font-bold leading-none flex-shrink-0" style={OUTFIT}>→</span>
|
|
564
620
|
<Image src="/iamgame_square_logo.jpg" alt="" width={18} height={18} className="rounded-[2px] flex-shrink-0" />
|
|
565
621
|
<span className="relative inline-flex items-baseline overflow-visible">
|
|
566
|
-
{/* Shine sweep — clipped to text via mix-blend */}
|
|
567
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>
|
|
568
|
-
<motion.span
|
|
569
|
-
className="absolute inset-y-0 w-[40%]"
|
|
623
|
+
<motion.span className="absolute inset-y-0 w-[40%]"
|
|
570
624
|
style={{ background: "linear-gradient(90deg, transparent, rgba(255,255,255,0.25), transparent)" }}
|
|
571
625
|
animate={{ left: ["-40%", "140%"] }}
|
|
572
|
-
transition={{ duration: 2, ease: "easeInOut", repeat: Infinity, repeatDelay: 3.5 }}
|
|
573
|
-
/>
|
|
626
|
+
transition={{ duration: 2, ease: "easeInOut", repeat: Infinity, repeatDelay: 3.5 }} />
|
|
574
627
|
</span>
|
|
575
|
-
{/* Floating particles — tiny, spread all over */}
|
|
576
628
|
{Array.from({ length: 12 }, (_, p) => {
|
|
577
629
|
const size = 1 + (p % 3) * 0.4;
|
|
578
630
|
const left = (p / 12) * 100 + (p % 3) * 3;
|
|
579
631
|
const top = 10 + (p * 7) % 80;
|
|
580
632
|
return (
|
|
581
|
-
<motion.span
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
width: size,
|
|
586
|
-
height: size,
|
|
587
|
-
background: p % 3 === 0 ? "rgba(255,255,255,0.9)" : "#22E3E8",
|
|
588
|
-
left: `${left}%`,
|
|
589
|
-
top: `${top}%`,
|
|
590
|
-
}}
|
|
591
|
-
animate={{
|
|
592
|
-
y: [0, -4 - (p % 3) * 1.5, 0],
|
|
593
|
-
x: [0, (p % 2 === 0 ? 1.5 : -1.5), 0],
|
|
594
|
-
opacity: [0, 0.4, 1, 0.4, 0],
|
|
595
|
-
}}
|
|
596
|
-
transition={{
|
|
597
|
-
duration: 4 + (p % 4) * 0.8,
|
|
598
|
-
repeat: Infinity,
|
|
599
|
-
delay: (p % 8) * 0.7,
|
|
600
|
-
ease: "easeInOut",
|
|
601
|
-
}}
|
|
602
|
-
/>
|
|
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" }} />
|
|
603
637
|
);
|
|
604
638
|
})}
|
|
605
639
|
<AnimatedNumber value={compoundedReward} className="text-[22px] text-[#22E3E8] font-bold leading-none relative" style={OUTFIT} />
|
|
@@ -609,99 +643,69 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
609
643
|
)}
|
|
610
644
|
</AnimatePresence>
|
|
611
645
|
</div>
|
|
612
|
-
{/* Right — action button, hidden until points are bet */}
|
|
613
|
-
<AnimatePresence>
|
|
614
|
-
{totalEntry > 0 && (() => {
|
|
615
|
-
const spentPercent = totalEntry / config.startingBalance;
|
|
616
|
-
const fillPercent = buttonState === "saved" ? 100 : 5 + spentPercent * 95;
|
|
617
|
-
const fillColor = "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)";
|
|
618
|
-
const bgColor = "linear-gradient(135deg, rgba(34,227,232,0.2), rgba(153,69,255,0.2), rgba(248,60,197,0.2))";
|
|
619
646
|
|
|
620
|
-
|
|
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
|
-
|
|
653
|
-
</motion.button>
|
|
654
|
-
);
|
|
655
|
-
})()}
|
|
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
|
+
)}
|
|
656
680
|
</AnimatePresence>
|
|
657
681
|
</div>
|
|
658
682
|
|
|
659
|
-
{/* Risk
|
|
683
|
+
{/* Risk bars */}
|
|
660
684
|
<AnimatePresence>
|
|
661
685
|
{riskPercent > 0 && (
|
|
662
|
-
<motion.div
|
|
663
|
-
|
|
664
|
-
initial={{ height: 0 }}
|
|
665
|
-
animate={{ height: "auto" }}
|
|
666
|
-
exit={{ height: 0 }}
|
|
667
|
-
transition={{ height: { duration: 0.4, ease: "easeOut", delay: 1.8 } }}
|
|
668
|
-
>
|
|
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 } }}>
|
|
669
688
|
<div className="flex flex-col gap-2 mt-3">
|
|
670
|
-
{/*
|
|
671
|
-
|
|
672
|
-
className="flex items-center gap-2"
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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>
|
|
678
704
|
<div className="flex-1 h-[5px] rounded-full bg-white/[0.06] overflow-hidden">
|
|
679
|
-
<div
|
|
680
|
-
className="h-full rounded-full"
|
|
681
|
-
style={{
|
|
682
|
-
width: `${riskPercent}%`,
|
|
683
|
-
background: "linear-gradient(90deg, #f59e0b, #ef4444)",
|
|
684
|
-
transition: "width 0.4s ease",
|
|
685
|
-
}}
|
|
686
|
-
/>
|
|
705
|
+
<div className="h-full rounded-full" style={{ width: `${riskPercent}%`, background: "linear-gradient(90deg, #f59e0b, #ef4444)", transition: "width 0.4s ease" }} />
|
|
687
706
|
</div>
|
|
688
707
|
<span className="text-[8px] text-white font-bold flex-shrink-0" style={OUTFIT}>{riskPercent}%</span>
|
|
689
708
|
</motion.div>
|
|
690
|
-
{/* Tip */}
|
|
691
|
-
<motion.p
|
|
692
|
-
className="text-[8px] text-white font-medium text-center mt-0.5"
|
|
693
|
-
style={OUTFIT}
|
|
694
|
-
initial={{ opacity: 0 }}
|
|
695
|
-
animate={{ opacity: 1 }}
|
|
696
|
-
transition={{ duration: 0.4, delay: 3 }}
|
|
697
|
-
>
|
|
698
|
-
{riskPercent > 60
|
|
699
|
-
? "High risk — spread your bets to lower risk while keeping outcome high"
|
|
700
|
-
: riskPercent > 30
|
|
701
|
-
? "Balanced — adjust your bets to find the sweet spot"
|
|
702
|
-
: "Low risk — you can afford to be bolder for a bigger outcome"
|
|
703
|
-
}
|
|
704
|
-
</motion.p>
|
|
705
709
|
</div>
|
|
706
710
|
</motion.div>
|
|
707
711
|
)}
|
|
@@ -709,21 +713,17 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
709
713
|
|
|
710
714
|
{/* Combined Bets (Parlays) */}
|
|
711
715
|
<div className="mt-4">
|
|
716
|
+
{hasAnyParlays ? (
|
|
717
|
+
<>
|
|
712
718
|
<div className="flex items-center gap-2 mb-1">
|
|
713
719
|
<p className="text-[9px] text-white/60 font-semibold uppercase tracking-wider" style={OUTFIT}>
|
|
714
|
-
|
|
720
|
+
Combined Bets
|
|
715
721
|
</p>
|
|
716
|
-
{!parlayEnabled && (
|
|
717
|
-
<span className="text-[7px] font-bold px-1.5 py-0.5 rounded-full" style={{ background: "rgba(153,69,255,0.15)", color: "#9945FF", border: "1px solid rgba(153,69,255,0.3)" }}>
|
|
718
|
-
Coming in v2
|
|
719
|
-
</span>
|
|
720
|
-
)}
|
|
721
722
|
</div>
|
|
722
723
|
<p className="text-[7px] text-white/30 font-medium mb-2.5" style={OUTFIT}>
|
|
723
|
-
|
|
724
|
+
Every leg must hit. One miss and the entire combined bet is lost.
|
|
724
725
|
</p>
|
|
725
726
|
|
|
726
|
-
{/* Parlay slots — concentric circles */}
|
|
727
727
|
<div className="flex items-end gap-3">
|
|
728
728
|
{Array.from({ length: config.parlayConfig.maxSlots }, (_, slot) => {
|
|
729
729
|
const legs = parlayGroups[slot] ?? [];
|
|
@@ -736,58 +736,29 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
736
736
|
|
|
737
737
|
return (
|
|
738
738
|
<div key={slot} className="flex flex-col items-center gap-1">
|
|
739
|
-
{
|
|
740
|
-
|
|
741
|
-
onClick={() => setEditingParlay(isEditing ? null : slot)}
|
|
742
|
-
className="relative flex items-center justify-center transition-all"
|
|
743
|
-
style={{
|
|
744
|
-
width: 36,
|
|
745
|
-
height: 36,
|
|
746
|
-
}}
|
|
747
|
-
>
|
|
739
|
+
<button onClick={() => setEditingParlay(isEditing ? null : slot)}
|
|
740
|
+
className="relative flex items-center justify-center transition-all" style={{ width: 36, height: 36 }}>
|
|
748
741
|
{legCount === 0 ? (
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
className="w-[28px] h-[28px] rounded-full border border-dashed transition-all"
|
|
752
|
-
style={{
|
|
753
|
-
borderColor: isEditing ? color : "rgba(255,255,255,0.15)",
|
|
754
|
-
boxShadow: isEditing ? `0 0 8px ${color}40` : "none",
|
|
755
|
-
}}
|
|
756
|
-
/>
|
|
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" }} />
|
|
757
744
|
) : (
|
|
758
|
-
/* Filled concentric rings */
|
|
759
745
|
<>
|
|
760
746
|
{Array.from({ length: Math.min(legCount, 6) }, (_, ring) => {
|
|
761
747
|
const size = 12 + ring * 5;
|
|
762
748
|
const opacity = 1 - ring * 0.12;
|
|
763
749
|
return (
|
|
764
|
-
<motion.div
|
|
765
|
-
key={ring}
|
|
766
|
-
initial={{ scale: 0 }}
|
|
767
|
-
animate={{ scale: 1 }}
|
|
750
|
+
<motion.div key={ring} initial={{ scale: 0 }} animate={{ scale: 1 }}
|
|
768
751
|
transition={{ type: "spring", stiffness: 400, damping: 20, delay: ring * 0.05 }}
|
|
769
752
|
className="absolute rounded-full"
|
|
770
|
-
style={{
|
|
771
|
-
width: size,
|
|
772
|
-
height: size,
|
|
773
|
-
border: `2px solid ${color}`,
|
|
774
|
-
opacity,
|
|
775
|
-
background: ring === 0 ? `${color}30` : "transparent",
|
|
776
|
-
boxShadow: isEditing ? `0 0 8px ${color}40` : "none",
|
|
777
|
-
}}
|
|
778
|
-
/>
|
|
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" }} />
|
|
779
754
|
);
|
|
780
755
|
})}
|
|
781
756
|
</>
|
|
782
757
|
)}
|
|
783
758
|
</button>
|
|
784
|
-
|
|
785
|
-
{/* Multiplier + info */}
|
|
786
759
|
{legCount >= 2 ? (
|
|
787
760
|
<div className="flex items-center gap-0.5">
|
|
788
|
-
<span className="text-[8px] font-bold text-white" style={OUTFIT}>
|
|
789
|
-
{multiplier}x
|
|
790
|
-
</span>
|
|
761
|
+
<span className="text-[8px] font-bold text-white" style={OUTFIT}>{multiplier}x</span>
|
|
791
762
|
<button onClick={() => setParlayInfo(parlayInfo === slot ? null : slot)}>
|
|
792
763
|
<Info size={8} className="text-white/40" />
|
|
793
764
|
</button>
|
|
@@ -795,22 +766,14 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
795
766
|
) : legCount === 1 ? (
|
|
796
767
|
<span className="text-[7px] text-white/30 font-medium" style={OUTFIT}>+1 more</span>
|
|
797
768
|
) : null}
|
|
798
|
-
|
|
799
|
-
{/* Info popup */}
|
|
800
769
|
<AnimatePresence>
|
|
801
770
|
{parlayInfo === slot && legCount >= 2 && (
|
|
802
|
-
<motion.div
|
|
803
|
-
initial={{ opacity: 0, y: 4 }}
|
|
804
|
-
animate={{ opacity: 1, y: 0 }}
|
|
805
|
-
exit={{ opacity: 0, y: 4 }}
|
|
771
|
+
<motion.div initial={{ opacity: 0, y: 4 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: 4 }}
|
|
806
772
|
className="absolute bottom-full mb-2 left-1/2 -translate-x-1/2 z-20 w-[180px] rounded-lg p-2.5"
|
|
807
|
-
style={{ background: "#1a1a2e", border: `1px solid ${color}30` }}
|
|
808
|
-
>
|
|
773
|
+
style={{ background: "#1a1a2e", border: `1px solid ${color}30` }}>
|
|
809
774
|
<div className="flex items-center justify-between mb-1.5">
|
|
810
775
|
<span className="text-[8px] text-white font-bold" style={OUTFIT}>Combined Bet</span>
|
|
811
|
-
<button onClick={() => setParlayInfo(null)}>
|
|
812
|
-
<X size={10} className="text-white/40" />
|
|
813
|
-
</button>
|
|
776
|
+
<button onClick={() => setParlayInfo(null)}><X size={10} className="text-white/40" /></button>
|
|
814
777
|
</div>
|
|
815
778
|
{breakdown.map((leg, i) => (
|
|
816
779
|
<div key={i} className="flex items-center justify-between py-0.5">
|
|
@@ -826,20 +789,12 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
826
789
|
{totalStake > 0 && (
|
|
827
790
|
<div className="flex items-center justify-between mt-0.5">
|
|
828
791
|
<span className="text-[7px] text-white/40 font-medium" style={OUTFIT}>Stake → Win</span>
|
|
829
|
-
<span className="text-[8px] text-white font-semibold" style={OUTFIT}>
|
|
830
|
-
{totalStake} → {Math.round(totalStake * multiplier).toLocaleString()}
|
|
831
|
-
</span>
|
|
792
|
+
<span className="text-[8px] text-white font-semibold" style={OUTFIT}>{totalStake} → {Math.round(totalStake * multiplier).toLocaleString()}</span>
|
|
832
793
|
</div>
|
|
833
794
|
)}
|
|
834
795
|
<p className="text-[6px] text-white/25 mt-1" style={OUTFIT}>All must hit to win</p>
|
|
835
|
-
{
|
|
836
|
-
|
|
837
|
-
onClick={() => { clearParlay(slot); setParlayInfo(null); }}
|
|
838
|
-
className="mt-1.5 text-[7px] text-red-400/60 font-medium"
|
|
839
|
-
style={OUTFIT}
|
|
840
|
-
>
|
|
841
|
-
Remove combined bet
|
|
842
|
-
</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>
|
|
843
798
|
</motion.div>
|
|
844
799
|
)}
|
|
845
800
|
</AnimatePresence>
|
|
@@ -847,22 +802,39 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
847
802
|
);
|
|
848
803
|
})}
|
|
849
804
|
</div>
|
|
850
|
-
|
|
851
|
-
{/* Edit mode hint */}
|
|
852
805
|
{editingParlay !== null && (
|
|
853
806
|
<div className="flex items-center justify-between mt-2">
|
|
854
807
|
<p className="text-[8px] font-medium" style={{ ...OUTFIT, color: parlayColors[editingParlay] }}>
|
|
855
808
|
Tap questions above to add to this combined bet
|
|
856
809
|
</p>
|
|
857
|
-
<button
|
|
858
|
-
|
|
859
|
-
className="text-[8px] text-white/50 font-semibold px-2 py-0.5 rounded"
|
|
860
|
-
style={{ ...OUTFIT, background: "rgba(255,255,255,0.06)" }}
|
|
861
|
-
>
|
|
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)" }}>
|
|
862
812
|
Done
|
|
863
813
|
</button>
|
|
864
814
|
</div>
|
|
865
815
|
)}
|
|
816
|
+
</>
|
|
817
|
+
) : (
|
|
818
|
+
<div className="flex items-center justify-between gap-3">
|
|
819
|
+
<div className="flex-1 min-w-0">
|
|
820
|
+
<p className="text-[18px] font-bold text-white leading-tight" style={OUTFIT}>
|
|
821
|
+
Now combine bets for crazy multipliers
|
|
822
|
+
</p>
|
|
823
|
+
{!parlayEnabled && (
|
|
824
|
+
<span className="inline-block mt-1.5 text-[7px] font-bold px-1.5 py-0.5 rounded-full" style={{ background: "rgba(153,69,255,0.15)", color: "#9945FF", border: "1px solid rgba(153,69,255,0.3)" }}>
|
|
825
|
+
Coming in v2
|
|
826
|
+
</span>
|
|
827
|
+
)}
|
|
828
|
+
</div>
|
|
829
|
+
<button
|
|
830
|
+
onClick={parlayEnabled ? () => setEditingParlay(0) : undefined}
|
|
831
|
+
className="relative w-[65px] min-h-[54px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center py-3"
|
|
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)" }} />
|
|
834
|
+
<Play size={26} fill="white" strokeWidth={0} className="relative z-10" />
|
|
835
|
+
</button>
|
|
836
|
+
</div>
|
|
837
|
+
)}
|
|
866
838
|
</div>
|
|
867
839
|
</div>
|
|
868
840
|
|
|
@@ -884,25 +856,15 @@ export const PreMatchGame = ({ config, bets, onBetsChange, onOptionClick, onAmou
|
|
|
884
856
|
<span className="text-[8px] text-white/20 font-semibold tracking-widest" style={OUTFIT}>···</span>
|
|
885
857
|
</div>
|
|
886
858
|
)}
|
|
887
|
-
<LeaderboardRow
|
|
888
|
-
entry={entry}
|
|
889
|
-
rank={entry.rank ?? i + 1}
|
|
890
|
-
isLast={i === arr.length - 1}
|
|
891
|
-
/>
|
|
859
|
+
<LeaderboardRow entry={entry} rank={entry.rank ?? i + 1} isLast={i === arr.length - 1} />
|
|
892
860
|
</div>
|
|
893
861
|
))}
|
|
894
862
|
</div>
|
|
895
863
|
{onViewLeaderboard && leaderboardRows.length > 0 && (
|
|
896
|
-
<button
|
|
897
|
-
|
|
898
|
-
className="mt-2 w-full text-center text-[9px] font-semibold"
|
|
899
|
-
style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}
|
|
900
|
-
>
|
|
901
|
-
See Full Leaderboard →
|
|
902
|
-
</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>
|
|
903
866
|
)}
|
|
904
867
|
</div>
|
|
905
|
-
|
|
906
868
|
</>
|
|
907
869
|
)}
|
|
908
870
|
</div>
|