Merge pull request #22 from stevedylandev/feat/mpa-template 4e43fa06
feat/mpa template
Steve Simkins · 2025-10-05 16:32 28 file(s) · +1439 −18
.github/workflows/test-cli-options.yml +183 −15
315 315
            linter: "biome"
316 316
            test_name: "Shadcn + No RPC + TanStack Query + React Router + Biome"
317 317
318 +
          # React Router MPA
319 +
          # Default template combinations
320 +
          - template: "default"
321 +
            rpc: true
322 +
            tanstackQuery: false
323 +
            router: "reactroutermpa"
324 +
            linter: "eslint"
325 +
            test_name: "Default + RPC + No TanStack Query + React Router MPA + ESLint"
326 +
          - template: "default"
327 +
            rpc: true
328 +
            tanstackQuery: false
329 +
            router: "reactroutermpa"
330 +
            linter: "biome"
331 +
            test_name: "Default + RPC + No TanStack Query + React Router MPA + Biome"
332 +
          - template: "default"
333 +
            rpc: false
334 +
            tanstackQuery: false
335 +
            router: "reactroutermpa"
336 +
            linter: "eslint"
337 +
            test_name: "Default + No RPC + No TanStack Query + React Router MPA + ESLint"
338 +
          - template: "default"
339 +
            rpc: false
340 +
            tanstackQuery: false
341 +
            router: "reactroutermpa"
342 +
            linter: "biome"
343 +
            test_name: "Default + No RPC + No TanStack Query + React Router MPA + Biome"
344 +
          - template: "default"
345 +
            rpc: true
346 +
            tanstackQuery: true
347 +
            router: "reactroutermpa"
348 +
            linter: "eslint"
349 +
            test_name: "Default + RPC + TanStack Query + React Router MPA + ESLint"
350 +
          - template: "default"
351 +
            rpc: true
352 +
            tanstackQuery: true
353 +
            router: "reactroutermpa"
354 +
            linter: "biome"
355 +
            test_name: "Default + RPC + TanStack Query + React Router MPA + Biome"
356 +
          - template: "default"
357 +
            rpc: false
358 +
            tanstackQuery: true
359 +
            router: "reactroutermpa"
360 +
            linter: "eslint"
361 +
            test_name: "Default + No RPC + TanStack Query + React Router MPA + ESLint"
362 +
          - template: "default"
363 +
            rpc: false
364 +
            tanstackQuery: true
365 +
            router: "reactroutermpa"
366 +
            linter: "biome"
367 +
            test_name: "Default + No RPC + TanStack Query + React Router MPA + Biome"
368 +
369 +
          # Tailwind template combinations
370 +
          - template: "tailwind"
371 +
            rpc: true
372 +
            tanstackQuery: false
373 +
            router: "reactroutermpa"
374 +
            linter: "eslint"
375 +
            test_name: "Tailwind + RPC + No TanStack Query + React Router MPA + ESLint"
376 +
          - template: "tailwind"
377 +
            rpc: true
378 +
            tanstackQuery: false
379 +
            router: "reactroutermpa"
380 +
            linter: "biome"
381 +
            test_name: "Tailwind + RPC + No TanStack Query + React Router MPA + Biome"
382 +
          - template: "tailwind"
383 +
            rpc: false
384 +
            tanstackQuery: false
385 +
            router: "reactroutermpa"
386 +
            linter: "eslint"
387 +
            test_name: "Tailwind + No RPC + No TanStack Query + React Router MPA + ESLint"
388 +
          - template: "tailwind"
389 +
            rpc: false
390 +
            tanstackQuery: false
391 +
            router: "reactroutermpa"
392 +
            linter: "biome"
393 +
            test_name: "Tailwind + No RPC + No TanStack Query + React Router MPA + Biome"
394 +
          - template: "tailwind"
395 +
            rpc: true
396 +
            tanstackQuery: true
397 +
            router: "reactroutermpa"
398 +
            linter: "eslint"
399 +
            test_name: "Tailwind + RPC + TanStack Query + React Router MPA + ESLint"
400 +
          - template: "tailwind"
401 +
            rpc: true
402 +
            tanstackQuery: true
403 +
            router: "reactroutermpa"
404 +
            linter: "biome"
405 +
            test_name: "Tailwind + RPC + TanStack Query + React Router MPA + Biome"
406 +
          - template: "tailwind"
407 +
            rpc: false
408 +
            tanstackQuery: true
409 +
            router: "reactroutermpa"
410 +
            linter: "eslint"
411 +
            test_name: "Tailwind + No RPC + TanStack Query + React Router MPA + ESLint"
412 +
          - template: "tailwind"
413 +
            rpc: false
414 +
            tanstackQuery: true
415 +
            router: "reactroutermpa"
416 +
            linter: "biome"
417 +
            test_name: "Tailwind + No RPC + TanStack Query + React Router MPA + Biome"
418 +
419 +
          # Shadcn template combinations
420 +
          - template: "shadcn"
421 +
            rpc: true
422 +
            tanstackQuery: false
423 +
            router: "reactroutermpa"
424 +
            linter: "eslint"
425 +
            test_name: "Shadcn + RPC + No TanStack Query + React Router MPA + ESLint"
426 +
          - template: "shadcn"
427 +
            rpc: true
428 +
            tanstackQuery: false
429 +
            router: "reactroutermpa"
430 +
            linter: "biome"
431 +
            test_name: "Shadcn + RPC + No TanStack Query + React Router MPA + Biome"
432 +
          - template: "shadcn"
433 +
            rpc: false
434 +
            tanstackQuery: false
435 +
            router: "reactroutermpa"
436 +
            linter: "eslint"
437 +
            test_name: "Shadcn + No RPC + No TanStack Query + React Router MPA + ESLint"
438 +
          - template: "shadcn"
439 +
            rpc: false
440 +
            tanstackQuery: false
441 +
            router: "reactroutermpa"
442 +
            linter: "biome"
443 +
            test_name: "Shadcn + No RPC + No TanStack Query + React Router MPA + Biome"
444 +
          - template: "shadcn"
445 +
            rpc: true
446 +
            tanstackQuery: true
447 +
            router: "reactroutermpa"
448 +
            linter: "eslint"
449 +
            test_name: "Shadcn + RPC + TanStack Query + React Router MPA + ESLint"
450 +
          - template: "shadcn"
451 +
            rpc: true
452 +
            tanstackQuery: true
453 +
            router: "reactroutermpa"
454 +
            linter: "biome"
455 +
            test_name: "Shadcn + RPC + TanStack Query + React Router MPA + Biome"
456 +
          - template: "shadcn"
457 +
            rpc: false
458 +
            tanstackQuery: true
459 +
            router: "reactroutermpa"
460 +
            linter: "eslint"
461 +
            test_name: "Shadcn + No RPC + TanStack Query + React Router MPA + ESLint"
462 +
          - template: "shadcn"
463 +
            rpc: false
464 +
            tanstackQuery: true
465 +
            router: "reactroutermpa"
466 +
            linter: "biome"
467 +
            test_name: "Shadcn + No RPC + TanStack Query + React Router MPA + Biome"
468 +
318 469
          # TanStack Router
319 470
          # Default template combinations
320 471
          - template: "default"
493 644
494 645
          # Build the command with conditional flags
495 646
          cmd="./dist/index.js test-project-${{ matrix.template }}-${{ matrix.rpc }}-${{ matrix.tanstackQuery }}-${{ matrix.router }}-${{ matrix.linter }} --yes --template ${{ matrix.template }}"
496 -
          
647 +
497 648
          if [ "${{ matrix.rpc }}" = "true" ]; then
498 649
            cmd="$cmd --rpc"
499 650
          fi
500 -
          
651 +
501 652
          if [ "${{ matrix.tanstackQuery }}" = "true" ]; then
502 653
            cmd="$cmd --tsquery"
503 654
          fi
504 -
          
655 +
505 656
          if [ "${{ matrix.router }}" != "none" ]; then
506 657
            cmd="$cmd --router ${{ matrix.router }}"
507 658
          fi
508 -
          
659 +
509 660
          cmd="$cmd --linter ${{ matrix.linter }}"
510 -
          
661 +
511 662
          echo "Running: $cmd"
512 663
          eval $cmd
513 664
536 687
            exit 1
537 688
          fi
538 689
539 -
          # Check for expected files
540 -
          if [ ! -f "client/dist/index.html" ]; then
541 -
            echo "❌ Client build incomplete - index.html not found"
542 -
            exit 1
543 -
          fi
690 +
          # Check for expected files based on router type
691 +
          if [ "${{ matrix.router }}" = "reactroutermpa" ]; then
692 +
            # React Router MPA has different build structure (client-side only)
693 +
            if [ ! -d "client/dist/client" ]; then
694 +
              echo "❌ Client build incomplete - dist/client directory not found"
695 +
              exit 1
696 +
            fi
697 +
698 +
            # Verify server build (separate from React Router MPA)
699 +
            if [ ! -f "server/dist/index.js" ]; then
700 +
              echo "❌ Server build incomplete - index.js not found"
701 +
              exit 1
702 +
            fi
703 +
704 +
            echo "✅ React Router MPA build verification passed for ${{ matrix.test_name }}"
705 +
          else
706 +
            # Standard build structure
707 +
            if [ ! -f "client/dist/index.html" ]; then
708 +
              echo "❌ Client build incomplete - index.html not found"
709 +
              exit 1
710 +
            fi
711 +
712 +
            if [ ! -f "server/dist/index.js" ]; then
713 +
              echo "❌ Server build incomplete - index.js not found"
714 +
              exit 1
715 +
            fi
544 716
545 -
          if [ ! -f "server/dist/index.js" ]; then
546 -
            echo "❌ Server build incomplete - index.js not found"
547 -
            exit 1
717 +
            echo "✅ Build verification passed for ${{ matrix.test_name }}"
548 718
          fi
549 -
550 -
          echo "✅ Build verification passed for ${{ matrix.test_name }}"
551 719
552 720
      - name: Run linter on generated project
553 721
        run: |
.gitignore +1 −0
42 42
# Bun
43 43
bun.lockb
44 44
.claude
45 +
test-projects/*
src/index.ts +1 −1
29 29
	)
30 30
	.option(
31 31
		"--router <router>",
32 -
		"specify a client router (none, reactrouter, tanstackrouter)",
32 +
		"specify a client router (none, reactrouter, reactroutermpa, tanstackrouter)",
33 33
	)
34 34
	.option("--linter <linter>", "specify the linter to use (eslint or biome)")
35 35
	.action(create);
src/installers/react-router-mpa.ts (added) +198 −0
1 +
import path from "node:path";
2 +
import fs from "fs-extra";
3 +
import type { ProjectOptions } from "@/types";
4 +
import yoctoSpinner from "yocto-spinner";
5 +
import pc from "picocolors";
6 +
import { consola } from "consola";
7 +
import { addPackageDependency } from "@/utils/add-package-dependency";
8 +
import { EXTRAS_DIR } from "@/utils";
9 +
import { nameGenerator } from "@/utils/name-generator";
10 +
11 +
export const reactRouterMpaInstaller = async (
12 +
	options: Required<ProjectOptions>,
13 +
): Promise<boolean> => {
14 +
	const spinner = yoctoSpinner({
15 +
		text: "Setting up React Router MPA...",
16 +
	}).start();
17 +
18 +
	try {
19 +
		const { projectName, rpc, shadcn, tailwind, tanstackQuery } = options;
20 +
21 +
		const projectPath = path.resolve(process.cwd(), projectName);
22 +
23 +
		spinner.text = "Installing React Router MPA dependencies...";
24 +
		await addPackageDependency({
25 +
			dependencies: [
26 +
				"react-router",
27 +
				"@react-router/dev",
28 +
				"@react-router/node",
29 +
				"@react-router/serve",
30 +
				"vite-tsconfig-paths",
31 +
				"isbot",
32 +
			],
33 +
			target: "client",
34 +
			projectName,
35 +
		});
36 +
37 +
		// Copy root.tsx
38 +
		const rootTsxSrc = path.join(
39 +
			EXTRAS_DIR,
40 +
			"client",
41 +
			"src",
42 +
			"root.tsx",
43 +
			"root.tsx",
44 +
		);
45 +
		const rootTsxTarget = path.join(projectPath, "client", "src", "root.tsx");
46 +
		fs.copySync(rootTsxSrc, rootTsxTarget);
47 +
48 +
		// Copy routes.ts
49 +
		const routesTsSrc = path.join(
50 +
			EXTRAS_DIR,
51 +
			"client",
52 +
			"src",
53 +
			"routes.ts",
54 +
			"routes.ts",
55 +
		);
56 +
		const routesTsTarget = path.join(projectPath, "client", "src", "routes.ts");
57 +
		fs.copySync(routesTsSrc, routesTsTarget);
58 +
59 +
		// Create routes directory and copy home.tsx
60 +
		const routesDir = path.join(projectPath, "client", "src", "routes");
61 +
		fs.ensureDirSync(routesDir);
62 +
63 +
		const homeTsxSrc = path.join(
64 +
			EXTRAS_DIR,
65 +
			"client",
66 +
			"src",
67 +
			"routes",
68 +
			"home.tsx",
69 +
			"home.tsx",
70 +
		);
71 +
		const homeTsxTarget = path.join(routesDir, "home.tsx");
72 +
		fs.copySync(homeTsxSrc, homeTsxTarget);
73 +
74 +
		// Copy ClientOnly component
75 +
		const clientOnlySrc = path.join(
76 +
			EXTRAS_DIR,
77 +
			"client",
78 +
			"src",
79 +
			"components",
80 +
			"ClientOnly.tsx",
81 +
			"ClientOnly.tsx",
82 +
		);
83 +
		const clientOnlyTarget = path.join(
84 +
			projectPath,
85 +
			"client",
86 +
			"src",
87 +
			"components",
88 +
			"ClientOnly.tsx",
89 +
		);
90 +
		fs.copySync(clientOnlySrc, clientOnlyTarget);
91 +
92 +
		// Update Home component with ClientOnly wrapper
93 +
		const homeTsxTemplate = nameGenerator("Home.tsx", {
94 +
			reactroutermpa: true,
95 +
			rpc,
96 +
			shadcn,
97 +
			tailwind,
98 +
			tanstackQuery,
99 +
		});
100 +
101 +
		const homeComponentSrc = path.join(
102 +
			EXTRAS_DIR,
103 +
			"client",
104 +
			"src",
105 +
			"components",
106 +
			"Home.tsx",
107 +
			homeTsxTemplate,
108 +
		);
109 +
		const homeComponentTarget = path.join(
110 +
			projectPath,
111 +
			"client",
112 +
			"src",
113 +
			"components",
114 +
			"Home.tsx",
115 +
		);
116 +
		fs.copySync(homeComponentSrc, homeComponentTarget);
117 +
118 +
		// Remove unused files (App.tsx and main.tsx are not used in MPA setup)
119 +
		const appTsxPath = path.join(projectPath, "client", "src", "App.tsx");
120 +
		const mainTsxPath = path.join(projectPath, "client", "src", "main.tsx");
121 +
122 +
		if (fs.existsSync(appTsxPath)) {
123 +
			fs.removeSync(appTsxPath);
124 +
		}
125 +
126 +
		if (fs.existsSync(mainTsxPath)) {
127 +
			fs.removeSync(mainTsxPath);
128 +
		}
129 +
130 +
		// Copy react-router.config.ts
131 +
		const reactRouterConfigSrc = path.join(
132 +
			EXTRAS_DIR,
133 +
			"client",
134 +
			"react-router.config.ts",
135 +
			"react-router.config.ts",
136 +
		);
137 +
		const reactRouterConfigTarget = path.join(
138 +
			projectPath,
139 +
			"client",
140 +
			"react-router.config.ts",
141 +
		);
142 +
		fs.copySync(reactRouterConfigSrc, reactRouterConfigTarget);
143 +
144 +
		// Copy tsconfig.app.json
145 +
		const tsconfigAppSrc = path.join(
146 +
			EXTRAS_DIR,
147 +
			"client",
148 +
			"tsconfig.app.json",
149 +
			"tsconfig.app.json",
150 +
		);
151 +
		const tsconfigAppTarget = path.join(
152 +
			projectPath,
153 +
			"client",
154 +
			"tsconfig.app.json",
155 +
		);
156 +
		fs.copySync(tsconfigAppSrc, tsconfigAppTarget);
157 +
158 +
		// Update vite.config.ts
159 +
		const viteConfigTemplate = nameGenerator("vite.config.ts", {
160 +
			reactroutermpa: true,
161 +
			shadcn,
162 +
			tailwind,
163 +
		});
164 +
165 +
		const viteConfigSrc = path.join(
166 +
			EXTRAS_DIR,
167 +
			"client",
168 +
			"vite.config.ts",
169 +
			viteConfigTemplate,
170 +
		);
171 +
		const viteConfigTarget = path.join(projectPath, "client", "vite.config.ts");
172 +
		fs.copySync(viteConfigSrc, viteConfigTarget);
173 +
174 +
		// Update package.json scripts
175 +
		const packageJsonPath = path.join(projectPath, "client", "package.json");
176 +
		const packageJson = fs.readJsonSync(packageJsonPath);
177 +
178 +
		packageJson.scripts = {
179 +
			...packageJson.scripts,
180 +
			dev: "react-router dev",
181 +
			build: "react-router typegen && tsc -b && react-router build",
182 +
			typecheck: "react-router typegen && tsc",
183 +
		};
184 +
185 +
		fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 });
186 +
187 +
		spinner.success("React Router MPA setup completed");
188 +
		return true;
189 +
	} catch (err: unknown) {
190 +
		spinner.error("Failed to set up React Router MPA");
191 +
		if (err instanceof Error) {
192 +
			consola.error(pc.red("Error:"), err.message);
193 +
		} else {
194 +
			consola.error(pc.red("Error: Unknown error"));
195 +
		}
196 +
		return false;
197 +
	}
198 +
};
src/lib/install-packages.ts +5 −0
4 4
import { tanstackQueryInstaller } from "@/installers/tanstack-query";
5 5
import { rpcInstaller } from "@/installers/rpc";
6 6
import { reactRouterInstaller } from "@/installers/react-router";
7 +
import { reactRouterMpaInstaller } from "@/installers/react-router-mpa";
7 8
import { tanstackRouterInstaller } from "@/installers/tanstack-router";
8 9
9 10
export async function installPackages(
29 30
		switch (router) {
30 31
			case "reactrouter": {
31 32
				await reactRouterInstaller(options);
33 +
				break;
34 +
			}
35 +
			case "reactroutermpa": {
36 +
				await reactRouterMpaInstaller(options);
32 37
				break;
33 38
			}
34 39
			case "tanstackrouter": {
src/lib/prompt-for-options.ts +6 −1
102 102
				options: [
103 103
					{ label: "None (default)", value: "none" },
104 104
					{ label: "React Router", value: "reactrouter" },
105 +
					{ label: "React Router MPA", value: "reactroutermpa" },
105 106
					{ label: "TanStack Router", value: "tanstackrouter" },
106 107
				],
107 108
				initial: "none",
114 115
			process.exit(1);
115 116
		}
116 117
117 -
		router = routerResponse as "none" | "reactrouter" | "tanstackrouter";
118 +
		router = routerResponse as
119 +
			| "none"
120 +
			| "reactrouter"
121 +
			| "reactroutermpa"
122 +
			| "tanstackrouter";
118 123
	}
119 124
120 125
	let useTanstackQuery = options.tanstackQuery;
src/templates/extras/client/react-router.config.ts/react-router.config.ts (added) +10 −0
1 +
import type { Config } from "@react-router/dev/config";
2 +
3 +
export default {
4 +
	buildDirectory: "dist",
5 +
	appDirectory: "src",
6 +
	ssr: false,
7 +
	async prerender() {
8 +
		return ["/"];
9 +
	},
10 +
} satisfies Config;
src/templates/extras/client/src/components/ClientOnly.tsx/ClientOnly.tsx (added) +22 −0
1 +
import {
2 +
	useEffect,
3 +
	useState,
4 +
	type PropsWithChildren,
5 +
	type ReactNode,
6 +
} from "react";
7 +
8 +
type ClientOnlyProps = PropsWithChildren & {
9 +
	fallback?: ReactNode;
10 +
};
11 +
12 +
const ClientOnly = ({ children, fallback }: ClientOnlyProps) => {
13 +
	const [isClient, setIsClient] = useState(false);
14 +
15 +
	useEffect(() => {
16 +
		setIsClient(true);
17 +
	}, []);
18 +
19 +
	return isClient ? children : fallback;
20 +
};
21 +
22 +
export default ClientOnly;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-shadcn-tailwind-tanstackquery.tsx (added) +73 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import { Button } from "@/components/ui/button";
4 +
import { hcWithType } from "server/dist/client";
5 +
import { useMutation } from "@tanstack/react-query";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
const client = hcWithType(SERVER_URL);
11 +
12 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
13 +
14 +
function Home() {
15 +
	const [data, setData] = useState<
16 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
17 +
	>();
18 +
19 +
	const { mutate: sendRequest } = useMutation({
20 +
		mutationFn: async () => {
21 +
			try {
22 +
				const res = await client.hello.$get();
23 +
				if (!res.ok) {
24 +
					console.log("Error fetching data");
25 +
					return;
26 +
				}
27 +
				const data = await res.json();
28 +
				setData(data);
29 +
			} catch (error) {
30 +
				console.log(error);
31 +
			}
32 +
		},
33 +
	});
34 +
35 +
	return (
36 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
37 +
			<a
38 +
				href="https://github.com/stevedylandev/bhvr"
39 +
				target="_blank"
40 +
				rel="noopener"
41 +
			>
42 +
				<img
43 +
					src={beaver}
44 +
					className="w-16 h-16 cursor-pointer"
45 +
					alt="beaver logo"
46 +
				/>
47 +
			</a>
48 +
			<h1 className="text-5xl font-black">bhvr</h1>
49 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
50 +
			<p>A typesafe fullstack monorepo</p>
51 +
			<ClientOnly>
52 +
				<div className="flex items-center gap-4">
53 +
					<Button onClick={() => sendRequest()}>Call API</Button>
54 +
					<Button variant="secondary" asChild>
55 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
56 +
							Docs
57 +
						</a>
58 +
					</Button>
59 +
				</div>
60 +
				{data && (
61 +
					<pre className="bg-gray-100 p-4 rounded-md">
62 +
						<code>
63 +
							Message: {data.message} <br />
64 +
							Success: {data.success.toString()}
65 +
						</code>
66 +
					</pre>
67 +
				)}
68 +
			</ClientOnly>
69 +
		</div>
70 +
	);
71 +
}
72 +
73 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-shadcn-tailwind.tsx (added) +70 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import { Button } from "@/components/ui/button";
4 +
import { hcWithType } from "server/dist/client";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function Home() {
14 +
	const [data, setData] = useState<
15 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
16 +
	>();
17 +
18 +
	async function sendRequest() {
19 +
		try {
20 +
			const res = await client.hello.$get();
21 +
			if (!res.ok) {
22 +
				console.log("Error fetching data");
23 +
				return;
24 +
			}
25 +
			const data = await res.json();
26 +
			setData(data);
27 +
		} catch (error) {
28 +
			console.log(error);
29 +
		}
30 +
	}
31 +
32 +
	return (
33 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
34 +
			<a
35 +
				href="https://github.com/stevedylandev/bhvr"
36 +
				target="_blank"
37 +
				rel="noopener"
38 +
			>
39 +
				<img
40 +
					src={beaver}
41 +
					className="w-16 h-16 cursor-pointer"
42 +
					alt="beaver logo"
43 +
				/>
44 +
			</a>
45 +
			<h1 className="text-5xl font-black">bhvr</h1>
46 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
47 +
			<p>A typesafe fullstack monorepo</p>
48 +
			<ClientOnly>
49 +
				<div className="flex items-center gap-4">
50 +
					<Button onClick={sendRequest}>Call API</Button>
51 +
					<Button variant="secondary" asChild>
52 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
53 +
							Docs
54 +
						</a>
55 +
					</Button>
56 +
				</div>
57 +
				{data && (
58 +
					<pre className="bg-gray-100 p-4 rounded-md">
59 +
						<code>
60 +
							Message: {data.message} <br />
61 +
							Success: {data.success.toString()}
62 +
						</code>
63 +
					</pre>
64 +
				)}
65 +
			</ClientOnly>
66 +
		</div>
67 +
	);
68 +
}
69 +
70 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-tailwind-tanstackquery.tsx (added) +81 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import { useMutation } from "@tanstack/react-query";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function Home() {
14 +
	const [data, setData] = useState<
15 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
16 +
	>();
17 +
18 +
	const { mutate: sendRequest } = useMutation({
19 +
		mutationFn: async () => {
20 +
			try {
21 +
				const res = await client.hello.$get();
22 +
				if (!res.ok) {
23 +
					console.log("Error fetching data");
24 +
					return;
25 +
				}
26 +
				const data = await res.json();
27 +
				setData(data);
28 +
			} catch (error) {
29 +
				console.log(error);
30 +
			}
31 +
		},
32 +
	});
33 +
34 +
	return (
35 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
36 +
			<a
37 +
				href="https://github.com/stevedylandev/bhvr"
38 +
				target="_blank"
39 +
				rel="noopener"
40 +
			>
41 +
				<img
42 +
					src={beaver}
43 +
					className="w-16 h-16 cursor-pointer"
44 +
					alt="beaver logo"
45 +
				/>
46 +
			</a>
47 +
			<h1 className="text-5xl font-black">bhvr</h1>
48 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
49 +
			<p>A typesafe fullstack monorepo</p>
50 +
			<ClientOnly>
51 +
				<div className="flex items-center gap-4">
52 +
					<button
53 +
						type="button"
54 +
						onClick={() => sendRequest()}
55 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
56 +
					>
57 +
						Call API
58 +
					</button>
59 +
					<a
60 +
						target="_blank"
61 +
						href="https://bhvr.dev"
62 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
63 +
						rel="noopener"
64 +
					>
65 +
						Docs
66 +
					</a>
67 +
				</div>
68 +
				{data && (
69 +
					<pre className="bg-gray-100 p-4 rounded-md">
70 +
						<code>
71 +
							Message: {data.message} <br />
72 +
							Success: {data.success.toString()}
73 +
						</code>
74 +
					</pre>
75 +
				)}
76 +
			</ClientOnly>
77 +
		</div>
78 +
	);
79 +
}
80 +
81 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-tailwind.tsx (added) +78 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import ClientOnly from "./ClientOnly";
5 +
6 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
7 +
8 +
const client = hcWithType(SERVER_URL);
9 +
10 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
11 +
12 +
function Home() {
13 +
	const [data, setData] = useState<
14 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
15 +
	>();
16 +
17 +
	async function sendRequest() {
18 +
		try {
19 +
			const res = await client.hello.$get();
20 +
			if (!res.ok) {
21 +
				console.log("Error fetching data");
22 +
				return;
23 +
			}
24 +
			const data = await res.json();
25 +
			setData(data);
26 +
		} catch (error) {
27 +
			console.log(error);
28 +
		}
29 +
	}
30 +
31 +
	return (
32 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
33 +
			<a
34 +
				href="https://github.com/stevedylandev/bhvr"
35 +
				target="_blank"
36 +
				rel="noopener"
37 +
			>
38 +
				<img
39 +
					src={beaver}
40 +
					className="w-16 h-16 cursor-pointer"
41 +
					alt="beaver logo"
42 +
				/>
43 +
			</a>
44 +
			<h1 className="text-5xl font-black">bhvr</h1>
45 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
46 +
			<p>A typesafe fullstack monorepo</p>
47 +
			<ClientOnly>
48 +
				<div className="flex items-center gap-4">
49 +
					<button
50 +
						type="button"
51 +
						onClick={sendRequest}
52 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
53 +
					>
54 +
						Call API
55 +
					</button>
56 +
					<a
57 +
						target="_blank"
58 +
						href="https://bhvr.dev"
59 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
60 +
						rel="noopener"
61 +
					>
62 +
						Docs
63 +
					</a>
64 +
				</div>
65 +
				{data && (
66 +
					<pre className="bg-gray-100 p-4 rounded-md">
67 +
						<code>
68 +
							Message: {data.message} <br />
69 +
							Success: {data.success.toString()}
70 +
						</code>
71 +
					</pre>
72 +
				)}
73 +
			</ClientOnly>
74 +
		</div>
75 +
	);
76 +
}
77 +
78 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc-tanstackquery.tsx (added) +85 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import { useMutation } from "@tanstack/react-query";
5 +
import "../App.css";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
const client = hcWithType(SERVER_URL);
11 +
12 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
13 +
14 +
function Home() {
15 +
	const [data, setData] = useState<
16 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
17 +
	>();
18 +
19 +
	const { mutate: sendRequest } = useMutation({
20 +
		mutationFn: async () => {
21 +
			try {
22 +
				const res = await client.hello.$get();
23 +
				if (!res.ok) {
24 +
					console.log("Error fetching data");
25 +
					return;
26 +
				}
27 +
				const data = await res.json();
28 +
				setData(data);
29 +
			} catch (error) {
30 +
				console.log(error);
31 +
			}
32 +
		},
33 +
	});
34 +
35 +
	return (
36 +
		<div
37 +
			style={{
38 +
				maxWidth: "1280px",
39 +
				margin: "0 auto",
40 +
				padding: "2rem",
41 +
				textAlign: "center",
42 +
			}}
43 +
		>
44 +
			<div>
45 +
				<a
46 +
					href="https://github.com/stevedylandev/bhvr"
47 +
					target="_blank"
48 +
					rel="noopener"
49 +
				>
50 +
					<img src={beaver} className="logo" alt="beaver logo" />
51 +
				</a>
52 +
			</div>
53 +
			<h1>bhvr</h1>
54 +
			<h2>Bun + Hono + Vite + React</h2>
55 +
			<p>A typesafe fullstack monorepo</p>
56 +
			<ClientOnly>
57 +
				<div className="card">
58 +
					<div className="button-container">
59 +
						<button type="button" onClick={() => sendRequest()}>
60 +
							Call API
61 +
						</button>
62 +
						<a
63 +
							className="docs-link"
64 +
							target="_blank"
65 +
							href="https://bhvr.dev"
66 +
							rel="noopener"
67 +
						>
68 +
							Docs
69 +
						</a>
70 +
					</div>
71 +
					{data && (
72 +
						<pre className="response">
73 +
							<code>
74 +
								Message: {data.message} <br />
75 +
								Success: {data.success.toString()}
76 +
							</code>
77 +
						</pre>
78 +
					)}
79 +
				</div>
80 +
			</ClientOnly>
81 +
		</div>
82 +
	);
83 +
}
84 +
85 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-rpc.tsx (added) +82 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { hcWithType } from "server/dist/client";
4 +
import "../App.css";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
const client = hcWithType(SERVER_URL);
10 +
11 +
type ResponseType = Awaited<ReturnType<typeof client.hello.$get>>;
12 +
13 +
function Home() {
14 +
	const [data, setData] = useState<
15 +
		Awaited<ReturnType<ResponseType["json"]>> | undefined
16 +
	>();
17 +
18 +
	async function sendRequest() {
19 +
		try {
20 +
			const res = await client.hello.$get();
21 +
			if (!res.ok) {
22 +
				console.log("Error fetching data");
23 +
				return;
24 +
			}
25 +
			const data = await res.json();
26 +
			setData(data);
27 +
		} catch (error) {
28 +
			console.log(error);
29 +
		}
30 +
	}
31 +
32 +
	return (
33 +
		<div
34 +
			style={{
35 +
				maxWidth: "1280px",
36 +
				margin: "0 auto",
37 +
				padding: "2rem",
38 +
				textAlign: "center",
39 +
			}}
40 +
		>
41 +
			<div>
42 +
				<a
43 +
					href="https://github.com/stevedylandev/bhvr"
44 +
					target="_blank"
45 +
					rel="noopener"
46 +
				>
47 +
					<img src={beaver} className="logo" alt="beaver logo" />
48 +
				</a>
49 +
			</div>
50 +
			<h1>bhvr</h1>
51 +
			<h2>Bun + Hono + Vite + React</h2>
52 +
			<p>A typesafe fullstack monorepo</p>
53 +
			<ClientOnly>
54 +
				<div className="card">
55 +
					<div className="button-container">
56 +
						<button type="button" onClick={sendRequest}>
57 +
							Call API
58 +
						</button>
59 +
						<a
60 +
							className="docs-link"
61 +
							target="_blank"
62 +
							href="https://bhvr.dev"
63 +
							rel="noopener"
64 +
						>
65 +
							Docs
66 +
						</a>
67 +
					</div>
68 +
					{data && (
69 +
						<pre className="response">
70 +
							<code>
71 +
								Message: {data.message} <br />
72 +
								Success: {data.success.toString()}
73 +
							</code>
74 +
						</pre>
75 +
					)}
76 +
				</div>
77 +
			</ClientOnly>
78 +
		</div>
79 +
	);
80 +
}
81 +
82 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-shadcn-tailwind-tanstackquery.tsx (added) +63 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import { Button } from "@/components/ui/button";
5 +
import { useMutation } from "@tanstack/react-query";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
function Home() {
11 +
	const [data, setData] = useState<ApiResponse | undefined>();
12 +
13 +
	const { mutate: sendRequest } = useMutation({
14 +
		mutationFn: async () => {
15 +
			try {
16 +
				const req = await fetch(`${SERVER_URL}/hello`);
17 +
				const res: ApiResponse = await req.json();
18 +
				setData(res);
19 +
			} catch (error) {
20 +
				console.log(error);
21 +
			}
22 +
		},
23 +
	});
24 +
25 +
	return (
26 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
27 +
			<a
28 +
				href="https://github.com/stevedylandev/bhvr"
29 +
				target="_blank"
30 +
				rel="noopener"
31 +
			>
32 +
				<img
33 +
					src={beaver}
34 +
					className="w-16 h-16 cursor-pointer"
35 +
					alt="beaver logo"
36 +
				/>
37 +
			</a>
38 +
			<h1 className="text-5xl font-black">bhvr</h1>
39 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
40 +
			<p>A typesafe fullstack monorepo</p>
41 +
			<ClientOnly>
42 +
				<div className="flex items-center gap-4">
43 +
					<Button onClick={() => sendRequest()}>Call API</Button>
44 +
					<Button variant="secondary" asChild>
45 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
46 +
							Docs
47 +
						</a>
48 +
					</Button>
49 +
				</div>
50 +
				{data && (
51 +
					<pre className="bg-gray-100 p-4 rounded-md">
52 +
						<code>
53 +
							Message: {data.message} <br />
54 +
							Success: {data.success.toString()}
55 +
						</code>
56 +
					</pre>
57 +
				)}
58 +
			</ClientOnly>
59 +
		</div>
60 +
	);
61 +
}
62 +
63 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-shadcn-tailwind.tsx (added) +60 −0
1 +
import { useState } from "react";
2 +
import beaver from "@/assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import { Button } from "@/components/ui/button";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
function Home() {
10 +
	const [data, setData] = useState<ApiResponse | undefined>();
11 +
12 +
	async function sendRequest() {
13 +
		try {
14 +
			const req = await fetch(`${SERVER_URL}/hello`);
15 +
			const res: ApiResponse = await req.json();
16 +
			setData(res);
17 +
		} catch (error) {
18 +
			console.log(error);
19 +
		}
20 +
	}
21 +
22 +
	return (
23 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
24 +
			<a
25 +
				href="https://github.com/stevedylandev/bhvr"
26 +
				target="_blank"
27 +
				rel="noopener"
28 +
			>
29 +
				<img
30 +
					src={beaver}
31 +
					className="w-16 h-16 cursor-pointer"
32 +
					alt="beaver logo"
33 +
				/>
34 +
			</a>
35 +
			<h1 className="text-5xl font-black">bhvr</h1>
36 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
37 +
			<p>A typesafe fullstack monorepo</p>
38 +
			<ClientOnly>
39 +
				<div className="flex items-center gap-4">
40 +
					<Button onClick={sendRequest}>Call API</Button>
41 +
					<Button variant="secondary" asChild>
42 +
						<a target="_blank" href="https://bhvr.dev" rel="noopener">
43 +
							Docs
44 +
						</a>
45 +
					</Button>
46 +
				</div>
47 +
				{data && (
48 +
					<pre className="bg-gray-100 p-4 rounded-md">
49 +
						<code>
50 +
							Message: {data.message} <br />
51 +
							Success: {data.success.toString()}
52 +
						</code>
53 +
					</pre>
54 +
				)}
55 +
			</ClientOnly>
56 +
		</div>
57 +
	);
58 +
}
59 +
60 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-tailwind-tanstackquery.tsx (added) +71 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import { useMutation } from "@tanstack/react-query";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
function Home() {
10 +
	const [data, setData] = useState<ApiResponse | undefined>();
11 +
12 +
	const { mutate: sendRequest } = useMutation({
13 +
		mutationFn: async () => {
14 +
			try {
15 +
				const req = await fetch(`${SERVER_URL}/hello`);
16 +
				const res: ApiResponse = await req.json();
17 +
				setData(res);
18 +
			} catch (error) {
19 +
				console.log(error);
20 +
			}
21 +
		},
22 +
	});
23 +
24 +
	return (
25 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
26 +
			<a
27 +
				href="https://github.com/stevedylandev/bhvr"
28 +
				target="_blank"
29 +
				rel="noopener"
30 +
			>
31 +
				<img
32 +
					src={beaver}
33 +
					className="w-16 h-16 cursor-pointer"
34 +
					alt="beaver logo"
35 +
				/>
36 +
			</a>
37 +
			<h1 className="text-5xl font-black">bhvr</h1>
38 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
39 +
			<p>A typesafe fullstack monorepo</p>
40 +
			<ClientOnly>
41 +
				<div className="flex items-center gap-4">
42 +
					<button
43 +
						type="button"
44 +
						onClick={() => sendRequest()}
45 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
46 +
					>
47 +
						Call API
48 +
					</button>
49 +
					<a
50 +
						target="_blank"
51 +
						href="https://bhvr.dev"
52 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
53 +
						rel="noopener"
54 +
					>
55 +
						Docs
56 +
					</a>
57 +
				</div>
58 +
				{data && (
59 +
					<pre className="bg-gray-100 p-4 rounded-md">
60 +
						<code>
61 +
							Message: {data.message} <br />
62 +
							Success: {data.success.toString()}
63 +
						</code>
64 +
					</pre>
65 +
				)}
66 +
			</ClientOnly>
67 +
		</div>
68 +
	);
69 +
}
70 +
71 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-tailwind.tsx (added) +68 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import ClientOnly from "./ClientOnly";
5 +
6 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
7 +
8 +
function Home() {
9 +
	const [data, setData] = useState<ApiResponse | undefined>();
10 +
11 +
	async function sendRequest() {
12 +
		try {
13 +
			const req = await fetch(`${SERVER_URL}/hello`);
14 +
			const res: ApiResponse = await req.json();
15 +
			setData(res);
16 +
		} catch (error) {
17 +
			console.log(error);
18 +
		}
19 +
	}
20 +
21 +
	return (
22 +
		<div className="max-w-xl mx-auto flex flex-col gap-6 items-center justify-center min-h-screen">
23 +
			<a
24 +
				href="https://github.com/stevedylandev/bhvr"
25 +
				target="_blank"
26 +
				rel="noopener"
27 +
			>
28 +
				<img
29 +
					src={beaver}
30 +
					className="w-16 h-16 cursor-pointer"
31 +
					alt="beaver logo"
32 +
				/>
33 +
			</a>
34 +
			<h1 className="text-5xl font-black">bhvr</h1>
35 +
			<h2 className="text-2xl font-bold">Bun + Hono + Vite + React</h2>
36 +
			<p>A typesafe fullstack monorepo</p>
37 +
			<ClientOnly>
38 +
				<div className="flex items-center gap-4">
39 +
					<button
40 +
						type="button"
41 +
						onClick={sendRequest}
42 +
						className="bg-black text-white px-2.5 py-1.5 rounded-md"
43 +
					>
44 +
						Call API
45 +
					</button>
46 +
					<a
47 +
						target="_blank"
48 +
						href="https://bhvr.dev"
49 +
						className="border-1 border-black text-black px-2.5 py-1.5 rounded-md"
50 +
						rel="noopener"
51 +
					>
52 +
						Docs
53 +
					</a>
54 +
				</div>
55 +
				{data && (
56 +
					<pre className="bg-gray-100 p-4 rounded-md">
57 +
						<code>
58 +
							Message: {data.message} <br />
59 +
							Success: {data.success.toString()}
60 +
						</code>
61 +
					</pre>
62 +
				)}
63 +
			</ClientOnly>
64 +
		</div>
65 +
	);
66 +
}
67 +
68 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa-tanstackquery.tsx (added) +72 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import { useMutation } from "@tanstack/react-query";
4 +
import type { ApiResponse } from "shared";
5 +
import "../App.css";
6 +
import ClientOnly from "./ClientOnly";
7 +
8 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
9 +
10 +
function Home() {
11 +
	const [data, setData] = useState<ApiResponse | undefined>();
12 +
13 +
	const { mutate: sendRequest } = useMutation({
14 +
		mutationFn: async () => {
15 +
			const req = await fetch(`${SERVER_URL}/hello`);
16 +
			const res: ApiResponse = await req.json();
17 +
			setData(res);
18 +
		},
19 +
		onError: (err) => console.log(err),
20 +
	});
21 +
22 +
	return (
23 +
		<div
24 +
			style={{
25 +
				maxWidth: "1280px",
26 +
				margin: "0 auto",
27 +
				padding: "2rem",
28 +
				textAlign: "center",
29 +
			}}
30 +
		>
31 +
			<div>
32 +
				<a
33 +
					href="https://github.com/stevedylandev/bhvr"
34 +
					target="_blank"
35 +
					rel="noopener"
36 +
				>
37 +
					<img src={beaver} className="logo" alt="beaver logo" />
38 +
				</a>
39 +
			</div>
40 +
			<h1>bhvr</h1>
41 +
			<h2>Bun + Hono + Vite + React</h2>
42 +
			<p>A typesafe fullstack monorepo</p>
43 +
			<ClientOnly>
44 +
				<div className="card">
45 +
					<div className="button-container">
46 +
						<button type="button" onClick={() => sendRequest()}>
47 +
							Call API
48 +
						</button>
49 +
						<a
50 +
							className="docs-link"
51 +
							target="_blank"
52 +
							href="https://bhvr.dev"
53 +
							rel="noopener"
54 +
						>
55 +
							Docs
56 +
						</a>
57 +
					</div>
58 +
					{data && (
59 +
						<pre className="response">
60 +
							<code>
61 +
								Message: {data.message} <br />
62 +
								Success: {data.success.toString()}
63 +
							</code>
64 +
						</pre>
65 +
					)}
66 +
				</div>
67 +
			</ClientOnly>
68 +
		</div>
69 +
	);
70 +
}
71 +
72 +
export default Home;
src/templates/extras/client/src/components/Home.tsx/Home-with-reactroutermpa.tsx (added) +72 −0
1 +
import { useState } from "react";
2 +
import beaver from "../assets/beaver.svg";
3 +
import type { ApiResponse } from "shared";
4 +
import "../App.css";
5 +
import ClientOnly from "./ClientOnly";
6 +
7 +
const SERVER_URL = import.meta.env.DEV ? "http://localhost:3000" : "/api";
8 +
9 +
function Home() {
10 +
	const [data, setData] = useState<ApiResponse | undefined>();
11 +
12 +
	async function sendRequest() {
13 +
		try {
14 +
			const req = await fetch(`${SERVER_URL}/hello`);
15 +
			const res: ApiResponse = await req.json();
16 +
			setData(res);
17 +
		} catch (error) {
18 +
			console.log(error);
19 +
		}
20 +
	}
21 +
22 +
	return (
23 +
		<div
24 +
			style={{
25 +
				maxWidth: "1280px",
26 +
				margin: "0 auto",
27 +
				padding: "2rem",
28 +
				textAlign: "center",
29 +
			}}
30 +
		>
31 +
			<div>
32 +
				<a
33 +
					href="https://github.com/stevedylandev/bhvr"
34 +
					target="_blank"
35 +
					rel="noopener"
36 +
				>
37 +
					<img src={beaver} className="logo" alt="beaver logo" />
38 +
				</a>
39 +
			</div>
40 +
			<h1>bhvr</h1>
41 +
			<h2>Bun + Hono + Vite + React</h2>
42 +
			<p>A typesafe fullstack monorepo</p>
43 +
			<ClientOnly>
44 +
				<div className="card">
45 +
					<div className="button-container">
46 +
						<button type="button" onClick={sendRequest}>
47 +
							Call API
48 +
						</button>
49 +
						<a
50 +
							className="docs-link"
51 +
							target="_blank"
52 +
							href="https://bhvr.dev"
53 +
							rel="noopener"
54 +
						>
55 +
							Docs
56 +
						</a>
57 +
					</div>
58 +
					{data && (
59 +
						<pre className="response">
60 +
							<code>
61 +
								Message: {data.message} <br />
62 +
								Success: {data.success.toString()}
63 +
							</code>
64 +
						</pre>
65 +
					)}
66 +
				</div>
67 +
			</ClientOnly>
68 +
		</div>
69 +
	);
70 +
}
71 +
72 +
export default Home;
src/templates/extras/client/src/root.tsx/root.tsx (added) +61 −0
1 +
import {
2 +
	isRouteErrorResponse,
3 +
	Links,
4 +
	Meta,
5 +
	Outlet,
6 +
	Scripts,
7 +
	ScrollRestoration,
8 +
} from "react-router";
9 +
10 +
import "./index.css";
11 +
12 +
export function Layout({ children }: { children: React.ReactNode }) {
13 +
	return (
14 +
		<html lang="en">
15 +
			<head>
16 +
				<meta charSet="utf-8" />
17 +
				<meta name="viewport" content="width=device-width, initial-scale=1" />
18 +
				<Meta />
19 +
				<Links />
20 +
			</head>
21 +
			<body>
22 +
				{children}
23 +
				<ScrollRestoration />
24 +
				<Scripts />
25 +
			</body>
26 +
		</html>
27 +
	);
28 +
}
29 +
30 +
export default function App() {
31 +
	return <Outlet />;
32 +
}
33 +
34 +
export function ErrorBoundary({ error }: any) {
35 +
	let message = "Oops!";
36 +
	let details = "An unexpected error occurred.";
37 +
	let stack: string | undefined;
38 +
39 +
	if (isRouteErrorResponse(error)) {
40 +
		message = error.status === 404 ? "404" : "Error";
41 +
		details =
42 +
			error.status === 404
43 +
				? "The requested page could not be found."
44 +
				: error.statusText || details;
45 +
	} else if (import.meta.env.DEV && error && error instanceof Error) {
46 +
		details = error.message;
47 +
		stack = error.stack;
48 +
	}
49 +
50 +
	return (
51 +
		<main className="pt-16 p-4 container mx-auto">
52 +
			<h1>{message}</h1>
53 +
			<p>{details}</p>
54 +
			{stack && (
55 +
				<pre className="w-full p-4 overflow-x-auto">
56 +
					<code>{stack}</code>
57 +
				</pre>
58 +
			)}
59 +
		</main>
60 +
	);
61 +
}
src/templates/extras/client/src/routes.ts/routes.ts (added) +3 −0
1 +
import { type RouteConfig, index } from "@react-router/dev/routes";
2 +
3 +
export default [index("routes/home.tsx")] satisfies RouteConfig;
src/templates/extras/client/src/routes/home.tsx/home.tsx (added) +12 −0
1 +
import Home from "../components/Home";
2 +
3 +
export function meta() {
4 +
	return [
5 +
		{ title: "bhvr" },
6 +
		{ name: "description", content: "A typesafe fullstack monorepo" },
7 +
	];
8 +
}
9 +
10 +
export default function HomePage() {
11 +
	return <Home />;
12 +
}
src/templates/extras/client/tsconfig.app.json/tsconfig.app.json (added) +32 −0
1 +
{
2 +
	"compilerOptions": {
3 +
		"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 +
		"target": "ES2020",
5 +
		"useDefineForClassFields": true,
6 +
		"lib": ["ES2020", "DOM", "DOM.Iterable"],
7 +
		"module": "ESNext",
8 +
		"skipLibCheck": true,
9 +
		"moduleResolution": "bundler",
10 +
		"allowImportingTsExtensions": true,
11 +
		"isolatedModules": true,
12 +
		"moduleDetection": "force",
13 +
		"noEmit": true,
14 +
		"jsx": "react-jsx",
15 +
		"rootDirs": [".", "./.react-router/types"],
16 +
		"paths": {
17 +
			"~/*": ["./src/*"],
18 +
			"@/*": ["./src/*"]
19 +
		},
20 +
		"strict": true,
21 +
		"noUnusedLocals": true,
22 +
		"noUnusedParameters": true,
23 +
		"noFallthroughCasesInSwitch": true,
24 +
		"noUncheckedSideEffectImports": true
25 +
	},
26 +
	"include": [
27 +
		"**/*",
28 +
		"**/.server/**/*",
29 +
		"**/.client/**/*",
30 +
		".react-router/types/**/*"
31 +
	]
32 +
}
src/templates/extras/client/vite.config.ts/vite.config-with-reactroutermpa-shadcn-tailwind.ts (added) +14 −0
1 +
import { reactRouter } from "@react-router/dev/vite";
2 +
import tailwindcss from "@tailwindcss/vite";
3 +
import { defineConfig } from "vite";
4 +
import path from "node:path";
5 +
import tsconfigPaths from "vite-tsconfig-paths";
6 +
7 +
export default defineConfig({
8 +
	plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
9 +
	resolve: {
10 +
		alias: {
11 +
			"@": path.resolve(__dirname, "./src"),
12 +
		},
13 +
	},
14 +
});
src/templates/extras/client/vite.config.ts/vite.config-with-reactroutermpa-tailwind.ts (added) +8 −0
1 +
import { reactRouter } from "@react-router/dev/vite";
2 +
import tailwindcss from "@tailwindcss/vite";
3 +
import { defineConfig } from "vite";
4 +
import tsconfigPaths from "vite-tsconfig-paths";
5 +
6 +
export default defineConfig({
7 +
	plugins: [tailwindcss(), reactRouter(), tsconfigPaths()],
8 +
});
src/templates/extras/client/vite.config.ts/vite.config-with-reactroutermpa.ts (added) +7 −0
1 +
import { reactRouter } from "@react-router/dev/vite";
2 +
import { defineConfig } from "vite";
3 +
import tsconfigPaths from "vite-tsconfig-paths";
4 +
5 +
export default defineConfig({
6 +
	plugins: [reactRouter(), tsconfigPaths()],
7 +
});
src/types.ts +1 −1
14 14
	shadcn?: boolean;
15 15
	rpc?: boolean;
16 16
	linter?: "eslint" | "biome";
17 -
	router?: "none" | "reactrouter" | "tanstackrouter";
17 +
	router?: "none" | "reactrouter" | "reactroutermpa" | "tanstackrouter";
18 18
	tanstackQuery?: boolean;
19 19
};
20 20