-
Notifications
You must be signed in to change notification settings - Fork 376
/
Copy path.cursorrules
248 lines (204 loc) · 7.93 KB
/
.cursorrules
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
You are an expert in TypeScript, Node.js, Next.js App Router, React, Prisma, PostgreSQL, Turborepo, Shadcn UI, Radix UI and Tailwind.
- Write concise, technical TypeScript code with accurate examples.
- Use functional and declarative programming patterns; avoid classes.
- Prefer iteration and modularization over code duplication. But if you don't have a proper abstraction then writing similar code twice is fine.
- Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).
- Structure files: exported component, subcomponents, helpers, static content, types.
- Use kebab case for route directories (e.g., `api/hello-world/route`). Use PascalCase for components (e.g. `components/Button.tsx`)
- Shadcn components are in `components/ui`. All other components are in `components/`.
- Colocate files in the folder they're used. Unless the component can be used in many places across the app. Then it can be placed in the `components` folder.
- We use Turborepo with pnpm workspaces.
- We have one app called `web` in `apps/web`.
- We have packages in the `packages` folder.
- We have Next.js server actions in the `apps/web/utils/actions` folder. Eg. `apps/web/utils/actions/cold-email` has actions related to cold email management.
- Favor named exports for components.
- Use TypeScript for all code.
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements.
- Use Shadcn UI and Tailwind for components and styling.
- Implement responsive design with Tailwind CSS; use a mobile-first approach.
- Use `next/image` package for images.
- For API get requests to server use the `swr` package. Example:
```typescript
const searchParams = useSearchParams();
const page = searchParams.get("page") || "1";
const { data, isLoading, error } = useSWR<PlanHistoryResponse>(
`/api/user/planned/history?page=${page}`
);
```
- Use the `Loading` component. Example:
```tsx
<Card>
<LoadingContent loading={isLoading} error={error}>
{data && <MyComponent data={data} />}
</LoadingContent>
</Card>
```
- If we're in a server file, you can fetch the data inline.
- For mutating data, use Next.js server actions.
- Use `isActionError` with `toastError` and `toastSuccess`. Success toast is optional. Example:
```typescript
import { toastError, toastSuccess } from "@/components/Toast";
const onSubmit: SubmitHandler<TestRulesInputs> = useCallback(async (data) => {
const result = await testAiCustomContentAction({ content: data.message });
if (isActionError(result)) {
toastError({
title: "Error testing email",
description: result.error,
});
} else {
toastSuccess({ description: "Saved!" });
}
}, []);
```
- Implement type-safe server actions with proper validation.
- Define input schemas using Zod for robust type checking and validation.
- Handle errors gracefully and return appropriate responses.
- Ensure all server actions return the `Promise<ServerActionResponse>` type
- Use React Hook Form with Zod for validation. The same validation should be done in the server action too. Example:
```tsx
import { Input } from "@/components/Input";
import { Button } from "@/components/ui/button";
export const ProcessHistory = () => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<ProcessHistoryOptions>({
resolver: zodResolver(processHistorySchema),
});
const onSubmit: SubmitHandler<ProcessHistoryOptions> = useCallback(
async (data) => {
const result = await processHistoryAction(data.email);
handleActionResult(result, `Processed history for ${data.email}`);
},
[]
);
return (
<form className="max-w-sm space-y-4" onSubmit={handleSubmit(onSubmit)}>
<Input
type="email"
name="email"
label="Email"
registerProps={register("email", { required: true })}
error={errors.email}
/>
<Button type="submit" loading={isSubmitting}>
Process History
</Button>
</form>
);
};
```
- This is how you do a text area. Example:
```tsx
<Input
type="text"
autosizeTextarea
rows={3}
name="message"
placeholder="Paste in email content"
registerProps={register("message", { required: true })}
error={errors.message}
/>
```
- This is the format of a server action. Example:
```typescript
import { auth } from "@/app/api/auth/[...nextauth]/auth";
export const deactivateApiKeyAction = withActionInstrumentation(
"deactivateApiKey",
async (unsafeData: DeactivateApiKeyBody) => {
const session = await auth();
const userId = session?.user.id;
if (!userId) return { error: "Not logged in" };
const { data, success, error } =
deactivateApiKeyBody.safeParse(unsafeData);
if (!success) return { error: error.message };
await prisma.apiKey.update({
where: { id: data.id, userId },
data: { isActive: false },
});
revalidatePath("/settings");
}
);
```
- Logging example:
```tsx
import { createScopedLogger } from "@/utils/logger";
const logger = createScopedLogger("Rules");
logger.log("Created rule", { userId });
```
- This is how we import Prisma:
```ts
import prisma from "@/utils/prisma";
```
- If an import is a type then use `type` to import it:
```ts
import type { Action } from "@prisma/client";
import { type Action, GroupItemType } from "@prisma/client";
```
## Adding new pages
- Create new pages at: `apps/web/app/(app)/PAGE_NAME/page.tsx`.
- Components for the page are either put in `page.tsx`, or in the `apps/web/app/(app)/PAGE_NAME` folder.
- Pages are Server components so you can load data into them direct. But if we're in a deeply nested component we will use `swr` to fetch via API.
- If you need to use `onClick` in a component, that component is a client component and file must start with `use client`.
- Server action files should start with `use server`.
## API routes
- Use this format for API routes:
```ts
import { z } from "zod";
import { NextResponse } from "next/server";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import prisma from "@/utils/prisma";
import { withError } from "@/utils/middleware";
const ApiNameBody = z.object({ id: z.string(), message: z.string() });
export type ApiNameBody = z.infer<typeof ApiNameBody>;
export type updateApiNameResponse = Awaited<ReturnType<typeof updateApiName>>;
async function updateApiName(body: ApiNameBody, options: { email: string }) {
const { email } = options;
const result = await prisma.table.update({
where: {
id: body.id,
email,
},
data: body
})
return { result };
};
export const POST = withError(async (request: Request) => {
const session = await auth();
if (!session?.user.email) return NextResponse.json({ error: "Not authenticated" });
const json = await request.json();
const body = ApiNameBody.parse(json);
const result = await updateApiName(body, { email: session.user.email });
return NextResponse.json(result);
});
```
## Utility Functions
- Use lodash utilities for common operations (arrays, objects, strings)
- Import specific lodash functions to minimize bundle size:
```ts
import groupBy from "lodash/groupBy";
```
- Create utility functions in `utils/` folder for reusable logic
## Testing
- `vitest` is used for testing.
- Tests are colocated next to the tested file. e.g. `dir/format.ts` and `dir/format.test.ts`.
- AI tests are placed in the `__tests__` directory and are not run by default as they use a real LLM to run which costs money each time.
- You need to mock server-only for tests:
```ts
vi.mock("server-only", () => ({}));
```
- To mock Prisma:
```ts
import { beforeEach } from "vitest";
import prisma from "@/utils/__mocks__/prisma";
vi.mock("@/utils/prisma");
describe("example", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("test", async () => {
prisma.group.findMany.mockResolvedValue([]);
});
});
```