@folpe/loom 0.2.0 → 0.3.0
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/README.md +82 -16
- package/data/agents/backend/AGENT.md +12 -0
- package/data/agents/database/AGENT.md +3 -0
- package/data/agents/frontend/AGENT.md +10 -0
- package/data/agents/marketing/AGENT.md +3 -0
- package/data/agents/orchestrator/AGENT.md +1 -19
- package/data/agents/security/AGENT.md +3 -0
- package/data/agents/tests/AGENT.md +2 -0
- package/data/agents/ux-ui/AGENT.md +5 -0
- package/data/presets/api-backend.yaml +0 -3
- package/data/presets/chrome-extension.yaml +0 -3
- package/data/presets/cli-tool.yaml +0 -3
- package/data/presets/e-commerce.yaml +0 -3
- package/data/presets/expo-mobile.yaml +0 -3
- package/data/presets/fullstack-auth.yaml +0 -3
- package/data/presets/landing-page.yaml +0 -3
- package/data/presets/mvp-lean.yaml +0 -3
- package/data/presets/saas-default.yaml +3 -4
- package/data/presets/saas-full.yaml +71 -0
- package/data/skills/api-design/SKILL.md +43 -2
- package/data/skills/auth-rbac/SKILL.md +179 -0
- package/data/skills/better-auth-patterns/SKILL.md +212 -0
- package/data/skills/chrome-extension-patterns/SKILL.md +13 -6
- package/data/skills/cli-development/SKILL.md +11 -3
- package/data/skills/drizzle-patterns/SKILL.md +166 -0
- package/data/skills/env-validation/SKILL.md +142 -0
- package/data/skills/form-validation/SKILL.md +169 -0
- package/data/skills/hero-copywriting/SKILL.md +12 -4
- package/data/skills/i18n-patterns/SKILL.md +176 -0
- package/data/skills/layered-architecture/SKILL.md +131 -0
- package/data/skills/nextjs-conventions/SKILL.md +46 -7
- package/data/skills/react-native-patterns/SKILL.md +10 -8
- package/data/skills/react-query-patterns/SKILL.md +193 -0
- package/data/skills/resend-email/SKILL.md +181 -0
- package/data/skills/seo-optimization/SKILL.md +10 -2
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +46 -12
- package/data/skills/stripe-integration/SKILL.md +11 -3
- package/data/skills/supabase-patterns/SKILL.md +23 -6
- package/data/skills/table-pagination/SKILL.md +224 -0
- package/data/skills/tailwind-patterns/SKILL.md +12 -2
- package/data/skills/testing-patterns/SKILL.md +203 -0
- package/data/skills/ui-ux-guidelines/SKILL.md +10 -5
- package/dist/index.js +254 -100
- package/package.json +2 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: react-query-patterns
|
|
3
|
+
description: "TanStack React Query for data fetching, caching, mutations, and optimistic updates. Use when managing server state in client components, implementing data fetching hooks, or caching API responses."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# React Query Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Query keys are structured** — use the factory pattern from `query-keys.ts`.
|
|
11
|
+
- **Custom hooks for reuse** — wrap `useQuery`/`useMutation` in domain hooks.
|
|
12
|
+
- **Invalidate on mutation** — always invalidate related queries after writes.
|
|
13
|
+
- **Optimistic updates for UX** — use for edits and deletes where latency matters.
|
|
14
|
+
- **Prefetch in RSC** — hydrate queries in server components for instant loads.
|
|
15
|
+
- **Never fetch in useEffect** — use React Query instead.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
// src/providers/query-provider.tsx
|
|
21
|
+
"use client";
|
|
22
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
23
|
+
import { useState } from "react";
|
|
24
|
+
|
|
25
|
+
export function QueryProvider({ children }: { children: React.ReactNode }) {
|
|
26
|
+
const [queryClient] = useState(
|
|
27
|
+
() =>
|
|
28
|
+
new QueryClient({
|
|
29
|
+
defaultOptions: {
|
|
30
|
+
queries: {
|
|
31
|
+
staleTime: 60 * 1000, // 1 minute
|
|
32
|
+
refetchOnWindowFocus: false,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Query Keys Convention
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// src/lib/query-keys.ts
|
|
48
|
+
export const queryKeys = {
|
|
49
|
+
users: {
|
|
50
|
+
all: ["users"] as const,
|
|
51
|
+
list: (filters: UserFilters) => ["users", "list", filters] as const,
|
|
52
|
+
detail: (id: string) => ["users", "detail", id] as const,
|
|
53
|
+
},
|
|
54
|
+
posts: {
|
|
55
|
+
all: ["posts"] as const,
|
|
56
|
+
list: (filters: PostFilters) => ["posts", "list", filters] as const,
|
|
57
|
+
detail: (id: string) => ["posts", "detail", id] as const,
|
|
58
|
+
comments: (postId: string) => ["posts", postId, "comments"] as const,
|
|
59
|
+
},
|
|
60
|
+
} as const;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Queries
|
|
64
|
+
|
|
65
|
+
### Basic Query
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
"use client";
|
|
69
|
+
import { useQuery } from "@tanstack/react-query";
|
|
70
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
71
|
+
|
|
72
|
+
export function UserList() {
|
|
73
|
+
const { data: users, isLoading, error } = useQuery({
|
|
74
|
+
queryKey: queryKeys.users.all,
|
|
75
|
+
queryFn: () => fetch("/api/users").then((r) => r.json()),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (isLoading) return <Skeleton />;
|
|
79
|
+
if (error) return <ErrorMessage error={error} />;
|
|
80
|
+
|
|
81
|
+
return <ul>{users.map((u) => <li key={u.id}>{u.name}</li>)}</ul>;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Query with Server Action
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useQuery } from "@tanstack/react-query";
|
|
89
|
+
import { getUserList } from "@/actions/user.actions";
|
|
90
|
+
|
|
91
|
+
const { data } = useQuery({
|
|
92
|
+
queryKey: queryKeys.users.list(filters),
|
|
93
|
+
queryFn: () => getUserList(filters),
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Mutations
|
|
98
|
+
|
|
99
|
+
### Basic Mutation
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
103
|
+
import { createUser } from "@/actions/user.actions";
|
|
104
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
105
|
+
import { toast } from "sonner";
|
|
106
|
+
|
|
107
|
+
export function useCreateUser() {
|
|
108
|
+
const queryClient = useQueryClient();
|
|
109
|
+
|
|
110
|
+
return useMutation({
|
|
111
|
+
mutationFn: createUser,
|
|
112
|
+
onSuccess: () => {
|
|
113
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.all });
|
|
114
|
+
toast.success("User created");
|
|
115
|
+
},
|
|
116
|
+
onError: (error) => {
|
|
117
|
+
toast.error(error.message);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Optimistic Update
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
export function useUpdateUser() {
|
|
127
|
+
const queryClient = useQueryClient();
|
|
128
|
+
|
|
129
|
+
return useMutation({
|
|
130
|
+
mutationFn: ({ id, data }: { id: string; data: UpdateUserInput }) =>
|
|
131
|
+
updateUser(id, data),
|
|
132
|
+
onMutate: async ({ id, data }) => {
|
|
133
|
+
await queryClient.cancelQueries({ queryKey: queryKeys.users.detail(id) });
|
|
134
|
+
const previous = queryClient.getQueryData(queryKeys.users.detail(id));
|
|
135
|
+
queryClient.setQueryData(queryKeys.users.detail(id), (old: User) => ({
|
|
136
|
+
...old,
|
|
137
|
+
...data,
|
|
138
|
+
}));
|
|
139
|
+
return { previous };
|
|
140
|
+
},
|
|
141
|
+
onError: (_err, { id }, context) => {
|
|
142
|
+
queryClient.setQueryData(queryKeys.users.detail(id), context?.previous);
|
|
143
|
+
},
|
|
144
|
+
onSettled: (_data, _err, { id }) => {
|
|
145
|
+
queryClient.invalidateQueries({ queryKey: queryKeys.users.detail(id) });
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Custom Hooks
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// src/hooks/use-users.ts
|
|
155
|
+
export function useUsers(filters?: UserFilters) {
|
|
156
|
+
return useQuery({
|
|
157
|
+
queryKey: queryKeys.users.list(filters ?? {}),
|
|
158
|
+
queryFn: () => getUserList(filters),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function useUser(id: string) {
|
|
163
|
+
return useQuery({
|
|
164
|
+
queryKey: queryKeys.users.detail(id),
|
|
165
|
+
queryFn: () => getUserById(id),
|
|
166
|
+
enabled: !!id,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Prefetching with RSC
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
// app/users/page.tsx (Server Component)
|
|
175
|
+
import { HydrationBoundary, dehydrate, QueryClient } from "@tanstack/react-query";
|
|
176
|
+
import { getUserList } from "@/actions/user.actions";
|
|
177
|
+
import { queryKeys } from "@/lib/query-keys";
|
|
178
|
+
import { UserList } from "./user-list";
|
|
179
|
+
|
|
180
|
+
export default async function UsersPage() {
|
|
181
|
+
const queryClient = new QueryClient();
|
|
182
|
+
await queryClient.prefetchQuery({
|
|
183
|
+
queryKey: queryKeys.users.all,
|
|
184
|
+
queryFn: getUserList,
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
189
|
+
<UserList />
|
|
190
|
+
</HydrationBoundary>
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
```
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resend-email
|
|
3
|
+
description: "Email sending patterns with Resend and React Email templates. Use when implementing transactional emails, creating email templates, or adding i18n email support."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Resend Email Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Emails via service layer** — never send directly from routes.
|
|
11
|
+
- **React Email for templates** — type-safe, previewable components.
|
|
12
|
+
- **I18n support** — every email must support multiple locales.
|
|
13
|
+
- **Inline styles only** — CSS classes don't work in most email clients.
|
|
14
|
+
- **Handle delivery webhooks** — bounces and complaints must be processed.
|
|
15
|
+
- **Preview emails locally** — use `email dev` command during development.
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
// src/lib/resend.ts
|
|
21
|
+
import { Resend } from "resend";
|
|
22
|
+
|
|
23
|
+
export const resend = new Resend(process.env.RESEND_API_KEY);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Email Service Layer
|
|
27
|
+
|
|
28
|
+
- Emails are sent from a dedicated service — never from routes or components directly.
|
|
29
|
+
- Located in `src/services/email.service.ts`.
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// src/services/email.service.ts
|
|
33
|
+
import { resend } from "@/lib/resend";
|
|
34
|
+
import { WelcomeEmail } from "@/emails/welcome";
|
|
35
|
+
import { InviteEmail } from "@/emails/invite";
|
|
36
|
+
|
|
37
|
+
const FROM = "App Name <noreply@yourdomain.com>";
|
|
38
|
+
|
|
39
|
+
export async function sendWelcomeEmail(to: string, name: string, locale: string) {
|
|
40
|
+
return resend.emails.send({
|
|
41
|
+
from: FROM,
|
|
42
|
+
to,
|
|
43
|
+
subject: getSubject("welcome", locale),
|
|
44
|
+
react: WelcomeEmail({ name, locale }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function sendInviteEmail(
|
|
49
|
+
to: string,
|
|
50
|
+
inviterName: string,
|
|
51
|
+
orgName: string,
|
|
52
|
+
inviteUrl: string,
|
|
53
|
+
locale: string
|
|
54
|
+
) {
|
|
55
|
+
return resend.emails.send({
|
|
56
|
+
from: FROM,
|
|
57
|
+
to,
|
|
58
|
+
subject: getSubject("invite", locale, { orgName }),
|
|
59
|
+
react: InviteEmail({ inviterName, orgName, inviteUrl, locale }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## React Email Templates
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
// src/emails/welcome.tsx
|
|
68
|
+
import {
|
|
69
|
+
Html, Head, Body, Container, Section, Text, Button, Hr,
|
|
70
|
+
} from "@react-email/components";
|
|
71
|
+
import { getEmailTranslations } from "@/lib/email-i18n";
|
|
72
|
+
|
|
73
|
+
interface WelcomeEmailProps {
|
|
74
|
+
name: string;
|
|
75
|
+
locale: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function WelcomeEmail({ name, locale }: WelcomeEmailProps) {
|
|
79
|
+
const t = getEmailTranslations(locale, "welcome");
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Html>
|
|
83
|
+
<Head />
|
|
84
|
+
<Body style={body}>
|
|
85
|
+
<Container style={container}>
|
|
86
|
+
<Text style={heading}>{t("title", { name })}</Text>
|
|
87
|
+
<Text style={paragraph}>{t("description")}</Text>
|
|
88
|
+
<Section style={buttonSection}>
|
|
89
|
+
<Button style={button} href="https://app.yourdomain.com/dashboard">
|
|
90
|
+
{t("cta")}
|
|
91
|
+
</Button>
|
|
92
|
+
</Section>
|
|
93
|
+
<Hr style={hr} />
|
|
94
|
+
<Text style={footer}>{t("footer")}</Text>
|
|
95
|
+
</Container>
|
|
96
|
+
</Body>
|
|
97
|
+
</Html>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Inline styles for email compatibility
|
|
102
|
+
const body = { backgroundColor: "#f6f9fc", fontFamily: "sans-serif" };
|
|
103
|
+
const container = { margin: "0 auto", padding: "40px 20px", maxWidth: "560px" };
|
|
104
|
+
const heading = { fontSize: "24px", fontWeight: "bold", marginBottom: "16px" };
|
|
105
|
+
const paragraph = { fontSize: "16px", lineHeight: "1.5", color: "#333" };
|
|
106
|
+
const buttonSection = { textAlign: "center" as const, margin: "32px 0" };
|
|
107
|
+
const button = {
|
|
108
|
+
backgroundColor: "#000", color: "#fff", padding: "12px 24px",
|
|
109
|
+
borderRadius: "6px", fontSize: "16px", textDecoration: "none",
|
|
110
|
+
};
|
|
111
|
+
const hr = { borderColor: "#e6ebf1", margin: "32px 0" };
|
|
112
|
+
const footer = { fontSize: "12px", color: "#8898aa" };
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## I18n for Emails
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// src/lib/email-i18n.ts
|
|
119
|
+
const emailMessages: Record<string, Record<string, Record<string, string>>> = {
|
|
120
|
+
en: {
|
|
121
|
+
welcome: {
|
|
122
|
+
title: "Welcome, {name}!",
|
|
123
|
+
description: "We're excited to have you on board.",
|
|
124
|
+
cta: "Go to Dashboard",
|
|
125
|
+
footer: "You received this email because you created an account.",
|
|
126
|
+
},
|
|
127
|
+
invite: {
|
|
128
|
+
title: "{inviterName} invited you to {orgName}",
|
|
129
|
+
description: "Click below to accept the invitation.",
|
|
130
|
+
cta: "Accept Invitation",
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
fr: {
|
|
134
|
+
welcome: {
|
|
135
|
+
title: "Bienvenue, {name} !",
|
|
136
|
+
description: "Nous sommes ravis de vous compter parmi nous.",
|
|
137
|
+
cta: "Aller au tableau de bord",
|
|
138
|
+
footer: "Vous avez reçu cet e-mail car vous avez créé un compte.",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export function getEmailTranslations(locale: string, namespace: string) {
|
|
144
|
+
const messages = emailMessages[locale]?.[namespace] ?? emailMessages.en[namespace];
|
|
145
|
+
|
|
146
|
+
return (key: string, params?: Record<string, string>) => {
|
|
147
|
+
let message = messages[key] ?? key;
|
|
148
|
+
if (params) {
|
|
149
|
+
for (const [k, v] of Object.entries(params)) {
|
|
150
|
+
message = message.replace(`{${k}}`, v);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return message;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Webhook for Delivery Status
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// src/app/api/webhook/resend/route.ts
|
|
162
|
+
import { NextResponse } from "next/server";
|
|
163
|
+
|
|
164
|
+
export async function POST(request: Request) {
|
|
165
|
+
const payload = await request.json();
|
|
166
|
+
|
|
167
|
+
switch (payload.type) {
|
|
168
|
+
case "email.delivered":
|
|
169
|
+
// Log successful delivery
|
|
170
|
+
break;
|
|
171
|
+
case "email.bounced":
|
|
172
|
+
// Mark email as bounced, disable future sends
|
|
173
|
+
break;
|
|
174
|
+
case "email.complained":
|
|
175
|
+
// Unsubscribe user
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return NextResponse.json({ received: true });
|
|
180
|
+
}
|
|
181
|
+
```
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: seo-optimization
|
|
3
|
-
description: "SEO best practices for structured data, meta tags, Core Web Vitals, and indexing. Use when building landing pages,
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "SEO best practices for structured data, meta tags, Core Web Vitals, and indexing. Use when building landing pages, optimizing page metadata, adding JSON-LD structured data, generating sitemaps, or improving search engine performance."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# SEO Optimization
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Every page must have unique `title` and `description`** — never duplicate meta tags.
|
|
11
|
+
- **One `<h1>` per page** — use proper heading hierarchy `h1` -> `h2` -> `h3`.
|
|
12
|
+
- **Use semantic HTML** — `<main>`, `<article>`, `<section>`, `<nav>`, `<aside>`, `<footer>`.
|
|
13
|
+
- **All images must have `alt` text** — descriptive, not keyword-stuffed.
|
|
14
|
+
- **Target Core Web Vitals** — LCP < 2.5s, INP < 200ms, CLS < 0.1.
|
|
15
|
+
- **Generate `sitemap.xml` and `robots.txt`** — never leave them missing on public sites.
|
|
16
|
+
|
|
9
17
|
## Meta Tags
|
|
10
18
|
|
|
11
19
|
- Every page must have unique `title` and `description` meta tags:
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: server-actions-patterns
|
|
3
|
+
description: "Next.js Server Actions with safe wrappers, validation, and error handling. Use when implementing mutations, creating form handlers, or building 'use server' functions."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Server Actions Patterns
|
|
7
|
+
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Always validate inputs** — never trust client data.
|
|
11
|
+
- **Always check auth** — every action must verify the user session.
|
|
12
|
+
- **Never expose internal errors** — return generic messages to the client.
|
|
13
|
+
- **Revalidate after mutations** — stale UI is a bug.
|
|
14
|
+
- **Keep actions thin** — delegate to facades/services for business logic.
|
|
15
|
+
- **One action per mutation** — avoid multi-purpose actions.
|
|
16
|
+
- **Never import server-only code in client files** — pass actions via props or use wrappers.
|
|
17
|
+
|
|
18
|
+
## File Organization
|
|
19
|
+
|
|
20
|
+
- Server Actions are defined in `src/actions/` directory.
|
|
21
|
+
- One file per domain entity: `src/actions/{entity}.actions.ts`.
|
|
22
|
+
- Every action file starts with `"use server"` directive at the top.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
src/actions/
|
|
26
|
+
user.actions.ts
|
|
27
|
+
post.actions.ts
|
|
28
|
+
organization.actions.ts
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Safe Server Action Wrapper
|
|
32
|
+
|
|
33
|
+
Use a `safeAction` wrapper for consistent validation, auth, and error handling:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// src/lib/safe-action.ts
|
|
37
|
+
"use server";
|
|
38
|
+
import { z } from "zod";
|
|
39
|
+
import { getCurrentUser } from "@/lib/auth";
|
|
40
|
+
|
|
41
|
+
type ActionResult<T> = { success: true; data: T } | { success: false; error: string };
|
|
42
|
+
|
|
43
|
+
export function createSafeAction<TInput extends z.ZodType, TOutput>(
|
|
44
|
+
schema: TInput,
|
|
45
|
+
handler: (input: z.infer<TInput>, user: AuthUser) => Promise<TOutput>
|
|
46
|
+
) {
|
|
47
|
+
return async (input: z.infer<TInput>): Promise<ActionResult<TOutput>> => {
|
|
48
|
+
try {
|
|
49
|
+
const user = await getCurrentUser();
|
|
50
|
+
if (!user) return { success: false, error: "Unauthorized" };
|
|
51
|
+
|
|
52
|
+
const parsed = schema.safeParse(input);
|
|
53
|
+
if (!parsed.success) {
|
|
54
|
+
return { success: false, error: parsed.error.errors[0].message };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const data = await handler(parsed.data, user);
|
|
58
|
+
return { success: true, data };
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error("Action error:", error);
|
|
61
|
+
return { success: false, error: "An unexpected error occurred" };
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Action Implementation
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
// src/actions/user.actions.ts
|
|
71
|
+
"use server";
|
|
72
|
+
|
|
73
|
+
import { z } from "zod";
|
|
74
|
+
import { revalidatePath } from "next/cache";
|
|
75
|
+
import { createSafeAction } from "@/lib/safe-action";
|
|
76
|
+
import { updateUserProfile } from "@/facades/user.facade";
|
|
77
|
+
|
|
78
|
+
const UpdateProfileSchema = z.object({
|
|
79
|
+
name: z.string().min(2).max(100),
|
|
80
|
+
bio: z.string().max(500).optional(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const updateProfile = createSafeAction(
|
|
84
|
+
UpdateProfileSchema,
|
|
85
|
+
async (input, user) => {
|
|
86
|
+
const result = await updateUserProfile(user.id, input);
|
|
87
|
+
revalidatePath("/profile");
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Server-Only Modules
|
|
94
|
+
|
|
95
|
+
- Mark server-only modules with the `server-only` package:
|
|
96
|
+
```ts
|
|
97
|
+
import "server-only";
|
|
98
|
+
```
|
|
99
|
+
- Use `"use server"` only in action files — never in utility or service files.
|
|
100
|
+
|
|
101
|
+
## Integration with Forms
|
|
102
|
+
|
|
103
|
+
### With useActionState (React 19)
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
"use client";
|
|
107
|
+
import { useActionState } from "react";
|
|
108
|
+
import { updateProfile } from "@/actions/user.actions";
|
|
109
|
+
|
|
110
|
+
export function ProfileForm() {
|
|
111
|
+
const [state, formAction, isPending] = useActionState(updateProfile, null);
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<form action={formAction}>
|
|
115
|
+
<input name="name" />
|
|
116
|
+
{state?.error && <p className="text-destructive">{state.error}</p>}
|
|
117
|
+
<button type="submit" disabled={isPending}>
|
|
118
|
+
{isPending ? "Saving..." : "Save"}
|
|
119
|
+
</button>
|
|
120
|
+
</form>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### With react-hook-form
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
"use client";
|
|
129
|
+
import { useForm } from "react-hook-form";
|
|
130
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
131
|
+
import { useTransition } from "react";
|
|
132
|
+
import { updateProfile } from "@/actions/user.actions";
|
|
133
|
+
import { toast } from "sonner";
|
|
134
|
+
|
|
135
|
+
export function ProfileForm() {
|
|
136
|
+
const [isPending, startTransition] = useTransition();
|
|
137
|
+
const form = useForm({ resolver: zodResolver(UpdateProfileSchema) });
|
|
138
|
+
|
|
139
|
+
const onSubmit = form.handleSubmit((data) => {
|
|
140
|
+
startTransition(async () => {
|
|
141
|
+
const result = await updateProfile(data);
|
|
142
|
+
if (result.success) toast.success("Profile updated");
|
|
143
|
+
else toast.error(result.error);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return <form onSubmit={onSubmit}>{/* fields */}</form>;
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Revalidation
|
|
152
|
+
|
|
153
|
+
- Always call `revalidatePath()` or `revalidateTag()` after mutations.
|
|
154
|
+
- Use path revalidation for simple cases: `revalidatePath("/users")`.
|
|
155
|
+
- Use tag revalidation for granular cache control: `revalidateTag("user-list")`.
|
|
156
|
+
- Redirect with `redirect()` after create operations when appropriate.
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: shadcn-ui
|
|
3
|
-
description: "ShadCN UI component patterns,
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "ShadCN UI component patterns, forms, data tables, and page layouts. Use when building interfaces with ShadCN components, creating forms with react-hook-form, tables with TanStack Table, or designing page layouts."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# ShadCN UI Patterns
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Always use ShadCN primitives first** — before building custom components.
|
|
11
|
+
- **Never rebuild keyboard or focus behavior** — use the component primitives.
|
|
12
|
+
- **Never mix primitive systems** — don't combine Radix, Headless UI, React Aria in the same surface.
|
|
13
|
+
- **Never use `h-screen`** — use `h-dvh` for correct mobile viewport.
|
|
14
|
+
- **Empty states must have one clear next action** — never blank screens.
|
|
15
|
+
- **No gradients or glow effects** unless explicitly requested.
|
|
16
|
+
|
|
9
17
|
## Installation & Setup
|
|
10
18
|
|
|
11
19
|
- Install components individually with `npx shadcn@latest add <component>` — never install all at once.
|
|
@@ -27,8 +35,6 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
27
35
|
- `Badge` for tags and status indicators
|
|
28
36
|
- `Tabs` for content switching
|
|
29
37
|
- `Toast` / `Sonner` for notifications
|
|
30
|
-
- Never rebuild keyboard or focus behavior by hand — use the component primitives.
|
|
31
|
-
- Never mix primitive systems (Radix, Headless UI, React Aria) within the same surface.
|
|
32
38
|
|
|
33
39
|
## Forms
|
|
34
40
|
|
|
@@ -52,6 +58,8 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
52
58
|
- Add row actions via `DropdownMenu`.
|
|
53
59
|
- Use `tabular-nums` on numeric columns for alignment.
|
|
54
60
|
- Add loading skeletons with ShadCN `Skeleton` for async data.
|
|
61
|
+
- **Toolbar pattern**: search input + result counter + limit selector in every table.
|
|
62
|
+
- Hide columns progressively by breakpoint: `hidden sm:table-cell`, `hidden md:table-cell`.
|
|
55
63
|
|
|
56
64
|
## Dialogs & Alerts
|
|
57
65
|
|
|
@@ -60,6 +68,40 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
60
68
|
- Keep dialog content focused — one primary action per dialog.
|
|
61
69
|
- Always provide a way to dismiss (close button, escape key, outside click).
|
|
62
70
|
|
|
71
|
+
## Page Layout Pattern
|
|
72
|
+
|
|
73
|
+
Structure pages with Cards for consistent visual hierarchy:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
// Header → Cards → Footer pattern
|
|
77
|
+
<div className="space-y-6">
|
|
78
|
+
<div className="flex items-center justify-between">
|
|
79
|
+
<h1 className="text-3xl font-bold tracking-tight">Page Title</h1>
|
|
80
|
+
<Button>Primary Action</Button>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<Card>
|
|
84
|
+
<CardHeader><CardTitle>Section</CardTitle></CardHeader>
|
|
85
|
+
<CardContent>{/* content */}</CardContent>
|
|
86
|
+
</Card>
|
|
87
|
+
</div>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- Multi-Card forms: split complex forms into logical Card sections.
|
|
91
|
+
- Use `CardHeader` + `CardTitle` + `CardDescription` for section context.
|
|
92
|
+
|
|
93
|
+
## Charts
|
|
94
|
+
|
|
95
|
+
- Use `ChartContainer` from ShadCN with Recharts for data visualization.
|
|
96
|
+
- Wrap charts in a `Card` with descriptive `CardHeader`.
|
|
97
|
+
- Always include a `ChartTooltip` for data point details.
|
|
98
|
+
|
|
99
|
+
## File Upload
|
|
100
|
+
|
|
101
|
+
- Use a `FileUpload` dropzone component with drag-and-drop support.
|
|
102
|
+
- Show preview for images, file name + size for documents.
|
|
103
|
+
- Validate file type and size client-side before upload.
|
|
104
|
+
|
|
63
105
|
## Theming
|
|
64
106
|
|
|
65
107
|
- Use CSS variables from the ShadCN theme system: `bg-background`, `text-foreground`, `bg-card`, `text-muted-foreground`, `bg-primary`, etc.
|
|
@@ -82,11 +124,3 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
82
124
|
- Keep interaction feedback under 200ms.
|
|
83
125
|
- Avoid animating layout properties (`width`, `height`, `margin`, `padding`) — use `transform` and `opacity` only.
|
|
84
126
|
- Respect `prefers-reduced-motion` media query.
|
|
85
|
-
|
|
86
|
-
## Anti-Patterns to Avoid
|
|
87
|
-
|
|
88
|
-
- Never use `h-screen` — use `h-dvh` for correct mobile viewport.
|
|
89
|
-
- Never use arbitrary `z-index` values — use a fixed scale.
|
|
90
|
-
- Never use gradients or glow effects unless explicitly requested.
|
|
91
|
-
- Never use custom easing curves unless explicitly requested.
|
|
92
|
-
- Empty states must have one clear next action.
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stripe-integration
|
|
3
|
-
description: "Stripe payment integration patterns for checkout, subscriptions, and
|
|
4
|
-
allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
3
|
+
description: "Stripe payment integration patterns for checkout, subscriptions, webhooks, and customer portal. Use when implementing payments, building SaaS billing, handling Stripe webhooks, creating checkout flows, or managing subscription lifecycle."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
6
|
# Stripe Integration
|
|
8
7
|
|
|
8
|
+
## Critical Rules
|
|
9
|
+
|
|
10
|
+
- **Always use Stripe Checkout or Payment Elements** — never collect card details directly.
|
|
11
|
+
- **Always verify webhook signatures** — never trust unverified payloads.
|
|
12
|
+
- **Never expose `STRIPE_SECRET_KEY` to the client** — server-side only.
|
|
13
|
+
- **Make webhook handlers idempotent** — the same event may arrive multiple times.
|
|
14
|
+
- **Use `metadata` to link Stripe objects to database records** — always pass `userId`, `orderId`.
|
|
15
|
+
- **Use `idempotencyKey` for critical operations** — prevent duplicate charges.
|
|
16
|
+
|
|
9
17
|
## Setup
|
|
10
18
|
|
|
11
19
|
- Install `stripe` (server) and `@stripe/stripe-js` + `@stripe/react-stripe-js` (client).
|
|
@@ -21,7 +29,7 @@ allowed-tools: "Read, Write, Edit, Glob, Grep"
|
|
|
21
29
|
|
|
22
30
|
## Checkout
|
|
23
31
|
|
|
24
|
-
-
|
|
32
|
+
- Always use Stripe Checkout or Payment Elements — never collect card details directly.
|
|
25
33
|
- Create a Checkout Session server-side, redirect client-side:
|
|
26
34
|
```ts
|
|
27
35
|
const session = await stripe.checkout.sessions.create({
|