@devrongx/games 0.3.4 → 0.4.0
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.
- package/package.json +1 -1
- package/src/games/prematch-bets/FullLeaderboard.tsx +149 -0
- package/src/games/prematch-bets/PreMatchBetsPopup.tsx +306 -76
- package/src/games/prematch-bets/PreMatchGame.tsx +64 -8
- package/src/games/prematch-bets/PreMatchLive.tsx +192 -0
- package/src/games/prematch-bets/PreMatchQuestions.tsx +66 -42
- package/src/games/prematch-bets/PreMatchResults.tsx +211 -0
- package/src/games/prematch-bets/PreMatchSubmitted.tsx +183 -0
- package/src/games/prematch-bets/config.ts +1 -0
- package/src/games/prematch-bets/constants.tsx +39 -1
- package/src/index.ts +16 -0
- package/src/matches/MatchCalendar.tsx +100 -50
- package/src/pools/actions.ts +19 -0
- package/src/pools/fetcher.ts +90 -0
- package/src/pools/hooks.ts +173 -0
- package/src/pools/index.ts +44 -0
- package/src/pools/mapper.ts +85 -0
- package/src/pools/types.ts +194 -0
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
import { useState, useEffect, useMemo, useCallback } 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 } from "lucide-react";
|
|
7
|
+
import { ChevronDown, Info, X, Play, Send } from "lucide-react";
|
|
8
8
|
import { IBetSummary, ILeaderboardEntry, IChallengeConfig, IUserBets, deriveParlayGroups, deriveMarketToParlay, calcDisplayReward, optionReward, calcParlayMultiplier } from "./config";
|
|
9
|
-
import { OUTFIT, MARKET_ICONS, PointsIcon, SelectedCheck } from "./constants";
|
|
9
|
+
import { OUTFIT, MARKET_ICONS, PointsIcon, SelectedCheck, AiInsightButton } from "./constants";
|
|
10
10
|
import { useGamePopupStore } from "../../core/gamePopupStore";
|
|
11
11
|
import { LeaderboardRow } from "./LeaderboardRow";
|
|
12
12
|
|
|
@@ -37,10 +37,18 @@ interface PreMatchGameProps {
|
|
|
37
37
|
leaderboardRows: ILeaderboardEntry[];
|
|
38
38
|
/** Render only the markets section (no header/balance/leaderboard) — used for card preview */
|
|
39
39
|
marketsOnly?: boolean;
|
|
40
|
+
/** Called when user taps Submit Bets. If provided, shows Submit button instead of Play. */
|
|
41
|
+
onSubmit?: () => Promise<void> | void;
|
|
42
|
+
/** Called when user taps "See Full Leaderboard" */
|
|
43
|
+
onViewLeaderboard?: () => void;
|
|
44
|
+
/** v1: parlays are cosmetic, shows "Coming in v2". Default = false (hides section) */
|
|
45
|
+
parlayEnabled?: boolean;
|
|
46
|
+
/** Whether a submit is in progress */
|
|
47
|
+
submitting?: boolean;
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
|
|
43
|
-
export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOptionClick, onAmountSelect, betSummary, leaderboardRows, marketsOnly }: PreMatchGameProps) => {
|
|
51
|
+
export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOptionClick, onAmountSelect, betSummary, leaderboardRows, marketsOnly, onSubmit, onViewLeaderboard, parlayEnabled = false, submitting = false }: PreMatchGameProps) => {
|
|
44
52
|
const { selectedCount, compoundMultiplier, totalEntry, compoundedReward, remainingBalance, riskPercent } = betSummary;
|
|
45
53
|
|
|
46
54
|
const goTo = useGamePopupStore(s => s.goTo);
|
|
@@ -355,6 +363,14 @@ export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOpt
|
|
|
355
363
|
);
|
|
356
364
|
})}
|
|
357
365
|
|
|
366
|
+
{/* AI Insights button — right-aligned, below options */}
|
|
367
|
+
<div className="col-span-2 flex justify-end pr-4 mt-3">
|
|
368
|
+
<AiInsightButton
|
|
369
|
+
question={market.question}
|
|
370
|
+
options={market.options.map(o => o.label)}
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
358
374
|
{/* Amount picker — inline chip carousel (always reserves height when picker is open) */}
|
|
359
375
|
{isPickerOpenForMarket && (
|
|
360
376
|
<div className="col-span-2 mt-0.5 relative">
|
|
@@ -563,11 +579,36 @@ export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOpt
|
|
|
563
579
|
)}
|
|
564
580
|
</AnimatePresence>
|
|
565
581
|
</div>
|
|
566
|
-
{/* Right —
|
|
582
|
+
{/* Right — action button, hidden until points are bet */}
|
|
567
583
|
<AnimatePresence>
|
|
568
584
|
{totalEntry > 0 && (() => {
|
|
569
585
|
const spentPercent = totalEntry / config.startingBalance;
|
|
570
586
|
const fillPercent = 5 + spentPercent * 95;
|
|
587
|
+
|
|
588
|
+
if (onSubmit) {
|
|
589
|
+
return (
|
|
590
|
+
<motion.button
|
|
591
|
+
onClick={onSubmit}
|
|
592
|
+
disabled={submitting}
|
|
593
|
+
className="relative rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center gap-1.5 px-3"
|
|
594
|
+
style={{ background: submitting ? "rgba(34,227,232,0.3)" : "linear-gradient(135deg, #22E3E8, #9945FF, #f83cc5)", minWidth: 90, height: "100%" }}
|
|
595
|
+
initial={{ opacity: 0, width: 0 }}
|
|
596
|
+
animate={{ opacity: 1, width: "auto" }}
|
|
597
|
+
exit={{ opacity: 0, width: 0 }}
|
|
598
|
+
transition={{ width: { duration: 0.4, ease: "easeOut", delay: 0.8 }, opacity: { duration: 0.3, delay: 1 } }}
|
|
599
|
+
>
|
|
600
|
+
{submitting ? (
|
|
601
|
+
<span className="text-[10px] font-bold text-white/70" style={OUTFIT}>Submitting…</span>
|
|
602
|
+
) : (
|
|
603
|
+
<>
|
|
604
|
+
<Send size={14} className="text-white" />
|
|
605
|
+
<span className="text-[11px] font-bold text-white" style={OUTFIT}>Submit Bets</span>
|
|
606
|
+
</>
|
|
607
|
+
)}
|
|
608
|
+
</motion.button>
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
571
612
|
return (
|
|
572
613
|
<motion.div
|
|
573
614
|
className="relative w-[65px] rounded-xl overflow-hidden flex-shrink-0 flex items-center justify-center"
|
|
@@ -577,7 +618,6 @@ export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOpt
|
|
|
577
618
|
exit={{ opacity: 0, width: 0 }}
|
|
578
619
|
transition={{ width: { duration: 0.4, ease: "easeOut", delay: 0.8 }, opacity: { duration: 0.3, delay: 1 } }}
|
|
579
620
|
>
|
|
580
|
-
{/* Fill bar — spring-animated left to right */}
|
|
581
621
|
<motion.div
|
|
582
622
|
className="absolute inset-y-0 left-0"
|
|
583
623
|
animate={{ width: `${fillPercent}%` }}
|
|
@@ -644,9 +684,16 @@ export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOpt
|
|
|
644
684
|
|
|
645
685
|
{/* Combined Bets (Parlays) */}
|
|
646
686
|
<div className="mt-4">
|
|
647
|
-
<
|
|
648
|
-
|
|
649
|
-
|
|
687
|
+
<div className="flex items-center gap-2 mb-1">
|
|
688
|
+
<p className="text-[9px] text-white/60 font-semibold uppercase tracking-wider" style={OUTFIT}>
|
|
689
|
+
Combine for crazy multipliers
|
|
690
|
+
</p>
|
|
691
|
+
{!parlayEnabled && (
|
|
692
|
+
<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)" }}>
|
|
693
|
+
Coming in v2
|
|
694
|
+
</span>
|
|
695
|
+
)}
|
|
696
|
+
</div>
|
|
650
697
|
<p className="text-[7px] text-white/30 font-medium mb-2.5" style={OUTFIT}>
|
|
651
698
|
All must hit to win. You lose all if any one goes wrong.
|
|
652
699
|
</p>
|
|
@@ -820,6 +867,15 @@ export const PreMatchGame = ({ config, bets, onBetsChange, expandedPicker, onOpt
|
|
|
820
867
|
</div>
|
|
821
868
|
))}
|
|
822
869
|
</div>
|
|
870
|
+
{onViewLeaderboard && leaderboardRows.length > 0 && (
|
|
871
|
+
<button
|
|
872
|
+
onClick={onViewLeaderboard}
|
|
873
|
+
className="mt-2 w-full text-center text-[9px] font-semibold"
|
|
874
|
+
style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}
|
|
875
|
+
>
|
|
876
|
+
See Full Leaderboard →
|
|
877
|
+
</button>
|
|
878
|
+
)}
|
|
823
879
|
</div>
|
|
824
880
|
|
|
825
881
|
</>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// @devrongx/games — games/prematch-bets/PreMatchLive.tsx
|
|
2
|
+
// Live match view: shows challenge resolution progress + live leaderboard.
|
|
3
|
+
// Pool status = CLOSED or RESOLVING.
|
|
4
|
+
"use client";
|
|
5
|
+
|
|
6
|
+
import { useEffect } from "react";
|
|
7
|
+
import { CheckCircle2, Clock, ArrowRight, Radio } from "lucide-react";
|
|
8
|
+
import { IChallengeConfig } from "./config";
|
|
9
|
+
import { LeaderboardRow } from "./LeaderboardRow";
|
|
10
|
+
import { OUTFIT, MARKET_ICONS } from "./constants";
|
|
11
|
+
import type { ITDPoolDetail, ITDMyEntryResponse, ITDLeaderboardEntry } from "../../pools/types";
|
|
12
|
+
|
|
13
|
+
interface PreMatchLiveProps {
|
|
14
|
+
config: IChallengeConfig;
|
|
15
|
+
poolId: number;
|
|
16
|
+
pool: ITDPoolDetail | null;
|
|
17
|
+
entryData: ITDMyEntryResponse | null;
|
|
18
|
+
rankings: ITDLeaderboardEntry[];
|
|
19
|
+
onRefresh: () => void;
|
|
20
|
+
onViewFullLeaderboard: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function PreMatchLive({
|
|
24
|
+
config,
|
|
25
|
+
pool,
|
|
26
|
+
entryData,
|
|
27
|
+
rankings,
|
|
28
|
+
onRefresh,
|
|
29
|
+
onViewFullLeaderboard,
|
|
30
|
+
}: PreMatchLiveProps) {
|
|
31
|
+
// Poll every 20s during live phase
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const id = setInterval(onRefresh, 20_000);
|
|
34
|
+
return () => clearInterval(id);
|
|
35
|
+
}, [onRefresh]);
|
|
36
|
+
|
|
37
|
+
const entry = entryData?.entry;
|
|
38
|
+
const myBets = entryData?.bets ?? [];
|
|
39
|
+
|
|
40
|
+
const challenges = pool?.challenges ?? config.markets.map((m, i) => ({
|
|
41
|
+
id: m.backendChallengeId ?? i,
|
|
42
|
+
question: m.question,
|
|
43
|
+
icon: m.icon,
|
|
44
|
+
accent_color: m.accent,
|
|
45
|
+
status: "open",
|
|
46
|
+
correct_option: null as string | null,
|
|
47
|
+
options: m.options.map((o) => ({ key: o.label.toLowerCase().replace(/\s+/g, "_"), label: o.label, odds: o.odds, entry: o.entry, risk: o.risk })),
|
|
48
|
+
category: m.category,
|
|
49
|
+
sort_order: i,
|
|
50
|
+
resolved_at: null as string | null,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
const resolvedCount = challenges.filter((c) => c.status === "resolved").length;
|
|
54
|
+
const totalCount = challenges.length;
|
|
55
|
+
const progressPct = totalCount > 0 ? Math.round((resolvedCount / totalCount) * 100) : 0;
|
|
56
|
+
|
|
57
|
+
const myRank = entry ? rankings.findIndex((r) => r.entry_id === entry.id) + 1 : null;
|
|
58
|
+
const miniRankings = rankings.slice(0, 5);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="w-full flex flex-col gap-4 px-4 pb-8">
|
|
62
|
+
{/* Live header */}
|
|
63
|
+
<div className="flex items-center gap-2 pt-5">
|
|
64
|
+
<div className="flex items-center gap-1">
|
|
65
|
+
<span className="relative flex h-2 w-2">
|
|
66
|
+
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-[#f83cc5] opacity-75" />
|
|
67
|
+
<span className="relative inline-flex rounded-full h-2 w-2 bg-[#f83cc5]" />
|
|
68
|
+
</span>
|
|
69
|
+
<span className="text-[11px] font-bold tracking-widest" style={{ color: "#f83cc5", fontFamily: "Barlow Condensed, sans-serif" }}>LIVE</span>
|
|
70
|
+
</div>
|
|
71
|
+
<span className="text-[14px] font-bold text-white" style={OUTFIT}>{config.matchTitle}</span>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{/* Balance + rank */}
|
|
75
|
+
{entry && (
|
|
76
|
+
<div className="flex items-center gap-3 px-3 py-2.5 rounded-xl" style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
|
77
|
+
<div className="flex-1">
|
|
78
|
+
<p className="text-[9px] text-white/40 uppercase tracking-wide" style={OUTFIT}>Balance</p>
|
|
79
|
+
<p className="text-[18px] font-bold text-white" style={OUTFIT}>{entry.current_coins.toLocaleString()} pts</p>
|
|
80
|
+
</div>
|
|
81
|
+
{myRank !== null && myRank > 0 && (
|
|
82
|
+
<div className="text-right">
|
|
83
|
+
<p className="text-[9px] text-white/40 uppercase tracking-wide" style={OUTFIT}>Your Rank</p>
|
|
84
|
+
<p className="text-[18px] font-bold" style={{ ...OUTFIT, color: "#22E3E8" }}>#{myRank} <span className="text-[11px] text-white/40">of {rankings.length}</span></p>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
|
|
91
|
+
|
|
92
|
+
{/* Challenge progress */}
|
|
93
|
+
<div>
|
|
94
|
+
<div className="flex items-center justify-between mb-2">
|
|
95
|
+
<p className="text-[10px] font-semibold uppercase tracking-wider text-white/50" style={OUTFIT}>
|
|
96
|
+
RESULTS ({resolvedCount}/{totalCount} resolved)
|
|
97
|
+
</p>
|
|
98
|
+
<span className="text-[9px] font-bold" style={{ ...OUTFIT, color: "#22E3E8" }}>{progressPct}%</span>
|
|
99
|
+
</div>
|
|
100
|
+
<div className="h-[5px] rounded-full mb-3" style={{ background: "rgba(255,255,255,0.06)" }}>
|
|
101
|
+
<div className="h-full rounded-full transition-all duration-700" style={{ width: `${progressPct}%`, background: "linear-gradient(90deg, #22E3E8, #9945FF)" }} />
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="flex flex-col gap-1.5">
|
|
105
|
+
{challenges.map((c) => {
|
|
106
|
+
const Icon = MARKET_ICONS[c.icon] ?? MARKET_ICONS.coin;
|
|
107
|
+
const myBet = myBets.find((b) => b.challenge_id === c.id);
|
|
108
|
+
const isResolved = c.status === "resolved";
|
|
109
|
+
const won = isResolved && myBet?.status === "won";
|
|
110
|
+
const lost = isResolved && myBet?.status === "lost";
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div
|
|
114
|
+
key={c.id}
|
|
115
|
+
className="flex items-start gap-2 px-3 py-2 rounded-lg"
|
|
116
|
+
style={{
|
|
117
|
+
background: isResolved
|
|
118
|
+
? won ? "rgba(34,197,94,0.06)" : lost ? "rgba(239,68,68,0.06)" : "rgba(255,255,255,0.03)"
|
|
119
|
+
: "rgba(255,255,255,0.03)",
|
|
120
|
+
border: `1px solid ${isResolved ? won ? "rgba(34,197,94,0.2)" : lost ? "rgba(239,68,68,0.2)" : "rgba(255,255,255,0.06)" : "rgba(255,255,255,0.05)"}`,
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{isResolved ? (
|
|
124
|
+
won
|
|
125
|
+
? <CheckCircle2 size={12} style={{ color: "#22c55e", flexShrink: 0, marginTop: 1 }} />
|
|
126
|
+
: <span className="text-[10px] flex-shrink-0 mt-0.5" style={{ color: "#ef4444" }}>✗</span>
|
|
127
|
+
) : (
|
|
128
|
+
<Clock size={12} style={{ color: "rgba(255,255,255,0.25)", flexShrink: 0, marginTop: 1 }} />
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
<div className="flex-1 min-w-0">
|
|
132
|
+
<p className="text-[10px] text-white/60 font-medium leading-tight" style={OUTFIT}>{c.question}</p>
|
|
133
|
+
{myBet ? (
|
|
134
|
+
<p className="text-[9px] mt-0.5" style={{ ...OUTFIT, color: isResolved ? won ? "#22c55e" : "#ef4444" : "rgba(255,255,255,0.4)" }}>
|
|
135
|
+
{c.options.find((o) => o.key === myBet.selected_option)?.label ?? myBet.selected_option}
|
|
136
|
+
{" @ "}{myBet.odds_at_bet}x — {myBet.coin_amount} pts
|
|
137
|
+
{isResolved && won && ` → +${Math.round(myBet.actual_return ?? myBet.potential_return)} pts`}
|
|
138
|
+
{isResolved && lost && " → Lost"}
|
|
139
|
+
</p>
|
|
140
|
+
) : (
|
|
141
|
+
<p className="text-[8px] text-white/25 mt-0.5" style={OUTFIT}>No bet placed</p>
|
|
142
|
+
)}
|
|
143
|
+
{isResolved && c.correct_option && (
|
|
144
|
+
<p className="text-[8px] text-white/35 mt-0.5" style={OUTFIT}>
|
|
145
|
+
Answer: {c.options.find((o) => o.key === c.correct_option)?.label ?? c.correct_option}
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
{!isResolved && <p className="text-[8px] text-white/25 mt-0.5" style={OUTFIT}>Waiting for resolution…</p>}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="h-px" style={{ background: "rgba(255,255,255,0.06)" }} />
|
|
157
|
+
|
|
158
|
+
{/* Live leaderboard */}
|
|
159
|
+
{miniRankings.length > 0 && (
|
|
160
|
+
<div>
|
|
161
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
162
|
+
<Radio size={10} style={{ color: "#f83cc5" }} />
|
|
163
|
+
<p className="text-[10px] font-semibold uppercase tracking-wider text-white/50" style={OUTFIT}>LIVE LEADERBOARD</p>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="flex flex-col">
|
|
166
|
+
{miniRankings.map((r) => (
|
|
167
|
+
<LeaderboardRow
|
|
168
|
+
key={r.entry_id}
|
|
169
|
+
entry={{
|
|
170
|
+
wallet: r.partner_ext_id ?? `User #${r.user_id}`,
|
|
171
|
+
pts: r.current_coins,
|
|
172
|
+
payout: 0,
|
|
173
|
+
isYou: r.entry_id === entry?.id,
|
|
174
|
+
rank: r.rank,
|
|
175
|
+
}}
|
|
176
|
+
rank={r.rank}
|
|
177
|
+
isLast={false}
|
|
178
|
+
/>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
<button
|
|
182
|
+
onClick={onViewFullLeaderboard}
|
|
183
|
+
className="mt-2 flex items-center justify-center gap-1 w-full text-[9px] font-semibold"
|
|
184
|
+
style={{ ...OUTFIT, color: "rgba(34,227,232,0.6)" }}
|
|
185
|
+
>
|
|
186
|
+
See Full Leaderboard <ArrowRight size={10} />
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
@@ -5,7 +5,7 @@ import { useState, useCallback, useEffect } from "react";
|
|
|
5
5
|
import { motion, AnimatePresence } from "framer-motion";
|
|
6
6
|
import { Check, Rewind, FastForward, ArrowRight } from "lucide-react";
|
|
7
7
|
import { IChallengeConfig, IChallengeMarket, IUserBets } from "./config";
|
|
8
|
-
import { OUTFIT, MARKET_ICONS, PointsIcon } from "./constants";
|
|
8
|
+
import { OUTFIT, MARKET_ICONS, PointsIcon, AiInsightButton } from "./constants";
|
|
9
9
|
import { useGamePopupStore } from "../../core/gamePopupStore";
|
|
10
10
|
|
|
11
11
|
interface PreMatchQuestionsProps {
|
|
@@ -80,6 +80,62 @@ const StartSlide = ({ onStart }: { onStart: () => void }) => {
|
|
|
80
80
|
);
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
+
// ── Points hint + locked carousel (hidden for now — uncomment when points flow is live) ──
|
|
84
|
+
// const QuestionPointsHint = ({
|
|
85
|
+
// market,
|
|
86
|
+
// startingBalance,
|
|
87
|
+
// stakeIncrements,
|
|
88
|
+
// }: {
|
|
89
|
+
// market: IChallengeMarket;
|
|
90
|
+
// startingBalance: number;
|
|
91
|
+
// stakeIncrements: number;
|
|
92
|
+
// }) => {
|
|
93
|
+
// const chipCount = Math.floor(startingBalance / stakeIncrements);
|
|
94
|
+
// return (
|
|
95
|
+
// <motion.div
|
|
96
|
+
// initial={{ opacity: 0, y: 14 }}
|
|
97
|
+
// animate={{ opacity: 1, y: 0 }}
|
|
98
|
+
// transition={{ duration: 0.5, delay: 0.6 }}
|
|
99
|
+
// className="w-full mt-8 flex flex-col gap-2.5 -mx-5"
|
|
100
|
+
// style={{ width: "calc(100% + 40px)", paddingLeft: 20 }}
|
|
101
|
+
// >
|
|
102
|
+
// <p className="text-[11px] text-white font-medium leading-relaxed" style={OUTFIT}>
|
|
103
|
+
// Pick using your cricket knowledge — the{" "}
|
|
104
|
+
// <span className="text-[#22E3E8] font-bold">{market.options[0].odds}x</span>
|
|
105
|
+
// {" "}
|
|
106
|
+
// <PointsIcon size={11} className="inline-block align-text-bottom" />
|
|
107
|
+
// {" "}multiplier next to each option is how much your points grow if you{"'"}re right.
|
|
108
|
+
// </p>
|
|
109
|
+
// <p className="text-[11px] text-white/70 font-medium" style={OUTFIT}>
|
|
110
|
+
// How many points to bet? You{"'"}ll decide that next.
|
|
111
|
+
// </p>
|
|
112
|
+
// {/* Locked chips carousel — extends past right edge */}
|
|
113
|
+
// <div className="relative mt-0.5 overflow-hidden" style={{ marginRight: -20 }}>
|
|
114
|
+
// <div className="flex items-center gap-1.5 py-1 opacity-40">
|
|
115
|
+
// {Array.from({ length: chipCount }, (_, i) => (i + 1) * stakeIncrements).map(amt => (
|
|
116
|
+
// <div
|
|
117
|
+
// key={amt}
|
|
118
|
+
// className="flex-shrink-0 flex items-center gap-0.5 px-2.5 py-[4px] rounded"
|
|
119
|
+
// style={{
|
|
120
|
+
// background: "rgba(34,227,232,0.08)",
|
|
121
|
+
// border: "1px solid rgba(34,227,232,0.15)",
|
|
122
|
+
// }}
|
|
123
|
+
// >
|
|
124
|
+
// <PointsIcon size={8} />
|
|
125
|
+
// <span className="text-[9px] font-bold text-[#22E3E8]" style={OUTFIT}>{amt}</span>
|
|
126
|
+
// </div>
|
|
127
|
+
// ))}
|
|
128
|
+
// </div>
|
|
129
|
+
// {/* Right fade */}
|
|
130
|
+
// <div
|
|
131
|
+
// className="absolute inset-y-0 right-0 w-20 pointer-events-none"
|
|
132
|
+
// style={{ background: "linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.85) 70%, black 100%)" }}
|
|
133
|
+
// />
|
|
134
|
+
// </div>
|
|
135
|
+
// </motion.div>
|
|
136
|
+
// );
|
|
137
|
+
// };
|
|
138
|
+
|
|
83
139
|
// ── Single question slide ──
|
|
84
140
|
const QuestionSlide = ({
|
|
85
141
|
market,
|
|
@@ -95,6 +151,7 @@ const QuestionSlide = ({
|
|
|
95
151
|
stakeIncrements: number;
|
|
96
152
|
}) => {
|
|
97
153
|
const chipCount = Math.floor(startingBalance / stakeIncrements);
|
|
154
|
+
void chipCount; // unused while QuestionPointsHint is hidden
|
|
98
155
|
const IconComponent = MARKET_ICONS[market.icon];
|
|
99
156
|
|
|
100
157
|
return (
|
|
@@ -190,50 +247,17 @@ const QuestionSlide = ({
|
|
|
190
247
|
})}
|
|
191
248
|
</motion.div>
|
|
192
249
|
|
|
193
|
-
{/*
|
|
250
|
+
{/* AI Insights button — right-aligned, below options */}
|
|
194
251
|
<motion.div
|
|
195
|
-
initial={{ opacity: 0
|
|
196
|
-
animate={{ opacity: 1
|
|
197
|
-
transition={{ duration: 0.
|
|
198
|
-
className="w-full
|
|
199
|
-
style={{ width: "calc(100% + 40px)", paddingLeft: 20 }}
|
|
252
|
+
initial={{ opacity: 0 }}
|
|
253
|
+
animate={{ opacity: 1 }}
|
|
254
|
+
transition={{ duration: 0.4, delay: 0.55 }}
|
|
255
|
+
className="w-full max-w-[320px] flex justify-end pr-4 mt-8"
|
|
200
256
|
>
|
|
201
|
-
<
|
|
202
|
-
Pick using your cricket knowledge — the{" "}
|
|
203
|
-
<span className="text-[#22E3E8] font-bold">{market.options[0].odds}x</span>
|
|
204
|
-
{" "}
|
|
205
|
-
<PointsIcon size={11} className="inline-block align-text-bottom" />
|
|
206
|
-
{" "}multiplier next to each option is how much your points grow if you{"'"}re right.
|
|
207
|
-
</p>
|
|
208
|
-
<p className="text-[11px] text-white/70 font-medium" style={OUTFIT}>
|
|
209
|
-
How many points to bet? You{"'"}ll decide that next.
|
|
210
|
-
</p>
|
|
211
|
-
|
|
212
|
-
{/* Locked chips carousel — extends past right edge.
|
|
213
|
-
Chips have reduced opacity directly so there's no flash before blur loads. */}
|
|
214
|
-
<div className="relative mt-0.5 overflow-hidden" style={{ marginRight: -20 }}>
|
|
215
|
-
<div className="flex items-center gap-1.5 py-1 opacity-40">
|
|
216
|
-
{Array.from({ length: chipCount }, (_, i) => (i + 1) * stakeIncrements).map(amt => (
|
|
217
|
-
<div
|
|
218
|
-
key={amt}
|
|
219
|
-
className="flex-shrink-0 flex items-center gap-0.5 px-2.5 py-[4px] rounded"
|
|
220
|
-
style={{
|
|
221
|
-
background: "rgba(34,227,232,0.08)",
|
|
222
|
-
border: "1px solid rgba(34,227,232,0.15)",
|
|
223
|
-
}}
|
|
224
|
-
>
|
|
225
|
-
<PointsIcon size={8} />
|
|
226
|
-
<span className="text-[9px] font-bold text-[#22E3E8]" style={OUTFIT}>{amt}</span>
|
|
227
|
-
</div>
|
|
228
|
-
))}
|
|
229
|
-
</div>
|
|
230
|
-
{/* Right fade — vanishes into the edge */}
|
|
231
|
-
<div
|
|
232
|
-
className="absolute inset-y-0 right-0 w-20 pointer-events-none"
|
|
233
|
-
style={{ background: "linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.85) 70%, black 100%)" }}
|
|
234
|
-
/>
|
|
235
|
-
</div>
|
|
257
|
+
<AiInsightButton question={market.question} options={market.options.map(o => o.label)} />
|
|
236
258
|
</motion.div>
|
|
259
|
+
|
|
260
|
+
{/* <QuestionPointsHint market={market} startingBalance={startingBalance} stakeIncrements={stakeIncrements} /> */}
|
|
237
261
|
</div>
|
|
238
262
|
);
|
|
239
263
|
};
|