create-aron-app 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/.cursor/worktrees.json +3 -0
  2. package/README.md +24 -31
  3. package/dist/index.js +38 -49
  4. package/package.json +3 -7
  5. package/templates/.cursor/rules/backend.mdc +112 -0
  6. package/templates/.cursor/rules/coding_standards.mdc +85 -4
  7. package/templates/.cursor/rules/frontend_architecture.mdc +334 -0
  8. package/templates/.env.example +6 -0
  9. package/templates/apps/{react-router → web}/.react-router/types/+routes.ts +11 -6
  10. package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/[id].ts +5 -2
  11. package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/index.ts +5 -2
  12. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/layout.ts +65 -0
  13. package/templates/apps/{react-router → web}/project.json +11 -4
  14. package/templates/apps/{react-router → web}/react-router.config.ts +1 -1
  15. package/templates/apps/web/src/libs/convex_query_client.ts +11 -0
  16. package/templates/apps/web/src/libs/react_query_client.ts +17 -0
  17. package/templates/apps/web/src/libs/server/auth.ts +32 -0
  18. package/templates/apps/web/src/libs/server/protected.ts +17 -0
  19. package/templates/apps/web/src/providers/api_auth_provider.tsx +26 -0
  20. package/templates/apps/web/src/providers/global_provider.tsx +28 -0
  21. package/templates/apps/web/src/providers/navigation_loading_bar_provider.tsx +72 -0
  22. package/templates/apps/web/src/root.tsx +68 -0
  23. package/templates/apps/web/src/routes/(dashboard)/(todos)/[id].tsx +33 -0
  24. package/templates/apps/web/src/routes/(dashboard)/(todos)/index.tsx +26 -0
  25. package/templates/apps/web/src/routes/(dashboard)/(todos)/layout.tsx +9 -0
  26. package/templates/apps/{react-router → web}/src/routes/(dashboard)/index.tsx +3 -2
  27. package/templates/apps/web/src/routes/(dashboard)/layout.tsx +20 -0
  28. package/templates/apps/{react-router → web}/src/routes.ts +4 -2
  29. package/templates/apps/{react-router → web}/src/surfaces/sidebar/install.tsx +1 -5
  30. package/templates/apps/{react-router → web}/src/surfaces/sidebar/layout.tsx +24 -15
  31. package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos.tsx +1 -1
  32. package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos_controller.ts +1 -1
  33. package/templates/apps/web/src/surfaces/todos/all_todos/bootstrap.ts +21 -0
  34. package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/bootstrap.ts +4 -5
  35. package/templates/apps/{nextjs → web}/src/surfaces/todos/single_todo/header/create.tsx +4 -6
  36. package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo_controller.ts +1 -1
  37. package/templates/components.json +20 -0
  38. package/templates/gitignore +60 -0
  39. package/templates/nx.json +0 -11
  40. package/templates/package.json +2 -3
  41. package/templates/shared/assets/src/styles/global.css +14 -8
  42. package/templates/shared/ui/src/base/collapsible.tsx +31 -0
  43. package/templates/shared/ui/src/base/hover-card.tsx +42 -0
  44. package/templates/shared/ui/src/base/input-group.tsx +168 -0
  45. package/templates/shared/ui/src/base/panel.tsx +93 -0
  46. package/templates/shared/ui/src/hooks/use_mobile.tsx +1 -1
  47. package/templates/shared/ui/src/hooks/use_query_params.tsx +6 -7
  48. package/templates/shared/utils/src/convex.ts +2 -1
  49. package/templates/tsconfig.base.json +2 -4
  50. package/templates/.cursor/commands/builder.md +0 -0
  51. package/templates/.cursor/rules/api_architecture.mdc +0 -262
  52. package/templates/.cursor/rules/convex_rules.mdc +0 -331
  53. package/templates/.cursor/rules/frontend_architecture_core.mdc +0 -495
  54. package/templates/.cursor/rules/frontend_architecture_nextjs.mdc +0 -458
  55. package/templates/.cursor/rules/frontend_architecture_reactrouter.mdc +0 -473
  56. package/templates/.github/workflows/ci.yml +0 -29
  57. package/templates/apps/api/tsconfig.json +0 -23
  58. package/templates/apps/nextjs/.env.example +0 -10
  59. package/templates/apps/nextjs/index.d.ts +0 -6
  60. package/templates/apps/nextjs/next-env.d.ts +0 -5
  61. package/templates/apps/nextjs/next.config.js +0 -22
  62. package/templates/apps/nextjs/postcss.config.js +0 -17
  63. package/templates/apps/nextjs/project.json +0 -22
  64. package/templates/apps/nextjs/src/app/(auth)/layout.tsx +0 -21
  65. package/templates/apps/nextjs/src/app/(auth)/not-allowed/page.tsx +0 -23
  66. package/templates/apps/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +0 -15
  67. package/templates/apps/nextjs/src/app/(dashboard)/layout.tsx +0 -22
  68. package/templates/apps/nextjs/src/app/(dashboard)/page.tsx +0 -12
  69. package/templates/apps/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +0 -26
  70. package/templates/apps/nextjs/src/app/(dashboard)/todos/page.tsx +0 -19
  71. package/templates/apps/nextjs/src/app/app.css +0 -3
  72. package/templates/apps/nextjs/src/app/layout.tsx +0 -26
  73. package/templates/apps/nextjs/src/middleware.ts +0 -18
  74. package/templates/apps/nextjs/src/providers/convex_provider.tsx +0 -44
  75. package/templates/apps/nextjs/src/surfaces/home/home.tsx +0 -27
  76. package/templates/apps/nextjs/src/surfaces/home/layout.tsx +0 -44
  77. package/templates/apps/nextjs/src/surfaces/home/main/create.tsx +0 -34
  78. package/templates/apps/nextjs/src/surfaces/sidebar/install.tsx +0 -23
  79. package/templates/apps/nextjs/src/surfaces/sidebar/layout.tsx +0 -118
  80. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/create.tsx +0 -19
  81. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_config.ts +0 -22
  82. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -25
  83. package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
  84. package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -33
  85. package/templates/apps/nextjs/src/surfaces/sidebar/sidebar.tsx +0 -23
  86. package/templates/apps/nextjs/src/surfaces/sidebar/ui/sidebar_nav_link.tsx +0 -39
  87. package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/create.tsx +0 -28
  88. package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -42
  89. package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos.tsx +0 -29
  90. package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos_controller.ts +0 -61
  91. package/templates/apps/nextjs/src/surfaces/todos/all_todos/bootstrap.ts +0 -21
  92. package/templates/apps/nextjs/src/surfaces/todos/all_todos/header/create.tsx +0 -23
  93. package/templates/apps/nextjs/src/surfaces/todos/all_todos/install.tsx +0 -23
  94. package/templates/apps/nextjs/src/surfaces/todos/all_todos/layout.tsx +0 -44
  95. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/create.tsx +0 -49
  96. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/main.tsx +0 -70
  97. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -56
  98. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -99
  99. package/templates/apps/nextjs/src/surfaces/todos/single_todo/bootstrap.ts +0 -32
  100. package/templates/apps/nextjs/src/surfaces/todos/single_todo/header/header.tsx +0 -22
  101. package/templates/apps/nextjs/src/surfaces/todos/single_todo/install.tsx +0 -27
  102. package/templates/apps/nextjs/src/surfaces/todos/single_todo/layout.tsx +0 -55
  103. package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/create.tsx +0 -38
  104. package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/main.tsx +0 -49
  105. package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo.tsx +0 -29
  106. package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo_controller.ts +0 -13
  107. package/templates/apps/nextjs/src/utils/auth.ts +0 -18
  108. package/templates/apps/nextjs/src/utils/convex.ts +0 -11
  109. package/templates/apps/nextjs/src/utils/font.ts +0 -9
  110. package/templates/apps/nextjs/tsconfig.json +0 -42
  111. package/templates/apps/react-router/src/providers/api_auth_provider.tsx +0 -40
  112. package/templates/apps/react-router/src/root.tsx +0 -37
  113. package/templates/apps/react-router/src/routes/(dashboard)/layout.tsx +0 -37
  114. package/templates/apps/react-router/src/routes/(dashboard)/todos/[id].tsx +0 -19
  115. package/templates/apps/react-router/src/routes/(dashboard)/todos/index.tsx +0 -19
  116. package/templates/apps/react-router/src/surfaces/home/bootstrap.ts +0 -9
  117. package/templates/apps/react-router/src/surfaces/home/install.tsx +0 -17
  118. package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
  119. package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -31
  120. package/templates/apps/react-router/src/surfaces/todos/all_todos/bootstrap.ts +0 -18
  121. package/templates/apps/react-router/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -11
  122. package/templates/apps/react-router/src/surfaces/todos/single_todo/header/create.tsx +0 -32
  123. package/templates/apps/react-router/tsconfig.json +0 -20
  124. package/templates/biome.json +0 -121
  125. package/templates/bun.lock +0 -3187
  126. package/templates/emails/tsconfig.json +0 -5
  127. package/templates/shared/assets/tsconfig.json +0 -5
  128. package/templates/shared/ui/tsconfig.json +0 -8
  129. package/templates/shared/utils/tsconfig.json +0 -5
  130. /package/templates/apps/{react-router → web}/.env.example +0 -0
  131. /package/templates/apps/{react-router → web}/.react-router/types/+future.ts +0 -0
  132. /package/templates/apps/{react-router → web}/.react-router/types/+server-build.d.ts +0 -0
  133. /package/templates/apps/{react-router → web}/.react-router/types/src/+types/root.ts +0 -0
  134. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/+types/layout.ts +0 -0
  135. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/sign-in/+types/index.ts +0 -0
  136. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/index.ts +0 -0
  137. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/layout.ts +0 -0
  138. /package/templates/apps/{react-router → web}/postcss.config.js +0 -0
  139. /package/templates/apps/{react-router → web}/public/favicon.ico +0 -0
  140. /package/templates/apps/{react-router → web}/src/app.css +0 -0
  141. /package/templates/apps/{react-router → web}/src/components/error_boundary.tsx +0 -0
  142. /package/templates/apps/{react-router → web}/src/routes/(auth)/layout.tsx +0 -0
  143. /package/templates/apps/{react-router → web}/src/routes/(auth)/sign-in/index.tsx +0 -0
  144. /package/templates/apps/{nextjs → web}/src/surfaces/home/bootstrap.ts +0 -0
  145. /package/templates/apps/{react-router → web}/src/surfaces/home/home.tsx +0 -0
  146. /package/templates/apps/{nextjs → web}/src/surfaces/home/install.tsx +0 -0
  147. /package/templates/apps/{react-router → web}/src/surfaces/home/layout.tsx +0 -0
  148. /package/templates/apps/{react-router → web}/src/surfaces/home/main/create.tsx +0 -0
  149. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/create.tsx +0 -0
  150. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -0
  151. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/sidebar.tsx +0 -0
  152. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/create.tsx +0 -0
  153. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -0
  154. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/header/create.tsx +0 -0
  155. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/install.tsx +0 -0
  156. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/layout.tsx +0 -0
  157. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/create.tsx +0 -0
  158. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/main.tsx +0 -0
  159. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -0
  160. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -0
  161. /package/templates/apps/{nextjs → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -0
  162. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/header/header.tsx +0 -0
  163. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/install.tsx +0 -0
  164. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/layout.tsx +0 -0
  165. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/create.tsx +0 -0
  166. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/main.tsx +0 -0
  167. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo.tsx +0 -0
  168. /package/templates/apps/{react-router → web}/vite.config.ts +0 -0
  169. /package/templates/emails/{welcome_email.tsx → src/welcome_email.tsx} +0 -0
@@ -0,0 +1,32 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { getAuth } from "@clerk/react-router/server";
6
+ import { ConvexHttpClient } from "convex/browser";
7
+ import type { LoaderFunctionArgs } from "react-router";
8
+ import { redirect } from "react-router";
9
+
10
+ const CONVEX_URL = process.env.VITE_CONVEX_URL;
11
+
12
+ if (!CONVEX_URL) {
13
+ throw new Error("VITE_CONVEX_URL is not set");
14
+ }
15
+
16
+ export const createConvexClient = async (args: LoaderFunctionArgs): Promise<ConvexHttpClient> => {
17
+ try {
18
+ const auth = await getAuth(args);
19
+ if (!auth.isAuthenticated) {
20
+ throw redirect(`/sign-in?redirect_url=${encodeURIComponent(args.request.url)}`);
21
+ }
22
+ const token = await auth.getToken({ template: "convex" });
23
+ const client = new ConvexHttpClient(CONVEX_URL);
24
+ if (token) {
25
+ client.setAuth(token);
26
+ }
27
+ return client;
28
+ } catch (error) {
29
+ console.error(error);
30
+ throw error;
31
+ }
32
+ };
@@ -0,0 +1,17 @@
1
+ import { getAuth } from "@clerk/react-router/server";
2
+ import { type MiddlewareFunction, redirect } from "react-router";
3
+
4
+ const PUBLIC_ROUTES = ["/sign-in"];
5
+
6
+ export const protectedRoute: MiddlewareFunction<Response> = async ({ request, context }, next) => {
7
+ const { pathname } = new URL(request.url);
8
+ const isPublic = PUBLIC_ROUTES.some((route) => pathname.startsWith(route));
9
+
10
+ if (!isPublic) {
11
+ const auth = await getAuth({ request, context } as Parameters<typeof getAuth>[0]);
12
+ if (!auth.isAuthenticated) {
13
+ throw redirect(`/sign-in?redirect_url=${encodeURIComponent(request.url)}`);
14
+ }
15
+ }
16
+ return next();
17
+ };
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { ClerkProvider, useAuth } from "@clerk/react-router";
6
+ import { QueryClientProvider } from "@tanstack/react-query";
7
+ import type { Route } from "apps/web/.react-router/types/src/+types/root";
8
+ import { ConvexProviderWithClerk } from "convex/react-clerk";
9
+
10
+ import { convex } from "@/web/libs/convex_query_client";
11
+ import { reactQueryClient } from "@/web/libs/react_query_client";
12
+
13
+ type ApiAuthProviderProps = {
14
+ children: React.ReactNode;
15
+ loaderData: Route.ComponentProps["loaderData"];
16
+ };
17
+
18
+ export const ApiAuthProvider = ({ children, loaderData }: ApiAuthProviderProps) => {
19
+ return (
20
+ <ClerkProvider signInUrl="/sign-in" loaderData={loaderData}>
21
+ <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
22
+ <QueryClientProvider client={reactQueryClient}>{children}</QueryClientProvider>
23
+ </ConvexProviderWithClerk>
24
+ </ClerkProvider>
25
+ );
26
+ };
@@ -0,0 +1,28 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { ReactNode } from "react";
6
+ import { createContext, useContext } from "react";
7
+
8
+ export type GlobalContextValue = Record<string, never>;
9
+
10
+ const GlobalContext = createContext<GlobalContextValue | undefined>(undefined);
11
+
12
+ type GlobalProviderProps = {
13
+ children: ReactNode;
14
+ };
15
+
16
+ export const GlobalProvider = ({ children }: GlobalProviderProps) => {
17
+ return <GlobalContext.Provider value={{}}>{children}</GlobalContext.Provider>;
18
+ };
19
+
20
+ export const useGlobal = (): GlobalContextValue => {
21
+ const context = useContext(GlobalContext);
22
+
23
+ if (context === undefined) {
24
+ throw new Error("useGlobal must be used within a GlobalProvider");
25
+ }
26
+
27
+ return context;
28
+ };
@@ -0,0 +1,72 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { createContext, type ReactNode, useContext, useEffect, useMemo, useRef } from "react";
6
+ import { useNavigation } from "react-router";
7
+ import { LoadingBarContainer, useLoadingBar } from "react-top-loading-bar";
8
+
9
+ export type NavigationLoadingBarApi = {
10
+ start: () => void;
11
+ complete: () => void;
12
+ };
13
+
14
+ const NavigationLoadingBarContext = createContext<NavigationLoadingBarApi | null>(null);
15
+
16
+ type NavigationLoadingBarProviderProps = {
17
+ children: ReactNode;
18
+ };
19
+
20
+ const NavigationLoadingBarInner = ({ children }: NavigationLoadingBarProviderProps) => {
21
+ const navigation = useNavigation();
22
+
23
+ const { start, complete } = useLoadingBar({
24
+ color: "hsl(var(--primary))",
25
+ height: 2,
26
+ shadow: true,
27
+ });
28
+
29
+ const api = useMemo((): NavigationLoadingBarApi => {
30
+ return { start, complete };
31
+ }, [start, complete]);
32
+
33
+ const skippedInitialIdle = useRef(false);
34
+
35
+ useEffect(() => {
36
+ if (navigation.state !== "idle") {
37
+ start();
38
+ return;
39
+ }
40
+
41
+ if (!skippedInitialIdle.current) {
42
+ skippedInitialIdle.current = true;
43
+ return;
44
+ }
45
+
46
+ complete();
47
+ }, [navigation.state, start, complete]);
48
+
49
+ return (
50
+ <NavigationLoadingBarContext.Provider value={api}>
51
+ {children}
52
+ </NavigationLoadingBarContext.Provider>
53
+ );
54
+ };
55
+
56
+ export const NavigationLoadingBarProvider = ({ children }: NavigationLoadingBarProviderProps) => {
57
+ return (
58
+ <LoadingBarContainer>
59
+ <NavigationLoadingBarInner>{children}</NavigationLoadingBarInner>
60
+ </LoadingBarContainer>
61
+ );
62
+ };
63
+
64
+ export const useNavigationLoadingBar = (): NavigationLoadingBarApi => {
65
+ const ctx = useContext(NavigationLoadingBarContext);
66
+
67
+ if (!ctx) {
68
+ throw new Error("useNavigationLoadingBar must be used within NavigationLoadingBarProvider");
69
+ }
70
+
71
+ return ctx;
72
+ };
@@ -0,0 +1,68 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import "./app.css";
6
+
7
+ import { clerkMiddleware, rootAuthLoader } from "@clerk/react-router/server";
8
+ import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
9
+ import { Toaster } from "sonner";
10
+
11
+ import { protectedRoute } from "@/web/libs/server/protected";
12
+ import { ApiAuthProvider } from "@/web/providers/api_auth_provider";
13
+ import { GlobalProvider } from "@/web/providers/global_provider";
14
+ import { NavigationLoadingBarProvider } from "@/web/providers/navigation_loading_bar_provider";
15
+
16
+ import type { Route } from "./+types/root";
17
+
18
+ export const middleware: Route.MiddlewareFunction[] = [clerkMiddleware(), protectedRoute];
19
+ export const loader = (args: Route.LoaderArgs) => rootAuthLoader(args);
20
+
21
+ export const links: Route.LinksFunction = () => {
22
+ return [
23
+ {
24
+ rel: "preconnect",
25
+ href: "https://fonts.googleapis.com",
26
+ },
27
+ {
28
+ rel: "preconnect",
29
+ href: "https://fonts.gstatic.com",
30
+ crossOrigin: "anonymous",
31
+ },
32
+ {
33
+ rel: "stylesheet",
34
+ href: "https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap",
35
+ },
36
+ ];
37
+ };
38
+
39
+ export function Layout({ children }: { children: React.ReactNode }) {
40
+ return (
41
+ <html lang="en">
42
+ <head>
43
+ <meta charSet="utf-8" />
44
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
45
+ <Meta />
46
+ <Links />
47
+ </head>
48
+ <body>
49
+ {children}
50
+ <ScrollRestoration />
51
+ <Scripts />
52
+ </body>
53
+ </html>
54
+ );
55
+ }
56
+
57
+ export default function App({ loaderData }: Route.ComponentProps) {
58
+ return (
59
+ <NavigationLoadingBarProvider>
60
+ <ApiAuthProvider loaderData={loaderData}>
61
+ <GlobalProvider>
62
+ <Outlet />
63
+ <Toaster richColors position="top-center" />
64
+ </GlobalProvider>
65
+ </ApiAuthProvider>
66
+ </NavigationLoadingBarProvider>
67
+ );
68
+ }
@@ -0,0 +1,33 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { createConvexClient } from "@/web/libs/server/auth";
6
+ import { bootstrapSingleTodo } from "@/web/surfaces/todos/single_todo/bootstrap";
7
+ import { SingleTodo } from "@/web/surfaces/todos/single_todo/single_todo";
8
+
9
+ import type { Route } from "./+types/[id]";
10
+
11
+ export async function loader(args: Route.LoaderArgs) {
12
+ const client = await createConvexClient(args);
13
+ return bootstrapSingleTodo({ params: args.params, client });
14
+ }
15
+
16
+ export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
17
+ return serverLoader();
18
+ }
19
+
20
+ export function meta({ loaderData: bootstrap }: Route.MetaArgs) {
21
+ if (!bootstrap) {
22
+ return [{ title: "Todo" }];
23
+ }
24
+
25
+ return [
26
+ { title: `${bootstrap.todo.title} - Todos` },
27
+ { name: "description", content: bootstrap.todo.description ?? "Todo details" },
28
+ ];
29
+ }
30
+
31
+ export default function TodoPage({ loaderData: bootstrap }: Route.ComponentProps) {
32
+ return <SingleTodo bootstrap={bootstrap} />;
33
+ }
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { createConvexClient } from "@/web/libs/server/auth";
6
+ import { AllTodos } from "@/web/surfaces/todos/all_todos/all_todos";
7
+ import { bootstrapAllTodos } from "@/web/surfaces/todos/all_todos/bootstrap";
8
+
9
+ import type { Route } from "./+types/index";
10
+
11
+ export async function loader(args: Route.LoaderArgs) {
12
+ const client = await createConvexClient(args);
13
+ return bootstrapAllTodos({ client });
14
+ }
15
+
16
+ export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
17
+ return serverLoader();
18
+ }
19
+
20
+ export function meta(_args: Route.MetaArgs) {
21
+ return [{ title: "Todos" }, { name: "description", content: "Manage your todos" }];
22
+ }
23
+
24
+ export default function TodosPage({ loaderData: bootstrap }: Route.ComponentProps) {
25
+ return <AllTodos bootstrap={bootstrap} />;
26
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import { Outlet } from "react-router";
6
+
7
+ export default function TodosLayout() {
8
+ return <Outlet />;
9
+ }
@@ -2,9 +2,10 @@
2
2
  * Copyright (c) Aron Weston 2026.
3
3
  */
4
4
 
5
+ import { bootstrapHome } from "@/web/surfaces/home/bootstrap";
6
+ import { Home } from "@/web/surfaces/home/home";
7
+
5
8
  import type { Route } from "./+types/index";
6
- import { Home } from "../../surfaces/home/home";
7
- import { bootstrapHome } from "../../surfaces/home/bootstrap";
8
9
 
9
10
  export async function clientLoader(_args: Route.ClientLoaderArgs) {
10
11
  return bootstrapHome();
@@ -0,0 +1,20 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { CSSProperties } from "react";
6
+ import { Outlet } from "react-router";
7
+
8
+ import { SidebarInset, SidebarProvider } from "@/ui/base/side_bar";
9
+ import { Sidebar } from "@/web/surfaces/sidebar/sidebar";
10
+
11
+ export default function DashboardLayout() {
12
+ return (
13
+ <SidebarProvider style={{ "--sidebar-width": "265px" } as CSSProperties} defaultOpen={true}>
14
+ <Sidebar />
15
+ <SidebarInset className="bg-white">
16
+ <Outlet />
17
+ </SidebarInset>
18
+ </SidebarProvider>
19
+ );
20
+ }
@@ -5,8 +5,10 @@ export default [
5
5
  layout("./routes/(dashboard)/layout.tsx", [
6
6
  index("./routes/(dashboard)/index.tsx"),
7
7
  ...prefix("todos", [
8
- index("./routes/(dashboard)/todos/index.tsx"),
9
- route(":id", "./routes/(dashboard)/todos/[id].tsx"),
8
+ layout("./routes/(dashboard)/(todos)/layout.tsx", [
9
+ index("./routes/(dashboard)/(todos)/index.tsx"),
10
+ route(":id", "./routes/(dashboard)/(todos)/[id].tsx"),
11
+ ]),
10
12
  ]),
11
13
  ]),
12
14
  ] satisfies RouteConfig;
@@ -10,11 +10,7 @@ type InstallSidebarOpts = {
10
10
 
11
11
  export const installSidebar = ({ layout }: InstallSidebarOpts) => {
12
12
  import("@/web/surfaces/sidebar/nav_main/create").then(({ createNavMain }) => {
13
- layout.setNavMain(createNavMain());
14
- });
15
-
16
- import("@/web/surfaces/sidebar/nav_secondary/create").then(({ createNavSecondary }) => {
17
- layout.setNavSecondary(createNavSecondary());
13
+ layout.setNavLinks(createNavMain());
18
14
  });
19
15
 
20
16
  import("@/web/surfaces/sidebar/user_menu/create").then(({ createUserMenu }) => {
@@ -2,6 +2,7 @@
2
2
  * Copyright (c) Aron Weston 2026.
3
3
  */
4
4
 
5
+ import { Plus } from "lucide-react";
5
6
  import { action, observable } from "mobx";
6
7
  import { observer } from "mobx-react-lite";
7
8
  import type { ComponentType } from "react";
@@ -20,22 +21,14 @@ import { Skeleton } from "@/ui/base/skeleton";
20
21
 
21
22
  export class SidebarLayoutController {
22
23
  @observable.ref
23
- accessor NavMain: ComponentType | undefined = undefined;
24
-
25
- @observable.ref
26
- accessor NavSecondary: ComponentType | undefined = undefined;
24
+ accessor NavLinks: ComponentType | undefined = undefined;
27
25
 
28
26
  @observable.ref
29
27
  accessor UserMenu: ComponentType | undefined = undefined;
30
28
 
31
29
  @action
32
- setNavMain(NavMain: ComponentType) {
33
- this.NavMain = NavMain;
34
- }
35
-
36
- @action
37
- setNavSecondary(NavSecondary: ComponentType) {
38
- this.NavSecondary = NavSecondary;
30
+ setNavLinks(NavLinks: ComponentType) {
31
+ this.NavLinks = NavLinks;
39
32
  }
40
33
 
41
34
  @action
@@ -81,8 +74,7 @@ export const createSidebarLayout = () => {
81
74
  return {
82
75
  layout,
83
76
  Layout: observer(() => {
84
- const NavMain = layout.NavMain;
85
- const NavSecondary = layout.NavSecondary;
77
+ const NavLinks = layout.NavLinks;
86
78
  const UserMenu = layout.UserMenu;
87
79
 
88
80
  return (
@@ -95,10 +87,27 @@ export const createSidebarLayout = () => {
95
87
  <BrandLink />
96
88
  </SidebarHeader>
97
89
  <SidebarContent className="flex flex-1 flex-col gap-4 overflow-hidden px-2">
98
- {NavMain ? <NavMain /> : <Skeleton className="h-24 bg-sidebar-accent" />}
90
+ <SidebarMenu>
91
+ <SidebarMenuItem>
92
+ <SidebarMenuButton
93
+ size="sm"
94
+ tooltip="Create a new todo"
95
+ className="bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90 active:bg-sidebar-primary/80 justify-center font-semibold shadow-sm"
96
+ asChild
97
+ >
98
+ <Link
99
+ to="/todos"
100
+ className="flex items-center gap-2"
101
+ >
102
+ <Plus className="size-4" />
103
+ <span>New Todo</span>
104
+ </Link>
105
+ </SidebarMenuButton>
106
+ </SidebarMenuItem>
107
+ </SidebarMenu>
99
108
  </SidebarContent>
100
109
  <SidebarFooter className="gap-2">
101
- {NavSecondary ? <NavSecondary /> : <Skeleton className="h-10 bg-sidebar-accent" />}
110
+ {NavLinks ? <NavLinks /> : <Skeleton className="h-10 bg-sidebar-accent" />}
102
111
  {UserMenu ? <UserMenu /> : <Skeleton className="h-12 bg-sidebar-accent" />}
103
112
  </SidebarFooter>
104
113
  </BaseSidebar>
@@ -13,7 +13,7 @@ export type AllTodosProps = {
13
13
  };
14
14
 
15
15
  export const AllTodos = ({ bootstrap }: AllTodosProps) => {
16
- const { layout, Layout } = useMemo(() => createLayout(), []);
16
+ const { layout, Layout } = useMemo(createLayout, []);
17
17
  const installed = useRef(false);
18
18
 
19
19
  if (!installed.current) {
@@ -6,7 +6,7 @@ import { action, observable } from "mobx";
6
6
 
7
7
  import { api } from "@/api/_generated/api";
8
8
  import type { TodoId } from "@/api/todos/types";
9
- import { convex } from "@/web/providers/api_auth_provider";
9
+ import { convex } from "@/web/libs/convex_query_client";
10
10
 
11
11
  export class AllTodosController {
12
12
  @observable
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { ConvexHttpClient } from "convex/browser";
6
+
7
+ import { api } from "@/api/_generated/api";
8
+ import type { Todo } from "@/api/todos/types";
9
+
10
+ export type AllTodosBootstrap = {
11
+ todos: Todo[];
12
+ };
13
+
14
+ export type AllTodosBootstrapArgs = {
15
+ client: ConvexHttpClient;
16
+ };
17
+
18
+ export const bootstrapAllTodos = async ({ client }: AllTodosBootstrapArgs): Promise<AllTodosBootstrap> => {
19
+ const todos = await client.query(api.todos.crud.listTodos, {});
20
+ return { todos };
21
+ };
@@ -2,15 +2,15 @@
2
2
  * Copyright (c) Aron Weston 2026.
3
3
  */
4
4
 
5
- import { convexQuery } from "@convex-dev/react-query";
5
+ import type { ConvexHttpClient } from "convex/browser";
6
6
  import { redirect } from "react-router";
7
7
 
8
8
  import { api } from "@/api/_generated/api";
9
9
  import type { Todo, TodoId } from "@/api/todos/types";
10
- import { queryClient } from "@/web/providers/api_auth_provider";
11
10
 
12
11
  export type SingleTodoBootstrapArgs = {
13
12
  params: { id?: string };
13
+ client: ConvexHttpClient;
14
14
  };
15
15
 
16
16
  export type SingleTodoBootstrap = {
@@ -20,15 +20,14 @@ export type SingleTodoBootstrap = {
20
20
 
21
21
  export const bootstrapSingleTodo = async ({
22
22
  params,
23
+ client,
23
24
  }: SingleTodoBootstrapArgs): Promise<SingleTodoBootstrap> => {
24
25
  if (!params.id) {
25
26
  throw redirect("/todos");
26
27
  }
27
28
  const todoId = params.id as TodoId;
28
29
  try {
29
- const todo = await queryClient.ensureQueryData(
30
- convexQuery(api.todos.crud.getTodo, { todoId }),
31
- );
30
+ const todo = await client.query(api.todos.crud.getTodo, { todoId });
32
31
  return { todoId, todo };
33
32
  } catch {
34
33
  throw redirect("/todos");
@@ -2,23 +2,21 @@
2
2
  * Copyright (c) Aron Weston 2026.
3
3
  */
4
4
 
5
- "use client";
6
-
7
5
  import { observer } from "mobx-react-lite";
8
- import type { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime";
6
+ import type { NavigateFunction } from "react-router";
9
7
 
10
8
  import { Header } from "@/web/surfaces/todos/single_todo/header/header";
11
9
 
12
10
  export type CreateHeaderOpts = {
13
- router: AppRouterInstance;
11
+ navigate: NavigateFunction;
14
12
  };
15
13
 
16
- export const createHeader = ({ router }: CreateHeaderOpts) => {
14
+ export const createHeader = ({ navigate }: CreateHeaderOpts) => {
17
15
  return observer(() => {
18
16
  return (
19
17
  <Header
20
18
  onBack={() => {
21
- router.push("/todos");
19
+ navigate("/todos");
22
20
  }}
23
21
  />
24
22
  );
@@ -6,7 +6,7 @@ import { action } from "mobx";
6
6
 
7
7
  import { api } from "@/api/_generated/api";
8
8
  import type { TodoId } from "@/api/todos/types";
9
- import { convex } from "@/web/providers/api_auth_provider";
9
+ import { convex } from "@/web/libs/convex_query_client";
10
10
 
11
11
  export class SingleTodoController {
12
12
  @action
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "new-york",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "shared/assets/src/styles/global.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true
11
+ },
12
+ "iconLibrary": "lucide",
13
+ "aliases": {
14
+ "components": "@/web/components",
15
+ "utils": "@/ui/base/utils",
16
+ "ui": "@/ui/base",
17
+ "lib": "@/ui/base",
18
+ "hooks": "@/ui/hooks"
19
+ }
20
+ }
@@ -0,0 +1,60 @@
1
+ # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2
+
3
+ # compiled output
4
+ dist
5
+ tmp
6
+ out-tsc
7
+
8
+ # dependencies
9
+ node_modules
10
+
11
+ # IDEs and editors
12
+ /.idea
13
+ .project
14
+ .classpath
15
+ .c9/
16
+ *.launch
17
+ .settings/
18
+ *.sublime-workspace
19
+
20
+ # IDE - VSCode
21
+ .vscode/*
22
+ !.vscode/settings.json
23
+ !.vscode/tasks.json
24
+ !.vscode/launch.json
25
+ !.vscode/extensions.json
26
+
27
+ # misc
28
+ .dist
29
+ /.sass-cache
30
+ /connect.lock
31
+ /coverage
32
+ /libpeerconnection.log
33
+ npm-debug.log
34
+ yarn-error.log
35
+ testem.log
36
+ /typings
37
+
38
+ # environment variables
39
+ .env
40
+ .env.local
41
+ .env.convex
42
+ .env.next
43
+ .env.router
44
+
45
+ # System Files
46
+ .DS_Store
47
+ Thumbs.db
48
+
49
+ .nx/cache
50
+ .nx/workspace-data
51
+
52
+ # Next.js
53
+ .next
54
+ out
55
+
56
+ .cursor/rules/nx-rules.mdc
57
+ .github/instructions/nx.instructions.md
58
+
59
+ # clerk configuration (can include secrets)
60
+ /.clerk/
package/templates/nx.json CHANGED
@@ -24,17 +24,6 @@
24
24
  "watchDepsName": "watch-deps"
25
25
  }
26
26
  }
27
- },
28
- {
29
- "plugin": "@nx/next/plugin",
30
- "options": {
31
- "startTargetName": "start",
32
- "buildTargetName": "build",
33
- "devTargetName": "dev",
34
- "serveStaticTargetName": "serve-static",
35
- "buildDepsTargetName": "build-deps",
36
- "watchDepsTargetName": "watch-deps"
37
- }
38
27
  }
39
28
  ]
40
29
  }