Files
myeasycms-v2/.cursorrules
2025-01-21 16:50:57 +08:00

463 lines
14 KiB
Plaintext

# Makerkit Guidelines
You are an expert programming assistant focusing on:
- Expertise: React, Supabase, TypeScript, Next.js 15, Shadcn UI, Tailwind CSS in a Turborepo project
- Focus: Code clarity, Readability, Best practices, Maintainability
- Style: Expert level, factual, solution-focused
- Libraries: TypeScript, React Hook Form, React Query, Zod, Lucide React
## Project Structure
The below is the Makerkit's Next.js App Router structure.
```
- apps
-- web
--- app
---- home # protected routes
------ (user) # user workspace
------ [account] # team workspace
---- (marketing) # marketing pages
---- auth # auth pages
--- components # global components
--- config # global config
--- lib # global utils
--- content # markdoc content
--- styles
--- supabase # supabase root
```
## Database
- Supabase uses Postgres
- We strive to create a safe, robust, performant schema
- Accounts are the general concept of a user account, defined by the having the same ID as Supabase Auth's users (personal). They can be a team account or a personal account.
- Generally speaking, other tables will be used to store data related to the account. For example, a table `notes` would have a foreign key `account_id` to link it to an account.
- Using RLS, we must ensure that only the account owner can access the data. Always write safe RLS policies and ensure that the policies are enforced.
- Unless specified, always enable RLS when creating a table. Propose the required RLS policies ensuring the safety of the data.
- Always consider any required constraints and triggers are in place for data consistency
- Always consider the compromises you need to make and explain them so I can make an educated decision. Follow up with the considerations make and explain them.
- Always consider the security of the data and explain the security implications of the data.
- Always use Postgres schemas explicitly (e.g., `public.accounts`)
## Personal Account Context
The user/personal account context in the application lives under the path `app/home/(user)`. Under this context, we identify the user using Supabase Auth.
We can use the `requireUserInServerComponent` to retrieve the relative Supabase User object and identify the user.
### Client Components
In a Client Component, we can access the `UserWorkspaceContext` and use the `user` object to identify the user.
We can use it like this:
```tsx
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
```
## Team Account Context
The team account context in the application lives under the path `app/home/[account]`. The `[account]` segment is the slug of the team account, from which we can identify the team.
### Accessing the Account Workspace Data in Client Components
The data fetched from the account workspace API is available in the team context. You can access this data using the useAccountWorkspace hook.
```tsx
'use client';
import { useTeamAccountWorkspace } from '@kit/team-accounts/hooks/use-team-account-workspace';
export default function SomeComponent() {
const { account, user, accounts } = useTeamAccountWorkspace();
// use account, user, and accounts
}
```
The useTeamAccountWorkspace hook returns the same data structure as the loadTeamWorkspace function.
NB: the hooks is not to be used is Server Components, only in Client Components. Additionally, this is only available in the pages under /home/[account] layout.
### Team Pages
These pages are dedicated to the team account, which means they are only accessible to team members. To access these pages, the user must be authenticated and belong to the team.
## UI Components
Reusable UI components are defined in the "packages/ui" package named "@kit/ui".
By exporting the component from the "exports" field, we can import it using the "@kit/ui/{component-name}" format.
### Code Standards
- Files
- Always use kebab-case
- Naming
- Functions/Vars: camelCase
- Constants: UPPER_SNAKE_CASE
- Types/Classes: PascalCase
- TypeScript
- Prefer types over interfaces
- Use type inference whenever possible
- Avoid any, any[], unknown, or any other generic type
- Use spaces between code blocks to improve readability
### Styling
- Styling is done using Tailwind CSS. We use the "cn" function from the "@kit/ui/utils" package to generate class names.
- Avoid fixes classes such as "bg-gray-500". Instead, use Shadcn classes such as "bg-background", "text-secondary-foreground", "text-muted-foreground", etc.
### Data Fetching
- In a Server Component context, please use the Supabase Client directly for data fetching
- In a Client Component context, please use the `useQuery` hook from the "@tanstack/react-query" package
Data Flow works in the following way:
1. Server Component uses the Supabase Client to fetch data.
2. Data is rendered in Server Components or passed down to Client Components when absolutely necessary to use a client component (e.g. when using React Hooks or any interaction with the DOM).
```tsx
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const { data, error } = await client.from('notes').select('*');
// use data
}
```
or pass down the data to a Client Component:
```tsx
import { getSupabaseServerClient } from '@kit/supabase/server-client';
export default function ServerComponent() {
const supabase = getSupabaseServerClient();
const { data, error } = await supabase.from('notes').select('*');
if (error) {
return <SomeErrorComponent error={error} />;
}
return <SomeClientComponent data={data} />;
}
```
#### Supabase Clients
- In a Server Component context, use the `getSupabaseServerClient` function from the "@kit/supabase/server-client" package.
- In a Client Component context, use the `useSupabase` hook from the "@kit/supabase/hooks/use-supabase" package.
##### Admin Actions
Only in rare cases suggest using the Admin client `getSupabaseServerAdminClient` when needing to bypass RLS from the package `@kit/supabase/server-admin-client`.
#### React Query
When using `useQuery`, make sure to define the data fetching hook. Create two components: one that fetches the data and one that displays the data.
## Server Actions
- For Data Mutations from Client Components, always use Server Actions
- Always name the server actions file as "server-actions.ts"
- Always name exported Server Actions suffixed as "Action", ex. "createPostAction"
- Always use the `enhanceAction` function from the "@kit/supabase/actions" package.
```tsx
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
const ZodSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export const myServerAction = enhanceAction(
async function (data, user) {
// 1. "data" is already a valid ZodSchema and it's safe to use
// 2. "user" is the authenticated user
// ... your code here
return {
success: true,
};
},
{
auth: true,
schema: ZodSchema,
},
);
```
## Route Handler / API Routes
- Use Route Handlers when data fetching from Client Components
- To create API routes (route.ts), always use the `enhanceRouteHandler` function from the "@kit/supabase/routes" package.
```tsx
import { z } from 'zod';
import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';
const ZodSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
});
export const POST = enhanceRouteHandler(
async function({ body, user, request }) {
// 1. "body" is already a valid ZodSchema and it's safe to use
// 2. "user" is the authenticated user
// 3. "request" is NextRequest
// ... your code here
return NextResponse.json({
success: true,
});
},
{
schema: ZodSchema,
},
);
// example of unauthenticated route (careful!)
export const GET = enhanceRouteHandler(
async function({ user, request }) {
// 1. "user" is null, as "auth" is false and we don't require authentication
// 2. "request" is NextRequest
// ... your code here
return NextResponse.json({
success: true,
});
},
{
auth: false,
},
);
```
Consider logging asynchronous requests using the `@kit/shared/logger` package in a structured way to provide context to the logs in both server actions and route handlers.
```tsx
const ctx = {
name: 'my-server-action', // use a meaningful name
userId: user.id, // use the authenticated user's ID
};
logger.info(ctx, 'Request started...');
const { data, error } = await supabase.from('notes').select('*');
if (error) {
logger.error(ctx, 'Request failed...');
// handle error
} else {
logger.info(ctx, 'Request succeeded...');
// use data
}
```
### Related APIs
When required, use the following APIs.
1. Personal Account APIs:
```tsx
import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const api = createAccountsApi(client);
// use api
}
```
2. Team Account APIs:
```tsx
import { createTeamAccountsApi } from '@kit/team-accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const api = createTeamAccountsApi(client);
// use api
}
```
3. Auth API:
```tsx
import { redirect } from 'next/navigation';
import { requireUser } from '@kit/supabase/require-user';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
async function ServerComponent() {
const client = getSupabaseServerClient();
const auth = await requireUser(client);
// check if the user needs redirect
if (auth.error) {
redirect(auth.redirectTo);
}
// user is authed!
const user = auth.data;
}
```
4. Billing API:
```tsx
import { createBillingGatewayService } from '@kit/billing-gateway';
const service = createBillingGatewayService('stripe');
```
## Creating Pages
When creating new pages ensure:
1. The page is exported using `withI18n` to enable i18n.
2. The page has the required and correct metadata using the `metadata` or `generateMetadata` function.
3. Don't worry about authentication, it will be handled automatically.
## Forms
- Use React Hook Form for form validation and submission.
- Use Zod for form validation.
- Use the `zodResolver` function to resolve the Zod schema to the form.
Follow the example below to create all forms:
### Define the schema
Zod schemas should be defined in the `schema` folder and exported, so we can reuse them across a Server Action and the client-side form:
```tsx
// _lib/schema/create-note.schema.ts
import { z } from 'zod';
export const CreateNoteSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
```
### Create the Server Action
```tsx
// _lib/server/server-actions.ts
'use server';
import { z } from 'zod';
import { enhanceAction } from '@kit/next/actions';
import { CreateNoteSchema } from '../schema/create-note.schema';
const CreateNoteSchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
export const createNoteAction = enhanceAction(
async function (data, user) {
// 1. "data" has been validated against the Zod schema, and it's safe to use
// 2. "user" is the authenticated user
// ... your code here
return {
success: true,
};
},
{
auth: true,
schema: CreateNoteSchema,
},
);
```
Then create a client component to handle the form submission:
```tsx
// _components/create-note-form.tsx
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@kit/ui/form';
import { CreateNoteSchema } from '../_lib/schema/create-note.schema';
export function CreateNoteForm() {
const [pending, startTransition] = useTransition();
const form = useForm({
resolver: zodResolver(CreateNoteSchema),
defaultValues: {
title: '',
content: '',
},
});
const onSubmit = (data) => {
startTransition(async () => {
try {
await createNoteAction(data);
} catch {
// handle error
}
});
};
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Form {...form}>
<FormField name={'title'} render={({ field }) => (
<FormItem>
<FormLabel>
<span className={'text-sm font-medium'}>Title</span>
</FormLabel>
<FormControl>
<input
type={'text'}
className={'w-full'}
placeholder={'Title'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
<FormField name={'content'} render={({ field }) => (
<FormItem>
<FormLabel>
<span className={'text-sm font-medium'}>Content</span>
</FormLabel>
<FormControl>
<textarea
className={'w-full'}
placeholder={'Content'}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)} />
<button disabled={pending} type={'submit'} className={'w-full'}>
Submit
</button>
</Form>
</form>
);
}
```
Always use `@kit/ui` for writing the UI of the form.
## Error Handling
- Logging using the `@kit/shared/logger` package
- Don't swallow errors, always handle them appropriately
- Handle promises and async/await gracefully
- Consider the unhappy path and handle errors appropriately
- Context without sensitive data