@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.
- package/README.md +75 -0
- package/package.json +27 -0
- package/src/generate-template.mjs +73 -0
- package/src/index.mjs +98 -0
- package/src/prompts.mjs +78 -0
- package/src/scaffold.mjs +129 -0
- package/src/scraper.mjs +79 -0
- package/template/.dockerignore +13 -0
- package/template/AGENTS.md +5 -0
- package/template/Dockerfile.sync +14 -0
- package/template/README.md +160 -0
- package/template/app/api/ipl/data.ts +24 -0
- package/template/app/api/ipl/route.ts +505 -0
- package/template/app/api/ipl/transfers/route.ts +261 -0
- package/template/app/api/ipl/transfers/transform.ts +156 -0
- package/template/app/api/ipl/transform.ts +20 -0
- package/template/app/api/ipl/upcoming-matches/route.ts +18 -0
- package/template/app/api/ops/status/route.ts +225 -0
- package/template/app/components/AIRoasting.tsx +278 -0
- package/template/app/components/ColorWave.tsx +193 -0
- package/template/app/components/CrownBattle.tsx +207 -0
- package/template/app/components/DashboardContent.tsx +377 -0
- package/template/app/components/FantasyStockTicker.tsx +192 -0
- package/template/app/components/FireworksBurst.tsx +225 -0
- package/template/app/components/LiveMatchTicker.tsx +117 -0
- package/template/app/components/MatchRecapScroll.tsx +135 -0
- package/template/app/components/MatchStoryScrubber.tsx +274 -0
- package/template/app/components/PerformanceTracker.tsx +132 -0
- package/template/app/components/ProgressGlowRings.tsx +157 -0
- package/template/app/components/TeamDNAScanner.tsx +238 -0
- package/template/app/components/ThemeToggle.tsx +74 -0
- package/template/app/components/dashboard/CaptainBoard.tsx +138 -0
- package/template/app/components/dashboard/ChartBoard.tsx +162 -0
- package/template/app/components/dashboard/LatestBadge.tsx +23 -0
- package/template/app/components/dashboard/LedgerTable.tsx +385 -0
- package/template/app/components/dashboard/SectionCard.tsx +59 -0
- package/template/app/components/dashboard/StickyMini.tsx +20 -0
- package/template/app/components/dashboard/index.ts +6 -0
- package/template/app/components/ui/DashboardChartFrame.tsx +74 -0
- package/template/app/components/ui/DoodleSpinner.tsx +15 -0
- package/template/app/components/ui/TeamPills.tsx +41 -0
- package/template/app/data/match-points.ts +3 -0
- package/template/app/data/teams.ts +32 -0
- package/template/app/globals.css +1267 -0
- package/template/app/hooks/dashboard/index.ts +1 -0
- package/template/app/hooks/dashboard/useDashboardModel.ts +25 -0
- package/template/app/hooks/dashboardCache.ts +53 -0
- package/template/app/hooks/dashboardPolling.ts +53 -0
- package/template/app/hooks/snapshotCache.ts +47 -0
- package/template/app/hooks/useDashboardData.ts +28 -0
- package/template/app/layout.tsx +75 -0
- package/template/app/lib/aiAgent.ts +444 -0
- package/template/app/lib/config.ts +29 -0
- package/template/app/lib/dashboard/index.ts +1 -0
- package/template/app/lib/dashboard/model.ts +257 -0
- package/template/app/lib/dashboardData.ts +50 -0
- package/template/app/lib/dashboardView.ts +22 -0
- package/template/app/lib/detailedData.ts +112 -0
- package/template/app/lib/matchStatus.ts +28 -0
- package/template/app/lib/matches.ts +131 -0
- package/template/app/lib/teamBadges.ts +223 -0
- package/template/app/lib/upcomingMatches.ts +154 -0
- package/template/app/lib/useDb.ts +29 -0
- package/template/app/lib/utils/diff.ts +24 -0
- package/template/app/lib/utils/getChartColor.ts +17 -0
- package/template/app/lib/utils/getStdDeviation.ts +6 -0
- package/template/app/lib/utils/time.ts +40 -0
- package/template/app/lib/utils.ts +70 -0
- package/template/app/page.tsx +15 -0
- package/template/app/store/dashboardStore.ts +85 -0
- package/template/app/types/dashboard.ts +75 -0
- package/template/app/types.ts +130 -0
- package/template/app/utils/dashboard/index.ts +72 -0
- package/template/eslint.config.mjs +18 -0
- package/template/infra/cloud-run/README.md +68 -0
- package/template/infra/cloud-run/sync-job.yaml +32 -0
- package/template/infra/cutover/README.md +84 -0
- package/template/infra/vercel/README.md +57 -0
- package/template/next.config.ts +7 -0
- package/template/package-lock.json +7330 -0
- package/template/package.json +47 -0
- package/template/packages/ipl-dashboard-utils/README.md +316 -0
- package/template/packages/ipl-dashboard-utils/package.json +34 -0
- package/template/packages/ipl-dashboard-utils/src/index.ts +22 -0
- package/template/packages/ipl-dashboard-utils/src/transform.ts +687 -0
- package/template/packages/ipl-dashboard-utils/src/types.ts +88 -0
- package/template/packages/ipl-dashboard-utils/tsconfig.build.json +17 -0
- package/template/postcss.config.mjs +7 -0
- package/template/scripts/capture-ipl-auth.mjs +54 -0
- package/template/scripts/deploy-cloud-run-sync.sh +48 -0
- package/template/scripts/deploy-cloud-scheduler.sh +42 -0
- package/template/scripts/dev-simple.js +31 -0
- package/template/scripts/dev-welcome.mjs +38 -0
- package/template/scripts/monitor-ops-status.sh +50 -0
- package/template/scripts/seed-mongodb.ts +115 -0
- package/template/scripts/sync-cloud.mjs +50 -0
- package/template/scripts/sync-ipl.mjs +238 -0
- package/template/scripts/sync-transfers-daily.mjs +175 -0
- package/template/scripts/verify-production.mjs +108 -0
- package/template/tests/coverage-gaps.test.ts +290 -0
- package/template/tests/dashboard-polling.test.ts +96 -0
- package/template/tests/detailed-data.test.ts +60 -0
- package/template/tests/ipl-transform.test.ts +590 -0
- package/template/tests/transfers-route.test.ts +109 -0
- package/template/tests/upcoming-matches.test.ts +34 -0
- package/template/tests/utils-and-cache.test.ts +267 -0
- package/template/tsconfig.json +35 -0
- 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
|
+
}
|