mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
fix(web): migrate auth from boolean to multi-option system
This commit is contained in:
@@ -48,6 +48,7 @@ interface AggregatedAnalyticsData {
|
||||
totalProjects: number;
|
||||
avgProjectsPerDay: number;
|
||||
authEnabledPercent: number;
|
||||
mostPopularAuth: string;
|
||||
mostPopularFrontend: string;
|
||||
mostPopularBackend: string;
|
||||
mostPopularORM: string;
|
||||
@@ -242,10 +243,18 @@ async function generateAnalyticsData() {
|
||||
cliVersionCounts[cliVersion] =
|
||||
(cliVersionCounts[cliVersion] || 0) + 1;
|
||||
|
||||
const auth =
|
||||
row["*.properties.auth"] === "True" ? "enabled" : "disabled";
|
||||
// Handle both old boolean format and new string format
|
||||
let auth: string;
|
||||
const authValue = row["*.properties.auth"];
|
||||
if (authValue === "True" || authValue === "true") {
|
||||
auth = "better-auth"; // Old format: true -> better-auth
|
||||
} else if (authValue === "False" || authValue === "false") {
|
||||
auth = "none"; // Old format: false -> none
|
||||
} else {
|
||||
auth = authValue || "none"; // New format: use actual value
|
||||
}
|
||||
authCounts[auth] = (authCounts[auth] || 0) + 1;
|
||||
if (auth === "enabled") authEnabledCount++;
|
||||
if (auth !== "none") authEnabledCount++;
|
||||
|
||||
const git =
|
||||
row["*.properties.git"] === "True" ? "enabled" : "disabled";
|
||||
@@ -529,6 +538,7 @@ async function generateAnalyticsData() {
|
||||
totalProjects: totalRecords,
|
||||
avgProjectsPerDay,
|
||||
authEnabledPercent,
|
||||
mostPopularAuth: getMostPopular(authCounts),
|
||||
mostPopularFrontend: getMostPopular(frontendCounts),
|
||||
mostPopularBackend: getMostPopular(backendCounts),
|
||||
mostPopularORM: getMostPopular(ormCounts),
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import { motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
import type React from "react";
|
||||
import { startTransition, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -44,7 +43,7 @@ import {
|
||||
CATEGORY_ORDER,
|
||||
generateStackCommand,
|
||||
generateStackUrlFromState,
|
||||
useStackStateWithAllParams,
|
||||
useStackState,
|
||||
} from "@/lib/stack-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -1171,7 +1170,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
|
||||
};
|
||||
|
||||
const StackBuilder = () => {
|
||||
const [stack, setStack] = useStackStateWithAllParams();
|
||||
const [stack, setStack] = useStackState();
|
||||
|
||||
const [command, setCommand] = useState("");
|
||||
const [copied, setCopied] = useState(false);
|
||||
@@ -1383,7 +1382,7 @@ const StackBuilder = () => {
|
||||
}
|
||||
|
||||
startTransition(() => {
|
||||
setStack((currentStack) => {
|
||||
setStack((currentStack: StackState) => {
|
||||
const catKey = category as keyof StackState;
|
||||
const update: Partial<StackState> = {};
|
||||
const currentValue = currentStack[catKey];
|
||||
|
||||
@@ -15,13 +15,13 @@ export default function AnalyticsPage({
|
||||
}) {
|
||||
const totalProjects = data?.summary?.totalProjects || 0;
|
||||
const avgProjectsPerDay = data?.summary?.avgProjectsPerDay || 0;
|
||||
const authEnabledPercent = data?.summary?.authEnabledPercent || 0;
|
||||
const mostPopularFrontend = data?.summary?.mostPopularFrontend || "None";
|
||||
const mostPopularBackend = data?.summary?.mostPopularBackend || "None";
|
||||
const mostPopularORM = data?.summary?.mostPopularORM || "None";
|
||||
const mostPopularAPI = data?.summary?.mostPopularAPI || "None";
|
||||
const mostPopularPackageManager =
|
||||
data?.summary?.mostPopularPackageManager || "npm";
|
||||
const mostPopularAuth = data?.summary?.mostPopularAuth || "None";
|
||||
|
||||
return (
|
||||
<div className="mx-auto min-h-svh max-w-[1280px]">
|
||||
@@ -34,7 +34,7 @@ export default function AnalyticsPage({
|
||||
<MetricsCards
|
||||
totalProjects={totalProjects}
|
||||
avgProjectsPerDay={avgProjectsPerDay}
|
||||
authEnabledPercent={authEnabledPercent}
|
||||
mostPopularAuth={mostPopularAuth}
|
||||
mostPopularFrontend={mostPopularFrontend}
|
||||
mostPopularBackend={mostPopularBackend}
|
||||
mostPopularORM={mostPopularORM}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Cpu, Download, Terminal, TrendingUp, Users } from "lucide-react";
|
||||
interface MetricsCardsProps {
|
||||
totalProjects: number;
|
||||
avgProjectsPerDay: number;
|
||||
authEnabledPercent: number;
|
||||
mostPopularAuth: string;
|
||||
mostPopularFrontend: string;
|
||||
mostPopularBackend: string;
|
||||
mostPopularORM: string;
|
||||
@@ -14,7 +14,7 @@ interface MetricsCardsProps {
|
||||
export function MetricsCards({
|
||||
totalProjects,
|
||||
avgProjectsPerDay,
|
||||
authEnabledPercent,
|
||||
mostPopularAuth,
|
||||
mostPopularFrontend,
|
||||
mostPopularBackend,
|
||||
mostPopularORM,
|
||||
@@ -117,16 +117,16 @@ export function MetricsCards({
|
||||
<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>
|
||||
<span className="font-semibold text-sm">TOP_AUTH</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 className="truncate font-bold text-accent text-lg">
|
||||
{mostPopularAuth}
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
$ auth_enabled_percentage.sh
|
||||
$ most_selected_auth.sh
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -412,10 +412,12 @@ export function StackConfigurationCharts({
|
||||
<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>
|
||||
<span className="font-semibold text-sm">
|
||||
AUTH_DISTRIBUTION.PIE
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-muted-foreground text-xs">
|
||||
Authentication implementation rate
|
||||
Authentication provider distribution
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4">
|
||||
@@ -439,9 +441,11 @@ export function StackConfigurationCharts({
|
||||
<Cell
|
||||
key={`auth-${entry.name}`}
|
||||
fill={
|
||||
entry.name === "enabled"
|
||||
entry.name === "better-auth"
|
||||
? "hsl(var(--chart-1))"
|
||||
: "hsl(var(--chart-7))"
|
||||
: entry.name === "clerk"
|
||||
? "hsl(var(--chart-2))"
|
||||
: "hsl(var(--chart-3))"
|
||||
}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface AggregatedAnalyticsData {
|
||||
totalProjects: number;
|
||||
avgProjectsPerDay: number;
|
||||
authEnabledPercent: number;
|
||||
mostPopularAuth: string;
|
||||
mostPopularFrontend: string;
|
||||
mostPopularBackend: string;
|
||||
mostPopularORM: string;
|
||||
@@ -284,14 +285,18 @@ export const cliVersionConfig = {
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const authConfig = {
|
||||
enabled: {
|
||||
label: "Enabled",
|
||||
"better-auth": {
|
||||
label: "Better Auth",
|
||||
color: "hsl(var(--chart-1))",
|
||||
},
|
||||
disabled: {
|
||||
label: "Disabled",
|
||||
clerk: {
|
||||
label: "Clerk",
|
||||
color: "hsl(var(--chart-2))",
|
||||
},
|
||||
none: {
|
||||
label: "No Auth",
|
||||
color: "hsl(var(--chart-3))",
|
||||
},
|
||||
} satisfies ChartConfig;
|
||||
|
||||
export const gitConfig = {
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "motion/react";
|
||||
import { Suspense } from "react";
|
||||
import StackBuilder from "../_components/stack-builder";
|
||||
|
||||
export default function FullScreenStackBuilder() {
|
||||
return (
|
||||
<Suspense>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="grid h-[calc(100vh-64px)] w-full flex-1 grid-cols-1 overflow-hidden"
|
||||
>
|
||||
<StackBuilder />
|
||||
</motion.div>
|
||||
</Suspense>
|
||||
<div className="grid h-[calc(100vh-64px)] w-full flex-1 grid-cols-1 overflow-hidden">
|
||||
<StackBuilder />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,10 +17,6 @@ import {
|
||||
stackUrlKeys,
|
||||
} from "@/lib/stack-url-state";
|
||||
|
||||
const getValidIds = (category: keyof typeof TECH_OPTIONS): string[] => {
|
||||
return TECH_OPTIONS[category]?.map((opt) => opt.id) ?? [];
|
||||
};
|
||||
|
||||
const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
|
||||
"webFrontend",
|
||||
"nativeFrontend",
|
||||
@@ -40,207 +36,142 @@ const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
|
||||
"install",
|
||||
];
|
||||
|
||||
function getStackKeyFromUrlKey(urlKey: string): keyof StackState | null {
|
||||
for (const [stackKey, urlKeyValue] of Object.entries(stackUrlKeys)) {
|
||||
if (urlKeyValue === urlKey) {
|
||||
return stackKey as keyof StackState;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const getStackKeyFromUrlKey = (urlKey: string): keyof StackState | null =>
|
||||
(Object.entries(stackUrlKeys).find(
|
||||
([, value]) => value === urlKey,
|
||||
)?.[0] as keyof StackState) || null;
|
||||
|
||||
export function parseSearchParamsToStack(searchParams: {
|
||||
[key: string]: string | string[] | undefined;
|
||||
}): StackState {
|
||||
const isDefaultStack = (stack: StackState): boolean =>
|
||||
Object.entries(DEFAULT_STACK).every(
|
||||
([key, _defaultValue]) =>
|
||||
key === "projectName" ||
|
||||
isStackDefault(
|
||||
stack,
|
||||
key as keyof StackState,
|
||||
stack[key as keyof StackState],
|
||||
),
|
||||
);
|
||||
|
||||
export function parseSearchParamsToStack(
|
||||
searchParams: Record<string, string | string[] | undefined>,
|
||||
): StackState {
|
||||
const parsedStack: StackState = { ...DEFAULT_STACK };
|
||||
|
||||
for (const [key, value] of Object.entries(searchParams)) {
|
||||
if (
|
||||
key === "utm_source" ||
|
||||
key === "utm_medium" ||
|
||||
key === "utm_campaign"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stackKey = getStackKeyFromUrlKey(key);
|
||||
if (stackKey && value !== undefined) {
|
||||
try {
|
||||
const parser = stackParsers[stackKey];
|
||||
if (parser) {
|
||||
const parsedValue = parser.parseServerSide(
|
||||
Array.isArray(value) ? value[0] : value,
|
||||
);
|
||||
(parsedStack as Record<string, unknown>)[stackKey] = parsedValue;
|
||||
Object.entries(searchParams)
|
||||
.filter(([key]) => !key.startsWith("utm_"))
|
||||
.forEach(([key, value]) => {
|
||||
const stackKey = getStackKeyFromUrlKey(key);
|
||||
if (stackKey && value !== undefined) {
|
||||
try {
|
||||
const parser = stackParsers[stackKey];
|
||||
if (parser) {
|
||||
parsedStack[stackKey] = parser.parseServerSide(
|
||||
Array.isArray(value) ? value[0] : value,
|
||||
) as never;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse ${key}:`, error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to parse ${key}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [key, defaultValue] of Object.entries(DEFAULT_STACK)) {
|
||||
if (parsedStack[key as keyof StackState] === undefined) {
|
||||
(parsedStack as Record<string, unknown>)[key] = defaultValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return parsedStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable summary of the stack
|
||||
*/
|
||||
export function generateStackSummary(stack: StackState): string {
|
||||
const selectedTechs: string[] = [];
|
||||
const selectedTechs = CATEGORY_ORDER.flatMap((category) => {
|
||||
const options = TECH_OPTIONS[category];
|
||||
const selectedValue = stack[category as keyof StackState];
|
||||
|
||||
for (const category of CATEGORY_ORDER) {
|
||||
const categoryKey = category as keyof StackState;
|
||||
const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS];
|
||||
const selectedValue = stack[categoryKey];
|
||||
if (!options) return [];
|
||||
|
||||
if (!options) continue;
|
||||
const getTechNames = (value: string | string[]) => {
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
return values
|
||||
.filter(
|
||||
(id) =>
|
||||
id !== "none" &&
|
||||
id !== "false" &&
|
||||
!(["git", "install", "auth"].includes(category) && id === "true"),
|
||||
)
|
||||
.map((id) => options.find((opt) => opt.id === id)?.name)
|
||||
.filter(Boolean) as string[];
|
||||
};
|
||||
|
||||
if (Array.isArray(selectedValue)) {
|
||||
if (
|
||||
selectedValue.length === 0 ||
|
||||
(selectedValue.length === 1 && selectedValue[0] === "none")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const id of selectedValue) {
|
||||
if (id === "none") continue;
|
||||
const tech = options.find((opt) => opt.id === id);
|
||||
if (tech) {
|
||||
selectedTechs.push(tech.name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const tech = options.find((opt) => opt.id === selectedValue);
|
||||
if (
|
||||
!tech ||
|
||||
tech.id === "none" ||
|
||||
tech.id === "false" ||
|
||||
((category === "git" ||
|
||||
category === "install" ||
|
||||
category === "auth") &&
|
||||
tech.id === "true")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
selectedTechs.push(tech.name);
|
||||
}
|
||||
}
|
||||
return getTechNames(selectedValue);
|
||||
});
|
||||
|
||||
return selectedTechs.length > 0 ? selectedTechs.join(" • ") : "Custom stack";
|
||||
}
|
||||
|
||||
export function generateStackCommand(stack: StackState): string {
|
||||
let base: string;
|
||||
switch (stack.packageManager) {
|
||||
case "npm":
|
||||
base = "npx create-better-t-stack@latest";
|
||||
break;
|
||||
case "pnpm":
|
||||
base = "pnpm create better-t-stack@latest";
|
||||
break;
|
||||
default:
|
||||
base = "bun create better-t-stack@latest";
|
||||
break;
|
||||
}
|
||||
const packageManagerCommands = {
|
||||
npm: "npx create-better-t-stack@latest",
|
||||
pnpm: "pnpm create better-t-stack@latest",
|
||||
default: "bun create better-t-stack@latest",
|
||||
};
|
||||
|
||||
const base =
|
||||
packageManagerCommands[
|
||||
stack.packageManager as keyof typeof packageManagerCommands
|
||||
] || packageManagerCommands.default;
|
||||
const projectName = stack.projectName || "my-better-t-app";
|
||||
const flags: string[] = [];
|
||||
|
||||
const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => {
|
||||
if (key === "projectName") return true;
|
||||
const defaultKey = key as keyof StackState;
|
||||
return isStackDefault(stack, defaultKey, stack[defaultKey]);
|
||||
});
|
||||
|
||||
if (isDefaultStack) {
|
||||
flags.push("--yes");
|
||||
} else {
|
||||
const combinedFrontends = [
|
||||
...stack.webFrontend,
|
||||
...stack.nativeFrontend,
|
||||
].filter((v, _, arr) => v !== "none" || arr.length === 1);
|
||||
|
||||
if (combinedFrontends.length === 0 || combinedFrontends[0] === "none") {
|
||||
flags.push("--frontend none");
|
||||
} else {
|
||||
flags.push(`--frontend ${combinedFrontends.join(" ")}`);
|
||||
}
|
||||
|
||||
flags.push(`--backend ${stack.backend}`);
|
||||
flags.push(`--runtime ${stack.runtime}`);
|
||||
flags.push(`--api ${stack.api}`);
|
||||
flags.push(`--auth ${stack.auth}`);
|
||||
flags.push(`--database ${stack.database}`);
|
||||
flags.push(`--orm ${stack.orm}`);
|
||||
flags.push(`--db-setup ${stack.dbSetup}`);
|
||||
flags.push(`--package-manager ${stack.packageManager}`);
|
||||
|
||||
if (stack.git === "false") {
|
||||
flags.push("--no-git");
|
||||
} else {
|
||||
flags.push("--git");
|
||||
}
|
||||
|
||||
flags.push(`--web-deploy ${stack.webDeploy}`);
|
||||
flags.push(`--server-deploy ${stack.serverDeploy}`);
|
||||
|
||||
if (stack.install === "false") {
|
||||
flags.push("--no-install");
|
||||
} else {
|
||||
flags.push("--install");
|
||||
}
|
||||
|
||||
if (stack.addons.length > 0) {
|
||||
const validAddons = stack.addons.filter((addon) =>
|
||||
[
|
||||
"pwa",
|
||||
"tauri",
|
||||
"starlight",
|
||||
"biome",
|
||||
"husky",
|
||||
"turborepo",
|
||||
"ultracite",
|
||||
"fumadocs",
|
||||
"oxlint",
|
||||
"ruler",
|
||||
].includes(addon),
|
||||
);
|
||||
if (validAddons.length > 0) {
|
||||
flags.push(`--addons ${validAddons.join(" ")}`);
|
||||
}
|
||||
} else {
|
||||
flags.push("--addons none");
|
||||
}
|
||||
|
||||
if (stack.examples.length > 0) {
|
||||
flags.push(`--examples ${stack.examples.join(" ")}`);
|
||||
} else {
|
||||
flags.push("--examples none");
|
||||
}
|
||||
if (isDefaultStack(stack)) {
|
||||
return `${base} ${projectName} --yes`;
|
||||
}
|
||||
|
||||
return `${base} ${projectName}${
|
||||
flags.length > 0 ? ` ${flags.join(" ")}` : ""
|
||||
}`;
|
||||
const flags = [
|
||||
`--frontend ${
|
||||
[...stack.webFrontend, ...stack.nativeFrontend]
|
||||
.filter((v, _, arr) => v !== "none" || arr.length === 1)
|
||||
.join(" ") || "none"
|
||||
}`,
|
||||
`--backend ${stack.backend}`,
|
||||
`--runtime ${stack.runtime}`,
|
||||
`--api ${stack.api}`,
|
||||
`--auth ${stack.auth}`,
|
||||
`--database ${stack.database}`,
|
||||
`--orm ${stack.orm}`,
|
||||
`--db-setup ${stack.dbSetup}`,
|
||||
`--package-manager ${stack.packageManager}`,
|
||||
stack.git === "false" ? "--no-git" : "--git",
|
||||
`--web-deploy ${stack.webDeploy}`,
|
||||
`--server-deploy ${stack.serverDeploy}`,
|
||||
stack.install === "false" ? "--no-install" : "--install",
|
||||
`--addons ${
|
||||
stack.addons.length > 0
|
||||
? stack.addons
|
||||
.filter((addon) =>
|
||||
[
|
||||
"pwa",
|
||||
"tauri",
|
||||
"starlight",
|
||||
"biome",
|
||||
"husky",
|
||||
"turborepo",
|
||||
"ultracite",
|
||||
"fumadocs",
|
||||
"oxlint",
|
||||
"ruler",
|
||||
].includes(addon),
|
||||
)
|
||||
.join(" ") || "none"
|
||||
: "none"
|
||||
}`,
|
||||
`--examples ${stack.examples.join(" ") || "none"}`,
|
||||
];
|
||||
|
||||
return `${base} ${projectName} ${flags.join(" ")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate stack URL from pathname and search params
|
||||
*/
|
||||
// URL generation functions
|
||||
export function generateStackUrl(
|
||||
pathname: string,
|
||||
searchParams: URLSearchParams,
|
||||
): string {
|
||||
const searchString = searchParams.toString();
|
||||
const relativeUrl = `${pathname}${searchString ? `?${searchString}` : ""}`;
|
||||
return `https://better-t-stack.dev${relativeUrl}`;
|
||||
return `https://better-t-stack.dev${pathname}${searchString ? `?${searchString}` : ""}`;
|
||||
}
|
||||
|
||||
export function generateStackUrlFromState(
|
||||
@@ -253,212 +184,148 @@ export function generateStackUrlFromState(
|
||||
? window.location.origin
|
||||
: "https://better-t-stack.dev");
|
||||
|
||||
const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => {
|
||||
if (key === "projectName") return true;
|
||||
const defaultKey = key as keyof StackState;
|
||||
return isStackDefault(stack, defaultKey, stack[defaultKey]);
|
||||
});
|
||||
|
||||
if (isDefaultStack) {
|
||||
if (isDefaultStack(stack)) {
|
||||
return `${origin}/stack`;
|
||||
}
|
||||
|
||||
const stackParams = new URLSearchParams();
|
||||
|
||||
for (const [stackKey, urlKey] of Object.entries(stackUrlKeys)) {
|
||||
Object.entries(stackUrlKeys).forEach(([stackKey, urlKey]) => {
|
||||
const value = stack[stackKey as keyof StackState];
|
||||
if (value !== undefined) {
|
||||
if (Array.isArray(value)) {
|
||||
stackParams.set(urlKey, value.join(","));
|
||||
} else {
|
||||
stackParams.set(urlKey, String(value));
|
||||
}
|
||||
stackParams.set(
|
||||
urlKey,
|
||||
Array.isArray(value) ? value.join(",") : String(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return `${origin}/stack?${stackParams.toString()}`;
|
||||
}
|
||||
|
||||
export function useStackStateWithAllParams() {
|
||||
// Primary hook - simplified approach
|
||||
export function useStackState() {
|
||||
const [stack, setStack] = useQueryStates(
|
||||
stackParsers,
|
||||
stackQueryStatesOptions,
|
||||
);
|
||||
|
||||
const setStackWithAllParams = async (
|
||||
newStack: Partial<StackState> | ((prev: StackState) => Partial<StackState>),
|
||||
const updateStack = async (
|
||||
updates: Partial<StackState> | ((prev: StackState) => Partial<StackState>),
|
||||
) => {
|
||||
const updatedStack =
|
||||
typeof newStack === "function" ? newStack(stack) : newStack;
|
||||
const finalStack = { ...stack, ...updatedStack };
|
||||
const newStack = typeof updates === "function" ? updates(stack) : updates;
|
||||
const finalStack = { ...stack, ...newStack };
|
||||
|
||||
const isFinalStackDefault = Object.keys(DEFAULT_STACK).every((key) => {
|
||||
if (key === "projectName") return true;
|
||||
const defaultKey = key as keyof StackState;
|
||||
return isStackDefault(finalStack, defaultKey, finalStack[defaultKey]);
|
||||
});
|
||||
|
||||
if (isFinalStackDefault) {
|
||||
await setStack(null);
|
||||
} else {
|
||||
await setStack(finalStack);
|
||||
}
|
||||
await setStack(isDefaultStack(finalStack) ? null : finalStack);
|
||||
};
|
||||
|
||||
return [stack, setStackWithAllParams] as const;
|
||||
return [stack, updateStack] as const;
|
||||
}
|
||||
|
||||
// Individual state hook - kept for backward compatibility but simplified
|
||||
export function useIndividualStackStates() {
|
||||
const [projectName, setProjectName] = useQueryState(
|
||||
"name",
|
||||
parseAsString.withDefault(DEFAULT_STACK.projectName),
|
||||
);
|
||||
const getValidIds = (category: keyof typeof TECH_OPTIONS) =>
|
||||
TECH_OPTIONS[category]?.map((opt) => opt.id) ?? [];
|
||||
|
||||
const [webFrontend, setWebFrontend] = useQueryState(
|
||||
"fe-w",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.webFrontend),
|
||||
);
|
||||
|
||||
const [nativeFrontend, setNativeFrontend] = useQueryState(
|
||||
"fe-n",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.nativeFrontend),
|
||||
);
|
||||
|
||||
const [runtime, setRuntime] = useQueryState(
|
||||
"rt",
|
||||
parseAsStringEnum(getValidIds("runtime")).withDefault(
|
||||
DEFAULT_STACK.runtime,
|
||||
// Individual query states
|
||||
const queryStates = {
|
||||
projectName: useQueryState(
|
||||
"name",
|
||||
parseAsString.withDefault(DEFAULT_STACK.projectName),
|
||||
),
|
||||
);
|
||||
|
||||
const [backend, setBackend] = useQueryState(
|
||||
"be",
|
||||
parseAsStringEnum(getValidIds("backend")).withDefault(
|
||||
DEFAULT_STACK.backend,
|
||||
webFrontend: useQueryState(
|
||||
"fe-w",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.webFrontend),
|
||||
),
|
||||
);
|
||||
|
||||
const [api, setApi] = useQueryState(
|
||||
"api",
|
||||
parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api),
|
||||
);
|
||||
|
||||
const [database, setDatabase] = useQueryState(
|
||||
"db",
|
||||
parseAsStringEnum(getValidIds("database")).withDefault(
|
||||
DEFAULT_STACK.database,
|
||||
nativeFrontend: useQueryState(
|
||||
"fe-n",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.nativeFrontend),
|
||||
),
|
||||
);
|
||||
|
||||
const [orm, setOrm] = useQueryState(
|
||||
"orm",
|
||||
parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm),
|
||||
);
|
||||
|
||||
const [dbSetup, setDbSetup] = useQueryState(
|
||||
"dbs",
|
||||
parseAsStringEnum(getValidIds("dbSetup")).withDefault(
|
||||
DEFAULT_STACK.dbSetup,
|
||||
runtime: useQueryState(
|
||||
"rt",
|
||||
parseAsStringEnum(getValidIds("runtime")).withDefault(
|
||||
DEFAULT_STACK.runtime,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const [auth, setAuth] = useQueryState(
|
||||
"au",
|
||||
parseAsStringEnum(getValidIds("auth")).withDefault(DEFAULT_STACK.auth),
|
||||
);
|
||||
|
||||
const [packageManager, setPackageManager] = useQueryState(
|
||||
"pm",
|
||||
parseAsStringEnum(getValidIds("packageManager")).withDefault(
|
||||
DEFAULT_STACK.packageManager,
|
||||
backend: useQueryState(
|
||||
"be",
|
||||
parseAsStringEnum(getValidIds("backend")).withDefault(
|
||||
DEFAULT_STACK.backend,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const [addons, setAddons] = useQueryState(
|
||||
"add",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons),
|
||||
);
|
||||
|
||||
const [examples, setExamples] = useQueryState(
|
||||
"ex",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples),
|
||||
);
|
||||
|
||||
const [git, setGit] = useQueryState(
|
||||
"git",
|
||||
parseAsStringEnum(["true", "false"] as const).withDefault(
|
||||
DEFAULT_STACK.git as "true" | "false",
|
||||
api: useQueryState(
|
||||
"api",
|
||||
parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api),
|
||||
),
|
||||
);
|
||||
|
||||
const [install, setInstall] = useQueryState(
|
||||
"i",
|
||||
parseAsStringEnum(["true", "false"] as const).withDefault(
|
||||
DEFAULT_STACK.install as "true" | "false",
|
||||
database: useQueryState(
|
||||
"db",
|
||||
parseAsStringEnum(getValidIds("database")).withDefault(
|
||||
DEFAULT_STACK.database,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const [webDeploy, setWebDeploy] = useQueryState(
|
||||
"wd",
|
||||
parseAsStringEnum(getValidIds("webDeploy")).withDefault(
|
||||
DEFAULT_STACK.webDeploy,
|
||||
orm: useQueryState(
|
||||
"orm",
|
||||
parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm),
|
||||
),
|
||||
);
|
||||
|
||||
const [serverDeploy, setServerDeploy] = useQueryState(
|
||||
"sd",
|
||||
parseAsStringEnum(getValidIds("serverDeploy")).withDefault(
|
||||
DEFAULT_STACK.serverDeploy,
|
||||
dbSetup: useQueryState(
|
||||
"dbs",
|
||||
parseAsStringEnum(getValidIds("dbSetup")).withDefault(
|
||||
DEFAULT_STACK.dbSetup,
|
||||
),
|
||||
),
|
||||
auth: useQueryState(
|
||||
"au",
|
||||
parseAsStringEnum(getValidIds("auth")).withDefault(DEFAULT_STACK.auth),
|
||||
),
|
||||
packageManager: useQueryState(
|
||||
"pm",
|
||||
parseAsStringEnum(getValidIds("packageManager")).withDefault(
|
||||
DEFAULT_STACK.packageManager,
|
||||
),
|
||||
),
|
||||
addons: useQueryState(
|
||||
"add",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.addons),
|
||||
),
|
||||
examples: useQueryState(
|
||||
"ex",
|
||||
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.examples),
|
||||
),
|
||||
git: useQueryState(
|
||||
"git",
|
||||
parseAsStringEnum(["true", "false"] as const).withDefault(
|
||||
DEFAULT_STACK.git as "true" | "false",
|
||||
),
|
||||
),
|
||||
install: useQueryState(
|
||||
"i",
|
||||
parseAsStringEnum(["true", "false"] as const).withDefault(
|
||||
DEFAULT_STACK.install as "true" | "false",
|
||||
),
|
||||
),
|
||||
webDeploy: useQueryState(
|
||||
"wd",
|
||||
parseAsStringEnum(getValidIds("webDeploy")).withDefault(
|
||||
DEFAULT_STACK.webDeploy,
|
||||
),
|
||||
),
|
||||
serverDeploy: useQueryState(
|
||||
"sd",
|
||||
parseAsStringEnum(getValidIds("serverDeploy")).withDefault(
|
||||
DEFAULT_STACK.serverDeploy,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const stack: StackState = {
|
||||
projectName,
|
||||
webFrontend,
|
||||
nativeFrontend,
|
||||
runtime,
|
||||
backend,
|
||||
api,
|
||||
database,
|
||||
orm,
|
||||
dbSetup,
|
||||
auth,
|
||||
packageManager,
|
||||
addons,
|
||||
examples,
|
||||
git,
|
||||
install,
|
||||
webDeploy,
|
||||
serverDeploy,
|
||||
};
|
||||
|
||||
const stack: StackState = Object.fromEntries(
|
||||
Object.entries(queryStates).map(([key, [value]]) => [key, value]),
|
||||
) as StackState;
|
||||
|
||||
const setStack = async (updates: Partial<StackState>) => {
|
||||
const setters = {
|
||||
projectName: setProjectName,
|
||||
webFrontend: setWebFrontend,
|
||||
nativeFrontend: setNativeFrontend,
|
||||
runtime: setRuntime,
|
||||
backend: setBackend,
|
||||
api: setApi,
|
||||
database: setDatabase,
|
||||
orm: setOrm,
|
||||
dbSetup: setDbSetup,
|
||||
auth: setAuth,
|
||||
packageManager: setPackageManager,
|
||||
addons: setAddons,
|
||||
examples: setExamples,
|
||||
git: setGit,
|
||||
install: setInstall,
|
||||
webDeploy: setWebDeploy,
|
||||
serverDeploy: setServerDeploy,
|
||||
};
|
||||
|
||||
const promises = Object.entries(updates).map(([key, value]) => {
|
||||
const setter = setters[key as keyof typeof setters];
|
||||
return setter(value as never);
|
||||
const setter = queryStates[key as keyof typeof queryStates]?.[1];
|
||||
return setter?.(value as never);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
await Promise.all(promises.filter(Boolean));
|
||||
};
|
||||
|
||||
return [stack, setStack] as const;
|
||||
|
||||
Reference in New Issue
Block a user