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 }>;
|
addonsDistribution: Array<{ name: string; value: number }>;
|
||||||
runtimeDistribution: Array<{ name: string; value: number }>;
|
runtimeDistribution: Array<{ name: string; value: number }>;
|
||||||
projectTypeDistribution: 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 }>;
|
popularStackCombinations: Array<{ name: string; value: number }>;
|
||||||
databaseORMCombinations: Array<{ name: string; value: number }>;
|
databaseORMCombinations: Array<{ name: string; value: number }>;
|
||||||
hourlyDistribution: Array<{
|
hourlyDistribution: Array<{
|
||||||
@@ -51,6 +53,8 @@ interface AggregatedAnalyticsData {
|
|||||||
mostPopularORM: string;
|
mostPopularORM: string;
|
||||||
mostPopularAPI: string;
|
mostPopularAPI: string;
|
||||||
mostPopularPackageManager: string;
|
mostPopularPackageManager: string;
|
||||||
|
mostPopularWebDeploy: string;
|
||||||
|
mostPopularServerDeploy: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +131,8 @@ async function generateAnalyticsData() {
|
|||||||
const addonsCounts: Record<string, number> = {};
|
const addonsCounts: Record<string, number> = {};
|
||||||
const runtimeCounts: Record<string, number> = {};
|
const runtimeCounts: Record<string, number> = {};
|
||||||
const projectTypeCounts: Record<string, number> = {};
|
const projectTypeCounts: Record<string, number> = {};
|
||||||
|
const webDeployCounts: Record<string, number> = {};
|
||||||
|
const serverDeployCounts: Record<string, number> = {};
|
||||||
const stackComboCounts: Record<string, number> = {};
|
const stackComboCounts: Record<string, number> = {};
|
||||||
const dbORMComboCounts: Record<string, number> = {};
|
const dbORMComboCounts: Record<string, number> = {};
|
||||||
const hourlyCounts: Record<number, number> = {};
|
const hourlyCounts: Record<number, number> = {};
|
||||||
@@ -286,6 +292,18 @@ async function generateAnalyticsData() {
|
|||||||
const runtime = row["*.properties.runtime"] || "unknown";
|
const runtime = row["*.properties.runtime"] || "unknown";
|
||||||
runtimeCounts[runtime] = (runtimeCounts[runtime] || 0) + 1;
|
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
|
// Project type
|
||||||
const hasFrontend =
|
const hasFrontend =
|
||||||
(frontend0 && frontend0 !== "none") ||
|
(frontend0 && frontend0 !== "none") ||
|
||||||
@@ -492,6 +510,16 @@ async function generateAnalyticsData() {
|
|||||||
runtimeDistribution: Object.entries(runtimeCounts)
|
runtimeDistribution: Object.entries(runtimeCounts)
|
||||||
.map(([name, value]) => ({ name, value }))
|
.map(([name, value]) => ({ name, value }))
|
||||||
.sort((a, b) => b.value - a.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(
|
projectTypeDistribution: Object.entries(projectTypeCounts).map(
|
||||||
([name, value]) => ({ name, value }),
|
([name, value]) => ({ name, value }),
|
||||||
),
|
),
|
||||||
@@ -517,6 +545,8 @@ async function generateAnalyticsData() {
|
|||||||
mostPopularORM: getMostPopular(ormCounts),
|
mostPopularORM: getMostPopular(ormCounts),
|
||||||
mostPopularAPI: getMostPopular(apiCounts),
|
mostPopularAPI: getMostPopular(apiCounts),
|
||||||
mostPopularPackageManager: getMostPopular(packageManagerCounts),
|
mostPopularPackageManager: getMostPopular(packageManagerCounts),
|
||||||
|
mostPopularWebDeploy: getMostPopular(webDeployCounts),
|
||||||
|
mostPopularServerDeploy: getMostPopular(serverDeployCounts),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,8 @@ function TechIcon({
|
|||||||
}) {
|
}) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
// If no icon, return empty
|
|
||||||
if (!icon) return null;
|
if (!icon) return null;
|
||||||
|
|
||||||
// If it's an emoji or text icon, render as span
|
|
||||||
if (!icon.startsWith("https://")) {
|
if (!icon.startsWith("https://")) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
@@ -48,7 +46,6 @@ function TechIcon({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle light theme variants
|
|
||||||
let iconSrc = icon;
|
let iconSrc = icon;
|
||||||
if (
|
if (
|
||||||
theme === "light" &&
|
theme === "light" &&
|
||||||
@@ -59,7 +56,6 @@ function TechIcon({
|
|||||||
iconSrc = icon.replace(".svg", "-light.svg");
|
iconSrc = icon.replace(".svg", "-light.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render as image
|
|
||||||
return (
|
return (
|
||||||
<Image
|
<Image
|
||||||
src={iconSrc}
|
src={iconSrc}
|
||||||
|
|||||||
@@ -637,7 +637,6 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workers runtime requires a server deployment (wrangler or alchemy)
|
|
||||||
if (nextStack.serverDeploy === "none") {
|
if (nextStack.serverDeploy === "none") {
|
||||||
notes.serverDeploy.notes.push(
|
notes.serverDeploy.notes.push(
|
||||||
"Cloudflare Workers runtime requires a server deployment. Wrangler will be selected.",
|
"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 (
|
if (
|
||||||
nextStack.serverDeploy !== "none" &&
|
nextStack.serverDeploy !== "none" &&
|
||||||
(nextStack.backend === "none" || nextStack.backend === "convex")
|
(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") {
|
if (nextStack.serverDeploy !== "none" && nextStack.runtime !== "workers") {
|
||||||
notes.serverDeploy.notes.push(
|
notes.serverDeploy.notes.push(
|
||||||
"Selected server deployment targets Cloudflare Workers. Runtime will be set to 'Cloudflare Workers'.",
|
"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)",
|
"Runtime set to 'Cloudflare Workers' (required by server deployment)",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply Workers runtime compatibility adjustments
|
|
||||||
if (nextStack.backend !== "hono") {
|
if (nextStack.backend !== "hono") {
|
||||||
notes.runtime.notes.push(
|
notes.runtime.notes.push(
|
||||||
"Cloudflare Workers runtime requires Hono backend. Hono will be selected.",
|
"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 isAlchemyWebDeploy = nextStack.webDeploy === "alchemy";
|
||||||
const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy";
|
const isAlchemyServerDeploy = nextStack.serverDeploy === "alchemy";
|
||||||
|
|
||||||
@@ -1034,12 +1029,10 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
|||||||
notes.webDeploy.hasIssue = true;
|
notes.webDeploy.hasIssue = true;
|
||||||
notes.serverDeploy.hasIssue = true;
|
notes.serverDeploy.hasIssue = true;
|
||||||
|
|
||||||
// Remove incompatible frontends
|
|
||||||
nextStack.webFrontend = nextStack.webFrontend.filter(
|
nextStack.webFrontend = nextStack.webFrontend.filter(
|
||||||
(f) => f !== "next" && f !== "react-router",
|
(f) => f !== "next" && f !== "react-router",
|
||||||
);
|
);
|
||||||
|
|
||||||
// If no web frontends remain, set to default
|
|
||||||
if (nextStack.webFrontend.length === 0) {
|
if (nextStack.webFrontend.length === 0) {
|
||||||
nextStack.webFrontend = ["tanstack-router"];
|
nextStack.webFrontend = ["tanstack-router"];
|
||||||
}
|
}
|
||||||
@@ -1639,7 +1632,6 @@ const StackBuilder = () => {
|
|||||||
const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
|
const { adjustedStack } = analyzeStackCompatibility(simulatedStack);
|
||||||
const finalStack = adjustedStack ?? simulatedStack;
|
const finalStack = adjustedStack ?? simulatedStack;
|
||||||
|
|
||||||
// Additional check for Alchemy compatibility with Next.js and React Router
|
|
||||||
if (
|
if (
|
||||||
category === "webFrontend" &&
|
category === "webFrontend" &&
|
||||||
(optionId === "next" || optionId === "react-router")
|
(optionId === "next" || optionId === "react-router")
|
||||||
|
|||||||
@@ -90,6 +90,34 @@ export const getProjectTypeData = (data: AggregatedAnalyticsData | null) => {
|
|||||||
return data.projectTypeDistribution || [];
|
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 = (
|
export const getMonthlyTimeSeriesData = (
|
||||||
data: AggregatedAnalyticsData | null,
|
data: AggregatedAnalyticsData | null,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import {
|
|||||||
getPopularStackCombinations,
|
getPopularStackCombinations,
|
||||||
getProjectTypeData,
|
getProjectTypeData,
|
||||||
getRuntimeData,
|
getRuntimeData,
|
||||||
|
getServerDeployData,
|
||||||
|
getWebDeployData,
|
||||||
} from "./data-utils";
|
} from "./data-utils";
|
||||||
import type { AggregatedAnalyticsData } from "./types";
|
import type { AggregatedAnalyticsData } from "./types";
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +41,8 @@ import {
|
|||||||
ormConfig,
|
ormConfig,
|
||||||
projectTypeConfig,
|
projectTypeConfig,
|
||||||
runtimeConfig,
|
runtimeConfig,
|
||||||
|
serverDeployConfig,
|
||||||
|
webDeployConfig,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
interface StackConfigurationChartsProps {
|
interface StackConfigurationChartsProps {
|
||||||
@@ -57,6 +61,8 @@ export function StackConfigurationCharts({
|
|||||||
const authData = getAuthData(data);
|
const authData = getAuthData(data);
|
||||||
const runtimeData = getRuntimeData(data);
|
const runtimeData = getRuntimeData(data);
|
||||||
const projectTypeData = getProjectTypeData(data);
|
const projectTypeData = getProjectTypeData(data);
|
||||||
|
const webDeployData = getWebDeployData(data);
|
||||||
|
const serverDeployData = getServerDeployData(data);
|
||||||
const popularStackCombinations = getPopularStackCombinations(data);
|
const popularStackCombinations = getPopularStackCombinations(data);
|
||||||
const databaseORMCombinations = getDatabaseORMCombinations(data);
|
const databaseORMCombinations = getDatabaseORMCombinations(data);
|
||||||
|
|
||||||
@@ -546,6 +552,108 @@ export function StackConfigurationCharts({
|
|||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</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">
|
||||||
|
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>
|
||||||
|
|
||||||
<div className="rounded border border-border">
|
<div className="rounded border border-border">
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export interface AggregatedAnalyticsData {
|
|||||||
addonsDistribution: Array<{ name: string; value: number }>;
|
addonsDistribution: Array<{ name: string; value: number }>;
|
||||||
runtimeDistribution: Array<{ name: string; value: number }>;
|
runtimeDistribution: Array<{ name: string; value: number }>;
|
||||||
projectTypeDistribution: 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 }>;
|
popularStackCombinations: Array<{ name: string; value: number }>;
|
||||||
databaseORMCombinations: Array<{ name: string; value: number }>;
|
databaseORMCombinations: Array<{ name: string; value: number }>;
|
||||||
hourlyDistribution: Array<{
|
hourlyDistribution: Array<{
|
||||||
@@ -43,6 +45,8 @@ export interface AggregatedAnalyticsData {
|
|||||||
mostPopularORM: string;
|
mostPopularORM: string;
|
||||||
mostPopularAPI: string;
|
mostPopularAPI: string;
|
||||||
mostPopularPackageManager: string;
|
mostPopularPackageManager: string;
|
||||||
|
mostPopularWebDeploy: string;
|
||||||
|
mostPopularServerDeploy: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,6 +400,28 @@ export const projectTypeConfig = {
|
|||||||
},
|
},
|
||||||
} satisfies ChartConfig;
|
} 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 = {
|
export const hourlyDistributionConfig = {
|
||||||
count: {
|
count: {
|
||||||
label: "Projects Created",
|
label: "Projects Created",
|
||||||
|
|||||||
Reference in New Issue
Block a user