@vatvaghool/create-ipl-dashboard 0.1.18 → 0.1.20

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
@@ -6,7 +6,7 @@ Scaffold a full-featured IPL fantasy cricket dashboard in seconds.
6
6
  npx @vatvaghool/create-ipl-dashboard my-league
7
7
  ```
8
8
 
9
- Follow the prompts to enter your MongoDB URI, fantasy league URL, collection name, and team names — you get a ready-to-run Next.js dashboard with standings charts, performance trackers, AI roasts, and more.
9
+ Follow the prompts to enter your MongoDB URI, fantasy league URL, and league name — you get a ready-to-run Next.js dashboard with standings charts, performance trackers, AI roasts, and more.
10
10
 
11
11
  ## Usage
12
12
 
@@ -27,16 +27,12 @@ npx @vatvaghool/create-ipl-dashboard [project-name] [options]
27
27
  | Prompt | Description |
28
28
  |--------|-------------|
29
29
  | Project name | Directory to scaffold into |
30
- | Storage backend | `mongodb` (default) or `google_sheets` |
31
- | MongoDB URI | Your MongoDB connection string (only if MongoDB chosen, press Enter for default) |
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) |
30
+ | MongoDB URI | Your MongoDB connection string (press Enter for default) |
35
31
  | 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 |
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)` (press Enter for defaults) |
38
34
 
39
- Set team names later via `app/data/teams.ts` or use `--scrape` to auto-detect from the league URL during scaffolding.
35
+ Use `--scrape` to auto-detect from the league URL, or press Enter at the team names prompt for default placeholder teams. You can always edit `app/data/teams.ts` later.
40
36
 
41
37
  ## Screenshots
42
38
 
@@ -75,7 +71,7 @@ A full Next.js 16 project with:
75
71
  - **AI roasting** — generated commentary in multiple languages
76
72
  - **Stock ticker** — fantasy stocks with sparklines
77
73
  - **Live updates** — bookmarklet or Playwright scraper for live sync
78
- - **MongoDB persistence** — auto-configured database connection
74
+ - **MongoDB persistence** — auto-configured database connection with collection per league
79
75
 
80
76
  ## Quick start after scaffold
81
77
 
@@ -115,13 +111,13 @@ The CLI:
115
111
 
116
112
  1. Copies a pre-built Next.js app template
117
113
  2. Writes your `.env` with the `MONGODB_URI`, `COLLECTION_NAME`, league URL, and league name
118
- 3. Generates `app/data/teams.ts` with your team roster
114
+ 3. Generates `app/data/teams.ts` with your team roster (if `--scrape` was used)
119
115
  4. Generates `app/data/league.ts` with league metadata
120
116
  5. Creates a placeholder `app/data/match-points.ts` (auto-populates as you sync)
121
117
  6. Installs dependencies
122
118
  7. Runs `seed:league` to create a **document in your specified collection** in the pre-configured database, storing league metadata (name, URL, teams, timestamps)
123
119
 
124
- Each league gets its own collection — run `create-ipl-dashboard` again with a different collection name to add another league.
120
+ Each league gets its own collection — run `create-ipl-dashboard` again with a different league name to add another league.
125
121
 
126
122
  The template includes all dashboard components, API endpoints, scrapers, and tests from the [ipl-dashboard](https://github.com/anomalyco/ipl-dashboard) project.
127
123
 
@@ -135,13 +131,11 @@ The template includes all dashboard components, API endpoints, scrapers, and tes
135
131
  npx @vatvaghool/create-ipl-dashboard another-league
136
132
  ```
137
133
 
138
- Provide a different league name and the new league will be stored in its own collection/sheet — data stays fully isolated.
134
+ Provide a different league name and the new league will be stored in its own collection — data stays fully isolated.
139
135
 
140
136
  ### Viewing seeded data
141
137
 
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.
138
+ Connect with any MongoDB client. Each league appears as a separate collection with `type: "league"`.
145
139
 
146
140
  ### Production deployment
147
141
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vatvaghool/create-ipl-dashboard",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Scaffold an IPL fantasy cricket dashboard project",
5
5
  "type": "module",
6
6
  "bin": {
package/src/index.mjs CHANGED
@@ -3,7 +3,7 @@
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, getConnectionUrl, getLeagueUrl, getLeagueName, close } from "./prompts.mjs";
6
+ import { getProjectName, getMongoUri, getLeagueUrl, getLeagueName, getTeams, close } from "./prompts.mjs";
7
7
  import { scaffoldProject } from "./scaffold.mjs";
8
8
  import { scrapeTeamsFromUrl } from "./scraper.mjs";
9
9
 
@@ -59,11 +59,11 @@ async function main() {
59
59
 
60
60
  await mkdir(projectPath, { recursive: true });
61
61
 
62
- const conn = await getConnectionUrl();
62
+ const mongoUri = await getMongoUri();
63
63
  const leagueUrl = await getLeagueUrl();
64
64
  const leagueName = await getLeagueName();
65
- let teams;
66
65
 
66
+ let teams;
67
67
  if (flags.scrape && leagueUrl) {
68
68
  console.log(" Attempting to scrape team names from league URL...");
69
69
  teams = await scrapeTeamsFromUrl(leagueUrl);
@@ -72,9 +72,26 @@ async function main() {
72
72
  }
73
73
  }
74
74
 
75
+ if (!teams) {
76
+ teams = await getTeams();
77
+ }
78
+
79
+ if (!teams || teams.length === 0) {
80
+ teams = [
81
+ { id: 1, name: "The Cricketers", owner: "Player 1" },
82
+ { id: 2, name: "Six Hitters", owner: "Player 2" },
83
+ { id: 3, name: "Bowling Titans", owner: "Player 3" },
84
+ { id: 4, name: "Fielding Masters", owner: "Player 4" },
85
+ { id: 5, name: "Night Watchmen", owner: "Player 5" },
86
+ { id: 6, name: "Super Kings", owner: "Player 6" },
87
+ { id: 7, name: "Royal Challengers", owner: "Player 7" },
88
+ { id: 8, name: "Mumbai Indians", owner: "Player 8" },
89
+ ];
90
+ }
91
+
75
92
  close();
76
93
 
77
- await scaffoldProject(projectPath, { conn, leagueUrl, leagueName, teams, skipInstall: flags.skipInstall });
94
+ await scaffoldProject(projectPath, { mongoUri, leagueUrl, leagueName, teams, skipInstall: flags.skipInstall });
78
95
 
79
96
  console.log("");
80
97
  console.log(" Next steps:");
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,31 +33,15 @@ export async function getProjectName(args) {
114
33
  return (await prompt("Project name: ")).trim() || "ipl-dashboard";
115
34
  }
116
35
 
117
- export async function getStorageBackend() {
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
- }
36
+ const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
134
37
 
135
- const DEFAULT_MONGODB_URI = "mongodb+srv://admin:admin@cricket.ptjjrub.mongodb.net/?appName=cricket";
38
+ export async function getMongoUri() {
136
39
  if (!isTTY) {
137
40
  await consumeAllLines();
138
- return { backend, uri: (pipedLines[promptIndex++] || DEFAULT_MONGODB_URI || "").trim() };
41
+ return (pipedLines[promptIndex++] || DEFAULT_MONGODB_URI || "").trim();
139
42
  }
140
43
  const answer = (await prompt("MongoDB URI (press Enter for default): ")).trim();
141
- return { backend, uri: answer || DEFAULT_MONGODB_URI || "" };
44
+ return answer || DEFAULT_MONGODB_URI || "";
142
45
  }
143
46
 
144
47
  export async function getLeagueUrl() {
@@ -154,7 +57,31 @@ export async function getLeagueName() {
154
57
  await consumeAllLines();
155
58
  return (pipedLines[promptIndex++] || "my_league").trim();
156
59
  }
157
- return (await prompt("League name (used as collection/table name, e.g. my_office_league): ")).trim() || "my_league";
60
+ return (await prompt("League name (used as collection name, e.g. my_office_league): ")).trim() || "my_league";
61
+ }
62
+
63
+ function parseTeams(input) {
64
+ return input.split(",").map((part, i) => {
65
+ part = part.trim();
66
+ if (!part) return null;
67
+ const match = part.match(/^(.+?)\s*\(([^)]+)\)$/);
68
+ if (match) {
69
+ return { id: i + 1, name: match[1].trim(), owner: match[2].trim() };
70
+ }
71
+ return { id: i + 1, name: part, owner: part };
72
+ }).filter(Boolean);
73
+ }
74
+
75
+ export async function getTeams() {
76
+ if (!isTTY) {
77
+ await consumeAllLines();
78
+ const raw = (pipedLines[promptIndex++] || "").trim();
79
+ if (!raw) return null;
80
+ return parseTeams(raw);
81
+ }
82
+ const answer = (await prompt("Team names (comma-separated, or press Enter for defaults):\n e.g. Team A (Alice), Team B (Bob)\n> ")).trim();
83
+ if (!answer) return null;
84
+ return parseTeams(answer);
158
85
  }
159
86
 
160
87
  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
- { conn, leagueUrl, leagueName, teams, skipInstall = false },
13
+ { mongoUri, leagueUrl, leagueName, teams, skipInstall = false },
14
14
  ) {
15
15
  console.log(`\nCreating project at ${projectPath}...`);
16
16
 
@@ -34,14 +34,14 @@ export async function scaffoldProject(
34
34
  }
35
35
 
36
36
  await writeEnvFile(projectPath, {
37
- conn,
37
+ mongoUri,
38
38
  leagueUrl,
39
39
  leagueName,
40
40
  });
41
41
  await writeTeamData(projectPath, teams || []);
42
42
  await writeLeagueData(projectPath, { leagueName, leagueUrl, teams: teams || [] });
43
43
  await writeMatchPointsPlaceholder(projectPath);
44
- await updatePackageJson(projectPath, conn.backend);
44
+ await updatePackageJson(projectPath);
45
45
 
46
46
  for (const f of [
47
47
  "app/api/ipl/live-snapshot.json",
@@ -78,15 +78,12 @@ export async function scaffoldProject(
78
78
 
79
79
  async function writeEnvFile(
80
80
  projectPath,
81
- { conn, leagueUrl, leagueName },
81
+ { mongoUri, leagueUrl, leagueName },
82
82
  ) {
83
83
  const envPath = join(projectPath, ".env");
84
84
  const lines = [
85
- `STORAGE_BACKEND=${conn.backend || "mongodb"}`,
86
85
  `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 || ""}` : "",
86
+ `MONGODB_URI=${mongoUri || ""}`,
90
87
  `IPL_LEAGUE_URL=${leagueUrl || ""}`,
91
88
  `IPL_LEAGUE_NAME=${leagueName || ""}`,
92
89
  `IPL_POST_SECRET=`,
@@ -96,7 +93,7 @@ async function writeEnvFile(
96
93
  `IPL_API_LOG_PAYLOAD=0`,
97
94
  `IPL_WRITE_SEED_DATA_FILE=1`,
98
95
  ``,
99
- ].filter(Boolean);
96
+ ];
100
97
  await writeFile(envPath, lines.join("\n"));
101
98
  }
102
99
 
@@ -182,7 +179,7 @@ async function writeMatchPointsPlaceholder(projectPath) {
182
179
  await writeFile(join(dir, "match-points.ts"), content);
183
180
  }
184
181
 
185
- async function updatePackageJson(projectPath, storageBackend) {
182
+ async function updatePackageJson(projectPath) {
186
183
  const pkgPath = join(projectPath, "package.json");
187
184
  try {
188
185
  const raw = await readFile(pkgPath, "utf8");
@@ -191,10 +188,6 @@ async function updatePackageJson(projectPath, storageBackend) {
191
188
  pkg.private = false;
192
189
  delete pkg.scripts?.["generate:template"];
193
190
  pkg.scripts["seed:league"] = "node scripts/seed-league.mjs";
194
- if (storageBackend === "google_sheets" && !pkg.dependencies.googleapis) {
195
- pkg.dependencies = pkg.dependencies || {};
196
- pkg.dependencies.googleapis = "^140.0.0";
197
- }
198
191
  await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
199
192
  } catch {}
200
193
  }
@@ -129,9 +129,9 @@ Open http://localhost:3000
129
129
 
130
130
  ```
131
131
  GET /api/ipl resolution order:
132
- 1. Storage raw users (if configured — MongoDB or Google Sheets)
132
+ 1. MongoDB raw users (if MONGODB_URI configured)
133
133
  2. Fallback: local seed data (app/api/ipl/data.ts)
134
- 3. Storage live snapshot (if configured)
134
+ 3. MongoDB live snapshot (if MONGODB_URI configured)
135
135
  4. Fallback: local snapshot file (app/api/ipl/live-snapshot.json)
136
136
  ```
137
137
 
@@ -143,16 +143,12 @@ All hardcoded values are centralized in `app/lib/config.ts` and overridable via
143
143
 
144
144
  | Variable | Default | Description |
145
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) |
146
+ | `MONGODB_URI` | - | MongoDB connection string |
147
+ | `LEAGUE_NAME` | - | League name (collection name) |
152
148
  | `IPL_LEAGUE_URL` | fantasy.iplt20.com/... | Fantasy league page URL |
153
149
  | `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) |
150
+ | `IPL_DB_NAME` | `ipl` | MongoDB database name |
151
+ | `IPL_COLLECTION_NAME` | `ipl` | MongoDB collection name |
156
152
  | `IPL_TOTAL_TRANSFERS` | `160` | Total transfers per season |
157
153
  | `IPL_SYNC_INTERVAL_MS` | `120000` | Dashboard polling interval |
158
154
  | `IPL_DASHBOARD_STALE_MINUTES` | `20` (prod) / `180` (dev) | Staleness threshold |
@@ -184,12 +180,8 @@ Set these environment variables in your Vercel project dashboard:
184
180
 
185
181
  | Variable | Required | Description |
186
182
  |----------|----------|-------------|
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 |
183
+ | `MONGODB_URI` | Yes | MongoDB connection string |
184
+ | `LEAGUE_NAME` | Yes | MongoDB collection name |
193
185
  | `IPL_POST_SECRET` | Yes | Bearer token protecting write endpoints |
194
186
  | `IPL_LEAGUE_URL` | Yes | Your fantasy league page URL |
195
187
  | `IPL_DASHBOARD_STALE_MINUTES` | No | Staleness threshold for health checks (default: `20`) |
@@ -215,7 +207,7 @@ The app runs as a standard Node.js server on `process.env.PORT` (default `3000`)
215
207
 
216
208
  ### Production requirements
217
209
 
218
- - **Storage** (MongoDB or Google Sheets) is required in production for POST endpoints (returns `503` without it)
210
+ - **MongoDB** is required in production for POST endpoints (returns `503` without it)
219
211
  - **IPL_POST_SECRET** should be a strong random value set in your hosting environment
220
212
  - The scraper should run outside the web app (Cloud Run, GitHub Actions, cron) — never in the same process
221
213
 
@@ -229,6 +221,6 @@ The app runs as a standard Node.js server on `process.env.PORT` (default `3000`)
229
221
  - **Tailwind CSS** 4
230
222
  - **Recharts** — charts
231
223
  - **Framer Motion** — animations
232
- - **MongoDB** or **Google Sheets** — data persistence
224
+ - **MongoDB** — data persistence
233
225
  - **Zustand** — client state
234
226
  - **Playwright** — scraper automation
@@ -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,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
 
@@ -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.");
@@ -1,147 +0,0 @@
1
- import type { IStorage, StorageResult } from "./types";
2
-
3
- const SHEET_ID = () => process.env.GOOGLE_SHEET_ID || "";
4
- const SHEET_NAMES: Record<string, string> = {
5
- dashboard: "dashboard",
6
- "raw-users": "raw_users",
7
- "transfer-stats": "transfers",
8
- league: "league",
9
- };
10
-
11
- function sheetNameFor(type: string): string {
12
- return SHEET_NAMES[type] || type.replace(/[^a-z0-9_]/g, "_");
13
- }
14
-
15
- let sheetsClient: any = null;
16
-
17
- async function getSheetsClient() {
18
- if (sheetsClient) return sheetsClient;
19
- try {
20
- const { google } = await import("googleapis");
21
- const auth = new google.auth.GoogleAuth({
22
- credentials: {
23
- client_email: process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL,
24
- private_key: process.env.GOOGLE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
25
- },
26
- scopes: ["https://www.googleapis.com/auth/spreadsheets"],
27
- });
28
- sheetsClient = google.sheets({ version: "v4", auth });
29
- return sheetsClient;
30
- } catch (error) {
31
- console.error("Failed to create Google Sheets client:", error);
32
- return null;
33
- }
34
- }
35
-
36
- export class GoogleSheetsStorage implements IStorage {
37
- isConfigured(): boolean {
38
- return Boolean(process.env.GOOGLE_SHEET_ID && process.env.GOOGLE_SERVICE_ACCOUNT_EMAIL && process.env.GOOGLE_PRIVATE_KEY);
39
- }
40
-
41
- async isConnected(): Promise<boolean> {
42
- const client = await getSheetsClient();
43
- return client !== null;
44
- }
45
-
46
- private async ensureSheet(sheetName: string): Promise<void> {
47
- const client = await getSheetsClient();
48
- if (!client) return;
49
- try {
50
- const res = await client.spreadsheets.get({ spreadsheetId: SHEET_ID() });
51
- const sheets: any[] = res.data.sheets || [];
52
- if (sheets.some((s: any) => s.properties?.title === sheetName)) return;
53
- await client.spreadsheets.batchUpdate({
54
- spreadsheetId: SHEET_ID(),
55
- requestBody: {
56
- requests: [{ addSheet: { properties: { title: sheetName } } }],
57
- },
58
- });
59
- } catch {
60
- // Sheet already exists or can't be created
61
- }
62
- }
63
-
64
- async readDocument(type: string): Promise<Record<string, unknown> | null> {
65
- const client = await getSheetsClient();
66
- if (!client) return null;
67
- const sheetName = sheetNameFor(type);
68
- try {
69
- const res = await client.spreadsheets.values.get({
70
- spreadsheetId: SHEET_ID(),
71
- range: `${sheetName}!A:C`,
72
- });
73
- const rows: string[][] = res.data.values || [];
74
- const dataRow = rows.find((r) => r[0] === type);
75
- if (dataRow?.[1]) {
76
- return { type, ...JSON.parse(dataRow[1]), updatedAt: dataRow[2] || new Date().toISOString() };
77
- }
78
- return null;
79
- } catch {
80
- return null;
81
- }
82
- }
83
-
84
- async upsertDocument(
85
- type: string,
86
- data: Record<string, unknown>,
87
- _filter?: Record<string, unknown>,
88
- ): Promise<StorageResult> {
89
- const client = await getSheetsClient();
90
- if (!client) return { configured: true, stored: false, status: "no_connection" };
91
- const sheetName = sheetNameFor(type);
92
- await this.ensureSheet(sheetName);
93
- try {
94
- const res = await client.spreadsheets.values.get({
95
- spreadsheetId: SHEET_ID(),
96
- range: `${sheetName}!A:C`,
97
- });
98
- const rows: string[][] = res.data.values || [];
99
- const rowIndex = rows.findIndex((r) => r[0] === type);
100
- const jsonData = JSON.stringify(data);
101
- const updatedAt = new Date().toISOString();
102
- if (rowIndex >= 0) {
103
- await client.spreadsheets.values.update({
104
- spreadsheetId: SHEET_ID(),
105
- range: `${sheetName}!A${rowIndex + 1}:C${rowIndex + 1}`,
106
- valueInputOption: "RAW",
107
- requestBody: { values: [[type, jsonData, updatedAt]] },
108
- });
109
- } else {
110
- await client.spreadsheets.values.append({
111
- spreadsheetId: SHEET_ID(),
112
- range: `${sheetName}!A:C`,
113
- valueInputOption: "RAW",
114
- requestBody: { values: [[type, jsonData, updatedAt]] },
115
- });
116
- }
117
- return { configured: true, stored: true, status: "updated" };
118
- } catch (error) {
119
- console.error(`Failed to write document type "${type}" to Google Sheets:`, error);
120
- return { configured: true, stored: false, status: "failed" };
121
- }
122
- }
123
-
124
- async readDocuments(type: string): Promise<Record<string, unknown>[]> {
125
- const client = await getSheetsClient();
126
- if (!client) return [];
127
- const sheetName = sheetNameFor(type);
128
- try {
129
- const res = await client.spreadsheets.values.get({
130
- spreadsheetId: SHEET_ID(),
131
- range: `${sheetName}!A:C`,
132
- });
133
- const rows: string[][] = res.data.values || [];
134
- return rows
135
- .filter((r) => r[0] && r[0] !== type)
136
- .map((r) => {
137
- try {
138
- return { team: r[0], ...JSON.parse(r[1] || "{}"), updatedAt: r[2] || new Date().toISOString() };
139
- } catch {
140
- return { team: r[0], updatedAt: r[2] || new Date().toISOString() };
141
- }
142
- });
143
- } catch {
144
- return [];
145
- }
146
- }
147
- }