From d28a2daea4839c258e70a56c0009d9b08e49fcfe Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sat, 23 Aug 2025 23:30:48 +0530 Subject: [PATCH] feat(web): add web and server deploy chart in analytics page --- apps/web/scripts/generate-analytics.ts | 30 +++++ .../app/(home)/_components/FeatureCard.tsx | 4 - .../app/(home)/_components/stack-builder.tsx | 8 -- .../analytics/_components/data-utils.ts | 28 +++++ .../stack-configuration-charts.tsx | 108 ++++++++++++++++++ .../app/(home)/analytics/_components/types.ts | 26 +++++ 6 files changed, 192 insertions(+), 12 deletions(-) diff --git a/apps/web/scripts/generate-analytics.ts b/apps/web/scripts/generate-analytics.ts index f26108b..fe17b6c 100644 --- a/apps/web/scripts/generate-analytics.ts +++ b/apps/web/scripts/generate-analytics.ts @@ -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 = {}; const runtimeCounts: Record = {}; const projectTypeCounts: Record = {}; + const webDeployCounts: Record = {}; + const serverDeployCounts: Record = {}; const stackComboCounts: Record = {}; const dbORMComboCounts: Record = {}; const hourlyCounts: Record = {}; @@ -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), }, }; diff --git a/apps/web/src/app/(home)/_components/FeatureCard.tsx b/apps/web/src/app/(home)/_components/FeatureCard.tsx index 4fc1f08..3f76267 100644 --- a/apps/web/src/app/(home)/_components/FeatureCard.tsx +++ b/apps/web/src/app/(home)/_components/FeatureCard.tsx @@ -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 ( { }); } - // 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") diff --git a/apps/web/src/app/(home)/analytics/_components/data-utils.ts b/apps/web/src/app/(home)/analytics/_components/data-utils.ts index 4105f72..84696a4 100644 --- a/apps/web/src/app/(home)/analytics/_components/data-utils.ts +++ b/apps/web/src/app/(home)/analytics/_components/data-utils.ts @@ -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, ) => { diff --git a/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx b/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx index 7570d42..97074ef 100644 --- a/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx +++ b/apps/web/src/app/(home)/analytics/_components/stack-configuration-charts.tsx @@ -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({ + +
+
+
+ + + WEB_DEPLOYMENT_COMPARISON.PIE + +
+

+ Web deployment platform comparison (Wrangler vs Alchemy) +

+
+
+ + + } + /> + + `${name} ${(percent * 100).toFixed(0)}%` + } + > + {webDeployData.map((entry) => ( + + ))} + + } /> + + +
+
+ +
+
+
+ + + SERVER_DEPLOYMENT_COMPARISON.PIE + +
+

+ Server deployment platform comparison (Wrangler vs Alchemy) +

+
+
+ + + } + /> + + `${name} ${(percent * 100).toFixed(0)}%` + } + > + {serverDeployData.map((entry) => ( + + ))} + + } /> + + +
+
diff --git a/apps/web/src/app/(home)/analytics/_components/types.ts b/apps/web/src/app/(home)/analytics/_components/types.ts index 461a1e9..35c606b 100644 --- a/apps/web/src/app/(home)/analytics/_components/types.ts +++ b/apps/web/src/app/(home)/analytics/_components/types.ts @@ -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",