Add more detailed analytics charts

This commit is contained in:
Aman Varshney
2025-05-31 21:29:37 +05:30
parent b72f381a69
commit 1bec912d85
2 changed files with 440 additions and 13 deletions

View File

@@ -30,6 +30,7 @@ import Navbar from "../_components/navbar";
interface AnalyticsData { interface AnalyticsData {
date: string; date: string;
hour: number;
cli_version: string; cli_version: string;
node_version: string; node_version: string;
platform: string; platform: string;
@@ -47,6 +48,7 @@ interface AnalyticsData {
addons: string[]; addons: string[];
git: string; git: string;
install: string; install: string;
runtime: string;
} }
const timeSeriesConfig = { const timeSeriesConfig = {
@@ -365,6 +367,43 @@ const addonsConfig = {
}, },
} satisfies ChartConfig; } satisfies ChartConfig;
const runtimeConfig = {
node: {
label: "Node.js",
color: "hsl(var(--chart-1))",
},
bun: {
label: "Bun",
color: "hsl(var(--chart-2))",
},
none: {
label: "None",
color: "hsl(var(--chart-6))",
},
} satisfies ChartConfig;
const projectTypeConfig = {
fullstack: {
label: "Full-stack",
color: "hsl(var(--chart-1))",
},
"frontend-only": {
label: "Frontend-only",
color: "hsl(var(--chart-2))",
},
"backend-only": {
label: "Backend-only",
color: "hsl(var(--chart-3))",
},
} satisfies ChartConfig;
const hourlyDistributionConfig = {
count: {
label: "Projects Created",
color: "hsl(var(--chart-1))",
},
} satisfies ChartConfig;
export default function AnalyticsPage() { export default function AnalyticsPage() {
const [data, setData] = useState<AnalyticsData[]>([]); const [data, setData] = useState<AnalyticsData[]>([]);
const [lastUpdated, setLastUpdated] = useState<string | null>(null); const [lastUpdated, setLastUpdated] = useState<string | null>(null);
@@ -387,6 +426,16 @@ export default function AnalyticsPage() {
? timestamp.split("T")[0] ? timestamp.split("T")[0]
: timestamp.split(" ")[0]; : timestamp.split(" ")[0];
let hour = 0;
try {
const timestampDate = new Date(timestamp);
if (!Number.isNaN(timestampDate.getTime())) {
hour = timestampDate.getUTCHours();
}
} catch {
hour = 0;
}
const addons = [ const addons = [
row["*.properties.addons.0"], row["*.properties.addons.0"],
row["*.properties.addons.1"], row["*.properties.addons.1"],
@@ -398,6 +447,7 @@ export default function AnalyticsPage() {
return { return {
date, date,
hour,
cli_version: row["*.properties.cli_version"] || "unknown", cli_version: row["*.properties.cli_version"] || "unknown",
node_version: row["*.properties.node_version"] || "unknown", node_version: row["*.properties.node_version"] || "unknown",
platform: row["*.properties.platform"] || "unknown", platform: row["*.properties.platform"] || "unknown",
@@ -423,6 +473,7 @@ export default function AnalyticsPage() {
row["*.properties.install"] === "True" row["*.properties.install"] === "True"
? "enabled" ? "enabled"
: "disabled", : "disabled",
runtime: row["*.properties.runtime"] || "unknown",
}; };
}) })
.filter((item): item is AnalyticsData => .filter((item): item is AnalyticsData =>
@@ -825,6 +876,140 @@ export default function AnalyticsPage() {
.sort((a, b) => b.value - a.value); .sort((a, b) => b.value - a.value);
}; };
const getRuntimeData = () => {
const runtimeCounts = data.reduce(
(acc, item) => {
const runtime = item.runtime || "none";
acc[runtime] = (acc[runtime] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
return Object.entries(runtimeCounts)
.map(([name, value]) => ({
name,
value,
}))
.sort((a, b) => b.value - a.value);
};
const getProjectTypeData = () => {
const typeCounts = data.reduce(
(acc, item) => {
const hasFrontend =
(item.frontend0 && item.frontend0 !== "none") ||
(item.frontend1 && item.frontend1 !== "none");
const hasBackend = item.backend && item.backend !== "none";
let type: string;
if (hasFrontend && hasBackend) {
type = "fullstack";
} else if (hasFrontend && !hasBackend) {
type = "frontend-only";
} else if (!hasFrontend && hasBackend) {
type = "backend-only";
} else {
type = "frontend-only";
}
acc[type] = (acc[type] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
return Object.entries(typeCounts).map(([name, value]) => ({
name,
value,
}));
};
const getMonthlyTimeSeriesData = () => {
if (data.length === 0) return [];
const monthlyCounts = data.reduce(
(acc, item) => {
const date = new Date(item.date);
const monthKey = format(date, "yyyy-MM");
acc[monthKey] = (acc[monthKey] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
return Object.entries(monthlyCounts)
.map(([month, count]) => ({
month,
displayMonth: format(parseISO(`${month}-01`), "MMM yyyy"),
count,
}))
.sort((a, b) => a.month.localeCompare(b.month));
};
const getPopularStackCombinations = () => {
const comboCounts = data.reduce(
(acc, item) => {
const frontend = item.frontend0 || item.frontend1 || "none";
const backend = item.backend || "none";
const combo = `${frontend} + ${backend}`;
acc[combo] = (acc[combo] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
return Object.entries(comboCounts)
.map(([name, value]) => ({
name,
value,
}))
.sort((a, b) => b.value - a.value)
.slice(0, 8);
};
const getDatabaseORMCombinations = () => {
const comboCounts = data.reduce(
(acc, item) => {
const database = item.database || "none";
const orm = item.orm || "none";
if (database !== "none" && orm !== "none") {
const combo = `${database} + ${orm}`;
acc[combo] = (acc[combo] || 0) + 1;
}
return acc;
},
{} as Record<string, number>,
);
return Object.entries(comboCounts)
.map(([name, value]) => ({
name,
value,
}))
.sort((a, b) => b.value - a.value)
.slice(0, 6);
};
const getHourlyDistributionData = () => {
if (data.length === 0) return [];
const hourlyCounts = data.reduce(
(acc, item) => {
const hour = item.hour;
acc[hour] = (acc[hour] || 0) + 1;
return acc;
},
{} as Record<number, number>,
);
return Array.from({ length: 24 }, (_, hour) => ({
hour: hour.toString().padStart(2, "0"),
displayHour: `${hour.toString().padStart(2, "0")}:00`,
count: hourlyCounts[hour] || 0,
}));
};
const totalProjects = data.length; const totalProjects = data.length;
const getAvgProjectsPerDay = () => { const getAvgProjectsPerDay = () => {
if (data.length === 0) return 0; if (data.length === 0) return 0;
@@ -847,11 +1032,18 @@ export default function AnalyticsPage() {
const frontendData = getFrontendData(); const frontendData = getFrontendData();
const backendData = getBackendData(); const backendData = getBackendData();
const runtimeData = getRuntimeData();
const mostPopularFrontend = const mostPopularFrontend =
frontendData.length > 0 ? frontendData[0].name : "None"; frontendData.length > 0 ? frontendData[0].name : "None";
const mostPopularBackend = const mostPopularBackend =
backendData.length > 0 ? backendData[0].name : "None"; backendData.length > 0 ? backendData[0].name : "None";
const projectTypeData = getProjectTypeData();
const monthlyTimeSeriesData = getMonthlyTimeSeriesData();
const popularStackCombinations = getPopularStackCombinations();
const databaseORMCombinations = getDatabaseORMCombinations();
const hourlyDistributionData = getHourlyDistributionData();
return ( return (
<div className="terminal-scanlines min-h-screen bg-background font-mono"> <div className="terminal-scanlines min-h-screen bg-background font-mono">
<Navbar /> <Navbar />
@@ -1164,6 +1356,44 @@ export default function AnalyticsPage() {
</div> </div>
</div> </div>
<div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-mono font-semibold text-sm">
MONTHLY_TRENDS.CHART
</span>
</div>
<p className="mt-1 font-mono text-muted-foreground text-xs">
# Monthly project creation trends
</p>
</div>
<div className="p-4">
<ChartContainer
config={timeSeriesConfig}
className="h-[300px] w-full"
>
<BarChart data={monthlyTimeSeriesData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="displayMonth"
tickLine={false}
tickMargin={10}
axisLine={false}
className="font-mono text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="count"
radius={4}
fill="var(--color-projects)"
/>
</BarChart>
</ChartContainer>
</div>
</div>
<div className="terminal-block-hover rounded border border-border bg-background"> <div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3"> <div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -1209,6 +1439,46 @@ export default function AnalyticsPage() {
</ChartContainer> </ChartContainer>
</div> </div>
</div> </div>
<div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-mono font-semibold text-sm">
HOURLY_DISTRIBUTION.BAR
</span>
</div>
<p className="mt-1 font-mono text-muted-foreground text-xs">
# Projects created by hour of day (UTC)
</p>
</div>
<div className="p-4">
<ChartContainer
config={hourlyDistributionConfig}
className="h-[350px] w-full"
>
<BarChart data={hourlyDistributionData}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="displayHour"
tickLine={false}
tickMargin={10}
axisLine={false}
className="font-mono text-xs"
/>
<YAxis hide />
<ChartTooltip
content={<ChartTooltipContent />}
labelFormatter={(value, payload) => {
const hour = payload?.[0]?.payload?.displayHour;
return hour ? `${hour} UTC` : value;
}}
/>
<Bar dataKey="count" radius={4} fill="var(--color-count)" />
</BarChart>
</ChartContainer>
</div>
</div>
</div> </div>
</div> </div>
@@ -1225,6 +1495,44 @@ export default function AnalyticsPage() {
</span> </span>
</div> </div>
<div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-mono font-semibold text-sm">
POPULAR_STACK_COMBINATIONS.BAR
</span>
</div>
<p className="mt-1 font-mono text-muted-foreground text-xs">
# Most popular frontend + backend combinations
</p>
</div>
<div className="p-4">
<ChartContainer
config={frontendConfig}
className="h-[400px] w-full"
>
<BarChart data={popularStackCombinations}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="name"
tickLine={false}
tickMargin={10}
axisLine={false}
className="font-mono text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar
dataKey="value"
radius={4}
fill="var(--color-react-router)"
/>
</BarChart>
</ChartContainer>
</div>
</div>
<div className="terminal-block-hover rounded border border-border bg-background"> <div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3"> <div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -1240,7 +1548,7 @@ export default function AnalyticsPage() {
<div className="p-4"> <div className="p-4">
<ChartContainer <ChartContainer
config={frontendConfig} config={frontendConfig}
className="h-[300px] w-full" className="h-[350px] w-full"
> >
<BarChart data={getFrontendData()}> <BarChart data={getFrontendData()}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
@@ -1507,6 +1815,130 @@ export default function AnalyticsPage() {
</ChartContainer> </ChartContainer>
</div> </div>
</div> </div>
<div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-mono font-semibold text-sm">
RUNTIME_DISTRIBUTION.PIE
</span>
</div>
<p className="mt-1 font-mono text-muted-foreground text-xs">
# JavaScript runtime preference distribution
</p>
</div>
<div className="p-4">
<ChartContainer
config={runtimeConfig}
className="h-[300px] w-full"
>
<PieChart>
<ChartTooltip
content={<ChartTooltipContent nameKey="name" />}
/>
<Pie
data={runtimeData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={80}
label={({ name, percent }) =>
`${name} ${(percent * 100).toFixed(0)}%`
}
>
{runtimeData.map((entry) => (
<Cell
key={`runtime-${entry.name}`}
fill={`var(--color-${entry.name})`}
/>
))}
</Pie>
<ChartLegend content={<ChartLegendContent />} />
</PieChart>
</ChartContainer>
</div>
</div>
<div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-mono font-semibold text-sm">
PROJECT_TYPES.PIE
</span>
</div>
<p className="mt-1 font-mono text-muted-foreground text-xs">
# Full-stack vs Frontend-only vs Backend-only projects
</p>
</div>
<div className="p-4">
<ChartContainer
config={projectTypeConfig}
className="h-[300px] w-full"
>
<PieChart>
<ChartTooltip
content={<ChartTooltipContent nameKey="name" />}
/>
<Pie
data={projectTypeData}
dataKey="value"
nameKey="name"
cx="50%"
cy="50%"
outerRadius={80}
label={({ name, percent }) =>
`${name} ${(percent * 100).toFixed(0)}%`
}
>
{projectTypeData.map((entry) => (
<Cell
key={`project-type-${entry.name}`}
fill={`var(--color-${entry.name})`}
/>
))}
</Pie>
<ChartLegend content={<ChartLegendContent />} />
</PieChart>
</ChartContainer>
</div>
</div>
</div>
<div className="terminal-block-hover rounded border border-border bg-background">
<div className="border-border border-b bg-muted/20 px-4 py-3">
<div className="flex items-center gap-2">
<span className="text-primary text-xs"></span>
<span className="font-mono font-semibold text-sm">
DATABASE_ORM_COMBINATIONS.BAR
</span>
</div>
<p className="mt-1 font-mono text-muted-foreground text-xs">
# Popular database + ORM combinations
</p>
</div>
<div className="p-4">
<ChartContainer
config={databaseConfig}
className="h-[350px] w-full"
>
<BarChart data={databaseORMCombinations}>
<CartesianGrid vertical={false} />
<XAxis
dataKey="name"
tickLine={false}
tickMargin={10}
axisLine={false}
className="font-mono text-xs"
/>
<YAxis hide />
<ChartTooltip content={<ChartTooltipContent />} />
<Bar dataKey="value" radius={4} fill="var(--color-sqlite)" />
</BarChart>
</ChartContainer>
</div>
</div> </div>
<div className="terminal-block-hover rounded border border-border bg-background"> <div className="terminal-block-hover rounded border border-border bg-background">
@@ -1524,7 +1956,7 @@ export default function AnalyticsPage() {
<div className="p-4"> <div className="p-4">
<ChartContainer <ChartContainer
config={addonsConfig} config={addonsConfig}
className="h-[300px] w-full" className="h-[350px] w-full"
> >
<BarChart data={getAddonsData()}> <BarChart data={getAddonsData()}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />
@@ -1593,16 +2025,11 @@ export default function AnalyticsPage() {
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<div className="mb-4 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap"> <div className="mb-4 flex items-center gap-2">
<div className="flex items-center gap-2"> <span className="font-bold font-mono text-lg">
<span className="font-bold font-mono text-lg sm:text-xl"> DEV_ENVIRONMENT.CONFIG
DEV_ENVIRONMENT.CONFIG
</span>
</div>
<div className="hidden h-px flex-1 bg-border sm:block" />
<span className="w-full text-right font-mono text-muted-foreground text-xs sm:w-auto sm:text-left">
[TOOLING_PREFERENCES]
</span> </span>
<div className="h-px flex-1 bg-border" />
</div> </div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2"> <div className="grid grid-cols-1 gap-6 md:grid-cols-2">
@@ -1783,7 +2210,7 @@ export default function AnalyticsPage() {
<div className="p-4"> <div className="p-4">
<ChartContainer <ChartContainer
config={cliVersionConfig} config={cliVersionConfig}
className="h-[300px] w-full" className="h-[350px] w-full"
> >
<BarChart data={getCLIVersionData()}> <BarChart data={getCLIVersionData()}>
<CartesianGrid vertical={false} /> <CartesianGrid vertical={false} />

View File

@@ -14,7 +14,7 @@
}, },
"apps/cli": { "apps/cli": {
"name": "create-better-t-stack", "name": "create-better-t-stack",
"version": "2.14.1", "version": "2.14.4",
"bin": { "bin": {
"create-better-t-stack": "dist/index.js", "create-better-t-stack": "dist/index.js",
}, },