@vatvaghool/create-ipl-dashboard 0.1.20 → 0.1.24
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 +37 -121
- package/package.json +2 -3
- package/src/index.mjs +5 -26
- package/src/prompts.mjs +0 -19
- package/src/scaffold.mjs +20 -25
- package/template/README.md +22 -190
- package/template/app/api/ops/seed/route.ts +76 -0
- package/template/app/lib/matchStatus.ts +0 -12
- package/template/package.json +1 -5
- package/template/scripts/dev-simple.js +1 -1
- package/template/scripts/dev-welcome.mjs +40 -18
- package/screenshots/ai-roasting.png +0 -0
- package/screenshots/captain-board.png +0 -0
- package/screenshots/dashboard-overview.png +0 -0
- package/screenshots/ledger-table.png +0 -0
- package/screenshots/match-scrubber.png +0 -0
- package/screenshots/performance-tracker.png +0 -0
- package/src/scraper.mjs +0 -79
- package/template/app/hooks/dashboardPolling.ts +0 -53
- package/template/app/hooks/snapshotCache.ts +0 -47
- package/template/app/lib/utils/diff.ts +0 -24
- package/template/app/lib/utils/time.ts +0 -40
- package/template/screenshots/ai-roasting.png +0 -0
- package/template/screenshots/captain-board.png +0 -0
- package/template/screenshots/dashboard-overview.png +0 -0
- package/template/screenshots/ledger-table.png +0 -0
- package/template/screenshots/match-scrubber.png +0 -0
- package/template/screenshots/performance-tracker.png +0 -0
- package/template/tests/coverage-gaps.test.ts +0 -290
- package/template/tests/dashboard-polling.test.ts +0 -96
- package/template/tests/utils-and-cache.test.ts +0 -267
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getStorage } from "../../../lib/storage";
|
|
3
|
+
import { config } from "../../../lib/config";
|
|
4
|
+
import { rawApiUsers } from "../../ipl/data";
|
|
5
|
+
import { normalizePayload } from "../../ipl/transform";
|
|
6
|
+
import { MATCH_POINTS } from "../../../data/match-points";
|
|
7
|
+
import { leagueInfo } from "../../../data/league";
|
|
8
|
+
|
|
9
|
+
export const dynamic = "force-dynamic";
|
|
10
|
+
export const runtime = "nodejs";
|
|
11
|
+
|
|
12
|
+
const COLLECTION = config.mongodb.collectionName;
|
|
13
|
+
|
|
14
|
+
const storage = getStorage();
|
|
15
|
+
|
|
16
|
+
export async function POST() {
|
|
17
|
+
if (!storage.isConfigured()) {
|
|
18
|
+
return NextResponse.json({ ok: false, error: "Storage not configured" }, { status: 400 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const connected = await storage.isConnected();
|
|
22
|
+
if (!connected) {
|
|
23
|
+
return NextResponse.json({ ok: false, error: "Cannot connect to storage" }, { status: 503 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const results: Record<string, string> = {};
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await storage.upsertDocument("league", {
|
|
30
|
+
type: "league",
|
|
31
|
+
name: leagueInfo.name,
|
|
32
|
+
leagueUrl: leagueInfo.leagueUrl,
|
|
33
|
+
teams: leagueInfo.teams,
|
|
34
|
+
createdAt: new Date().toISOString(),
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
});
|
|
37
|
+
results.league = "seeded";
|
|
38
|
+
} catch (e: any) {
|
|
39
|
+
results.league = `failed: ${e.message}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await storage.upsertDocument("raw-users", {
|
|
44
|
+
type: "raw-users",
|
|
45
|
+
users: rawApiUsers,
|
|
46
|
+
updatedAt: new Date().toISOString(),
|
|
47
|
+
});
|
|
48
|
+
results.rawUsers = "seeded";
|
|
49
|
+
} catch (e: any) {
|
|
50
|
+
results.rawUsers = `failed: ${e.message}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const snapshot = normalizePayload({
|
|
55
|
+
type: "dashboard",
|
|
56
|
+
updatedAt: new Date().toISOString(),
|
|
57
|
+
completedMatches: Math.max(...MATCH_POINTS.map((m) => m.matchId), 0),
|
|
58
|
+
leaders: rawApiUsers
|
|
59
|
+
.sort((a, b) => b.points - a.points)
|
|
60
|
+
.map((u, i) => ({
|
|
61
|
+
rank: i + 1,
|
|
62
|
+
teamId: u.rno,
|
|
63
|
+
teamName: u.temname,
|
|
64
|
+
points: u.points,
|
|
65
|
+
})),
|
|
66
|
+
});
|
|
67
|
+
await storage.upsertDocument("dashboard", snapshot);
|
|
68
|
+
results.dashboard = "seeded";
|
|
69
|
+
} catch (e: any) {
|
|
70
|
+
results.dashboard = `failed: ${e.message}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const allOk = Object.values(results).every((v) => v === "seeded");
|
|
74
|
+
|
|
75
|
+
return NextResponse.json({ ok: allOk, results });
|
|
76
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { matches } from "./matches";
|
|
2
|
-
|
|
3
1
|
export type MatchStatus = "ENDED" | "LIVE" | "SOON" | "UPCOMING";
|
|
4
2
|
|
|
5
3
|
export function getMatchStatus(date: string) {
|
|
@@ -15,14 +13,4 @@ export function getMatchStatus(date: string) {
|
|
|
15
13
|
return "UPCOMING" as const;
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
export function getCurrentMatchStatus() {
|
|
19
|
-
const current = matches
|
|
20
|
-
.map((match) => ({
|
|
21
|
-
...match,
|
|
22
|
-
status: getMatchStatus(match.date),
|
|
23
|
-
}))
|
|
24
|
-
.filter((match) => match.status !== "ENDED")
|
|
25
|
-
.sort((a, b) => +new Date(a.date) - +new Date(b.date))[0];
|
|
26
16
|
|
|
27
|
-
return current?.status ?? "ENDED";
|
|
28
|
-
}
|
package/template/package.json
CHANGED
|
@@ -11,15 +11,11 @@
|
|
|
11
11
|
"sync:ipl:watch": "node scripts/sync-ipl.mjs --watch",
|
|
12
12
|
"sync:ipl:transfers-daily": "node scripts/sync-transfers-daily.mjs",
|
|
13
13
|
"sync:cloud": "node scripts/sync-cloud.mjs",
|
|
14
|
-
"verify:production": "node scripts/verify-production.mjs",
|
|
15
|
-
"monitor:ops": "bash scripts/monitor-ops-status.sh",
|
|
16
14
|
"build": "next build",
|
|
17
|
-
"build:utils-package": "node ./node_modules/typescript/bin/tsc -p packages/ipl-dashboard-utils/tsconfig.build.json",
|
|
18
15
|
"start": "next start",
|
|
19
16
|
"lint": "eslint",
|
|
20
17
|
"test": "node --test --experimental-strip-types \"tests/**/*.test.ts\"",
|
|
21
|
-
"seed:
|
|
22
|
-
"seed:mongodb:reset": "node --experimental-strip-types scripts/seed-mongodb.ts --reset --force"
|
|
18
|
+
"seed:api": "node --experimental-strip-types -e \"fetch('http://localhost:3000/api/ops/seed',{method:'POST'}).then(r=>r.json()).then(console.log).catch(console.error)\""
|
|
23
19
|
},
|
|
24
20
|
"dependencies": {
|
|
25
21
|
"@vercel/analytics": "^2.0.1",
|
|
@@ -7,7 +7,7 @@ const seedMongoIfConfigured = () => {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
console.log("MONGODB_URI detected. Running seed once (best effort)...");
|
|
10
|
-
const result = spawnSync("npm", ["run", "seed:
|
|
10
|
+
const result = spawnSync("npm", ["run", "seed:api"], {
|
|
11
11
|
stdio: "inherit",
|
|
12
12
|
env: process.env,
|
|
13
13
|
});
|
|
@@ -6,33 +6,55 @@ const amber = "\x1b[33m";
|
|
|
6
6
|
const magenta = "\x1b[35m";
|
|
7
7
|
const lime = "\x1b[32m";
|
|
8
8
|
|
|
9
|
-
const cmds = [
|
|
10
|
-
{ cmd: "capture:ipl-auth", desc: "Log in to fantasy.iplt20.com and save auth state" },
|
|
11
|
-
{ cmd: "sync:ipl", desc: "Scrape live leaderboard + squad data (one-shot)" },
|
|
12
|
-
{ cmd: "sync:ipl:transfers-daily", desc: "Scrape transfer/booster data for all teams" },
|
|
13
|
-
{ cmd: "sync:ipl:watch", desc: "Watch mode — rescrape leaderboard every 60s" },
|
|
14
|
-
{ cmd: "test", desc: "Run all tests (43 total)" },
|
|
15
|
-
{ cmd: "lint", desc: "Run ESLint" },
|
|
16
|
-
{ cmd: "build", desc: "Production build" },
|
|
17
|
-
];
|
|
18
|
-
|
|
19
9
|
console.log("");
|
|
20
10
|
console.log(` ${bold}${magenta}╭──────────────────────────────────────────╮${reset}`);
|
|
21
|
-
console.log(` ${bold}${magenta}│${reset} ${bold}🏏 IPL Dashboard Dev Server
|
|
11
|
+
console.log(` ${bold}${magenta}│${reset} ${bold}🏏 IPL Dashboard Dev Server${reset} ${bold}${magenta}│${reset}`);
|
|
22
12
|
console.log(` ${bold}${magenta}╰──────────────────────────────────────────╯${reset}`);
|
|
23
13
|
console.log("");
|
|
24
|
-
|
|
14
|
+
|
|
15
|
+
const setupSteps = [
|
|
16
|
+
{ cmd: "Edit .env", desc: "Set MONGODB_URI, IPL_LEAGUE_URL & IPL_POST_SECRET" },
|
|
17
|
+
{ cmd: "npm run seed:api", desc: "Seed initial match data into MongoDB" },
|
|
18
|
+
{ cmd: "npm run capture:ipl-auth", desc: "Log in to fantasy.iplt20.com and save auth state" },
|
|
19
|
+
{ cmd: "npm run sync:ipl", desc: "Scrape live leaderboard snapshot" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
console.log(` ${bold}${cyan}Setup${reset}`);
|
|
25
23
|
console.log(` ${dim}────────────────────────────────────────────${reset}`);
|
|
24
|
+
for (const { cmd, desc } of setupSteps) {
|
|
25
|
+
console.log(` ${dim}${cmd.padEnd(24)} ${desc}${reset}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
26
28
|
console.log("");
|
|
29
|
+
console.log(` ${bold}${cyan}Commands${reset}`);
|
|
30
|
+
console.log(` ${dim}────────────────────────────────────────────${reset}`);
|
|
31
|
+
|
|
32
|
+
const cmds = [
|
|
33
|
+
{ cmd: "dev:simple", desc: "Start dev server" },
|
|
34
|
+
{ cmd: "build", desc: "Production build" },
|
|
35
|
+
{ cmd: "sync:ipl", desc: "Scrape leaderboard snapshot" },
|
|
36
|
+
{ cmd: "sync:ipl:watch", desc: "Watch mode — rescrape every 60s" },
|
|
37
|
+
{ cmd: "sync:cloud", desc: "Run all scrapers" },
|
|
38
|
+
{ cmd: "sync:ipl:transfers-daily", desc: "Scrape transfer/booster data" },
|
|
39
|
+
{ cmd: "seed:api", desc: "Seed match data into MongoDB" },
|
|
40
|
+
{ cmd: "seed:league", desc: "Seed league metadata" },
|
|
41
|
+
{ cmd: "test", desc: "Run tests" },
|
|
42
|
+
{ cmd: "lint", desc: "Run ESLint" },
|
|
43
|
+
];
|
|
27
44
|
|
|
28
45
|
for (const { cmd, desc } of cmds) {
|
|
29
|
-
console.log(` ${amber}npm run${reset} ${bold}${cmd.padEnd(
|
|
46
|
+
console.log(` ${amber}npm run${reset} ${bold}${cmd.padEnd(20)}${reset} ${dim}${desc}${reset}`);
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
console.log("");
|
|
33
|
-
console.log(` ${bold}${lime}
|
|
34
|
-
console.log(` ${dim}
|
|
35
|
-
console.log(` ${dim}
|
|
36
|
-
console.log(` ${dim}
|
|
37
|
-
console.log(` ${dim}
|
|
50
|
+
console.log(` ${bold}${lime}Next Steps${reset}`);
|
|
51
|
+
console.log(` ${dim}────────────────────────────────────────────${reset}`);
|
|
52
|
+
console.log(` ${dim} 1. Edit${reset} .env ${dim}with your MongoDB URI, league URL & secret${reset}`);
|
|
53
|
+
console.log(` ${dim} 2. Run${reset} ${amber}npm run seed:api${reset} ${dim}(seed match data)${reset}`);
|
|
54
|
+
console.log(` ${dim} 3. Run${reset} ${amber}npm run seed:league${reset} ${dim}(seed league metadata)${reset}`);
|
|
55
|
+
console.log(` ${dim} 4. Run${reset} ${amber}npm run capture:ipl-auth${reset} ${dim}(one-time login)${reset}`);
|
|
56
|
+
console.log(` ${dim} 5. Run${reset} ${amber}npm run sync:ipl:watch${reset} ${dim}(start live sync)${reset}`);
|
|
57
|
+
console.log(` ${dim} 6. Open${reset} ${cyan}http://localhost:3000${reset}${dim} to view the dashboard${reset}`);
|
|
58
|
+
console.log("");
|
|
59
|
+
console.log(` ${dim} Deploy:${reset} ${amber}npm run build${reset} ${dim}&&${reset} ${amber}npx vercel --prod${reset}`);
|
|
38
60
|
console.log("");
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/scraper.mjs
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { DashboardData } from "../types";
|
|
2
|
-
|
|
3
|
-
export const DASHBOARD_POLL_INTERVAL_MS = 120000;
|
|
4
|
-
|
|
5
|
-
type DashboardFetcher = typeof fetch;
|
|
6
|
-
|
|
7
|
-
type DashboardPollerOptions = {
|
|
8
|
-
fetcher: DashboardFetcher;
|
|
9
|
-
onData: (data: DashboardData) => void;
|
|
10
|
-
onError?: (error: unknown) => void;
|
|
11
|
-
intervalMs?: number;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export const createDashboardPoller = ({
|
|
15
|
-
fetcher,
|
|
16
|
-
onData,
|
|
17
|
-
onError = console.error,
|
|
18
|
-
intervalMs = DASHBOARD_POLL_INTERVAL_MS,
|
|
19
|
-
}: DashboardPollerOptions) => {
|
|
20
|
-
let active = true;
|
|
21
|
-
let previousSerialized = "";
|
|
22
|
-
|
|
23
|
-
const fetchData = async () => {
|
|
24
|
-
try {
|
|
25
|
-
const res = await fetcher(`/api/ipl?t=${Date.now()}`, {
|
|
26
|
-
cache: "no-store",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
if (!res.ok) {
|
|
30
|
-
throw new Error(`Dashboard API failed with ${res.status}`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const data = (await res.json()) as DashboardData;
|
|
34
|
-
const serialized = JSON.stringify(data);
|
|
35
|
-
|
|
36
|
-
if (active && serialized !== previousSerialized) {
|
|
37
|
-
previousSerialized = serialized;
|
|
38
|
-
onData(data);
|
|
39
|
-
}
|
|
40
|
-
} catch (error) {
|
|
41
|
-
onError(error);
|
|
42
|
-
}
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const interval = setInterval(fetchData, intervalMs);
|
|
46
|
-
|
|
47
|
-
void fetchData();
|
|
48
|
-
|
|
49
|
-
return () => {
|
|
50
|
-
active = false;
|
|
51
|
-
clearInterval(interval);
|
|
52
|
-
};
|
|
53
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import type { ScrapedDashboardPayload } from "../types";
|
|
4
|
-
|
|
5
|
-
const CACHE_KEY = "ipl:snapshot-cache:v1";
|
|
6
|
-
|
|
7
|
-
type CachePayload = {
|
|
8
|
-
cachedAt: string;
|
|
9
|
-
snapshot: ScrapedDashboardPayload;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const readSnapshotCache = (): CachePayload | null => {
|
|
13
|
-
if (typeof window === "undefined") return null;
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const raw = window.localStorage.getItem(CACHE_KEY);
|
|
17
|
-
if (!raw) return null;
|
|
18
|
-
|
|
19
|
-
const parsed = JSON.parse(raw) as Partial<CachePayload> | null;
|
|
20
|
-
if (!parsed || typeof parsed !== "object") return null;
|
|
21
|
-
if (!parsed.snapshot || typeof parsed.snapshot !== "object") return null;
|
|
22
|
-
|
|
23
|
-
const cachedAt =
|
|
24
|
-
typeof parsed.cachedAt === "string" ? parsed.cachedAt : undefined;
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
cachedAt: cachedAt ?? new Date().toISOString(),
|
|
28
|
-
snapshot: parsed.snapshot as ScrapedDashboardPayload,
|
|
29
|
-
};
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const writeSnapshotCache = (snapshot: ScrapedDashboardPayload) => {
|
|
36
|
-
if (typeof window === "undefined") return;
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const payload: CachePayload = {
|
|
40
|
-
cachedAt: new Date().toISOString(),
|
|
41
|
-
snapshot,
|
|
42
|
-
};
|
|
43
|
-
window.localStorage.setItem(CACHE_KEY, JSON.stringify(payload));
|
|
44
|
-
} catch {
|
|
45
|
-
// ignore cache write failures (private mode, quota, etc.)
|
|
46
|
-
}
|
|
47
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// utils/diff.ts
|
|
2
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
3
|
-
export function getDiff(prev: any, next: any, path = ""): Record<string, any> {
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
-
const diff: Record<string, any> = {};
|
|
6
|
-
|
|
7
|
-
for (const key in next) {
|
|
8
|
-
const newPath = path ? `${path}.${key}` : key;
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
typeof next[key] === "object" &&
|
|
12
|
-
next[key] !== null &&
|
|
13
|
-
typeof prev?.[key] === "object"
|
|
14
|
-
) {
|
|
15
|
-
Object.assign(diff, getDiff(prev[key], next[key], newPath));
|
|
16
|
-
} else {
|
|
17
|
-
if (prev?.[key] !== next[key]) {
|
|
18
|
-
diff[newPath] = next[key];
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return diff;
|
|
24
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// utils/time.ts
|
|
2
|
-
|
|
3
|
-
export function formatMatchTime(date: string, timeZone = "Asia/Kolkata") {
|
|
4
|
-
return new Date(date).toLocaleString("en-IN", {
|
|
5
|
-
timeZone,
|
|
6
|
-
hour: "2-digit",
|
|
7
|
-
minute: "2-digit",
|
|
8
|
-
hour12: true,
|
|
9
|
-
day: "2-digit",
|
|
10
|
-
month: "short",
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getMatchStatus(date: string) {
|
|
15
|
-
const now = new Date().getTime();
|
|
16
|
-
const matchTime = new Date(date).getTime();
|
|
17
|
-
|
|
18
|
-
const diff = matchTime - now;
|
|
19
|
-
|
|
20
|
-
if (diff < -4 * 60 * 60 * 1000) return "ENDED"; // 4h after start
|
|
21
|
-
if (diff <= 0) return "LIVE";
|
|
22
|
-
if (diff <= 30 * 60 * 1000) return "STARTING SOON";
|
|
23
|
-
|
|
24
|
-
return "UPCOMING";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function getTimeLeft(date: string) {
|
|
28
|
-
const now = new Date().getTime();
|
|
29
|
-
const matchTime = new Date(date).getTime();
|
|
30
|
-
|
|
31
|
-
const diff = matchTime - now;
|
|
32
|
-
|
|
33
|
-
if (diff <= 0) return "Started";
|
|
34
|
-
|
|
35
|
-
const mins = Math.floor(diff / 60000);
|
|
36
|
-
const hrs = Math.floor(mins / 60);
|
|
37
|
-
|
|
38
|
-
if (hrs > 0) return `${hrs}h ${mins % 60}m`;
|
|
39
|
-
return `${mins}m`;
|
|
40
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|