@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.
- package/README.md +75 -0
- package/package.json +27 -0
- package/src/generate-template.mjs +73 -0
- package/src/index.mjs +98 -0
- package/src/prompts.mjs +78 -0
- package/src/scaffold.mjs +129 -0
- package/src/scraper.mjs +79 -0
- package/template/.dockerignore +13 -0
- package/template/AGENTS.md +5 -0
- package/template/Dockerfile.sync +14 -0
- package/template/README.md +160 -0
- package/template/app/api/ipl/data.ts +24 -0
- package/template/app/api/ipl/route.ts +505 -0
- package/template/app/api/ipl/transfers/route.ts +261 -0
- package/template/app/api/ipl/transfers/transform.ts +156 -0
- package/template/app/api/ipl/transform.ts +20 -0
- package/template/app/api/ipl/upcoming-matches/route.ts +18 -0
- package/template/app/api/ops/status/route.ts +225 -0
- package/template/app/components/AIRoasting.tsx +278 -0
- package/template/app/components/ColorWave.tsx +193 -0
- package/template/app/components/CrownBattle.tsx +207 -0
- package/template/app/components/DashboardContent.tsx +377 -0
- package/template/app/components/FantasyStockTicker.tsx +192 -0
- package/template/app/components/FireworksBurst.tsx +225 -0
- package/template/app/components/LiveMatchTicker.tsx +117 -0
- package/template/app/components/MatchRecapScroll.tsx +135 -0
- package/template/app/components/MatchStoryScrubber.tsx +274 -0
- package/template/app/components/PerformanceTracker.tsx +132 -0
- package/template/app/components/ProgressGlowRings.tsx +157 -0
- package/template/app/components/TeamDNAScanner.tsx +238 -0
- package/template/app/components/ThemeToggle.tsx +74 -0
- package/template/app/components/dashboard/CaptainBoard.tsx +138 -0
- package/template/app/components/dashboard/ChartBoard.tsx +162 -0
- package/template/app/components/dashboard/LatestBadge.tsx +23 -0
- package/template/app/components/dashboard/LedgerTable.tsx +385 -0
- package/template/app/components/dashboard/SectionCard.tsx +59 -0
- package/template/app/components/dashboard/StickyMini.tsx +20 -0
- package/template/app/components/dashboard/index.ts +6 -0
- package/template/app/components/ui/DashboardChartFrame.tsx +74 -0
- package/template/app/components/ui/DoodleSpinner.tsx +15 -0
- package/template/app/components/ui/TeamPills.tsx +41 -0
- package/template/app/data/match-points.ts +3 -0
- package/template/app/data/teams.ts +32 -0
- package/template/app/globals.css +1267 -0
- package/template/app/hooks/dashboard/index.ts +1 -0
- package/template/app/hooks/dashboard/useDashboardModel.ts +25 -0
- package/template/app/hooks/dashboardCache.ts +53 -0
- package/template/app/hooks/dashboardPolling.ts +53 -0
- package/template/app/hooks/snapshotCache.ts +47 -0
- package/template/app/hooks/useDashboardData.ts +28 -0
- package/template/app/layout.tsx +75 -0
- package/template/app/lib/aiAgent.ts +444 -0
- package/template/app/lib/config.ts +29 -0
- package/template/app/lib/dashboard/index.ts +1 -0
- package/template/app/lib/dashboard/model.ts +257 -0
- package/template/app/lib/dashboardData.ts +50 -0
- package/template/app/lib/dashboardView.ts +22 -0
- package/template/app/lib/detailedData.ts +112 -0
- package/template/app/lib/matchStatus.ts +28 -0
- package/template/app/lib/matches.ts +131 -0
- package/template/app/lib/teamBadges.ts +223 -0
- package/template/app/lib/upcomingMatches.ts +154 -0
- package/template/app/lib/useDb.ts +29 -0
- package/template/app/lib/utils/diff.ts +24 -0
- package/template/app/lib/utils/getChartColor.ts +17 -0
- package/template/app/lib/utils/getStdDeviation.ts +6 -0
- package/template/app/lib/utils/time.ts +40 -0
- package/template/app/lib/utils.ts +70 -0
- package/template/app/page.tsx +15 -0
- package/template/app/store/dashboardStore.ts +85 -0
- package/template/app/types/dashboard.ts +75 -0
- package/template/app/types.ts +130 -0
- package/template/app/utils/dashboard/index.ts +72 -0
- package/template/eslint.config.mjs +18 -0
- package/template/infra/cloud-run/README.md +68 -0
- package/template/infra/cloud-run/sync-job.yaml +32 -0
- package/template/infra/cutover/README.md +84 -0
- package/template/infra/vercel/README.md +57 -0
- package/template/next.config.ts +7 -0
- package/template/package-lock.json +7330 -0
- package/template/package.json +47 -0
- package/template/packages/ipl-dashboard-utils/README.md +316 -0
- package/template/packages/ipl-dashboard-utils/package.json +34 -0
- package/template/packages/ipl-dashboard-utils/src/index.ts +22 -0
- package/template/packages/ipl-dashboard-utils/src/transform.ts +687 -0
- package/template/packages/ipl-dashboard-utils/src/types.ts +88 -0
- package/template/packages/ipl-dashboard-utils/tsconfig.build.json +17 -0
- package/template/postcss.config.mjs +7 -0
- package/template/scripts/capture-ipl-auth.mjs +54 -0
- package/template/scripts/deploy-cloud-run-sync.sh +48 -0
- package/template/scripts/deploy-cloud-scheduler.sh +42 -0
- package/template/scripts/dev-simple.js +31 -0
- package/template/scripts/dev-welcome.mjs +38 -0
- package/template/scripts/monitor-ops-status.sh +50 -0
- package/template/scripts/seed-mongodb.ts +115 -0
- package/template/scripts/sync-cloud.mjs +50 -0
- package/template/scripts/sync-ipl.mjs +238 -0
- package/template/scripts/sync-transfers-daily.mjs +175 -0
- package/template/scripts/verify-production.mjs +108 -0
- package/template/tests/coverage-gaps.test.ts +290 -0
- package/template/tests/dashboard-polling.test.ts +96 -0
- package/template/tests/detailed-data.test.ts +60 -0
- package/template/tests/ipl-transform.test.ts +590 -0
- package/template/tests/transfers-route.test.ts +109 -0
- package/template/tests/upcoming-matches.test.ts +34 -0
- package/template/tests/utils-and-cache.test.ts +267 -0
- package/template/tsconfig.json +35 -0
- 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
|
+
});
|
package/src/prompts.mjs
ADDED
|
@@ -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() {}
|
package/src/scaffold.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/scraper.mjs
ADDED
|
@@ -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,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"]
|