@vatvaghool/create-ipl-dashboard 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/README.md +75 -0
  2. package/package.json +27 -0
  3. package/src/generate-template.mjs +73 -0
  4. package/src/index.mjs +98 -0
  5. package/src/prompts.mjs +78 -0
  6. package/src/scaffold.mjs +129 -0
  7. package/src/scraper.mjs +79 -0
  8. package/template/.dockerignore +13 -0
  9. package/template/AGENTS.md +5 -0
  10. package/template/Dockerfile.sync +14 -0
  11. package/template/README.md +160 -0
  12. package/template/app/api/ipl/data.ts +24 -0
  13. package/template/app/api/ipl/route.ts +505 -0
  14. package/template/app/api/ipl/transfers/route.ts +261 -0
  15. package/template/app/api/ipl/transfers/transform.ts +156 -0
  16. package/template/app/api/ipl/transform.ts +20 -0
  17. package/template/app/api/ipl/upcoming-matches/route.ts +18 -0
  18. package/template/app/api/ops/status/route.ts +225 -0
  19. package/template/app/components/AIRoasting.tsx +278 -0
  20. package/template/app/components/ColorWave.tsx +193 -0
  21. package/template/app/components/CrownBattle.tsx +207 -0
  22. package/template/app/components/DashboardContent.tsx +377 -0
  23. package/template/app/components/FantasyStockTicker.tsx +192 -0
  24. package/template/app/components/FireworksBurst.tsx +225 -0
  25. package/template/app/components/LiveMatchTicker.tsx +117 -0
  26. package/template/app/components/MatchRecapScroll.tsx +135 -0
  27. package/template/app/components/MatchStoryScrubber.tsx +274 -0
  28. package/template/app/components/PerformanceTracker.tsx +132 -0
  29. package/template/app/components/ProgressGlowRings.tsx +157 -0
  30. package/template/app/components/TeamDNAScanner.tsx +238 -0
  31. package/template/app/components/ThemeToggle.tsx +74 -0
  32. package/template/app/components/dashboard/CaptainBoard.tsx +138 -0
  33. package/template/app/components/dashboard/ChartBoard.tsx +162 -0
  34. package/template/app/components/dashboard/LatestBadge.tsx +23 -0
  35. package/template/app/components/dashboard/LedgerTable.tsx +385 -0
  36. package/template/app/components/dashboard/SectionCard.tsx +59 -0
  37. package/template/app/components/dashboard/StickyMini.tsx +20 -0
  38. package/template/app/components/dashboard/index.ts +6 -0
  39. package/template/app/components/ui/DashboardChartFrame.tsx +74 -0
  40. package/template/app/components/ui/DoodleSpinner.tsx +15 -0
  41. package/template/app/components/ui/TeamPills.tsx +41 -0
  42. package/template/app/data/match-points.ts +3 -0
  43. package/template/app/data/teams.ts +32 -0
  44. package/template/app/globals.css +1267 -0
  45. package/template/app/hooks/dashboard/index.ts +1 -0
  46. package/template/app/hooks/dashboard/useDashboardModel.ts +25 -0
  47. package/template/app/hooks/dashboardCache.ts +53 -0
  48. package/template/app/hooks/dashboardPolling.ts +53 -0
  49. package/template/app/hooks/snapshotCache.ts +47 -0
  50. package/template/app/hooks/useDashboardData.ts +28 -0
  51. package/template/app/layout.tsx +75 -0
  52. package/template/app/lib/aiAgent.ts +444 -0
  53. package/template/app/lib/config.ts +29 -0
  54. package/template/app/lib/dashboard/index.ts +1 -0
  55. package/template/app/lib/dashboard/model.ts +257 -0
  56. package/template/app/lib/dashboardData.ts +50 -0
  57. package/template/app/lib/dashboardView.ts +22 -0
  58. package/template/app/lib/detailedData.ts +112 -0
  59. package/template/app/lib/matchStatus.ts +28 -0
  60. package/template/app/lib/matches.ts +131 -0
  61. package/template/app/lib/teamBadges.ts +223 -0
  62. package/template/app/lib/upcomingMatches.ts +154 -0
  63. package/template/app/lib/useDb.ts +29 -0
  64. package/template/app/lib/utils/diff.ts +24 -0
  65. package/template/app/lib/utils/getChartColor.ts +17 -0
  66. package/template/app/lib/utils/getStdDeviation.ts +6 -0
  67. package/template/app/lib/utils/time.ts +40 -0
  68. package/template/app/lib/utils.ts +70 -0
  69. package/template/app/page.tsx +15 -0
  70. package/template/app/store/dashboardStore.ts +85 -0
  71. package/template/app/types/dashboard.ts +75 -0
  72. package/template/app/types.ts +130 -0
  73. package/template/app/utils/dashboard/index.ts +72 -0
  74. package/template/eslint.config.mjs +18 -0
  75. package/template/infra/cloud-run/README.md +68 -0
  76. package/template/infra/cloud-run/sync-job.yaml +32 -0
  77. package/template/infra/cutover/README.md +84 -0
  78. package/template/infra/vercel/README.md +57 -0
  79. package/template/next.config.ts +7 -0
  80. package/template/package-lock.json +7330 -0
  81. package/template/package.json +47 -0
  82. package/template/packages/ipl-dashboard-utils/README.md +316 -0
  83. package/template/packages/ipl-dashboard-utils/package.json +34 -0
  84. package/template/packages/ipl-dashboard-utils/src/index.ts +22 -0
  85. package/template/packages/ipl-dashboard-utils/src/transform.ts +687 -0
  86. package/template/packages/ipl-dashboard-utils/src/types.ts +88 -0
  87. package/template/packages/ipl-dashboard-utils/tsconfig.build.json +17 -0
  88. package/template/postcss.config.mjs +7 -0
  89. package/template/scripts/capture-ipl-auth.mjs +54 -0
  90. package/template/scripts/deploy-cloud-run-sync.sh +48 -0
  91. package/template/scripts/deploy-cloud-scheduler.sh +42 -0
  92. package/template/scripts/dev-simple.js +31 -0
  93. package/template/scripts/dev-welcome.mjs +38 -0
  94. package/template/scripts/monitor-ops-status.sh +50 -0
  95. package/template/scripts/seed-mongodb.ts +115 -0
  96. package/template/scripts/sync-cloud.mjs +50 -0
  97. package/template/scripts/sync-ipl.mjs +238 -0
  98. package/template/scripts/sync-transfers-daily.mjs +175 -0
  99. package/template/scripts/verify-production.mjs +108 -0
  100. package/template/tests/coverage-gaps.test.ts +290 -0
  101. package/template/tests/dashboard-polling.test.ts +96 -0
  102. package/template/tests/detailed-data.test.ts +60 -0
  103. package/template/tests/ipl-transform.test.ts +590 -0
  104. package/template/tests/transfers-route.test.ts +109 -0
  105. package/template/tests/upcoming-matches.test.ts +34 -0
  106. package/template/tests/utils-and-cache.test.ts +267 -0
  107. package/template/tsconfig.json +35 -0
  108. package/template/vercel.json +7 -0
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # create-ipl-dashboard
2
+
3
+ Scaffold a full-featured IPL fantasy cricket dashboard in seconds.
4
+
5
+ ```bash
6
+ npx create-ipl-dashboard my-league
7
+ ```
8
+
9
+ Follow the prompts to enter your MongoDB URI, fantasy league URL, and team names — then get a ready-to-run Next.js dashboard with standings charts, performance trackers, AI roasts, and more.
10
+
11
+ ## Usage
12
+
13
+ ```bash
14
+ npx create-ipl-dashboard [project-name] [options]
15
+ ```
16
+
17
+ ### Options
18
+
19
+ | Flag | Description |
20
+ |------|-------------|
21
+ | `--help`, `-h` | Show help message |
22
+ | `--scrape` | Auto-detect team names from the league URL |
23
+ | `--skip-install` | Skip `npm install` (useful for testing) |
24
+
25
+ ### Interactive prompts
26
+
27
+ | Prompt | Description |
28
+ |--------|-------------|
29
+ | MongoDB URI | Your MongoDB connection string (or leave blank for file-based fallback) |
30
+ | League URL | The fantasy.iplt20.com league page URL |
31
+ | Teams | Team names (and optional owners) in your league |
32
+
33
+ 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.
34
+
35
+ ## What you get
36
+
37
+ A full Next.js 16 project with:
38
+
39
+ - **Leaderboard dashboard** — live standings with rank movements and point gaps
40
+ - **Performance charts** — Recharts line charts tracking every team's trajectory
41
+ - **Captain board** — captain/vice-captain picks per team
42
+ - **Match scrubber** — interactive timeline through match history
43
+ - **AI roasting** — generated commentary in multiple languages
44
+ - **Stock ticker** — fantasy stocks with sparklines
45
+ - **Live updates** — bookmarklet or Playwright scraper for live sync
46
+ - **MongoDB persistence** — optional, with file-based fallback for dev
47
+
48
+ ## Quick start after scaffold
49
+
50
+ ```bash
51
+ cd my-league
52
+
53
+ # Start the dev server
54
+ npm run dev:simple
55
+
56
+ # Capture auth state for scrapers (one-time setup)
57
+ npm run capture:ipl-auth
58
+
59
+ # Scrape live leaderboard data
60
+ npm run sync:ipl
61
+ ```
62
+
63
+ Open http://localhost:3000 to see your dashboard.
64
+
65
+ ## How it works
66
+
67
+ The CLI:
68
+
69
+ 1. Copies a pre-built Next.js app template
70
+ 2. Writes your `.env` with MongoDB URI and league URL
71
+ 3. Generates `app/data/teams.ts` with your team roster
72
+ 4. Creates a placeholder `app/data/match-points.ts` (auto-populates as you sync)
73
+ 5. Installs dependencies
74
+
75
+ The template includes all dashboard components, API endpoints, scrapers, and tests from the [ipl-dashboard](https://github.com/anomalyco/ipl-dashboard) project.
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@vatvaghool/create-ipl-dashboard",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold an IPL fantasy cricket dashboard project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-ipl-dashboard": "./src/index.mjs"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "template"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "generate:template": "node src/generate-template.mjs"
18
+ },
19
+ "keywords": [
20
+ "ipl",
21
+ "fantasy-cricket",
22
+ "dashboard",
23
+ "scaffold",
24
+ "create-app"
25
+ ],
26
+ "license": "MIT"
27
+ }
@@ -0,0 +1,73 @@
1
+ import { cp, readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join, dirname, relative } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const PACKAGE_ROOT = join(__dirname, "..");
8
+ const TEMPLATE_DIR = join(PACKAGE_ROOT, "template");
9
+ const MONOREPO_ROOT = join(PACKAGE_ROOT, "..", "..");
10
+
11
+ const SKIP_PATTERNS = [
12
+ "node_modules",
13
+ ".next",
14
+ ".git",
15
+ "dist",
16
+ "packages/create-ipl-dashboard",
17
+ "ipl-auth.json",
18
+ ".env",
19
+ ".env.example",
20
+ "live-snapshot.json",
21
+ "packages/ipl-dashboard-utils/dist",
22
+ "next-env.d.ts",
23
+ "tsconfig.tsbuildinfo",
24
+ ];
25
+
26
+
27
+ async function getAllFiles(dir) {
28
+ const entries = [];
29
+ const { readdir, stat } = await import("node:fs/promises");
30
+ async function walk(current) {
31
+ const items = await readdir(current, { withFileTypes: true });
32
+ for (const item of items) {
33
+ const full = join(current, item.name);
34
+ const rel = relative(MONOREPO_ROOT, full);
35
+ if (SKIP_PATTERNS.some((p) => rel === p || rel.startsWith(p + "/") || item.name === p)) continue;
36
+ if (item.name.startsWith(".") && item.name !== ".env.example" && item.name !== ".dockerignore" && item.name !== ".gitignore") continue;
37
+ if (item.isDirectory()) {
38
+ await walk(full);
39
+ } else {
40
+ entries.push(rel);
41
+ }
42
+ }
43
+ }
44
+ await walk(dir);
45
+ return entries;
46
+ }
47
+
48
+ async function generateTemplate() {
49
+ console.log("Generating template from monorepo...");
50
+
51
+ if (existsSync(TEMPLATE_DIR)) {
52
+ const { rm } = await import("node:fs/promises");
53
+ await rm(TEMPLATE_DIR, { recursive: true, force: true });
54
+ }
55
+
56
+ const files = await getAllFiles(MONOREPO_ROOT);
57
+ for (const file of files) {
58
+ const src = join(MONOREPO_ROOT, file);
59
+ const dest = join(TEMPLATE_DIR, file);
60
+ await mkdir(dirname(dest), { recursive: true });
61
+ await cp(src, dest);
62
+ console.log(` ${file}`);
63
+ }
64
+
65
+ // Write a note about generated files
66
+ console.log("\nTemplate generated at:", TEMPLATE_DIR);
67
+ console.log(`Files: ${files.length}`);
68
+ }
69
+
70
+ generateTemplate().catch((err) => {
71
+ console.error("Failed to generate template:", err);
72
+ process.exit(1);
73
+ });
package/src/index.mjs ADDED
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { join } from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ import { mkdir } from "node:fs/promises";
6
+ import { getProjectName, getMongoUri, getLeagueUrl, getTeams, close } from "./prompts.mjs";
7
+ import { scaffoldProject } from "./scaffold.mjs";
8
+ import { scrapeTeamsFromUrl } from "./scraper.mjs";
9
+
10
+ function printHelp(exitCode) {
11
+ console.log(`
12
+ create-ipl-dashboard [project-name] [options]
13
+
14
+ Scaffolds a new IPL fantasy cricket dashboard project.
15
+
16
+ Arguments:
17
+ project-name Directory name to create (default: prompted)
18
+
19
+ Options:
20
+ --help, -h Show this help message
21
+ --skip-install Skip npm install after scaffolding
22
+ --scrape Auto-detect team names from the league URL
23
+
24
+ Examples:
25
+ npx create-ipl-dashboard my-league
26
+ npx create-ipl-dashboard my-league --scrape
27
+ npx create-ipl-dashboard --skip-install
28
+ `);
29
+ process.exit(exitCode);
30
+ }
31
+
32
+ async function main() {
33
+ const args = process.argv.slice(2);
34
+ const flags = { scrape: false, skipInstall: false, help: false };
35
+ const positional = [];
36
+
37
+ for (const arg of args) {
38
+ if (arg === "--help" || arg === "-h") flags.help = true;
39
+ else if (arg === "--scrape") flags.scrape = true;
40
+ else if (arg === "--skip-install") flags.skipInstall = true;
41
+ else if (!arg.startsWith("--")) positional.push(arg);
42
+ else { console.error(`Unknown flag: ${arg}`); printHelp(1); }
43
+ }
44
+
45
+ if (flags.help) printHelp(0);
46
+
47
+ console.log("");
48
+ console.log(" 🏏 IPL Fantasy Dashboard Scaffold");
49
+ console.log("");
50
+
51
+ const projectName = await getProjectName(positional);
52
+ const projectPath = join(process.cwd(), projectName);
53
+
54
+ if (existsSync(projectPath)) {
55
+ const { rm } = await import("node:fs/promises");
56
+ console.log(` Directory "${projectName}" already exists. Removing...`);
57
+ await rm(projectPath, { recursive: true, force: true });
58
+ }
59
+
60
+ await mkdir(projectPath, { recursive: true });
61
+
62
+ const mongoUri = await getMongoUri();
63
+ const leagueUrl = await getLeagueUrl();
64
+ let teams;
65
+
66
+ if (flags.scrape && leagueUrl) {
67
+ console.log(" Attempting to scrape team names from league URL...");
68
+ teams = await scrapeTeamsFromUrl(leagueUrl);
69
+ if (teams && teams.length > 0) {
70
+ console.log(` Found ${teams.length} team(s): ${teams.map((t) => t.name).join(", ")}`);
71
+ } else {
72
+ console.log(" Could not auto-detect teams. Enter them manually:");
73
+ teams = await getTeams();
74
+ }
75
+ } else {
76
+ teams = await getTeams();
77
+ }
78
+
79
+ close();
80
+
81
+ await scaffoldProject(projectPath, { mongoUri, leagueUrl, teams, skipInstall: flags.skipInstall });
82
+
83
+ console.log("");
84
+ console.log(" Next steps:");
85
+ console.log(` cd ${projectName}`);
86
+ console.log(" npm run dev:simple");
87
+ console.log("");
88
+ console.log(" For live data sync, capture auth state:");
89
+ console.log(` cd ${projectName}`);
90
+ console.log(" npm run capture:ipl-auth");
91
+ console.log(" npm run sync:ipl");
92
+ console.log("");
93
+ }
94
+
95
+ main().catch((err) => {
96
+ console.error("Scaffold failed:", err);
97
+ process.exit(1);
98
+ });
@@ -0,0 +1,78 @@
1
+ import { createInterface } from "node:readline";
2
+ import { stdin as input, stdout as output, stdin } from "node:process";
3
+
4
+ const isTTY = stdin.isTTY;
5
+
6
+ let promptIndex = 0;
7
+ let pipedLines = [];
8
+
9
+ async function consumeAllLines() {
10
+ if (pipedLines.length > 0) return;
11
+ const rl = createInterface({ input });
12
+ for await (const line of rl) {
13
+ pipedLines.push(line);
14
+ }
15
+ }
16
+
17
+ function prompt(query) {
18
+ return new Promise((resolve) => {
19
+ const rl = createInterface({ input, output });
20
+ rl.question(query, (answer) => {
21
+ rl.close();
22
+ resolve(answer);
23
+ });
24
+ });
25
+ }
26
+
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
+ }
35
+
36
+ export async function getMongoUri() {
37
+ if (!isTTY) {
38
+ await consumeAllLines();
39
+ return (pipedLines[promptIndex++] || "").trim();
40
+ }
41
+ return (await prompt("MongoDB URI (or press Enter to skip, can be set later): ")).trim();
42
+ }
43
+
44
+ export async function getLeagueUrl() {
45
+ if (!isTTY) {
46
+ await consumeAllLines();
47
+ return (pipedLines[promptIndex++] || "").trim();
48
+ }
49
+ return (await prompt("IPL fantasy league URL (or press Enter to skip, can be set later): ")).trim();
50
+ }
51
+
52
+ export async function getTeams() {
53
+ if (!isTTY) {
54
+ await consumeAllLines();
55
+ const count = Math.max(1, parseInt(pipedLines[promptIndex++], 10) || 9);
56
+ const teams = [];
57
+ for (let i = 0; i < count; i++) {
58
+ const line = pipedLines[promptIndex++] || "";
59
+ const parts = line.split(",").map((s) => s.trim());
60
+ teams.push({ id: i + 1, name: parts[0] || `Team ${i + 1}`, owner: parts[1] || parts[0] || `Team ${i + 1}` });
61
+ }
62
+ return teams;
63
+ }
64
+
65
+ const countAnswer = await prompt("How many teams in your league? (default: 9): ");
66
+ const count = Math.max(1, parseInt(countAnswer, 10) || 9);
67
+
68
+ console.log(`\nEnter ${count} team(s) — at minimum a name, optionally an owner:\n`);
69
+ const teams = [];
70
+ for (let i = 0; i < count; i++) {
71
+ const line = await prompt(` Team ${i + 1} (Name or "Name,Owner"): `);
72
+ const parts = line.split(",").map((s) => s.trim());
73
+ teams.push({ id: i + 1, name: parts[0] || `Team ${i + 1}`, owner: parts[1] || parts[0] || `Team ${i + 1}` });
74
+ }
75
+ return teams;
76
+ }
77
+
78
+ export function close() {}
@@ -0,0 +1,129 @@
1
+ import { mkdir, writeFile, cp, readFile, rm } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { execSync } from "node:child_process";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const PACKAGE_ROOT = join(__dirname, "..");
9
+ const TEMPLATE_DIR = join(PACKAGE_ROOT, "template");
10
+
11
+ export async function scaffoldProject(projectPath, { mongoUri, leagueUrl, teams, skipInstall = false }) {
12
+ console.log(`\nCreating project at ${projectPath}...`);
13
+
14
+ await mkdir(projectPath, { recursive: true });
15
+
16
+ if (existsSync(TEMPLATE_DIR)) {
17
+ await cp(TEMPLATE_DIR, projectPath, { recursive: true });
18
+ } else {
19
+ console.log(" (no template directory found, generating from scratch)");
20
+ }
21
+
22
+ // Remove monorepo-specific files not needed in scaffolded project
23
+ for (const f of ["AGENTS.md", "tsconfig.tsbuildinfo", "tsconfig.buildinfo", "package-lock.json"]) {
24
+ try { await rm(join(projectPath, f)); } catch {}
25
+ }
26
+
27
+ await writeEnvFile(projectPath, { mongoUri, leagueUrl });
28
+ await writeTeamData(projectPath, teams);
29
+ await writeMatchPointsPlaceholder(projectPath);
30
+ await updatePackageJson(projectPath);
31
+
32
+ // Remove monorepo's live snapshots (they're specific to original league)
33
+ for (const f of ["app/api/ipl/live-snapshot.json", "app/api/ipl/transfers/live-snapshot.json"]) {
34
+ try { await rm(join(projectPath, f)); } catch {}
35
+ }
36
+
37
+ if (!skipInstall) {
38
+ console.log(" Running npm install...");
39
+ execSync("npm install", { cwd: projectPath, stdio: "inherit" });
40
+ } else {
41
+ console.log(" Skipping npm install (run manually: cd <project> && npm install)");
42
+ }
43
+
44
+ console.log("\n Done! Your project is ready at:", projectPath);
45
+ }
46
+
47
+ async function writeEnvFile(projectPath, { mongoUri, leagueUrl }) {
48
+ const envPath = join(projectPath, ".env");
49
+ const lines = [
50
+ `MONGODB_URI=${mongoUri || ""}`,
51
+ `IPL_LEAGUE_URL=${leagueUrl || ""}`,
52
+ `IPL_POST_SECRET=`,
53
+ ``,
54
+ `IPL_API_BASE_URL=http://localhost:3000`,
55
+ `IPL_API_LOG=1`,
56
+ `IPL_API_LOG_PAYLOAD=0`,
57
+ `IPL_WRITE_SEED_DATA_FILE=1`,
58
+ ``,
59
+ ];
60
+ await writeFile(envPath, lines.join("\n"));
61
+ }
62
+
63
+ async function writeTeamData(projectPath, teams) {
64
+ const dir = join(projectPath, "app/data");
65
+ await mkdir(dir, { recursive: true });
66
+
67
+ const teamEntries = teams
68
+ .map(
69
+ (t) =>
70
+ ` { id: ${t.id}, name: "${t.name}", owner: "${t.owner}", fantasyNames: ["${t.name}"] }`,
71
+ )
72
+ .join(",\n");
73
+
74
+ const content = `export const TEAMS: { id: number; name: string; owner: string; fantasyNames: string[] }[] = [
75
+ ${teamEntries},
76
+ ];
77
+
78
+ const byAlias = new Map<string, number>();
79
+ for (const team of TEAMS) {
80
+ for (const alias of team.fantasyNames) {
81
+ byAlias.set(alias.toLowerCase().replace(/[^a-z0-9]/g, ""), team.id);
82
+ }
83
+ }
84
+
85
+ export const resolveTeamId = (name: string): number | undefined => {
86
+ const key = name.toLowerCase().replace(/[^a-z0-9]/g, "");
87
+ return byAlias.get(key);
88
+ };
89
+
90
+ export const TEAM_ALIAS_MAP: Record<string, string> = {};
91
+ for (const team of TEAMS) {
92
+ for (const alias of team.fantasyNames) {
93
+ if (alias !== team.name) {
94
+ TEAM_ALIAS_MAP[alias] = team.name;
95
+ }
96
+ }
97
+ }
98
+ `;
99
+
100
+ await writeFile(join(dir, "teams.ts"), content);
101
+ }
102
+
103
+ async function writeMatchPointsPlaceholder(projectPath) {
104
+ const dir = join(projectPath, "app/data");
105
+ await mkdir(dir, { recursive: true });
106
+ const content = `export const MATCH_POINTS: { teamId: number; matchId: number; points: number }[] = [];
107
+
108
+ // TODO: Populate with per-match point data.
109
+ // The bookmarklet and Playwright scraper will auto-accumulate match data
110
+ // as you sync live snapshots. You can also manually add rows here.
111
+ //
112
+ // Format:
113
+ // { teamId: 1, matchId: 1, points: 727 },
114
+ // { teamId: 2, matchId: 1, points: 650 },
115
+ `;
116
+ await writeFile(join(dir, "match-points.ts"), content);
117
+ }
118
+
119
+ async function updatePackageJson(projectPath) {
120
+ const pkgPath = join(projectPath, "package.json");
121
+ try {
122
+ const raw = await readFile(pkgPath, "utf8");
123
+ const pkg = JSON.parse(raw);
124
+ pkg.name = "ipl-dashboard";
125
+ pkg.private = false;
126
+ delete pkg.scripts?.["generate:template"];
127
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
128
+ } catch {}
129
+ }
@@ -0,0 +1,79 @@
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
+ }
@@ -0,0 +1,13 @@
1
+ .git
2
+ .github
3
+ .next
4
+ node_modules
5
+ coverage
6
+ build
7
+ .vercel
8
+ .env*
9
+ ipl-auth.json
10
+ README.md
11
+ app
12
+ packages
13
+ tests
@@ -0,0 +1,5 @@
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
@@ -0,0 +1,14 @@
1
+ FROM mcr.microsoft.com/playwright:v1.54.2-noble
2
+
3
+ WORKDIR /app
4
+
5
+ COPY package.json package-lock.json ./
6
+ RUN npm ci
7
+ RUN npm install --no-save playwright@1.54.2
8
+
9
+ COPY scripts ./scripts
10
+
11
+ ENV NODE_ENV=production
12
+ ENV IPL_HEADLESS=1
13
+
14
+ CMD ["npm", "run", "sync:cloud"]