@devrongx/games 0.1.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 +33 -0
- package/src/core/GamePopupShell.tsx +67 -0
- package/src/core/gamePopupStore.ts +30 -0
- package/src/core/types.ts +20 -0
- package/src/games/prematch-bets/LeaderboardRow.tsx +67 -0
- package/src/games/prematch-bets/PreMatchBetsPopup.tsx +97 -0
- package/src/games/prematch-bets/PreMatchGame.tsx +829 -0
- package/src/games/prematch-bets/PreMatchIntro.tsx +759 -0
- package/src/games/prematch-bets/PreMatchQuestions.tsx +491 -0
- package/src/games/prematch-bets/config.ts +604 -0
- package/src/games/prematch-bets/constants.tsx +33 -0
- package/src/games/prematch-bets/index.ts +4 -0
- package/src/index.ts +10 -0
- package/src/styles/animations.css +16 -0
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@devrongx/games",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Game UI components for sports prediction markets",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./*": "./*"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"react": ">=18",
|
|
20
|
+
"react-dom": ">=18",
|
|
21
|
+
"next": ">=14",
|
|
22
|
+
"framer-motion": ">=10",
|
|
23
|
+
"zustand": ">=4",
|
|
24
|
+
"lucide-react": ">=0.300",
|
|
25
|
+
"clsx": ">=2",
|
|
26
|
+
"seedrandom": ">=3"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/react": "^19",
|
|
30
|
+
"@types/react-dom": "^19",
|
|
31
|
+
"@types/seedrandom": "^3.0.8"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @devrongx/games — core/GamePopupShell.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { ReactNode } from "react";
|
|
5
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
6
|
+
import { X, ChevronLeft } from "lucide-react";
|
|
7
|
+
import { useGamePopupStore } from "./gamePopupStore";
|
|
8
|
+
|
|
9
|
+
interface GamePopupShellProps {
|
|
10
|
+
gameId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
hideHeaderViews?: string[];
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const GamePopupShell = ({ gameId, title, hideHeaderViews, children }: GamePopupShellProps) => {
|
|
17
|
+
const { activeGame, activeView, viewHistory, close, goBack } = useGamePopupStore();
|
|
18
|
+
|
|
19
|
+
const isOpen = activeGame === gameId;
|
|
20
|
+
const showHeader = !hideHeaderViews?.includes(activeView);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<AnimatePresence>
|
|
24
|
+
{isOpen && (
|
|
25
|
+
<motion.div
|
|
26
|
+
className="fixed inset-0 z-50 flex flex-col"
|
|
27
|
+
style={{ background: "linear-gradient(180deg, #0a0a12 0%, #111119 100%)" }}
|
|
28
|
+
initial={{ opacity: 0 }}
|
|
29
|
+
animate={{ opacity: 1 }}
|
|
30
|
+
exit={{ opacity: 0 }}
|
|
31
|
+
transition={{ duration: 0.2 }}
|
|
32
|
+
>
|
|
33
|
+
{showHeader && (
|
|
34
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
|
|
35
|
+
{viewHistory.length > 0 ? (
|
|
36
|
+
<button onClick={goBack} className="text-white/60 p-0.5">
|
|
37
|
+
<ChevronLeft size={20} />
|
|
38
|
+
</button>
|
|
39
|
+
) : (
|
|
40
|
+
<div className="w-[21px]" />
|
|
41
|
+
)}
|
|
42
|
+
<span className="text-[13px] text-white font-bold" style={{ fontFamily: "Outfit, sans-serif" }}>{title}</span>
|
|
43
|
+
<button onClick={close} className="text-white/60 p-0.5">
|
|
44
|
+
<X size={18} />
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
|
|
49
|
+
<div className="flex-1 overflow-y-auto overflow-x-hidden">
|
|
50
|
+
<AnimatePresence mode="wait">
|
|
51
|
+
<motion.div
|
|
52
|
+
key={activeView}
|
|
53
|
+
initial={{ opacity: 0, x: 30 }}
|
|
54
|
+
animate={{ opacity: 1, x: 0 }}
|
|
55
|
+
exit={{ opacity: 0, x: -30 }}
|
|
56
|
+
transition={{ duration: 0.2 }}
|
|
57
|
+
className="min-h-full h-full"
|
|
58
|
+
>
|
|
59
|
+
{children}
|
|
60
|
+
</motion.div>
|
|
61
|
+
</AnimatePresence>
|
|
62
|
+
</div>
|
|
63
|
+
</motion.div>
|
|
64
|
+
)}
|
|
65
|
+
</AnimatePresence>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// @devrongx/games — core/gamePopupStore.ts
|
|
2
|
+
|
|
3
|
+
import { create, type StoreApi, type UseBoundStore } from "zustand";
|
|
4
|
+
import type { IGamePopupState } from "./types";
|
|
5
|
+
|
|
6
|
+
export const useGamePopupStore: UseBoundStore<StoreApi<IGamePopupState>> = create<IGamePopupState>((set) => ({
|
|
7
|
+
activeGame: null,
|
|
8
|
+
activeView: "",
|
|
9
|
+
viewHistory: [],
|
|
10
|
+
|
|
11
|
+
open: (gameId, initialView) =>
|
|
12
|
+
set({ activeGame: gameId, activeView: initialView, viewHistory: [] }),
|
|
13
|
+
|
|
14
|
+
close: () =>
|
|
15
|
+
set({ activeGame: null, activeView: "", viewHistory: [] }),
|
|
16
|
+
|
|
17
|
+
goTo: (view) =>
|
|
18
|
+
set((s) => ({
|
|
19
|
+
activeView: view,
|
|
20
|
+
viewHistory: [...s.viewHistory, s.activeView],
|
|
21
|
+
})),
|
|
22
|
+
|
|
23
|
+
goBack: () =>
|
|
24
|
+
set((s) => {
|
|
25
|
+
const history = [...s.viewHistory];
|
|
26
|
+
const prev = history.pop();
|
|
27
|
+
if (prev != null) return { activeView: prev, viewHistory: history };
|
|
28
|
+
return { activeGame: null, activeView: "", viewHistory: [] };
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// @devrongx/games — core/types.ts
|
|
2
|
+
// Shared types for the game popup infrastructure.
|
|
3
|
+
|
|
4
|
+
export interface IGamePopupState {
|
|
5
|
+
/** Which game's popup is open, null = closed */
|
|
6
|
+
activeGame: string | null;
|
|
7
|
+
/** Current view key within the popup (game-defined, e.g. "intro", "game", "results") */
|
|
8
|
+
activeView: string;
|
|
9
|
+
/** Stack for back-navigation between views */
|
|
10
|
+
viewHistory: string[];
|
|
11
|
+
|
|
12
|
+
/** Open a game popup at a specific view */
|
|
13
|
+
open: (gameId: string, initialView: string) => void;
|
|
14
|
+
/** Close the popup and reset */
|
|
15
|
+
close: () => void;
|
|
16
|
+
/** Navigate to a new view, pushing current to history */
|
|
17
|
+
goTo: (view: string) => void;
|
|
18
|
+
/** Go back one view, or close if at the first view */
|
|
19
|
+
goBack: () => void;
|
|
20
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @devrongx/games — games/prematch-bets/LeaderboardRow.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import Image from "next/image";
|
|
5
|
+
import { ILeaderboardEntry } from "./config";
|
|
6
|
+
|
|
7
|
+
const RANK_STYLES = [
|
|
8
|
+
{ accent: "#FFD700" },
|
|
9
|
+
{ accent: "#C0C0C0" },
|
|
10
|
+
{ accent: "#CD7F32" },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
interface LeaderboardRowProps {
|
|
14
|
+
entry: ILeaderboardEntry;
|
|
15
|
+
rank: number;
|
|
16
|
+
isLast?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const LeaderboardRow = ({ entry, rank, isLast }: LeaderboardRowProps) => {
|
|
20
|
+
const rankIdx = rank - 1;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
className={`flex items-center gap-2 px-3 py-[5px] transition-all duration-700 ${entry.isYou ? "rounded" : ""}`}
|
|
25
|
+
style={{
|
|
26
|
+
borderBottom: isLast ? "none" : "1px solid rgba(255,255,255,0.03)",
|
|
27
|
+
background: entry.isYou ? "rgba(34,227,232,0.08)" : "transparent",
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<div className="w-[46px] flex-shrink-0 text-right pr-1">
|
|
31
|
+
{rankIdx < 3 ? (
|
|
32
|
+
<span
|
|
33
|
+
className="inline-flex items-center justify-center w-[18px] h-[18px] rounded text-[9px] font-bold"
|
|
34
|
+
style={{ background: `${RANK_STYLES[rankIdx].accent}15`, color: RANK_STYLES[rankIdx].accent, fontFamily: "Outfit, sans-serif" }}
|
|
35
|
+
>
|
|
36
|
+
{rank}
|
|
37
|
+
</span>
|
|
38
|
+
) : (
|
|
39
|
+
<span className="text-[10px] text-white/50 font-semibold" style={{ fontFamily: "Outfit, sans-serif" }}>{rank.toLocaleString()}</span>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
<div className="flex items-center gap-1.5 flex-1 min-w-0">
|
|
43
|
+
<span
|
|
44
|
+
className={`text-[11px] truncate ${entry.isYou ? "text-[#22E3E8] font-bold" : "text-white"}`}
|
|
45
|
+
style={{ fontFamily: "Outfit, sans-serif" }}
|
|
46
|
+
>
|
|
47
|
+
{entry.wallet}
|
|
48
|
+
</span>
|
|
49
|
+
{entry.isYou && (
|
|
50
|
+
<span className="text-[7px] px-1 py-[1px] rounded bg-[#22E3E8]/20 text-[#22E3E8] font-bold uppercase flex-shrink-0">You</span>
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
<div className="w-[66px] flex-shrink-0 flex items-center justify-end gap-[3px]">
|
|
54
|
+
<Image src="/iamgame_square_logo.jpg" alt="" width={9} height={9} className="rounded-[2px]" />
|
|
55
|
+
<span className={`text-[10px] font-semibold ${entry.isYou ? "text-[#22E3E8]" : "text-white"}`}>
|
|
56
|
+
{entry.pts.toLocaleString()}
|
|
57
|
+
</span>
|
|
58
|
+
</div>
|
|
59
|
+
<div className="w-[72px] flex-shrink-0 flex items-center justify-end gap-[2px]">
|
|
60
|
+
<span className={`text-[10px] font-semibold ${entry.isYou ? "text-[#22E3E8]" : "text-green-400"}`}>
|
|
61
|
+
+${entry.payout.toFixed(1)}
|
|
62
|
+
</span>
|
|
63
|
+
<Image src="/icons/ic_usdc.png" alt="" width={12} height={12} className="rounded-full" />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// @devrongx/games — games/prematch-bets/PreMatchBetsPopup.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { useState, useCallback, useMemo } from "react";
|
|
5
|
+
import { useGamePopupStore } from "../../core/gamePopupStore";
|
|
6
|
+
import { CSK_VS_RCB_CHALLENGE, CSK_VS_RCB_POOL, IUserBets, calcBetSummary, buildPoolLeaderboard } from "./config";
|
|
7
|
+
import { GamePopupShell } from "../../core/GamePopupShell";
|
|
8
|
+
import { PreMatchIntro } from "./PreMatchIntro";
|
|
9
|
+
import { PreMatchQuestions } from "./PreMatchQuestions";
|
|
10
|
+
import { PreMatchGame } from "./PreMatchGame";
|
|
11
|
+
|
|
12
|
+
const GAME_ID = "pre-match";
|
|
13
|
+
const config = CSK_VS_RCB_CHALLENGE;
|
|
14
|
+
const pool = CSK_VS_RCB_POOL;
|
|
15
|
+
|
|
16
|
+
export const PreMatchBetsPopup = () => {
|
|
17
|
+
const { activeView } = useGamePopupStore();
|
|
18
|
+
|
|
19
|
+
// Unified bet state
|
|
20
|
+
const [bets, setBets] = useState<IUserBets>({});
|
|
21
|
+
|
|
22
|
+
// Picker state: which market has its amount picker open, and for which option
|
|
23
|
+
const [expandedPicker, setExpandedPicker] = useState<Record<number, number>>({});
|
|
24
|
+
|
|
25
|
+
// Compute summary and leaderboard from bets
|
|
26
|
+
const betSummary = useMemo(() => calcBetSummary(bets, config), [bets]);
|
|
27
|
+
const leaderboard = useMemo(
|
|
28
|
+
() => buildPoolLeaderboard(pool, betSummary.potentialBalance, config),
|
|
29
|
+
[betSummary.potentialBalance],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Questions completion callback
|
|
33
|
+
const handleQuestionsComplete = useCallback((selections: IUserBets) => {
|
|
34
|
+
setBets(selections);
|
|
35
|
+
// Open pickers for all selected markets
|
|
36
|
+
const pickers: Record<number, number> = {};
|
|
37
|
+
for (const [mIdxStr, entry] of Object.entries(selections)) {
|
|
38
|
+
pickers[Number(mIdxStr)] = entry.optionIdx;
|
|
39
|
+
}
|
|
40
|
+
setExpandedPicker(pickers);
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
// Option click: toggle selection + open/close picker
|
|
44
|
+
const handleOptionClick = useCallback((mIdx: number, oIdx: number) => {
|
|
45
|
+
setBets(prev => {
|
|
46
|
+
const existing = prev[mIdx];
|
|
47
|
+
if (existing?.optionIdx === oIdx) {
|
|
48
|
+
// Deselect
|
|
49
|
+
const next = { ...prev };
|
|
50
|
+
delete next[mIdx];
|
|
51
|
+
setExpandedPicker(p => { const n = { ...p }; delete n[mIdx]; return n; });
|
|
52
|
+
return next;
|
|
53
|
+
}
|
|
54
|
+
// Select new option — open picker
|
|
55
|
+
setExpandedPicker(p => ({ ...p, [mIdx]: oIdx }));
|
|
56
|
+
return {
|
|
57
|
+
...prev,
|
|
58
|
+
[mIdx]: { optionIdx: oIdx, amount: existing?.amount ?? 0, parlaySlot: existing?.parlaySlot ?? null },
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
}, []);
|
|
62
|
+
|
|
63
|
+
// Amount select from chip picker
|
|
64
|
+
const handleAmountSelect = useCallback((mIdx: number, oIdx: number, amount: number) => {
|
|
65
|
+
setBets(prev => ({
|
|
66
|
+
...prev,
|
|
67
|
+
[mIdx]: { optionIdx: oIdx, amount, parlaySlot: prev[mIdx]?.parlaySlot ?? null },
|
|
68
|
+
}));
|
|
69
|
+
}, []);
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<GamePopupShell
|
|
73
|
+
gameId={GAME_ID}
|
|
74
|
+
title="Pre-Match Bets"
|
|
75
|
+
hideHeaderViews={["intro"]}
|
|
76
|
+
>
|
|
77
|
+
{activeView === "intro" && (
|
|
78
|
+
<PreMatchIntro config={config} />
|
|
79
|
+
)}
|
|
80
|
+
{activeView === "questions" && (
|
|
81
|
+
<PreMatchQuestions config={config} onComplete={handleQuestionsComplete} />
|
|
82
|
+
)}
|
|
83
|
+
{activeView === "game" && (
|
|
84
|
+
<PreMatchGame
|
|
85
|
+
config={config}
|
|
86
|
+
bets={bets}
|
|
87
|
+
onBetsChange={setBets}
|
|
88
|
+
expandedPicker={expandedPicker}
|
|
89
|
+
onOptionClick={handleOptionClick}
|
|
90
|
+
onAmountSelect={handleAmountSelect}
|
|
91
|
+
betSummary={betSummary}
|
|
92
|
+
leaderboardRows={leaderboard.rows}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
</GamePopupShell>
|
|
96
|
+
);
|
|
97
|
+
};
|