@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useDashboardModel } from "./useDashboardModel";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { useDashboardData } from "../../hooks/useDashboardData";
|
|
3
|
+
import { useDashboardStore } from "../../store/dashboardStore";
|
|
4
|
+
import { buildDashboardModel } from "../../lib/dashboard/model";
|
|
5
|
+
import type { DashboardModel } from "../../types/dashboard";
|
|
6
|
+
|
|
7
|
+
export function useDashboardModel(): {
|
|
8
|
+
model: DashboardModel | null;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
error: string | null;
|
|
11
|
+
lastSyncedAt: string | null;
|
|
12
|
+
} {
|
|
13
|
+
const { loading, error } = useDashboardData();
|
|
14
|
+
const data = useDashboardStore((state) => state.data);
|
|
15
|
+
const lastSyncedAt = useDashboardStore((state) => state.lastSyncedAt);
|
|
16
|
+
|
|
17
|
+
const model = useMemo(() => buildDashboardModel(data), [data]);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
model,
|
|
21
|
+
loading,
|
|
22
|
+
error,
|
|
23
|
+
lastSyncedAt,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { DashboardData } from "../types";
|
|
4
|
+
|
|
5
|
+
const CACHE_KEY = "ipl:dashboard-cache:v1";
|
|
6
|
+
|
|
7
|
+
type CachePayload = {
|
|
8
|
+
cachedAt: string;
|
|
9
|
+
data: DashboardData;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const readDashboardCache = (): 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.data || typeof parsed.data !== "object") return null;
|
|
22
|
+
if (
|
|
23
|
+
typeof (parsed.data as { updatedAt?: unknown }).updatedAt === "string" &&
|
|
24
|
+
!("snapshot" in parsed.data)
|
|
25
|
+
) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cachedAt =
|
|
30
|
+
typeof parsed.cachedAt === "string" ? parsed.cachedAt : undefined;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
cachedAt: cachedAt ?? new Date().toISOString(),
|
|
34
|
+
data: parsed.data as DashboardData,
|
|
35
|
+
};
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const writeDashboardCache = (data: DashboardData) => {
|
|
42
|
+
if (typeof window === "undefined") return;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const payload: CachePayload = {
|
|
46
|
+
cachedAt: new Date().toISOString(),
|
|
47
|
+
data,
|
|
48
|
+
};
|
|
49
|
+
window.localStorage.setItem(CACHE_KEY, JSON.stringify(payload));
|
|
50
|
+
} catch {
|
|
51
|
+
// ignore cache write failures (private mode, quota, etc.)
|
|
52
|
+
}
|
|
53
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import {
|
|
5
|
+
DASHBOARD_SYNC_INTERVAL_MS,
|
|
6
|
+
useDashboardStore,
|
|
7
|
+
} from "../store/dashboardStore";
|
|
8
|
+
|
|
9
|
+
export function useDashboardData() {
|
|
10
|
+
const hydrateFromCache = useDashboardStore((state) => state.hydrateFromCache);
|
|
11
|
+
const fetchDashboard = useDashboardStore((state) => state.fetchDashboard);
|
|
12
|
+
const data = useDashboardStore((state) => state.data);
|
|
13
|
+
const loading = useDashboardStore((state) => state.loading);
|
|
14
|
+
const error = useDashboardStore((state) => state.error);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
hydrateFromCache();
|
|
18
|
+
void fetchDashboard();
|
|
19
|
+
|
|
20
|
+
const interval = window.setInterval(() => {
|
|
21
|
+
void fetchDashboard();
|
|
22
|
+
}, DASHBOARD_SYNC_INTERVAL_MS);
|
|
23
|
+
|
|
24
|
+
return () => window.clearInterval(interval);
|
|
25
|
+
}, [fetchDashboard, hydrateFromCache]);
|
|
26
|
+
|
|
27
|
+
return { data, loading, error };
|
|
28
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Analytics } from "@vercel/analytics/next";
|
|
3
|
+
import {
|
|
4
|
+
Caveat,
|
|
5
|
+
JetBrains_Mono,
|
|
6
|
+
Nunito,
|
|
7
|
+
Patrick_Hand,
|
|
8
|
+
} from "next/font/google";
|
|
9
|
+
import "./globals.css";
|
|
10
|
+
|
|
11
|
+
const bodyFont = Nunito({
|
|
12
|
+
variable: "--font-sans",
|
|
13
|
+
subsets: ["latin"],
|
|
14
|
+
weight: ["400", "600", "700"],
|
|
15
|
+
display: "swap",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const displayFont = Caveat({
|
|
19
|
+
variable: "--font-display",
|
|
20
|
+
subsets: ["latin"],
|
|
21
|
+
weight: ["400", "700"],
|
|
22
|
+
display: "swap",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const monoFont = JetBrains_Mono({
|
|
26
|
+
variable: "--font-mono",
|
|
27
|
+
subsets: ["latin"],
|
|
28
|
+
weight: ["400", "600"],
|
|
29
|
+
display: "swap",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const noteFont = Patrick_Hand({
|
|
33
|
+
variable: "--font-note",
|
|
34
|
+
subsets: ["latin"],
|
|
35
|
+
weight: ["400"],
|
|
36
|
+
display: "swap",
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export const metadata: Metadata = {
|
|
40
|
+
title: "IPL 2026 — Fantasy Scorebook",
|
|
41
|
+
description:
|
|
42
|
+
"A hand-drawn, scribbled notebook for IPL 2026 fantasy league standings, captain picks, and match mayhem.",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const themeInitScript = `
|
|
46
|
+
(() => {
|
|
47
|
+
try {
|
|
48
|
+
const key = "ipl:theme";
|
|
49
|
+
const stored = window.localStorage.getItem(key);
|
|
50
|
+
const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
51
|
+
const theme = stored === "light" || stored === "dark" ? stored : preferred;
|
|
52
|
+
document.documentElement.dataset.theme = theme;
|
|
53
|
+
} catch {}
|
|
54
|
+
}).call(this);
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
export default function RootLayout({
|
|
58
|
+
children,
|
|
59
|
+
}: Readonly<{
|
|
60
|
+
children: React.ReactNode;
|
|
61
|
+
}>) {
|
|
62
|
+
return (
|
|
63
|
+
<html
|
|
64
|
+
lang="en"
|
|
65
|
+
suppressHydrationWarning
|
|
66
|
+
className={`${bodyFont.variable} ${displayFont.variable} ${monoFont.variable} ${noteFont.variable} h-full antialiased`}
|
|
67
|
+
>
|
|
68
|
+
<body className="min-h-full flex flex-col">
|
|
69
|
+
<script dangerouslySetInnerHTML={{ __html: themeInitScript }} />
|
|
70
|
+
{children}
|
|
71
|
+
<Analytics />
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
);
|
|
75
|
+
}
|