@vatvaghool/create-ipl-dashboard 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.
Files changed (108) hide show
  1. package/README.md +75 -0
  2. package/package.json +27 -0
  3. package/src/generate-template.mjs +73 -0
  4. package/src/index.mjs +98 -0
  5. package/src/prompts.mjs +78 -0
  6. package/src/scaffold.mjs +129 -0
  7. package/src/scraper.mjs +79 -0
  8. package/template/.dockerignore +13 -0
  9. package/template/AGENTS.md +5 -0
  10. package/template/Dockerfile.sync +14 -0
  11. package/template/README.md +160 -0
  12. package/template/app/api/ipl/data.ts +24 -0
  13. package/template/app/api/ipl/route.ts +505 -0
  14. package/template/app/api/ipl/transfers/route.ts +261 -0
  15. package/template/app/api/ipl/transfers/transform.ts +156 -0
  16. package/template/app/api/ipl/transform.ts +20 -0
  17. package/template/app/api/ipl/upcoming-matches/route.ts +18 -0
  18. package/template/app/api/ops/status/route.ts +225 -0
  19. package/template/app/components/AIRoasting.tsx +278 -0
  20. package/template/app/components/ColorWave.tsx +193 -0
  21. package/template/app/components/CrownBattle.tsx +207 -0
  22. package/template/app/components/DashboardContent.tsx +377 -0
  23. package/template/app/components/FantasyStockTicker.tsx +192 -0
  24. package/template/app/components/FireworksBurst.tsx +225 -0
  25. package/template/app/components/LiveMatchTicker.tsx +117 -0
  26. package/template/app/components/MatchRecapScroll.tsx +135 -0
  27. package/template/app/components/MatchStoryScrubber.tsx +274 -0
  28. package/template/app/components/PerformanceTracker.tsx +132 -0
  29. package/template/app/components/ProgressGlowRings.tsx +157 -0
  30. package/template/app/components/TeamDNAScanner.tsx +238 -0
  31. package/template/app/components/ThemeToggle.tsx +74 -0
  32. package/template/app/components/dashboard/CaptainBoard.tsx +138 -0
  33. package/template/app/components/dashboard/ChartBoard.tsx +162 -0
  34. package/template/app/components/dashboard/LatestBadge.tsx +23 -0
  35. package/template/app/components/dashboard/LedgerTable.tsx +385 -0
  36. package/template/app/components/dashboard/SectionCard.tsx +59 -0
  37. package/template/app/components/dashboard/StickyMini.tsx +20 -0
  38. package/template/app/components/dashboard/index.ts +6 -0
  39. package/template/app/components/ui/DashboardChartFrame.tsx +74 -0
  40. package/template/app/components/ui/DoodleSpinner.tsx +15 -0
  41. package/template/app/components/ui/TeamPills.tsx +41 -0
  42. package/template/app/data/match-points.ts +3 -0
  43. package/template/app/data/teams.ts +32 -0
  44. package/template/app/globals.css +1267 -0
  45. package/template/app/hooks/dashboard/index.ts +1 -0
  46. package/template/app/hooks/dashboard/useDashboardModel.ts +25 -0
  47. package/template/app/hooks/dashboardCache.ts +53 -0
  48. package/template/app/hooks/dashboardPolling.ts +53 -0
  49. package/template/app/hooks/snapshotCache.ts +47 -0
  50. package/template/app/hooks/useDashboardData.ts +28 -0
  51. package/template/app/layout.tsx +75 -0
  52. package/template/app/lib/aiAgent.ts +444 -0
  53. package/template/app/lib/config.ts +29 -0
  54. package/template/app/lib/dashboard/index.ts +1 -0
  55. package/template/app/lib/dashboard/model.ts +257 -0
  56. package/template/app/lib/dashboardData.ts +50 -0
  57. package/template/app/lib/dashboardView.ts +22 -0
  58. package/template/app/lib/detailedData.ts +112 -0
  59. package/template/app/lib/matchStatus.ts +28 -0
  60. package/template/app/lib/matches.ts +131 -0
  61. package/template/app/lib/teamBadges.ts +223 -0
  62. package/template/app/lib/upcomingMatches.ts +154 -0
  63. package/template/app/lib/useDb.ts +29 -0
  64. package/template/app/lib/utils/diff.ts +24 -0
  65. package/template/app/lib/utils/getChartColor.ts +17 -0
  66. package/template/app/lib/utils/getStdDeviation.ts +6 -0
  67. package/template/app/lib/utils/time.ts +40 -0
  68. package/template/app/lib/utils.ts +70 -0
  69. package/template/app/page.tsx +15 -0
  70. package/template/app/store/dashboardStore.ts +85 -0
  71. package/template/app/types/dashboard.ts +75 -0
  72. package/template/app/types.ts +130 -0
  73. package/template/app/utils/dashboard/index.ts +72 -0
  74. package/template/eslint.config.mjs +18 -0
  75. package/template/infra/cloud-run/README.md +68 -0
  76. package/template/infra/cloud-run/sync-job.yaml +32 -0
  77. package/template/infra/cutover/README.md +84 -0
  78. package/template/infra/vercel/README.md +57 -0
  79. package/template/next.config.ts +7 -0
  80. package/template/package-lock.json +7330 -0
  81. package/template/package.json +47 -0
  82. package/template/packages/ipl-dashboard-utils/README.md +316 -0
  83. package/template/packages/ipl-dashboard-utils/package.json +34 -0
  84. package/template/packages/ipl-dashboard-utils/src/index.ts +22 -0
  85. package/template/packages/ipl-dashboard-utils/src/transform.ts +687 -0
  86. package/template/packages/ipl-dashboard-utils/src/types.ts +88 -0
  87. package/template/packages/ipl-dashboard-utils/tsconfig.build.json +17 -0
  88. package/template/postcss.config.mjs +7 -0
  89. package/template/scripts/capture-ipl-auth.mjs +54 -0
  90. package/template/scripts/deploy-cloud-run-sync.sh +48 -0
  91. package/template/scripts/deploy-cloud-scheduler.sh +42 -0
  92. package/template/scripts/dev-simple.js +31 -0
  93. package/template/scripts/dev-welcome.mjs +38 -0
  94. package/template/scripts/monitor-ops-status.sh +50 -0
  95. package/template/scripts/seed-mongodb.ts +115 -0
  96. package/template/scripts/sync-cloud.mjs +50 -0
  97. package/template/scripts/sync-ipl.mjs +238 -0
  98. package/template/scripts/sync-transfers-daily.mjs +175 -0
  99. package/template/scripts/verify-production.mjs +108 -0
  100. package/template/tests/coverage-gaps.test.ts +290 -0
  101. package/template/tests/dashboard-polling.test.ts +96 -0
  102. package/template/tests/detailed-data.test.ts +60 -0
  103. package/template/tests/ipl-transform.test.ts +590 -0
  104. package/template/tests/transfers-route.test.ts +109 -0
  105. package/template/tests/upcoming-matches.test.ts +34 -0
  106. package/template/tests/utils-and-cache.test.ts +267 -0
  107. package/template/tsconfig.json +35 -0
  108. package/template/vercel.json +7 -0
@@ -0,0 +1,377 @@
1
+ "use client";
2
+
3
+ import { motion } from "framer-motion";
4
+ import {
5
+ FaArrowTrendUp,
6
+ FaBolt,
7
+ FaChartColumn,
8
+ FaClock,
9
+ FaMedal,
10
+ FaRobot,
11
+ FaTableList,
12
+ FaTrophy,
13
+ FaUserShield,
14
+ FaMicroscope,
15
+ FaStar,
16
+ FaGem,
17
+ FaWandSparkles,
18
+ } from "react-icons/fa6";
19
+
20
+ // Extracted components
21
+ import {
22
+ SectionCard,
23
+ StickyMini,
24
+ ChartBoard,
25
+ CaptainBoard,
26
+ LedgerTable,
27
+ } from "./dashboard";
28
+
29
+ // Extracted hooks
30
+ import { useDashboardModel } from "../hooks/dashboard";
31
+
32
+ // Existing components (keeping as-is for now)
33
+ import PerformanceTracker from "./PerformanceTracker";
34
+ import AIRoasting from "./AIRoasting";
35
+ import MatchRecapScroll from "./MatchRecapScroll";
36
+ import TeamPills from "./ui/TeamPills";
37
+ import { DoodleSpinner } from "./ui/DoodleSpinner";
38
+ import FantasyStockTicker from "./FantasyStockTicker";
39
+ import TeamDNAScanner from "./TeamDNAScanner";
40
+ import MatchStoryScrubber from "./MatchStoryScrubber";
41
+ import ColorWave from "./ColorWave";
42
+ import ProgressGlowRings from "./ProgressGlowRings";
43
+ import FireworksBurst from "./FireworksBurst";
44
+ import CrownBattle from "./CrownBattle";
45
+ import ThemeToggle from "./ThemeToggle";
46
+
47
+ // Utilities
48
+ import { formatDateTime } from "../utils/dashboard";
49
+ import { getChartColor } from "../lib/utils/getChartColor";
50
+
51
+ // Store
52
+ import { useDashboardStore } from "../store/dashboardStore";
53
+
54
+ export default function DashboardContent() {
55
+ const { model, loading, error, lastSyncedAt } = useDashboardModel();
56
+ const data = useDashboardStore((state) => state.data);
57
+
58
+ if (loading && !model) {
59
+ return (
60
+ <div className="dashboard-empty-state">
61
+ <div className="paper-card paper-card-compact paper-card-center">
62
+ <DoodleSpinner />
63
+ <p className="scribble-kicker mt-5">scribbling the scorebook...</p>
64
+ <p className="ledger-note mt-2">
65
+ Pulling standings, live match notes, and captain doodles.
66
+ </p>
67
+ </div>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ if (error && !model) {
73
+ return (
74
+ <div className="dashboard-empty-state">
75
+ <div className="paper-card paper-card-compact paper-card-center">
76
+ <p className="section-title">Ink Spill</p>
77
+ <p className="ledger-note mt-3">{error}</p>
78
+ <button
79
+ type="button"
80
+ onClick={() => window.location.reload()}
81
+ className="scribble-button mt-5"
82
+ >
83
+ Reload the notebook
84
+ </button>
85
+ </div>
86
+ </div>
87
+ );
88
+ }
89
+
90
+ if (!model) {
91
+ return (
92
+ <div className="dashboard-empty-state">
93
+ <div className="paper-card paper-card-compact paper-card-center">
94
+ <p className="section-title">Blank Page</p>
95
+ <p className="ledger-note mt-3">
96
+ The notebook is ready, but no season data has landed yet.
97
+ </p>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <div className="dashboard-page">
105
+ <motion.section
106
+ className="paper-hero"
107
+ initial={{ opacity: 0, y: 18, rotate: -1.2 }}
108
+ animate={{ opacity: 1, y: 0, rotate: -0.4 }}
109
+ transition={{ duration: 0.5, ease: "easeOut" }}
110
+ >
111
+ <div className="paper-hero-main">
112
+ <div className="flex items-start justify-between gap-3">
113
+ <div>
114
+ <span className="scribble-kicker">
115
+ Pinned above the study desk
116
+ </span>
117
+ <h1 className="hero-title">
118
+ IPL Fantasy
119
+ <br />
120
+ <span>Scorebook Scrapbook</span>
121
+ </h1>
122
+ </div>
123
+ <ThemeToggle />
124
+ </div>
125
+ <p className="hero-copy">
126
+ A hand-scribbled wall of live standings, match swings, captain
127
+ gambles, and season gossip — penciled in, crossed out, and loved.
128
+ </p>
129
+ <div className="hero-annotations">
130
+ <span className="scribble-pill">coffee rings included</span>
131
+ <span className="scribble-pill scribble-pill-pink">
132
+ rough edges only
133
+ </span>
134
+ <span className="crossed-label">corporate dashboard</span>
135
+ </div>
136
+ </div>
137
+
138
+ <div className="sticky-stack">
139
+ <StickyMini
140
+ title="Leader"
141
+ value={model.leader.name}
142
+ note={`${model.leader.points.toLocaleString()} pts`}
143
+ variant="yellow"
144
+ icon={FaTrophy}
145
+ />
146
+ <StickyMini
147
+ title={model.liveLabel}
148
+ value={model.liveLeader?.team ?? "Waiting"}
149
+ note={
150
+ model.liveLeader
151
+ ? `${model.liveLeader.points.toLocaleString()} pts`
152
+ : "no live row yet"
153
+ }
154
+ variant="blue"
155
+ icon={FaBolt}
156
+ />
157
+ <StickyMini
158
+ title="Updated"
159
+ value={formatDateTime(lastSyncedAt ?? "")}
160
+ note={`${model.matchCount} matches inked`}
161
+ variant="pink"
162
+ icon={FaClock}
163
+ />
164
+ </div>
165
+
166
+ <div className="hero-doodles" aria-hidden="true">
167
+ <span className="doodle-circle doodle-circle-a" />
168
+ <span className="doodle-circle doodle-circle-b" />
169
+ <span className="doodle-arrow">look here</span>
170
+ </div>
171
+ </motion.section>
172
+
173
+ <div className="dashboard-grid">
174
+ <SectionCard
175
+ title="Match Recap Scroll"
176
+ note="latest match results unrolled"
177
+ className="span-6"
178
+ index={12}
179
+ icon={FaChartColumn}
180
+ accent="var(--accent-amber)"
181
+ >
182
+ <div className="mb-3">
183
+ <TeamPills
184
+ items={model.liveBars.map((item, i) => ({
185
+ label: item.label,
186
+ fill: getChartColor(i, model.liveBars.length),
187
+ }))}
188
+ max={3}
189
+ />
190
+ </div>
191
+ <MatchRecapScroll data={data} />
192
+ </SectionCard>
193
+
194
+ <SectionCard
195
+ title="Overall Scribble Bars"
196
+ note="season points for all 8 teams"
197
+ className="span-6"
198
+ index={0}
199
+ icon={FaChartColumn}
200
+ accent="var(--accent-cyan)"
201
+ >
202
+ <ChartBoard items={model.barLeaderboard} valueLabel="pts" />
203
+ </SectionCard>
204
+
205
+ <SectionCard
206
+ title="Fantasy Stock Market"
207
+ note="live ticker - points as stock prices"
208
+ className="span-12"
209
+ index={13}
210
+ icon={FaArrowTrendUp}
211
+ accent="var(--accent-lime)"
212
+ >
213
+ <FantasyStockTicker data={data} />
214
+ </SectionCard>
215
+
216
+ <SectionCard
217
+ title="Captain Corner"
218
+ note="full-width captain board with color, role cards, and squad energy"
219
+ className="span-12"
220
+ index={2}
221
+ icon={FaUserShield}
222
+ accent="var(--accent-lime)"
223
+ >
224
+ <CaptainBoard teams={model.captainTeams} />
225
+ </SectionCard>
226
+
227
+ <SectionCard
228
+ title="Ledger Snapshot"
229
+ note="color-led table for rank shifts, boosters, efficiency, and transfers"
230
+ className="span-12"
231
+ index={3}
232
+ icon={FaTableList}
233
+ accent="var(--accent-cyan)"
234
+ >
235
+ <LedgerTable rows={model.tableRows} />
236
+ </SectionCard>
237
+
238
+ <SectionCard
239
+ title="Performance Tracker"
240
+ note="color race lines across the full season, visible again"
241
+ className="span-12"
242
+ index={4}
243
+ icon={FaArrowTrendUp}
244
+ accent="var(--accent-blue)"
245
+ >
246
+ <PerformanceTracker />
247
+ </SectionCard>
248
+
249
+ <SectionCard
250
+ title="Margin Notes"
251
+ note="little facts circled in neon marker"
252
+ className="span-12"
253
+ index={6}
254
+ icon={FaMedal}
255
+ accent="var(--accent-magenta)"
256
+ >
257
+ <div className="gap-to-next-list-compact">
258
+ {model.normalizedOverall.map((team, idx) => {
259
+ const gap = team.gapToNext ?? 0;
260
+ const maxGap = Math.max(
261
+ ...model.normalizedOverall.map((t) => t.gapToNext ?? 0),
262
+ );
263
+ const barWidth = maxGap > 0 ? (gap / maxGap) * 100 : 0;
264
+ const color = getChartColor(idx, model.normalizedOverall.length);
265
+ return (
266
+ <div key={team.name} className="gap-item-single-line">
267
+ <div className="gap-badge-mini" style={{ background: color }}>
268
+ #{team.rank}
269
+ </div>
270
+ <div className="gap-team-label">{team.name}</div>
271
+ <div className="gap-bar-inline">
272
+ <div
273
+ className="gap-bar-inline-track"
274
+ style={{
275
+ background: `color-mix(in srgb, ${color} 12%, var(--panel-strong))`,
276
+ border: `1px dashed color-mix(in srgb, ${color} 32%, var(--line))`,
277
+ }}
278
+ >
279
+ <div
280
+ className="gap-bar-inline-fill"
281
+ style={{
282
+ width: `${barWidth}%`,
283
+ background: `linear-gradient(90deg, ${color} 0%, color-mix(in srgb, ${color} 70%, var(--panel-strong) 30%))`,
284
+ boxShadow: `0 1px 3px color-mix(in srgb, ${color} 40%, transparent)`,
285
+ }}
286
+ />
287
+ </div>
288
+ </div>
289
+ <div className="gap-value-mini font-black">
290
+ {gap.toLocaleString()}
291
+ </div>
292
+ </div>
293
+ );
294
+ })}
295
+ </div>
296
+ </SectionCard>
297
+
298
+ <SectionCard
299
+ title="Team DNA Scanner"
300
+ note="genetic breakdown of every squad"
301
+ className="span-12"
302
+ index={15}
303
+ icon={FaMicroscope}
304
+ accent="var(--accent-blue)"
305
+ >
306
+ <TeamDNAScanner data={data} />
307
+ </SectionCard>
308
+
309
+ <SectionCard
310
+ title="Match Story Scrubber"
311
+ note="drag across the season timeline"
312
+ className="span-12"
313
+ index={18}
314
+ icon={FaClock}
315
+ accent="var(--accent-blue)"
316
+ >
317
+ <MatchStoryScrubber data={data} />
318
+ </SectionCard>
319
+
320
+ <SectionCard
321
+ title="Points Color Wave"
322
+ note="flowing waveform of every team"
323
+ className="span-12"
324
+ index={20}
325
+ icon={FaArrowTrendUp}
326
+ accent="var(--accent-cyan)"
327
+ >
328
+ <ColorWave data={data} />
329
+ </SectionCard>
330
+
331
+ <SectionCard
332
+ title="Progress Glow Rings"
333
+ note="colorful rings — % distance to leader"
334
+ className="span-12"
335
+ index={22}
336
+ icon={FaGem}
337
+ accent="var(--accent-amber)"
338
+ >
339
+ <ProgressGlowRings data={data} />
340
+ </SectionCard>
341
+
342
+ <SectionCard
343
+ title="Fireworks Burst"
344
+ note="click a team to celebrate"
345
+ className="span-6"
346
+ index={23}
347
+ icon={FaWandSparkles}
348
+ accent="var(--accent-magenta)"
349
+ >
350
+ <FireworksBurst data={data} />
351
+ </SectionCard>
352
+
353
+ <SectionCard
354
+ title="Crown Battle"
355
+ note="top 3 battle for the throne"
356
+ className="span-6"
357
+ index={24}
358
+ icon={FaStar}
359
+ accent="var(--accent-amber)"
360
+ >
361
+ <CrownBattle data={data} />
362
+ </SectionCard>
363
+
364
+ <SectionCard
365
+ title="AI Roast Corner"
366
+ note="sarcastic commentary from our digital drama queen"
367
+ className="span-12"
368
+ index={7}
369
+ icon={FaRobot}
370
+ accent="var(--accent-magenta)"
371
+ >
372
+ <AIRoasting data={data} />
373
+ </SectionCard>
374
+ </div>
375
+ </div>
376
+ );
377
+ }
@@ -0,0 +1,192 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import { FaArrowTrendUp, FaArrowTrendDown, FaMinus } from "react-icons/fa6";
5
+ import { motion } from "framer-motion";
6
+ import { Line, LineChart, ResponsiveContainer } from "recharts";
7
+ import type { DashboardData } from "../types";
8
+ import { getPlayedMatchRows } from "../lib/dashboardData";
9
+
10
+ export default function FantasyStockTicker({
11
+ data,
12
+ }: {
13
+ data: DashboardData | null;
14
+ }) {
15
+ const stocks = useMemo(() => {
16
+ if (!data?.overall?.length) return [];
17
+
18
+ const daily = getPlayedMatchRows(data.daily);
19
+
20
+ return data.overall
21
+ .sort((a, b) => a.rank - b.rank)
22
+ .slice(0, 8)
23
+ .map((team) => {
24
+ const scores = daily
25
+ .map((r) => Number(r[team.name] ?? 0))
26
+ .filter((v) => v > 0);
27
+ const last = team.lastMatchPoints ?? scores.at(-1) ?? 0;
28
+ const prev = scores.length > 1 ? (scores.at(-2) ?? 0) : 0;
29
+ const change = last - prev;
30
+ const changePct = prev > 0 ? ((change / prev) * 100).toFixed(1) : "0.0";
31
+
32
+ const sparkData = scores.slice(-10).map((s, i) => ({ v: s, i }));
33
+
34
+ return {
35
+ symbol: team.name
36
+ .split(" ")
37
+ .map((w) => w[0])
38
+ .join("")
39
+ .toUpperCase()
40
+ .slice(0, 5),
41
+ name: team.name,
42
+ price: team.points,
43
+ change,
44
+ changePct,
45
+ rank: team.rank,
46
+ sparkData,
47
+ prevPoints: prev,
48
+ };
49
+ });
50
+ }, [data]);
51
+
52
+ if (!stocks.length) {
53
+ return (
54
+ <div className="ledger-note py-6 text-center">No market data yet...</div>
55
+ );
56
+ }
57
+
58
+ const tickerItems = [...stocks, ...stocks, ...stocks];
59
+
60
+ return (
61
+ <div
62
+ className="rounded-2xl border-2 p-4 wobbly overflow-hidden"
63
+ style={{
64
+ borderColor: "color-mix(in srgb, var(--marker-green) 34%, var(--line))",
65
+ background:
66
+ "linear-gradient(180deg, color-mix(in srgb, var(--paper) 90%, var(--paper-3) 10%), color-mix(in srgb, var(--paper) 92%, var(--paper-2) 8%))",
67
+ }}
68
+ >
69
+ <div className="mb-3 flex items-center gap-2 border-b border-dashed border-(--border-wobbly)/40 pb-2">
70
+ <span
71
+ className="text-sm font-bold"
72
+ style={{ color: "var(--marker-green)" }}
73
+ >
74
+
75
+ </span>
76
+ <span className="font-bold text-sm text-(--ink)">
77
+ Fantasy Stock Market
78
+ </span>
79
+ <span className="text-[8px] text-(--ink-faint) ml-auto">
80
+ live ticker
81
+ </span>
82
+ </div>
83
+
84
+ <div className="ticker-window">
85
+ <motion.div
86
+ className="ticker-track"
87
+ animate={{ x: ["0%", "-50%"] }}
88
+ transition={{ repeat: Infinity, duration: 48, ease: "linear" }}
89
+ style={{ display: "flex", gap: 12, paddingRight: 12 }}
90
+ >
91
+ {tickerItems.map((stock, index) => {
92
+ const isUp = stock.change > 0;
93
+ const isDown = stock.change < 0;
94
+ return (
95
+ <div
96
+ key={`${stock.symbol}-${index}`}
97
+ className="flex shrink-0 items-center gap-3 rounded-xl border-2 p-2.5"
98
+ style={{
99
+ width: 280,
100
+ borderColor: isUp
101
+ ? "color-mix(in srgb, var(--marker-green) 34%, var(--line))"
102
+ : isDown
103
+ ? "color-mix(in srgb, var(--marker-pink) 34%, var(--line))"
104
+ : "color-mix(in srgb, var(--marker-blue) 24%, var(--line))",
105
+ background: isUp
106
+ ? "color-mix(in srgb, var(--marker-green) 6%, var(--paper) 94%)"
107
+ : isDown
108
+ ? "color-mix(in srgb, var(--marker-pink) 6%, var(--paper) 94%)"
109
+ : "color-mix(in srgb, var(--paper) 96%, var(--panel) 4%)",
110
+ }}
111
+ >
112
+ <div className="flex min-w-0 flex-1 flex-col">
113
+ <div className="flex items-center gap-1.5">
114
+ <span className="text-[9px] font-black uppercase tracking-wider text-(--ink-faint)">
115
+ {stock.symbol}
116
+ </span>
117
+ <span
118
+ className="rounded px-1 py-0.5 text-[8px] font-bold"
119
+ style={{
120
+ background:
121
+ "color-mix(in srgb, var(--panel) 80%, transparent)",
122
+ color: "var(--ink-soft)",
123
+ }}
124
+ >
125
+ #{stock.rank}
126
+ </span>
127
+ </div>
128
+ <span className="truncate text-[13px] font-bold text-(--ink) leading-tight">
129
+ {stock.name}
130
+ </span>
131
+ <div className="flex items-center gap-2 mt-0.5">
132
+ <span className="text-sm font-black text-(--ink)">
133
+ {stock.price.toLocaleString()}
134
+ </span>
135
+ <span
136
+ className={`inline-flex items-center gap-0.5 text-[9px] font-bold ${
137
+ isUp
138
+ ? "text-(--marker-green)"
139
+ : isDown
140
+ ? "text-(--marker-pink)"
141
+ : "text-(--ink-faint)"
142
+ }`}
143
+ >
144
+ {isUp ? (
145
+ <FaArrowTrendUp className="text-[8px]" />
146
+ ) : isDown ? (
147
+ <FaArrowTrendDown className="text-[8px]" />
148
+ ) : (
149
+ <FaMinus className="text-[8px]" />
150
+ )}
151
+ <span>
152
+ {isUp ? "+" : ""}
153
+ {stock.change.toLocaleString()}
154
+ </span>
155
+ <span className="text-[7px] opacity-70">
156
+ ({isUp ? "+" : ""}
157
+ {stock.changePct}%)
158
+ </span>
159
+ </span>
160
+ </div>
161
+ </div>
162
+
163
+ {stock.sparkData.length > 1 && (
164
+ <div className="h-9 w-16 shrink-0">
165
+ <ResponsiveContainer width="100%" height="100%">
166
+ <LineChart data={stock.sparkData}>
167
+ <Line
168
+ type="natural"
169
+ dataKey="v"
170
+ stroke={
171
+ isUp
172
+ ? "var(--marker-green)"
173
+ : isDown
174
+ ? "var(--marker-pink)"
175
+ : "var(--marker-blue)"
176
+ }
177
+ strokeWidth={2}
178
+ dot={false}
179
+ isAnimationActive={false}
180
+ />
181
+ </LineChart>
182
+ </ResponsiveContainer>
183
+ </div>
184
+ )}
185
+ </div>
186
+ );
187
+ })}
188
+ </motion.div>
189
+ </div>
190
+ </div>
191
+ );
192
+ }