From 837d46c675b536694a3b5b817613d997584d595a Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Tue, 13 May 2025 11:27:41 +0530 Subject: [PATCH] add seperate rows for native and web frontends --- .../app/(home)/_components/stack-builder.tsx | 191 +++++++++++------- apps/web/src/lib/constant.ts | 57 +++++- apps/web/src/lib/stack-url-state.ts | 10 +- 3 files changed, 174 insertions(+), 84 deletions(-) diff --git a/apps/web/src/app/(home)/_components/stack-builder.tsx b/apps/web/src/app/(home)/_components/stack-builder.tsx index 472795d..6def9cd 100644 --- a/apps/web/src/app/(home)/_components/stack-builder.tsx +++ b/apps/web/src/app/(home)/_components/stack-builder.tsx @@ -63,7 +63,8 @@ const validateProjectName = (name: string): string | undefined => { }; const CATEGORY_ORDER: Array = [ - "frontend", + "webFrontend", + "nativeFrontend", "backend", "runtime", "api", @@ -78,8 +79,8 @@ const CATEGORY_ORDER: Array = [ "install", ]; -const hasWebFrontend = (frontend: string[]) => - frontend.some((f) => +const hasWebFrontend = (webFrontend: string[]) => + webFrontend.some((f) => [ "tanstack-router", "react-router", @@ -91,17 +92,17 @@ const hasWebFrontend = (frontend: string[]) => ].includes(f), ); -const checkHasNativeFrontend = (frontend: string[]) => - frontend.includes("native-nativewind") || - frontend.includes("native-unistyles"); +const checkHasNativeFrontend = (nativeFrontend: string[]) => + nativeFrontend.includes("native-nativewind") || + nativeFrontend.includes("native-unistyles"); -const hasPWACompatibleFrontend = (frontend: string[]) => - frontend.some((f) => +const hasPWACompatibleFrontend = (webFrontend: string[]) => + webFrontend.some((f) => ["tanstack-router", "react-router", "solid"].includes(f), ); -const hasTauriCompatibleFrontend = (frontend: string[]) => - frontend.some((f) => +const hasTauriCompatibleFrontend = (webFrontend: string[]) => + webFrontend.some((f) => [ "tanstack-router", "react-router", @@ -114,7 +115,8 @@ const hasTauriCompatibleFrontend = (frontend: string[]) => const getBadgeColors = (category: string): string => { switch (category) { - case "frontend": + case "webFrontend": + case "nativeFrontend": return "border-blue-300 bg-blue-100 text-blue-800 dark:border-blue-700/30 dark:bg-blue-900/30 dark:text-blue-300"; case "runtime": return "border-amber-300 bg-amber-100 text-amber-800 dark:border-amber-700/30 dark:bg-amber-900/30 dark:text-amber-300"; @@ -229,33 +231,39 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } } const incompatibleConvexFrontends = ["nuxt", "solid"]; - const originalFrontendLength = nextStack.frontend.length; - nextStack.frontend = nextStack.frontend.filter( + const originalWebFrontendLength = nextStack.webFrontend.length; + nextStack.webFrontend = nextStack.webFrontend.filter( (f) => !incompatibleConvexFrontends.includes(f), ); - if (nextStack.frontend.length !== originalFrontendLength) { + if (nextStack.webFrontend.length !== originalWebFrontendLength) { changed = true; - notes.frontend.notes.push( + notes.webFrontend.notes.push( "Nuxt and Solid are not compatible with Convex backend and have been removed.", ); notes.backend.notes.push( "Convex backend is not compatible with Nuxt or Solid.", ); - notes.frontend.hasIssue = true; + notes.webFrontend.hasIssue = true; notes.backend.hasIssue = true; changes.push({ category: "convex", - message: "Removed incompatible frontends (Nuxt, Solid)", + message: "Removed incompatible web frontends (Nuxt, Solid)", }); } - if (nextStack.frontend.length === 0) { - nextStack.frontend = ["tanstack-router"]; + if ( + nextStack.webFrontend.length === 0 || + nextStack.webFrontend[0] === "none" + ) { + nextStack.webFrontend = ["tanstack-router"]; changed = true; changes.push({ category: "convex", - message: "Frontend defaulted to TanStack Router", + message: "Web Frontend defaulted to TanStack Router", }); } + if (nextStack.nativeFrontend[0] === "none") { + } else { + } } else if (isBackendNone) { const noneOverrides: Partial = { auth: "false", @@ -506,19 +514,19 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } } - const isNuxt = nextStack.frontend.includes("nuxt"); - const isSvelte = nextStack.frontend.includes("svelte"); - const isSolid = nextStack.frontend.includes("solid"); + const isNuxt = nextStack.webFrontend.includes("nuxt"); + const isSvelte = nextStack.webFrontend.includes("svelte"); + const isSolid = nextStack.webFrontend.includes("solid"); if ((isNuxt || isSvelte || isSolid) && nextStack.api === "trpc") { const frontendName = isNuxt ? "Nuxt" : isSvelte ? "Svelte" : "Solid"; notes.api.notes.push( `${frontendName} requires oRPC. It will be selected automatically.`, ); - notes.frontend.notes.push( + notes.webFrontend.notes.push( `Selected ${frontendName}: API will be set to oRPC.`, ); notes.api.hasIssue = true; - notes.frontend.hasIssue = true; + notes.webFrontend.hasIssue = true; nextStack.api = "orpc"; changed = true; changes.push({ @@ -528,37 +536,37 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } const incompatibleAddons: string[] = []; - const isPWACompat = hasPWACompatibleFrontend(nextStack.frontend); - const isTauriCompat = hasTauriCompatibleFrontend(nextStack.frontend); + const isPWACompat = hasPWACompatibleFrontend(nextStack.webFrontend); + const isTauriCompat = hasTauriCompatibleFrontend(nextStack.webFrontend); if (!isPWACompat && nextStack.addons.includes("pwa")) { incompatibleAddons.push("pwa"); - notes.frontend.notes.push( + notes.webFrontend.notes.push( "PWA addon requires TanStack/React Router or Solid. Addon will be removed.", ); notes.addons.notes.push( "PWA requires TanStack/React Router/Solid. It will be removed.", ); - notes.frontend.hasIssue = true; + notes.webFrontend.hasIssue = true; notes.addons.hasIssue = true; changes.push({ category: "addons", - message: "PWA addon removed (requires compatible frontend)", + message: "PWA addon removed (requires compatible web frontend)", }); } if (!isTauriCompat && nextStack.addons.includes("tauri")) { incompatibleAddons.push("tauri"); - notes.frontend.notes.push( + notes.webFrontend.notes.push( "Tauri addon requires TanStack/React Router, Nuxt, Svelte, Solid, or Next.js. Addon will be removed.", ); notes.addons.notes.push( "Tauri requires TanStack/React Router/Nuxt/Svelte/Solid/Next.js. It will be removed.", ); - notes.frontend.hasIssue = true; + notes.webFrontend.hasIssue = true; notes.addons.hasIssue = true; changes.push({ category: "addons", - message: "Tauri addon removed (requires compatible frontend)", + message: "Tauri addon removed (requires compatible web frontend)", }); } @@ -580,18 +588,19 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { } const incompatibleExamples: string[] = []; - const isWeb = hasWebFrontend(nextStack.frontend); - const isNativeOnly = checkHasNativeFrontend(nextStack.frontend) && !isWeb; + const isWeb = hasWebFrontend(nextStack.webFrontend); + const hasNative = checkHasNativeFrontend(nextStack.nativeFrontend); + const isNativeOnly = hasNative && !isWeb; if (isNativeOnly) { if (nextStack.examples.length > 0) { - notes.frontend.notes.push( + notes.webFrontend.notes.push( "Examples are not supported with Native-only frontend. Examples will be removed.", ); notes.examples.notes.push( "Examples require a web frontend. They will be removed.", ); - notes.frontend.hasIssue = true; + notes.webFrontend.hasIssue = true; notes.examples.hasIssue = true; incompatibleExamples.push(...nextStack.examples); changes.push({ @@ -653,13 +662,13 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { uniqueIncompatibleExamples.includes("todo") || uniqueIncompatibleExamples.includes("ai") ) { - notes.frontend.notes.push( + notes.webFrontend.notes.push( "Examples require a web frontend. Incompatible examples will be removed.", ); notes.examples.notes.push( "Requires a web frontend. Incompatible examples will be removed.", ); - notes.frontend.hasIssue = true; + notes.webFrontend.hasIssue = true; notes.examples.hasIssue = true; } } @@ -690,13 +699,13 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { notes.examples.hasIssue = true; } if (isSolid && uniqueIncompatibleExamples.includes("ai")) { - notes.frontend.notes.push( + notes.webFrontend.notes.push( "AI example is not compatible with Solid. It will be removed.", ); notes.examples.notes.push( "AI example is not compatible with Solid. It will be removed.", ); - notes.frontend.hasIssue = true; + notes.webFrontend.hasIssue = true; notes.examples.hasIssue = true; } @@ -720,12 +729,12 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => { const getCompatibilityRules = (stack: StackState) => { const isConvex = stack.backend === "convex"; const isBackendNone = stack.backend === "none"; - const hasWebFrontendSelected = hasWebFrontend(stack.frontend); - const hasNativeFrontend = checkHasNativeFrontend(stack.frontend); + const hasWebFrontendSelected = hasWebFrontend(stack.webFrontend); + const hasNativeFrontend = checkHasNativeFrontend(stack.nativeFrontend); const hasNativeOnly = hasNativeFrontend && !hasWebFrontendSelected; - const hasSolid = stack.frontend.includes("solid"); - const hasNuxt = stack.frontend.includes("nuxt"); - const hasSvelte = stack.frontend.includes("svelte"); + const hasSolid = stack.webFrontend.includes("solid"); + const hasNuxt = stack.webFrontend.includes("nuxt"); + const hasSvelte = stack.webFrontend.includes("svelte"); return { isConvex, @@ -733,8 +742,8 @@ const getCompatibilityRules = (stack: StackState) => { hasWebFrontend: hasWebFrontendSelected, hasNativeFrontend, hasNativeOnly, - hasPWACompatible: hasPWACompatibleFrontend(stack.frontend), - hasTauriCompatible: hasTauriCompatibleFrontend(stack.frontend), + hasPWACompatible: hasPWACompatibleFrontend(stack.webFrontend), + hasTauriCompatible: hasTauriCompatibleFrontend(stack.webFrontend), hasNuxtOrSvelteOrSolid: hasNuxt || hasSvelte || hasSolid, hasSolid, hasNuxt, @@ -764,11 +773,34 @@ const generateCommand = (stackState: StackState): string => { value: StackState[K], ) => isStackDefault(stackState, key, value); - if (!checkDefault("frontend", stackState.frontend)) { - if (stackState.frontend.length === 0 || stackState.frontend[0] === "none") { + if (!checkDefault("webFrontend", stackState.webFrontend)) { + if ( + stackState.webFrontend.length === 0 || + stackState.webFrontend[0] === "none" + ) { flags.push("--frontend none"); } else { - flags.push(`--frontend ${stackState.frontend.join(" ")}`); + flags.push(`--frontend ${stackState.webFrontend.join(" ")}`); + } + } + + if (!checkDefault("nativeFrontend", stackState.nativeFrontend)) { + if ( + stackState.nativeFrontend.length > 0 && + stackState.nativeFrontend[0] !== "none" + ) { + if (checkDefault("webFrontend", stackState.webFrontend)) { + flags.push(`--frontend ${stackState.nativeFrontend.join(" ")}`); + } else { + const existingFrontendIndex = flags.findIndex((f) => + f.startsWith("--frontend "), + ); + if (existingFrontendIndex !== -1) { + flags[existingFrontendIndex] += ` ${stackState.nativeFrontend.join( + " ", + )}`; + } + } } } @@ -876,12 +908,15 @@ const StackBuilder = () => { const catKey = category as keyof StackState; - if (["frontend", "addons", "examples"].includes(catKey)) { + if ( + ["webFrontend", "nativeFrontend", "addons", "examples"].includes(catKey) + ) { const currentValues: string[] = []; - randomStack[catKey as "frontend" | "addons" | "examples"] = - currentValues; + randomStack[ + catKey as "webFrontend" | "nativeFrontend" | "addons" | "examples" + ] = currentValues; - if (catKey === "frontend") { + if (catKey === "webFrontend" || catKey === "nativeFrontend") { const randomIndex = Math.floor(Math.random() * options.length); const selectedOption = options[randomIndex].id; currentValues.push(selectedOption); @@ -986,7 +1021,7 @@ const StackBuilder = () => { ); } } else if ( - catKey === "frontend" && + catKey === "webFrontend" && (techId === "nuxt" || techId === "solid") ) { addRule( @@ -1274,9 +1309,15 @@ const StackBuilder = () => { if (!options) continue; if (Array.isArray(selectedValue)) { - if (selectedValue.length === 0 || selectedValue[0] === "none") continue; + 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) { badges.push( @@ -1376,7 +1417,8 @@ const StackBuilder = () => { const currentValue = currentStack[catKey]; if ( - catKey === "frontend" || + catKey === "webFrontend" || + catKey === "nativeFrontend" || catKey === "addons" || catKey === "examples" ) { @@ -1386,7 +1428,7 @@ const StackBuilder = () => { let nextArray = [...currentArray]; const isSelected = currentArray.includes(techId); - if (catKey === "frontend") { + if (catKey === "webFrontend") { const webTypes = [ "tanstack-router", "react-router", @@ -1405,19 +1447,15 @@ const StackBuilder = () => { nextArray = ["none"]; } } else { - nextArray = nextArray.filter((id) => id !== "none"); - if (webTypes.includes(techId)) { - nextArray = nextArray.filter((id) => !webTypes.includes(id)); - } else if (techId.startsWith("native-")) { - nextArray = nextArray.filter((id) => !id.startsWith("native-")); - } - nextArray.push(techId); + nextArray = [techId]; } - if (nextArray.length > 1) { - nextArray = nextArray.filter((id) => id !== "none"); - } - if (nextArray.length === 0) { + } else if (catKey === "nativeFrontend") { + if (techId === "none") { nextArray = ["none"]; + } else if (isSelected) { + nextArray = ["none"]; + } else { + nextArray = [techId]; } } else { if (isSelected) { @@ -1425,6 +1463,16 @@ const StackBuilder = () => { } else { nextArray.push(techId); } + if (nextArray.length > 1) { + nextArray = nextArray.filter((id) => id !== "none"); + } + if ( + nextArray.length === 0 && + (catKey === "addons" || catKey === "examples") + ) { + } else if (nextArray.length === 0) { + nextArray = ["none"]; + } } const uniqueNext = [...new Set(nextArray)].sort(); @@ -1730,7 +1778,8 @@ const StackBuilder = () => { if ( category === "addons" || category === "examples" || - category === "frontend" + category === "webFrontend" || + category === "nativeFrontend" ) { isSelected = ( (currentValue as string[]) || [] diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index c9b7934..c0bea51 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -23,7 +23,7 @@ export const TECH_OPTIONS = { color: "from-gray-400 to-gray-600", }, ], - frontend: [ + webFrontend: [ { id: "tanstack-router", name: "TanStack Router", @@ -81,6 +81,16 @@ export const TECH_OPTIONS = { color: "from-blue-600 to-blue-800", default: false, }, + { + id: "none", + name: "No Web Frontend", + description: "No web-based frontend", + icon: "⚙️", + color: "from-gray-400 to-gray-600", + default: false, + }, + ], + nativeFrontend: [ { id: "native-nativewind", name: "React Native + NativeWind", @@ -99,9 +109,9 @@ export const TECH_OPTIONS = { }, { id: "none", - name: "No Frontend", - description: "API-only backend", - icon: "⚙️", + name: "No Native Frontend", + description: "No native mobile frontend", + icon: "📱", color: "from-gray-400 to-gray-600", default: false, }, @@ -427,7 +437,8 @@ export const PRESET_TEMPLATES = [ description: "Standard web app with TanStack Router, Bun, Hono and SQLite", stack: { projectName: "my-better-t-app", - frontend: ["tanstack-router"], + webFrontend: ["tanstack-router"], + nativeFrontend: ["none"], runtime: "bun", backend: "hono", database: "sqlite", @@ -448,7 +459,8 @@ export const PRESET_TEMPLATES = [ description: "Reactive full-stack app with Convex and TanStack Router", stack: { projectName: "my-better-t-app", - frontend: ["tanstack-router"], + webFrontend: ["tanstack-router"], + nativeFrontend: ["none"], backend: "convex", runtime: "none", database: "none", @@ -469,7 +481,8 @@ export const PRESET_TEMPLATES = [ description: "React Native with Expo and SQLite database", stack: { projectName: "my-better-t-app", - frontend: ["native-nativewind"], + webFrontend: ["none"], + nativeFrontend: ["native-nativewind"], runtime: "bun", backend: "hono", database: "sqlite", @@ -490,7 +503,8 @@ export const PRESET_TEMPLATES = [ description: "Backend API with Hono and PostgreSQL", stack: { projectName: "my-better-t-app", - frontend: ["none"], + webFrontend: ["none"], + nativeFrontend: ["none"], runtime: "bun", backend: "hono", database: "postgres", @@ -511,7 +525,8 @@ export const PRESET_TEMPLATES = [ description: "Complete setup with web, native, Turso, and addons", stack: { projectName: "my-better-t-app", - frontend: ["tanstack-router", "native-nativewind"], + webFrontend: ["tanstack-router"], + nativeFrontend: ["native-nativewind"], runtime: "bun", backend: "hono", database: "sqlite", @@ -530,7 +545,8 @@ export const PRESET_TEMPLATES = [ export type StackState = { projectName: string; - frontend: string[]; + webFrontend: string[]; + nativeFrontend: string[]; runtime: string; backend: string; database: string; @@ -547,7 +563,8 @@ export type StackState = { export const DEFAULT_STACK: StackState = { projectName: "my-better-t-app", - frontend: ["tanstack-router"], + webFrontend: ["tanstack-router"], + nativeFrontend: ["none"], runtime: "bun", backend: "hono", database: "sqlite", @@ -585,6 +602,24 @@ export const isStackDefault = ( return true; } + if (key === "webFrontend" && stack.webFrontend) { + const currentWeb = (stack.webFrontend as string[]).filter( + (f) => !f.startsWith("native-") && f !== "none", + ); + const currentNative = (stack.webFrontend as string[]).filter((f) => + f.startsWith("native-"), + ); + + if (key === "webFrontend") { + const defaultWeb = (DEFAULT_STACK.webFrontend as string[]).sort(); + const valueWeb = (value as string[]).sort(); + return ( + defaultWeb.length === valueWeb.length && + defaultWeb.every((item, index) => item === valueWeb[index]) + ); + } + } + if (Array.isArray(defaultValue) && Array.isArray(value)) { const sortedDefault = [...defaultValue].sort(); const sortedValue = [...value].sort(); diff --git a/apps/web/src/lib/stack-url-state.ts b/apps/web/src/lib/stack-url-state.ts index a9dac72..6d426bb 100644 --- a/apps/web/src/lib/stack-url-state.ts +++ b/apps/web/src/lib/stack-url-state.ts @@ -12,7 +12,12 @@ const getValidIds = (category: keyof typeof TECH_OPTIONS): string[] => { export const stackParsers = { projectName: parseAsString.withDefault(DEFAULT_STACK.projectName), - frontend: parseAsArrayOf(parseAsString).withDefault(DEFAULT_STACK.frontend), + webFrontend: parseAsArrayOf(parseAsString).withDefault( + DEFAULT_STACK.webFrontend, + ), + nativeFrontend: parseAsArrayOf(parseAsString).withDefault( + DEFAULT_STACK.nativeFrontend, + ), runtime: parseAsStringEnum( getValidIds("runtime"), ).withDefault(DEFAULT_STACK.runtime), @@ -50,7 +55,8 @@ export const stackParsers = { export const stackUrlKeys: UrlKeys = { projectName: "name", - frontend: "fe", + webFrontend: "fe-w", + nativeFrontend: "fe-n", runtime: "rt", backend: "be", api: "api",