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,268 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Frontend coding standards for the Next.js web app. Covers TanStack Query + Convex data fetching, component structure, form handling, and UI patterns.
|
|
3
|
+
globs: apps/web/**/*.tsx,apps/web/**/*.ts
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Frontend Rules
|
|
7
|
+
|
|
8
|
+
## Surfaces & File Structure
|
|
9
|
+
|
|
10
|
+
### Surfaces as Route Entry Points
|
|
11
|
+
|
|
12
|
+
Every route must have a corresponding **surface** — the single entry point for that page. Surfaces are isolated vertical slices: they own their data fetching and pass data strictly downward to child components. Pages are thin wrappers that simply render the surface.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
app/(dashboard)/todos/page.tsx → renders <AllTodosSurface />
|
|
16
|
+
app/(dashboard)/todos/[id]/page.tsx → renders <SingleTodoSurface />
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
// app/(dashboard)/todos/page.tsx
|
|
21
|
+
import { AllTodosSurface } from "@/surfaces/todos/all_todos_surface";
|
|
22
|
+
|
|
23
|
+
export default function TodosPage() {
|
|
24
|
+
return <AllTodosSurface />;
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Naming Convention
|
|
29
|
+
|
|
30
|
+
All file names must be **snake_case** (e.g. `all_todos_surface.tsx`, `create_todo_sheet.tsx`).
|
|
31
|
+
|
|
32
|
+
Use **`All`** for list pages and **`Single`** for id-specific pages — both in the surface name and the file name:
|
|
33
|
+
|
|
34
|
+
| Route | Surface component | File |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `/todos` | `AllTodosSurface` | `surfaces/todos/all_todos_surface.tsx` |
|
|
37
|
+
| `/todos/[id]` | `SingleTodoSurface` | `surfaces/todos/single_todo_surface.tsx` |
|
|
38
|
+
|
|
39
|
+
### Component Placement
|
|
40
|
+
|
|
41
|
+
| What | Where |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Shared components (used across surfaces) | `src/ui/` |
|
|
44
|
+
| Components scoped to one surface | inside that surface's folder |
|
|
45
|
+
| Multiple shared components within a surface | `src/surfaces/<feature>/ui/` subfolder |
|
|
46
|
+
| Providers | `src/providers/` |
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
src/
|
|
50
|
+
surfaces/
|
|
51
|
+
todos/
|
|
52
|
+
all_todos_surface.tsx
|
|
53
|
+
single_todo_surface.tsx
|
|
54
|
+
ui/ ← shared components for this surface only
|
|
55
|
+
todo_card.tsx
|
|
56
|
+
todo_filters.tsx
|
|
57
|
+
ui/ ← truly shared across the app
|
|
58
|
+
sidebar/
|
|
59
|
+
sidebar.tsx
|
|
60
|
+
providers/
|
|
61
|
+
convex_provider.tsx
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Data Fetching with TanStack Query
|
|
69
|
+
|
|
70
|
+
**ALWAYS use TanStack Query for Convex queries and mutations**, never the direct Convex hooks.
|
|
71
|
+
|
|
72
|
+
### Queries
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { useQuery } from "@tanstack/react-query";
|
|
76
|
+
import { convexQuery } from "@convex-dev/react-query";
|
|
77
|
+
import { api } from "@/api/_generated/api";
|
|
78
|
+
|
|
79
|
+
// Basic query
|
|
80
|
+
const { data: todos } = useQuery(
|
|
81
|
+
convexQuery(api.todos.crud.listTodos, { userId: user.id }),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Conditional / skipped query
|
|
85
|
+
const { data: todos } = useQuery(
|
|
86
|
+
convexQuery(
|
|
87
|
+
api.todos.crud.listTodos,
|
|
88
|
+
user?.id ? { userId: user.id } : "skip"
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Mutations
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { useMutation } from "@tanstack/react-query";
|
|
97
|
+
import { useConvexMutation } from "@convex-dev/react-query";
|
|
98
|
+
import { api } from "@/api/_generated/api";
|
|
99
|
+
|
|
100
|
+
const { mutate, isPending } = useMutation({
|
|
101
|
+
mutationFn: useConvexMutation(api.todos.crud.createTodo),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const handleButtonClick = () => {
|
|
105
|
+
mutate({ title, userId });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Component Structure
|
|
111
|
+
|
|
112
|
+
### Declaration
|
|
113
|
+
|
|
114
|
+
Always export components as **const arrow functions**:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// Good
|
|
118
|
+
export const ComponentName = ({ prop1, prop2 }: ComponentNameProps) => {
|
|
119
|
+
return <div />;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Bad
|
|
123
|
+
export function ComponentName({ prop1, prop2 }: ComponentNameProps) {
|
|
124
|
+
return <div />;
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Props Types
|
|
129
|
+
|
|
130
|
+
Always define props as a **separate named type** above the component:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
type ComponentNameProps = {
|
|
134
|
+
entityId: Id<"entities">;
|
|
135
|
+
onComplete: () => void;
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const ComponentName = ({ entityId, onComplete }: ComponentNameProps) => {
|
|
139
|
+
// ...
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Form Handling
|
|
144
|
+
|
|
145
|
+
Use `react-hook-form` + `zod` + TanStack `useMutation`. Standard pattern:
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
export const CreateEntityForm = ({ onSuccess }: CreateEntityFormProps) => {
|
|
149
|
+
const form = useForm<FormSchema>({
|
|
150
|
+
resolver: zodResolver(formSchema),
|
|
151
|
+
defaultValues: { title: "" },
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const { mutate: createEntity, isPending } = useMutation({
|
|
155
|
+
mutationFn: useConvexMutation(api.entities.crud.createEntity),
|
|
156
|
+
onSuccess,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
const handleSubmit = form.handleSubmit((values) => {
|
|
160
|
+
createEntity(values);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<Form {...form}>
|
|
165
|
+
<form
|
|
166
|
+
onSubmit={handleSubmit}
|
|
167
|
+
className="space-y-4"
|
|
168
|
+
onKeyDown={(e) => {
|
|
169
|
+
if (e.key === "Enter") {
|
|
170
|
+
e.preventDefault();
|
|
171
|
+
handleSubmit();
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
<FormField
|
|
176
|
+
control={form.control}
|
|
177
|
+
name="title"
|
|
178
|
+
render={({ field }) => (
|
|
179
|
+
<FormItem>
|
|
180
|
+
<FormLabel>Title</FormLabel>
|
|
181
|
+
<FormControl>
|
|
182
|
+
<Input {...field} />
|
|
183
|
+
</FormControl>
|
|
184
|
+
<FormMessage />
|
|
185
|
+
</FormItem>
|
|
186
|
+
)}
|
|
187
|
+
/>
|
|
188
|
+
<Button type="submit" disabled={isPending}>
|
|
189
|
+
{isPending ? "Saving..." : "Save"}
|
|
190
|
+
</Button>
|
|
191
|
+
</form>
|
|
192
|
+
</Form>
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Key rules:
|
|
198
|
+
- Store `form.handleSubmit(...)` in a `handleSubmit` const
|
|
199
|
+
- Include `onKeyDown` to prevent implicit form submission on Enter
|
|
200
|
+
- Use `isPending` for loading states
|
|
201
|
+
|
|
202
|
+
## Sheet / Dialog Components
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
export const CreateEntitySheet = ({ entityId }: CreateEntitySheetProps) => {
|
|
206
|
+
return (
|
|
207
|
+
<Sheet>
|
|
208
|
+
<SheetTrigger asChild>
|
|
209
|
+
<Button>
|
|
210
|
+
<PlusIcon className="mr-2 h-4 w-4" />
|
|
211
|
+
Add Entity
|
|
212
|
+
</Button>
|
|
213
|
+
</SheetTrigger>
|
|
214
|
+
<SheetContent className="sm:max-w-[500px]">
|
|
215
|
+
<DialogHeader>
|
|
216
|
+
<DialogTitle>Add Entity</DialogTitle>
|
|
217
|
+
<DialogDescription>Fill in the details below.</DialogDescription>
|
|
218
|
+
</DialogHeader>
|
|
219
|
+
{/* form content */}
|
|
220
|
+
<DialogFooter>
|
|
221
|
+
<Button type="submit">Save</Button>
|
|
222
|
+
</DialogFooter>
|
|
223
|
+
</SheetContent>
|
|
224
|
+
</Sheet>
|
|
225
|
+
);
|
|
226
|
+
};
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- Use `SheetContent` with `DialogHeader` / `DialogTitle` / `DialogDescription` for consistent styling
|
|
230
|
+
- Use `DialogFooter` for action buttons
|
|
231
|
+
|
|
232
|
+
## Import Aliases
|
|
233
|
+
|
|
234
|
+
Always use alias-based imports — never relative paths outside the same directory:
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
import { api } from "@/api/_generated/api";
|
|
238
|
+
import { Button } from "@/ui/base/button";
|
|
239
|
+
import { SomeUtil } from "@/utils/some_util";
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Routing
|
|
244
|
+
|
|
245
|
+
The app uses Next.js App Router:
|
|
246
|
+
|
|
247
|
+
| Path | Description |
|
|
248
|
+
|---|---|
|
|
249
|
+
| `(auth)/sign-in` | Clerk sign-in (public) |
|
|
250
|
+
| `(dashboard)/` | Authenticated area — wrapped in sidebar layout |
|
|
251
|
+
|
|
252
|
+
Add new authenticated pages inside `apps/web/src/app/(dashboard)/`.
|
|
253
|
+
|
|
254
|
+
## Sidebar Navigation
|
|
255
|
+
|
|
256
|
+
To add a nav item, update `apps/web/src/components/sidebar/sidebar.tsx`:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
export const nav = {
|
|
260
|
+
main: [
|
|
261
|
+
{ id: "dashboard", title: "Dashboard", url: "/", icon: HomeIcon },
|
|
262
|
+
{ id: "my-feature", title: "My Feature", url: "/my-feature", icon: SomeIcon },
|
|
263
|
+
],
|
|
264
|
+
secondary: [
|
|
265
|
+
{ id: "settings", title: "Settings", url: "/settings", icon: SettingsIcon },
|
|
266
|
+
],
|
|
267
|
+
};
|
|
268
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
actions: read
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
main:
|
|
15
|
+
name:
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
|
|
22
|
+
- uses: oven-sh/setup-bun@v1
|
|
23
|
+
with:
|
|
24
|
+
bun-version: latest
|
|
25
|
+
|
|
26
|
+
# - run: bun install --no-cache
|
|
27
|
+
# - uses: nrwl/nx-set-shas@v4
|
|
28
|
+
|
|
29
|
+
# - run: bun nx affected -t build
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
22
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AuthConfig } from "convex/server";
|
|
2
|
+
|
|
3
|
+
if (!process.env.CLERK_JWT_ISSUER_DOMAIN) {
|
|
4
|
+
throw new Error("CLERK_JWT_ISSUER_DOMAIN is not set");
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
providers: [
|
|
9
|
+
{
|
|
10
|
+
// Replace with your own Clerk Issuer URL from your "convex" JWT template
|
|
11
|
+
// or with `process.env.CLERK_JWT_ISSUER_DOMAIN`
|
|
12
|
+
// and configure CLERK_JWT_ISSUER_DOMAIN on the Convex Dashboard
|
|
13
|
+
// See https://docs.convex.dev/auth/clerk#configuring-dev-and-prod-instances
|
|
14
|
+
domain: process.env.CLERK_JWT_ISSUER_DOMAIN!,
|
|
15
|
+
applicationID: "convex",
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
} satisfies AuthConfig;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (c) Fabra Pty Ltd 2025.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { entsTableFactory } from "convex-ents";
|
|
6
|
+
import {
|
|
7
|
+
customCtx,
|
|
8
|
+
customMutation,
|
|
9
|
+
customQuery,
|
|
10
|
+
} from "convex-helpers/server/customFunctions";
|
|
11
|
+
import { zCustomMutation, zCustomQuery } from "convex-helpers/server/zod";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
internalMutation as baseInternalMutation,
|
|
15
|
+
internalQuery as baseInternalQuery,
|
|
16
|
+
mutation as baseMutation,
|
|
17
|
+
query as baseQuery,
|
|
18
|
+
} from "./_generated/server";
|
|
19
|
+
import { entDefinitions } from "./schema";
|
|
20
|
+
|
|
21
|
+
export const query = customQuery(
|
|
22
|
+
baseQuery,
|
|
23
|
+
customCtx(async (ctx) => {
|
|
24
|
+
return {
|
|
25
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
26
|
+
db: undefined,
|
|
27
|
+
};
|
|
28
|
+
}),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export const zQuery = zCustomQuery(
|
|
32
|
+
baseQuery,
|
|
33
|
+
customCtx(async (ctx) => {
|
|
34
|
+
return {
|
|
35
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
36
|
+
db: undefined,
|
|
37
|
+
};
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
export const internalQuery = customQuery(
|
|
42
|
+
baseInternalQuery,
|
|
43
|
+
customCtx(async (ctx) => {
|
|
44
|
+
return {
|
|
45
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
46
|
+
db: undefined,
|
|
47
|
+
};
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const zInternalQuery = zCustomQuery(
|
|
52
|
+
baseInternalQuery,
|
|
53
|
+
customCtx(async (ctx) => {
|
|
54
|
+
return {
|
|
55
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
56
|
+
db: undefined,
|
|
57
|
+
};
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const mutation = customMutation(
|
|
62
|
+
baseMutation,
|
|
63
|
+
customCtx(async (ctx) => {
|
|
64
|
+
return {
|
|
65
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
66
|
+
db: undefined,
|
|
67
|
+
};
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export const zMutation = zCustomMutation(
|
|
72
|
+
baseMutation,
|
|
73
|
+
customCtx(async (ctx) => {
|
|
74
|
+
return {
|
|
75
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
76
|
+
db: undefined,
|
|
77
|
+
};
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const internalMutation = customMutation(
|
|
82
|
+
baseInternalMutation,
|
|
83
|
+
customCtx(async (ctx) => {
|
|
84
|
+
return {
|
|
85
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
86
|
+
db: undefined,
|
|
87
|
+
};
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export const zInternalMutation = zCustomMutation(
|
|
92
|
+
baseInternalMutation,
|
|
93
|
+
customCtx(async (ctx) => {
|
|
94
|
+
return {
|
|
95
|
+
table: entsTableFactory(ctx, entDefinitions),
|
|
96
|
+
db: undefined,
|
|
97
|
+
};
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "project:api",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "./apps/api",
|
|
5
|
+
"projectType": "application",
|
|
6
|
+
"tags": [],
|
|
7
|
+
"targets": {
|
|
8
|
+
"dev": {
|
|
9
|
+
"executor": "nx:run-commands",
|
|
10
|
+
"options": {
|
|
11
|
+
"command": "bunx convex dev"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"env": {
|
|
15
|
+
"executor": "nx:run-commands",
|
|
16
|
+
"options": {
|
|
17
|
+
"command": "bun scripts/sync_convex_env.ts",
|
|
18
|
+
"cwd": "{workspaceRoot}"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineEntSchema, getEntDefinitions } from "convex-ents";
|
|
2
|
+
|
|
3
|
+
import { todoEnts } from "@/api/todos/schema";
|
|
4
|
+
|
|
5
|
+
const schema = defineEntSchema({
|
|
6
|
+
...todoEnts,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export default schema;
|
|
10
|
+
|
|
11
|
+
export const entDefinitions = getEntDefinitions(schema);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ConvexError } from "convex/values";
|
|
2
|
+
import { zid } from "convex-helpers/server/zod";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import { zMutation, zQuery } from "@/api/functions";
|
|
6
|
+
|
|
7
|
+
export const getTodo = zQuery({
|
|
8
|
+
args: {
|
|
9
|
+
todoId: zid("todos"),
|
|
10
|
+
},
|
|
11
|
+
handler: async (ctx, args) => {
|
|
12
|
+
return ctx.table("todos").getX(args.todoId);
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const listTodos = zQuery({
|
|
17
|
+
handler: async (ctx) => {
|
|
18
|
+
const user = await ctx.auth.getUserIdentity();
|
|
19
|
+
|
|
20
|
+
if (!user) {
|
|
21
|
+
throw new ConvexError("Unauthorized");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return ctx.table("todos", "by_user_id", (q) => q.eq("userId", user.subject));
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const updateTodo = zMutation({
|
|
29
|
+
args: {
|
|
30
|
+
todoId: zid("todos"),
|
|
31
|
+
title: z.string().min(1).optional(),
|
|
32
|
+
description: z.string().optional(),
|
|
33
|
+
isCompleted: z.boolean().optional(),
|
|
34
|
+
},
|
|
35
|
+
handler: async (ctx, args) => {
|
|
36
|
+
const { todoId, ...updates } = args;
|
|
37
|
+
const todo = await ctx.table("todos").getX(todoId);
|
|
38
|
+
return todo.patch(updates);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const createTodo = zMutation({
|
|
43
|
+
args: {
|
|
44
|
+
title: z.string().min(1),
|
|
45
|
+
description: z.string().optional(),
|
|
46
|
+
},
|
|
47
|
+
handler: async (ctx, args) => {
|
|
48
|
+
const user = await ctx.auth.getUserIdentity();
|
|
49
|
+
|
|
50
|
+
if (!user) {
|
|
51
|
+
throw new ConvexError("Unauthorized");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return ctx.table("todos").insert({
|
|
55
|
+
title: args.title,
|
|
56
|
+
description: args.description,
|
|
57
|
+
isCompleted: false,
|
|
58
|
+
userId: user.subject,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export const deleteTodo = zMutation({
|
|
64
|
+
args: {
|
|
65
|
+
todoId: zid("todos"),
|
|
66
|
+
},
|
|
67
|
+
handler: async (ctx, args) => {
|
|
68
|
+
const user = await ctx.auth.getUserIdentity();
|
|
69
|
+
if (!user) {
|
|
70
|
+
throw new ConvexError("Unauthorized");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const todo = await ctx.table("todos").getX(args.todoId);
|
|
74
|
+
|
|
75
|
+
if (todo.userId !== user.subject) {
|
|
76
|
+
throw new ConvexError("Unauthorized");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
await todo.delete();
|
|
80
|
+
},
|
|
81
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { defineEnt } from "convex-ents";
|
|
3
|
+
|
|
4
|
+
export const todoEnts = {
|
|
5
|
+
todos: defineEnt({
|
|
6
|
+
title: v.string(),
|
|
7
|
+
description: v.optional(v.string()),
|
|
8
|
+
isCompleted: v.boolean(),
|
|
9
|
+
userId: v.string(),
|
|
10
|
+
}).index("by_user_id", ["userId"]),
|
|
11
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import type { Doc, Id } from "@/api/_generated/dataModel";
|
|
4
|
+
import type { Ent } from "@/api/types";
|
|
5
|
+
|
|
6
|
+
export type TodoId = Id<"todos">;
|
|
7
|
+
export type Todo = Doc<"todos">;
|
|
8
|
+
export type TodoEnt = Ent<"todos">;
|
|
9
|
+
|
|
10
|
+
export type CreateTodo = z.infer<typeof CreateTodo>;
|
|
11
|
+
export const CreateTodo = z.object({
|
|
12
|
+
title: z.string().min(1),
|
|
13
|
+
description: z.string().optional(),
|
|
14
|
+
userId: z.string(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type UpdateTodo = z.infer<typeof UpdateTodo>;
|
|
18
|
+
export const UpdateTodo = z.object({
|
|
19
|
+
title: z.string().min(1).optional(),
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
isCompleted: z.boolean().optional(),
|
|
22
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
/* These settings are not required by Convex and can be modified. */
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"moduleResolution": "Bundler",
|
|
8
|
+
"noUnusedLocals": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
/* These compiler options are required by Convex */
|
|
13
|
+
"target": "ESNext",
|
|
14
|
+
"lib": ["ES2021", "dom"],
|
|
15
|
+
"forceConsistentCasingInFileNames": true,
|
|
16
|
+
"module": "ESNext",
|
|
17
|
+
"isolatedModules": true,
|
|
18
|
+
"noEmit": true
|
|
19
|
+
},
|
|
20
|
+
"exclude": ["node_modules", "./_generated", "CODEOWNERS"],
|
|
21
|
+
"extends": "../../tsconfig.base.json",
|
|
22
|
+
"include": ["**/*.ts", "**/*.js", "**/*.json"]
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { mutation, query } from "apps/api/functions";
|
|
2
|
+
import type { entDefinitions } from "apps/api/schema";
|
|
3
|
+
import type { GenericEnt, GenericEntWriter } from "convex-ents";
|
|
4
|
+
import type { CustomCtx } from "convex-helpers/server/customFunctions";
|
|
5
|
+
|
|
6
|
+
import type { TableNames } from "@/api/_generated/dataModel";
|
|
7
|
+
|
|
8
|
+
export type QueryCtx = CustomCtx<typeof query>;
|
|
9
|
+
export type MutationCtx = CustomCtx<typeof mutation>;
|
|
10
|
+
|
|
11
|
+
export type Ent<TableName extends TableNames> = GenericEnt<typeof entDefinitions, TableName>;
|
|
12
|
+
|
|
13
|
+
export type EntWriter<TableName extends TableNames> = GenericEntWriter<
|
|
14
|
+
typeof entDefinitions,
|
|
15
|
+
TableName
|
|
16
|
+
>;
|