@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 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
+ };