@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.
@@ -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 — play button stretches to match both lines, hidden until points are bet */}
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
- <p className="text-[9px] text-white/60 font-semibold uppercase tracking-wider mb-1" style={OUTFIT}>
648
- Combine for crazy multipliers
649
- </p>
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
- {/* Hint text + locked points carousel */}
250
+ {/* AI Insights button right-aligned, below options */}
194
251
  <motion.div
195
- initial={{ opacity: 0, y: 14 }}
196
- animate={{ opacity: 1, y: 0 }}
197
- transition={{ duration: 0.5, delay: 0.6 }}
198
- className="w-full mt-8 flex flex-col gap-2.5 -mx-5"
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
- <p className="text-[11px] text-white font-medium leading-relaxed" style={OUTFIT}>
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
  };