@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,243 +0,0 @@
|
|
|
1
|
-
# File Download Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for serving file downloads.
|
|
4
|
-
|
|
5
|
-
## Basic File Download
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// app/api/download/[filename]/route.ts
|
|
9
|
-
import { readFile, stat } from 'fs/promises'
|
|
10
|
-
import { join } from 'path'
|
|
11
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
12
|
-
|
|
13
|
-
const UPLOAD_DIR = join(process.cwd(), 'uploads')
|
|
14
|
-
|
|
15
|
-
export async function GET(
|
|
16
|
-
request: NextRequest,
|
|
17
|
-
{ params }: { params: { filename: string } }
|
|
18
|
-
) {
|
|
19
|
-
const filePath = join(UPLOAD_DIR, params.filename)
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
const fileBuffer = await readFile(filePath)
|
|
23
|
-
const stats = await stat(filePath)
|
|
24
|
-
|
|
25
|
-
return new NextResponse(fileBuffer, {
|
|
26
|
-
headers: {
|
|
27
|
-
'Content-Type': 'application/octet-stream',
|
|
28
|
-
'Content-Disposition': `attachment; filename="${params.filename}"`,
|
|
29
|
-
'Content-Length': stats.size.toString()
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
} catch {
|
|
33
|
-
return NextResponse.json({ error: 'File not found' }, { status: 404 })
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Protected Download
|
|
39
|
-
|
|
40
|
-
```typescript
|
|
41
|
-
// app/api/files/[id]/download/route.ts
|
|
42
|
-
import { auth } from '@/auth'
|
|
43
|
-
import { getPresignedDownloadUrl } from '@/lib/s3'
|
|
44
|
-
|
|
45
|
-
export async function GET(
|
|
46
|
-
request: Request,
|
|
47
|
-
{ params }: { params: { id: string } }
|
|
48
|
-
) {
|
|
49
|
-
const session = await auth()
|
|
50
|
-
if (!session) {
|
|
51
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Check file access
|
|
55
|
-
const file = await prisma.file.findUnique({
|
|
56
|
-
where: { id: params.id }
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
if (!file) {
|
|
60
|
-
return Response.json({ error: 'File not found' }, { status: 404 })
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (file.userId !== session.user.id) {
|
|
64
|
-
return Response.json({ error: 'Forbidden' }, { status: 403 })
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Generate temporary download URL
|
|
68
|
-
const downloadUrl = await getPresignedDownloadUrl(file.s3Key)
|
|
69
|
-
|
|
70
|
-
return Response.redirect(downloadUrl)
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Streaming Large Files
|
|
75
|
-
|
|
76
|
-
```typescript
|
|
77
|
-
// app/api/download/large/route.ts
|
|
78
|
-
import { createReadStream, statSync } from 'fs'
|
|
79
|
-
import { join } from 'path'
|
|
80
|
-
|
|
81
|
-
export async function GET(request: Request) {
|
|
82
|
-
const filePath = join(process.cwd(), 'large-file.zip')
|
|
83
|
-
const stats = statSync(filePath)
|
|
84
|
-
|
|
85
|
-
// Create readable stream
|
|
86
|
-
const stream = createReadStream(filePath)
|
|
87
|
-
|
|
88
|
-
// Convert to web stream
|
|
89
|
-
const webStream = new ReadableStream({
|
|
90
|
-
start(controller) {
|
|
91
|
-
stream.on('data', (chunk) => controller.enqueue(chunk))
|
|
92
|
-
stream.on('end', () => controller.close())
|
|
93
|
-
stream.on('error', (err) => controller.error(err))
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
return new Response(webStream, {
|
|
98
|
-
headers: {
|
|
99
|
-
'Content-Type': 'application/zip',
|
|
100
|
-
'Content-Disposition': 'attachment; filename="large-file.zip"',
|
|
101
|
-
'Content-Length': stats.size.toString()
|
|
102
|
-
}
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
## Range Requests (Resume Support)
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
// app/api/download/resumable/route.ts
|
|
111
|
-
import { createReadStream, statSync } from 'fs'
|
|
112
|
-
|
|
113
|
-
export async function GET(request: Request) {
|
|
114
|
-
const filePath = '/path/to/large-file.mp4'
|
|
115
|
-
const stats = statSync(filePath)
|
|
116
|
-
const fileSize = stats.size
|
|
117
|
-
|
|
118
|
-
const range = request.headers.get('range')
|
|
119
|
-
|
|
120
|
-
if (range) {
|
|
121
|
-
const parts = range.replace(/bytes=/, '').split('-')
|
|
122
|
-
const start = parseInt(parts[0], 10)
|
|
123
|
-
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1
|
|
124
|
-
const chunkSize = end - start + 1
|
|
125
|
-
|
|
126
|
-
const stream = createReadStream(filePath, { start, end })
|
|
127
|
-
|
|
128
|
-
return new Response(stream as any, {
|
|
129
|
-
status: 206,
|
|
130
|
-
headers: {
|
|
131
|
-
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
|
132
|
-
'Accept-Ranges': 'bytes',
|
|
133
|
-
'Content-Length': chunkSize.toString(),
|
|
134
|
-
'Content-Type': 'video/mp4'
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const stream = createReadStream(filePath)
|
|
140
|
-
|
|
141
|
-
return new Response(stream as any, {
|
|
142
|
-
headers: {
|
|
143
|
-
'Content-Length': fileSize.toString(),
|
|
144
|
-
'Content-Type': 'video/mp4',
|
|
145
|
-
'Accept-Ranges': 'bytes'
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Generate and Download
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
// app/api/export/csv/route.ts
|
|
155
|
-
import { auth } from '@/auth'
|
|
156
|
-
|
|
157
|
-
export async function GET(request: Request) {
|
|
158
|
-
const session = await auth()
|
|
159
|
-
if (!session) {
|
|
160
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Fetch data
|
|
164
|
-
const users = await prisma.user.findMany({
|
|
165
|
-
select: { name: true, email: true, createdAt: true }
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// Generate CSV
|
|
169
|
-
const headers = ['Name', 'Email', 'Created At']
|
|
170
|
-
const rows = users.map(u => [
|
|
171
|
-
u.name ?? '',
|
|
172
|
-
u.email,
|
|
173
|
-
u.createdAt.toISOString()
|
|
174
|
-
])
|
|
175
|
-
|
|
176
|
-
const csv = [
|
|
177
|
-
headers.join(','),
|
|
178
|
-
...rows.map(r => r.map(c => `"${c}"`).join(','))
|
|
179
|
-
].join('\n')
|
|
180
|
-
|
|
181
|
-
return new Response(csv, {
|
|
182
|
-
headers: {
|
|
183
|
-
'Content-Type': 'text/csv',
|
|
184
|
-
'Content-Disposition': `attachment; filename="users-${Date.now()}.csv"`
|
|
185
|
-
}
|
|
186
|
-
})
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
## PDF Generation
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
// app/api/invoice/[id]/pdf/route.ts
|
|
194
|
-
import { jsPDF } from 'jspdf'
|
|
195
|
-
|
|
196
|
-
export async function GET(
|
|
197
|
-
request: Request,
|
|
198
|
-
{ params }: { params: { id: string } }
|
|
199
|
-
) {
|
|
200
|
-
const invoice = await prisma.invoice.findUnique({
|
|
201
|
-
where: { id: params.id },
|
|
202
|
-
include: { items: true, customer: true }
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
if (!invoice) {
|
|
206
|
-
return Response.json({ error: 'Not found' }, { status: 404 })
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Generate PDF
|
|
210
|
-
const doc = new jsPDF()
|
|
211
|
-
|
|
212
|
-
doc.setFontSize(20)
|
|
213
|
-
doc.text(`Invoice #${invoice.number}`, 20, 20)
|
|
214
|
-
|
|
215
|
-
doc.setFontSize(12)
|
|
216
|
-
doc.text(`Customer: ${invoice.customer.name}`, 20, 40)
|
|
217
|
-
doc.text(`Date: ${invoice.date.toLocaleDateString()}`, 20, 50)
|
|
218
|
-
|
|
219
|
-
let y = 70
|
|
220
|
-
invoice.items.forEach(item => {
|
|
221
|
-
doc.text(`${item.name} - $${item.price}`, 20, y)
|
|
222
|
-
y += 10
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
doc.text(`Total: $${invoice.total}`, 20, y + 10)
|
|
226
|
-
|
|
227
|
-
const pdfBuffer = Buffer.from(doc.output('arraybuffer'))
|
|
228
|
-
|
|
229
|
-
return new Response(pdfBuffer, {
|
|
230
|
-
headers: {
|
|
231
|
-
'Content-Type': 'application/pdf',
|
|
232
|
-
'Content-Disposition': `attachment; filename="invoice-${invoice.number}.pdf"`
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
## When to Use
|
|
239
|
-
|
|
240
|
-
- File downloads
|
|
241
|
-
- Report exports
|
|
242
|
-
- Invoice generation
|
|
243
|
-
- Large file streaming
|
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
# File Upload Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for handling file uploads.
|
|
4
|
-
|
|
5
|
-
## Basic Upload API
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// app/api/upload/route.ts
|
|
9
|
-
import { writeFile, mkdir } from 'fs/promises'
|
|
10
|
-
import { join } from 'path'
|
|
11
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
12
|
-
|
|
13
|
-
const UPLOAD_DIR = join(process.cwd(), 'uploads')
|
|
14
|
-
|
|
15
|
-
export async function POST(request: NextRequest) {
|
|
16
|
-
const formData = await request.formData()
|
|
17
|
-
const file = formData.get('file') as File | null
|
|
18
|
-
|
|
19
|
-
if (!file) {
|
|
20
|
-
return NextResponse.json({ error: 'No file provided' }, { status: 400 })
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Validate file type
|
|
24
|
-
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
|
|
25
|
-
if (!allowedTypes.includes(file.type)) {
|
|
26
|
-
return NextResponse.json({ error: 'Invalid file type' }, { status: 400 })
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Validate file size (5MB)
|
|
30
|
-
if (file.size > 5 * 1024 * 1024) {
|
|
31
|
-
return NextResponse.json({ error: 'File too large' }, { status: 400 })
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Generate unique filename
|
|
35
|
-
const ext = file.name.split('.').pop()
|
|
36
|
-
const filename = `${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`
|
|
37
|
-
|
|
38
|
-
// Ensure upload directory exists
|
|
39
|
-
await mkdir(UPLOAD_DIR, { recursive: true })
|
|
40
|
-
|
|
41
|
-
// Write file
|
|
42
|
-
const bytes = await file.arrayBuffer()
|
|
43
|
-
const buffer = Buffer.from(bytes)
|
|
44
|
-
await writeFile(join(UPLOAD_DIR, filename), buffer)
|
|
45
|
-
|
|
46
|
-
return NextResponse.json({
|
|
47
|
-
filename,
|
|
48
|
-
url: `/uploads/${filename}`
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## S3 Upload
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
// lib/s3.ts
|
|
57
|
-
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3'
|
|
58
|
-
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
|
|
59
|
-
|
|
60
|
-
const s3 = new S3Client({
|
|
61
|
-
region: process.env.AWS_REGION!,
|
|
62
|
-
credentials: {
|
|
63
|
-
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
|
|
64
|
-
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
|
|
65
|
-
}
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
const BUCKET = process.env.AWS_S3_BUCKET!
|
|
69
|
-
|
|
70
|
-
export async function uploadToS3(file: File, key: string) {
|
|
71
|
-
const buffer = Buffer.from(await file.arrayBuffer())
|
|
72
|
-
|
|
73
|
-
await s3.send(new PutObjectCommand({
|
|
74
|
-
Bucket: BUCKET,
|
|
75
|
-
Key: key,
|
|
76
|
-
Body: buffer,
|
|
77
|
-
ContentType: file.type
|
|
78
|
-
}))
|
|
79
|
-
|
|
80
|
-
return `https://${BUCKET}.s3.amazonaws.com/${key}`
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function getPresignedUploadUrl(key: string, contentType: string) {
|
|
84
|
-
const command = new PutObjectCommand({
|
|
85
|
-
Bucket: BUCKET,
|
|
86
|
-
Key: key,
|
|
87
|
-
ContentType: contentType
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
return getSignedUrl(s3, command, { expiresIn: 3600 })
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export async function getPresignedDownloadUrl(key: string) {
|
|
94
|
-
const command = new GetObjectCommand({
|
|
95
|
-
Bucket: BUCKET,
|
|
96
|
-
Key: key
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
return getSignedUrl(s3, command, { expiresIn: 3600 })
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
## Presigned Upload Flow
|
|
104
|
-
|
|
105
|
-
```typescript
|
|
106
|
-
// app/api/upload/presign/route.ts
|
|
107
|
-
import { getPresignedUploadUrl } from '@/lib/s3'
|
|
108
|
-
import { auth } from '@/auth'
|
|
109
|
-
|
|
110
|
-
export async function POST(request: Request) {
|
|
111
|
-
const session = await auth()
|
|
112
|
-
if (!session) {
|
|
113
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 })
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const { filename, contentType } = await request.json()
|
|
117
|
-
|
|
118
|
-
const key = `uploads/${session.user.id}/${Date.now()}-${filename}`
|
|
119
|
-
const uploadUrl = await getPresignedUploadUrl(key, contentType)
|
|
120
|
-
|
|
121
|
-
return Response.json({ uploadUrl, key })
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Client-side upload
|
|
125
|
-
async function uploadFile(file: File) {
|
|
126
|
-
// Get presigned URL
|
|
127
|
-
const { uploadUrl, key } = await fetch('/api/upload/presign', {
|
|
128
|
-
method: 'POST',
|
|
129
|
-
body: JSON.stringify({
|
|
130
|
-
filename: file.name,
|
|
131
|
-
contentType: file.type
|
|
132
|
-
})
|
|
133
|
-
}).then(r => r.json())
|
|
134
|
-
|
|
135
|
-
// Upload directly to S3
|
|
136
|
-
await fetch(uploadUrl, {
|
|
137
|
-
method: 'PUT',
|
|
138
|
-
body: file,
|
|
139
|
-
headers: { 'Content-Type': file.type }
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
return key
|
|
143
|
-
}
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## UploadThing Integration
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
// lib/uploadthing.ts
|
|
150
|
-
import { createUploadthing, type FileRouter } from 'uploadthing/next'
|
|
151
|
-
import { auth } from '@/auth'
|
|
152
|
-
|
|
153
|
-
const f = createUploadthing()
|
|
154
|
-
|
|
155
|
-
export const uploadRouter = {
|
|
156
|
-
imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
|
|
157
|
-
.middleware(async () => {
|
|
158
|
-
const session = await auth()
|
|
159
|
-
if (!session) throw new Error('Unauthorized')
|
|
160
|
-
return { userId: session.user.id }
|
|
161
|
-
})
|
|
162
|
-
.onUploadComplete(async ({ metadata, file }) => {
|
|
163
|
-
await prisma.upload.create({
|
|
164
|
-
data: {
|
|
165
|
-
userId: metadata.userId,
|
|
166
|
-
url: file.url,
|
|
167
|
-
name: file.name,
|
|
168
|
-
size: file.size
|
|
169
|
-
}
|
|
170
|
-
})
|
|
171
|
-
return { url: file.url }
|
|
172
|
-
}),
|
|
173
|
-
|
|
174
|
-
documentUploader: f({ pdf: { maxFileSize: '16MB' } })
|
|
175
|
-
.middleware(async () => {
|
|
176
|
-
const session = await auth()
|
|
177
|
-
if (!session) throw new Error('Unauthorized')
|
|
178
|
-
return { userId: session.user.id }
|
|
179
|
-
})
|
|
180
|
-
.onUploadComplete(async ({ file }) => {
|
|
181
|
-
return { url: file.url }
|
|
182
|
-
})
|
|
183
|
-
} satisfies FileRouter
|
|
184
|
-
|
|
185
|
-
export type OurFileRouter = typeof uploadRouter
|
|
186
|
-
|
|
187
|
-
// app/api/uploadthing/route.ts
|
|
188
|
-
import { createRouteHandler } from 'uploadthing/next'
|
|
189
|
-
import { uploadRouter } from '@/lib/uploadthing'
|
|
190
|
-
|
|
191
|
-
export const { GET, POST } = createRouteHandler({ router: uploadRouter })
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## Image Processing
|
|
195
|
-
|
|
196
|
-
```typescript
|
|
197
|
-
// lib/image.ts
|
|
198
|
-
import sharp from 'sharp'
|
|
199
|
-
|
|
200
|
-
export async function processImage(buffer: Buffer) {
|
|
201
|
-
// Resize and optimize
|
|
202
|
-
const processed = await sharp(buffer)
|
|
203
|
-
.resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
|
|
204
|
-
.webp({ quality: 80 })
|
|
205
|
-
.toBuffer()
|
|
206
|
-
|
|
207
|
-
// Generate thumbnail
|
|
208
|
-
const thumbnail = await sharp(buffer)
|
|
209
|
-
.resize(200, 200, { fit: 'cover' })
|
|
210
|
-
.webp({ quality: 70 })
|
|
211
|
-
.toBuffer()
|
|
212
|
-
|
|
213
|
-
return { processed, thumbnail }
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Usage in upload handler
|
|
217
|
-
export async function POST(request: NextRequest) {
|
|
218
|
-
const formData = await request.formData()
|
|
219
|
-
const file = formData.get('file') as File
|
|
220
|
-
|
|
221
|
-
const buffer = Buffer.from(await file.arrayBuffer())
|
|
222
|
-
const { processed, thumbnail } = await processImage(buffer)
|
|
223
|
-
|
|
224
|
-
// Upload both versions
|
|
225
|
-
const [imageUrl, thumbUrl] = await Promise.all([
|
|
226
|
-
uploadToS3(processed, `images/${filename}.webp`),
|
|
227
|
-
uploadToS3(thumbnail, `thumbnails/${filename}.webp`)
|
|
228
|
-
])
|
|
229
|
-
|
|
230
|
-
return Response.json({ imageUrl, thumbUrl })
|
|
231
|
-
}
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
## When to Use
|
|
235
|
-
|
|
236
|
-
- User uploads
|
|
237
|
-
- Profile pictures
|
|
238
|
-
- Document storage
|
|
239
|
-
- Media galleries
|
|
@@ -1,188 +0,0 @@
|
|
|
1
|
-
# Next-Intl Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for internationalization with next-intl.
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// i18n.ts
|
|
9
|
-
import { notFound } from 'next/navigation'
|
|
10
|
-
import { getRequestConfig } from 'next-intl/server'
|
|
11
|
-
|
|
12
|
-
export const locales = ['en', 'es', 'fr', 'de'] as const
|
|
13
|
-
export type Locale = (typeof locales)[number]
|
|
14
|
-
export const defaultLocale: Locale = 'en'
|
|
15
|
-
|
|
16
|
-
export default getRequestConfig(async ({ locale }) => {
|
|
17
|
-
if (!locales.includes(locale as Locale)) notFound()
|
|
18
|
-
|
|
19
|
-
return {
|
|
20
|
-
messages: (await import(`./messages/${locale}.json`)).default
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// middleware.ts
|
|
25
|
-
import createMiddleware from 'next-intl/middleware'
|
|
26
|
-
import { locales, defaultLocale } from './i18n'
|
|
27
|
-
|
|
28
|
-
export default createMiddleware({
|
|
29
|
-
locales,
|
|
30
|
-
defaultLocale,
|
|
31
|
-
localePrefix: 'as-needed'
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
export const config = {
|
|
35
|
-
matcher: ['/((?!api|_next|.*\\..*).*)']
|
|
36
|
-
}
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Message Files
|
|
40
|
-
|
|
41
|
-
```json
|
|
42
|
-
// messages/en.json
|
|
43
|
-
{
|
|
44
|
-
"common": {
|
|
45
|
-
"welcome": "Welcome",
|
|
46
|
-
"login": "Log in",
|
|
47
|
-
"logout": "Log out"
|
|
48
|
-
},
|
|
49
|
-
"home": {
|
|
50
|
-
"title": "Welcome to {name}",
|
|
51
|
-
"description": "The best platform for {purpose}"
|
|
52
|
-
},
|
|
53
|
-
"products": {
|
|
54
|
-
"count": "{count, plural, =0 {No products} =1 {1 product} other {# products}}"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// messages/es.json
|
|
59
|
-
{
|
|
60
|
-
"common": {
|
|
61
|
-
"welcome": "Bienvenido",
|
|
62
|
-
"login": "Iniciar sesión",
|
|
63
|
-
"logout": "Cerrar sesión"
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## Using Translations
|
|
69
|
-
|
|
70
|
-
```tsx
|
|
71
|
-
// app/[locale]/page.tsx
|
|
72
|
-
import { useTranslations } from 'next-intl'
|
|
73
|
-
|
|
74
|
-
export default function HomePage() {
|
|
75
|
-
const t = useTranslations('home')
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div>
|
|
79
|
-
<h1>{t('title', { name: 'MyApp' })}</h1>
|
|
80
|
-
<p>{t('description', { purpose: 'collaboration' })}</p>
|
|
81
|
-
</div>
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// With pluralization
|
|
86
|
-
function ProductCount({ count }: { count: number }) {
|
|
87
|
-
const t = useTranslations('products')
|
|
88
|
-
return <span>{t('count', { count })}</span>
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Server Components
|
|
93
|
-
|
|
94
|
-
```tsx
|
|
95
|
-
// app/[locale]/products/page.tsx
|
|
96
|
-
import { getTranslations } from 'next-intl/server'
|
|
97
|
-
|
|
98
|
-
export async function generateMetadata({ params: { locale } }: Props) {
|
|
99
|
-
const t = await getTranslations({ locale, namespace: 'products' })
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
title: t('pageTitle')
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export default async function ProductsPage() {
|
|
107
|
-
const t = await getTranslations('products')
|
|
108
|
-
const products = await getProducts()
|
|
109
|
-
|
|
110
|
-
return (
|
|
111
|
-
<div>
|
|
112
|
-
<h1>{t('title')}</h1>
|
|
113
|
-
<p>{t('count', { count: products.length })}</p>
|
|
114
|
-
</div>
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
## Language Switcher
|
|
120
|
-
|
|
121
|
-
```tsx
|
|
122
|
-
// components/LanguageSwitcher.tsx
|
|
123
|
-
'use client'
|
|
124
|
-
|
|
125
|
-
import { useLocale } from 'next-intl'
|
|
126
|
-
import { usePathname, useRouter } from 'next-intl/client'
|
|
127
|
-
import { locales } from '@/i18n'
|
|
128
|
-
|
|
129
|
-
const localeNames: Record<string, string> = {
|
|
130
|
-
en: 'English',
|
|
131
|
-
es: 'Español',
|
|
132
|
-
fr: 'Français',
|
|
133
|
-
de: 'Deutsch'
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function LanguageSwitcher() {
|
|
137
|
-
const locale = useLocale()
|
|
138
|
-
const router = useRouter()
|
|
139
|
-
const pathname = usePathname()
|
|
140
|
-
|
|
141
|
-
const handleChange = (newLocale: string) => {
|
|
142
|
-
router.replace(pathname, { locale: newLocale })
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return (
|
|
146
|
-
<select
|
|
147
|
-
value={locale}
|
|
148
|
-
onChange={(e) => handleChange(e.target.value)}
|
|
149
|
-
className="rounded border px-2 py-1"
|
|
150
|
-
>
|
|
151
|
-
{locales.map(loc => (
|
|
152
|
-
<option key={loc} value={loc}>
|
|
153
|
-
{localeNames[loc]}
|
|
154
|
-
</option>
|
|
155
|
-
))}
|
|
156
|
-
</select>
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
## Date and Number Formatting
|
|
162
|
-
|
|
163
|
-
```tsx
|
|
164
|
-
// components/FormattedContent.tsx
|
|
165
|
-
import { useFormatter } from 'next-intl'
|
|
166
|
-
|
|
167
|
-
export function FormattedContent() {
|
|
168
|
-
const format = useFormatter()
|
|
169
|
-
|
|
170
|
-
const price = 1234.56
|
|
171
|
-
const date = new Date()
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div>
|
|
175
|
-
<p>Price: {format.number(price, { style: 'currency', currency: 'USD' })}</p>
|
|
176
|
-
<p>Date: {format.dateTime(date, { dateStyle: 'long' })}</p>
|
|
177
|
-
<p>Relative: {format.relativeTime(date)}</p>
|
|
178
|
-
</div>
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## When to Use
|
|
184
|
-
|
|
185
|
-
- Multi-language apps
|
|
186
|
-
- Regional content
|
|
187
|
-
- Date/number formatting
|
|
188
|
-
- RTL support
|