From d943bf0d80d6938f7b18d60ef6911299551023ee Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Sun, 6 Apr 2025 18:07:40 +0530 Subject: [PATCH] add tanstack start --- .changeset/shiny-bees-sin.md | 5 + apps/cli/README.md | 4 +- apps/cli/src/helpers/addons-setup.ts | 3 - apps/cli/src/helpers/auth-setup.ts | 3 +- apps/cli/src/helpers/env-setup.ts | 9 +- apps/cli/src/helpers/examples-setup.ts | 48 ++--- apps/cli/src/helpers/post-installation.ts | 8 +- apps/cli/src/helpers/template-manager.ts | 26 ++- apps/cli/src/index.ts | 56 +++--- apps/cli/src/prompts/addons.ts | 9 +- apps/cli/src/prompts/examples.ts | 3 +- apps/cli/src/prompts/frontend-option.ts | 15 +- apps/cli/src/types.ts | 6 +- .../src/components/mode-toggle.tsx | 0 .../src/components/theme-provider.tsx | 0 .../src/components/mode-toggle.tsx | 37 ++++ .../src/components/theme-provider.tsx | 73 ++++++++ .../web-tanstack-router/src/routes/__root.tsx | 3 +- .../apps/web-tanstack-start/app.config.ts | 17 ++ .../base/apps/web-tanstack-start/package.json | 52 ++++++ .../apps/web-tanstack-start/public/robots.txt | 3 + .../base/apps/web-tanstack-start/src/api.ts | 6 + .../apps/web-tanstack-start/src/client.tsx | 8 + .../src/components/header.tsx | 27 +++ .../apps/web-tanstack-start/src/index.css | 135 ++++++++++++++ .../apps/web-tanstack-start/src/router.tsx | 70 ++++++++ .../web-tanstack-start/src/routes/__root.tsx | 68 ++++++++ .../web-tanstack-start/src/routes/index.tsx | 88 ++++++++++ .../base/apps/web-tanstack-start/src/ssr.tsx | 12 ++ .../apps/web-tanstack-start/src/utils/trpc.ts | 5 + .../apps/web-tanstack-start/tsconfig.json | 28 +++ .../apps/web-tanstack-start/src/routes/ai.tsx | 69 ++++++++ .../web-tanstack-start/src/routes/todos.tsx | 135 ++++++++++++++ .../src/utils/trpc.ts | 0 .../web-tanstack-router/src/utils/trpc.ts | 39 +++++ .../src/components/header.tsx | 32 ++++ .../src/components/sign-in-form.tsx | 139 +++++++++++++++ .../src/components/sign-up-form.tsx | 164 ++++++++++++++++++ .../src/components/user-menu.tsx | 62 +++++++ .../apps/web-tanstack-start/src/router.tsx | 76 ++++++++ .../src/routes/dashboard.tsx | 37 ++++ .../web-tanstack-start/src/routes/login.tsx | 18 ++ .../app/(home)/_components/StackArchitech.tsx | 23 ++- apps/web/src/lib/constant.ts | 8 + 44 files changed, 1559 insertions(+), 70 deletions(-) create mode 100644 .changeset/shiny-bees-sin.md rename apps/cli/template/base/apps/{web-base => web-react-router}/src/components/mode-toggle.tsx (100%) rename apps/cli/template/base/apps/{web-base => web-react-router}/src/components/theme-provider.tsx (100%) create mode 100644 apps/cli/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-start/app.config.ts create mode 100644 apps/cli/template/base/apps/web-tanstack-start/package.json create mode 100644 apps/cli/template/base/apps/web-tanstack-start/public/robots.txt create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/api.ts create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/client.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/index.css create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/router.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/routes/__root.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/routes/index.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx create mode 100644 apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts create mode 100644 apps/cli/template/base/apps/web-tanstack-start/tsconfig.json create mode 100644 apps/cli/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx create mode 100644 apps/cli/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx rename apps/cli/template/with-auth/apps/{web-base => web-react-router}/src/utils/trpc.ts (100%) create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/router.tsx create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx create mode 100644 apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx diff --git a/.changeset/shiny-bees-sin.md b/.changeset/shiny-bees-sin.md new file mode 100644 index 0000000..0183a82 --- /dev/null +++ b/.changeset/shiny-bees-sin.md @@ -0,0 +1,5 @@ +--- +"create-better-t-stack": minor +--- + +add tanstack start diff --git a/apps/cli/README.md b/apps/cli/README.md index b43bad1..3b23bed 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -49,7 +49,7 @@ Options: --orm ORM type (none, drizzle, prisma) --auth Include authentication --no-auth Exclude authentication - --frontend Frontend types (web, native, none) + --frontend Frontend types (tanstack-router, react-router, tanstack-start, native, none) --addons Additional addons (pwa, tauri, biome, husky, none) --examples Examples to include (todo, ai) --no-examples Skip all examples @@ -60,6 +60,8 @@ Options: --no-install Skip installing dependencies --turso Set up Turso for SQLite database --no-turso Skip Turso setup + --prisma-postgres Set up Prisma Postgres + --no-prisma-postgres Skip Prisma Postgres setup --backend Backend framework (hono, elysia) --runtime Runtime (bun, node) -h, --help Display help diff --git a/apps/cli/src/helpers/addons-setup.ts b/apps/cli/src/helpers/addons-setup.ts index e1d808f..acadec7 100644 --- a/apps/cli/src/helpers/addons-setup.ts +++ b/apps/cli/src/helpers/addons-setup.ts @@ -18,9 +18,6 @@ export async function setupAddons( const hasWebFrontend = frontends.includes("react-router") || frontends.includes("tanstack-router"); - // if (addons.includes("docker")) { - // await setupDocker(projectDir); - // } if (addons.includes("pwa") && hasWebFrontend) { await setupPwa(projectDir, frontends); } diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts index b20d8dc..2a28ab0 100644 --- a/apps/cli/src/helpers/auth-setup.ts +++ b/apps/cli/src/helpers/auth-setup.ts @@ -31,7 +31,8 @@ export async function setupAuth( try { if ( frontends.includes("react-router") || - frontends.includes("tanstack-router") + frontends.includes("tanstack-router") || + frontends.includes("tanstack-start") ) { addPackageDependency({ dependencies: ["better-auth"], diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts index 42ca562..42974f5 100644 --- a/apps/cli/src/helpers/env-setup.ts +++ b/apps/cli/src/helpers/env-setup.ts @@ -19,12 +19,13 @@ export async function setupEnvironmentVariables( if (!envContent.includes("CORS_ORIGIN")) { const hasReactRouter = options.frontend.includes("react-router"); const hasTanStackRouter = options.frontend.includes("tanstack-router"); + const hasTanStackStart = options.frontend.includes("tanstack-start"); let corsOrigin = "http://localhost:3000"; if (hasReactRouter) { corsOrigin = "http://localhost:5173"; - } else if (hasTanStackRouter) { + } else if (hasTanStackRouter || hasTanStackStart) { corsOrigin = "http://localhost:3001"; } @@ -68,10 +69,12 @@ export async function setupEnvironmentVariables( const hasReactRouter = options.frontend.includes("react-router"); const hasTanStackRouter = options.frontend.includes("tanstack-router"); + const hasTanStackStart = options.frontend.includes("tanstack-start"); + const hasWebFrontend = + hasReactRouter || hasTanStackRouter || hasTanStackStart; - if (hasReactRouter || hasTanStackRouter) { + if (hasWebFrontend) { const clientDir = path.join(projectDir, "apps/web"); - await setupClientEnvFile(clientDir); } diff --git a/apps/cli/src/helpers/examples-setup.ts b/apps/cli/src/helpers/examples-setup.ts index 81dedad..a6fb2be 100644 --- a/apps/cli/src/helpers/examples-setup.ts +++ b/apps/cli/src/helpers/examples-setup.ts @@ -13,12 +13,19 @@ export async function setupExamples( frontend: ProjectFrontend[] = ["tanstack-router"], ): Promise { const hasTanstackRouter = frontend.includes("tanstack-router"); + const hasTanstackStart = frontend.includes("tanstack-start"); const hasReactRouter = frontend.includes("react-router"); - const hasWebFrontend = hasTanstackRouter || hasReactRouter; + const hasWebFrontend = + hasTanstackRouter || hasReactRouter || hasTanstackStart; - const routerType = hasTanstackRouter - ? "web-tanstack-router" - : "web-react-router"; + let routerType: string; + if (hasTanstackRouter) { + routerType = "web-tanstack-router"; + } else if (hasTanstackStart) { + routerType = "web-tanstack-start"; + } else { + routerType = "web-react-router"; + } const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web")); @@ -87,27 +94,28 @@ async function updateServerIndexWithAIRoute(projectDir: string): Promise { const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`; const aiRouteHandler = ` - app.post("/ai", async (c) => { - const body = await c.req.json(); - const messages = body.messages || []; +// AI chat endpoint +app.post("/ai", async (c) => { + const body = await c.req.json(); + const messages = body.messages || []; - const result = streamText({ - model: google("gemini-2.0-flash-exp"), - messages, - }); + const result = streamText({ + model: google("gemini-1.5-flash"), + messages, + }); - c.header("X-Vercel-AI-Data-Stream", "v1"); - c.header("Content-Type", "text/plain; charset=utf-8"); + c.header("X-Vercel-AI-Data-Stream", "v1"); + c.header("Content-Type", "text/plain; charset=utf-8"); - return stream(c, (stream) => stream.pipe(result.toDataStream())); - });`; + return stream(c, (stream) => stream.pipe(result.toDataStream())); +});`; if (indexContent.includes("import {")) { const lastImportIndex = indexContent.lastIndexOf("import"); const endOfLastImport = indexContent.indexOf("\n", lastImportIndex); indexContent = `${indexContent.substring(0, endOfLastImport + 1)} - ${importSection} - ${indexContent.substring(endOfLastImport + 1)}`; +${importSection} +${indexContent.substring(endOfLastImport + 1)}`; } else { indexContent = `${importSection} @@ -177,7 +185,7 @@ async function setupTodoExample( const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo"); if (await fs.pathExists(todoExampleDir)) { - const todoRouteSourceDir = path.join( + const todoRouteSourcePath = path.join( todoExampleDir, `apps/${routerType}/src/routes/todos.tsx`, ); @@ -186,8 +194,8 @@ async function setupTodoExample( "apps/web/src/routes/todos.tsx", ); - if (await fs.pathExists(todoRouteSourceDir)) { - await fs.copy(todoRouteSourceDir, todoRouteTargetPath, { + if (await fs.pathExists(todoRouteSourcePath)) { + await fs.copy(todoRouteSourcePath, todoRouteTargetPath, { overwrite: true, }); } diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts index d046baa..3c0d255 100644 --- a/apps/cli/src/helpers/post-installation.ts +++ b/apps/cli/src/helpers/post-installation.ts @@ -43,11 +43,15 @@ export function displayPostInstallInstructions( : ""; const hasTanstackRouter = frontends?.includes("tanstack-router"); + const hasTanstackStart = frontends?.includes("tanstack-start"); const hasReactRouter = frontends?.includes("react-router"); - const hasWebFrontend = hasTanstackRouter || hasReactRouter; + const hasWebFrontend = + hasTanstackRouter || hasReactRouter || hasTanstackStart; const hasNativeFrontend = frontends?.includes("native"); const hasFrontend = hasWebFrontend || hasNativeFrontend; + const webPort = hasReactRouter ? "5173" : "3001"; + note( `${pc.cyan("1.")} ${cdCmd} ${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev @@ -55,7 +59,7 @@ ${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan ${pc.bold("Your project will be available at:")} ${ hasFrontend - ? `${hasWebFrontend ? `${pc.cyan("•")} Frontend: http://localhost:${hasReactRouter ? "5173" : "3001"}\n` : ""}` + ? `${hasWebFrontend ? `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n` : ""}` : `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n` }${pc.cyan("•")} API: http://localhost:3000 ${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""} diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts index cfba254..f694b33 100644 --- a/apps/cli/src/helpers/template-manager.ts +++ b/apps/cli/src/helpers/template-manager.ts @@ -49,10 +49,11 @@ export async function setupFrontendTemplates( frontends: ProjectFrontend[], ): Promise { const hasTanstackWeb = frontends.includes("tanstack-router"); + const hasTanstackStart = frontends.includes("tanstack-start"); const hasReactRouterWeb = frontends.includes("react-router"); const hasNative = frontends.includes("native"); - if (hasTanstackWeb || hasReactRouterWeb) { + if (hasTanstackWeb || hasReactRouterWeb || hasTanstackStart) { const webDir = path.join(projectDir, "apps/web"); await fs.ensureDir(webDir); @@ -61,9 +62,13 @@ export async function setupFrontendTemplates( await fs.copy(webBaseDir, webDir); } - const frameworkName = hasTanstackWeb - ? "web-tanstack-router" - : "web-react-router"; + let frameworkName = "web-react-router"; + if (hasTanstackWeb) { + frameworkName = "web-tanstack-router"; + } else if (hasTanstackStart) { + frameworkName = "web-tanstack-start"; + } + const webFrameworkDir = path.join( PKG_ROOT, `template/base/apps/${frameworkName}`, @@ -155,8 +160,9 @@ export async function setupAuthTemplate( if (await fs.pathExists(authTemplateDir)) { const hasReactRouter = frontends.includes("react-router"); const hasTanStackRouter = frontends.includes("tanstack-router"); + const hasTanStackStart = frontends.includes("tanstack-start"); - if (hasReactRouter || hasTanStackRouter) { + if (hasReactRouter || hasTanStackRouter || hasTanStackStart) { const webDir = path.join(projectDir, "apps/web"); const webBaseAuthDir = path.join(authTemplateDir, "apps/web-base"); @@ -183,6 +189,16 @@ export async function setupAuthTemplate( await fs.copy(tanstackAuthDir, webDir, { overwrite: true }); } } + + if (hasTanStackStart) { + const tanstackStartAuthDir = path.join( + authTemplateDir, + "apps/web-tanstack-start", + ); + if (await fs.pathExists(tanstackStartAuthDir)) { + await fs.copy(tanstackStartAuthDir, webDir, { overwrite: true }); + } + } } const serverAuthDir = path.join(authTemplateDir, "apps/server/src"); diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 8edd50c..1f054bc 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -43,7 +43,7 @@ async function main() { .option("--no-auth", "Exclude authentication") .option( "--frontend ", - "Frontend types (tanstack-router, react-router, native, none)", + "Frontend types (tanstack-router, react-router, tanstack-start, native, none)", ) .option( "--addons ", @@ -271,7 +271,7 @@ function validateOptions(options: CLIOptions): void { if ( options.frontend && !options.frontend.some((f) => - ["tanstack-router", "react-router"].includes(f), + ["tanstack-router", "react-router", "tanstack-start"].includes(f), ) && !options.frontend.includes("none") ) { @@ -288,6 +288,7 @@ function validateOptions(options: CLIOptions): void { const validFrontends = [ "tanstack-router", "react-router", + "tanstack-start", "native", "none", ]; @@ -305,13 +306,16 @@ function validateOptions(options: CLIOptions): void { } const webFrontends = options.frontend.filter( - (f) => f === "tanstack-router" || f === "react-router", + (f) => + f === "tanstack-router" || + f === "react-router" || + f === "tanstack-start", ); if (webFrontends.length > 1) { cancel( pc.red( - "Cannot select multiple web frameworks. Choose only one of: tanstack-router, react-router", + "Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router", ), ); process.exit(1); @@ -357,14 +361,13 @@ function validateOptions(options: CLIOptions): void { options.frontend && !options.frontend.some((f) => ["tanstack-router", "react-router"].includes(f), - ) && - !options.frontend.includes("none") + ) ) { cancel( pc.red( - `PWA and Tauri addons require a web frontend. Cannot use --addons ${options.addons + `PWA and Tauri addons require tanstack-router or react-router. Cannot use --addons ${options.addons .filter((a) => webSpecificAddons.includes(a)) - .join(", ")} with --frontend native only`, + .join(", ")} with incompatible frontend options.`, ), ); process.exit(1); @@ -384,11 +387,17 @@ function processFlags( } else { frontend = options.frontend.filter( (f): f is ProjectFrontend => - f === "tanstack-router" || f === "react-router" || f === "native", + f === "tanstack-router" || + f === "react-router" || + f === "tanstack-start" || + f === "native", ); const webFrontends = frontend.filter( - (f) => f === "tanstack-router" || f === "react-router", + (f) => + f === "tanstack-router" || + f === "react-router" || + f === "tanstack-start", ); if (webFrontends.length > 1) { @@ -448,8 +457,9 @@ function processFlags( if ( frontend && frontend.length > 0 && - !frontend.includes("tanstack-router") && - !frontend.includes("react-router") + !frontend.some((f) => + ["tanstack-router", "react-router", "tanstack-start"].includes(f), + ) ) { examples = []; log.warn( @@ -481,19 +491,23 @@ function processFlags( addon === "husky", ); - if ( - frontend && - frontend.length > 0 && - !frontend.includes("tanstack-router") && - !frontend.includes("react-router") - ) { - addons = addons.filter((addon) => !["pwa", "tauri"].includes(addon)); - if (addons.length !== options.addons.length) { + const hasCompatibleWebFrontend = frontend?.some( + (f) => f === "tanstack-router" || f === "react-router", + ); + + if (!hasCompatibleWebFrontend) { + const webSpecificAddons = ["pwa", "tauri"]; + const filteredAddons = addons.filter( + (addon) => !webSpecificAddons.includes(addon), + ); + + if (filteredAddons.length !== addons.length) { log.warn( pc.yellow( - "PWA and Tauri addons require web frontend - removing these addons", + "PWA and Tauri addons require tanstack-router or react-router - removing these addons", ), ); + addons = filteredAddons; } } diff --git a/apps/cli/src/prompts/addons.ts b/apps/cli/src/prompts/addons.ts index 77fb0e6..5b92671 100644 --- a/apps/cli/src/prompts/addons.ts +++ b/apps/cli/src/prompts/addons.ts @@ -9,7 +9,7 @@ export async function getAddonsChoice( ): Promise { if (Addons !== undefined) return Addons; - const hasWeb = + const hasCompatibleWebFrontend = frontends?.includes("react-router") || frontends?.includes("tanstack-router"); @@ -39,10 +39,13 @@ export async function getAddonsChoice( }, ]; - const options = hasWeb ? [...webAddonOptions, ...addonOptions] : addonOptions; + const options = hasCompatibleWebFrontend + ? [...webAddonOptions, ...addonOptions] + : addonOptions; const initialValues = DEFAULT_CONFIG.addons.filter( - (addon) => hasWeb || (addon !== "pwa" && addon !== "tauri"), + (addon) => + hasCompatibleWebFrontend || (addon !== "pwa" && addon !== "tauri"), ); const response = await multiselect({ diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts index 6319b2a..8d6ef41 100644 --- a/apps/cli/src/prompts/examples.ts +++ b/apps/cli/src/prompts/examples.ts @@ -20,7 +20,8 @@ export async function getExamplesChoice( const hasWebFrontend = frontends?.includes("react-router") || - frontends?.includes("tanstack-router"); + frontends?.includes("tanstack-router") || + frontends?.includes("tanstack-start"); if (!hasWebFrontend) return []; diff --git a/apps/cli/src/prompts/frontend-option.ts b/apps/cli/src/prompts/frontend-option.ts index d8c4318..d9373ee 100644 --- a/apps/cli/src/prompts/frontend-option.ts +++ b/apps/cli/src/prompts/frontend-option.ts @@ -23,7 +23,10 @@ export async function getFrontendChoice( }, ], initialValues: DEFAULT_CONFIG.frontend.some( - (f) => f === "tanstack-router" || f === "react-router", + (f) => + f === "tanstack-router" || + f === "react-router" || + f === "tanstack-start", ) ? ["web"] : [], @@ -50,10 +53,18 @@ export async function getFrontendChoice( label: "React Router", hint: "A user‑obsessed, standards‑focused, multi‑strategy router you can deploy anywhere.", }, + { + value: "tanstack-start", + label: "TanStack Start (beta)", + hint: "SSR, Streaming, Server Functions, API Routes, bundling and more powered by TanStack Router and Vite.", + }, ], initialValue: DEFAULT_CONFIG.frontend.find( - (f) => f === "tanstack-router" || f === "react-router", + (f) => + f === "tanstack-router" || + f === "react-router" || + f === "tanstack-start", ) || "tanstack-router", }); diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts index 85079a8..be8ed13 100644 --- a/apps/cli/src/types.ts +++ b/apps/cli/src/types.ts @@ -5,7 +5,11 @@ export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky"; export type ProjectBackend = "hono" | "elysia"; export type ProjectRuntime = "node" | "bun"; export type ProjectExamples = "todo" | "ai"; -export type ProjectFrontend = "react-router" | "tanstack-router" | "native"; +export type ProjectFrontend = + | "react-router" + | "tanstack-router" + | "tanstack-start" + | "native"; export interface ProjectConfig { projectName: string; diff --git a/apps/cli/template/base/apps/web-base/src/components/mode-toggle.tsx b/apps/cli/template/base/apps/web-react-router/src/components/mode-toggle.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/mode-toggle.tsx rename to apps/cli/template/base/apps/web-react-router/src/components/mode-toggle.tsx diff --git a/apps/cli/template/base/apps/web-base/src/components/theme-provider.tsx b/apps/cli/template/base/apps/web-react-router/src/components/theme-provider.tsx similarity index 100% rename from apps/cli/template/base/apps/web-base/src/components/theme-provider.tsx rename to apps/cli/template/base/apps/web-react-router/src/components/theme-provider.tsx diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx b/apps/cli/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx new file mode 100644 index 0000000..199d09f --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-router/src/components/mode-toggle.tsx @@ -0,0 +1,37 @@ +import { Moon, Sun } from "lucide-react"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useTheme } from "@/components/theme-provider"; + +export function ModeToggle() { + const { setTheme } = useTheme(); + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + setTheme("system")}> + System + + + + ); +} diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx b/apps/cli/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx new file mode 100644 index 0000000..7b9eeb2 --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-router/src/components/theme-provider.tsx @@ -0,0 +1,73 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "dark" | "light" | "system"; + +type ThemeProviderProps = { + children: React.ReactNode; + defaultTheme?: Theme; + storageKey?: string; +}; + +type ThemeProviderState = { + theme: Theme; + setTheme: (theme: Theme) => void; +}; + +const initialState: ThemeProviderState = { + theme: "system", + setTheme: () => null, +}; + +const ThemeProviderContext = createContext(initialState); + +export function ThemeProvider({ + children, + defaultTheme = "system", + storageKey = "vite-ui-theme", + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ); + + useEffect(() => { + const root = window.document.documentElement; + + root.classList.remove("light", "dark"); + + if (theme === "system") { + const systemTheme = window.matchMedia("(prefers-color-scheme: dark)") + .matches + ? "dark" + : "light"; + + root.classList.add(systemTheme); + return; + } + + root.classList.add(theme); + }, [theme]); + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme); + setTheme(theme); + }, + }; + + return ( + + {children} + + ); +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext); + + if (context === undefined) + throw new Error("useTheme must be used within a ThemeProvider"); + + return context; +}; diff --git a/apps/cli/template/base/apps/web-tanstack-router/src/routes/__root.tsx b/apps/cli/template/base/apps/web-tanstack-router/src/routes/__root.tsx index 9787deb..2f47b7d 100644 --- a/apps/cli/template/base/apps/web-tanstack-router/src/routes/__root.tsx +++ b/apps/cli/template/base/apps/web-tanstack-router/src/routes/__root.tsx @@ -50,8 +50,7 @@ function RootComponent() {
- {isFetching && } - + {isFetching ? : }
diff --git a/apps/cli/template/base/apps/web-tanstack-start/app.config.ts b/apps/cli/template/base/apps/web-tanstack-start/app.config.ts new file mode 100644 index 0000000..e891c17 --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/app.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from "@tanstack/react-start/config"; +import viteTsConfigPaths from "vite-tsconfig-paths"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + tsr: { + appDirectory: "src", + }, + vite: { + plugins: [ + viteTsConfigPaths({ + projects: ["./tsconfig.json"], + }), + tailwindcss(), + ], + }, +}); diff --git a/apps/cli/template/base/apps/web-tanstack-start/package.json b/apps/cli/template/base/apps/web-tanstack-start/package.json new file mode 100644 index 0000000..6446b2a --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/package.json @@ -0,0 +1,52 @@ +{ + "name": "tanstack-start", + "private": true, + "type": "module", + "scripts": { + "start": "vinxi start", + "build": "vinxi build", + "serve": "vite preview", + "dev": "vinxi dev --port=3001" + }, + "dependencies": { + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@tanstack/react-form": "^1.0.5", + "@tailwindcss/vite": "^4.0.6", + "@tanstack/react-query": "^5.71.10", + "@tanstack/react-router": "^1.114.3", + "@tanstack/react-router-with-query": "^1.114.3", + "@tanstack/react-start": "^1.114.3", + "@tanstack/router-plugin": "^1.114.3", + "@trpc/client": "^11.0.2", + "@trpc/server": "^11.0.2", + "@trpc/tanstack-react-query": "^11.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.473.0", + "next-themes": "^0.4.6", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "sonner": "^2.0.3", + "tailwindcss": "^4.1.3", + "tailwind-merge": "^2.6.0", + "tw-animate-css": "^1.2.5", + "vinxi": "^0.5.3", + "vite-tsconfig-paths": "^5.1.4" + }, + "devDependencies": { + "@tanstack/react-router-devtools": "^1.114.3", + "@tanstack/react-query-devtools": "^5.71.10", + "@testing-library/dom": "^10.4.0", + "@testing-library/react": "^16.2.0", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^26.0.0", + "typescript": "^5.7.2", + "vite": "^6.1.0", + "web-vitals": "^4.2.4" + } +} diff --git a/apps/cli/template/base/apps/web-tanstack-start/public/robots.txt b/apps/cli/template/base/apps/web-tanstack-start/public/robots.txt new file mode 100644 index 0000000..e9e57dc --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/api.ts b/apps/cli/template/base/apps/web-tanstack-start/src/api.ts new file mode 100644 index 0000000..ac23f7c --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/api.ts @@ -0,0 +1,6 @@ +import { + createStartAPIHandler, + defaultAPIFileRouteHandler, +} from "@tanstack/react-start/api"; + +export default createStartAPIHandler(defaultAPIFileRouteHandler); diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/client.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/client.tsx new file mode 100644 index 0000000..ca1d304 --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/client.tsx @@ -0,0 +1,8 @@ +import { StartClient } from "@tanstack/react-start"; +import { hydrateRoot } from "react-dom/client"; + +import { createRouter } from "./router"; + +const router = createRouter(); + +hydrateRoot(document, ); diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx new file mode 100644 index 0000000..a315561 --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/components/header.tsx @@ -0,0 +1,27 @@ +import { Link } from "@tanstack/react-router"; + +export default function Header() { + const links = [ + { to: "/", label: "Home" }, + ]; + + return ( +
+
+ +
+
+
+ ); +} diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/index.css b/apps/cli/template/base/apps/web-tanstack-start/src/index.css new file mode 100644 index 0000000..1c64373 --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/index.css @@ -0,0 +1,135 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + --font-sans: + "Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +} + +html, +body { + @apply bg-white dark:bg-gray-950; + + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/router.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/router.tsx new file mode 100644 index 0000000..eaea17d --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/router.tsx @@ -0,0 +1,70 @@ +import { + QueryCache, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; +import { createRouter as createTanstackRouter } from "@tanstack/react-router"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import { toast } from "sonner"; +import type { AppRouter } from "../../server/src/routers"; +import Loader from "./components/loader"; +import "./index.css"; +import { routeTree } from "./routeTree.gen"; +import { TRPCProvider } from "./utils/trpc"; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + toast.error(error.message, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), + defaultOptions: { queries: { staleTime: 60 * 1000 } }, +}); + +const trpcClient = createTRPCClient({ + links: [ + httpBatchLink({ + url: `${import.meta.env.VITE_SERVER_URL}/trpc`, + }), + ], +}); + +const trpc = createTRPCOptionsProxy({ + client: trpcClient, + queryClient: queryClient, +}); + +export const createRouter = () => { + const router = createTanstackRouter({ + routeTree, + scrollRestoration: true, + defaultPreloadStaleTime: 0, + context: { trpc, queryClient }, + defaultPendingComponent: () => , + defaultNotFoundComponent: () =>
Not Found
, + Wrap: ({ children }) => ( + + + {children} + + + ), + }); + + return router; +}; + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/routes/__root.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/routes/__root.tsx new file mode 100644 index 0000000..d44549d --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/routes/__root.tsx @@ -0,0 +1,68 @@ +import { Toaster } from "@/components/ui/sonner"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import { + HeadContent, + Outlet, + Scripts, + createRootRouteWithContext, + useRouterState, +} from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/react-router-devtools"; +import Header from "../components/header"; +import appCss from "../index.css?url"; +import type { QueryClient } from "@tanstack/react-query"; +import type { TRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import type { AppRouter } from "../../../server/src/routers"; +import Loader from "@/components/loader"; + +export interface RouterAppContext { + trpc: TRPCOptionsProxy; + queryClient: QueryClient; +} + +export const Route = createRootRouteWithContext()({ + head: () => ({ + meta: [ + { + charSet: "utf-8", + }, + { + name: "viewport", + content: "width=device-width, initial-scale=1", + }, + { + title: "My App", + }, + ], + links: [ + { + rel: "stylesheet", + href: appCss, + }, + ], + }), + + component: RootDocument, +}); + +function RootDocument() { + const isFetching = useRouterState({ select: (s) => s.isLoading }); + + return ( + + + + + +
+
+ {isFetching ? : } +
+ + + + + + + ); +} diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/routes/index.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/routes/index.tsx new file mode 100644 index 0000000..5bf6e63 --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/routes/index.tsx @@ -0,0 +1,88 @@ +import { useTRPC } from "@/utils/trpc"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + component: HomeComponent, +}); + +const TITLE_TEXT = ` + ██████╗ ███████╗████████╗████████╗███████╗██████╗ + ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗ + ██████╔╝█████╗ ██║ ██║ █████╗ ██████╔╝ + ██╔══██╗██╔══╝ ██║ ██║ ██╔══╝ ██╔══██╗ + ██████╔╝███████╗ ██║ ██║ ███████╗██║ ██║ + ╚═════╝ ╚══════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ + + ████████╗ ███████╗████████╗ █████╗ ██████╗██╗ ██╗ + ╚══██╔══╝ ██╔════╝╚══██╔══╝██╔══██╗██╔════╝██║ ██╔╝ + ██║ ███████╗ ██║ ███████║██║ █████╔╝ + ██║ ╚════██║ ██║ ██╔══██║██║ ██╔═██╗ + ██║ ███████║ ██║ ██║ ██║╚██████╗██║ ██╗ + ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ + `; + +function HomeComponent() { + const trpc = useTRPC(); + const healthCheck = useQuery(trpc.healthCheck.queryOptions()); + + return ( +
+
{TITLE_TEXT}
+
+
+

API Status

+
+
+ + {healthCheck.isLoading + ? "Checking..." + : healthCheck.data + ? "Connected" + : "Disconnected"} + +
+
+ +
+

Core Features

+
    + + + + +
+
+
+
+ ); +} + +function FeatureItem({ + title, + description, +}: { + title: string; + description: string; +}) { + return ( +
  • +

    {title}

    +

    {description}

    +
  • + ); +} diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx b/apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx new file mode 100644 index 0000000..8bea78d --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx @@ -0,0 +1,12 @@ +import { getRouterManifest } from "@tanstack/react-start/router-manifest"; +import { + createStartHandler, + defaultStreamHandler, +} from "@tanstack/react-start/server"; + +import { createRouter } from "./router"; + +export default createStartHandler({ + createRouter, + getRouterManifest, +})(defaultStreamHandler); diff --git a/apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts b/apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts new file mode 100644 index 0000000..307a96b --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/src/utils/trpc.ts @@ -0,0 +1,5 @@ +import { createTRPCContext } from "@trpc/tanstack-react-query"; +import type { AppRouter } from "../../../server/src/routers"; + +export const { TRPCProvider, useTRPC, useTRPCClient } = + createTRPCContext(); diff --git a/apps/cli/template/base/apps/web-tanstack-start/tsconfig.json b/apps/cli/template/base/apps/web-tanstack-start/tsconfig.json new file mode 100644 index 0000000..fbb99a2 --- /dev/null +++ b/apps/cli/template/base/apps/web-tanstack-start/tsconfig.json @@ -0,0 +1,28 @@ +{ + "include": ["**/*.ts", "**/*.tsx"], + "compilerOptions": { + "target": "ES2022", + "jsx": "react-jsx", + "module": "ESNext", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "types": ["vite/client"], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + /* Linting */ + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/apps/cli/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx b/apps/cli/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx new file mode 100644 index 0000000..58418b0 --- /dev/null +++ b/apps/cli/template/examples/ai/apps/web-tanstack-start/src/routes/ai.tsx @@ -0,0 +1,69 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useChat } from "@ai-sdk/react"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Send } from "lucide-react"; +import { useRef, useEffect } from "react"; + +export const Route = createFileRoute("/ai")({ + component: RouteComponent, +}); + +function RouteComponent() { + const { messages, input, handleInputChange, handleSubmit } = useChat({ + api: `${import.meta.env.VITE_SERVER_URL}/ai`, + }); + + const messagesEndRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + return ( +
    +
    + {messages.length === 0 ? ( +
    + Ask me anything to get started! +
    + ) : ( + messages.map((message) => ( +
    +

    + {message.role === "user" ? "You" : "AI Assistant"} +

    +
    {message.content}
    +
    + )) + )} +
    +
    + +
    + + +
    +
    + ); +} diff --git a/apps/cli/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx b/apps/cli/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx new file mode 100644 index 0000000..a9c3414 --- /dev/null +++ b/apps/cli/template/examples/todo/apps/web-tanstack-start/src/routes/todos.tsx @@ -0,0 +1,135 @@ +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; +import { useTRPC } from "@/utils/trpc"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { Loader2, Trash2 } from "lucide-react"; +import { useState } from "react"; + +export const Route = createFileRoute("/todos")({ + component: TodosRoute, +}); + +function TodosRoute() { + const trpc = useTRPC(); + + const [newTodoText, setNewTodoText] = useState(""); + + const todos = useQuery(trpc.todo.getAll.queryOptions()); + const createMutation = useMutation( + trpc.todo.create.mutationOptions({ + onSuccess: () => { + todos.refetch(); + setNewTodoText(""); + }, + }), + ); + const toggleMutation = useMutation( + trpc.todo.toggle.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + const deleteMutation = useMutation( + trpc.todo.delete.mutationOptions({ + onSuccess: () => todos.refetch(), + }), + ); + + const handleAddTodo = (e: React.FormEvent) => { + e.preventDefault(); + if (newTodoText.trim()) { + createMutation.mutate({ text: newTodoText }); + } + }; + + const handleToggleTodo = (id: number, completed: boolean) => { + toggleMutation.mutate({ id, completed: !completed }); + }; + + const handleDeleteTodo = (id: number) => { + deleteMutation.mutate({ id }); + }; + + return ( +
    + + + Todo List + Manage your tasks efficiently + + +
    + setNewTodoText(e.target.value)} + placeholder="Add a new task..." + disabled={createMutation.isPending} + /> + +
    + + {todos.isLoading ? ( +
    + +
    + ) : todos.data?.length === 0 ? ( +

    No todos yet. Add one above!

    + ) : ( +
      + {todos.data?.map((todo) => ( +
    • +
      + + handleToggleTodo(todo.id, todo.completed) + } + id={`todo-${todo.id}`} + /> + +
      + +
    • + ))} +
    + )} +
    +
    +
    + ); +} diff --git a/apps/cli/template/with-auth/apps/web-base/src/utils/trpc.ts b/apps/cli/template/with-auth/apps/web-react-router/src/utils/trpc.ts similarity index 100% rename from apps/cli/template/with-auth/apps/web-base/src/utils/trpc.ts rename to apps/cli/template/with-auth/apps/web-react-router/src/utils/trpc.ts diff --git a/apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts b/apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts new file mode 100644 index 0000000..986941c --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-router/src/utils/trpc.ts @@ -0,0 +1,39 @@ +import type { AppRouter } from "../../../server/src/routers"; +import { QueryCache, QueryClient } from "@tanstack/react-query"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import { toast } from "sonner"; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + toast.error(error.message, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), +}); + +export const trpcClient = createTRPCClient({ + links: [ + httpBatchLink({ + url: `${import.meta.env.VITE_SERVER_URL}/trpc`, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }), + ], +}); + +export const trpc = createTRPCOptionsProxy({ + client: trpcClient, + queryClient, +}); diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx new file mode 100644 index 0000000..b30ea1b --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/header.tsx @@ -0,0 +1,32 @@ +import { Link } from "@tanstack/react-router"; +import UserMenu from "./user-menu"; + +export default function Header() { + const links = [ + { to: "/", label: "Home" }, + { to: "/dashboard", label: "Dashboard" }, + ]; + + return ( +
    +
    + +
    + +
    +
    +
    +
    + ); +} diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx new file mode 100644 index 0000000..2cfe9fa --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-in-form.tsx @@ -0,0 +1,139 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { z } from "zod"; +import Loader from "./loader"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignInForm({ + onSwitchToSignUp, +}: { + onSwitchToSignUp: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + const { isPending } = authClient.useSession(); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + }, + onSubmit: async ({ value }) => { + await authClient.signIn.email( + { + email: value.email, + password: value.password, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + toast.success("Sign in successful"); + }, + onError: (error) => { + toast.error(error.error.message); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), + }), + }, + }); + + if (isPending) { + return ; + } + + return ( +
    +

    Welcome Back

    + +
    { + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + className="space-y-4" + > +
    + + {(field) => ( +
    + + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

    + {error?.message} +

    + ))} +
    + )} +
    +
    + +
    + + {(field) => ( +
    + + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

    + {error?.message} +

    + ))} +
    + )} +
    +
    + + + {(state) => ( + + )} + +
    + +
    + +
    +
    + ); +} diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx new file mode 100644 index 0000000..1d323d0 --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/sign-up-form.tsx @@ -0,0 +1,164 @@ +import { authClient } from "@/lib/auth-client"; +import { useForm } from "@tanstack/react-form"; +import { useNavigate } from "@tanstack/react-router"; +import { toast } from "sonner"; +import { z } from "zod"; +import Loader from "./loader"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; + +export default function SignUpForm({ + onSwitchToSignIn, +}: { + onSwitchToSignIn: () => void; +}) { + const navigate = useNavigate({ + from: "/", + }); + const { isPending } = authClient.useSession(); + + const form = useForm({ + defaultValues: { + email: "", + password: "", + name: "", + }, + onSubmit: async ({ value }) => { + await authClient.signUp.email( + { + email: value.email, + password: value.password, + name: value.name, + }, + { + onSuccess: () => { + navigate({ + to: "/dashboard", + }); + toast.success("Sign up successful"); + }, + onError: (error) => { + toast.error(error.error.message); + }, + }, + ); + }, + validators: { + onSubmit: z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), + }), + }, + }); + + if (isPending) { + return ; + } + + return ( +
    +

    Create Account

    + +
    { + e.preventDefault(); + e.stopPropagation(); + void form.handleSubmit(); + }} + className="space-y-4" + > +
    + + {(field) => ( +
    + + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

    + {error?.message} +

    + ))} +
    + )} +
    +
    + +
    + + {(field) => ( +
    + + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

    + {error?.message} +

    + ))} +
    + )} +
    +
    + +
    + + {(field) => ( +
    + + field.handleChange(e.target.value)} + /> + {field.state.meta.errors.map((error) => ( +

    + {error?.message} +

    + ))} +
    + )} +
    +
    + + + {(state) => ( + + )} + +
    + +
    + +
    +
    + ); +} diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx new file mode 100644 index 0000000..3f92e13 --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-start/src/components/user-menu.tsx @@ -0,0 +1,62 @@ +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { authClient } from "@/lib/auth-client"; +import { useNavigate } from "@tanstack/react-router"; +import { Button } from "./ui/button"; +import { Skeleton } from "./ui/skeleton"; +import { Link } from "@tanstack/react-router"; + +export default function UserMenu() { + const navigate = useNavigate(); + const { data: session, isPending } = authClient.useSession(); + + if (isPending) { + return ; + } + + if (!session) { + return ( + + ); + } + + return ( + + + + + + My Account + + {session.user.email} + + + + + + ); +} diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/router.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/router.tsx new file mode 100644 index 0000000..50d7d71 --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-start/src/router.tsx @@ -0,0 +1,76 @@ +import { + QueryCache, + QueryClient, + QueryClientProvider, +} from "@tanstack/react-query"; +import { createRouter as createTanstackRouter } from "@tanstack/react-router"; +import { createTRPCClient, httpBatchLink } from "@trpc/client"; +import { createTRPCOptionsProxy } from "@trpc/tanstack-react-query"; +import { toast } from "sonner"; +import type { AppRouter } from "../../server/src/routers"; +import Loader from "./components/loader"; +import "./index.css"; +import { routeTree } from "./routeTree.gen"; +import { TRPCProvider } from "./utils/trpc"; + +export const queryClient = new QueryClient({ + queryCache: new QueryCache({ + onError: (error) => { + toast.error(error.message, { + action: { + label: "retry", + onClick: () => { + queryClient.invalidateQueries(); + }, + }, + }); + }, + }), + defaultOptions: { queries: { staleTime: 60 * 1000 } }, +}); + +const trpcClient = createTRPCClient({ + links: [ + httpBatchLink({ + url: `${import.meta.env.VITE_SERVER_URL}/trpc`, + fetch(url, options) { + return fetch(url, { + ...options, + credentials: "include", + }); + }, + }), + ], +}); + +const trpc = createTRPCOptionsProxy({ + client: trpcClient, + queryClient: queryClient, +}); + +export const createRouter = () => { + const router = createTanstackRouter({ + routeTree, + scrollRestoration: true, + defaultPreloadStaleTime: 0, + context: { trpc, queryClient }, + defaultPendingComponent: () => , + defaultNotFoundComponent: () =>
    Not Found
    , + Wrap: ({ children }) => ( + + + {children} + + + ), + }); + + return router; +}; + +// Register the router instance for type safety +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx new file mode 100644 index 0000000..0c7d689 --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/dashboard.tsx @@ -0,0 +1,37 @@ +import { authClient } from "@/lib/auth-client"; +import { useTRPC } from "@/utils/trpc"; +import { useQuery } from "@tanstack/react-query"; +import { createFileRoute } from "@tanstack/react-router"; +import { useEffect } from "react"; + +export const Route = createFileRoute("/dashboard")({ + component: RouteComponent, +}); + +function RouteComponent() { + const navigate = Route.useNavigate(); + const trpc = useTRPC(); + const { data: session, isPending } = authClient.useSession(); + + const privateData = useQuery(trpc.privateData.queryOptions()); + + useEffect(() => { + if (!session && !isPending) { + navigate({ + to: "/login", + }); + } + }, [session, isPending]); + + if (isPending) { + return
    Loading...
    ; + } + + return ( +
    +

    Dashboard

    +

    Welcome {session?.user.name}

    +

    privateData: {privateData.data?.message}

    +
    + ); +} diff --git a/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx b/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx new file mode 100644 index 0000000..81dc6a0 --- /dev/null +++ b/apps/cli/template/with-auth/apps/web-tanstack-start/src/routes/login.tsx @@ -0,0 +1,18 @@ +import SignInForm from "@/components/sign-in-form"; +import SignUpForm from "@/components/sign-up-form"; +import { createFileRoute } from "@tanstack/react-router"; +import { useState } from "react"; + +export const Route = createFileRoute("/login")({ + component: RouteComponent, +}); + +function RouteComponent() { + const [showSignIn, setShowSignIn] = useState(false); + + return showSignIn ? ( + setShowSignIn(false)} /> + ) : ( + setShowSignIn(true)} /> + ); +} diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx index 0c66b3e..85e4a4d 100644 --- a/apps/web/src/app/(home)/_components/StackArchitech.tsx +++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx @@ -108,7 +108,8 @@ const StackArchitect = () => { const notes: Record = {}; const hasWebFrontend = stack.frontend.includes("tanstack-router") || - stack.frontend.includes("react-router"); + stack.frontend.includes("react-router") || + stack.frontend.includes("tanstack-start"); notes.frontend = []; @@ -241,7 +242,11 @@ const StackArchitect = () => { setStack((prev) => { if (category === "frontend") { const currentSelection = [...prev.frontend]; - const webTypes = ["tanstack-router", "react-router"]; + const webTypes = [ + "tanstack-router", + "react-router", + "tanstack-start", + ]; if (techId === "none") { return { @@ -288,7 +293,8 @@ const StackArchitect = () => { const index = currentArray.indexOf(techId); const hasWebFrontend = prev.frontend.includes("tanstack-router") || - prev.frontend.includes("react-router"); + prev.frontend.includes("react-router") || + prev.frontend.includes("tanstack-start"); if (index >= 0) { currentArray.splice(index, 1); @@ -312,7 +318,8 @@ const StackArchitect = () => { if ( category === "addons" && (techId === "pwa" || techId === "tauri") && - !hasWebFrontend + !prev.frontend.includes("tanstack-router") && + !prev.frontend.includes("react-router") ) { return prev; } @@ -437,6 +444,7 @@ const StackArchitect = () => { return ( frontendOptions.includes("tanstack-router") || frontendOptions.includes("react-router") || + frontendOptions.includes("tanstack-start") || frontendOptions.includes("native") ); }, []); @@ -680,6 +688,11 @@ const StackArchitect = () => { } const hasWebFrontendSelected = + stack.frontend.includes("tanstack-router") || + stack.frontend.includes("react-router") || + stack.frontend.includes("tanstack-start"); + + const hasPWACompatibleFrontend = stack.frontend.includes("tanstack-router") || stack.frontend.includes("react-router"); @@ -698,7 +711,7 @@ const StackArchitect = () => { stack.backendFramework === "elysia"))) || (activeTab === "addons" && (tech.id === "pwa" || tech.id === "tauri") && - !hasWebFrontendSelected) || + !hasPWACompatibleFrontend) || (activeTab === "auth" && tech.id === "true" && stack.database === "none"); diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts index 8109acb..280b742 100644 --- a/apps/web/src/lib/constant.ts +++ b/apps/web/src/lib/constant.ts @@ -16,6 +16,14 @@ export const TECH_OPTIONS = { color: "from-cyan-400 to-cyan-600", default: false, }, + { + id: "tanstack-start", + name: "TanStack Start", + description: "Quick starter template from TanStack", + icon: "🚀", + color: "from-purple-400 to-purple-600", + default: false, + }, { id: "native", name: "React Native",