Skip to content

Commit 5de9467

Browse files
refactor(web): break down analytics page into smaller components
1 parent 83025c7 commit 5de9467

File tree

11 files changed

+2038
-1848
lines changed

11 files changed

+2038
-1848
lines changed

apps/web/scripts/generate-analytics.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ async function generateAnalyticsData() {
9999
.filter((row) => row.trim().length > 0)
100100
.map((row) => row.trim());
101101

102-
csvText = header + "\n" + dataRows.join("\n");
102+
csvText = `${header}\n${dataRows.join("\n")}`;
103103
console.log(`✅ Manually parsed ${dataRows.length} rows`);
104104
}
105105
}
@@ -145,7 +145,7 @@ async function generateAnalyticsData() {
145145

146146
results.data.forEach((row, index) => {
147147
// Skip rows that don't have essential data
148-
if (!row["*.timestamp"] && !row["timestamp"]) {
148+
if (!row["*.timestamp"] && !row.timestamp) {
149149
if (index < 5) {
150150
console.log(
151151
`⚠️ Skipping row ${index} - no timestamp:`,
@@ -156,9 +156,7 @@ async function generateAnalyticsData() {
156156
}
157157

158158
const timestamp =
159-
row["*.timestamp"] ||
160-
row["timestamp"] ||
161-
new Date().toISOString();
159+
row["*.timestamp"] || row.timestamp || new Date().toISOString();
162160
const date = timestamp.includes("T")
163161
? timestamp.split("T")[0]
164162
: timestamp.split(" ")[0];
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Bar, BarChart, CartesianGrid, Cell, XAxis, YAxis } from "recharts";
2+
import {
3+
ChartContainer,
4+
ChartTooltip,
5+
ChartTooltipContent,
6+
} from "@/components/ui/chart";
7+
import { getAddonsData, getExamplesData } from "./data-utils";
8+
import type { AggregatedAnalyticsData } from "./types";
9+
import { addonsConfig, examplesConfig } from "./types";
10+
11+
interface AddonsExamplesChartsProps {
12+
data: AggregatedAnalyticsData | null;
13+
}
14+
15+
export function AddonsExamplesCharts({ data }: AddonsExamplesChartsProps) {
16+
const addonsData = getAddonsData(data);
17+
const examplesData = getExamplesData(data);
18+
19+
return (
20+
<>
21+
<div className="rounded border border-border">
22+
<div className="border-border border-b px-4 py-3">
23+
<div className="flex items-center gap-2">
24+
<span className="text-primary text-xs"></span>
25+
<span className=" font-semibold text-sm">ADDONS_USAGE.BAR</span>
26+
</div>
27+
<p className="mt-1 text-muted-foreground text-xs">
28+
Additional features and tooling adoption
29+
</p>
30+
</div>
31+
<div className="p-4">
32+
<ChartContainer config={addonsConfig} className="h-[350px] w-full">
33+
<BarChart data={addonsData}>
34+
<CartesianGrid vertical={false} />
35+
<XAxis
36+
dataKey="name"
37+
tickLine={false}
38+
tickMargin={10}
39+
axisLine={false}
40+
className=" text-xs"
41+
/>
42+
<YAxis hide />
43+
<ChartTooltip content={<ChartTooltipContent />} />
44+
<Bar dataKey="value" radius={4}>
45+
{addonsData.map((entry) => (
46+
<Cell
47+
key={`addons-${entry.name}`}
48+
fill={
49+
entry.name === "pwa"
50+
? "hsl(var(--chart-1))"
51+
: entry.name === "biome"
52+
? "hsl(var(--chart-2))"
53+
: entry.name === "tauri"
54+
? "hsl(var(--chart-3))"
55+
: entry.name === "husky"
56+
? "hsl(var(--chart-4))"
57+
: entry.name === "starlight"
58+
? "hsl(var(--chart-5))"
59+
: entry.name === "turborepo"
60+
? "hsl(var(--chart-6))"
61+
: "hsl(var(--chart-7))"
62+
}
63+
/>
64+
))}
65+
</Bar>
66+
</BarChart>
67+
</ChartContainer>
68+
</div>
69+
</div>
70+
71+
<div className="rounded border border-border">
72+
<div className="border-border border-b px-4 py-3">
73+
<div className="flex items-center gap-2">
74+
<span className="text-primary text-xs"></span>
75+
<span className=" font-semibold text-sm">EXAMPLES_USAGE.BAR</span>
76+
</div>
77+
<p className="mt-1 text-muted-foreground text-xs">
78+
Example applications included in projects
79+
</p>
80+
</div>
81+
<div className="p-4">
82+
<ChartContainer config={examplesConfig} className="h-[300px] w-full">
83+
<BarChart data={examplesData}>
84+
<CartesianGrid vertical={false} />
85+
<XAxis
86+
dataKey="name"
87+
tickLine={false}
88+
tickMargin={10}
89+
axisLine={false}
90+
className=" text-xs"
91+
/>
92+
<YAxis hide />
93+
<ChartTooltip content={<ChartTooltipContent />} />
94+
<Bar dataKey="value" radius={4}>
95+
{examplesData.map((entry) => (
96+
<Cell
97+
key={`examples-${entry.name}`}
98+
fill={
99+
entry.name === "todo"
100+
? "hsl(var(--chart-1))"
101+
: entry.name === "ai"
102+
? "hsl(var(--chart-2))"
103+
: "hsl(var(--chart-7))"
104+
}
105+
/>
106+
))}
107+
</Bar>
108+
</BarChart>
109+
</ChartContainer>
110+
</div>
111+
</div>
112+
</>
113+
);
114+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { Terminal } from "lucide-react";
2+
import Image from "next/image";
3+
import Link from "next/link";
4+
import discordIcon from "@/public/icon/discord.svg";
5+
6+
interface AnalyticsHeaderProps {
7+
totalProjects: number;
8+
lastUpdated: string | null;
9+
loadingLastUpdated: boolean;
10+
}
11+
12+
export function AnalyticsHeader({
13+
totalProjects,
14+
lastUpdated,
15+
loadingLastUpdated,
16+
}: AnalyticsHeaderProps) {
17+
return (
18+
<div className="mb-8">
19+
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
20+
<div className="flex items-center gap-2">
21+
<Terminal className="h-5 w-5 text-primary" />
22+
<span className="font-bold text-lg sm:text-xl">
23+
ANALYTICS_DASHBOARD.SH
24+
</span>
25+
</div>
26+
<div className="hidden h-px flex-1 bg-border sm:block" />
27+
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
28+
[{totalProjects} PROJECTS_ANALYZED]
29+
</span>
30+
</div>
31+
32+
<div className="rounded rounded-b-none border border-border p-4">
33+
<div className="flex items-center gap-2 text-sm">
34+
<span className="text-primary">$</span>
35+
<span className=" text-foreground">
36+
Analytics from Better-T-Stack CLI usage data
37+
</span>
38+
</div>
39+
<div className="mt-2 flex items-center gap-2 text-sm">
40+
<span className="text-primary">$</span>
41+
<span className=" text-muted-foreground">
42+
Uses PostHog - no personal info tracked, runs on each project
43+
creation
44+
</span>
45+
</div>
46+
<div className="mt-2 flex items-center gap-2 text-sm">
47+
<span className="text-primary">$</span>
48+
<span className=" text-muted-foreground">
49+
Source:{" "}
50+
<Link
51+
href="https://github.com/amanvarshney01/create-better-t-stack/blob/main/apps/cli/src/utils/analytics.ts"
52+
target="_blank"
53+
rel="noopener noreferrer"
54+
className="text-accent underline hover:text-primary"
55+
>
56+
analytics.ts
57+
</Link>
58+
{" | "}
59+
<Link
60+
href="https://r2.amanv.dev/export.csv"
61+
target="_blank"
62+
rel="noopener noreferrer"
63+
className="text-accent underline hover:text-primary"
64+
>
65+
export.csv
66+
</Link>
67+
</span>
68+
</div>
69+
<div className="mt-2 flex items-center gap-2 text-sm">
70+
<span className="text-primary">$</span>
71+
<span className=" text-muted-foreground">
72+
Last updated:{" "}
73+
{loadingLastUpdated
74+
? "CHECKING..."
75+
: lastUpdated
76+
? `${lastUpdated} UTC`
77+
: "UNKNOWN"}
78+
</span>
79+
</div>
80+
</div>
81+
82+
<Link
83+
href="https://discord.gg/ZYsbjpDaM5"
84+
target="_blank"
85+
rel="noopener noreferrer"
86+
className="block rounded rounded-t-none border border-border border-t-0"
87+
>
88+
<div className="flex items-center justify-between p-3">
89+
<div className="flex items-center gap-3">
90+
<Image
91+
src={discordIcon}
92+
alt="discord"
93+
className="h-4 w-4 invert-0 dark:invert"
94+
/>
95+
<div>
96+
<span className=" font-semibold text-sm">
97+
DISCORD_NOTIFICATIONS.IRC
98+
</span>
99+
<p className=" text-muted-foreground text-xs">
100+
Join for LIVE project creation alerts
101+
</p>
102+
</div>
103+
</div>
104+
<div className="flex items-center gap-1 rounded border border-border bg-primary/10 px-2 py-1">
105+
<span className="text-primary text-xs"></span>
106+
<span className=" font-semibold text-primary text-xs">JOIN</span>
107+
</div>
108+
</div>
109+
</Link>
110+
</div>
111+
);
112+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { AggregatedAnalyticsData } from "./types";
2+
3+
export const getPlatformData = (data: AggregatedAnalyticsData | null) => {
4+
if (!data) return [];
5+
return data.platformDistribution || [];
6+
};
7+
8+
export const getPackageManagerData = (data: AggregatedAnalyticsData | null) => {
9+
if (!data) return [];
10+
return data.packageManagerDistribution || [];
11+
};
12+
13+
export const getBackendData = (data: AggregatedAnalyticsData | null) => {
14+
if (!data) return [];
15+
return data.backendDistribution || [];
16+
};
17+
18+
export const getDatabaseData = (data: AggregatedAnalyticsData | null) => {
19+
if (!data) return [];
20+
return data.databaseDistribution || [];
21+
};
22+
23+
export const getORMData = (data: AggregatedAnalyticsData | null) => {
24+
if (!data) return [];
25+
return data.ormDistribution || [];
26+
};
27+
28+
export const getDBSetupData = (data: AggregatedAnalyticsData | null) => {
29+
if (!data) return [];
30+
return data.dbSetupDistribution || [];
31+
};
32+
33+
export const getAPIData = (data: AggregatedAnalyticsData | null) => {
34+
if (!data) return [];
35+
return data.apiDistribution || [];
36+
};
37+
38+
export const getFrontendData = (data: AggregatedAnalyticsData | null) => {
39+
if (!data) return [];
40+
return data.frontendDistribution || [];
41+
};
42+
43+
export const getTimeSeriesData = (data: AggregatedAnalyticsData | null) => {
44+
if (!data) return [];
45+
return data.timeSeries || [];
46+
};
47+
48+
export const getNodeVersionData = (data: AggregatedAnalyticsData | null) => {
49+
if (!data) return [];
50+
return data.nodeVersionDistribution || [];
51+
};
52+
53+
export const getCLIVersionData = (data: AggregatedAnalyticsData | null) => {
54+
if (!data) return [];
55+
return data.cliVersionDistribution || [];
56+
};
57+
58+
export const getAuthData = (data: AggregatedAnalyticsData | null) => {
59+
if (!data) return [];
60+
return data.authDistribution || [];
61+
};
62+
63+
export const getGitData = (data: AggregatedAnalyticsData | null) => {
64+
if (!data) return [];
65+
return data.gitDistribution || [];
66+
};
67+
68+
export const getInstallData = (data: AggregatedAnalyticsData | null) => {
69+
if (!data) return [];
70+
return data.installDistribution || [];
71+
};
72+
73+
export const getExamplesData = (data: AggregatedAnalyticsData | null) => {
74+
if (!data) return [];
75+
return data.examplesDistribution || [];
76+
};
77+
78+
export const getAddonsData = (data: AggregatedAnalyticsData | null) => {
79+
if (!data) return [];
80+
return data.addonsDistribution || [];
81+
};
82+
83+
export const getRuntimeData = (data: AggregatedAnalyticsData | null) => {
84+
if (!data) return [];
85+
return data.runtimeDistribution || [];
86+
};
87+
88+
export const getProjectTypeData = (data: AggregatedAnalyticsData | null) => {
89+
if (!data) return [];
90+
return data.projectTypeDistribution || [];
91+
};
92+
93+
export const getMonthlyTimeSeriesData = (
94+
data: AggregatedAnalyticsData | null,
95+
) => {
96+
if (!data) return [];
97+
return data.monthlyTimeSeries || [];
98+
};
99+
100+
export const getPopularStackCombinations = (
101+
data: AggregatedAnalyticsData | null,
102+
) => {
103+
if (!data) return [];
104+
return data.popularStackCombinations || [];
105+
};
106+
107+
export const getDatabaseORMCombinations = (
108+
data: AggregatedAnalyticsData | null,
109+
) => {
110+
if (!data) return [];
111+
return data.databaseORMCombinations || [];
112+
};
113+
114+
export const getHourlyDistributionData = (
115+
data: AggregatedAnalyticsData | null,
116+
) => {
117+
if (!data) return [];
118+
return data.hourlyDistribution || [];
119+
};

0 commit comments

Comments
 (0)