@vatvaghool/create-ipl-dashboard 0.1.18 → 0.1.23

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 (36) hide show
  1. package/README.md +38 -129
  2. package/package.json +2 -3
  3. package/src/index.mjs +18 -22
  4. package/src/prompts.mjs +20 -112
  5. package/src/scaffold.mjs +23 -35
  6. package/template/README.md +23 -200
  7. package/template/app/api/ops/seed/route.ts +76 -0
  8. package/template/app/api/ops/status/route.ts +0 -2
  9. package/template/app/lib/config.ts +0 -4
  10. package/template/app/lib/matchStatus.ts +0 -12
  11. package/template/app/lib/storage/index.ts +1 -10
  12. package/template/package.json +1 -6
  13. package/template/scripts/dev-simple.js +1 -1
  14. package/template/scripts/dev-welcome.mjs +39 -18
  15. package/template/scripts/seed-league.mjs +0 -55
  16. package/screenshots/ai-roasting.png +0 -0
  17. package/screenshots/captain-board.png +0 -0
  18. package/screenshots/dashboard-overview.png +0 -0
  19. package/screenshots/ledger-table.png +0 -0
  20. package/screenshots/match-scrubber.png +0 -0
  21. package/screenshots/performance-tracker.png +0 -0
  22. package/src/scraper.mjs +0 -79
  23. package/template/app/hooks/dashboardPolling.ts +0 -53
  24. package/template/app/hooks/snapshotCache.ts +0 -47
  25. package/template/app/lib/storage/google-sheets-storage.ts +0 -147
  26. package/template/app/lib/utils/diff.ts +0 -24
  27. package/template/app/lib/utils/time.ts +0 -40
  28. package/template/screenshots/ai-roasting.png +0 -0
  29. package/template/screenshots/captain-board.png +0 -0
  30. package/template/screenshots/dashboard-overview.png +0 -0
  31. package/template/screenshots/ledger-table.png +0 -0
  32. package/template/screenshots/match-scrubber.png +0 -0
  33. package/template/screenshots/performance-tracker.png +0 -0
  34. package/template/tests/coverage-gaps.test.ts +0 -290
  35. package/template/tests/dashboard-polling.test.ts +0 -96
  36. package/template/tests/utils-and-cache.test.ts +0 -267
@@ -1,234 +1,57 @@
1
1
  # IPL Fantasy Cricket Dashboard
2
2
 
3
- Private Next.js dashboard for an IPL fantasy league. Combines seeded match history with live leaderboard snapshots to show standings, match-by-match movement, captain picks, cumulative trends, and league insights in one place.
4
-
5
3
  ## Quick Start
6
4
 
7
5
  ```bash
8
- # Scaffold a new project
9
6
  npx @vatvaghool/create-ipl-dashboard my-league
10
-
11
- # Or from source:
12
- npm install
13
- ```
14
-
15
- Start the dev server:
16
-
17
- ```bash
18
- npm run dev:simple
7
+ cd my-league
8
+ # Edit .env — set MONGODB_URI, IPL_LEAGUE_URL and IPL_POST_SECRET
9
+ npm run seed:api # Seed match data into MongoDB
10
+ npm run seed:league # Seed league metadata
11
+ npm run capture:ipl-auth # One-time Playwright login
12
+ npm run sync:ipl:watch # Start live sync
19
13
  ```
20
14
 
21
15
  Open http://localhost:3000
22
16
 
23
- ### Available commands
17
+ ## Commands
24
18
 
25
19
  | Command | Description |
26
20
  |---------|-------------|
27
- | `npm run dev` | Dev server with welcome splash |
28
- | `npm run dev:simple` | Dev server (simple, no splash) |
21
+ | `npm run dev:simple` | Start dev server |
29
22
  | `npm run build` | Production build |
30
- | `npm start` | Production server |
31
23
  | `npm run capture:ipl-auth` | Capture Playwright login state (one-time) |
32
24
  | `npm run sync:ipl` | Scrape live leaderboard snapshot |
33
- | `npm run sync:ipl:watch` | Scrape leaderboard in watch mode (polls every 2 min) |
34
- | `npm run sync:ipl:transfers-daily` | Scrape transfer/booster data |
35
- | `npm run sync:cloud` | Run both leaderboard + transfer sync (for cloud jobs) |
36
- | `npm run seed:league` | Seed league metadata into storage |
37
- | `npm run seed:mongodb` | Seed initial raw user data from `data.ts` |
38
- | `npm run seed:mongodb:reset` | Reset and re-seed MongoDB data |
39
- | `npm run verify:production` | Verify production setup |
40
- | `npm run monitor:ops` | Check ops health status |
41
- | `npm run test` | Run test suite |
25
+ | `npm run sync:ipl:watch` | Scrape leaderboard in watch mode |
26
+ | `npm run sync:cloud` | Run all scrapers |
27
+ | `npm run seed:api` | Seed match data into MongoDB |
28
+ | `npm run seed:league` | Seed league metadata |
29
+ | `npm run test` | Run tests |
42
30
  | `npm run lint` | Run linter |
43
31
 
44
- ---
45
-
46
- ## Screenshots
47
-
48
- ![](https://files.catbox.moe/a83xlk.png)
49
-
50
- *Dashboard overview — full page*
51
-
52
- ![](https://files.catbox.moe/hl0kxd.png)
53
-
54
- *Performance Tracker — Recharts line chart*
55
-
56
- ![](https://files.catbox.moe/k4sw53.png)
57
-
58
- *Captain Board — captain/vice-captain picks*
59
-
60
- ![](https://files.catbox.moe/vt5i8w.png)
61
-
62
- *Ledger Table — standings with rank shifts and efficiency*
63
-
64
- ![](https://files.catbox.moe/tuvkjo.png)
65
-
66
- *Match Scrubber — interactive timeline scrubber*
67
-
68
- ![](https://files.catbox.moe/5nfi5i.png)
69
-
70
- *AI Roast Corner — generated commentary*
71
-
72
- ---
73
-
74
- ## Components
75
-
76
- ### Dashboard Sections
77
-
78
- | Component | File | Description |
79
- |-----------|------|-------------|
80
- | **StickyMini** | `components/dashboard/StickyMini.tsx` | Compact stat card with title, value, note, and color-coded icon |
81
- | **SectionCard** | `components/dashboard/SectionCard.tsx` | Animated motion-section wrapper with title, note, accent color, and staggered entrance |
82
- | **ChartBoard** | `components/dashboard/ChartBoard.tsx` | Recharts bar chart for team data with pill tags and tooltips |
83
- | **CaptainBoard** | `components/dashboard/CaptainBoard.tsx` | All playing players with points as TeamPills badges, C/VC highlighted |
84
- | **LedgerTable** | `components/dashboard/LedgerTable.tsx` | Full standings table with colors, boosters, rank arrows, and formatted points |
85
- | **LatestBadge** | `components/dashboard/LatestBadge.tsx` | Color-coded pill badge for points (pink <200, orange 200-400, green >400) |
86
-
87
- ### Widgets
88
-
89
- | Component | File | Description |
90
- |-----------|------|-------------|
91
- | **PerformanceTracker** | `components/PerformanceTracker.tsx` | Recharts line chart tracking each team's point trajectory across match days |
92
- | **CrownBattle** | `components/CrownBattle.tsx` | Head-to-head showdown between #1 and #2 ranked teams |
93
- | **AIRoasting** | `components/AIRoasting.tsx` | AI-generated roast commentary cards cycling through hot/cold/lightning/trend types |
94
- | **FantasyStockTicker** | `components/FantasyStockTicker.tsx` | Team stocks with trend arrows, sparklines, and fluctuating values |
95
- | **TeamDNAScanner** | `components/TeamDNAScanner.tsx` | Sci-fi DNA scan visualization with moving scan-line and std deviation metrics |
96
- | **MatchRecapScroll** | `components/MatchRecapScroll.tsx` | Scroll-styled recap panel for the latest match day results |
97
- | **MatchStoryScrubber** | `components/MatchStoryScrubber.tsx` | Interactive timeline scrubber to step through match history with bar charts |
98
- | **LiveMatchTicker** | `components/LiveMatchTicker.tsx` | Live-scrolling ticker of upcoming matches with countdowns |
99
- | **ProgressGlowRings** | `components/ProgressGlowRings.tsx` | Animated SVG glowing ring progress indicators per team |
100
- | **ColorWave** | `components/ColorWave.tsx` | Animated SVG wave/chart visualizing team scores over time |
101
- | **FireworksBurst** | `components/FireworksBurst.tsx` | Particle-based fireworks celebration on milestones |
102
- | **ThemeToggle** | `components/ThemeToggle.tsx` | Dark/light mode toggle persisted to localStorage |
103
-
104
- ### UI Primitives
105
-
106
- | Component | File | Description |
107
- |-----------|------|-------------|
108
- | **TeamPills** | `components/ui/TeamPills.tsx` | Flex-wrap row of bordered pill/chip team labels |
109
- | **DashboardChartFrame** | `components/ui/DashboardChartFrame.tsx` | Responsive chart wrapper with hover-jitter effects |
110
- | **DoodleSpinner** | `components/ui/DoodleSpinner.tsx` | Hand-drawn SVG loading spinner with dotted circles |
111
-
112
- ---
113
-
114
- ## API Endpoints
32
+ ## API
115
33
 
116
34
  | Route | Method | Description |
117
35
  |-------|--------|-------------|
118
- | `/api/ipl` | GET | Full dashboard payload used by the UI |
119
- | `/api/ipl?format=snapshot` | GET | Raw normalized snapshot only |
120
- | `/api/ipl` | POST | Ingest a leaderboard snapshot |
121
- | `/api/ipl/transfers` | GET | Transfer/booster snapshot for all teams (fetched once on mount, not on poll) |
122
- | `/api/ipl/transfers` | POST | Ingest a single team transfer record |
36
+ | `/api/ipl` | GET | Dashboard payload |
37
+ | `/api/ipl` | POST | Ingest leaderboard snapshot |
38
+ | `/api/ipl/transfers` | GET/POST | Transfer/booster data |
123
39
  | `/api/ipl/upcoming-matches` | GET | Upcoming match schedule |
124
- | `/api/ops/status` | GET | Health/monitoring endpoint |
125
-
126
- ---
127
-
128
- ## Data Flow
129
-
130
- ```
131
- GET /api/ipl resolution order:
132
- 1. Storage raw users (if configured — MongoDB or Google Sheets)
133
- 2. Fallback: local seed data (app/api/ipl/data.ts)
134
- 3. Storage live snapshot (if configured)
135
- 4. Fallback: local snapshot file (app/api/ipl/live-snapshot.json)
136
- ```
137
-
138
- ---
139
-
140
- ## Configuration
141
-
142
- All hardcoded values are centralized in `app/lib/config.ts` and overridable via environment variables:
143
-
144
- | Variable | Default | Description |
145
- |----------|---------|-------------|
146
- | `STORAGE_BACKEND` | `mongodb` | Storage backend: `mongodb` or `google_sheets` |
147
- | `MONGODB_URI` | - | MongoDB connection string (for MongoDB backend) |
148
- | `GOOGLE_SHEET_ID` | - | Google Sheet ID (for Google Sheets backend) |
149
- | `GOOGLE_SERVICE_ACCOUNT_EMAIL` | - | Google service account email |
150
- | `GOOGLE_PRIVATE_KEY` | - | Google service account private key |
151
- | `LEAGUE_NAME` | - | League name (collection/sheet name) |
152
- | `IPL_LEAGUE_URL` | fantasy.iplt20.com/... | Fantasy league page URL |
153
- | `IPL_POST_SECRET` | - | Bearer token for POST endpoints |
154
- | `IPL_DB_NAME` | `ipl` | MongoDB database name (MongoDB only) |
155
- | `IPL_COLLECTION_NAME` | `ipl` | MongoDB collection name (MongoDB only) |
156
- | `IPL_TOTAL_TRANSFERS` | `160` | Total transfers per season |
157
- | `IPL_SYNC_INTERVAL_MS` | `120000` | Dashboard polling interval |
158
- | `IPL_DASHBOARD_STALE_MINUTES` | `20` (prod) / `180` (dev) | Staleness threshold |
159
- | `IPL_TRANSFERS_STALE_MINUTES` | `720` (prod) / `1440` (dev) | Transfer staleness threshold |
160
-
161
- ---
162
-
163
- ## Automation
164
-
165
- See the [commands table](#available-commands) above for all options.
166
-
167
- Also supports a browser bookmarklet — visit `/api/ipl/bookmarklet` while the app is running, copy the returned `javascript:` code, and save it as a bookmark. Click it on the fantasy leaderboard page to sync data.
168
-
169
- ---
40
+ | `/api/ops/status` | GET | Health check |
41
+ | `/api/ops/seed` | POST | Seed initial match data into MongoDB |
170
42
 
171
43
  ## Deployment
172
44
 
173
- ### Deploy to Vercel (recommended)
174
-
175
45
  ```bash
176
- # Build the app
177
46
  npm run build
178
-
179
- # Deploy to Vercel
180
47
  npx vercel --prod
181
48
  ```
182
49
 
183
- Set these environment variables in your Vercel project dashboard:
184
-
185
- | Variable | Required | Description |
186
- |----------|----------|-------------|
187
- | `STORAGE_BACKEND` | Yes | `mongodb` or `google_sheets` |
188
- | `MONGODB_URI` | Yes* | MongoDB connection string (required if `mongodb`) |
189
- | `GOOGLE_SHEET_ID` | Yes* | Google Sheet ID (required if `google_sheets`) |
190
- | `GOOGLE_SERVICE_ACCOUNT_EMAIL` | Yes* | Service account email (required if `google_sheets`) |
191
- | `GOOGLE_PRIVATE_KEY` | Yes* | Private key (required if `google_sheets`) |
192
- | `LEAGUE_NAME` | Yes | Collection or sheet name |
193
- | `IPL_POST_SECRET` | Yes | Bearer token protecting write endpoints |
194
- | `IPL_LEAGUE_URL` | Yes | Your fantasy league page URL |
195
- | `IPL_DASHBOARD_STALE_MINUTES` | No | Staleness threshold for health checks (default: `20`) |
196
- | `IPL_TRANSFERS_STALE_MINUTES` | No | Transfer staleness threshold (default: `720`) |
197
-
198
- After deployment:
199
-
200
- 1. Verify the app is healthy — visit `https://your-app.vercel.app/api/ops/status`
201
- 2. Run the Playwright scraper (`npm run sync:cloud`) pointed at your production URL to populate live data
202
- 3. Optionally deploy the scraper as a Cloud Run Job with `Dockerfile.sync` for automated sync every 5-10 minutes
203
-
204
- ### Without Vercel
205
-
206
- ```bash
207
- # Build
208
- npm run build
209
-
210
- # Start production server
211
- npm start
212
- ```
213
-
214
- The app runs as a standard Node.js server on `process.env.PORT` (default `3000`).
215
-
216
- ### Production requirements
217
-
218
- - **Storage** (MongoDB or Google Sheets) is required in production for POST endpoints (returns `503` without it)
219
- - **IPL_POST_SECRET** should be a strong random value set in your hosting environment
220
- - The scraper should run outside the web app (Cloud Run, GitHub Actions, cron) — never in the same process
221
-
222
- ---
50
+ Set `MONGODB_URI`, `COLLECTION_NAME`, `IPL_LEAGUE_URL`, and `IPL_POST_SECRET` in your Vercel dashboard.
223
51
 
224
52
  ## Stack
225
53
 
226
- - **Next.js** 16 App Router
227
- - **React** 19
228
- - **TypeScript**
229
- - **Tailwind CSS** 4
230
- - **Recharts** — charts
231
- - **Framer Motion** — animations
232
- - **MongoDB** or **Google Sheets** — data persistence
233
- - **Zustand** — client state
54
+ - **Next.js** 16 App Router, **React** 19, **TypeScript**, **Tailwind CSS** 4
55
+ - **Recharts**, **Framer Motion**, **Zustand**
56
+ - **MongoDB** — data persistence
234
57
  - **Playwright** — scraper automation
@@ -0,0 +1,76 @@
1
+ import { NextResponse } from "next/server";
2
+ import { getStorage } from "../../../lib/storage";
3
+ import { config } from "../../../lib/config";
4
+ import { rawApiUsers } from "../../ipl/data";
5
+ import { normalizePayload } from "../../ipl/transform";
6
+ import { MATCH_POINTS } from "../../../data/match-points";
7
+ import { leagueInfo } from "../../../data/league";
8
+
9
+ export const dynamic = "force-dynamic";
10
+ export const runtime = "nodejs";
11
+
12
+ const COLLECTION = config.mongodb.collectionName;
13
+
14
+ const storage = getStorage();
15
+
16
+ export async function POST() {
17
+ if (!storage.isConfigured()) {
18
+ return NextResponse.json({ ok: false, error: "Storage not configured" }, { status: 400 });
19
+ }
20
+
21
+ const connected = await storage.isConnected();
22
+ if (!connected) {
23
+ return NextResponse.json({ ok: false, error: "Cannot connect to storage" }, { status: 503 });
24
+ }
25
+
26
+ const results: Record<string, string> = {};
27
+
28
+ try {
29
+ await storage.upsertDocument("league", {
30
+ type: "league",
31
+ name: leagueInfo.name,
32
+ leagueUrl: leagueInfo.leagueUrl,
33
+ teams: leagueInfo.teams,
34
+ createdAt: new Date().toISOString(),
35
+ updatedAt: new Date().toISOString(),
36
+ });
37
+ results.league = "seeded";
38
+ } catch (e: any) {
39
+ results.league = `failed: ${e.message}`;
40
+ }
41
+
42
+ try {
43
+ await storage.upsertDocument("raw-users", {
44
+ type: "raw-users",
45
+ users: rawApiUsers,
46
+ updatedAt: new Date().toISOString(),
47
+ });
48
+ results.rawUsers = "seeded";
49
+ } catch (e: any) {
50
+ results.rawUsers = `failed: ${e.message}`;
51
+ }
52
+
53
+ try {
54
+ const snapshot = normalizePayload({
55
+ type: "dashboard",
56
+ updatedAt: new Date().toISOString(),
57
+ completedMatches: Math.max(...MATCH_POINTS.map((m) => m.matchId), 0),
58
+ leaders: rawApiUsers
59
+ .sort((a, b) => b.points - a.points)
60
+ .map((u, i) => ({
61
+ rank: i + 1,
62
+ teamId: u.rno,
63
+ teamName: u.temname,
64
+ points: u.points,
65
+ })),
66
+ });
67
+ await storage.upsertDocument("dashboard", snapshot);
68
+ results.dashboard = "seeded";
69
+ } catch (e: any) {
70
+ results.dashboard = `failed: ${e.message}`;
71
+ }
72
+
73
+ const allOk = Object.values(results).every((v) => v === "seeded");
74
+
75
+ return NextResponse.json({ ok: allOk, results });
76
+ }
@@ -77,7 +77,6 @@ export async function GET() {
77
77
  mode: isProduction ? "cloud" : "local",
78
78
  mongoConfigured: storageConfigured,
79
79
  mongoConnected: false,
80
- storageBackend: process.env.STORAGE_BACKEND || "mongodb",
81
80
  reason: storageConfigured
82
81
  ? "Storage connection failed."
83
82
  : "Storage is not configured.",
@@ -157,7 +156,6 @@ export async function GET() {
157
156
  checkedAt: new Date().toISOString(),
158
157
  mongoConfigured: storageConfigured,
159
158
  mongoConnected: storageConnected,
160
- storageBackend: process.env.STORAGE_BACKEND || "mongodb",
161
159
  localSnapshotFallbackEnabled: !isProduction,
162
160
  thresholds: {
163
161
  dashboardStaleMinutes,
@@ -1,10 +1,6 @@
1
1
  import path from "node:path";
2
2
 
3
3
  export const config = {
4
- storage: {
5
- backend: process.env.STORAGE_BACKEND || "mongodb",
6
- },
7
-
8
4
  mongodb: {
9
5
  databaseName: process.env.IPL_DB_NAME || "ipl",
10
6
  collectionName: process.env.IPL_COLLECTION_NAME || "ipl",
@@ -1,5 +1,3 @@
1
- import { matches } from "./matches";
2
-
3
1
  export type MatchStatus = "ENDED" | "LIVE" | "SOON" | "UPCOMING";
4
2
 
5
3
  export function getMatchStatus(date: string) {
@@ -15,14 +13,4 @@ export function getMatchStatus(date: string) {
15
13
  return "UPCOMING" as const;
16
14
  }
17
15
 
18
- export function getCurrentMatchStatus() {
19
- const current = matches
20
- .map((match) => ({
21
- ...match,
22
- status: getMatchStatus(match.date),
23
- }))
24
- .filter((match) => match.status !== "ENDED")
25
- .sort((a, b) => +new Date(a.date) - +new Date(b.date))[0];
26
16
 
27
- return current?.status ?? "ENDED";
28
- }
@@ -1,19 +1,10 @@
1
1
  import type { IStorage } from "./types";
2
2
  import { MongoStorage } from "./mongo-storage";
3
- import { GoogleSheetsStorage } from "./google-sheets-storage";
4
3
 
5
4
  let _instance: IStorage | null = null;
6
5
 
7
- function detectBackend(): "mongodb" | "google_sheets" {
8
- if (process.env.STORAGE_BACKEND === "google_sheets") return "google_sheets";
9
- if (process.env.GOOGLE_SHEET_ID && process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL && process.env.GOOGLE_PRIVATE_KEY) return "google_sheets";
10
- return "mongodb";
11
- }
12
-
13
6
  export function getStorage(): IStorage {
14
- if (_instance) return _instance;
15
- const backend = detectBackend();
16
- _instance = backend === "google_sheets" ? new GoogleSheetsStorage() : new MongoStorage();
7
+ if (!_instance) _instance = new MongoStorage();
17
8
  return _instance;
18
9
  }
19
10
 
@@ -9,17 +9,12 @@
9
9
  "capture:ipl-auth": "node scripts/capture-ipl-auth.mjs",
10
10
  "sync:ipl": "node scripts/sync-ipl.mjs",
11
11
  "sync:ipl:watch": "node scripts/sync-ipl.mjs --watch",
12
- "sync:ipl:transfers-daily": "node scripts/sync-transfers-daily.mjs",
13
12
  "sync:cloud": "node scripts/sync-cloud.mjs",
14
- "verify:production": "node scripts/verify-production.mjs",
15
- "monitor:ops": "bash scripts/monitor-ops-status.sh",
16
13
  "build": "next build",
17
- "build:utils-package": "node ./node_modules/typescript/bin/tsc -p packages/ipl-dashboard-utils/tsconfig.build.json",
18
14
  "start": "next start",
19
15
  "lint": "eslint",
20
16
  "test": "node --test --experimental-strip-types \"tests/**/*.test.ts\"",
21
- "seed:mongodb": "node --experimental-strip-types scripts/seed-mongodb.ts",
22
- "seed:mongodb:reset": "node --experimental-strip-types scripts/seed-mongodb.ts --reset --force"
17
+ "seed:api": "node --experimental-strip-types -e \"fetch('http://localhost:3000/api/ops/seed',{method:'POST'}).then(r=>r.json()).then(console.log).catch(console.error)\""
23
18
  },
24
19
  "dependencies": {
25
20
  "@vercel/analytics": "^2.0.1",
@@ -7,7 +7,7 @@ const seedMongoIfConfigured = () => {
7
7
  }
8
8
 
9
9
  console.log("MONGODB_URI detected. Running seed once (best effort)...");
10
- const result = spawnSync("npm", ["run", "seed:mongodb"], {
10
+ const result = spawnSync("npm", ["run", "seed:api"], {
11
11
  stdio: "inherit",
12
12
  env: process.env,
13
13
  });
@@ -6,33 +6,54 @@ const amber = "\x1b[33m";
6
6
  const magenta = "\x1b[35m";
7
7
  const lime = "\x1b[32m";
8
8
 
9
- const cmds = [
10
- { cmd: "capture:ipl-auth", desc: "Log in to fantasy.iplt20.com and save auth state" },
11
- { cmd: "sync:ipl", desc: "Scrape live leaderboard + squad data (one-shot)" },
12
- { cmd: "sync:ipl:transfers-daily", desc: "Scrape transfer/booster data for all teams" },
13
- { cmd: "sync:ipl:watch", desc: "Watch mode — rescrape leaderboard every 60s" },
14
- { cmd: "test", desc: "Run all tests (43 total)" },
15
- { cmd: "lint", desc: "Run ESLint" },
16
- { cmd: "build", desc: "Production build" },
17
- ];
18
-
19
9
  console.log("");
20
10
  console.log(` ${bold}${magenta}╭──────────────────────────────────────────╮${reset}`);
21
- console.log(` ${bold}${magenta}│${reset} ${bold}🏏 IPL Dashboard Dev Server Ready${reset} ${bold}${magenta}│${reset}`);
11
+ console.log(` ${bold}${magenta}│${reset} ${bold}🏏 IPL Dashboard Dev Server${reset} ${bold}${magenta}│${reset}`);
22
12
  console.log(` ${bold}${magenta}╰──────────────────────────────────────────╯${reset}`);
23
13
  console.log("");
24
- console.log(` ${bold}${cyan}Next Steps${reset}`);
14
+
15
+ const setupSteps = [
16
+ { cmd: "Edit .env", desc: "Set MONGODB_URI, IPL_LEAGUE_URL & IPL_POST_SECRET" },
17
+ { cmd: "npm run seed:api", desc: "Seed initial match data into MongoDB" },
18
+ { cmd: "npm run capture:ipl-auth", desc: "Log in to fantasy.iplt20.com and save auth state" },
19
+ { cmd: "npm run sync:ipl", desc: "Scrape live leaderboard snapshot" },
20
+ ];
21
+
22
+ console.log(` ${bold}${cyan}Setup${reset}`);
25
23
  console.log(` ${dim}────────────────────────────────────────────${reset}`);
24
+ for (const { cmd, desc } of setupSteps) {
25
+ console.log(` ${dim}${cmd.padEnd(24)} ${desc}${reset}`);
26
+ }
27
+
26
28
  console.log("");
29
+ console.log(` ${bold}${cyan}Commands${reset}`);
30
+ console.log(` ${dim}────────────────────────────────────────────${reset}`);
31
+
32
+ const cmds = [
33
+ { cmd: "dev:simple", desc: "Start dev server" },
34
+ { cmd: "build", desc: "Production build" },
35
+ { cmd: "sync:ipl", desc: "Scrape leaderboard snapshot" },
36
+ { cmd: "sync:ipl:watch", desc: "Watch mode — rescrape every 60s" },
37
+ { cmd: "sync:cloud", desc: "Run all scrapers" },
38
+ { cmd: "seed:api", desc: "Seed match data into MongoDB" },
39
+ { cmd: "seed:league", desc: "Seed league metadata" },
40
+ { cmd: "test", desc: "Run tests" },
41
+ { cmd: "lint", desc: "Run ESLint" },
42
+ ];
27
43
 
28
44
  for (const { cmd, desc } of cmds) {
29
- console.log(` ${amber}npm run${reset} ${bold}${cmd.padEnd(25)}${reset} ${dim}${desc}${reset}`);
45
+ console.log(` ${amber}npm run${reset} ${bold}${cmd.padEnd(20)}${reset} ${dim}${desc}${reset}`);
30
46
  }
31
47
 
32
48
  console.log("");
33
- console.log(` ${bold}${lime}Quick Start${reset}`);
34
- console.log(` ${dim} 1. Run${reset} ${amber}npm run capture:ipl-auth${reset} ${dim}(one-time login)${reset}`);
35
- console.log(` ${dim} 2. Run${reset} ${amber}npm run sync:ipl:transfers-daily${reset} ${dim}(load transfer data)${reset}`);
36
- console.log(` ${dim} 3. Run${reset} ${amber}npm run sync:ipl:watch${reset} ${dim}(start live sync)${reset}`);
37
- console.log(` ${dim} 4. Open${reset} ${cyan}http://localhost:3000${reset} ${dim}to view the dashboard${reset}`);
49
+ console.log(` ${bold}${lime}Next Steps${reset}`);
50
+ console.log(` ${dim}────────────────────────────────────────────${reset}`);
51
+ console.log(` ${dim} 1. Edit${reset} .env ${dim}with your MongoDB URI, league URL & secret${reset}`);
52
+ console.log(` ${dim} 2. Run${reset} ${amber}npm run seed:api${reset} ${dim}(seed match data)${reset}`);
53
+ console.log(` ${dim} 3. Run${reset} ${amber}npm run seed:league${reset} ${dim}(seed league metadata)${reset}`);
54
+ console.log(` ${dim} 4. Run${reset} ${amber}npm run capture:ipl-auth${reset} ${dim}(one-time login)${reset}`);
55
+ console.log(` ${dim} 5. Run${reset} ${amber}npm run sync:ipl:watch${reset} ${dim}(start live sync)${reset}`);
56
+ console.log(` ${dim} 6. Open${reset} ${cyan}http://localhost:3000${reset}${dim} to view the dashboard${reset}`);
57
+ console.log("");
58
+ console.log(` ${dim} Deploy:${reset} ${amber}npm run build${reset} ${dim}&&${reset} ${amber}npx vercel --prod${reset}`);
38
59
  console.log("");
@@ -29,61 +29,6 @@ async function main() {
29
29
 
30
30
  const collectionName = process.argv[3] || process.env.COLLECTION_NAME?.trim() || sanitizeCollectionName(leagueName);
31
31
 
32
- const isGoogleSheets = process.env.STORAGE_BACKEND === "google_sheets";
33
-
34
- if (isGoogleSheets) {
35
- const sheetId = process.env.GOOGLE_SHEET_ID;
36
- const email = process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL;
37
- const key = process.env.GOOGLE_PRIVATE_KEY;
38
-
39
- if (!sheetId || !email || !key) {
40
- console.log("Google Sheets not configured. Skipping league seed.");
41
- return;
42
- }
43
-
44
- const leagueUrl = process.env.IPL_LEAGUE_URL?.trim();
45
-
46
- let teams = [];
47
- const leagueDataPath = join(ROOT, "app/data/league.ts");
48
- if (existsSync(leagueDataPath)) {
49
- try {
50
- const content = readFileSync(leagueDataPath, "utf8");
51
- const match = content.match(/teams:\s*\[([\s\S]*?)\],/);
52
- if (match) {
53
- const entries = [...match[1].matchAll(/\{\s*id:\s*(\d+),\s*name:\s*"([^"]+)",\s*owner:\s*"([^"]+)"\s*\}/g)];
54
- teams = entries.map(([, id, name, owner]) => ({
55
- id: Number(id),
56
- name,
57
- owner,
58
- }));
59
- }
60
- } catch {}
61
- }
62
-
63
- const { google } = await import("googleapis");
64
- const auth = new google.auth.GoogleAuth({
65
- credentials: { client_email: email, private_key: key.replace(/\\n/g, "\n") },
66
- scopes: ["https://www.googleapis.com/auth/spreadsheets"],
67
- });
68
- const sheets = google.sheets({ version: "v4", auth });
69
- const sheetName = collectionName.replace(/[^a-z0-9_]/g, "_");
70
-
71
- try {
72
- await sheets.spreadsheets.values.append({
73
- spreadsheetId: sheetId,
74
- range: `${sheetName}!A:C`,
75
- valueInputOption: "RAW",
76
- requestBody: {
77
- values: [["league", JSON.stringify({ name: leagueName, leagueUrl, teams }), new Date().toISOString()]],
78
- },
79
- });
80
- console.log(`League "${leagueName}" seeded in sheet "${sheetName}" with ${teams.length} team(s).`);
81
- } catch (error) {
82
- console.error("Failed to seed league in Google Sheets:", error.message);
83
- }
84
- return;
85
- }
86
-
87
32
  const uri = process.env.MONGODB_URI;
88
33
  if (!uri) {
89
34
  console.log("MONGODB_URI not set. Skipping league seed.");
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/src/scraper.mjs DELETED
@@ -1,79 +0,0 @@
1
- import { get } from "node:https";
2
-
3
- const TIMEOUT_MS = 15000;
4
-
5
- function httpsGet(url) {
6
- return new Promise((resolve, reject) => {
7
- const req = get(url, { timeout: TIMEOUT_MS }, (res) => {
8
- const chunks = [];
9
- res.on("data", (c) => chunks.push(c));
10
- res.on("end", () => resolve(Buffer.concat(chunks).toString()));
11
- });
12
- req.on("error", reject);
13
- req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
14
- });
15
- }
16
-
17
- function extractTeamsFromHtml(html) {
18
- const names = new Set();
19
-
20
- // Try to find team/player names in tables (fantasy site pattern)
21
- const tableRowPatterns = [
22
- /<tr[^>]*>[\s\S]*?<td[^>]*>[\s\S]*?<a[^>]*class="team-name"[^>]*>([^<]+)<\/a>/gi,
23
- /<tr[^>]*>[\s\S]*?<td[^>]*class="[^"]*name[^"]*"[^>]*>([^<]+)<\/td>/gi,
24
- /<td[^>]*class="[^"]*team[^"]*"[^>]*>([^<]+)<\/td>/gi,
25
- /<div[^>]*class="[^"]*team-name[^"]*"[^>]*>([^<]+)<\/div>/gi,
26
- /<span[^>]*class="[^"]*user-name[^"]*"[^>]*>([^<]+)<\/span>/gi,
27
- /<div[^>]*class="[^"]*leaderboard-name[^"]*"[^>]*>([^<]+)<\/div>/gi,
28
- ];
29
-
30
- for (const pattern of tableRowPatterns) {
31
- let match;
32
- const re = new RegExp(pattern.source, "gi");
33
- while ((match = re.exec(html)) !== null) {
34
- const name = match[1].trim();
35
- if (name.length > 1 && name.length < 80) {
36
- names.add(name);
37
- }
38
- }
39
- }
40
-
41
- // Fallback: look for JSON-like data structures with team names
42
- const jsonPatterns = [
43
- /"teamName"\s*:\s*"([^"]+)"/g,
44
- /"leaderName"\s*:\s*"([^"]+)"/g,
45
- /"userName"\s*:\s*"([^"]+)"/g,
46
- /"name"\s*:\s*"([^"]+)"[^}]*"points"/g,
47
- ];
48
-
49
- for (const pattern of jsonPatterns) {
50
- let match;
51
- while ((match = pattern.exec(html)) !== null) {
52
- const name = match[1].trim();
53
- if (name.length > 1 && name.length < 80 && !name.includes("/")) {
54
- names.add(name);
55
- }
56
- }
57
- }
58
-
59
- return [...names].filter((n) => n.length > 1);
60
- }
61
-
62
- export async function scrapeTeamsFromUrl(url) {
63
- try {
64
- const html = await httpsGet(url);
65
- const names = extractTeamsFromHtml(html);
66
-
67
- if (names.length === 0) {
68
- return null;
69
- }
70
-
71
- return names.map((name, i) => ({
72
- id: i + 1,
73
- name,
74
- owner: name,
75
- }));
76
- } catch {
77
- return null;
78
- }
79
- }