fix(web): migrate auth from boolean to multi-option system

This commit is contained in:
Aman Varshney
2025-09-02 01:56:09 +05:30
parent 9802332184
commit ea3d8d9745
8 changed files with 254 additions and 380 deletions

View File

@@ -48,6 +48,7 @@ interface AggregatedAnalyticsData {
totalProjects: number; totalProjects: number;
avgProjectsPerDay: number; avgProjectsPerDay: number;
authEnabledPercent: number; authEnabledPercent: number;
mostPopularAuth: string;
mostPopularFrontend: string; mostPopularFrontend: string;
mostPopularBackend: string; mostPopularBackend: string;
mostPopularORM: string; mostPopularORM: string;
@@ -242,10 +243,18 @@ async function generateAnalyticsData() {
cliVersionCounts[cliVersion] = cliVersionCounts[cliVersion] =
(cliVersionCounts[cliVersion] || 0) + 1; (cliVersionCounts[cliVersion] || 0) + 1;
const auth = // Handle both old boolean format and new string format
row["*.properties.auth"] === "True" ? "enabled" : "disabled"; 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; authCounts[auth] = (authCounts[auth] || 0) + 1;
if (auth === "enabled") authEnabledCount++; if (auth !== "none") authEnabledCount++;
const git = const git =
row["*.properties.git"] === "True" ? "enabled" : "disabled"; row["*.properties.git"] === "True" ? "enabled" : "disabled";
@@ -529,6 +538,7 @@ async function generateAnalyticsData() {
totalProjects: totalRecords, totalProjects: totalRecords,
avgProjectsPerDay, avgProjectsPerDay,
authEnabledPercent, authEnabledPercent,
mostPopularAuth: getMostPopular(authCounts),
mostPopularFrontend: getMostPopular(frontendCounts), mostPopularFrontend: getMostPopular(frontendCounts),
mostPopularBackend: getMostPopular(backendCounts), mostPopularBackend: getMostPopular(backendCounts),
mostPopularORM: getMostPopular(ormCounts), mostPopularORM: getMostPopular(ormCounts),

View File

@@ -16,7 +16,6 @@ import {
import { motion } from "motion/react"; import { motion } from "motion/react";
import Image from "next/image"; import Image from "next/image";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import type React from "react"; import type React from "react";
import { startTransition, useEffect, useMemo, useRef, useState } from "react"; import { startTransition, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -44,7 +43,7 @@ import {
CATEGORY_ORDER, CATEGORY_ORDER,
generateStackCommand, generateStackCommand,
generateStackUrlFromState, generateStackUrlFromState,
useStackStateWithAllParams, useStackState,
} from "@/lib/stack-utils"; } from "@/lib/stack-utils";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -1171,7 +1170,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}; };
const StackBuilder = () => { const StackBuilder = () => {
const [stack, setStack] = useStackStateWithAllParams(); const [stack, setStack] = useStackState();
const [command, setCommand] = useState(""); const [command, setCommand] = useState("");
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -1383,7 +1382,7 @@ const StackBuilder = () => {
} }
startTransition(() => { startTransition(() => {
setStack((currentStack) => { setStack((currentStack: StackState) => {
const catKey = category as keyof StackState; const catKey = category as keyof StackState;
const update: Partial<StackState> = {}; const update: Partial<StackState> = {};
const currentValue = currentStack[catKey]; const currentValue = currentStack[catKey];

View File

@@ -15,13 +15,13 @@ export default function AnalyticsPage({
}) { }) {
const totalProjects = data?.summary?.totalProjects || 0; const totalProjects = data?.summary?.totalProjects || 0;
const avgProjectsPerDay = data?.summary?.avgProjectsPerDay || 0; const avgProjectsPerDay = data?.summary?.avgProjectsPerDay || 0;
const authEnabledPercent = data?.summary?.authEnabledPercent || 0;
const mostPopularFrontend = data?.summary?.mostPopularFrontend || "None"; const mostPopularFrontend = data?.summary?.mostPopularFrontend || "None";
const mostPopularBackend = data?.summary?.mostPopularBackend || "None"; const mostPopularBackend = data?.summary?.mostPopularBackend || "None";
const mostPopularORM = data?.summary?.mostPopularORM || "None"; const mostPopularORM = data?.summary?.mostPopularORM || "None";
const mostPopularAPI = data?.summary?.mostPopularAPI || "None"; const mostPopularAPI = data?.summary?.mostPopularAPI || "None";
const mostPopularPackageManager = const mostPopularPackageManager =
data?.summary?.mostPopularPackageManager || "npm"; data?.summary?.mostPopularPackageManager || "npm";
const mostPopularAuth = data?.summary?.mostPopularAuth || "None";
return ( return (
<div className="mx-auto min-h-svh max-w-[1280px]"> <div className="mx-auto min-h-svh max-w-[1280px]">
@@ -34,7 +34,7 @@ export default function AnalyticsPage({
<MetricsCards <MetricsCards
totalProjects={totalProjects} totalProjects={totalProjects}
avgProjectsPerDay={avgProjectsPerDay} avgProjectsPerDay={avgProjectsPerDay}
authEnabledPercent={authEnabledPercent} mostPopularAuth={mostPopularAuth}
mostPopularFrontend={mostPopularFrontend} mostPopularFrontend={mostPopularFrontend}
mostPopularBackend={mostPopularBackend} mostPopularBackend={mostPopularBackend}
mostPopularORM={mostPopularORM} mostPopularORM={mostPopularORM}

View File

@@ -3,7 +3,7 @@ import { Cpu, Download, Terminal, TrendingUp, Users } from "lucide-react";
interface MetricsCardsProps { interface MetricsCardsProps {
totalProjects: number; totalProjects: number;
avgProjectsPerDay: number; avgProjectsPerDay: number;
authEnabledPercent: number; mostPopularAuth: string;
mostPopularFrontend: string; mostPopularFrontend: string;
mostPopularBackend: string; mostPopularBackend: string;
mostPopularORM: string; mostPopularORM: string;
@@ -14,7 +14,7 @@ interface MetricsCardsProps {
export function MetricsCards({ export function MetricsCards({
totalProjects, totalProjects,
avgProjectsPerDay, avgProjectsPerDay,
authEnabledPercent, mostPopularAuth,
mostPopularFrontend, mostPopularFrontend,
mostPopularBackend, mostPopularBackend,
mostPopularORM, mostPopularORM,
@@ -117,16 +117,16 @@ export function MetricsCards({
<div className="rounded border border-border"> <div className="rounded border border-border">
<div className="border-border border-b px-4 py-3"> <div className="border-border border-b px-4 py-3">
<div className="flex items-center justify-between"> <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" /> <Users className="h-4 w-4 text-primary" />
</div> </div>
</div> </div>
<div className="p-4"> <div className="p-4">
<div className="font-bold text-2xl text-primary"> <div className="truncate font-bold text-accent text-lg">
{authEnabledPercent}% {mostPopularAuth}
</div> </div>
<p className="mt-1 text-muted-foreground text-xs"> <p className="mt-1 text-muted-foreground text-xs">
$ auth_enabled_percentage.sh $ most_selected_auth.sh
</p> </p>
</div> </div>
</div> </div>

View File

@@ -412,10 +412,12 @@ export function StackConfigurationCharts({
<div className="border-border border-b px-4 py-3"> <div className="border-border border-b px-4 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-primary text-xs"></span> <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> </div>
<p className="mt-1 text-muted-foreground text-xs"> <p className="mt-1 text-muted-foreground text-xs">
Authentication implementation rate Authentication provider distribution
</p> </p>
</div> </div>
<div className="p-4"> <div className="p-4">
@@ -439,9 +441,11 @@ export function StackConfigurationCharts({
<Cell <Cell
key={`auth-${entry.name}`} key={`auth-${entry.name}`}
fill={ fill={
entry.name === "enabled" entry.name === "better-auth"
? "hsl(var(--chart-1))" ? "hsl(var(--chart-1))"
: "hsl(var(--chart-7))" : entry.name === "clerk"
? "hsl(var(--chart-2))"
: "hsl(var(--chart-3))"
} }
/> />
))} ))}

View File

@@ -40,6 +40,7 @@ export interface AggregatedAnalyticsData {
totalProjects: number; totalProjects: number;
avgProjectsPerDay: number; avgProjectsPerDay: number;
authEnabledPercent: number; authEnabledPercent: number;
mostPopularAuth: string;
mostPopularFrontend: string; mostPopularFrontend: string;
mostPopularBackend: string; mostPopularBackend: string;
mostPopularORM: string; mostPopularORM: string;
@@ -284,14 +285,18 @@ export const cliVersionConfig = {
} satisfies ChartConfig; } satisfies ChartConfig;
export const authConfig = { export const authConfig = {
enabled: { "better-auth": {
label: "Enabled", label: "Better Auth",
color: "hsl(var(--chart-1))", color: "hsl(var(--chart-1))",
}, },
disabled: { clerk: {
label: "Disabled", label: "Clerk",
color: "hsl(var(--chart-2))", color: "hsl(var(--chart-2))",
}, },
none: {
label: "No Auth",
color: "hsl(var(--chart-3))",
},
} satisfies ChartConfig; } satisfies ChartConfig;
export const gitConfig = { export const gitConfig = {

View File

@@ -1,20 +1,9 @@
"use client";
import { motion } from "motion/react";
import { Suspense } from "react";
import StackBuilder from "../_components/stack-builder"; import StackBuilder from "../_components/stack-builder";
export default function FullScreenStackBuilder() { export default function FullScreenStackBuilder() {
return ( return (
<Suspense> <div className="grid h-[calc(100vh-64px)] w-full flex-1 grid-cols-1 overflow-hidden">
<motion.div <StackBuilder />
initial={{ opacity: 0 }} </div>
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>
); );
} }

View File

@@ -17,10 +17,6 @@ import {
stackUrlKeys, stackUrlKeys,
} from "@/lib/stack-url-state"; } 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> = [ const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
"webFrontend", "webFrontend",
"nativeFrontend", "nativeFrontend",
@@ -40,207 +36,142 @@ const CATEGORY_ORDER: Array<keyof typeof TECH_OPTIONS> = [
"install", "install",
]; ];
function getStackKeyFromUrlKey(urlKey: string): keyof StackState | null { const getStackKeyFromUrlKey = (urlKey: string): keyof StackState | null =>
for (const [stackKey, urlKeyValue] of Object.entries(stackUrlKeys)) { (Object.entries(stackUrlKeys).find(
if (urlKeyValue === urlKey) { ([, value]) => value === urlKey,
return stackKey as keyof StackState; )?.[0] as keyof StackState) || null;
}
}
return null;
}
export function parseSearchParamsToStack(searchParams: { const isDefaultStack = (stack: StackState): boolean =>
[key: string]: string | string[] | undefined; Object.entries(DEFAULT_STACK).every(
}): StackState { ([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 }; const parsedStack: StackState = { ...DEFAULT_STACK };
for (const [key, value] of Object.entries(searchParams)) { Object.entries(searchParams)
if ( .filter(([key]) => !key.startsWith("utm_"))
key === "utm_source" || .forEach(([key, value]) => {
key === "utm_medium" || const stackKey = getStackKeyFromUrlKey(key);
key === "utm_campaign" if (stackKey && value !== undefined) {
) { try {
continue; const parser = stackParsers[stackKey];
} if (parser) {
parsedStack[stackKey] = parser.parseServerSide(
const stackKey = getStackKeyFromUrlKey(key); Array.isArray(value) ? value[0] : value,
if (stackKey && value !== undefined) { ) as never;
try { }
const parser = stackParsers[stackKey]; } catch (error) {
if (parser) { console.warn(`Failed to parse ${key}:`, error);
const parsedValue = parser.parseServerSide(
Array.isArray(value) ? value[0] : value,
);
(parsedStack as Record<string, unknown>)[stackKey] = parsedValue;
} }
} 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; return parsedStack;
} }
/**
* Generate a human-readable summary of the stack
*/
export function generateStackSummary(stack: StackState): string { 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) { if (!options) return [];
const categoryKey = category as keyof StackState;
const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS];
const selectedValue = stack[categoryKey];
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)) { return getTechNames(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 selectedTechs.length > 0 ? selectedTechs.join(" • ") : "Custom stack"; return selectedTechs.length > 0 ? selectedTechs.join(" • ") : "Custom stack";
} }
export function generateStackCommand(stack: StackState): string { export function generateStackCommand(stack: StackState): string {
let base: string; const packageManagerCommands = {
switch (stack.packageManager) { npm: "npx create-better-t-stack@latest",
case "npm": pnpm: "pnpm create better-t-stack@latest",
base = "npx create-better-t-stack@latest"; default: "bun 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 base =
packageManagerCommands[
stack.packageManager as keyof typeof packageManagerCommands
] || packageManagerCommands.default;
const projectName = stack.projectName || "my-better-t-app"; const projectName = stack.projectName || "my-better-t-app";
const flags: string[] = [];
const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { if (isDefaultStack(stack)) {
if (key === "projectName") return true; return `${base} ${projectName} --yes`;
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");
}
} }
return `${base} ${projectName}${ const flags = [
flags.length > 0 ? ` ${flags.join(" ")}` : "" `--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(" ")}`;
} }
/** // URL generation functions
* Generate stack URL from pathname and search params
*/
export function generateStackUrl( export function generateStackUrl(
pathname: string, pathname: string,
searchParams: URLSearchParams, searchParams: URLSearchParams,
): string { ): string {
const searchString = searchParams.toString(); const searchString = searchParams.toString();
const relativeUrl = `${pathname}${searchString ? `?${searchString}` : ""}`; return `https://better-t-stack.dev${pathname}${searchString ? `?${searchString}` : ""}`;
return `https://better-t-stack.dev${relativeUrl}`;
} }
export function generateStackUrlFromState( export function generateStackUrlFromState(
@@ -253,212 +184,148 @@ export function generateStackUrlFromState(
? window.location.origin ? window.location.origin
: "https://better-t-stack.dev"); : "https://better-t-stack.dev");
const isDefaultStack = Object.keys(DEFAULT_STACK).every((key) => { if (isDefaultStack(stack)) {
if (key === "projectName") return true;
const defaultKey = key as keyof StackState;
return isStackDefault(stack, defaultKey, stack[defaultKey]);
});
if (isDefaultStack) {
return `${origin}/stack`; return `${origin}/stack`;
} }
const stackParams = new URLSearchParams(); const stackParams = new URLSearchParams();
Object.entries(stackUrlKeys).forEach(([stackKey, urlKey]) => {
for (const [stackKey, urlKey] of Object.entries(stackUrlKeys)) {
const value = stack[stackKey as keyof StackState]; const value = stack[stackKey as keyof StackState];
if (value !== undefined) { if (value !== undefined) {
if (Array.isArray(value)) { stackParams.set(
stackParams.set(urlKey, value.join(",")); urlKey,
} else { Array.isArray(value) ? value.join(",") : String(value),
stackParams.set(urlKey, String(value)); );
}
} }
} });
return `${origin}/stack?${stackParams.toString()}`; return `${origin}/stack?${stackParams.toString()}`;
} }
export function useStackStateWithAllParams() { // Primary hook - simplified approach
export function useStackState() {
const [stack, setStack] = useQueryStates( const [stack, setStack] = useQueryStates(
stackParsers, stackParsers,
stackQueryStatesOptions, stackQueryStatesOptions,
); );
const setStackWithAllParams = async ( const updateStack = async (
newStack: Partial<StackState> | ((prev: StackState) => Partial<StackState>), updates: Partial<StackState> | ((prev: StackState) => Partial<StackState>),
) => { ) => {
const updatedStack = const newStack = typeof updates === "function" ? updates(stack) : updates;
typeof newStack === "function" ? newStack(stack) : newStack; const finalStack = { ...stack, ...newStack };
const finalStack = { ...stack, ...updatedStack };
const isFinalStackDefault = Object.keys(DEFAULT_STACK).every((key) => { await setStack(isDefaultStack(finalStack) ? null : finalStack);
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);
}
}; };
return [stack, setStackWithAllParams] as const; return [stack, updateStack] as const;
} }
// Individual state hook - kept for backward compatibility but simplified
export function useIndividualStackStates() { export function useIndividualStackStates() {
const [projectName, setProjectName] = useQueryState( const getValidIds = (category: keyof typeof TECH_OPTIONS) =>
"name", TECH_OPTIONS[category]?.map((opt) => opt.id) ?? [];
parseAsString.withDefault(DEFAULT_STACK.projectName),
);
const [webFrontend, setWebFrontend] = useQueryState( // Individual query states
"fe-w", const queryStates = {
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.webFrontend), projectName: useQueryState(
); "name",
parseAsString.withDefault(DEFAULT_STACK.projectName),
const [nativeFrontend, setNativeFrontend] = useQueryState(
"fe-n",
parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.nativeFrontend),
);
const [runtime, setRuntime] = useQueryState(
"rt",
parseAsStringEnum(getValidIds("runtime")).withDefault(
DEFAULT_STACK.runtime,
), ),
); webFrontend: useQueryState(
"fe-w",
const [backend, setBackend] = useQueryState( parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.webFrontend),
"be",
parseAsStringEnum(getValidIds("backend")).withDefault(
DEFAULT_STACK.backend,
), ),
); nativeFrontend: useQueryState(
"fe-n",
const [api, setApi] = useQueryState( parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.nativeFrontend),
"api",
parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api),
);
const [database, setDatabase] = useQueryState(
"db",
parseAsStringEnum(getValidIds("database")).withDefault(
DEFAULT_STACK.database,
), ),
); runtime: useQueryState(
"rt",
const [orm, setOrm] = useQueryState( parseAsStringEnum(getValidIds("runtime")).withDefault(
"orm", DEFAULT_STACK.runtime,
parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm), ),
);
const [dbSetup, setDbSetup] = useQueryState(
"dbs",
parseAsStringEnum(getValidIds("dbSetup")).withDefault(
DEFAULT_STACK.dbSetup,
), ),
); backend: useQueryState(
"be",
const [auth, setAuth] = useQueryState( parseAsStringEnum(getValidIds("backend")).withDefault(
"au", DEFAULT_STACK.backend,
parseAsStringEnum(getValidIds("auth")).withDefault(DEFAULT_STACK.auth), ),
);
const [packageManager, setPackageManager] = useQueryState(
"pm",
parseAsStringEnum(getValidIds("packageManager")).withDefault(
DEFAULT_STACK.packageManager,
), ),
); api: useQueryState(
"api",
const [addons, setAddons] = useQueryState( parseAsStringEnum(getValidIds("api")).withDefault(DEFAULT_STACK.api),
"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",
), ),
); database: useQueryState(
"db",
const [install, setInstall] = useQueryState( parseAsStringEnum(getValidIds("database")).withDefault(
"i", DEFAULT_STACK.database,
parseAsStringEnum(["true", "false"] as const).withDefault( ),
DEFAULT_STACK.install as "true" | "false",
), ),
); orm: useQueryState(
"orm",
const [webDeploy, setWebDeploy] = useQueryState( parseAsStringEnum(getValidIds("orm")).withDefault(DEFAULT_STACK.orm),
"wd",
parseAsStringEnum(getValidIds("webDeploy")).withDefault(
DEFAULT_STACK.webDeploy,
), ),
); dbSetup: useQueryState(
"dbs",
const [serverDeploy, setServerDeploy] = useQueryState( parseAsStringEnum(getValidIds("dbSetup")).withDefault(
"sd", DEFAULT_STACK.dbSetup,
parseAsStringEnum(getValidIds("serverDeploy")).withDefault( ),
DEFAULT_STACK.serverDeploy, ),
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 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 promises = Object.entries(updates).map(([key, value]) => {
const setter = setters[key as keyof typeof setters]; const setter = queryStates[key as keyof typeof queryStates]?.[1];
return setter(value as never); return setter?.(value as never);
}); });
await Promise.all(promises.filter(Boolean));
await Promise.all(promises);
}; };
return [stack, setStack] as const; return [stack, setStack] as const;