mirror of
https://github.com/FranP-code/create-better-t-stack.git
synced 2025-10-12 23:52:15 +00:00
add tanstack start
This commit is contained in:
5
.changeset/shiny-bees-sin.md
Normal file
5
.changeset/shiny-bees-sin.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"create-better-t-stack": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add tanstack start
|
||||||
@@ -49,7 +49,7 @@ Options:
|
|||||||
--orm <type> ORM type (none, drizzle, prisma)
|
--orm <type> ORM type (none, drizzle, prisma)
|
||||||
--auth Include authentication
|
--auth Include authentication
|
||||||
--no-auth Exclude authentication
|
--no-auth Exclude authentication
|
||||||
--frontend <types...> Frontend types (web, native, none)
|
--frontend <types...> Frontend types (tanstack-router, react-router, tanstack-start, native, none)
|
||||||
--addons <types...> Additional addons (pwa, tauri, biome, husky, none)
|
--addons <types...> Additional addons (pwa, tauri, biome, husky, none)
|
||||||
--examples <types...> Examples to include (todo, ai)
|
--examples <types...> Examples to include (todo, ai)
|
||||||
--no-examples Skip all examples
|
--no-examples Skip all examples
|
||||||
@@ -60,6 +60,8 @@ Options:
|
|||||||
--no-install Skip installing dependencies
|
--no-install Skip installing dependencies
|
||||||
--turso Set up Turso for SQLite database
|
--turso Set up Turso for SQLite database
|
||||||
--no-turso Skip Turso setup
|
--no-turso Skip Turso setup
|
||||||
|
--prisma-postgres Set up Prisma Postgres
|
||||||
|
--no-prisma-postgres Skip Prisma Postgres setup
|
||||||
--backend <framework> Backend framework (hono, elysia)
|
--backend <framework> Backend framework (hono, elysia)
|
||||||
--runtime <runtime> Runtime (bun, node)
|
--runtime <runtime> Runtime (bun, node)
|
||||||
-h, --help Display help
|
-h, --help Display help
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ export async function setupAddons(
|
|||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
frontends.includes("react-router") || frontends.includes("tanstack-router");
|
frontends.includes("react-router") || frontends.includes("tanstack-router");
|
||||||
|
|
||||||
// if (addons.includes("docker")) {
|
|
||||||
// await setupDocker(projectDir);
|
|
||||||
// }
|
|
||||||
if (addons.includes("pwa") && hasWebFrontend) {
|
if (addons.includes("pwa") && hasWebFrontend) {
|
||||||
await setupPwa(projectDir, frontends);
|
await setupPwa(projectDir, frontends);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export async function setupAuth(
|
|||||||
try {
|
try {
|
||||||
if (
|
if (
|
||||||
frontends.includes("react-router") ||
|
frontends.includes("react-router") ||
|
||||||
frontends.includes("tanstack-router")
|
frontends.includes("tanstack-router") ||
|
||||||
|
frontends.includes("tanstack-start")
|
||||||
) {
|
) {
|
||||||
addPackageDependency({
|
addPackageDependency({
|
||||||
dependencies: ["better-auth"],
|
dependencies: ["better-auth"],
|
||||||
|
|||||||
@@ -19,12 +19,13 @@ export async function setupEnvironmentVariables(
|
|||||||
if (!envContent.includes("CORS_ORIGIN")) {
|
if (!envContent.includes("CORS_ORIGIN")) {
|
||||||
const hasReactRouter = options.frontend.includes("react-router");
|
const hasReactRouter = options.frontend.includes("react-router");
|
||||||
const hasTanStackRouter = options.frontend.includes("tanstack-router");
|
const hasTanStackRouter = options.frontend.includes("tanstack-router");
|
||||||
|
const hasTanStackStart = options.frontend.includes("tanstack-start");
|
||||||
|
|
||||||
let corsOrigin = "http://localhost:3000";
|
let corsOrigin = "http://localhost:3000";
|
||||||
|
|
||||||
if (hasReactRouter) {
|
if (hasReactRouter) {
|
||||||
corsOrigin = "http://localhost:5173";
|
corsOrigin = "http://localhost:5173";
|
||||||
} else if (hasTanStackRouter) {
|
} else if (hasTanStackRouter || hasTanStackStart) {
|
||||||
corsOrigin = "http://localhost:3001";
|
corsOrigin = "http://localhost:3001";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +69,12 @@ export async function setupEnvironmentVariables(
|
|||||||
|
|
||||||
const hasReactRouter = options.frontend.includes("react-router");
|
const hasReactRouter = options.frontend.includes("react-router");
|
||||||
const hasTanStackRouter = options.frontend.includes("tanstack-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");
|
const clientDir = path.join(projectDir, "apps/web");
|
||||||
|
|
||||||
await setupClientEnvFile(clientDir);
|
await setupClientEnvFile(clientDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,12 +13,19 @@ export async function setupExamples(
|
|||||||
frontend: ProjectFrontend[] = ["tanstack-router"],
|
frontend: ProjectFrontend[] = ["tanstack-router"],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const hasTanstackRouter = frontend.includes("tanstack-router");
|
const hasTanstackRouter = frontend.includes("tanstack-router");
|
||||||
|
const hasTanstackStart = frontend.includes("tanstack-start");
|
||||||
const hasReactRouter = frontend.includes("react-router");
|
const hasReactRouter = frontend.includes("react-router");
|
||||||
const hasWebFrontend = hasTanstackRouter || hasReactRouter;
|
const hasWebFrontend =
|
||||||
|
hasTanstackRouter || hasReactRouter || hasTanstackStart;
|
||||||
|
|
||||||
const routerType = hasTanstackRouter
|
let routerType: string;
|
||||||
? "web-tanstack-router"
|
if (hasTanstackRouter) {
|
||||||
: "web-react-router";
|
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"));
|
const webAppExists = await fs.pathExists(path.join(projectDir, "apps/web"));
|
||||||
|
|
||||||
@@ -87,27 +94,28 @@ async function updateServerIndexWithAIRoute(projectDir: string): Promise<void> {
|
|||||||
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`;
|
const importSection = `import { streamText } from "ai";\nimport { google } from "@ai-sdk/google";\nimport { stream } from "hono/streaming";`;
|
||||||
|
|
||||||
const aiRouteHandler = `
|
const aiRouteHandler = `
|
||||||
app.post("/ai", async (c) => {
|
// AI chat endpoint
|
||||||
const body = await c.req.json();
|
app.post("/ai", async (c) => {
|
||||||
const messages = body.messages || [];
|
const body = await c.req.json();
|
||||||
|
const messages = body.messages || [];
|
||||||
|
|
||||||
const result = streamText({
|
const result = streamText({
|
||||||
model: google("gemini-2.0-flash-exp"),
|
model: google("gemini-1.5-flash"),
|
||||||
messages,
|
messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
c.header("X-Vercel-AI-Data-Stream", "v1");
|
c.header("X-Vercel-AI-Data-Stream", "v1");
|
||||||
c.header("Content-Type", "text/plain; charset=utf-8");
|
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 {")) {
|
if (indexContent.includes("import {")) {
|
||||||
const lastImportIndex = indexContent.lastIndexOf("import");
|
const lastImportIndex = indexContent.lastIndexOf("import");
|
||||||
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
|
const endOfLastImport = indexContent.indexOf("\n", lastImportIndex);
|
||||||
indexContent = `${indexContent.substring(0, endOfLastImport + 1)}
|
indexContent = `${indexContent.substring(0, endOfLastImport + 1)}
|
||||||
${importSection}
|
${importSection}
|
||||||
${indexContent.substring(endOfLastImport + 1)}`;
|
${indexContent.substring(endOfLastImport + 1)}`;
|
||||||
} else {
|
} else {
|
||||||
indexContent = `${importSection}
|
indexContent = `${importSection}
|
||||||
|
|
||||||
@@ -177,7 +185,7 @@ async function setupTodoExample(
|
|||||||
const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo");
|
const todoExampleDir = path.join(PKG_ROOT, "template/examples/todo");
|
||||||
|
|
||||||
if (await fs.pathExists(todoExampleDir)) {
|
if (await fs.pathExists(todoExampleDir)) {
|
||||||
const todoRouteSourceDir = path.join(
|
const todoRouteSourcePath = path.join(
|
||||||
todoExampleDir,
|
todoExampleDir,
|
||||||
`apps/${routerType}/src/routes/todos.tsx`,
|
`apps/${routerType}/src/routes/todos.tsx`,
|
||||||
);
|
);
|
||||||
@@ -186,8 +194,8 @@ async function setupTodoExample(
|
|||||||
"apps/web/src/routes/todos.tsx",
|
"apps/web/src/routes/todos.tsx",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (await fs.pathExists(todoRouteSourceDir)) {
|
if (await fs.pathExists(todoRouteSourcePath)) {
|
||||||
await fs.copy(todoRouteSourceDir, todoRouteTargetPath, {
|
await fs.copy(todoRouteSourcePath, todoRouteTargetPath, {
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,11 +43,15 @@ export function displayPostInstallInstructions(
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const hasTanstackRouter = frontends?.includes("tanstack-router");
|
const hasTanstackRouter = frontends?.includes("tanstack-router");
|
||||||
|
const hasTanstackStart = frontends?.includes("tanstack-start");
|
||||||
const hasReactRouter = frontends?.includes("react-router");
|
const hasReactRouter = frontends?.includes("react-router");
|
||||||
const hasWebFrontend = hasTanstackRouter || hasReactRouter;
|
const hasWebFrontend =
|
||||||
|
hasTanstackRouter || hasReactRouter || hasTanstackStart;
|
||||||
const hasNativeFrontend = frontends?.includes("native");
|
const hasNativeFrontend = frontends?.includes("native");
|
||||||
const hasFrontend = hasWebFrontend || hasNativeFrontend;
|
const hasFrontend = hasWebFrontend || hasNativeFrontend;
|
||||||
|
|
||||||
|
const webPort = hasReactRouter ? "5173" : "3001";
|
||||||
|
|
||||||
note(
|
note(
|
||||||
`${pc.cyan("1.")} ${cdCmd}
|
`${pc.cyan("1.")} ${cdCmd}
|
||||||
${!depsInstalled ? `${pc.cyan("2.")} ${packageManager} install\n` : ""}${pc.cyan(depsInstalled ? "2." : "3.")} ${runCmd} dev
|
${!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:")}
|
${pc.bold("Your project will be available at:")}
|
||||||
${
|
${
|
||||||
hasFrontend
|
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.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`
|
||||||
}${pc.cyan("•")} API: http://localhost:3000
|
}${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()}` : ""}
|
${nativeInstructions ? `\n${nativeInstructions.trim()}` : ""}${databaseInstructions ? `\n${databaseInstructions.trim()}` : ""}${tauriInstructions ? `\n${tauriInstructions.trim()}` : ""}${lintingInstructions ? `\n${lintingInstructions.trim()}` : ""}${pwaInstructions ? `\n${pwaInstructions.trim()}` : ""}
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ export async function setupFrontendTemplates(
|
|||||||
frontends: ProjectFrontend[],
|
frontends: ProjectFrontend[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const hasTanstackWeb = frontends.includes("tanstack-router");
|
const hasTanstackWeb = frontends.includes("tanstack-router");
|
||||||
|
const hasTanstackStart = frontends.includes("tanstack-start");
|
||||||
const hasReactRouterWeb = frontends.includes("react-router");
|
const hasReactRouterWeb = frontends.includes("react-router");
|
||||||
const hasNative = frontends.includes("native");
|
const hasNative = frontends.includes("native");
|
||||||
|
|
||||||
if (hasTanstackWeb || hasReactRouterWeb) {
|
if (hasTanstackWeb || hasReactRouterWeb || hasTanstackStart) {
|
||||||
const webDir = path.join(projectDir, "apps/web");
|
const webDir = path.join(projectDir, "apps/web");
|
||||||
await fs.ensureDir(webDir);
|
await fs.ensureDir(webDir);
|
||||||
|
|
||||||
@@ -61,9 +62,13 @@ export async function setupFrontendTemplates(
|
|||||||
await fs.copy(webBaseDir, webDir);
|
await fs.copy(webBaseDir, webDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const frameworkName = hasTanstackWeb
|
let frameworkName = "web-react-router";
|
||||||
? "web-tanstack-router"
|
if (hasTanstackWeb) {
|
||||||
: "web-react-router";
|
frameworkName = "web-tanstack-router";
|
||||||
|
} else if (hasTanstackStart) {
|
||||||
|
frameworkName = "web-tanstack-start";
|
||||||
|
}
|
||||||
|
|
||||||
const webFrameworkDir = path.join(
|
const webFrameworkDir = path.join(
|
||||||
PKG_ROOT,
|
PKG_ROOT,
|
||||||
`template/base/apps/${frameworkName}`,
|
`template/base/apps/${frameworkName}`,
|
||||||
@@ -155,8 +160,9 @@ export async function setupAuthTemplate(
|
|||||||
if (await fs.pathExists(authTemplateDir)) {
|
if (await fs.pathExists(authTemplateDir)) {
|
||||||
const hasReactRouter = frontends.includes("react-router");
|
const hasReactRouter = frontends.includes("react-router");
|
||||||
const hasTanStackRouter = frontends.includes("tanstack-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 webDir = path.join(projectDir, "apps/web");
|
||||||
|
|
||||||
const webBaseAuthDir = path.join(authTemplateDir, "apps/web-base");
|
const webBaseAuthDir = path.join(authTemplateDir, "apps/web-base");
|
||||||
@@ -183,6 +189,16 @@ export async function setupAuthTemplate(
|
|||||||
await fs.copy(tanstackAuthDir, webDir, { overwrite: true });
|
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");
|
const serverAuthDir = path.join(authTemplateDir, "apps/server/src");
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ async function main() {
|
|||||||
.option("--no-auth", "Exclude authentication")
|
.option("--no-auth", "Exclude authentication")
|
||||||
.option(
|
.option(
|
||||||
"--frontend <types...>",
|
"--frontend <types...>",
|
||||||
"Frontend types (tanstack-router, react-router, native, none)",
|
"Frontend types (tanstack-router, react-router, tanstack-start, native, none)",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--addons <types...>",
|
"--addons <types...>",
|
||||||
@@ -271,7 +271,7 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
if (
|
if (
|
||||||
options.frontend &&
|
options.frontend &&
|
||||||
!options.frontend.some((f) =>
|
!options.frontend.some((f) =>
|
||||||
["tanstack-router", "react-router"].includes(f),
|
["tanstack-router", "react-router", "tanstack-start"].includes(f),
|
||||||
) &&
|
) &&
|
||||||
!options.frontend.includes("none")
|
!options.frontend.includes("none")
|
||||||
) {
|
) {
|
||||||
@@ -288,6 +288,7 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
const validFrontends = [
|
const validFrontends = [
|
||||||
"tanstack-router",
|
"tanstack-router",
|
||||||
"react-router",
|
"react-router",
|
||||||
|
"tanstack-start",
|
||||||
"native",
|
"native",
|
||||||
"none",
|
"none",
|
||||||
];
|
];
|
||||||
@@ -305,13 +306,16 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const webFrontends = options.frontend.filter(
|
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) {
|
if (webFrontends.length > 1) {
|
||||||
cancel(
|
cancel(
|
||||||
pc.red(
|
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);
|
process.exit(1);
|
||||||
@@ -357,14 +361,13 @@ function validateOptions(options: CLIOptions): void {
|
|||||||
options.frontend &&
|
options.frontend &&
|
||||||
!options.frontend.some((f) =>
|
!options.frontend.some((f) =>
|
||||||
["tanstack-router", "react-router"].includes(f),
|
["tanstack-router", "react-router"].includes(f),
|
||||||
) &&
|
)
|
||||||
!options.frontend.includes("none")
|
|
||||||
) {
|
) {
|
||||||
cancel(
|
cancel(
|
||||||
pc.red(
|
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))
|
.filter((a) => webSpecificAddons.includes(a))
|
||||||
.join(", ")} with --frontend native only`,
|
.join(", ")} with incompatible frontend options.`,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -384,11 +387,17 @@ function processFlags(
|
|||||||
} else {
|
} else {
|
||||||
frontend = options.frontend.filter(
|
frontend = options.frontend.filter(
|
||||||
(f): f is ProjectFrontend =>
|
(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(
|
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) {
|
if (webFrontends.length > 1) {
|
||||||
@@ -448,8 +457,9 @@ function processFlags(
|
|||||||
if (
|
if (
|
||||||
frontend &&
|
frontend &&
|
||||||
frontend.length > 0 &&
|
frontend.length > 0 &&
|
||||||
!frontend.includes("tanstack-router") &&
|
!frontend.some((f) =>
|
||||||
!frontend.includes("react-router")
|
["tanstack-router", "react-router", "tanstack-start"].includes(f),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
examples = [];
|
examples = [];
|
||||||
log.warn(
|
log.warn(
|
||||||
@@ -481,19 +491,23 @@ function processFlags(
|
|||||||
addon === "husky",
|
addon === "husky",
|
||||||
);
|
);
|
||||||
|
|
||||||
if (
|
const hasCompatibleWebFrontend = frontend?.some(
|
||||||
frontend &&
|
(f) => f === "tanstack-router" || f === "react-router",
|
||||||
frontend.length > 0 &&
|
);
|
||||||
!frontend.includes("tanstack-router") &&
|
|
||||||
!frontend.includes("react-router")
|
if (!hasCompatibleWebFrontend) {
|
||||||
) {
|
const webSpecificAddons = ["pwa", "tauri"];
|
||||||
addons = addons.filter((addon) => !["pwa", "tauri"].includes(addon));
|
const filteredAddons = addons.filter(
|
||||||
if (addons.length !== options.addons.length) {
|
(addon) => !webSpecificAddons.includes(addon),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (filteredAddons.length !== addons.length) {
|
||||||
log.warn(
|
log.warn(
|
||||||
pc.yellow(
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export async function getAddonsChoice(
|
|||||||
): Promise<ProjectAddons[]> {
|
): Promise<ProjectAddons[]> {
|
||||||
if (Addons !== undefined) return Addons;
|
if (Addons !== undefined) return Addons;
|
||||||
|
|
||||||
const hasWeb =
|
const hasCompatibleWebFrontend =
|
||||||
frontends?.includes("react-router") ||
|
frontends?.includes("react-router") ||
|
||||||
frontends?.includes("tanstack-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(
|
const initialValues = DEFAULT_CONFIG.addons.filter(
|
||||||
(addon) => hasWeb || (addon !== "pwa" && addon !== "tauri"),
|
(addon) =>
|
||||||
|
hasCompatibleWebFrontend || (addon !== "pwa" && addon !== "tauri"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await multiselect<ProjectAddons>({
|
const response = await multiselect<ProjectAddons>({
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ export async function getExamplesChoice(
|
|||||||
|
|
||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
frontends?.includes("react-router") ||
|
frontends?.includes("react-router") ||
|
||||||
frontends?.includes("tanstack-router");
|
frontends?.includes("tanstack-router") ||
|
||||||
|
frontends?.includes("tanstack-start");
|
||||||
|
|
||||||
if (!hasWebFrontend) return [];
|
if (!hasWebFrontend) return [];
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ export async function getFrontendChoice(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
initialValues: DEFAULT_CONFIG.frontend.some(
|
initialValues: DEFAULT_CONFIG.frontend.some(
|
||||||
(f) => f === "tanstack-router" || f === "react-router",
|
(f) =>
|
||||||
|
f === "tanstack-router" ||
|
||||||
|
f === "react-router" ||
|
||||||
|
f === "tanstack-start",
|
||||||
)
|
)
|
||||||
? ["web"]
|
? ["web"]
|
||||||
: [],
|
: [],
|
||||||
@@ -50,10 +53,18 @@ export async function getFrontendChoice(
|
|||||||
label: "React Router",
|
label: "React Router",
|
||||||
hint: "A user‑obsessed, standards‑focused, multi‑strategy router you can deploy anywhere.",
|
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:
|
initialValue:
|
||||||
DEFAULT_CONFIG.frontend.find(
|
DEFAULT_CONFIG.frontend.find(
|
||||||
(f) => f === "tanstack-router" || f === "react-router",
|
(f) =>
|
||||||
|
f === "tanstack-router" ||
|
||||||
|
f === "react-router" ||
|
||||||
|
f === "tanstack-start",
|
||||||
) || "tanstack-router",
|
) || "tanstack-router",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ export type ProjectAddons = "pwa" | "biome" | "tauri" | "husky";
|
|||||||
export type ProjectBackend = "hono" | "elysia";
|
export type ProjectBackend = "hono" | "elysia";
|
||||||
export type ProjectRuntime = "node" | "bun";
|
export type ProjectRuntime = "node" | "bun";
|
||||||
export type ProjectExamples = "todo" | "ai";
|
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 {
|
export interface ProjectConfig {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon">
|
||||||
|
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||||
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||||
|
<span className="sr-only">Toggle theme</span>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("light")}>
|
||||||
|
Light
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("dark")}>
|
||||||
|
Dark
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setTheme("system")}>
|
||||||
|
System
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<ThemeProviderState>(initialState);
|
||||||
|
|
||||||
|
export function ThemeProvider({
|
||||||
|
children,
|
||||||
|
defaultTheme = "system",
|
||||||
|
storageKey = "vite-ui-theme",
|
||||||
|
...props
|
||||||
|
}: ThemeProviderProps) {
|
||||||
|
const [theme, setTheme] = useState<Theme>(
|
||||||
|
() => (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 (
|
||||||
|
<ThemeProviderContext.Provider {...props} value={value}>
|
||||||
|
{children}
|
||||||
|
</ThemeProviderContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTheme = () => {
|
||||||
|
const context = useContext(ThemeProviderContext);
|
||||||
|
|
||||||
|
if (context === undefined)
|
||||||
|
throw new Error("useTheme must be used within a ThemeProvider");
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -50,8 +50,7 @@ function RootComponent() {
|
|||||||
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
|
||||||
<div className="grid grid-rows-[auto_1fr] h-svh">
|
<div className="grid grid-rows-[auto_1fr] h-svh">
|
||||||
<Header />
|
<Header />
|
||||||
{isFetching && <Loader />}
|
{isFetching ? <Loader /> : <Outlet />}
|
||||||
<Outlet />
|
|
||||||
</div>
|
</div>
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
17
apps/cli/template/base/apps/web-tanstack-start/app.config.ts
Normal file
17
apps/cli/template/base/apps/web-tanstack-start/app.config.ts
Normal file
@@ -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(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
52
apps/cli/template/base/apps/web-tanstack-start/package.json
Normal file
52
apps/cli/template/base/apps/web-tanstack-start/package.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import {
|
||||||
|
createStartAPIHandler,
|
||||||
|
defaultAPIFileRouteHandler,
|
||||||
|
} from "@tanstack/react-start/api";
|
||||||
|
|
||||||
|
export default createStartAPIHandler(defaultAPIFileRouteHandler);
|
||||||
@@ -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, <StartClient router={router} />);
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
const links = [
|
||||||
|
{ to: "/", label: "Home" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-row items-center justify-between px-2 py-1">
|
||||||
|
<nav className="flex gap-4 text-lg">
|
||||||
|
{links.map(({ to, label }) => (
|
||||||
|
<Link
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
activeProps={{ className: "font-bold" }}
|
||||||
|
activeOptions={{ exact: true }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
135
apps/cli/template/base/apps/web-tanstack-start/src/index.css
Normal file
135
apps/cli/template/base/apps/web-tanstack-start/src/index.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AppRouter>({
|
||||||
|
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: () => <Loader />,
|
||||||
|
defaultNotFoundComponent: () => <div>Not Found</div>,
|
||||||
|
Wrap: ({ children }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
||||||
|
{children}
|
||||||
|
</TRPCProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the router instance for type safety
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: ReturnType<typeof createRouter>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AppRouter>;
|
||||||
|
queryClient: QueryClient;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createRootRouteWithContext<RouterAppContext>()({
|
||||||
|
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 (
|
||||||
|
<html lang="en" className="dark">
|
||||||
|
<head>
|
||||||
|
<HeadContent />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div className="grid h-svh grid-rows-[auto_1fr]">
|
||||||
|
<Header />
|
||||||
|
{isFetching ? <Loader /> : <Outlet />}
|
||||||
|
</div>
|
||||||
|
<Toaster richColors />
|
||||||
|
<TanStackRouterDevtools position="bottom-left" />
|
||||||
|
<ReactQueryDevtools position="bottom" buttonPosition="bottom-right" />
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="container mx-auto max-w-3xl px-4 py-2">
|
||||||
|
<pre className="overflow-x-auto font-mono text-sm">{TITLE_TEXT}</pre>
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<section className="rounded-lg border p-4">
|
||||||
|
<h2 className="mb-2 font-medium">API Status</h2>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={`h-2 w-2 rounded-full ${healthCheck.data ? "bg-green-500" : "bg-red-500"}`}
|
||||||
|
/>
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{healthCheck.isLoading
|
||||||
|
? "Checking..."
|
||||||
|
: healthCheck.data
|
||||||
|
? "Connected"
|
||||||
|
: "Disconnected"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2 className="mb-3 font-medium">Core Features</h2>
|
||||||
|
<ul className="grid grid-cols-2 gap-3">
|
||||||
|
<FeatureItem
|
||||||
|
title="Type-Safe API"
|
||||||
|
description="End-to-end type safety with tRPC"
|
||||||
|
/>
|
||||||
|
<FeatureItem
|
||||||
|
title="Modern React"
|
||||||
|
description="TanStack Router + TanStack Query"
|
||||||
|
/>
|
||||||
|
<FeatureItem
|
||||||
|
title="Fast Backend"
|
||||||
|
description="Lightweight Hono server"
|
||||||
|
/>
|
||||||
|
<FeatureItem
|
||||||
|
title="Beautiful UI"
|
||||||
|
description="TailwindCSS + shadcn/ui components"
|
||||||
|
/>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FeatureItem({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<li className="border-primary border-l-2 py-1 pl-3">
|
||||||
|
<h3 className="font-medium">{title}</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">{description}</p>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx
Normal file
12
apps/cli/template/base/apps/web-tanstack-start/src/ssr.tsx
Normal file
@@ -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);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { createTRPCContext } from "@trpc/tanstack-react-query";
|
||||||
|
import type { AppRouter } from "../../../server/src/routers";
|
||||||
|
|
||||||
|
export const { TRPCProvider, useTRPC, useTRPCClient } =
|
||||||
|
createTRPCContext<AppRouter>();
|
||||||
28
apps/cli/template/base/apps/web-tanstack-start/tsconfig.json
Normal file
28
apps/cli/template/base/apps/web-tanstack-start/tsconfig.json
Normal file
@@ -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/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
}, [messages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-rows-[1fr_auto] overflow-hidden w-full mx-auto p-4">
|
||||||
|
<div className="overflow-y-auto space-y-4 pb-4">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground mt-8">
|
||||||
|
Ask me anything to get started!
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`p-3 rounded-lg ${
|
||||||
|
message.role === "user"
|
||||||
|
? "bg-primary/10 ml-8"
|
||||||
|
: "bg-secondary/20 mr-8"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-semibold mb-1">
|
||||||
|
{message.role === "user" ? "You" : "AI Assistant"}
|
||||||
|
</p>
|
||||||
|
<div className="whitespace-pre-wrap">{message.content}</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="w-full flex items-center space-x-2 pt-2 border-t"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
name="prompt"
|
||||||
|
value={input}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Type your message..."
|
||||||
|
className="flex-1"
|
||||||
|
autoComplete="off"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<Button type="submit" size="icon">
|
||||||
|
<Send size={18} />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="mx-auto w-full max-w-md py-10">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Todo List</CardTitle>
|
||||||
|
<CardDescription>Manage your tasks efficiently</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form
|
||||||
|
onSubmit={handleAddTodo}
|
||||||
|
className="mb-6 flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={newTodoText}
|
||||||
|
onChange={(e) => setNewTodoText(e.target.value)}
|
||||||
|
placeholder="Add a new task..."
|
||||||
|
disabled={createMutation.isPending}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={createMutation.isPending || !newTodoText.trim()}
|
||||||
|
>
|
||||||
|
{createMutation.isPending ? (
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
"Add"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{todos.isLoading ? (
|
||||||
|
<div className="flex justify-center py-4">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : todos.data?.length === 0 ? (
|
||||||
|
<p className="py-4 text-center">No todos yet. Add one above!</p>
|
||||||
|
) : (
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{todos.data?.map((todo) => (
|
||||||
|
<li
|
||||||
|
key={todo.id}
|
||||||
|
className="flex items-center justify-between rounded-md border p-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Checkbox
|
||||||
|
checked={todo.completed}
|
||||||
|
onCheckedChange={() =>
|
||||||
|
handleToggleTodo(todo.id, todo.completed)
|
||||||
|
}
|
||||||
|
id={`todo-${todo.id}`}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`todo-${todo.id}`}
|
||||||
|
className={`${todo.completed ? "line-through" : ""}`}
|
||||||
|
>
|
||||||
|
{todo.text}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => handleDeleteTodo(todo.id)}
|
||||||
|
aria-label="Delete todo"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<AppRouter>({
|
||||||
|
links: [
|
||||||
|
httpBatchLink({
|
||||||
|
url: `${import.meta.env.VITE_SERVER_URL}/trpc`,
|
||||||
|
fetch(url, options) {
|
||||||
|
return fetch(url, {
|
||||||
|
...options,
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
export const trpc = createTRPCOptionsProxy<AppRouter>({
|
||||||
|
client: trpcClient,
|
||||||
|
queryClient,
|
||||||
|
});
|
||||||
@@ -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 (
|
||||||
|
<div>
|
||||||
|
<div className="flex flex-row items-center justify-between px-2 py-1">
|
||||||
|
<nav className="flex gap-4 text-lg">
|
||||||
|
{links.map(({ to, label }) => (
|
||||||
|
<Link
|
||||||
|
key={to}
|
||||||
|
to={to}
|
||||||
|
activeProps={{ className: "font-bold" }}
|
||||||
|
activeOptions={{ exact: true }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<UserMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
||||||
|
<h1 className="mb-6 text-center text-3xl font-bold">Welcome Back</h1>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
void form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<form.Field name="email">
|
||||||
|
{(field) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={field.name}>Email</Label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type="email"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{field.state.meta.errors.map((error) => (
|
||||||
|
<p key={error?.message} className="text-red-500">
|
||||||
|
{error?.message}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={field.name}>Password</Label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{field.state.meta.errors.map((error) => (
|
||||||
|
<p key={error?.message} className="text-red-500">
|
||||||
|
{error?.message}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form.Subscribe>
|
||||||
|
{(state) => (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
disabled={!state.canSubmit || state.isSubmitting}
|
||||||
|
>
|
||||||
|
{state.isSubmitting ? "Submitting..." : "Sign In"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={onSwitchToSignUp}
|
||||||
|
className="text-indigo-600 hover:text-indigo-800"
|
||||||
|
>
|
||||||
|
Need an account? Sign Up
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 <Loader />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full mt-10 max-w-md p-6">
|
||||||
|
<h1 className="mb-6 text-center text-3xl font-bold">Create Account</h1>
|
||||||
|
|
||||||
|
<form
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
void form.handleSubmit();
|
||||||
|
}}
|
||||||
|
className="space-y-4"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<form.Field name="name">
|
||||||
|
{(field) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={field.name}>Name</Label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{field.state.meta.errors.map((error) => (
|
||||||
|
<p key={error?.message} className="text-red-500">
|
||||||
|
{error?.message}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form.Field name="email">
|
||||||
|
{(field) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={field.name}>Email</Label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type="email"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{field.state.meta.errors.map((error) => (
|
||||||
|
<p key={error?.message} className="text-red-500">
|
||||||
|
{error?.message}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<form.Field name="password">
|
||||||
|
{(field) => (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={field.name}>Password</Label>
|
||||||
|
<Input
|
||||||
|
id={field.name}
|
||||||
|
name={field.name}
|
||||||
|
type="password"
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
/>
|
||||||
|
{field.state.meta.errors.map((error) => (
|
||||||
|
<p key={error?.message} className="text-red-500">
|
||||||
|
{error?.message}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form.Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form.Subscribe>
|
||||||
|
{(state) => (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full"
|
||||||
|
disabled={!state.canSubmit || state.isSubmitting}
|
||||||
|
>
|
||||||
|
{state.isSubmitting ? "Submitting..." : "Sign Up"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</form.Subscribe>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={onSwitchToSignIn}
|
||||||
|
className="text-indigo-600 hover:text-indigo-800"
|
||||||
|
>
|
||||||
|
Already have an account? Sign In
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 <Skeleton className="h-9 w-24" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
return (
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<Link to="/login">Sign In</Link>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="outline">{session.user.name}</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="bg-card">
|
||||||
|
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>{session.user.email}</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem asChild>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
authClient.signOut({
|
||||||
|
fetchOptions: {
|
||||||
|
onSuccess: () => {
|
||||||
|
navigate({
|
||||||
|
to: "/",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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<AppRouter>({
|
||||||
|
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: () => <Loader />,
|
||||||
|
defaultNotFoundComponent: () => <div>Not Found</div>,
|
||||||
|
Wrap: ({ children }) => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
||||||
|
{children}
|
||||||
|
</TRPCProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return router;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the router instance for type safety
|
||||||
|
declare module "@tanstack/react-router" {
|
||||||
|
interface Register {
|
||||||
|
router: ReturnType<typeof createRouter>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Dashboard</h1>
|
||||||
|
<p>Welcome {session?.user.name}</p>
|
||||||
|
<p>privateData: {privateData.data?.message}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 ? (
|
||||||
|
<SignInForm onSwitchToSignUp={() => setShowSignIn(false)} />
|
||||||
|
) : (
|
||||||
|
<SignUpForm onSwitchToSignIn={() => setShowSignIn(true)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -108,7 +108,8 @@ const StackArchitect = () => {
|
|||||||
const notes: Record<string, string[]> = {};
|
const notes: Record<string, string[]> = {};
|
||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
stack.frontend.includes("tanstack-router") ||
|
stack.frontend.includes("tanstack-router") ||
|
||||||
stack.frontend.includes("react-router");
|
stack.frontend.includes("react-router") ||
|
||||||
|
stack.frontend.includes("tanstack-start");
|
||||||
|
|
||||||
notes.frontend = [];
|
notes.frontend = [];
|
||||||
|
|
||||||
@@ -241,7 +242,11 @@ const StackArchitect = () => {
|
|||||||
setStack((prev) => {
|
setStack((prev) => {
|
||||||
if (category === "frontend") {
|
if (category === "frontend") {
|
||||||
const currentSelection = [...prev.frontend];
|
const currentSelection = [...prev.frontend];
|
||||||
const webTypes = ["tanstack-router", "react-router"];
|
const webTypes = [
|
||||||
|
"tanstack-router",
|
||||||
|
"react-router",
|
||||||
|
"tanstack-start",
|
||||||
|
];
|
||||||
|
|
||||||
if (techId === "none") {
|
if (techId === "none") {
|
||||||
return {
|
return {
|
||||||
@@ -288,7 +293,8 @@ const StackArchitect = () => {
|
|||||||
const index = currentArray.indexOf(techId);
|
const index = currentArray.indexOf(techId);
|
||||||
const hasWebFrontend =
|
const hasWebFrontend =
|
||||||
prev.frontend.includes("tanstack-router") ||
|
prev.frontend.includes("tanstack-router") ||
|
||||||
prev.frontend.includes("react-router");
|
prev.frontend.includes("react-router") ||
|
||||||
|
prev.frontend.includes("tanstack-start");
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
currentArray.splice(index, 1);
|
currentArray.splice(index, 1);
|
||||||
@@ -312,7 +318,8 @@ const StackArchitect = () => {
|
|||||||
if (
|
if (
|
||||||
category === "addons" &&
|
category === "addons" &&
|
||||||
(techId === "pwa" || techId === "tauri") &&
|
(techId === "pwa" || techId === "tauri") &&
|
||||||
!hasWebFrontend
|
!prev.frontend.includes("tanstack-router") &&
|
||||||
|
!prev.frontend.includes("react-router")
|
||||||
) {
|
) {
|
||||||
return prev;
|
return prev;
|
||||||
}
|
}
|
||||||
@@ -437,6 +444,7 @@ const StackArchitect = () => {
|
|||||||
return (
|
return (
|
||||||
frontendOptions.includes("tanstack-router") ||
|
frontendOptions.includes("tanstack-router") ||
|
||||||
frontendOptions.includes("react-router") ||
|
frontendOptions.includes("react-router") ||
|
||||||
|
frontendOptions.includes("tanstack-start") ||
|
||||||
frontendOptions.includes("native")
|
frontendOptions.includes("native")
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -680,6 +688,11 @@ const StackArchitect = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasWebFrontendSelected =
|
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("tanstack-router") ||
|
||||||
stack.frontend.includes("react-router");
|
stack.frontend.includes("react-router");
|
||||||
|
|
||||||
@@ -698,7 +711,7 @@ const StackArchitect = () => {
|
|||||||
stack.backendFramework === "elysia"))) ||
|
stack.backendFramework === "elysia"))) ||
|
||||||
(activeTab === "addons" &&
|
(activeTab === "addons" &&
|
||||||
(tech.id === "pwa" || tech.id === "tauri") &&
|
(tech.id === "pwa" || tech.id === "tauri") &&
|
||||||
!hasWebFrontendSelected) ||
|
!hasPWACompatibleFrontend) ||
|
||||||
(activeTab === "auth" &&
|
(activeTab === "auth" &&
|
||||||
tech.id === "true" &&
|
tech.id === "true" &&
|
||||||
stack.database === "none");
|
stack.database === "none");
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ export const TECH_OPTIONS = {
|
|||||||
color: "from-cyan-400 to-cyan-600",
|
color: "from-cyan-400 to-cyan-600",
|
||||||
default: false,
|
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",
|
id: "native",
|
||||||
name: "React Native",
|
name: "React Native",
|
||||||
|
|||||||
Reference in New Issue
Block a user