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.
- package/package.json +5 -2
- package/templates/_base/.cursor/agents/skills/clerk/SKILL.md +89 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/SKILL.md +142 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/api-specs-context.sh +30 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/execute-request.sh +88 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-endpoint-detail.sh +165 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tag-endpoints.sh +208 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-backend-api/scripts/extract-tags.js +14 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/SKILL.md +157 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-in.md +224 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-2/custom-sign-up.md +190 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-in.md +314 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/custom-sign-up.md +259 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-custom-ui/core-3/show-component.md +125 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/SKILL.md +94 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/api-routes.md +50 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/caching-auth.md +56 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/middleware-strategies.md +68 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-actions.md +56 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-nextjs-patterns/references/server-vs-client.md +104 -0
- package/templates/_base/.cursor/agents/skills/clerk/clerk-webhooks/SKILL.md +131 -0
- package/templates/_base/.cursor/agents/skills/shadcn/SKILL.md +241 -0
- package/templates/_base/.cursor/agents/skills/shadcn/agents/openai.yml +5 -0
- package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/templates/_base/.cursor/agents/skills/shadcn/assets/shadcn.png +0 -0
- package/templates/_base/.cursor/agents/skills/shadcn/cli.md +257 -0
- package/templates/_base/.cursor/agents/skills/shadcn/customization.md +202 -0
- package/templates/_base/.cursor/agents/skills/shadcn/evals/evals.json +47 -0
- package/templates/_base/.cursor/agents/skills/shadcn/mcp.md +94 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/composition.md +195 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/forms.md +192 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/icons.md +101 -0
- package/templates/_base/.cursor/agents/skills/shadcn/rules/styling.md +162 -0
- package/templates/_base/.cursor/commands/builder.md +0 -0
- package/templates/_base/.cursor/commands/pr.md +7 -0
- package/templates/_base/.cursor/rules/api_architecture.mdc +268 -0
- package/templates/_base/.cursor/rules/coding_standards.mdc +64 -0
- package/templates/_base/.cursor/rules/convex_rules.mdc +675 -0
- package/templates/_base/.cursor/rules/frontend_rules.mdc +268 -0
- package/templates/_base/.env.convex.example +3 -0
- package/templates/_base/.github/workflows/ci.yml +29 -0
- package/templates/_base/.nvmrc +1 -0
- package/templates/_base/.vscode/settings.json +9 -0
- package/templates/_base/apps/api/auth.config.ts +18 -0
- package/templates/_base/apps/api/functions.ts +99 -0
- package/templates/_base/apps/api/project.json +22 -0
- package/templates/_base/apps/api/schema.ts +11 -0
- package/templates/_base/apps/api/todos/crud.ts +81 -0
- package/templates/_base/apps/api/todos/schema.ts +11 -0
- package/templates/_base/apps/api/todos/types.ts +22 -0
- package/templates/_base/apps/api/tsconfig.json +23 -0
- package/templates/_base/apps/api/types.ts +16 -0
- package/templates/_base/biome.json +114 -0
- package/templates/_base/convex.json +4 -0
- package/templates/_base/emails/project.json +16 -0
- package/templates/_base/emails/tsconfig.json +5 -0
- package/templates/_base/emails/welcome_email.tsx +53 -0
- package/templates/_base/nx.json +29 -0
- package/templates/_base/package.json +73 -0
- package/templates/_base/scripts/sync_convex_env.ts +63 -0
- package/templates/_base/shared/assets/image.d.ts +4 -0
- package/templates/_base/shared/assets/src/styles/global.css +73 -0
- package/templates/_base/shared/assets/tsconfig.json +5 -0
- package/templates/_base/shared/ui/src/base/alert_dialog.tsx +139 -0
- package/templates/_base/shared/ui/src/base/badge.tsx +33 -0
- package/templates/_base/shared/ui/src/base/basic_data_table.tsx +61 -0
- package/templates/_base/shared/ui/src/base/button.tsx +69 -0
- package/templates/_base/shared/ui/src/base/button_group.tsx +82 -0
- package/templates/_base/shared/ui/src/base/card.tsx +79 -0
- package/templates/_base/shared/ui/src/base/checkbox.tsx +26 -0
- package/templates/_base/shared/ui/src/base/command.tsx +165 -0
- package/templates/_base/shared/ui/src/base/dialog.tsx +129 -0
- package/templates/_base/shared/ui/src/base/dropdown_menu.tsx +232 -0
- package/templates/_base/shared/ui/src/base/form.tsx +161 -0
- package/templates/_base/shared/ui/src/base/input.tsx +129 -0
- package/templates/_base/shared/ui/src/base/label.tsx +19 -0
- package/templates/_base/shared/ui/src/base/popover.tsx +46 -0
- package/templates/_base/shared/ui/src/base/radio_group.tsx +49 -0
- package/templates/_base/shared/ui/src/base/resizable.tsx +55 -0
- package/templates/_base/shared/ui/src/base/scroll_area.tsx +44 -0
- package/templates/_base/shared/ui/src/base/select.tsx +151 -0
- package/templates/_base/shared/ui/src/base/separator.tsx +32 -0
- package/templates/_base/shared/ui/src/base/sheet.tsx +130 -0
- package/templates/_base/shared/ui/src/base/side_bar.tsx +688 -0
- package/templates/_base/shared/ui/src/base/skeleton.tsx +7 -0
- package/templates/_base/shared/ui/src/base/spinner.tsx +20 -0
- package/templates/_base/shared/ui/src/base/switch.tsx +27 -0
- package/templates/_base/shared/ui/src/base/table.tsx +91 -0
- package/templates/_base/shared/ui/src/base/text_area.tsx +21 -0
- package/templates/_base/shared/ui/src/base/tooltip.tsx +31 -0
- package/templates/_base/shared/ui/src/base/utils.ts +17 -0
- package/templates/_base/shared/ui/src/hooks/use_keyboard_press.tsx +48 -0
- package/templates/_base/shared/ui/src/hooks/use_keyboard_release.tsx +48 -0
- package/templates/_base/shared/ui/src/hooks/use_mobile.tsx +25 -0
- package/templates/_base/shared/ui/src/hooks/use_mouse_click.tsx +44 -0
- package/templates/_base/shared/ui/src/hooks/use_mouse_location.tsx +55 -0
- package/templates/_base/shared/ui/src/hooks/use_outside_click.tsx +29 -0
- package/templates/_base/shared/ui/src/hooks/use_query_params.tsx +33 -0
- package/templates/_base/shared/ui/tsconfig.json +8 -0
- package/templates/_base/shared/utils/src/convex.ts +3 -0
- package/templates/_base/shared/utils/src/time.ts +12 -0
- package/templates/_base/shared/utils/tsconfig.json +5 -0
- package/templates/_base/skills-lock.json +35 -0
- package/templates/_base/tsconfig.base.json +34 -0
- package/templates/nextjs/.env.example +8 -0
- package/templates/nextjs/index.d.ts +6 -0
- package/templates/nextjs/next-env.d.ts +5 -0
- package/templates/nextjs/next.config.js +22 -0
- package/templates/nextjs/postcss.config.js +17 -0
- package/templates/nextjs/project.json +22 -0
- package/templates/nextjs/src/app/(auth)/layout.tsx +21 -0
- package/templates/nextjs/src/app/(auth)/not-allowed/page.tsx +22 -0
- package/templates/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +15 -0
- package/templates/nextjs/src/app/(dashboard)/layout.tsx +27 -0
- package/templates/nextjs/src/app/(dashboard)/page.tsx +5 -0
- package/templates/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +23 -0
- package/templates/nextjs/src/app/(dashboard)/todos/page.tsx +16 -0
- package/templates/nextjs/src/app/app.css +3 -0
- package/templates/nextjs/src/app/layout.tsx +26 -0
- package/templates/nextjs/src/convex.ts +11 -0
- package/templates/nextjs/src/middleware.ts +18 -0
- package/templates/nextjs/src/providers/convex_provider.tsx +44 -0
- package/templates/nextjs/src/surfaces/home_surface.tsx +22 -0
- package/templates/nextjs/src/surfaces/todos/all_todos_surface.tsx +97 -0
- package/templates/nextjs/src/surfaces/todos/create_todo_sheet.tsx +107 -0
- package/templates/nextjs/src/surfaces/todos/single_todo_surface.tsx +90 -0
- package/templates/nextjs/src/ui/sidebar/nav_link.tsx +36 -0
- package/templates/nextjs/src/ui/sidebar/sidebar.tsx +125 -0
- package/templates/nextjs/src/utils/font.ts +9 -0
- package/templates/nextjs/tsconfig.json +42 -0
- package/templates/react-router/.env.example +8 -0
- package/templates/react-router/postcss.config.js +15 -0
- package/templates/react-router/project.json +23 -0
- package/templates/react-router/public/favicon.ico +0 -0
- package/templates/react-router/react-router.config.ts +9 -0
- package/templates/react-router/src/app.css +3 -0
- package/templates/react-router/src/components/error_boundary.tsx +33 -0
- package/templates/react-router/src/layouts/sidebar/sidebar_aside/sidebar_aside.tsx +76 -0
- package/templates/react-router/src/layouts/sidebar/sidebar_aside/user_menu.tsx +36 -0
- package/templates/react-router/src/layouts/sidebar/sidebar_layout.tsx +22 -0
- package/templates/react-router/src/providers/api_auth_provider.tsx +38 -0
- package/templates/react-router/src/root.tsx +37 -0
- package/templates/react-router/src/routes/auth/layout.tsx +13 -0
- package/templates/react-router/src/routes/auth/sign-in.tsx +13 -0
- package/templates/react-router/src/routes/index.tsx +9 -0
- package/templates/react-router/src/routes/layout.tsx +26 -0
- package/templates/react-router/src/routes/todos/[id].tsx +22 -0
- package/templates/react-router/src/routes/todos/index.tsx +13 -0
- package/templates/react-router/src/routes.ts +12 -0
- package/templates/react-router/src/surfaces/home_surface.tsx +20 -0
- package/templates/react-router/src/surfaces/todos/all_todos_surface.tsx +87 -0
- package/templates/react-router/src/surfaces/todos/create_todo_sheet.tsx +102 -0
- package/templates/react-router/src/surfaces/todos/single_todo_surface.tsx +81 -0
- package/templates/react-router/tsconfig.json +20 -0
- 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,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,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
|
+
};
|