@vatvaghool/create-ipl-dashboard 0.1.15 → 0.1.17

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 CHANGED
@@ -34,10 +34,9 @@ npx @vatvaghool/create-ipl-dashboard [project-name] [options]
34
34
  | Private key | Google service account private key (only if google_sheets chosen) |
35
35
  | League URL | The fantasy.iplt20.com league page URL |
36
36
  | Collection/sheet name | MongoDB collection or Google sheet for this league's data |
37
- | League name | Display name for your league |
38
- | Teams | Team names (and optional owners) in your league
37
+ | League name | Used as collection/table name in storage |
39
38
 
40
- If `--scrape` is provided, the CLI attempts to extract team names from the league page HTML. If that fails, it falls back to manual entry.
39
+ Set team names later via `app/data/teams.ts` or use `--scrape` to auto-detect from the league URL during scaffolding.
41
40
 
42
41
  ## Screenshots
43
42
 
@@ -85,16 +84,31 @@ cd my-league
85
84
 
86
85
  # Start the dev server
87
86
  npm run dev:simple
88
-
89
- # Capture auth state for scrapers (one-time setup)
90
- npm run capture:ipl-auth
91
-
92
- # Scrape live leaderboard data
93
- npm run sync:ipl
94
87
  ```
95
88
 
96
89
  Open http://localhost:3000 to see your dashboard.
97
90
 
91
+ ### Available commands
92
+
93
+ | Command | Description |
94
+ |---------|-------------|
95
+ | `npm run dev` | Dev server with welcome splash |
96
+ | `npm run dev:simple` | Dev server (simple, no splash) |
97
+ | `npm run build` | Production build |
98
+ | `npm start` | Production server |
99
+ | `npm run capture:ipl-auth` | Capture Playwright login state (one-time) |
100
+ | `npm run sync:ipl` | Scrape live leaderboard snapshot |
101
+ | `npm run sync:ipl:watch` | Scrape leaderboard in watch mode (polls every 2 min) |
102
+ | `npm run sync:ipl:transfers-daily` | Scrape transfer/booster data |
103
+ | `npm run sync:cloud` | Run both leaderboard + transfer sync (for cloud jobs) |
104
+ | `npm run seed:league` | Seed league metadata into storage |
105
+ | `npm run seed:mongodb` | Seed initial raw user data from `data.ts` |
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 |
110
+ | `npm run lint` | Run linter |
111
+
98
112
  ## How it works
99
113
 
100
114
  The CLI:
@@ -121,11 +135,13 @@ The template includes all dashboard components, API endpoints, scrapers, and tes
121
135
  npx @vatvaghool/create-ipl-dashboard another-league
122
136
  ```
123
137
 
124
- Provide a different collection name (e.g. `ipl_2025_friends_league`) and the new league will be stored in its own collection — data stays fully isolated.
138
+ Provide a different league name and the new league will be stored in its own collection/sheet — data stays fully isolated.
125
139
 
126
140
  ### Viewing seeded data
127
141
 
128
- Connect to the MongoDB instance with any MongoDB client. Each league appears as a separate collection containing a document with `type: "league"` and all the metadata (name, URL, teams, timestamps).
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.
129
145
 
130
146
  ### Production deployment
131
147
 
@@ -135,4 +151,23 @@ npm run build
135
151
  npx vercel --prod
136
152
  ```
137
153
 
138
- Set `IPL_POST_SECRET` in your Vercel dashboard. The `MONGODB_URI` and `COLLECTION_NAME` are already populated in the scaffolded `.env`.
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
+ ```
172
+
173
+ See the [Available commands](#quick-start-after-scaffold) table above for all options.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vatvaghool/create-ipl-dashboard",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Scaffold an IPL fantasy cricket dashboard project",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.mjs CHANGED
@@ -3,12 +3,10 @@
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, getMongoUri, getStorageBackend, getGoogleSheetsConfig, getLeagueUrl, getCollectionName, getLeagueName, getTeams, close } from "./prompts.mjs";
6
+ import { getProjectName, getConnectionUrl, getLeagueUrl, getLeagueName, close } from "./prompts.mjs";
7
7
  import { scaffoldProject } from "./scaffold.mjs";
8
8
  import { scrapeTeamsFromUrl } from "./scraper.mjs";
9
9
 
10
- const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
11
-
12
10
  function printHelp(exitCode) {
13
11
  console.log(`
14
12
  create-ipl-dashboard [project-name] [options]
@@ -61,18 +59,8 @@ async function main() {
61
59
 
62
60
  await mkdir(projectPath, { recursive: true });
63
61
 
64
- const storageBackend = await getStorageBackend();
65
- let mongoUri = "";
66
- let googleSheets = {};
67
-
68
- if (storageBackend === "google_sheets") {
69
- googleSheets = await getGoogleSheetsConfig();
70
- } else {
71
- mongoUri = await getMongoUri(DEFAULT_MONGODB_URI);
72
- }
73
-
62
+ const conn = await getConnectionUrl();
74
63
  const leagueUrl = await getLeagueUrl();
75
- const collectionName = await getCollectionName();
76
64
  const leagueName = await getLeagueName();
77
65
  let teams;
78
66
 
@@ -81,17 +69,12 @@ async function main() {
81
69
  teams = await scrapeTeamsFromUrl(leagueUrl);
82
70
  if (teams && teams.length > 0) {
83
71
  console.log(` Found ${teams.length} team(s): ${teams.map((t) => t.name).join(", ")}`);
84
- } else {
85
- console.log(" Could not auto-detect teams. Enter them manually:");
86
- teams = await getTeams();
87
72
  }
88
- } else {
89
- teams = await getTeams();
90
73
  }
91
74
 
92
75
  close();
93
76
 
94
- await scaffoldProject(projectPath, { mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName, teams, skipInstall: flags.skipInstall });
77
+ await scaffoldProject(projectPath, { conn, leagueUrl, leagueName, teams, skipInstall: flags.skipInstall });
95
78
 
96
79
  console.log("");
97
80
  console.log(" Next steps:");
package/src/prompts.mjs CHANGED
@@ -1,5 +1,6 @@
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";
3
4
 
4
5
  const isTTY = stdin.isTTY;
5
6
 
@@ -24,96 +25,136 @@ function prompt(query) {
24
25
  });
25
26
  }
26
27
 
27
- export async function getProjectName(args) {
28
- if (args[0]) return args[0];
29
- if (!isTTY) {
30
- await consumeAllLines();
31
- return pipedLines[promptIndex++] || "ipl-dashboard";
32
- }
33
- return (await prompt("Project name: ")).trim() || "ipl-dashboard";
34
- }
28
+ const BACKEND_VALUES = { mongodb: "mongodb", google_sheets: "google_sheets" };
35
29
 
36
- export async function getMongoUri(defaultUri) {
30
+ async function select(message, choices, valueMap) {
37
31
  if (!isTTY) {
38
32
  await consumeAllLines();
39
- return (pipedLines[promptIndex++] || defaultUri || "").trim();
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];
40
37
  }
41
- const answer = (await prompt(`MongoDB URI (press Enter for default): `)).trim();
42
- return answer || defaultUri || "";
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
+ });
43
106
  }
44
107
 
45
- export async function getLeagueUrl() {
108
+ export async function getProjectName(args) {
109
+ if (args[0]) return args[0];
46
110
  if (!isTTY) {
47
111
  await consumeAllLines();
48
- return (pipedLines[promptIndex++] || "").trim();
112
+ return pipedLines[promptIndex++] || "ipl-dashboard";
49
113
  }
50
- return (await prompt("IPL fantasy league URL (or press Enter to skip, can be set later): ")).trim();
114
+ return (await prompt("Project name: ")).trim() || "ipl-dashboard";
51
115
  }
52
116
 
53
117
  export async function getStorageBackend() {
54
- if (!isTTY) {
55
- await consumeAllLines();
56
- return (pipedLines[promptIndex++] || "mongodb").trim();
57
- }
58
- const answer = (await prompt("Storage backend (mongodb / google_sheets, default: mongodb): ")).trim().toLowerCase();
59
- return answer === "google_sheets" ? "google_sheets" : "mongodb";
118
+ return select("Storage backend:", ["MongoDB", "Google Sheets"], BACKEND_VALUES);
60
119
  }
61
120
 
62
- export async function getGoogleSheetsConfig() {
63
- if (!isTTY) {
64
- await consumeAllLines();
65
- const sheetId = (pipedLines[promptIndex++] || "").trim();
66
- const email = (pipedLines[promptIndex++] || "").trim();
67
- const key = (pipedLines[promptIndex++] || "").trim();
68
- return { sheetId, serviceAccountEmail: email, privateKey: key };
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 };
69
133
  }
70
- console.log("\n Google Sheets configuration:\n");
71
- const sheetId = (await prompt(" Google Sheet ID: ")).trim();
72
- const serviceAccountEmail = (await prompt(" Service account email: ")).trim();
73
- const privateKey = (await prompt(" Private key (paste the full key, including -----BEGIN/END-----): ")).trim();
74
- return { sheetId, serviceAccountEmail, privateKey };
75
- }
76
134
 
77
- export async function getCollectionName() {
135
+ const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
78
136
  if (!isTTY) {
79
137
  await consumeAllLines();
80
- return (pipedLines[promptIndex++] || "league_data").trim();
138
+ return { backend, uri: (pipedLines[promptIndex++] || DEFAULT_MONGODB_URI || "").trim() };
81
139
  }
82
- return (await prompt("Collection/sheet name for this league's data (e.g. ipl_2025_office_league): ")).trim() || "league_data";
140
+ const answer = (await prompt("MongoDB URI (press Enter for default): ")).trim();
141
+ return { backend, uri: answer || DEFAULT_MONGODB_URI || "" };
83
142
  }
84
143
 
85
- export async function getLeagueName() {
144
+ export async function getLeagueUrl() {
86
145
  if (!isTTY) {
87
146
  await consumeAllLines();
88
- return (pipedLines[promptIndex++] || "My IPL League").trim();
147
+ return (pipedLines[promptIndex++] || "").trim();
89
148
  }
90
- return (await prompt("League name (e.g. Office Fantasy League): ")).trim() || "My IPL League";
149
+ return (await prompt("IPL fantasy league URL: ")).trim();
91
150
  }
92
151
 
93
- export async function getTeams() {
152
+ export async function getLeagueName() {
94
153
  if (!isTTY) {
95
154
  await consumeAllLines();
96
- const count = Math.max(1, parseInt(pipedLines[promptIndex++], 10) || 9);
97
- const teams = [];
98
- for (let i = 0; i < count; i++) {
99
- const line = pipedLines[promptIndex++] || "";
100
- const parts = line.split(",").map((s) => s.trim());
101
- teams.push({ id: i + 1, name: parts[0] || `Team ${i + 1}`, owner: parts[1] || parts[0] || `Team ${i + 1}` });
102
- }
103
- return teams;
104
- }
105
-
106
- const countAnswer = await prompt("How many teams in your league? (default: 9): ");
107
- const count = Math.max(1, parseInt(countAnswer, 10) || 9);
108
-
109
- console.log(`\nEnter ${count} team(s) — at minimum a name, optionally an owner:\n`);
110
- const teams = [];
111
- for (let i = 0; i < count; i++) {
112
- const line = await prompt(` Team ${i + 1} (Name or "Name,Owner"): `);
113
- const parts = line.split(",").map((s) => s.trim());
114
- teams.push({ id: i + 1, name: parts[0] || `Team ${i + 1}`, owner: parts[1] || parts[0] || `Team ${i + 1}` });
155
+ return (pipedLines[promptIndex++] || "my_league").trim();
115
156
  }
116
- return teams;
157
+ return (await prompt("League name (used as collection/table name, e.g. my_office_league): ")).trim() || "my_league";
117
158
  }
118
159
 
119
160
  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
- { mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName, teams, skipInstall = false },
13
+ { conn, leagueUrl, leagueName, teams, skipInstall = false },
14
14
  ) {
15
15
  console.log(`\nCreating project at ${projectPath}...`);
16
16
 
@@ -34,17 +34,14 @@ export async function scaffoldProject(
34
34
  }
35
35
 
36
36
  await writeEnvFile(projectPath, {
37
- mongoUri,
38
- storageBackend,
39
- googleSheets,
37
+ conn,
40
38
  leagueUrl,
41
- collectionName,
42
39
  leagueName,
43
40
  });
44
- await writeTeamData(projectPath, teams);
45
- await writeLeagueData(projectPath, { leagueName, leagueUrl, teams });
41
+ await writeTeamData(projectPath, teams || []);
42
+ await writeLeagueData(projectPath, { leagueName, leagueUrl, teams: teams || [] });
46
43
  await writeMatchPointsPlaceholder(projectPath);
47
- await updatePackageJson(projectPath, storageBackend);
44
+ await updatePackageJson(projectPath, conn.backend);
48
45
 
49
46
  for (const f of [
50
47
  "app/api/ipl/live-snapshot.json",
@@ -62,7 +59,7 @@ export async function scaffoldProject(
62
59
  if (leagueName) {
63
60
  console.log(" Seeding league metadata...");
64
61
  try {
65
- execSync(`npm run seed:league -- "${leagueName}" "${collectionName}"`, {
62
+ execSync(`npm run seed:league -- "${leagueName}" "${leagueName}"`, {
66
63
  cwd: projectPath,
67
64
  stdio: "inherit",
68
65
  });
@@ -81,16 +78,15 @@ export async function scaffoldProject(
81
78
 
82
79
  async function writeEnvFile(
83
80
  projectPath,
84
- { mongoUri, storageBackend, googleSheets, leagueUrl, collectionName, leagueName },
81
+ { conn, leagueUrl, leagueName },
85
82
  ) {
86
83
  const envPath = join(projectPath, ".env");
87
84
  const lines = [
88
- `STORAGE_BACKEND=${storageBackend || "mongodb"}`,
89
- `MONGODB_URI=${mongoUri || ""}`,
90
- `COLLECTION_NAME=${collectionName || "league_data"}`,
91
- `GOOGLE_SHEET_ID=${(googleSheets && googleSheets.sheetId) || ""}`,
92
- `GOOGLE_SERVICE_ACCOUNT_EMAIL=${(googleSheets && googleSheets.serviceAccountEmail) || ""}`,
93
- `GOOGLE_PRIVATE_KEY=${(googleSheets && googleSheets.privateKey) || ""}`,
85
+ `STORAGE_BACKEND=${conn.backend || "mongodb"}`,
86
+ `COLLECTION_NAME=${leagueName || "my_league"}`,
87
+ conn.backend === "google_sheets" ? `GOOGLE_SHEET_ID=${conn.sheetId || ""}` : `MONGODB_URI=${conn.uri || ""}`,
88
+ conn.backend === "google_sheets" ? `GOOGLE_SERVICE_ACCOUNT_EMAIL=${conn.serviceAccountEmail || ""}` : "",
89
+ conn.backend === "google_sheets" ? `GOOGLE_PRIVATE_KEY=${conn.privateKey || ""}` : "",
94
90
  `IPL_LEAGUE_URL=${leagueUrl || ""}`,
95
91
  `IPL_LEAGUE_NAME=${leagueName || ""}`,
96
92
  `IPL_POST_SECRET=`,
@@ -100,7 +96,7 @@ async function writeEnvFile(
100
96
  `IPL_API_LOG_PAYLOAD=0`,
101
97
  `IPL_WRITE_SEED_DATA_FILE=1`,
102
98
  ``,
103
- ];
99
+ ].filter(Boolean);
104
100
  await writeFile(envPath, lines.join("\n"));
105
101
  }
106
102