mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
Add more detailed analytics charts
This commit is contained in:
@@ -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} />
|
||||||
|
|||||||
Reference in New Issue
Block a user