mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
feat(web): add web and server deploy chart in analytics page
This commit is contained in:
@@ -35,6 +35,8 @@ interface AggregatedAnalyticsData {
|
||||
addonsDistribution: Array<{ name: string; value: number }>;
|
||||
runtimeDistribution: Array<{ name: string; value: number }>;
|
||||
projectTypeDistribution: Array<{ name: string; value: number }>;
|
||||
webDeployDistribution: Array<{ name: string; value: number }>;
|
||||
serverDeployDistribution: Array<{ name: string; value: number }>;
|
||||
popularStackCombinations: Array<{ name: string; value: number }>;
|
||||
databaseORMCombinations: Array<{ name: string; value: number }>;
|
||||
hourlyDistribution: Array<{
|
||||
@@ -51,6 +53,8 @@ interface AggregatedAnalyticsData {
|
||||
mostPopularORM: string;
|
||||
mostPopularAPI: string;
|
||||
mostPopularPackageManager: string;
|
||||
mostPopularWebDeploy: string;
|
||||
mostPopularServerDeploy: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -127,6 +131,8 @@ async function generateAnalyticsData() {
|
||||
const addonsCounts: Record<string, number> = {};
|
||||
const runtimeCounts: Record<string, number> = {};
|
||||
const projectTypeCounts: Record<string, number> = {};
|
||||
const webDeployCounts: Record<string, number> = {};
|
||||
const serverDeployCounts: Record<string, number> = {};
|
||||
const stackComboCounts: Record<string, number> = {};
|
||||
const dbORMComboCounts: Record<string, number> = {};
|
||||
const hourlyCounts: Record<number, number> = {};
|
||||
@@ -286,6 +292,18 @@ async function generateAnalyticsData() {
|
||||
const runtime = row["*.properties.runtime"] || "unknown";
|
||||
runtimeCounts[runtime] = (runtimeCounts[runtime] || 0) + 1;
|
||||
|
||||
// Web Deploy (migrate "workers" to "wrangler")
|
||||
const webDeploy = row["*.properties.webDeploy"] || "none";
|
||||
const normalizedWebDeploy =
|
||||
webDeploy === "workers" ? "wrangler" : webDeploy;
|
||||
webDeployCounts[normalizedWebDeploy] =
|
||||
(webDeployCounts[normalizedWebDeploy] || 0) + 1;
|
||||
|
||||
// Server Deploy
|
||||
const serverDeploy = row["*.properties.serverDeploy"] || "none";
|
||||
serverDeployCounts[serverDeploy] =
|
||||
(serverDeployCounts[serverDeploy] || 0) + 1;
|
||||
|
||||
// Project type
|
||||
const hasFrontend =
|
||||
(frontend0 && frontend0 !== "none") ||
|
||||
@@ -492,6 +510,16 @@ async function generateAnalyticsData() {
|
||||
runtimeDistribution: Object.entries(runtimeCounts)
|
||||
.map(([name, value]) => ({ name, value }))
|
||||
.sort((a, b) => b.value - a.value),
|
||||
// Compare only actual deployment platforms (exclude "none")
|
||||
webDeployDistribution: [
|
||||
{ name: "wrangler", value: webDeployCounts.wrangler || 0 },
|
||||
{ name: "alchemy", value: webDeployCounts.alchemy || 0 },
|
||||
].sort((a, b) => b.value - a.value),
|
||||
serverDeployDistribution: [
|
||||
{ name: "wrangler", value: serverDeployCounts.wrangler || 0 },
|
||||
{ name: "alchemy", value: serverDeployCounts.alchemy || 0 },
|
||||
].sort((a, b) => b.value - a.value),
|
||||
|
||||
projectTypeDistribution: Object.entries(projectTypeCounts).map(
|
||||
([name, value]) => ({ name, value }),
|
||||
),
|
||||
@@ -517,6 +545,8 @@ async function generateAnalyticsData() {
|
||||
mostPopularORM: getMostPopular(ormCounts),
|
||||
mostPopularAPI: getMostPopular(apiCounts),
|
||||
mostPopularPackageManager: getMostPopular(packageManagerCounts),
|
||||
mostPopularWebDeploy: getMostPopular(webDeployCounts),
|
||||
mostPopularServerDeploy: getMostPopular(serverDeployCounts),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -31,10 +31,8 @@ function TechIcon({
|
||||
}) {
|
||||
const { theme } = useTheme();
|
||||
|
||||
// If no icon, return empty
|
||||
if (!icon) return null;
|
||||
|
||||
// If it's an emoji or text icon, render as span
|
||||
if (!icon.startsWith("https://")) {
|
||||
return (
|
||||
<span
|
||||
@@ -48,7 +46,6 @@ function TechIcon({
|
||||
);
|
||||
}
|
||||
|
||||
// Handle light theme variants
|
||||
let iconSrc = icon;
|
||||
if (
|
||||
theme === "light" &&
|
||||
@@ -59,7 +56,6 @@ function TechIcon({
|
||||
iconSrc = icon.replace(".svg", "-light.svg");
|
||||
}
|
||||
|
||||
// Render as image
|
||||
return (
|
||||
<Image
|
||||
src={iconSrc}
|
||||
|
||||
@@ -637,7 +637,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
});
|
||||
}
|
||||
|
||||
// Workers runtime requires a server deployment (wrangler or alchemy)
|
||||
if (nextStack.serverDeploy === "none") {
|
||||
notes.serverDeploy.notes.push(
|
||||
"Cloudflare Workers runtime requires a server deployment. Wrangler will be selected.",
|
||||
@@ -898,7 +897,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
});
|
||||
}
|
||||
|
||||
// Server deployment requires a backend (and not Convex)
|
||||
if (
|
||||
nextStack.serverDeploy !== "none" &&
|
||||
(nextStack.backend === "none" || nextStack.backend === "convex")
|
||||
@@ -919,7 +917,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
});
|
||||
}
|
||||
|
||||
// Cloudflare server deployments (wrangler/alchemy) require Workers runtime
|
||||
if (nextStack.serverDeploy !== "none" && nextStack.runtime !== "workers") {
|
||||
notes.serverDeploy.notes.push(
|
||||
"Selected server deployment targets Cloudflare Workers. Runtime will be set to 'Cloudflare Workers'.",
|
||||
@@ -937,7 +934,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
"Runtime set to 'Cloudflare Workers' (required by server deployment)",
|
||||
});
|
||||
|
||||
// Apply Workers runtime compatibility adjustments
|
||||
if (nextStack.backend !== "hono") {
|
||||
notes.runtime.notes.push(
|
||||
"Cloudflare Workers runtime requires Hono backend. Hono will be selected.",
|
||||
@@ -1004,7 +1000,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
}
|
||||
}
|
||||
|
||||
// Alchemy deployment validation - temporarily not compatible with Next.js and React Router
|
||||
const isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
|
||||
const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy";
|
||||
|
||||
@@ -1034,12 +1029,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
notes.webDeploy.hasIssue = true;
|
||||
notes.serverDeploy.hasIssue = true;
|
||||
|
||||
// Remove incompatible frontends
|
||||
nextStack.webFrontend = nextStack.webFrontend.filter(
|
||||
(f) => f !== "next" && f !== "react-router",
|
||||
);
|
||||
|
||||
// If no web frontends remain, set to default
|
||||
if (nextStack.webFrontend.length === 0) {
|
||||
nextStack.webFrontend = ["tanstack-router"];
|
||||
}
|
||||
@@ -1639,7 +1632,6 @@ const StackBuilder = () => {
|
||||
const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
|
||||
const finalStack = adjustedStack ?? simulatedStack;
|
||||
|
||||
// Additional check for Alchemy compatibility with Next.js and React Router
|
||||
if (
|
||||
category === "webFrontend" &&
|
||||
(optionId === "next" || optionId === "react-router")
|
||||
|
||||
@@ -90,6 +90,34 @@ export const getProjectTypeData = (data: AggregatedAnalyticsData | null) => {
|
||||
return data.projectTypeDistribution || [];
|
||||
};
|
||||
|
||||
export const getWebDeployData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
|
||||
const filteredData = (data.webDeployDistribution || []).filter(
|
||||
(item) => item.name !== "none",
|
||||
);
|
||||
|
||||
const wranglerCount = filteredData
|
||||
.filter((item) => item.name === "wrangler" || item.name === "workers")
|
||||
.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
const alchemyCount = filteredData
|
||||
.filter((item) => item.name === "alchemy")
|
||||
.reduce((sum, item) => sum + item.value, 0);
|
||||
|
||||
return [
|
||||
{ name: "wrangler", value: wranglerCount },
|
||||
{ name: "alchemy", value: alchemyCount },
|
||||
].filter((item) => item.value > 0);
|
||||
};
|
||||
|
||||
export const getServerDeployData = (data: AggregatedAnalyticsData | null) => {
|
||||
if (!data) return [];
|
||||
return (data.serverDeployDistribution || []).filter(
|
||||
(item) => item.name !== "none",
|
||||
);
|
||||
};
|
||||
|
||||
export const getMonthlyTimeSeriesData = (
|
||||
data: AggregatedAnalyticsData | null,
|
||||
) => {
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
getPopularStackCombinations,
|
||||
getProjectTypeData,
|
||||
getRuntimeData,
|
||||
getServerDeployData,
|
||||
getWebDeployData,
|
||||
} from "./data-utils";
|
||||
import type { AggregatedAnalyticsData } from "./types";
|
||||
import {
|
||||
@@ -39,6 +41,8 @@ import {
|
||||
ormConfig,
|
||||
projectTypeConfig,
|
||||
runtimeConfig,
|
||||
serverDeployConfig,
|
||||
webDeployConfig,
|
||||
} from "./types";
|
||||
|
||||
interface StackConfigurationChartsProps {
|
||||
@@ -57,6 +61,8 @@ export function StackConfigurationCharts({
|
||||
const authData = getAuthData(data);
|
||||
const runtimeData = getRuntimeData(data);
|
||||
const projectTypeData = getProjectTypeData(data);
|
||||
const webDeployData = getWebDeployData(data);
|
||||
const serverDeployData = getServerDeployData(data);
|
||||
const popularStackCombinations = getPopularStackCombinations(data);
|
||||
const databaseORMCombinations = getDatabaseORMCombinations(data);
|
||||
|
||||
@@ -546,6 +552,108 @@ export function StackConfigurationCharts({
|
||||
</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">
|
||||
WEB_DEPLOYMENT_COMPARISON.PIE
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Web deployment platform comparison (Wrangler vs Alchemy)
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={webDeployConfig}
|
||||
className="h-[300px] w-full"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent nameKey="name" />}
|
||||
/>
|
||||
<Pie
|
||||
data={webDeployData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label={({ name, percent }) =>
|
||||
`${name} ${(percent * 100).toFixed(0)}%`
|
||||
}
|
||||
>
|
||||
{webDeployData.map((entry) => (
|
||||
<Cell
|
||||
key={`web-deploy-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "wrangler"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "alchemy"
|
||||
? "hsl(var(--chart-2))"
|
||||
: "hsl(var(--chart-3))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</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">
|
||||
SERVER_DEPLOYMENT_COMPARISON.PIE
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Server deployment platform comparison (Wrangler vs Alchemy)
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<ChartContainer
|
||||
config={serverDeployConfig}
|
||||
className="h-[300px] w-full"
|
||||
>
|
||||
<PieChart>
|
||||
<ChartTooltip
|
||||
content={<ChartTooltipContent nameKey="name" />}
|
||||
/>
|
||||
<Pie
|
||||
data={serverDeployData}
|
||||
dataKey="value"
|
||||
nameKey="name"
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
outerRadius={80}
|
||||
label={({ name, percent }) =>
|
||||
`${name} ${(percent * 100).toFixed(0)}%`
|
||||
}
|
||||
>
|
||||
{serverDeployData.map((entry) => (
|
||||
<Cell
|
||||
key={`server-deploy-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "wrangler"
|
||||
? "hsl(var(--chart-1))"
|
||||
: entry.name === "alchemy"
|
||||
? "hsl(var(--chart-2))"
|
||||
: "hsl(var(--chart-3))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Pie>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
</PieChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded border border-border">
|
||||
|
||||
@@ -27,6 +27,8 @@ export interface AggregatedAnalyticsData {
|
||||
addonsDistribution: Array<{ name: string; value: number }>;
|
||||
runtimeDistribution: Array<{ name: string; value: number }>;
|
||||
projectTypeDistribution: Array<{ name: string; value: number }>;
|
||||
webDeployDistribution: Array<{ name: string; value: number }>;
|
||||
serverDeployDistribution: Array<{ name: string; value: number }>;
|
||||
popularStackCombinations: Array<{ name: string; value: number }>;
|
||||
databaseORMCombinations: Array<{ name: string; value: number }>;
|
||||
hourlyDistribution: Array<{
|
||||
@@ -43,6 +45,8 @@ export interface AggregatedAnalyticsData {
|
||||
mostPopularORM: string;
|
||||
mostPopularAPI: string;
|
||||
mostPopularPackageManager: string;
|
||||
mostPopularWebDeploy: string;
|
||||
mostPopularServerDeploy: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -396,6 +400,28 @@ export const projectTypeConfig = {
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const webDeployConfig = {
|
||||
wrangler: {
|
||||
label: "Cloudflare Wrangler",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
alchemy: {
|
||||
label: "Alchemy",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const serverDeployConfig = {
|
||||
wrangler: {
|
||||
label: "Cloudflare Wrangler",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
alchemy: {
|
||||
label: "Alchemy",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const hourlyDistributionConfig = {
|
||||
count: {
|
||||
label: "Projects Created",
|
||||
|
||||
Reference in New Issue
Block a user