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;
|
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),
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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))"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user