@girardmedia/bootspring 1.2.0 → 2.0.3
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 +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -1,503 +0,0 @@
|
|
|
1
|
-
# Transactional Email Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for sending transactional emails.
|
|
4
|
-
|
|
5
|
-
## Email Service Setup
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/email/index.ts
|
|
9
|
-
import { Resend } from 'resend'
|
|
10
|
-
|
|
11
|
-
export const resend = new Resend(process.env.RESEND_API_KEY)
|
|
12
|
-
|
|
13
|
-
interface SendEmailOptions {
|
|
14
|
-
to: string | string[]
|
|
15
|
-
subject: string
|
|
16
|
-
html: string
|
|
17
|
-
text?: string
|
|
18
|
-
from?: string
|
|
19
|
-
replyTo?: string
|
|
20
|
-
tags?: { name: string; value: string }[]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export async function sendEmail(options: SendEmailOptions) {
|
|
24
|
-
const { to, subject, html, text, from, replyTo, tags } = options
|
|
25
|
-
|
|
26
|
-
const result = await resend.emails.send({
|
|
27
|
-
from: from ?? `${process.env.EMAIL_FROM_NAME} <${process.env.EMAIL_FROM_ADDRESS}>`,
|
|
28
|
-
to: Array.isArray(to) ? to : [to],
|
|
29
|
-
subject,
|
|
30
|
-
html,
|
|
31
|
-
text: text ?? stripHtml(html),
|
|
32
|
-
reply_to: replyTo,
|
|
33
|
-
tags
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
if (result.error) {
|
|
37
|
-
console.error('Email send failed:', result.error)
|
|
38
|
-
throw new Error(`Failed to send email: ${result.error.message}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return result.data
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function stripHtml(html: string): string {
|
|
45
|
-
return html.replace(/<[^>]*>/g, '').trim()
|
|
46
|
-
}
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
## Email Templates
|
|
50
|
-
|
|
51
|
-
```tsx
|
|
52
|
-
// emails/WelcomeEmail.tsx
|
|
53
|
-
import {
|
|
54
|
-
Body,
|
|
55
|
-
Button,
|
|
56
|
-
Container,
|
|
57
|
-
Head,
|
|
58
|
-
Heading,
|
|
59
|
-
Html,
|
|
60
|
-
Img,
|
|
61
|
-
Link,
|
|
62
|
-
Preview,
|
|
63
|
-
Section,
|
|
64
|
-
Text
|
|
65
|
-
} from '@react-email/components'
|
|
66
|
-
|
|
67
|
-
interface WelcomeEmailProps {
|
|
68
|
-
name: string
|
|
69
|
-
loginUrl: string
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
|
|
73
|
-
return (
|
|
74
|
-
<Html>
|
|
75
|
-
<Head />
|
|
76
|
-
<Preview>Welcome to our platform!</Preview>
|
|
77
|
-
<Body style={main}>
|
|
78
|
-
<Container style={container}>
|
|
79
|
-
<Img
|
|
80
|
-
src={`${process.env.NEXT_PUBLIC_APP_URL}/logo.png`}
|
|
81
|
-
width="150"
|
|
82
|
-
height="50"
|
|
83
|
-
alt="Logo"
|
|
84
|
-
/>
|
|
85
|
-
|
|
86
|
-
<Heading style={heading}>Welcome, {name}!</Heading>
|
|
87
|
-
|
|
88
|
-
<Text style={text}>
|
|
89
|
-
Thank you for joining us. We're excited to have you on board.
|
|
90
|
-
</Text>
|
|
91
|
-
|
|
92
|
-
<Section style={buttonContainer}>
|
|
93
|
-
<Button style={button} href={loginUrl}>
|
|
94
|
-
Get Started
|
|
95
|
-
</Button>
|
|
96
|
-
</Section>
|
|
97
|
-
|
|
98
|
-
<Text style={footer}>
|
|
99
|
-
If you didn't create this account, please ignore this email.
|
|
100
|
-
</Text>
|
|
101
|
-
</Container>
|
|
102
|
-
</Body>
|
|
103
|
-
</Html>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const main = {
|
|
108
|
-
backgroundColor: '#f6f9fc',
|
|
109
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const container = {
|
|
113
|
-
backgroundColor: '#ffffff',
|
|
114
|
-
margin: '0 auto',
|
|
115
|
-
padding: '40px 20px',
|
|
116
|
-
maxWidth: '600px'
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const heading = {
|
|
120
|
-
fontSize: '24px',
|
|
121
|
-
fontWeight: '600',
|
|
122
|
-
color: '#1a1a1a',
|
|
123
|
-
margin: '30px 0'
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const text = {
|
|
127
|
-
fontSize: '16px',
|
|
128
|
-
color: '#4a4a4a',
|
|
129
|
-
lineHeight: '26px'
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const buttonContainer = {
|
|
133
|
-
textAlign: 'center' as const,
|
|
134
|
-
margin: '30px 0'
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const button = {
|
|
138
|
-
backgroundColor: '#2563eb',
|
|
139
|
-
borderRadius: '6px',
|
|
140
|
-
color: '#ffffff',
|
|
141
|
-
fontSize: '16px',
|
|
142
|
-
fontWeight: '600',
|
|
143
|
-
padding: '12px 24px',
|
|
144
|
-
textDecoration: 'none'
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const footer = {
|
|
148
|
-
fontSize: '14px',
|
|
149
|
-
color: '#8a8a8a',
|
|
150
|
-
marginTop: '40px'
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export default WelcomeEmail
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
## Password Reset Email
|
|
157
|
-
|
|
158
|
-
```tsx
|
|
159
|
-
// emails/PasswordResetEmail.tsx
|
|
160
|
-
import {
|
|
161
|
-
Body,
|
|
162
|
-
Button,
|
|
163
|
-
Container,
|
|
164
|
-
Head,
|
|
165
|
-
Heading,
|
|
166
|
-
Html,
|
|
167
|
-
Preview,
|
|
168
|
-
Section,
|
|
169
|
-
Text
|
|
170
|
-
} from '@react-email/components'
|
|
171
|
-
|
|
172
|
-
interface PasswordResetEmailProps {
|
|
173
|
-
name: string
|
|
174
|
-
resetUrl: string
|
|
175
|
-
expiresIn: string
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function PasswordResetEmail({
|
|
179
|
-
name,
|
|
180
|
-
resetUrl,
|
|
181
|
-
expiresIn
|
|
182
|
-
}: PasswordResetEmailProps) {
|
|
183
|
-
return (
|
|
184
|
-
<Html>
|
|
185
|
-
<Head />
|
|
186
|
-
<Preview>Reset your password</Preview>
|
|
187
|
-
<Body style={main}>
|
|
188
|
-
<Container style={container}>
|
|
189
|
-
<Heading style={heading}>Reset Your Password</Heading>
|
|
190
|
-
|
|
191
|
-
<Text style={text}>Hi {name},</Text>
|
|
192
|
-
|
|
193
|
-
<Text style={text}>
|
|
194
|
-
We received a request to reset your password. Click the button
|
|
195
|
-
below to choose a new password.
|
|
196
|
-
</Text>
|
|
197
|
-
|
|
198
|
-
<Section style={buttonContainer}>
|
|
199
|
-
<Button style={button} href={resetUrl}>
|
|
200
|
-
Reset Password
|
|
201
|
-
</Button>
|
|
202
|
-
</Section>
|
|
203
|
-
|
|
204
|
-
<Text style={text}>
|
|
205
|
-
This link will expire in {expiresIn}. If you didn't request
|
|
206
|
-
a password reset, you can safely ignore this email.
|
|
207
|
-
</Text>
|
|
208
|
-
|
|
209
|
-
<Text style={smallText}>
|
|
210
|
-
If the button doesn't work, copy and paste this URL into your browser:
|
|
211
|
-
<br />
|
|
212
|
-
{resetUrl}
|
|
213
|
-
</Text>
|
|
214
|
-
</Container>
|
|
215
|
-
</Body>
|
|
216
|
-
</Html>
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ... styles same as WelcomeEmail
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Order Confirmation Email
|
|
224
|
-
|
|
225
|
-
```tsx
|
|
226
|
-
// emails/OrderConfirmationEmail.tsx
|
|
227
|
-
import {
|
|
228
|
-
Body,
|
|
229
|
-
Column,
|
|
230
|
-
Container,
|
|
231
|
-
Head,
|
|
232
|
-
Heading,
|
|
233
|
-
Hr,
|
|
234
|
-
Html,
|
|
235
|
-
Img,
|
|
236
|
-
Preview,
|
|
237
|
-
Row,
|
|
238
|
-
Section,
|
|
239
|
-
Text
|
|
240
|
-
} from '@react-email/components'
|
|
241
|
-
|
|
242
|
-
interface OrderItem {
|
|
243
|
-
name: string
|
|
244
|
-
quantity: number
|
|
245
|
-
price: number
|
|
246
|
-
image?: string
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
interface OrderConfirmationEmailProps {
|
|
250
|
-
orderNumber: string
|
|
251
|
-
customerName: string
|
|
252
|
-
items: OrderItem[]
|
|
253
|
-
subtotal: number
|
|
254
|
-
shipping: number
|
|
255
|
-
tax: number
|
|
256
|
-
total: number
|
|
257
|
-
shippingAddress: {
|
|
258
|
-
line1: string
|
|
259
|
-
line2?: string
|
|
260
|
-
city: string
|
|
261
|
-
state: string
|
|
262
|
-
postalCode: string
|
|
263
|
-
country: string
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export function OrderConfirmationEmail({
|
|
268
|
-
orderNumber,
|
|
269
|
-
customerName,
|
|
270
|
-
items,
|
|
271
|
-
subtotal,
|
|
272
|
-
shipping,
|
|
273
|
-
tax,
|
|
274
|
-
total,
|
|
275
|
-
shippingAddress
|
|
276
|
-
}: OrderConfirmationEmailProps) {
|
|
277
|
-
const formatCurrency = (amount: number) =>
|
|
278
|
-
new Intl.NumberFormat('en-US', {
|
|
279
|
-
style: 'currency',
|
|
280
|
-
currency: 'USD'
|
|
281
|
-
}).format(amount)
|
|
282
|
-
|
|
283
|
-
return (
|
|
284
|
-
<Html>
|
|
285
|
-
<Head />
|
|
286
|
-
<Preview>Order #{orderNumber} confirmed</Preview>
|
|
287
|
-
<Body style={main}>
|
|
288
|
-
<Container style={container}>
|
|
289
|
-
<Heading style={heading}>Order Confirmed!</Heading>
|
|
290
|
-
|
|
291
|
-
<Text style={text}>Hi {customerName},</Text>
|
|
292
|
-
|
|
293
|
-
<Text style={text}>
|
|
294
|
-
Thank you for your order. We'll send you a shipping confirmation
|
|
295
|
-
when your order ships.
|
|
296
|
-
</Text>
|
|
297
|
-
|
|
298
|
-
<Section style={orderInfo}>
|
|
299
|
-
<Text style={orderNumber}>Order #{orderNumber}</Text>
|
|
300
|
-
</Section>
|
|
301
|
-
|
|
302
|
-
<Hr style={divider} />
|
|
303
|
-
|
|
304
|
-
{/* Order Items */}
|
|
305
|
-
<Section>
|
|
306
|
-
{items.map((item, index) => (
|
|
307
|
-
<Row key={index} style={itemRow}>
|
|
308
|
-
{item.image && (
|
|
309
|
-
<Column style={imageColumn}>
|
|
310
|
-
<Img
|
|
311
|
-
src={item.image}
|
|
312
|
-
width="60"
|
|
313
|
-
height="60"
|
|
314
|
-
alt={item.name}
|
|
315
|
-
style={itemImage}
|
|
316
|
-
/>
|
|
317
|
-
</Column>
|
|
318
|
-
)}
|
|
319
|
-
<Column style={detailsColumn}>
|
|
320
|
-
<Text style={itemName}>{item.name}</Text>
|
|
321
|
-
<Text style={itemQuantity}>Qty: {item.quantity}</Text>
|
|
322
|
-
</Column>
|
|
323
|
-
<Column style={priceColumn}>
|
|
324
|
-
<Text style={itemPrice}>{formatCurrency(item.price)}</Text>
|
|
325
|
-
</Column>
|
|
326
|
-
</Row>
|
|
327
|
-
))}
|
|
328
|
-
</Section>
|
|
329
|
-
|
|
330
|
-
<Hr style={divider} />
|
|
331
|
-
|
|
332
|
-
{/* Order Summary */}
|
|
333
|
-
<Section style={summarySection}>
|
|
334
|
-
<Row>
|
|
335
|
-
<Column style={summaryLabel}>Subtotal</Column>
|
|
336
|
-
<Column style={summaryValue}>{formatCurrency(subtotal)}</Column>
|
|
337
|
-
</Row>
|
|
338
|
-
<Row>
|
|
339
|
-
<Column style={summaryLabel}>Shipping</Column>
|
|
340
|
-
<Column style={summaryValue}>{formatCurrency(shipping)}</Column>
|
|
341
|
-
</Row>
|
|
342
|
-
<Row>
|
|
343
|
-
<Column style={summaryLabel}>Tax</Column>
|
|
344
|
-
<Column style={summaryValue}>{formatCurrency(tax)}</Column>
|
|
345
|
-
</Row>
|
|
346
|
-
<Row style={totalRow}>
|
|
347
|
-
<Column style={totalLabel}>Total</Column>
|
|
348
|
-
<Column style={totalValue}>{formatCurrency(total)}</Column>
|
|
349
|
-
</Row>
|
|
350
|
-
</Section>
|
|
351
|
-
|
|
352
|
-
<Hr style={divider} />
|
|
353
|
-
|
|
354
|
-
{/* Shipping Address */}
|
|
355
|
-
<Section>
|
|
356
|
-
<Text style={sectionHeading}>Shipping Address</Text>
|
|
357
|
-
<Text style={addressText}>
|
|
358
|
-
{shippingAddress.line1}
|
|
359
|
-
{shippingAddress.line2 && <br />}
|
|
360
|
-
{shippingAddress.line2}
|
|
361
|
-
<br />
|
|
362
|
-
{shippingAddress.city}, {shippingAddress.state}{' '}
|
|
363
|
-
{shippingAddress.postalCode}
|
|
364
|
-
<br />
|
|
365
|
-
{shippingAddress.country}
|
|
366
|
-
</Text>
|
|
367
|
-
</Section>
|
|
368
|
-
</Container>
|
|
369
|
-
</Body>
|
|
370
|
-
</Html>
|
|
371
|
-
)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Styles...
|
|
375
|
-
const main = {
|
|
376
|
-
backgroundColor: '#f6f9fc',
|
|
377
|
-
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const container = {
|
|
381
|
-
backgroundColor: '#ffffff',
|
|
382
|
-
margin: '0 auto',
|
|
383
|
-
padding: '40px 20px',
|
|
384
|
-
maxWidth: '600px'
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// ... additional styles
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
## Email Sending Service
|
|
391
|
-
|
|
392
|
-
```typescript
|
|
393
|
-
// lib/email/service.ts
|
|
394
|
-
import { render } from '@react-email/render'
|
|
395
|
-
import { sendEmail } from '.'
|
|
396
|
-
import WelcomeEmail from '@/emails/WelcomeEmail'
|
|
397
|
-
import PasswordResetEmail from '@/emails/PasswordResetEmail'
|
|
398
|
-
import OrderConfirmationEmail from '@/emails/OrderConfirmationEmail'
|
|
399
|
-
|
|
400
|
-
export const emailService = {
|
|
401
|
-
async sendWelcome(to: string, name: string) {
|
|
402
|
-
const loginUrl = `${process.env.NEXT_PUBLIC_APP_URL}/login`
|
|
403
|
-
|
|
404
|
-
const html = await render(WelcomeEmail({ name, loginUrl }))
|
|
405
|
-
|
|
406
|
-
return sendEmail({
|
|
407
|
-
to,
|
|
408
|
-
subject: 'Welcome to Our Platform!',
|
|
409
|
-
html,
|
|
410
|
-
tags: [{ name: 'category', value: 'welcome' }]
|
|
411
|
-
})
|
|
412
|
-
},
|
|
413
|
-
|
|
414
|
-
async sendPasswordReset(to: string, name: string, token: string) {
|
|
415
|
-
const resetUrl = `${process.env.NEXT_PUBLIC_APP_URL}/reset-password?token=${token}`
|
|
416
|
-
|
|
417
|
-
const html = await render(
|
|
418
|
-
PasswordResetEmail({ name, resetUrl, expiresIn: '1 hour' })
|
|
419
|
-
)
|
|
420
|
-
|
|
421
|
-
return sendEmail({
|
|
422
|
-
to,
|
|
423
|
-
subject: 'Reset Your Password',
|
|
424
|
-
html,
|
|
425
|
-
tags: [{ name: 'category', value: 'password-reset' }]
|
|
426
|
-
})
|
|
427
|
-
},
|
|
428
|
-
|
|
429
|
-
async sendOrderConfirmation(
|
|
430
|
-
to: string,
|
|
431
|
-
orderData: Parameters<typeof OrderConfirmationEmail>[0]
|
|
432
|
-
) {
|
|
433
|
-
const html = await render(OrderConfirmationEmail(orderData))
|
|
434
|
-
|
|
435
|
-
return sendEmail({
|
|
436
|
-
to,
|
|
437
|
-
subject: `Order Confirmed - #${orderData.orderNumber}`,
|
|
438
|
-
html,
|
|
439
|
-
tags: [
|
|
440
|
-
{ name: 'category', value: 'order' },
|
|
441
|
-
{ name: 'order_id', value: orderData.orderNumber }
|
|
442
|
-
]
|
|
443
|
-
})
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
## Email Preview Route
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
// app/api/email-preview/[template]/route.ts
|
|
452
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
453
|
-
import { render } from '@react-email/render'
|
|
454
|
-
import WelcomeEmail from '@/emails/WelcomeEmail'
|
|
455
|
-
import PasswordResetEmail from '@/emails/PasswordResetEmail'
|
|
456
|
-
|
|
457
|
-
const templates: Record<string, React.ComponentType<any>> = {
|
|
458
|
-
welcome: WelcomeEmail,
|
|
459
|
-
'password-reset': PasswordResetEmail
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
const mockData: Record<string, any> = {
|
|
463
|
-
welcome: {
|
|
464
|
-
name: 'John Doe',
|
|
465
|
-
loginUrl: 'https://example.com/login'
|
|
466
|
-
},
|
|
467
|
-
'password-reset': {
|
|
468
|
-
name: 'John Doe',
|
|
469
|
-
resetUrl: 'https://example.com/reset?token=abc123',
|
|
470
|
-
expiresIn: '1 hour'
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
export async function GET(
|
|
475
|
-
request: NextRequest,
|
|
476
|
-
{ params }: { params: { template: string } }
|
|
477
|
-
) {
|
|
478
|
-
// Only allow in development
|
|
479
|
-
if (process.env.NODE_ENV !== 'development') {
|
|
480
|
-
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const Template = templates[params.template]
|
|
484
|
-
const data = mockData[params.template]
|
|
485
|
-
|
|
486
|
-
if (!Template || !data) {
|
|
487
|
-
return NextResponse.json({ error: 'Template not found' }, { status: 404 })
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const html = await render(Template(data))
|
|
491
|
-
|
|
492
|
-
return new NextResponse(html, {
|
|
493
|
-
headers: { 'Content-Type': 'text/html' }
|
|
494
|
-
})
|
|
495
|
-
}
|
|
496
|
-
```
|
|
497
|
-
|
|
498
|
-
## When to Use
|
|
499
|
-
|
|
500
|
-
- Welcome emails
|
|
501
|
-
- Password resets
|
|
502
|
-
- Order confirmations
|
|
503
|
-
- Account notifications
|
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
# Email Verification Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for email verification flows.
|
|
4
|
-
|
|
5
|
-
## Token Generation
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/verification.ts
|
|
9
|
-
import { prisma } from '@/lib/db'
|
|
10
|
-
import { randomBytes } from 'crypto'
|
|
11
|
-
|
|
12
|
-
export async function createVerificationToken(email: string) {
|
|
13
|
-
const token = randomBytes(32).toString('hex')
|
|
14
|
-
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours
|
|
15
|
-
|
|
16
|
-
// Delete any existing tokens
|
|
17
|
-
await prisma.verificationToken.deleteMany({
|
|
18
|
-
where: { identifier: email }
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
// Create new token
|
|
22
|
-
await prisma.verificationToken.create({
|
|
23
|
-
data: {
|
|
24
|
-
identifier: email,
|
|
25
|
-
token,
|
|
26
|
-
expires
|
|
27
|
-
}
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
return token
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
## Send Verification Email
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
// lib/verification.ts
|
|
38
|
-
import { emailService } from '@/lib/email-service'
|
|
39
|
-
|
|
40
|
-
export async function sendVerificationEmail(email: string) {
|
|
41
|
-
const token = await createVerificationToken(email)
|
|
42
|
-
|
|
43
|
-
const verifyUrl = `${process.env.NEXT_PUBLIC_APP_URL}/verify-email?token=${token}`
|
|
44
|
-
|
|
45
|
-
await emailService.sendVerification(email, verifyUrl)
|
|
46
|
-
|
|
47
|
-
return { success: true }
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Verify Token
|
|
52
|
-
|
|
53
|
-
```typescript
|
|
54
|
-
// lib/verification.ts
|
|
55
|
-
export async function verifyEmail(token: string) {
|
|
56
|
-
const record = await prisma.verificationToken.findUnique({
|
|
57
|
-
where: { token }
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
if (!record) {
|
|
61
|
-
throw new Error('Invalid verification token')
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (record.expires < new Date()) {
|
|
65
|
-
await prisma.verificationToken.delete({ where: { token } })
|
|
66
|
-
throw new Error('Verification token expired')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Mark email as verified
|
|
70
|
-
await prisma.user.update({
|
|
71
|
-
where: { email: record.identifier },
|
|
72
|
-
data: { emailVerified: new Date() }
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// Delete used token
|
|
76
|
-
await prisma.verificationToken.delete({ where: { token } })
|
|
77
|
-
|
|
78
|
-
return { email: record.identifier }
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Verification Page
|
|
83
|
-
|
|
84
|
-
```tsx
|
|
85
|
-
// app/verify-email/page.tsx
|
|
86
|
-
import { verifyEmail } from '@/lib/verification'
|
|
87
|
-
import { redirect } from 'next/navigation'
|
|
88
|
-
|
|
89
|
-
export default async function VerifyEmailPage({
|
|
90
|
-
searchParams
|
|
91
|
-
}: {
|
|
92
|
-
searchParams: { token?: string }
|
|
93
|
-
}) {
|
|
94
|
-
if (!searchParams.token) {
|
|
95
|
-
redirect('/login?error=missing-token')
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
await verifyEmail(searchParams.token)
|
|
100
|
-
redirect('/login?verified=true')
|
|
101
|
-
} catch (error) {
|
|
102
|
-
redirect('/login?error=verification-failed')
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Resend Verification
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// app/api/resend-verification/route.ts
|
|
111
|
-
import { sendVerificationEmail } from '@/lib/verification'
|
|
112
|
-
import { auth } from '@/auth'
|
|
113
|
-
|
|
114
|
-
export async function POST() {
|
|
115
|
-
const session = await auth()
|
|
116
|
-
|
|
117
|
-
if (!session?.user?.email) {
|
|
118
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Rate limiting
|
|
122
|
-
const lastSent = await prisma.verificationToken.findFirst({
|
|
123
|
-
where: { identifier: session.user.email },
|
|
124
|
-
orderBy: { createdAt: 'desc' }
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
if (lastSent && lastSent.createdAt > new Date(Date.now() - 60000)) {
|
|
128
|
-
return Response.json(
|
|
129
|
-
{ error: 'Please wait before requesting another email' },
|
|
130
|
-
{ status: 429 }
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
await sendVerificationEmail(session.user.email)
|
|
135
|
-
|
|
136
|
-
return Response.json({ success: true })
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
## Check Verification Status
|
|
141
|
-
|
|
142
|
-
```typescript
|
|
143
|
-
// lib/verification.ts
|
|
144
|
-
export async function isEmailVerified(userId: string) {
|
|
145
|
-
const user = await prisma.user.findUnique({
|
|
146
|
-
where: { id: userId },
|
|
147
|
-
select: { emailVerified: true }
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
return !!user?.emailVerified
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Middleware or guard
|
|
154
|
-
export async function requireVerifiedEmail() {
|
|
155
|
-
const session = await auth()
|
|
156
|
-
|
|
157
|
-
if (!session?.user) {
|
|
158
|
-
throw new Error('Unauthorized')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const verified = await isEmailVerified(session.user.id)
|
|
162
|
-
|
|
163
|
-
if (!verified) {
|
|
164
|
-
throw new Error('Email not verified')
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return session
|
|
168
|
-
}
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
## When to Use
|
|
172
|
-
|
|
173
|
-
- User registration
|
|
174
|
-
- Email change confirmation
|
|
175
|
-
- Security-sensitive operations
|
|
176
|
-
- Marketing consent verification
|