@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.
- package/README.md +38 -129
- package/package.json +2 -3
- package/src/index.mjs +18 -22
- package/src/prompts.mjs +20 -112
- package/src/scaffold.mjs +23 -35
- package/template/README.md +23 -200
- package/template/app/api/ops/seed/route.ts +76 -0
- package/template/app/api/ops/status/route.ts +0 -2
- package/template/app/lib/config.ts +0 -4
- package/template/app/lib/matchStatus.ts +0 -12
- package/template/app/lib/storage/index.ts +1 -10
- package/template/package.json +1 -6
- package/template/scripts/dev-simple.js +1 -1
- package/template/scripts/dev-welcome.mjs +39 -18
- package/template/scripts/seed-league.mjs +0 -55
- package/screenshots/ai-roasting.png +0 -0
- package/screenshots/captain-board.png +0 -0
- package/screenshots/dashboard-overview.png +0 -0
- package/screenshots/ledger-table.png +0 -0
- package/screenshots/match-scrubber.png +0 -0
- package/screenshots/performance-tracker.png +0 -0
- package/src/scraper.mjs +0 -79
- package/template/app/hooks/dashboardPolling.ts +0 -53
- package/template/app/hooks/snapshotCache.ts +0 -47
- package/template/app/lib/storage/google-sheets-storage.ts +0 -147
- package/template/app/lib/utils/diff.ts +0 -24
- package/template/app/lib/utils/time.ts +0 -40
- package/template/screenshots/ai-roasting.png +0 -0
- package/template/screenshots/captain-board.png +0 -0
- package/template/screenshots/dashboard-overview.png +0 -0
- package/template/screenshots/ledger-table.png +0 -0
- package/template/screenshots/match-scrubber.png +0 -0
- package/template/screenshots/performance-tracker.png +0 -0
- package/template/tests/coverage-gaps.test.ts +0 -290
- package/template/tests/dashboard-polling.test.ts +0 -96
- package/template/tests/utils-and-cache.test.ts +0 -267
package/README.md
CHANGED
|
@@ -4,13 +4,16 @@ Scaffold a full-featured IPL fantasy cricket dashboard in seconds.
|
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx @vatvaghool/create-ipl-dashboard my-league
|
|
7
|
+
cd my-league
|
|
8
|
+
# Edit .env with your MongoDB URI, league URL and POST secret
|
|
9
|
+
npm run dev:simple
|
|
7
10
|
```
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
Open http://localhost:3000
|
|
10
13
|
|
|
11
14
|
## Usage
|
|
12
15
|
|
|
13
|
-
```
|
|
16
|
+
```
|
|
14
17
|
npx @vatvaghool/create-ipl-dashboard [project-name] [options]
|
|
15
18
|
```
|
|
16
19
|
|
|
@@ -19,155 +22,61 @@ npx @vatvaghool/create-ipl-dashboard [project-name] [options]
|
|
|
19
22
|
| Flag | Description |
|
|
20
23
|
|------|-------------|
|
|
21
24
|
| `--help`, `-h` | Show help message |
|
|
22
|
-
| `--scrape` | Auto-detect team names from the league URL |
|
|
23
25
|
| `--skip-install` | Skip `npm install` (useful for testing) |
|
|
24
26
|
|
|
25
|
-
###
|
|
27
|
+
### Prompts
|
|
26
28
|
|
|
27
29
|
| Prompt | Description |
|
|
28
30
|
|--------|-------------|
|
|
29
31
|
| Project name | Directory to scaffold into |
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
| Google Sheet ID | Google Sheet ID (only if google_sheets chosen) |
|
|
33
|
-
| Service account email | Google service account email (only if google_sheets chosen) |
|
|
34
|
-
| Private key | Google service account private key (only if google_sheets chosen) |
|
|
35
|
-
| League URL | The fantasy.iplt20.com league page URL |
|
|
36
|
-
| Collection/sheet name | MongoDB collection or Google sheet for this league's data |
|
|
37
|
-
| League name | Used as collection/table name in storage |
|
|
38
|
-
|
|
39
|
-
Set team names later via `app/data/teams.ts` or use `--scrape` to auto-detect from the league URL during scaffolding.
|
|
40
|
-
|
|
41
|
-
## Screenshots
|
|
42
|
-
|
|
43
|
-

|
|
44
|
-
|
|
45
|
-
*Dashboard overview — full page*
|
|
46
|
-
|
|
47
|
-

|
|
48
|
-
|
|
49
|
-
*Performance Tracker — Recharts line chart*
|
|
50
|
-
|
|
51
|
-

|
|
52
|
-
|
|
53
|
-
*Captain Board — captain/vice-captain picks*
|
|
54
|
-
|
|
55
|
-

|
|
32
|
+
| League name | Used as MongoDB collection name |
|
|
33
|
+
| Team names | Comma-separated with optional owner in parens e.g. `Team A (Alice), Team B (Bob)` (Enter for 8 default teams) |
|
|
56
34
|
|
|
57
|
-
|
|
35
|
+
### Setup
|
|
58
36
|
|
|
59
|
-
|
|
37
|
+
After scaffolding, edit `.env` and fill in:
|
|
60
38
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## What you get
|
|
68
|
-
|
|
69
|
-
A full Next.js 16 project with:
|
|
70
|
-
|
|
71
|
-
- **Leaderboard dashboard** — live standings with rank movements and point gaps
|
|
72
|
-
- **Performance charts** — Recharts line charts tracking every team's trajectory
|
|
73
|
-
- **Captain board** — captain/vice-captain picks per team
|
|
74
|
-
- **Match scrubber** — interactive timeline through match history
|
|
75
|
-
- **AI roasting** — generated commentary in multiple languages
|
|
76
|
-
- **Stock ticker** — fantasy stocks with sparklines
|
|
77
|
-
- **Live updates** — bookmarklet or Playwright scraper for live sync
|
|
78
|
-
- **MongoDB persistence** — auto-configured database connection
|
|
39
|
+
```
|
|
40
|
+
MONGODB_URI=mongodb+srv://...
|
|
41
|
+
IPL_LEAGUE_URL=https://fantasy.iplt20.com/classic/league/view/...
|
|
42
|
+
IPL_POST_SECRET=your-secret
|
|
43
|
+
```
|
|
79
44
|
|
|
80
|
-
|
|
45
|
+
Then seed data and start syncing:
|
|
81
46
|
|
|
82
47
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
#
|
|
86
|
-
npm run
|
|
48
|
+
npm run seed:api # Seed match data into MongoDB
|
|
49
|
+
npm run seed:league # Seed league metadata
|
|
50
|
+
npm run capture:ipl-auth # One-time Playwright login
|
|
51
|
+
npm run sync:ipl:watch # Start live sync
|
|
87
52
|
```
|
|
88
53
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
### Available commands
|
|
54
|
+
## Commands
|
|
92
55
|
|
|
93
56
|
| Command | Description |
|
|
94
57
|
|---------|-------------|
|
|
95
|
-
| `npm run dev` |
|
|
96
|
-
| `npm run dev:simple` | Dev server (simple, no splash) |
|
|
58
|
+
| `npm run dev:simple` | Start dev server |
|
|
97
59
|
| `npm run build` | Production build |
|
|
98
|
-
| `npm start` | Production server |
|
|
99
60
|
| `npm run capture:ipl-auth` | Capture Playwright login state (one-time) |
|
|
100
61
|
| `npm run sync:ipl` | Scrape live leaderboard snapshot |
|
|
101
|
-
| `npm run sync:ipl:watch` | Scrape leaderboard in watch mode
|
|
102
|
-
| `npm run sync:
|
|
103
|
-
| `npm run
|
|
104
|
-
| `npm run seed:league` | Seed league metadata into
|
|
105
|
-
| `npm run
|
|
106
|
-
| `npm run seed:mongodb:reset` | Reset and re-seed MongoDB data |
|
|
107
|
-
| `npm run verify:production` | Verify production setup |
|
|
108
|
-
| `npm run monitor:ops` | Check ops health status |
|
|
109
|
-
| `npm run test` | Run test suite |
|
|
62
|
+
| `npm run sync:ipl:watch` | Scrape leaderboard in watch mode |
|
|
63
|
+
| `npm run sync:cloud` | Run all scrapers |
|
|
64
|
+
| `npm run seed:api` | POST /api/ops/seed — seed match data into MongoDB |
|
|
65
|
+
| `npm run seed:league` | Seed league metadata into MongoDB |
|
|
66
|
+
| `npm run test` | Run tests |
|
|
110
67
|
| `npm run lint` | Run linter |
|
|
111
68
|
|
|
112
|
-
##
|
|
113
|
-
|
|
114
|
-
The CLI:
|
|
69
|
+
## API
|
|
115
70
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
71
|
+
| Route | Method | Description |
|
|
72
|
+
|-------|--------|-------------|
|
|
73
|
+
| `/api/ipl` | GET | Dashboard payload |
|
|
74
|
+
| `/api/ipl` | POST | Ingest leaderboard snapshot |
|
|
75
|
+
| `/api/ipl/transfers` | GET/POST | Transfer/booster data |
|
|
76
|
+
| `/api/ipl/upcoming-matches` | GET | Upcoming match schedule |
|
|
77
|
+
| `/api/ops/status` | GET | Health check |
|
|
78
|
+
| `/api/ops/seed` | POST | Seed initial match data into MongoDB |
|
|
123
79
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
The template includes all dashboard components, API endpoints, scrapers, and tests from the [ipl-dashboard](https://github.com/anomalyco/ipl-dashboard) project.
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## Next Steps
|
|
131
|
-
|
|
132
|
-
### Adding more leagues
|
|
133
|
-
|
|
134
|
-
```bash
|
|
135
|
-
npx @vatvaghool/create-ipl-dashboard another-league
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
Provide a different league name and the new league will be stored in its own collection/sheet — data stays fully isolated.
|
|
139
|
-
|
|
140
|
-
### Viewing seeded data
|
|
141
|
-
|
|
142
|
-
**MongoDB:** Connect with any MongoDB client. Each league appears as a separate collection with `type: "league"`.
|
|
143
|
-
|
|
144
|
-
**Google Sheets:** Open your spreadsheet in a browser. Each league has its own sheet (tab) with league metadata in row 2.
|
|
145
|
-
|
|
146
|
-
### Production deployment
|
|
147
|
-
|
|
148
|
-
```bash
|
|
149
|
-
cd my-league
|
|
150
|
-
npm run build
|
|
151
|
-
npx vercel --prod
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Set `IPL_POST_SECRET` in your Vercel dashboard. Storage credentials are already populated in the scaffolded `.env`.
|
|
155
|
-
|
|
156
|
-
### Data sync workflow
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
# 1. Capture browser login (one-time)
|
|
160
|
-
npm run capture:ipl-auth
|
|
161
|
-
|
|
162
|
-
# 2. Scrape leaderboard (manual or watch mode)
|
|
163
|
-
npm run sync:ipl
|
|
164
|
-
npm run sync:ipl:watch # auto-poll every 2 min
|
|
165
|
-
|
|
166
|
-
# 3. Scrape transfer/booster data
|
|
167
|
-
npm run sync:ipl:transfers-daily
|
|
168
|
-
|
|
169
|
-
# 4. Run all scrapers (for cloud jobs)
|
|
170
|
-
npm run sync:cloud
|
|
171
|
-
```
|
|
80
|
+
## Project
|
|
172
81
|
|
|
173
|
-
|
|
82
|
+
A full Next.js 16 project with dashboard components, API endpoints, scrapers, and tests.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vatvaghool/create-ipl-dashboard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.23",
|
|
4
4
|
"description": "Scaffold an IPL fantasy cricket dashboard project",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
|
11
|
-
"template"
|
|
12
|
-
"screenshots"
|
|
11
|
+
"template"
|
|
13
12
|
],
|
|
14
13
|
"publishConfig": {
|
|
15
14
|
"access": "public"
|
package/src/index.mjs
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import { mkdir } from "node:fs/promises";
|
|
6
|
-
import { getProjectName,
|
|
6
|
+
import { getProjectName, getLeagueName, getTeams, close } from "./prompts.mjs";
|
|
7
7
|
import { scaffoldProject } from "./scaffold.mjs";
|
|
8
|
-
import { scrapeTeamsFromUrl } from "./scraper.mjs";
|
|
9
8
|
|
|
10
9
|
function printHelp(exitCode) {
|
|
11
10
|
console.log(`
|
|
@@ -19,11 +18,9 @@ function printHelp(exitCode) {
|
|
|
19
18
|
Options:
|
|
20
19
|
--help, -h Show this help message
|
|
21
20
|
--skip-install Skip npm install after scaffolding
|
|
22
|
-
--scrape Auto-detect team names from the league URL
|
|
23
21
|
|
|
24
22
|
Examples:
|
|
25
23
|
npx create-ipl-dashboard my-league
|
|
26
|
-
npx create-ipl-dashboard my-league --scrape
|
|
27
24
|
npx create-ipl-dashboard --skip-install
|
|
28
25
|
`);
|
|
29
26
|
process.exit(exitCode);
|
|
@@ -31,12 +28,11 @@ function printHelp(exitCode) {
|
|
|
31
28
|
|
|
32
29
|
async function main() {
|
|
33
30
|
const args = process.argv.slice(2);
|
|
34
|
-
const flags = {
|
|
31
|
+
const flags = { skipInstall: false, help: false };
|
|
35
32
|
const positional = [];
|
|
36
33
|
|
|
37
34
|
for (const arg of args) {
|
|
38
35
|
if (arg === "--help" || arg === "-h") flags.help = true;
|
|
39
|
-
else if (arg === "--scrape") flags.scrape = true;
|
|
40
36
|
else if (arg === "--skip-install") flags.skipInstall = true;
|
|
41
37
|
else if (!arg.startsWith("--")) positional.push(arg);
|
|
42
38
|
else { console.error(`Unknown flag: ${arg}`); printHelp(1); }
|
|
@@ -59,33 +55,33 @@ async function main() {
|
|
|
59
55
|
|
|
60
56
|
await mkdir(projectPath, { recursive: true });
|
|
61
57
|
|
|
62
|
-
const conn = await getConnectionUrl();
|
|
63
|
-
const leagueUrl = await getLeagueUrl();
|
|
64
58
|
const leagueName = await getLeagueName();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
teams =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
|
|
60
|
+
let teams = await getTeams();
|
|
61
|
+
|
|
62
|
+
if (!teams || teams.length === 0) {
|
|
63
|
+
teams = [
|
|
64
|
+
{ id: 1, name: "The Cricketers", owner: "Player 1" },
|
|
65
|
+
{ id: 2, name: "Six Hitters", owner: "Player 2" },
|
|
66
|
+
{ id: 3, name: "Bowling Titans", owner: "Player 3" },
|
|
67
|
+
{ id: 4, name: "Fielding Masters", owner: "Player 4" },
|
|
68
|
+
{ id: 5, name: "Night Watchmen", owner: "Player 5" },
|
|
69
|
+
{ id: 6, name: "Super Kings", owner: "Player 6" },
|
|
70
|
+
{ id: 7, name: "Royal Challengers", owner: "Player 7" },
|
|
71
|
+
{ id: 8, name: "Mumbai Indians", owner: "Player 8" },
|
|
72
|
+
];
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
close();
|
|
76
76
|
|
|
77
|
-
await scaffoldProject(projectPath, {
|
|
77
|
+
await scaffoldProject(projectPath, { leagueName, teams, skipInstall: flags.skipInstall });
|
|
78
78
|
|
|
79
79
|
console.log("");
|
|
80
80
|
console.log(" Next steps:");
|
|
81
81
|
console.log(` cd ${projectName}`);
|
|
82
|
+
console.log(" Edit .env — set MONGODB_URI, IPL_LEAGUE_URL and IPL_POST_SECRET");
|
|
82
83
|
console.log(" npm run dev:simple");
|
|
83
84
|
console.log("");
|
|
84
|
-
console.log(" For live data sync, capture auth state:");
|
|
85
|
-
console.log(` cd ${projectName}`);
|
|
86
|
-
console.log(" npm run capture:ipl-auth");
|
|
87
|
-
console.log(" npm run sync:ipl");
|
|
88
|
-
console.log("");
|
|
89
85
|
}
|
|
90
86
|
|
|
91
87
|
main().catch((err) => {
|
package/src/prompts.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
2
|
import { stdin as input, stdout as output, stdin } from "node:process";
|
|
3
|
-
import * as readline from "node:readline";
|
|
4
3
|
|
|
5
4
|
const isTTY = stdin.isTTY;
|
|
6
5
|
|
|
@@ -25,86 +24,6 @@ function prompt(query) {
|
|
|
25
24
|
});
|
|
26
25
|
}
|
|
27
26
|
|
|
28
|
-
const BACKEND_VALUES = { mongodb: "mongodb", google_sheets: "google_sheets" };
|
|
29
|
-
|
|
30
|
-
async function select(message, choices, valueMap) {
|
|
31
|
-
if (!isTTY) {
|
|
32
|
-
await consumeAllLines();
|
|
33
|
-
const answer = (pipedLines[promptIndex++] || "").trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
34
|
-
const match = choices.find((c) => c.toLowerCase().replace(/[\s_-]+/g, "").startsWith(answer));
|
|
35
|
-
const idx = match ? choices.indexOf(match) : 0;
|
|
36
|
-
return valueMap ? valueMap[Object.keys(valueMap)[idx]] : choices[idx];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return new Promise((resolve) => {
|
|
40
|
-
let selected = 0;
|
|
41
|
-
const rl = createInterface({ input, output });
|
|
42
|
-
|
|
43
|
-
function render(n) {
|
|
44
|
-
for (let i = 0; i < n; i++) {
|
|
45
|
-
readline.moveCursor(output, 0, -1);
|
|
46
|
-
readline.clearLine(output, 0);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
process.stdout.write(`${message}\n`);
|
|
51
|
-
choices.forEach((c, i) => {
|
|
52
|
-
process.stdout.write(i === selected ? `\x1B[7m ${c} \x1B[0m\n` : ` ${c}\n`);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const numLines = choices.length;
|
|
56
|
-
|
|
57
|
-
readline.emitKeypressEvents(input);
|
|
58
|
-
if (input.isTTY) input.setRawMode(true);
|
|
59
|
-
|
|
60
|
-
function cleanup() {
|
|
61
|
-
input.setRawMode(false);
|
|
62
|
-
input.pause();
|
|
63
|
-
input.removeListener("keypress", handler);
|
|
64
|
-
rl.close();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function handler(str, key) {
|
|
68
|
-
if (!key) return;
|
|
69
|
-
if (key.name === "up" && selected > 0) {
|
|
70
|
-
selected--;
|
|
71
|
-
for (let i = 0; i < numLines; i++) readline.moveCursor(output, 0, -1);
|
|
72
|
-
choices.forEach((c, i) => {
|
|
73
|
-
readline.clearLine(output, 0);
|
|
74
|
-
readline.cursorTo(output, 0);
|
|
75
|
-
output.write(i === selected ? `\x1B[7m ${c} \x1B[0m` : ` ${c} `);
|
|
76
|
-
if (i < numLines - 1) output.write("\n");
|
|
77
|
-
});
|
|
78
|
-
output.write("\n");
|
|
79
|
-
} else if (key.name === "down" && selected < numLines - 1) {
|
|
80
|
-
selected++;
|
|
81
|
-
for (let i = 0; i < numLines; i++) readline.moveCursor(output, 0, -1);
|
|
82
|
-
choices.forEach((c, i) => {
|
|
83
|
-
readline.clearLine(output, 0);
|
|
84
|
-
readline.cursorTo(output, 0);
|
|
85
|
-
output.write(i === selected ? `\x1B[7m ${c} \x1B[0m` : ` ${c} `);
|
|
86
|
-
if (i < numLines - 1) output.write("\n");
|
|
87
|
-
});
|
|
88
|
-
output.write("\n");
|
|
89
|
-
} else if (key.name === "return") {
|
|
90
|
-
cleanup();
|
|
91
|
-
for (let i = 0; i < numLines + 1; i++) {
|
|
92
|
-
readline.moveCursor(output, 0, -1);
|
|
93
|
-
readline.clearLine(output, 0);
|
|
94
|
-
}
|
|
95
|
-
output.write(`${message} \x1B[32m${choices[selected]}\x1B[0m\n`);
|
|
96
|
-
const keys = Object.keys(valueMap || {});
|
|
97
|
-
resolve(valueMap ? valueMap[keys[selected]] : choices[selected]);
|
|
98
|
-
} else if (key.name === "c" && key.ctrl) {
|
|
99
|
-
cleanup();
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
input.on("keypress", handler);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
|
|
108
27
|
export async function getProjectName(args) {
|
|
109
28
|
if (args[0]) return args[0];
|
|
110
29
|
if (!isTTY) {
|
|
@@ -114,47 +33,36 @@ export async function getProjectName(args) {
|
|
|
114
33
|
return (await prompt("Project name: ")).trim() || "ipl-dashboard";
|
|
115
34
|
}
|
|
116
35
|
|
|
117
|
-
export async function
|
|
118
|
-
return select("Storage backend:", ["MongoDB", "Google Sheets"], BACKEND_VALUES);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export async function getConnectionUrl() {
|
|
122
|
-
const backend = await getStorageBackend();
|
|
123
|
-
if (backend === "google_sheets") {
|
|
124
|
-
if (!isTTY) {
|
|
125
|
-
await consumeAllLines();
|
|
126
|
-
return { backend, sheetId: (pipedLines[promptIndex++] || "").trim(), serviceAccountEmail: (pipedLines[promptIndex++] || "").trim(), privateKey: (pipedLines[promptIndex++] || "").trim() };
|
|
127
|
-
}
|
|
128
|
-
console.log("\n Google Sheets credentials:\n");
|
|
129
|
-
const sheetId = (await prompt(" Google Sheet ID: ")).trim();
|
|
130
|
-
const serviceAccountEmail = (await prompt(" Service account email: ")).trim();
|
|
131
|
-
const privateKey = (await prompt(" Private key (including -----BEGIN/END-----): ")).trim();
|
|
132
|
-
return { backend, sheetId, serviceAccountEmail, privateKey };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
|
|
36
|
+
export async function getLeagueName() {
|
|
136
37
|
if (!isTTY) {
|
|
137
38
|
await consumeAllLines();
|
|
138
|
-
return
|
|
39
|
+
return (pipedLines[promptIndex++] || "my_league").trim();
|
|
139
40
|
}
|
|
140
|
-
|
|
141
|
-
return { backend, uri: answer || DEFAULT_MONGODB_URI || "" };
|
|
41
|
+
return (await prompt("League name (used as collection name, e.g. my_office_league): ")).trim() || "my_league";
|
|
142
42
|
}
|
|
143
43
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
44
|
+
function parseTeams(input) {
|
|
45
|
+
return input.split(",").map((part, i) => {
|
|
46
|
+
part = part.trim();
|
|
47
|
+
if (!part) return null;
|
|
48
|
+
const match = part.match(/^(.+?)\s*\(([^)]+)\)$/);
|
|
49
|
+
if (match) {
|
|
50
|
+
return { id: i + 1, name: match[1].trim(), owner: match[2].trim() };
|
|
51
|
+
}
|
|
52
|
+
return { id: i + 1, name: part, owner: part };
|
|
53
|
+
}).filter(Boolean);
|
|
150
54
|
}
|
|
151
55
|
|
|
152
|
-
export async function
|
|
56
|
+
export async function getTeams() {
|
|
153
57
|
if (!isTTY) {
|
|
154
58
|
await consumeAllLines();
|
|
155
|
-
|
|
59
|
+
const raw = (pipedLines[promptIndex++] || "").trim();
|
|
60
|
+
if (!raw) return null;
|
|
61
|
+
return parseTeams(raw);
|
|
156
62
|
}
|
|
157
|
-
|
|
63
|
+
const answer = (await prompt("Team names (comma-separated, or press Enter for defaults):\n e.g. Team A (Alice), Team B (Bob)\n> ")).trim();
|
|
64
|
+
if (!answer) return null;
|
|
65
|
+
return parseTeams(answer);
|
|
158
66
|
}
|
|
159
67
|
|
|
160
68
|
export function close() {}
|
package/src/scaffold.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const TEMPLATE_DIR = join(PACKAGE_ROOT, "template");
|
|
|
10
10
|
|
|
11
11
|
export async function scaffoldProject(
|
|
12
12
|
projectPath,
|
|
13
|
-
{
|
|
13
|
+
{ leagueName, teams, skipInstall = false },
|
|
14
14
|
) {
|
|
15
15
|
console.log(`\nCreating project at ${projectPath}...`);
|
|
16
16
|
|
|
@@ -33,15 +33,11 @@ export async function scaffoldProject(
|
|
|
33
33
|
} catch {}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
await writeEnvFile(projectPath, {
|
|
37
|
-
conn,
|
|
38
|
-
leagueUrl,
|
|
39
|
-
leagueName,
|
|
40
|
-
});
|
|
36
|
+
await writeEnvFile(projectPath, { leagueName });
|
|
41
37
|
await writeTeamData(projectPath, teams || []);
|
|
42
|
-
await writeLeagueData(projectPath, { leagueName,
|
|
38
|
+
await writeLeagueData(projectPath, { leagueName, teams: teams || [] });
|
|
43
39
|
await writeMatchPointsPlaceholder(projectPath);
|
|
44
|
-
await updatePackageJson(projectPath
|
|
40
|
+
await updatePackageJson(projectPath);
|
|
45
41
|
|
|
46
42
|
for (const f of [
|
|
47
43
|
"app/api/ipl/live-snapshot.json",
|
|
@@ -56,16 +52,11 @@ export async function scaffoldProject(
|
|
|
56
52
|
console.log(" Running npm install...");
|
|
57
53
|
execSync("npm install", { cwd: projectPath, stdio: "inherit" });
|
|
58
54
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
stdio: "inherit",
|
|
65
|
-
});
|
|
66
|
-
} catch {
|
|
67
|
-
console.log(" League seed skipped (storage may not be reachable yet)");
|
|
68
|
-
}
|
|
55
|
+
console.log(" Installing Playwright browser...");
|
|
56
|
+
try {
|
|
57
|
+
execSync("npx playwright install chromium", { cwd: projectPath, stdio: "inherit" });
|
|
58
|
+
} catch {
|
|
59
|
+
console.log(" Playwright install skipped (run manually: npx playwright install chromium)");
|
|
69
60
|
}
|
|
70
61
|
} else {
|
|
71
62
|
console.log(
|
|
@@ -76,27 +67,27 @@ export async function scaffoldProject(
|
|
|
76
67
|
console.log("\n Done! Your project is ready at:", projectPath);
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
async function writeEnvFile(
|
|
80
|
-
projectPath,
|
|
81
|
-
{ conn, leagueUrl, leagueName },
|
|
82
|
-
) {
|
|
70
|
+
async function writeEnvFile(projectPath, { leagueName }) {
|
|
83
71
|
const envPath = join(projectPath, ".env");
|
|
84
72
|
const lines = [
|
|
85
|
-
`STORAGE_BACKEND=${conn.backend || "mongodb"}`,
|
|
86
73
|
`COLLECTION_NAME=${leagueName || "my_league"}`,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
conn.backend === "google_sheets" ? `GOOGLE_PRIVATE_KEY=${conn.privateKey || ""}` : "",
|
|
90
|
-
`IPL_LEAGUE_URL=${leagueUrl || ""}`,
|
|
74
|
+
`MONGODB_URI=`,
|
|
75
|
+
`IPL_LEAGUE_URL=`,
|
|
91
76
|
`IPL_LEAGUE_NAME=${leagueName || ""}`,
|
|
92
77
|
`IPL_POST_SECRET=`,
|
|
93
78
|
``,
|
|
79
|
+
`# Replace the values above, then run:`,
|
|
80
|
+
`# npm run seed:api (seed match data into MongoDB)`,
|
|
81
|
+
`# npm run seed:league (seed league metadata)`,
|
|
82
|
+
`# npm run capture:ipl-auth (one-time login)`,
|
|
83
|
+
`# npm run sync:ipl (scrape live leaderboard)`,
|
|
84
|
+
``,
|
|
94
85
|
`IPL_API_BASE_URL=http://localhost:3000`,
|
|
95
86
|
`IPL_API_LOG=1`,
|
|
96
87
|
`IPL_API_LOG_PAYLOAD=0`,
|
|
97
88
|
`IPL_WRITE_SEED_DATA_FILE=1`,
|
|
98
89
|
``,
|
|
99
|
-
]
|
|
90
|
+
];
|
|
100
91
|
await writeFile(envPath, lines.join("\n"));
|
|
101
92
|
}
|
|
102
93
|
|
|
@@ -140,7 +131,7 @@ for (const team of TEAMS) {
|
|
|
140
131
|
await writeFile(join(dir, "teams.ts"), content);
|
|
141
132
|
}
|
|
142
133
|
|
|
143
|
-
async function writeLeagueData(projectPath, { leagueName,
|
|
134
|
+
async function writeLeagueData(projectPath, { leagueName, teams }) {
|
|
144
135
|
const dir = join(projectPath, "app/data");
|
|
145
136
|
await mkdir(dir, { recursive: true });
|
|
146
137
|
|
|
@@ -156,7 +147,7 @@ async function writeLeagueData(projectPath, { leagueName, leagueUrl, teams }) {
|
|
|
156
147
|
|
|
157
148
|
export const leagueInfo: LeagueInfo = {
|
|
158
149
|
name: "${leagueName || ""}",
|
|
159
|
-
leagueUrl: "
|
|
150
|
+
leagueUrl: "",
|
|
160
151
|
teams: [
|
|
161
152
|
${teamEntries},
|
|
162
153
|
],
|
|
@@ -182,7 +173,7 @@ async function writeMatchPointsPlaceholder(projectPath) {
|
|
|
182
173
|
await writeFile(join(dir, "match-points.ts"), content);
|
|
183
174
|
}
|
|
184
175
|
|
|
185
|
-
async function updatePackageJson(projectPath
|
|
176
|
+
async function updatePackageJson(projectPath) {
|
|
186
177
|
const pkgPath = join(projectPath, "package.json");
|
|
187
178
|
try {
|
|
188
179
|
const raw = await readFile(pkgPath, "utf8");
|
|
@@ -191,10 +182,7 @@ async function updatePackageJson(projectPath, storageBackend) {
|
|
|
191
182
|
pkg.private = false;
|
|
192
183
|
delete pkg.scripts?.["generate:template"];
|
|
193
184
|
pkg.scripts["seed:league"] = "node scripts/seed-league.mjs";
|
|
194
|
-
|
|
195
|
-
pkg.dependencies = pkg.dependencies || {};
|
|
196
|
-
pkg.dependencies.googleapis = "^140.0.0";
|
|
197
|
-
}
|
|
185
|
+
pkg.scripts["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)\"";
|
|
198
186
|
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
199
187
|
} catch {}
|
|
200
188
|
}
|