mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
refactor(web): break down analytics page into smaller components
This commit is contained in:
@@ -99,7 +99,7 @@ async function generateAnalyticsData() {
|
||||
.filter((row) => row.trim().length > 0)
|
||||
.map((row) => row.trim());
|
||||
|
||||
csvText = header + "\n" + dataRows.join("\n");
|
||||
csvText = `${header}\n${dataRows.join("\n")}`;
|
||||
console.log(`✅ Manually parsed ${dataRows.length} rows`);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ async function generateAnalyticsData() {
|
||||
|
||||
results.data.forEach((row, index) => {
|
||||
// Skip rows that don't have essential data
|
||||
if (!row["*.timestamp"] && !row["timestamp"]) {
|
||||
if (!row["*.timestamp"] && !row.timestamp) {
|
||||
if (index < 5) {
|
||||
console.log(
|
||||
`⚠️ Skipping row ${index} - no timestamp:`,
|
||||
@@ -156,9 +156,7 @@ async function generateAnalyticsData() {
|
||||
}
|
||||
|
||||
const timestamp =
|
||||
row["*.timestamp"] ||
|
||||
row["timestamp"] ||
|
||||
new Date().toISOString();
|
||||
row["*.timestamp"] || row.timestamp || new Date().toISOString();
|
||||
const date = timestamp.includes("T")
|
||||
? timestamp.split("T")[0]
|
||||
: timestamp.split(" ")[0];
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Bar, BarChart, CartesianGrid, Cell, XAxis, YAxis } from "recharts";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { getAddonsData, getExamplesData } from "./data-utils";
|
||||
import type { AggregatedAnalyticsData } from "./types";
|
||||
import { addonsConfig, examplesConfig } from "./types";
|
||||
|
||||
interface AddonsExamplesChartsProps {
|
||||
data: AggregatedAnalyticsData | null;
|
||||
}
|
||||
|
||||
export function AddonsExamplesCharts({ data }: AddonsExamplesChartsProps) {
|
||||
const addonsData = getAddonsData(data);
|
||||
const examplesData = getExamplesData(data);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">ADDONS_USAGE.BAR</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Additional features and tooling adoption
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={addonsConfig} className="h-[350px] w-full">
|
||||
<BarChart data={addonsData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
className=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{addonsData.map((entry) => (
|
||||
<Cell
|
||||
key={`addons-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "pwa"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "biome"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "tauri"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "husky"
|
||||
? "hsl(var(--chart-4))"
|
||||
: entry.name === "starlight"
|
||||
? "hsl(var(--chart-5))"
|
||||
: entry.name === "turborepo"
|
||||
? "hsl(var(--chart-6))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">EXAMPLES_USAGE.BAR</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Example applications included in projects
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={examplesConfig} className="h-[300px] w-full">
|
||||
<BarChart data={examplesData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
className=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{examplesData.map((entry) => (
|
||||
<Cell
|
||||
key={`examples-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "todo"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "ai"
|
||||
? "hsl(var(--chart-2))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Terminal } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import discordIcon from "@/public/icon/discord.svg";
|
||||
|
||||
interface AnalyticsHeaderProps {
|
||||
totalProjects: number;
|
||||
lastUpdated: string | null;
|
||||
loadingLastUpdated: boolean;
|
||||
}
|
||||
|
||||
export function AnalyticsHeader({
|
||||
totalProjects,
|
||||
lastUpdated,
|
||||
loadingLastUpdated,
|
||||
}: AnalyticsHeaderProps) {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<Terminal className="h-5 w-5 text-primary" />
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
ANALYTICS_DASHBOARD.SH
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
|
||||
[{totalProjects} PROJECTS_ANALYZED]
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded rounded-b-none border border-border p-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className=" text-foreground">
|
||||
Analytics from Better-T-Stack CLI usage data
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className=" text-muted-foreground">
|
||||
Uses PostHog - no personal info tracked, runs on each project
|
||||
creation
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className=" text-muted-foreground">
|
||||
Source:{" "}
|
||||
<Link
|
||||
href="https://github.com/amanvarshney01/create-better-t-stack/blob/main/apps/cli/src/utils/analytics.ts"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent underline hover:text-primary"
|
||||
>
|
||||
analytics.ts
|
||||
</Link>
|
||||
{" | "}
|
||||
<Link
|
||||
href="https://r2.amanv.dev/export.csv"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-accent underline hover:text-primary"
|
||||
>
|
||||
export.csv
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 text-sm">
|
||||
<span className="text-primary">$</span>
|
||||
<span className=" text-muted-foreground">
|
||||
Last updated:{" "}
|
||||
{loadingLastUpdated
|
||||
? "CHECKING..."
|
||||
: lastUpdated
|
||||
? `${lastUpdated} UTC`
|
||||
: "UNKNOWN"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="https://discord.gg/ZYsbjpDaM5"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block rounded rounded-t-none border border-border border-t-0"
|
||||
>
|
||||
<div className="flex items-center justify-between p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Image
|
||||
src={discordIcon}
|
||||
alt="discord"
|
||||
className="h-4 w-4 invert-0 dark:invert"
|
||||
/>
|
||||
<div>
|
||||
<span className=" font-semibold text-sm">
|
||||
DISCORD_NOTIFICATIONS.IRC
|
||||
</span>
|
||||
<p className=" text-muted-foreground text-xs">
|
||||
Join for LIVE project creation alerts
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 rounded border border-border bg-primary/10 px-2 py-1">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-primary text-xs">JOIN</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
119
apps/web/src/app/(home)/analytics/_components/data-utils.ts
Normal file
119
apps/web/src/app/(home)/analytics/_components/data-utils.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { AggregatedAnalyticsData } from "./types";
|
||||
|
||||
export const getPlatformData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.platformDistribution || [];
|
||||
};
|
||||
|
||||
export const getPackageManagerData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.packageManagerDistribution || [];
|
||||
};
|
||||
|
||||
export const getBackendData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.backendDistribution || [];
|
||||
};
|
||||
|
||||
export const getDatabaseData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.databaseDistribution || [];
|
||||
};
|
||||
|
||||
export const getORMData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.ormDistribution || [];
|
||||
};
|
||||
|
||||
export const getDBSetupData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.dbSetupDistribution || [];
|
||||
};
|
||||
|
||||
export const getAPIData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.apiDistribution || [];
|
||||
};
|
||||
|
||||
export const getFrontendData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.frontendDistribution || [];
|
||||
};
|
||||
|
||||
export const getTimeSeriesData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.timeSeries || [];
|
||||
};
|
||||
|
||||
export const getNodeVersionData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.nodeVersionDistribution || [];
|
||||
};
|
||||
|
||||
export const getCLIVersionData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.cliVersionDistribution || [];
|
||||
};
|
||||
|
||||
export const getAuthData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.authDistribution || [];
|
||||
};
|
||||
|
||||
export const getGitData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.gitDistribution || [];
|
||||
};
|
||||
|
||||
export const getInstallData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.installDistribution || [];
|
||||
};
|
||||
|
||||
export const getExamplesData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.examplesDistribution || [];
|
||||
};
|
||||
|
||||
export const getAddonsData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.addonsDistribution || [];
|
||||
};
|
||||
|
||||
export const getRuntimeData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.runtimeDistribution || [];
|
||||
};
|
||||
|
||||
export const getProjectTypeData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return data.projectTypeDistribution || [];
|
||||
};
|
||||
|
||||
export const getMonthlyTimeSeriesData = (
|
||||
data: AggregatedAnalyticsData | null,
|
||||
) => {
|
||||
if (!data) return [];
|
||||
return data.monthlyTimeSeries || [];
|
||||
};
|
||||
|
||||
export const getPopularStackCombinations = (
|
||||
data: AggregatedAnalyticsData | null,
|
||||
) => {
|
||||
if (!data) return [];
|
||||
return data.popularStackCombinations || [];
|
||||
};
|
||||
|
||||
export const getDatabaseORMCombinations = (
|
||||
data: AggregatedAnalyticsData | null,
|
||||
) => {
|
||||
if (!data) return [];
|
||||
return data.databaseORMCombinations || [];
|
||||
};
|
||||
|
||||
export const getHourlyDistributionData = (
|
||||
data: AggregatedAnalyticsData | null,
|
||||
) => {
|
||||
if (!data) return [];
|
||||
return data.hourlyDistribution || [];
|
||||
};
|
||||
@@ -0,0 +1,261 @@
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import {
|
||||
getCLIVersionData,
|
||||
getGitData,
|
||||
getInstallData,
|
||||
getNodeVersionData,
|
||||
getPackageManagerData,
|
||||
} from "./data-utils";
|
||||
import type { AggregatedAnalyticsData } from "./types";
|
||||
import {
|
||||
cliVersionConfig,
|
||||
gitConfig,
|
||||
installConfig,
|
||||
nodeVersionConfig,
|
||||
packageManagerConfig,
|
||||
} from "./types";
|
||||
|
||||
interface DevEnvironmentChartsProps {
|
||||
data: AggregatedAnalyticsData | null;
|
||||
}
|
||||
|
||||
export function DevEnvironmentCharts({ data }: DevEnvironmentChartsProps) {
|
||||
const gitData = getGitData(data);
|
||||
const packageManagerData = getPackageManagerData(data);
|
||||
const installData = getInstallData(data);
|
||||
const nodeVersionData = getNodeVersionData(data);
|
||||
const cliVersionData = getCLIVersionData(data);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<span className="font-bold text-lg">DEV_ENVIRONMENT.CONFIG</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
GIT_INITIALIZATION.PIE
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Git repository initialization rate
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={gitConfig} className="h-[300px] w-full">
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent nameKey="name" />}
|
||||
/>
|
||||
<Pie
|
||||
data={gitData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label={({ name, percent }) =>
|
||||
`${name} ${(percent * 100).toFixed(0)}%`
|
||||
}
|
||||
>
|
||||
{gitData.map((entry) => (
|
||||
<Cell
|
||||
key={`git-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "enabled"
|
||||
? "hsl(var(--chart-1))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
PACKAGE_MANAGER.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Package manager usage distribution
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={packageManagerConfig}
|
||||
className="h-[300px] w-full"
|
||||
>
|
||||
<BarChart data={packageManagerData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{packageManagerData.map((entry) => (
|
||||
<Cell
|
||||
key={`package-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "npm"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "pnpm"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "bun"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "yarn"
|
||||
? "hsl(var(--chart-4))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
INSTALL_PREFERENCE.PIE
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Automatic dependency installation preference
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={installConfig} className="h-[300px] w-full">
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent nameKey="name" />}
|
||||
/>
|
||||
<Pie
|
||||
data={installData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label={({ name, percent }) =>
|
||||
`${name} ${(percent * 100).toFixed(0)}%`
|
||||
}
|
||||
>
|
||||
{installData.map((entry) => (
|
||||
<Cell
|
||||
key={`install-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "enabled"
|
||||
? "hsl(var(--chart-1))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">NODE_VERSIONS.BAR</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Node.js version distribution (major versions)
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={nodeVersionConfig}
|
||||
className="h-[300px] w-full"
|
||||
>
|
||||
<BarChart data={nodeVersionData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="version"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
className=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="count" radius={4} fill="hsl(var(--chart-1))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">CLI_VERSIONS.BAR</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
CLI version distribution across project creations
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={cliVersionConfig}
|
||||
className="h-[350px] w-full"
|
||||
>
|
||||
<BarChart data={cliVersionData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="version"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
className=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="count" radius={4} fill="hsl(var(--chart-1))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
apps/web/src/app/(home)/analytics/_components/index.ts
Normal file
8
apps/web/src/app/(home)/analytics/_components/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { AddonsExamplesCharts } from "./addons-examples-charts";
|
||||
export { AnalyticsHeader } from "./analytics-header";
|
||||
export * from "./data-utils";
|
||||
export { DevEnvironmentCharts } from "./dev-environment-charts";
|
||||
export { MetricsCards } from "./metrics-cards";
|
||||
export { StackConfigurationCharts } from "./stack-configuration-charts";
|
||||
export { TimelineCharts } from "./timeline-charts";
|
||||
export * from "./types";
|
||||
170
apps/web/src/app/(home)/analytics/_components/metrics-cards.tsx
Normal file
170
apps/web/src/app/(home)/analytics/_components/metrics-cards.tsx
Normal file
@@ -0,0 +1,170 @@
|
||||
import { Cpu, Download, Terminal, TrendingUp, Users } from "lucide-react";
|
||||
|
||||
interface MetricsCardsProps {
|
||||
totalProjects: number;
|
||||
avgProjectsPerDay: number;
|
||||
authEnabledPercent: number;
|
||||
mostPopularFrontend: string;
|
||||
mostPopularBackend: string;
|
||||
mostPopularORM: string;
|
||||
mostPopularAPI: string;
|
||||
mostPopularPackageManager: string;
|
||||
}
|
||||
|
||||
export function MetricsCards({
|
||||
totalProjects,
|
||||
avgProjectsPerDay,
|
||||
authEnabledPercent,
|
||||
mostPopularFrontend,
|
||||
mostPopularBackend,
|
||||
mostPopularORM,
|
||||
mostPopularAPI,
|
||||
mostPopularPackageManager,
|
||||
}: MetricsCardsProps) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<span className="font-bold text-lg">SYSTEM_METRICS.LOG</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-4">
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">TOTAL_PROJECTS</span>
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="font-bold text-2xl text-primary">
|
||||
{totalProjects.toLocaleString()}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ ./create-better-t-stack executions
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">TOP_FRONTEND</span>
|
||||
<Cpu className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="truncate font-bold text-accent text-lg">
|
||||
{mostPopularFrontend}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ most_selected_frontend.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">TOP_BACKEND</span>
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="truncate font-bold text-accent text-lg">
|
||||
{mostPopularBackend}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ most_selected_backend.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">TOP_ORM</span>
|
||||
<Download className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="truncate font-bold text-accent text-lg">
|
||||
{mostPopularORM}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ most_selected_orm.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">TOP_API</span>
|
||||
<TrendingUp className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="truncate font-bold text-accent text-lg">
|
||||
{mostPopularAPI}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ most_selected_api.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">AUTH_ADOPTION</span>
|
||||
<Users className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="font-bold text-2xl text-primary">
|
||||
{authEnabledPercent}%
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ auth_enabled_percentage.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">TOP_PKG_MGR</span>
|
||||
<Terminal className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="truncate font-bold text-accent text-lg">
|
||||
{mostPopularPackageManager}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ most_used_package_manager.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className=" font-semibold text-sm">AVG_DAILY</span>
|
||||
<TrendingUp className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<div className="font-bold text-2xl text-primary">
|
||||
{avgProjectsPerDay.toFixed(1)}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ average_projects_per_day.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,583 @@
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import {
|
||||
getAPIData,
|
||||
getAuthData,
|
||||
getBackendData,
|
||||
getDatabaseData,
|
||||
getDatabaseORMCombinations,
|
||||
getDBSetupData,
|
||||
getFrontendData,
|
||||
getORMData,
|
||||
getPopularStackCombinations,
|
||||
getProjectTypeData,
|
||||
getRuntimeData,
|
||||
} from "./data-utils";
|
||||
import type { AggregatedAnalyticsData } from "./types";
|
||||
import {
|
||||
apiConfig,
|
||||
authConfig,
|
||||
backendConfig,
|
||||
databaseConfig,
|
||||
dbSetupConfig,
|
||||
frontendConfig,
|
||||
ormConfig,
|
||||
projectTypeConfig,
|
||||
runtimeConfig,
|
||||
} from "./types";
|
||||
|
||||
interface StackConfigurationChartsProps {
|
||||
data: AggregatedAnalyticsData | null;
|
||||
}
|
||||
|
||||
export function StackConfigurationCharts({
|
||||
data,
|
||||
}: StackConfigurationChartsProps) {
|
||||
const backendData = getBackendData(data);
|
||||
const databaseData = getDatabaseData(data);
|
||||
const ormData = getORMData(data);
|
||||
const dbSetupData = getDBSetupData(data);
|
||||
const apiData = getAPIData(data);
|
||||
const frontendData = getFrontendData(data);
|
||||
const authData = getAuthData(data);
|
||||
const runtimeData = getRuntimeData(data);
|
||||
const projectTypeData = getProjectTypeData(data);
|
||||
const popularStackCombinations = getPopularStackCombinations(data);
|
||||
const databaseORMCombinations = getDatabaseORMCombinations(data);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="mb-6 flex flex-wrap items-center justify-between gap-2 sm:flex-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-lg sm:text-xl">
|
||||
STACK_CONFIGURATION.DB
|
||||
</span>
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-border sm:block" />
|
||||
<span className="w-full text-right text-muted-foreground text-xs sm:w-auto sm:text-left">
|
||||
[CORE_COMPONENTS]
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
POPULAR_STACK_COMBINATIONS.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 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=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4} fill="hsl(var(--chart-1))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
FRONTEND_FRAMEWORKS.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Frontend framework and meta-framework usage
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={frontendConfig} className="h-[350px] w-full">
|
||||
<BarChart data={frontendData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
className=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{frontendData.map((entry) => (
|
||||
<Cell
|
||||
key={`frontend-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "react-router"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "tanstack-router"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "tanstack-start"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "next"
|
||||
? "hsl(var(--chart-4))"
|
||||
: entry.name === "nuxt"
|
||||
? "hsl(var(--chart-5))"
|
||||
: entry.name === "native-nativewind"
|
||||
? "hsl(var(--chart-6))"
|
||||
: entry.name === "native-unistyles"
|
||||
? "hsl(var(--chart-7))"
|
||||
: entry.name === "svelte"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "solid"
|
||||
? "hsl(var(--chart-4))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
BACKEND_FRAMEWORKS.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Backend framework distribution
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={backendConfig} className="h-[300px] w-full">
|
||||
<BarChart data={backendData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{backendData.map((entry) => (
|
||||
<Cell
|
||||
key={`backend-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "hono"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "express"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "fastify"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "next"
|
||||
? "hsl(var(--chart-4))"
|
||||
: entry.name === "elysia"
|
||||
? "hsl(var(--chart-5))"
|
||||
: entry.name === "convex"
|
||||
? "hsl(var(--chart-6))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
DATABASE_DISTRIBUTION.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Database technology distribution
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={databaseConfig}
|
||||
className="h-[300px] w-full"
|
||||
>
|
||||
<BarChart data={databaseData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{databaseData.map((entry) => (
|
||||
<Cell
|
||||
key={`database-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "sqlite"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "postgres"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "mysql"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "mongodb"
|
||||
? "hsl(var(--chart-4))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
ORM_DISTRIBUTION.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
ORM/Database layer distribution
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={ormConfig} className="h-[300px] w-full">
|
||||
<BarChart data={ormData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{ormData.map((entry) => (
|
||||
<Cell
|
||||
key={`orm-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "drizzle"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "prisma"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "mongoose"
|
||||
? "hsl(var(--chart-3))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
DATABASE_HOSTING.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Database hosting service preferences
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={dbSetupConfig} className="h-[300px] w-full">
|
||||
<BarChart data={dbSetupData}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4}>
|
||||
{dbSetupData.map((entry) => (
|
||||
<Cell
|
||||
key={`dbsetup-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "turso"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "prisma-postgres"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "mongodb-atlas"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "neon"
|
||||
? "hsl(var(--chart-4))"
|
||||
: entry.name === "supabase"
|
||||
? "hsl(var(--chart-5))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">API_LAYER.PIE</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
API layer technology distribution
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={apiConfig} className="h-[300px] w-full">
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent nameKey="name" />}
|
||||
/>
|
||||
<Pie
|
||||
data={apiData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
>
|
||||
{apiData.map((entry) => (
|
||||
<Cell
|
||||
key={`api-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "trpc"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "orpc"
|
||||
? "hsl(var(--chart-2))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">AUTH_ADOPTION.PIE</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Authentication implementation rate
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer config={authConfig} className="h-[300px] w-full">
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent nameKey="name" />}
|
||||
/>
|
||||
<Pie
|
||||
data={authData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label={({ name, percent }) =>
|
||||
`${name} ${(percent * 100).toFixed(0)}%`
|
||||
}
|
||||
>
|
||||
{authData.map((entry) => (
|
||||
<Cell
|
||||
key={`auth-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "enabled"
|
||||
? "hsl(var(--chart-1))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
RUNTIME_DISTRIBUTION.PIE
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 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={
|
||||
entry.name === "node"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "bun"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "workers"
|
||||
? "hsl(var(--chart-3))"
|
||||
: "hsl(var(--chart-7))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">PROJECT_TYPES.PIE</span>
|
||||
</div>
|
||||
<p className="mt-1 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={
|
||||
entry.name === "fullstack"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "frontend-only"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "backend-only"
|
||||
? "hsl(var(--chart-3))"
|
||||
: "hsl(var(--chart-4))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
DATABASE_ORM_COMBINATIONS.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 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=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="value" radius={4} fill="hsl(var(--chart-1))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import { format, parseISO } from "date-fns";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
Pie,
|
||||
PieChart,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import {
|
||||
ChartContainer,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import {
|
||||
getHourlyDistributionData,
|
||||
getMonthlyTimeSeriesData,
|
||||
getPlatformData,
|
||||
getTimeSeriesData,
|
||||
} from "./data-utils";
|
||||
import type { AggregatedAnalyticsData } from "./types";
|
||||
import {
|
||||
hourlyDistributionConfig,
|
||||
platformConfig,
|
||||
timeSeriesConfig,
|
||||
} from "./types";
|
||||
|
||||
interface TimelineChartsProps {
|
||||
data: AggregatedAnalyticsData | null;
|
||||
}
|
||||
|
||||
export function TimelineCharts({ data }: TimelineChartsProps) {
|
||||
const timeSeriesData = getTimeSeriesData(data);
|
||||
const monthlyTimeSeriesData = getMonthlyTimeSeriesData(data);
|
||||
const platformData = getPlatformData(data);
|
||||
const hourlyDistributionData = getHourlyDistributionData(data);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="mb-4 flex items-center gap-2">
|
||||
<span className="font-bold text-lg">TIMELINE_ANALYSIS.CHARTS</span>
|
||||
<div className="h-px flex-1 bg-border" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
PROJECT_TIMELINE.CHART
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Daily project creation timeline from actual data
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={timeSeriesConfig}
|
||||
className="h-[300px] w-full"
|
||||
>
|
||||
<AreaChart data={timeSeriesData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="displayDate" />
|
||||
<YAxis />
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent />}
|
||||
labelFormatter={(value, payload) => {
|
||||
const date = payload?.[0]?.payload?.date;
|
||||
return date
|
||||
? format(parseISO(date), "MMM dd, yyyy")
|
||||
: value;
|
||||
}}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="count"
|
||||
stroke="hsl(var(--chart-1))"
|
||||
fill="hsl(var(--chart-1))"
|
||||
fillOpacity={0.2}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
MONTHLY_TRENDS.CHART
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 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=" text-xs"
|
||||
/>
|
||||
<YAxis hide />
|
||||
<ChartTooltip content={<ChartTooltipContent />} />
|
||||
<Bar dataKey="count" radius={4} fill="hsl(var(--chart-1))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
PLATFORM_DISTRIBUTION.PIE
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Operating system distribution
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={platformConfig}
|
||||
className="h-[300px] w-full"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent nameKey="name" />}
|
||||
/>
|
||||
<Pie
|
||||
data={platformData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) =>
|
||||
`${name} ${(percent * 100).toFixed(0)}%`
|
||||
}
|
||||
outerRadius={80}
|
||||
fill="hsl(var(--chart-1))"
|
||||
dataKey="value"
|
||||
>
|
||||
{platformData.map((entry) => (
|
||||
<Cell
|
||||
key={entry.name}
|
||||
fill={
|
||||
entry.name === "darwin"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "linux"
|
||||
? "hsl(var(--chart-2))"
|
||||
: entry.name === "win32"
|
||||
? "hsl(var(--chart-3))"
|
||||
: entry.name === "android"
|
||||
? "hsl(var(--chart-4))"
|
||||
: "hsl(var(--chart-5))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
<div className="border-border border-b px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-primary text-xs">▶</span>
|
||||
<span className=" font-semibold text-sm">
|
||||
HOURLY_DISTRIBUTION.BAR
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 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=" 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="hsl(var(--chart-1))" />
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
404
apps/web/src/app/(home)/analytics/_components/types.ts
Normal file
404
apps/web/src/app/(home)/analytics/_components/types.ts
Normal file
@@ -0,0 +1,404 @@
|
||||
import type { ChartConfig } from "@/components/ui/chart";
|
||||
|
||||
export interface AggregatedAnalyticsData {
|
||||
lastUpdated: string | null;
|
||||
generatedAt: string;
|
||||
totalRecords: number;
|
||||
timeSeries: Array<{ date: string; displayDate: string; count: number }>;
|
||||
monthlyTimeSeries: Array<{
|
||||
month: string;
|
||||
displayMonth: string;
|
||||
count: number;
|
||||
}>;
|
||||
platformDistribution: Array<{ name: string; value: number }>;
|
||||
packageManagerDistribution: Array<{ name: string; value: number }>;
|
||||
backendDistribution: Array<{ name: string; value: number }>;
|
||||
databaseDistribution: Array<{ name: string; value: number }>;
|
||||
ormDistribution: Array<{ name: string; value: number }>;
|
||||
dbSetupDistribution: Array<{ name: string; value: number }>;
|
||||
apiDistribution: Array<{ name: string; value: number }>;
|
||||
frontendDistribution: Array<{ name: string; value: number }>;
|
||||
nodeVersionDistribution: Array<{ version: string; count: number }>;
|
||||
cliVersionDistribution: Array<{ version: string; count: number }>;
|
||||
authDistribution: Array<{ name: string; value: number }>;
|
||||
gitDistribution: Array<{ name: string; value: number }>;
|
||||
installDistribution: Array<{ name: string; value: number }>;
|
||||
examplesDistribution: Array<{ name: string; value: number }>;
|
||||
addonsDistribution: Array<{ name: string; value: number }>;
|
||||
runtimeDistribution: Array<{ name: string; value: number }>;
|
||||
projectTypeDistribution: Array<{ name: string; value: number }>;
|
||||
popularStackCombinations: Array<{ name: string; value: number }>;
|
||||
databaseORMCombinations: Array<{ name: string; value: number }>;
|
||||
hourlyDistribution: Array<{
|
||||
hour: string;
|
||||
displayHour: string;
|
||||
count: number;
|
||||
}>;
|
||||
summary: {
|
||||
totalProjects: number;
|
||||
avgProjectsPerDay: number;
|
||||
authEnabledPercent: number;
|
||||
mostPopularFrontend: string;
|
||||
mostPopularBackend: string;
|
||||
mostPopularORM: string;
|
||||
mostPopularAPI: string;
|
||||
mostPopularPackageManager: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const timeSeriesConfig = {
|
||||
projects: {
|
||||
label: "Projects Created",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const platformConfig = {
|
||||
darwin: {
|
||||
label: "macOS",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
linux: {
|
||||
label: "Linux",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
win32: {
|
||||
label: "Windows",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
android: {
|
||||
label: "Android",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const packageManagerConfig = {
|
||||
npm: {
|
||||
label: "npm",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
pnpm: {
|
||||
label: "pnpm",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
bun: {
|
||||
label: "bun",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const backendConfig = {
|
||||
hono: {
|
||||
label: "Hono",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
express: {
|
||||
label: "Express",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
fastify: {
|
||||
label: "Fastify",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
next: {
|
||||
label: "Next.js",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
elysia: {
|
||||
label: "Elysia",
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
convex: {
|
||||
label: "Convex",
|
||||
color: "hsl(var(--chart-6))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-7))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const databaseConfig = {
|
||||
sqlite: {
|
||||
label: "SQLite",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
postgres: {
|
||||
label: "PostgreSQL",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
mysql: {
|
||||
label: "MySQL",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
mongodb: {
|
||||
label: "MongoDB",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const ormConfig = {
|
||||
drizzle: {
|
||||
label: "Drizzle",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
prisma: {
|
||||
label: "Prisma",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
mongoose: {
|
||||
label: "Mongoose",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const dbSetupConfig = {
|
||||
turso: {
|
||||
label: "Turso",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
"prisma-postgres": {
|
||||
label: "Prisma Postgres",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
"mongodb-atlas": {
|
||||
label: "MongoDB Atlas",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
neon: {
|
||||
label: "Neon",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
supabase: {
|
||||
label: "Supabase",
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-6))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const apiConfig = {
|
||||
trpc: {
|
||||
label: "tRPC",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
orpc: {
|
||||
label: "oRPC",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const frontendConfig = {
|
||||
"react-router": {
|
||||
label: "React Router",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
"tanstack-router": {
|
||||
label: "TanStack Router",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
"tanstack-start": {
|
||||
label: "TanStack Start",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
next: {
|
||||
label: "Next",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
nuxt: {
|
||||
label: "Nuxt",
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
"native-nativewind": {
|
||||
label: "Expo NativeWind",
|
||||
color: "hsl(var(--chart-6))",
|
||||
},
|
||||
"native-unistyles": {
|
||||
label: "Expo Unistyles",
|
||||
color: "hsl(var(--chart-7))",
|
||||
},
|
||||
svelte: {
|
||||
label: "Svelte",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
solid: {
|
||||
label: "Solid",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-7))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const nodeVersionConfig = {
|
||||
"18": {
|
||||
label: "Node.js 18",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
"20": {
|
||||
label: "Node.js 20",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
"22": {
|
||||
label: "Node.js 22",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
"16": {
|
||||
label: "Node.js 16",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
other: {
|
||||
label: "Other",
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const cliVersionConfig = {
|
||||
latest: {
|
||||
label: "Latest",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
outdated: {
|
||||
label: "Outdated",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const authConfig = {
|
||||
enabled: {
|
||||
label: "Enabled",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
disabled: {
|
||||
label: "Disabled",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const gitConfig = {
|
||||
enabled: {
|
||||
label: "Git Init",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
disabled: {
|
||||
label: "No Git",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const installConfig = {
|
||||
enabled: {
|
||||
label: "Auto Install",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
disabled: {
|
||||
label: "Skip Install",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const examplesConfig = {
|
||||
todo: {
|
||||
label: "Todo App",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
ai: {
|
||||
label: "AI Example",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
none: {
|
||||
label: "No Examples",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const addonsConfig = {
|
||||
pwa: {
|
||||
label: "PWA",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
biome: {
|
||||
label: "Biome",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
tauri: {
|
||||
label: "Tauri",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
husky: {
|
||||
label: "Husky",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
starlight: {
|
||||
label: "Starlight",
|
||||
color: "hsl(var(--chart-5))",
|
||||
},
|
||||
turborepo: {
|
||||
label: "Turborepo",
|
||||
color: "hsl(var(--chart-6))",
|
||||
},
|
||||
none: {
|
||||
label: "No Addons",
|
||||
color: "hsl(var(--chart-7))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const runtimeConfig = {
|
||||
node: {
|
||||
label: "Node.js",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
bun: {
|
||||
label: "Bun",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
workers: {
|
||||
label: "Cloudflare Workers",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export 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))",
|
||||
},
|
||||
none: {
|
||||
label: "None",
|
||||
color: "hsl(var(--chart-4))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const hourlyDistributionConfig = {
|
||||
count: {
|
||||
label: "Projects Created",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user