create-aron-app 0.1.0 → 0.1.1

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 (156) hide show
  1. package/package.json +5 -2
  2. package/templates/_base/.cursor/agents/skills/clerk/SKILL.md +89 -0
  3. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/SKILL.md +142 -0
  4. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/api-specs-context.sh +30 -0
  5. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/execute-request.sh +88 -0
  6. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-endpoint-detail.sh +165 -0
  7. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tag-endpoints.sh +208 -0
  8. package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tags.js +14 -0
  9. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/SKILL.md +157 -0
  10. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-in.md +224 -0
  11. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-up.md +190 -0
  12. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-in.md +314 -0
  13. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-up.md +259 -0
  14. package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/show-component.md +125 -0
  15. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/SKILL.md +94 -0
  16. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md +50 -0
  17. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md +56 -0
  18. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/middleware-strategies.md +68 -0
  19. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-actions.md +56 -0
  20. package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-vs-client.md +104 -0
  21. package/templates/_base/.cursor/agents/skills/clerk/clerk-webhooks/SKILL.md +131 -0
  22. package/templates/_base/.cursor/agents/skills/shadcn/SKILL.md +241 -0
  23. package/templates/_base/.cursor/agents/skills/shadcn/agents/openai.yml +5 -0
  24. package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn-small.png +0 -0
  25. package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn.png +0 -0
  26. package/templates/_base/.cursor/agents/skills/shadcn/cli.md +257 -0
  27. package/templates/_base/.cursor/agents/skills/shadcn/customization.md +202 -0
  28. package/templates/_base/.cursor/agents/skills/shadcn/evals/evals.json +47 -0
  29. package/templates/_base/.cursor/agents/skills/shadcn/mcp.md +94 -0
  30. package/templates/_base/.cursor/agents/skills/shadcn/rules/base-vs-radix.md +306 -0
  31. package/templates/_base/.cursor/agents/skills/shadcn/rules/composition.md +195 -0
  32. package/templates/_base/.cursor/agents/skills/shadcn/rules/forms.md +192 -0
  33. package/templates/_base/.cursor/agents/skills/shadcn/rules/icons.md +101 -0
  34. package/templates/_base/.cursor/agents/skills/shadcn/rules/styling.md +162 -0
  35. package/templates/_base/.cursor/commands/builder.md +0 -0
  36. package/templates/_base/.cursor/commands/pr.md +7 -0
  37. package/templates/_base/.cursor/rules/api_architecture.mdc +268 -0
  38. package/templates/_base/.cursor/rules/coding_standards.mdc +64 -0
  39. package/templates/_base/.cursor/rules/convex_rules.mdc +675 -0
  40. package/templates/_base/.cursor/rules/frontend_rules.mdc +268 -0
  41. package/templates/_base/.env.convex.example +3 -0
  42. package/templates/_base/.github/workflows/ci.yml +29 -0
  43. package/templates/_base/.nvmrc +1 -0
  44. package/templates/_base/.vscode/settings.json +9 -0
  45. package/templates/_base/apps/api/auth.config.ts +18 -0
  46. package/templates/_base/apps/api/functions.ts +99 -0
  47. package/templates/_base/apps/api/project.json +22 -0
  48. package/templates/_base/apps/api/schema.ts +11 -0
  49. package/templates/_base/apps/api/todos/crud.ts +81 -0
  50. package/templates/_base/apps/api/todos/schema.ts +11 -0
  51. package/templates/_base/apps/api/todos/types.ts +22 -0
  52. package/templates/_base/apps/api/tsconfig.json +23 -0
  53. package/templates/_base/apps/api/types.ts +16 -0
  54. package/templates/_base/biome.json +114 -0
  55. package/templates/_base/convex.json +4 -0
  56. package/templates/_base/emails/project.json +16 -0
  57. package/templates/_base/emails/tsconfig.json +5 -0
  58. package/templates/_base/emails/welcome_email.tsx +53 -0
  59. package/templates/_base/nx.json +29 -0
  60. package/templates/_base/package.json +73 -0
  61. package/templates/_base/scripts/sync_convex_env.ts +63 -0
  62. package/templates/_base/shared/assets/image.d.ts +4 -0
  63. package/templates/_base/shared/assets/src/styles/global.css +73 -0
  64. package/templates/_base/shared/assets/tsconfig.json +5 -0
  65. package/templates/_base/shared/ui/src/base/alert_dialog.tsx +139 -0
  66. package/templates/_base/shared/ui/src/base/badge.tsx +33 -0
  67. package/templates/_base/shared/ui/src/base/basic_data_table.tsx +61 -0
  68. package/templates/_base/shared/ui/src/base/button.tsx +69 -0
  69. package/templates/_base/shared/ui/src/base/button_group.tsx +82 -0
  70. package/templates/_base/shared/ui/src/base/card.tsx +79 -0
  71. package/templates/_base/shared/ui/src/base/checkbox.tsx +26 -0
  72. package/templates/_base/shared/ui/src/base/command.tsx +165 -0
  73. package/templates/_base/shared/ui/src/base/dialog.tsx +129 -0
  74. package/templates/_base/shared/ui/src/base/dropdown_menu.tsx +232 -0
  75. package/templates/_base/shared/ui/src/base/form.tsx +161 -0
  76. package/templates/_base/shared/ui/src/base/input.tsx +129 -0
  77. package/templates/_base/shared/ui/src/base/label.tsx +19 -0
  78. package/templates/_base/shared/ui/src/base/popover.tsx +46 -0
  79. package/templates/_base/shared/ui/src/base/radio_group.tsx +49 -0
  80. package/templates/_base/shared/ui/src/base/resizable.tsx +55 -0
  81. package/templates/_base/shared/ui/src/base/scroll_area.tsx +44 -0
  82. package/templates/_base/shared/ui/src/base/select.tsx +151 -0
  83. package/templates/_base/shared/ui/src/base/separator.tsx +32 -0
  84. package/templates/_base/shared/ui/src/base/sheet.tsx +130 -0
  85. package/templates/_base/shared/ui/src/base/side_bar.tsx +688 -0
  86. package/templates/_base/shared/ui/src/base/skeleton.tsx +7 -0
  87. package/templates/_base/shared/ui/src/base/spinner.tsx +20 -0
  88. package/templates/_base/shared/ui/src/base/switch.tsx +27 -0
  89. package/templates/_base/shared/ui/src/base/table.tsx +91 -0
  90. package/templates/_base/shared/ui/src/base/text_area.tsx +21 -0
  91. package/templates/_base/shared/ui/src/base/tooltip.tsx +31 -0
  92. package/templates/_base/shared/ui/src/base/utils.ts +17 -0
  93. package/templates/_base/shared/ui/src/hooks/use_keyboard_press.tsx +48 -0
  94. package/templates/_base/shared/ui/src/hooks/use_keyboard_release.tsx +48 -0
  95. package/templates/_base/shared/ui/src/hooks/use_mobile.tsx +25 -0
  96. package/templates/_base/shared/ui/src/hooks/use_mouse_click.tsx +44 -0
  97. package/templates/_base/shared/ui/src/hooks/use_mouse_location.tsx +55 -0
  98. package/templates/_base/shared/ui/src/hooks/use_outside_click.tsx +29 -0
  99. package/templates/_base/shared/ui/src/hooks/use_query_params.tsx +33 -0
  100. package/templates/_base/shared/ui/tsconfig.json +8 -0
  101. package/templates/_base/shared/utils/src/convex.ts +3 -0
  102. package/templates/_base/shared/utils/src/time.ts +12 -0
  103. package/templates/_base/shared/utils/tsconfig.json +5 -0
  104. package/templates/_base/skills-lock.json +35 -0
  105. package/templates/_base/tsconfig.base.json +34 -0
  106. package/templates/nextjs/.env.example +8 -0
  107. package/templates/nextjs/index.d.ts +6 -0
  108. package/templates/nextjs/next-env.d.ts +5 -0
  109. package/templates/nextjs/next.config.js +22 -0
  110. package/templates/nextjs/postcss.config.js +17 -0
  111. package/templates/nextjs/project.json +22 -0
  112. package/templates/nextjs/src/app/(auth)/layout.tsx +21 -0
  113. package/templates/nextjs/src/app/(auth)/not-allowed/page.tsx +22 -0
  114. package/templates/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +15 -0
  115. package/templates/nextjs/src/app/(dashboard)/layout.tsx +27 -0
  116. package/templates/nextjs/src/app/(dashboard)/page.tsx +5 -0
  117. package/templates/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +23 -0
  118. package/templates/nextjs/src/app/(dashboard)/todos/page.tsx +16 -0
  119. package/templates/nextjs/src/app/app.css +3 -0
  120. package/templates/nextjs/src/app/layout.tsx +26 -0
  121. package/templates/nextjs/src/convex.ts +11 -0
  122. package/templates/nextjs/src/middleware.ts +18 -0
  123. package/templates/nextjs/src/providers/convex_provider.tsx +44 -0
  124. package/templates/nextjs/src/surfaces/home_surface.tsx +22 -0
  125. package/templates/nextjs/src/surfaces/todos/all_todos_surface.tsx +97 -0
  126. package/templates/nextjs/src/surfaces/todos/create_todo_sheet.tsx +107 -0
  127. package/templates/nextjs/src/surfaces/todos/single_todo_surface.tsx +90 -0
  128. package/templates/nextjs/src/ui/sidebar/nav_link.tsx +36 -0
  129. package/templates/nextjs/src/ui/sidebar/sidebar.tsx +125 -0
  130. package/templates/nextjs/src/utils/font.ts +9 -0
  131. package/templates/nextjs/tsconfig.json +42 -0
  132. package/templates/react-router/.env.example +8 -0
  133. package/templates/react-router/postcss.config.js +15 -0
  134. package/templates/react-router/project.json +23 -0
  135. package/templates/react-router/public/favicon.ico +0 -0
  136. package/templates/react-router/react-router.config.ts +9 -0
  137. package/templates/react-router/src/app.css +3 -0
  138. package/templates/react-router/src/components/error_boundary.tsx +33 -0
  139. package/templates/react-router/src/layouts/sidebar/sidebar_aside/sidebar_aside.tsx +76 -0
  140. package/templates/react-router/src/layouts/sidebar/sidebar_aside/user_menu.tsx +36 -0
  141. package/templates/react-router/src/layouts/sidebar/sidebar_layout.tsx +22 -0
  142. package/templates/react-router/src/providers/api_auth_provider.tsx +38 -0
  143. package/templates/react-router/src/root.tsx +37 -0
  144. package/templates/react-router/src/routes/auth/layout.tsx +13 -0
  145. package/templates/react-router/src/routes/auth/sign-in.tsx +13 -0
  146. package/templates/react-router/src/routes/index.tsx +9 -0
  147. package/templates/react-router/src/routes/layout.tsx +26 -0
  148. package/templates/react-router/src/routes/todos/[id].tsx +22 -0
  149. package/templates/react-router/src/routes/todos/index.tsx +13 -0
  150. package/templates/react-router/src/routes.ts +12 -0
  151. package/templates/react-router/src/surfaces/home_surface.tsx +20 -0
  152. package/templates/react-router/src/surfaces/todos/all_todos_surface.tsx +87 -0
  153. package/templates/react-router/src/surfaces/todos/create_todo_sheet.tsx +102 -0
  154. package/templates/react-router/src/surfaces/todos/single_todo_surface.tsx +81 -0
  155. package/templates/react-router/tsconfig.json +20 -0
  156. package/templates/react-router/vite.config.ts +40 -0
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "my-app:web",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "./apps/web",
5
+ "projectType": "application",
6
+ "tags": [],
7
+ "targets": {
8
+ "dev": {
9
+ "executor": "nx:run-commands",
10
+ "options": {
11
+ "command": "bunx next dev apps/web --port 5001"
12
+ }
13
+ },
14
+ "build": {
15
+ "executor": "@nx/next:build",
16
+ "options": {
17
+ "root": "apps/web",
18
+ "outputPath": "dist/apps/web"
19
+ }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ import "../app.css";
6
+
7
+ import { Toaster } from "sonner";
8
+
9
+
10
+ type RootLayoutProps = {
11
+ children: React.ReactNode;
12
+ };
13
+
14
+ export default function AuthLayout({ children }: RootLayoutProps) {
15
+ return (
16
+ <main className="flex min-h-screen flex-col justify-center items-center overflow-hidden">
17
+ {children}
18
+ <Toaster />
19
+ </main>
20
+ );
21
+ }
@@ -0,0 +1,22 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ import Link from "next/link";
6
+
7
+ export const metadata: Metadata = {
8
+ title: "Not Allowed",
9
+ description: "You are not allowed to access this page.",
10
+ };
11
+
12
+ export default async function NotAllowed() {
13
+ return (
14
+ <div className="flex flex-col items-center justify-center h-screen">
15
+ <h1 className="text-2xl font-bold">Not Allowed</h1>
16
+ <p className="text-sm text-gray-500">You are not allowed to access this page.</p>
17
+ <Link href="/sign-in" className="text-blue-500">
18
+ Continue to login page
19
+ </Link>
20
+ </div>
21
+ );
22
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ import { SignIn } from "@clerk/nextjs";
6
+ import type { Metadata } from "next";
7
+
8
+ export const metadata: Metadata = {
9
+ title: "Sign In",
10
+ description: "Sign In to your account",
11
+ };
12
+
13
+ export default function LoginPage() {
14
+ return <SignIn withSignUp={true} />;
15
+ }
@@ -0,0 +1,27 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ import { SidebarInset, SidebarProvider } from "@/ui/base/side_bar";
6
+ import { Sidebar as AppSidebar } from "@/web/ui/sidebar/sidebar";
7
+
8
+ type DashboardLayoutProps = {
9
+ children: React.ReactNode;
10
+ modal: React.ReactNode;
11
+ };
12
+
13
+ export default function DashboardLayout({ children }: DashboardLayoutProps) {
14
+ return (
15
+ <SidebarProvider
16
+ style={
17
+ {
18
+ "--sidebar-width": "calc(var(--spacing) * 45)",
19
+ "--header-height": "calc(var(--spacing) * 12)",
20
+ } as React.CSSProperties
21
+ }
22
+ >
23
+ <AppSidebar variant="inset" />
24
+ <SidebarInset>{children}</SidebarInset>
25
+ </SidebarProvider>
26
+ );
27
+ }
@@ -0,0 +1,5 @@
1
+ import { HomeSurface } from "@/web/surfaces/home_surface";
2
+
3
+ export default function HomePage() {
4
+ return <HomeSurface />;
5
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { Metadata } from "next";
6
+
7
+ import type { TodoId } from "@/api/todos/types";
8
+ import { SingleTodoSurface } from "@/web/surfaces/todos/single_todo_surface";
9
+
10
+ type SingleTodoPageProps = {
11
+ params: Promise<{ id: TodoId }>;
12
+ };
13
+
14
+ export const metadata: Metadata = {
15
+ title: "Todo",
16
+ description: "Todo",
17
+ };
18
+
19
+ export default async function SingleTodoPage({ params }: SingleTodoPageProps) {
20
+ const { id } = await params;
21
+
22
+ return <SingleTodoSurface todoId={id} />;
23
+ }
@@ -0,0 +1,16 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ import type { Metadata } from "next";
6
+
7
+ import { AllTodosSurface } from "@/web/surfaces/todos/all_todos_surface";
8
+
9
+ export const metadata: Metadata = {
10
+ title: "Todos",
11
+ description: "Todos",
12
+ };
13
+
14
+ export default function AllTodosPage() {
15
+ return <AllTodosSurface />;
16
+ }
@@ -0,0 +1,3 @@
1
+ @import "../../../../shared/assets/src/styles/global.css";
2
+ @source "../../../../shared/ui/src/**/*.{js,ts,jsx,tsx}";
3
+ @source "../../../../apps/web/src/**/*.{js,ts,jsx,tsx}";
@@ -0,0 +1,26 @@
1
+ import { ClerkProvider } from "@clerk/nextjs";
2
+ import "./app.css";
3
+
4
+ import { Toaster } from "sonner";
5
+
6
+ import { ConvexClientProvider } from "@/web/providers/convex_provider";
7
+ import { fontCn } from "@/web/utils/font";
8
+
9
+ type RootLayoutProps = {
10
+ children: React.ReactNode;
11
+ };
12
+
13
+ export default function RootLayout({ children }: RootLayoutProps) {
14
+ return (
15
+ <ClerkProvider dynamic>
16
+ <ConvexClientProvider>
17
+ <html lang="en" className={fontCn}>
18
+ <body>
19
+ {children}
20
+ <Toaster />
21
+ </body>
22
+ </html>
23
+ </ConvexClientProvider>
24
+ </ClerkProvider>
25
+ );
26
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2025.
3
+ */
4
+
5
+ import { ConvexClient } from "convex/browser";
6
+
7
+ if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
8
+ throw new Error("Missing NEXT_PUBLIC_CONVEX_URL in your .env file");
9
+ }
10
+
11
+ export const convex = new ConvexClient(process.env.NEXT_PUBLIC_CONVEX_URL);
@@ -0,0 +1,18 @@
1
+ import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
2
+
3
+ const isPublicRoute = createRouteMatcher(["/sign-in(.*)"]);
4
+
5
+ export default clerkMiddleware(async (auth, req) => {
6
+ if (!isPublicRoute(req)) {
7
+ await auth.protect();
8
+ }
9
+ });
10
+
11
+ export const config = {
12
+ matcher: [
13
+ // Skip Next.js internals and all static files, unless found in search params
14
+ "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
15
+ // Always run for API routes
16
+ "/(api|trpc)(.*)",
17
+ ],
18
+ };
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ "use client";
6
+
7
+ import { useAuth } from "@clerk/nextjs";
8
+ import { ConvexQueryClient } from "@convex-dev/react-query";
9
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
10
+ import { ConvexReactClient } from "convex/react";
11
+ import { ConvexProviderWithClerk } from "convex/react-clerk";
12
+ import { type ReactNode, useState } from "react";
13
+
14
+ if (!process.env.NEXT_PUBLIC_CONVEX_URL) {
15
+ throw new Error("Missing NEXT_PUBLIC_CONVEX_URL in your .env file");
16
+ }
17
+
18
+ export function ConvexClientProvider({ children }: { children: ReactNode }) {
19
+ const [{ convex, queryClient }] = useState(() => {
20
+ const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL as string);
21
+ const convexQueryClient = new ConvexQueryClient(convex);
22
+ const queryClient = new QueryClient({
23
+ defaultOptions: {
24
+ queries: {
25
+ queryKeyHashFn: convexQueryClient.hashFn(),
26
+ queryFn: convexQueryClient.queryFn(),
27
+ },
28
+ },
29
+ });
30
+
31
+ convexQueryClient.connect(queryClient);
32
+
33
+ return {
34
+ convex,
35
+ queryClient,
36
+ };
37
+ });
38
+
39
+ return (
40
+ <ConvexProviderWithClerk client={convex} useAuth={useAuth}>
41
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
42
+ </ConvexProviderWithClerk>
43
+ );
44
+ }
@@ -0,0 +1,22 @@
1
+ "use client";
2
+
3
+ import { useUser } from "@clerk/nextjs";
4
+ import Link from "next/link";
5
+
6
+ import { Button } from "@/ui/base/button";
7
+
8
+ export const HomeSurface = () => {
9
+ const { user } = useUser();
10
+
11
+ return (
12
+ <main className="flex flex-1 flex-col items-center justify-center gap-4 p-8">
13
+ <h1 className="text-3xl font-bold">
14
+ Welcome{user?.firstName ? `, ${user.firstName}` : ""}
15
+ </h1>
16
+ <p className="text-muted-foreground">Get started by managing your todos.</p>
17
+ <Button asChild>
18
+ <Link href="/todos">View Todos</Link>
19
+ </Button>
20
+ </main>
21
+ );
22
+ };
@@ -0,0 +1,97 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ "use client";
6
+
7
+ import { useUser } from "@clerk/nextjs";
8
+ import { convexQuery, useConvexMutation } from "@convex-dev/react-query";
9
+ import { useMutation, useQuery } from "@tanstack/react-query";
10
+ import { Trash2 } from "lucide-react";
11
+ import Link from "next/link";
12
+
13
+ import { api } from "@/api/_generated/api";
14
+ import type { Id } from "@/api/_generated/dataModel";
15
+ import { Button } from "@/ui/base/button";
16
+ import { Checkbox } from "@/ui/base/checkbox";
17
+ import { Spinner } from "@/ui/base/spinner";
18
+ import { CreateTodoSheet } from "@/web/surfaces/todos/create_todo_sheet";
19
+
20
+ export const AllTodosSurface = () => {
21
+ const { user } = useUser();
22
+
23
+ const { data: todos, isPending } = useQuery(
24
+ convexQuery(
25
+ api.todos.crud.listTodos,
26
+ user?.id ? { userId: user.id } : "skip",
27
+ ),
28
+ );
29
+
30
+ const { mutate: updateTodo } = useMutation({
31
+ mutationFn: useConvexMutation(api.todos.crud.updateTodo),
32
+ });
33
+
34
+ const { mutate: deleteTodo } = useMutation({
35
+ mutationFn: useConvexMutation(api.todos.crud.deleteTodo),
36
+ });
37
+
38
+ return (
39
+ <main className="flex flex-1 flex-col overflow-auto p-6">
40
+ <div className="flex items-center justify-between mb-6">
41
+ <h1 className="text-2xl font-semibold">Todos</h1>
42
+ <CreateTodoSheet />
43
+ </div>
44
+
45
+ {isPending && (
46
+ <div className="flex flex-1 items-center justify-center">
47
+ <Spinner />
48
+ </div>
49
+ )}
50
+
51
+ {!isPending && todos?.length === 0 && (
52
+ <div className="flex flex-1 flex-col items-center justify-center gap-2 text-muted-foreground">
53
+ <p>No todos yet.</p>
54
+ <CreateTodoSheet />
55
+ </div>
56
+ )}
57
+
58
+ {todos && todos.length > 0 && (
59
+ <ul className="flex flex-col gap-2 max-w-2xl">
60
+ {todos.map((todo) => (
61
+ <li
62
+ key={todo._id}
63
+ className="flex items-center gap-3 rounded-lg border bg-card px-4 py-3"
64
+ >
65
+ <Checkbox
66
+ checked={todo.isCompleted}
67
+ onCheckedChange={(checked) =>
68
+ updateTodo({
69
+ todoId: todo._id as Id<"todos">,
70
+ isCompleted: !!checked,
71
+ })
72
+ }
73
+ />
74
+ <Link
75
+ href={`/todos/${todo._id}`}
76
+ className="flex-1 truncate text-sm hover:underline"
77
+ style={{
78
+ textDecoration: todo.isCompleted ? "line-through" : undefined,
79
+ }}
80
+ >
81
+ {todo.title}
82
+ </Link>
83
+ <Button
84
+ variant="ghost"
85
+ size="icon"
86
+ className="size-7 shrink-0 text-muted-foreground hover:text-destructive"
87
+ onClick={() => deleteTodo({ todoId: todo._id as Id<"todos"> })}
88
+ >
89
+ <Trash2 className="size-4" />
90
+ </Button>
91
+ </li>
92
+ ))}
93
+ </ul>
94
+ )}
95
+ </main>
96
+ );
97
+ };
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ import { useConvexMutation } from "@convex-dev/react-query";
4
+ import { zodResolver } from "@hookform/resolvers/zod";
5
+ import { useMutation } from "@tanstack/react-query";
6
+ import { PlusIcon } from "lucide-react";
7
+ import { useForm } from "react-hook-form";
8
+ import { z } from "zod";
9
+
10
+ import { api } from "@/api/_generated/api";
11
+ import { Button } from "@/ui/base/button";
12
+ import { DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/ui/base/dialog";
13
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/ui/base/form";
14
+ import { Input } from "@/ui/base/input";
15
+ import { Sheet, SheetContent, SheetTrigger } from "@/ui/base/sheet";
16
+ import { TextArea } from "@/ui/base/text_area";
17
+
18
+ type CreateTodoSchema = z.infer<typeof createTodoSchema>;
19
+ const createTodoSchema = z.object({
20
+ title: z.string().min(1, "Title is required"),
21
+ description: z.string().optional(),
22
+ });
23
+
24
+ export const CreateTodoSheet = () => {
25
+ const form = useForm<CreateTodoSchema>({
26
+ resolver: zodResolver(createTodoSchema),
27
+ defaultValues: {
28
+ title: "",
29
+ description: "",
30
+ },
31
+ });
32
+
33
+ const { mutate: createTodo, isPending } = useMutation({
34
+ mutationFn: useConvexMutation(api.todos.crud.createTodo),
35
+ });
36
+
37
+ const handleSubmit = form.handleSubmit(async (values) => {
38
+ createTodo(values);
39
+ form.reset();
40
+ });
41
+
42
+ return (
43
+ <Sheet>
44
+ <SheetTrigger asChild>
45
+ <Button>
46
+ <PlusIcon className="mr-2 size-4" />
47
+ New Todo
48
+ </Button>
49
+ </SheetTrigger>
50
+ <SheetContent className="sm:max-w-[440px]">
51
+ <DialogHeader>
52
+ <DialogTitle>Create Todo</DialogTitle>
53
+ <DialogDescription>Add a new item to your todo list.</DialogDescription>
54
+ </DialogHeader>
55
+ <Form {...form}>
56
+ <form
57
+ onSubmit={handleSubmit}
58
+ className="flex flex-col gap-4 pt-4"
59
+ onKeyDown={(e) => {
60
+ if (e.key === "Enter") {
61
+ e.preventDefault();
62
+ handleSubmit();
63
+ }
64
+ }}
65
+ >
66
+ <FormField
67
+ control={form.control}
68
+ name="title"
69
+ render={({ field }) => (
70
+ <FormItem>
71
+ <FormLabel>Title</FormLabel>
72
+ <FormControl>
73
+ <Input placeholder="Buy groceries..." {...field} />
74
+ </FormControl>
75
+ <FormMessage />
76
+ </FormItem>
77
+ )}
78
+ />
79
+ <FormField
80
+ control={form.control}
81
+ name="description"
82
+ render={({ field }) => (
83
+ <FormItem>
84
+ <FormLabel>Description</FormLabel>
85
+ <FormControl>
86
+ <TextArea
87
+ placeholder="Optional details..."
88
+ className="resize-none"
89
+ rows={3}
90
+ {...field}
91
+ />
92
+ </FormControl>
93
+ <FormMessage />
94
+ </FormItem>
95
+ )}
96
+ />
97
+ <DialogFooter>
98
+ <Button type="submit" disabled={isPending}>
99
+ {isPending ? "Creating..." : "Create Todo"}
100
+ </Button>
101
+ </DialogFooter>
102
+ </form>
103
+ </Form>
104
+ </SheetContent>
105
+ </Sheet>
106
+ );
107
+ };
@@ -0,0 +1,90 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ "use client";
6
+
7
+ import { convexQuery, useConvexMutation } from "@convex-dev/react-query";
8
+ import { useMutation, useQuery } from "@tanstack/react-query";
9
+ import { ArrowLeft } from "lucide-react";
10
+ import Link from "next/link";
11
+
12
+ import { api } from "@/api/_generated/api";
13
+ import type { TodoId } from "@/api/todos/types";
14
+ import { Badge } from "@/ui/base/badge";
15
+ import { Button } from "@/ui/base/button";
16
+ import { Checkbox } from "@/ui/base/checkbox";
17
+ import { Spinner } from "@/ui/base/spinner";
18
+
19
+ type SingleTodoSurfaceProps = {
20
+ todoId: TodoId;
21
+ };
22
+
23
+ export const SingleTodoSurface = ({ todoId }: SingleTodoSurfaceProps) => {
24
+ const { data: todo, isPending } = useQuery(convexQuery(api.todos.crud.getTodo, { todoId }));
25
+
26
+ const { mutate: updateTodo } = useMutation({
27
+ mutationFn: useConvexMutation(api.todos.crud.updateTodo),
28
+ });
29
+
30
+ if (isPending) {
31
+ return (
32
+ <main className="flex flex-1 items-center justify-center">
33
+ <Spinner />
34
+ </main>
35
+ );
36
+ }
37
+
38
+ if (!todo) {
39
+ return (
40
+ <main className="flex flex-1 flex-col items-center justify-center gap-4">
41
+ <p className="text-muted-foreground">Todo not found.</p>
42
+ <Button variant="outline" asChild>
43
+ <Link href="/todos">
44
+ <ArrowLeft className="mr-2 size-4" />
45
+ Back to todos
46
+ </Link>
47
+ </Button>
48
+ </main>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <main className="flex flex-1 flex-col gap-6 overflow-auto p-6 max-w-2xl">
54
+ <Button variant="ghost" className="w-fit -ml-2 text-muted-foreground" asChild>
55
+ <Link href="/todos">
56
+ <ArrowLeft className="mr-2 size-4" />
57
+ Back
58
+ </Link>
59
+ </Button>
60
+
61
+ <div className="flex items-start gap-3">
62
+ <Checkbox
63
+ className="mt-1"
64
+ checked={todo.isCompleted}
65
+ onCheckedChange={(checked) =>
66
+ updateTodo({
67
+ todoId,
68
+ isCompleted: !!checked,
69
+ })
70
+ }
71
+ />
72
+ <div className="flex flex-col gap-2">
73
+ <h1
74
+ className="text-2xl font-semibold"
75
+ style={{
76
+ textDecoration: todo.isCompleted ? "line-through" : undefined,
77
+ }}
78
+ >
79
+ {todo.title}
80
+ </h1>
81
+ <Badge variant={todo.isCompleted ? "secondary" : "default"}>
82
+ {todo.isCompleted ? "Completed" : "In progress"}
83
+ </Badge>
84
+ </div>
85
+ </div>
86
+
87
+ {todo.description && <p className="text-muted-foreground">{todo.description}</p>}
88
+ </main>
89
+ );
90
+ };
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright (c) Aron Weston 2026.
3
+ */
4
+
5
+ "use client";
6
+
7
+ import type { LucideIcon } from "lucide-react";
8
+ import Link from "next/link";
9
+ import { usePathname } from "next/navigation";
10
+
11
+ import { SidebarMenuButton, SidebarMenuItem } from "@/ui/base/side_bar";
12
+
13
+ type NavLinkProps = {
14
+ item: {
15
+ id: string;
16
+ title: string;
17
+ url: string;
18
+ icon: LucideIcon;
19
+ };
20
+ };
21
+
22
+ export const NavLink = ({ item }: NavLinkProps) => {
23
+ const pathname = usePathname();
24
+ const isActive = pathname === item.url;
25
+
26
+ return (
27
+ <SidebarMenuItem>
28
+ <SidebarMenuButton asChild isActive={isActive} tooltip={item.title}>
29
+ <Link href={item.url}>
30
+ <item.icon />
31
+ <span>{item.title}</span>
32
+ </Link>
33
+ </SidebarMenuButton>
34
+ </SidebarMenuItem>
35
+ );
36
+ };