@folpe/loom 0.1.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 -15
- 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 +37 -0
- package/data/presets/chrome-extension.yaml +36 -0
- package/data/presets/cli-tool.yaml +31 -0
- package/data/presets/e-commerce.yaml +49 -0
- package/data/presets/expo-mobile.yaml +41 -0
- package/data/presets/fullstack-auth.yaml +45 -0
- package/data/presets/landing-page.yaml +38 -0
- package/data/presets/mvp-lean.yaml +35 -0
- package/data/presets/saas-default.yaml +7 -11
- package/data/presets/saas-full.yaml +71 -0
- package/data/skills/api-design/SKILL.md +149 -0
- 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 +105 -0
- package/data/skills/cli-development/SKILL.md +147 -0
- 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 +87 -0
- 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 +106 -0
- package/data/skills/server-actions-patterns/SKILL.md +156 -0
- package/data/skills/shadcn-ui/SKILL.md +126 -0
- package/data/skills/stripe-integration/SKILL.md +96 -0
- package/data/skills/supabase-patterns/SKILL.md +110 -0
- 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 +179 -0
- package/dist/index.js +254 -100
- package/package.json +2 -1
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: seo-optimization
|
|
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."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SEO Optimization
|
|
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
|
+
|
|
17
|
+
## Meta Tags
|
|
18
|
+
|
|
19
|
+
- Every page must have unique `title` and `description` meta tags:
|
|
20
|
+
```ts
|
|
21
|
+
export const metadata: Metadata = {
|
|
22
|
+
title: 'Product Name — Clear Value Proposition',
|
|
23
|
+
description: 'Compelling 150-160 char description with primary keyword and call to action.',
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
- Title format: `Page Title — Brand Name` (50-60 characters).
|
|
27
|
+
- Description: 150-160 characters, include primary keyword, end with CTA or benefit.
|
|
28
|
+
- Use `generateMetadata()` for dynamic routes to generate unique meta per page.
|
|
29
|
+
|
|
30
|
+
## Open Graph & Social
|
|
31
|
+
|
|
32
|
+
- Include Open Graph tags for social sharing:
|
|
33
|
+
```ts
|
|
34
|
+
openGraph: {
|
|
35
|
+
title: 'Page Title',
|
|
36
|
+
description: 'Social description',
|
|
37
|
+
images: [{ url: '/og-image.png', width: 1200, height: 630 }],
|
|
38
|
+
type: 'website',
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
- Create a dedicated OG image for each important page (1200x630px).
|
|
42
|
+
- Add Twitter card meta: `twitter: { card: 'summary_large_image' }`.
|
|
43
|
+
|
|
44
|
+
## Structured Data (JSON-LD)
|
|
45
|
+
|
|
46
|
+
- Add JSON-LD structured data for rich search results:
|
|
47
|
+
```tsx
|
|
48
|
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify({
|
|
49
|
+
'@context': 'https://schema.org',
|
|
50
|
+
'@type': 'Product',
|
|
51
|
+
name: product.name,
|
|
52
|
+
description: product.description,
|
|
53
|
+
offers: { '@type': 'Offer', price: product.price, priceCurrency: 'EUR' }
|
|
54
|
+
}) }} />
|
|
55
|
+
```
|
|
56
|
+
- Use appropriate schema types: `Organization`, `Product`, `Article`, `FAQPage`, `BreadcrumbList`.
|
|
57
|
+
- Validate with Google's Rich Results Test.
|
|
58
|
+
|
|
59
|
+
## Semantic HTML
|
|
60
|
+
|
|
61
|
+
- Use proper heading hierarchy: one `<h1>` per page, then `<h2>`, `<h3>`, etc.
|
|
62
|
+
- Use semantic elements: `<main>`, `<article>`, `<section>`, `<nav>`, `<aside>`, `<footer>`.
|
|
63
|
+
- Add `alt` text to all images — descriptive, not keyword-stuffed.
|
|
64
|
+
- Use `<a>` with descriptive anchor text — avoid "click here".
|
|
65
|
+
|
|
66
|
+
## Performance (Core Web Vitals)
|
|
67
|
+
|
|
68
|
+
- Target scores: LCP < 2.5s, INP < 200ms, CLS < 0.1.
|
|
69
|
+
- Optimize images with `next/image`: proper sizing, modern formats (WebP/AVIF), lazy loading.
|
|
70
|
+
- Minimize render-blocking resources: defer non-critical CSS/JS.
|
|
71
|
+
- Use `next/font` to prevent FOUT/FOIT.
|
|
72
|
+
- Preload critical assets: hero images, above-the-fold fonts.
|
|
73
|
+
|
|
74
|
+
## Technical SEO
|
|
75
|
+
|
|
76
|
+
- Generate `sitemap.xml` dynamically:
|
|
77
|
+
```ts
|
|
78
|
+
// src/app/sitemap.ts
|
|
79
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
80
|
+
return [
|
|
81
|
+
{ url: 'https://example.com', lastModified: new Date(), changeFrequency: 'weekly', priority: 1 },
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
- Create `robots.txt`:
|
|
86
|
+
```ts
|
|
87
|
+
// src/app/robots.ts
|
|
88
|
+
export default function robots(): MetadataRoute.Robots {
|
|
89
|
+
return { rules: { userAgent: '*', allow: '/' }, sitemap: 'https://example.com/sitemap.xml' }
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
- Use canonical URLs to prevent duplicate content issues.
|
|
93
|
+
- Implement proper redirects (301) for moved pages — never soft 404s.
|
|
94
|
+
|
|
95
|
+
## Content
|
|
96
|
+
|
|
97
|
+
- Place primary keyword in the first 100 words of page content.
|
|
98
|
+
- Use internal links between related pages to distribute page authority.
|
|
99
|
+
- Add breadcrumb navigation with `BreadcrumbList` schema.
|
|
100
|
+
- Ensure all pages are reachable within 3 clicks from the homepage.
|
|
101
|
+
|
|
102
|
+
## Internationalization
|
|
103
|
+
|
|
104
|
+
- Use `hreflang` tags for multi-language sites.
|
|
105
|
+
- Use `lang` attribute on `<html>` element.
|
|
106
|
+
- Create dedicated URLs per language: `/en/about`, `/fr/about`.
|
|
@@ -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.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shadcn-ui
|
|
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."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ShadCN UI Patterns
|
|
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
|
+
|
|
17
|
+
## Installation & Setup
|
|
18
|
+
|
|
19
|
+
- Install components individually with `npx shadcn@latest add <component>` — never install all at once.
|
|
20
|
+
- Components are copied to `src/components/ui/` — they are yours to customize.
|
|
21
|
+
- Configure `components.json` for path aliases and styling preferences.
|
|
22
|
+
- Use `cn()` from `src/lib/utils` for class merging (clsx + tailwind-merge).
|
|
23
|
+
|
|
24
|
+
## Component Usage
|
|
25
|
+
|
|
26
|
+
- **Always use ShadCN primitives** before building custom components:
|
|
27
|
+
- `Button` for all clickable actions (with appropriate `variant` and `size`)
|
|
28
|
+
- `Card` for content containers
|
|
29
|
+
- `Dialog` for modal overlays
|
|
30
|
+
- `Sheet` for slide-out panels
|
|
31
|
+
- `DropdownMenu` for action menus
|
|
32
|
+
- `Select` for option picking
|
|
33
|
+
- `Table` for data display
|
|
34
|
+
- `Form` + `Input` + `Label` for forms
|
|
35
|
+
- `Badge` for tags and status indicators
|
|
36
|
+
- `Tabs` for content switching
|
|
37
|
+
- `Toast` / `Sonner` for notifications
|
|
38
|
+
|
|
39
|
+
## Forms
|
|
40
|
+
|
|
41
|
+
- Use `react-hook-form` + `zod` with ShadCN's `Form` component:
|
|
42
|
+
```tsx
|
|
43
|
+
const form = useForm<z.infer<typeof schema>>({
|
|
44
|
+
resolver: zodResolver(schema),
|
|
45
|
+
defaultValues: { name: '' },
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
- Stack form fields with `space-y-4`.
|
|
49
|
+
- Use `grid gap-4 md:grid-cols-2` for side-by-side fields on desktop.
|
|
50
|
+
- Show validation errors inline with `FormMessage`.
|
|
51
|
+
- Use `FormDescription` for helper text below fields.
|
|
52
|
+
|
|
53
|
+
## Data Tables
|
|
54
|
+
|
|
55
|
+
- Use ShadCN `DataTable` pattern with `@tanstack/react-table`:
|
|
56
|
+
- Define columns with type-safe `ColumnDef`.
|
|
57
|
+
- Support sorting, filtering, and pagination.
|
|
58
|
+
- Add row actions via `DropdownMenu`.
|
|
59
|
+
- Use `tabular-nums` on numeric columns for alignment.
|
|
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`.
|
|
63
|
+
|
|
64
|
+
## Dialogs & Alerts
|
|
65
|
+
|
|
66
|
+
- Use `Dialog` for informational or form-based modals.
|
|
67
|
+
- Use `AlertDialog` for destructive or irreversible actions — never a plain `Dialog`.
|
|
68
|
+
- Keep dialog content focused — one primary action per dialog.
|
|
69
|
+
- Always provide a way to dismiss (close button, escape key, outside click).
|
|
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
|
+
|
|
105
|
+
## Theming
|
|
106
|
+
|
|
107
|
+
- Use CSS variables from the ShadCN theme system: `bg-background`, `text-foreground`, `bg-card`, `text-muted-foreground`, `bg-primary`, etc.
|
|
108
|
+
- Never hardcode hex colors — always use semantic tokens.
|
|
109
|
+
- Support dark mode via the `dark` class on `<html>` — CSS variables handle the switch.
|
|
110
|
+
- Limit accent color usage to one per view.
|
|
111
|
+
|
|
112
|
+
## Accessibility
|
|
113
|
+
|
|
114
|
+
- All icon-only buttons must have `aria-label`.
|
|
115
|
+
- Use `sr-only` class for screen-reader-only text.
|
|
116
|
+
- Ensure all interactive elements have visible focus states (ShadCN handles this by default).
|
|
117
|
+
- Use `role` and `aria-*` attributes when composing custom components.
|
|
118
|
+
- Never block paste in `input` or `textarea` elements.
|
|
119
|
+
|
|
120
|
+
## Animation
|
|
121
|
+
|
|
122
|
+
- Never add animation unless explicitly requested.
|
|
123
|
+
- Use `transition-colors` for hover/focus state changes.
|
|
124
|
+
- Keep interaction feedback under 200ms.
|
|
125
|
+
- Avoid animating layout properties (`width`, `height`, `margin`, `padding`) — use `transform` and `opacity` only.
|
|
126
|
+
- Respect `prefers-reduced-motion` media query.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: stripe-integration
|
|
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."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Stripe Integration
|
|
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
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
- Install `stripe` (server) and `@stripe/stripe-js` + `@stripe/react-stripe-js` (client).
|
|
20
|
+
- Store keys in environment variables:
|
|
21
|
+
- `STRIPE_SECRET_KEY` — server-side only, never expose to client
|
|
22
|
+
- `NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY` — safe for client-side
|
|
23
|
+
- `STRIPE_WEBHOOK_SECRET` — for verifying webhook signatures
|
|
24
|
+
- Create a Stripe instance in `src/lib/stripe.ts`:
|
|
25
|
+
```ts
|
|
26
|
+
import Stripe from 'stripe'
|
|
27
|
+
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Checkout
|
|
31
|
+
|
|
32
|
+
- Always use Stripe Checkout or Payment Elements — never collect card details directly.
|
|
33
|
+
- Create a Checkout Session server-side, redirect client-side:
|
|
34
|
+
```ts
|
|
35
|
+
const session = await stripe.checkout.sessions.create({
|
|
36
|
+
mode: 'payment', // or 'subscription'
|
|
37
|
+
line_items: [{ price: priceId, quantity: 1 }],
|
|
38
|
+
success_url: `${origin}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
39
|
+
cancel_url: `${origin}/cart`,
|
|
40
|
+
metadata: { userId, orderId },
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
- Use `metadata` to link Stripe objects back to your database records.
|
|
44
|
+
- For subscriptions, use `mode: 'subscription'` with recurring prices.
|
|
45
|
+
|
|
46
|
+
## Webhooks
|
|
47
|
+
|
|
48
|
+
- Create a webhook endpoint at `/api/webhooks/stripe`:
|
|
49
|
+
```ts
|
|
50
|
+
const sig = request.headers.get('stripe-signature')!
|
|
51
|
+
const event = stripe.webhooks.constructEvent(body, sig, process.env.STRIPE_WEBHOOK_SECRET!)
|
|
52
|
+
```
|
|
53
|
+
- Always verify webhook signatures. Never trust unverified payloads.
|
|
54
|
+
- Handle these critical events:
|
|
55
|
+
- `checkout.session.completed` — fulfill the order
|
|
56
|
+
- `invoice.payment_succeeded` — extend subscription
|
|
57
|
+
- `invoice.payment_failed` — notify user, retry logic
|
|
58
|
+
- `customer.subscription.deleted` — revoke access
|
|
59
|
+
- Make webhook handlers idempotent — the same event may arrive multiple times.
|
|
60
|
+
- Return 200 quickly. Process heavy logic asynchronously if needed.
|
|
61
|
+
|
|
62
|
+
## Products & Prices
|
|
63
|
+
|
|
64
|
+
- Define products and prices in Stripe Dashboard for simplicity.
|
|
65
|
+
- Use `price` IDs (not `product` IDs) when creating Checkout Sessions.
|
|
66
|
+
- For multiple pricing tiers, create one product with multiple prices (monthly/yearly).
|
|
67
|
+
- Sync product/price data to your database via webhooks or a scheduled job.
|
|
68
|
+
|
|
69
|
+
## Customer Portal
|
|
70
|
+
|
|
71
|
+
- Use Stripe Customer Portal for subscription management (upgrade, downgrade, cancel, update payment method).
|
|
72
|
+
- Create a portal session and redirect:
|
|
73
|
+
```ts
|
|
74
|
+
const portalSession = await stripe.billingPortal.sessions.create({
|
|
75
|
+
customer: customerId,
|
|
76
|
+
return_url: `${origin}/account`,
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Security
|
|
81
|
+
|
|
82
|
+
- Never log or store raw card numbers, CVV, or full card details.
|
|
83
|
+
- Use Stripe's PCI-compliant elements for all card input.
|
|
84
|
+
- Validate amounts and currencies server-side before creating charges.
|
|
85
|
+
- Use `idempotencyKey` for critical operations to prevent duplicate charges.
|
|
86
|
+
- Restrict Stripe API key permissions in production (read-only where possible).
|
|
87
|
+
|
|
88
|
+
## Testing
|
|
89
|
+
|
|
90
|
+
- Use Stripe test mode keys during development.
|
|
91
|
+
- Use test card numbers: `4242424242424242` (success), `4000000000000002` (decline).
|
|
92
|
+
- Use Stripe CLI to forward webhooks to localhost:
|
|
93
|
+
```bash
|
|
94
|
+
stripe listen --forward-to localhost:3000/api/webhooks/stripe
|
|
95
|
+
```
|
|
96
|
+
- Test all webhook event types, especially failure scenarios.
|