@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,320 +0,0 @@
|
|
|
1
|
-
# Empty State Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for empty states and zero-data displays.
|
|
4
|
-
|
|
5
|
-
## Basic Empty State
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// components/ui/EmptyState.tsx
|
|
9
|
-
interface Props {
|
|
10
|
-
icon?: React.ReactNode
|
|
11
|
-
title: string
|
|
12
|
-
description: string
|
|
13
|
-
action?: {
|
|
14
|
-
label: string
|
|
15
|
-
onClick: () => void
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function EmptyState({ icon, title, description, action }: Props) {
|
|
20
|
-
return (
|
|
21
|
-
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
22
|
-
{icon && (
|
|
23
|
-
<div className="mb-4 rounded-full bg-gray-100 p-4 text-gray-400">
|
|
24
|
-
{icon}
|
|
25
|
-
</div>
|
|
26
|
-
)}
|
|
27
|
-
|
|
28
|
-
<h3 className="mb-2 text-lg font-medium">{title}</h3>
|
|
29
|
-
<p className="mb-6 max-w-sm text-gray-500">{description}</p>
|
|
30
|
-
|
|
31
|
-
{action && (
|
|
32
|
-
<button
|
|
33
|
-
onClick={action.onClick}
|
|
34
|
-
className="rounded-lg bg-black px-4 py-2 text-white hover:bg-gray-800"
|
|
35
|
-
>
|
|
36
|
-
{action.label}
|
|
37
|
-
</button>
|
|
38
|
-
)}
|
|
39
|
-
</div>
|
|
40
|
-
)
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
## Illustrated Empty State
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
// components/ui/IllustratedEmptyState.tsx
|
|
48
|
-
import Image from 'next/image'
|
|
49
|
-
|
|
50
|
-
interface Props {
|
|
51
|
-
illustration: string
|
|
52
|
-
title: string
|
|
53
|
-
description: string
|
|
54
|
-
children?: React.ReactNode
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function IllustratedEmptyState({
|
|
58
|
-
illustration,
|
|
59
|
-
title,
|
|
60
|
-
description,
|
|
61
|
-
children
|
|
62
|
-
}: Props) {
|
|
63
|
-
return (
|
|
64
|
-
<div className="flex flex-col items-center py-16 text-center">
|
|
65
|
-
<Image
|
|
66
|
-
src={illustration}
|
|
67
|
-
alt=""
|
|
68
|
-
width={200}
|
|
69
|
-
height={200}
|
|
70
|
-
className="mb-6"
|
|
71
|
-
/>
|
|
72
|
-
|
|
73
|
-
<h2 className="mb-2 text-xl font-semibold">{title}</h2>
|
|
74
|
-
<p className="mb-8 max-w-md text-gray-600">{description}</p>
|
|
75
|
-
|
|
76
|
-
{children}
|
|
77
|
-
</div>
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Usage
|
|
82
|
-
<IllustratedEmptyState
|
|
83
|
-
illustration="/illustrations/no-projects.svg"
|
|
84
|
-
title="No projects yet"
|
|
85
|
-
description="Create your first project to get started with collaboration and task management."
|
|
86
|
-
>
|
|
87
|
-
<button className="rounded-lg bg-blue-600 px-6 py-3 text-white">
|
|
88
|
-
Create Project
|
|
89
|
-
</button>
|
|
90
|
-
</IllustratedEmptyState>
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Search Empty State
|
|
94
|
-
|
|
95
|
-
```tsx
|
|
96
|
-
// components/ui/SearchEmptyState.tsx
|
|
97
|
-
import { Search } from 'lucide-react'
|
|
98
|
-
|
|
99
|
-
interface Props {
|
|
100
|
-
query: string
|
|
101
|
-
onClear: () => void
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export function SearchEmptyState({ query, onClear }: Props) {
|
|
105
|
-
return (
|
|
106
|
-
<div className="flex flex-col items-center py-12 text-center">
|
|
107
|
-
<div className="mb-4 rounded-full bg-gray-100 p-4">
|
|
108
|
-
<Search className="h-8 w-8 text-gray-400" />
|
|
109
|
-
</div>
|
|
110
|
-
|
|
111
|
-
<h3 className="mb-2 text-lg font-medium">No results found</h3>
|
|
112
|
-
<p className="mb-4 text-gray-500">
|
|
113
|
-
No results for "<span className="font-medium">{query}</span>"
|
|
114
|
-
</p>
|
|
115
|
-
|
|
116
|
-
<div className="space-y-2 text-sm text-gray-500">
|
|
117
|
-
<p>Try:</p>
|
|
118
|
-
<ul className="list-inside list-disc">
|
|
119
|
-
<li>Using different keywords</li>
|
|
120
|
-
<li>Checking for typos</li>
|
|
121
|
-
<li>Using more general terms</li>
|
|
122
|
-
</ul>
|
|
123
|
-
</div>
|
|
124
|
-
|
|
125
|
-
<button
|
|
126
|
-
onClick={onClear}
|
|
127
|
-
className="mt-6 text-blue-600 hover:underline"
|
|
128
|
-
>
|
|
129
|
-
Clear search
|
|
130
|
-
</button>
|
|
131
|
-
</div>
|
|
132
|
-
)
|
|
133
|
-
}
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
## Error Empty State
|
|
137
|
-
|
|
138
|
-
```tsx
|
|
139
|
-
// components/ui/ErrorState.tsx
|
|
140
|
-
import { AlertTriangle, RefreshCw } from 'lucide-react'
|
|
141
|
-
|
|
142
|
-
interface Props {
|
|
143
|
-
title?: string
|
|
144
|
-
message?: string
|
|
145
|
-
onRetry?: () => void
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function ErrorState({
|
|
149
|
-
title = 'Something went wrong',
|
|
150
|
-
message = 'We encountered an error loading this content.',
|
|
151
|
-
onRetry
|
|
152
|
-
}: Props) {
|
|
153
|
-
return (
|
|
154
|
-
<div className="flex flex-col items-center py-12 text-center">
|
|
155
|
-
<div className="mb-4 rounded-full bg-red-100 p-4">
|
|
156
|
-
<AlertTriangle className="h-8 w-8 text-red-500" />
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<h3 className="mb-2 text-lg font-medium text-gray-900">{title}</h3>
|
|
160
|
-
<p className="mb-6 max-w-sm text-gray-500">{message}</p>
|
|
161
|
-
|
|
162
|
-
{onRetry && (
|
|
163
|
-
<button
|
|
164
|
-
onClick={onRetry}
|
|
165
|
-
className="inline-flex items-center gap-2 rounded-lg border px-4 py-2 hover:bg-gray-50"
|
|
166
|
-
>
|
|
167
|
-
<RefreshCw className="h-4 w-4" />
|
|
168
|
-
Try again
|
|
169
|
-
</button>
|
|
170
|
-
)}
|
|
171
|
-
</div>
|
|
172
|
-
)
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
## List Empty States
|
|
177
|
-
|
|
178
|
-
```tsx
|
|
179
|
-
// components/lists/EmptyListStates.tsx
|
|
180
|
-
|
|
181
|
-
export function EmptyInbox() {
|
|
182
|
-
return (
|
|
183
|
-
<EmptyState
|
|
184
|
-
icon={<Inbox className="h-8 w-8" />}
|
|
185
|
-
title="Your inbox is empty"
|
|
186
|
-
description="When you receive messages, they'll appear here."
|
|
187
|
-
/>
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export function EmptyNotifications() {
|
|
192
|
-
return (
|
|
193
|
-
<EmptyState
|
|
194
|
-
icon={<Bell className="h-8 w-8" />}
|
|
195
|
-
title="No notifications"
|
|
196
|
-
description="You're all caught up! Check back later for updates."
|
|
197
|
-
/>
|
|
198
|
-
)
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
export function EmptyTasks() {
|
|
202
|
-
return (
|
|
203
|
-
<EmptyState
|
|
204
|
-
icon={<CheckSquare className="h-8 w-8" />}
|
|
205
|
-
title="No tasks yet"
|
|
206
|
-
description="Create your first task to start organizing your work."
|
|
207
|
-
action={{
|
|
208
|
-
label: 'Create Task',
|
|
209
|
-
onClick: () => openCreateTaskModal()
|
|
210
|
-
}}
|
|
211
|
-
/>
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function EmptyTeamMembers() {
|
|
216
|
-
return (
|
|
217
|
-
<EmptyState
|
|
218
|
-
icon={<Users className="h-8 w-8" />}
|
|
219
|
-
title="No team members"
|
|
220
|
-
description="Invite your team members to start collaborating."
|
|
221
|
-
action={{
|
|
222
|
-
label: 'Invite Members',
|
|
223
|
-
onClick: () => openInviteModal()
|
|
224
|
-
}}
|
|
225
|
-
/>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
## Conditional Empty State
|
|
231
|
-
|
|
232
|
-
```tsx
|
|
233
|
-
// components/DataList.tsx
|
|
234
|
-
interface Props<T> {
|
|
235
|
-
data: T[]
|
|
236
|
-
renderItem: (item: T) => React.ReactNode
|
|
237
|
-
emptyState: React.ReactNode
|
|
238
|
-
loading?: boolean
|
|
239
|
-
loadingSkeleton?: React.ReactNode
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function DataList<T>({
|
|
243
|
-
data,
|
|
244
|
-
renderItem,
|
|
245
|
-
emptyState,
|
|
246
|
-
loading,
|
|
247
|
-
loadingSkeleton
|
|
248
|
-
}: Props<T>) {
|
|
249
|
-
if (loading) {
|
|
250
|
-
return loadingSkeleton ?? <DefaultSkeleton />
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (data.length === 0) {
|
|
254
|
-
return emptyState
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return (
|
|
258
|
-
<div className="space-y-2">
|
|
259
|
-
{data.map(renderItem)}
|
|
260
|
-
</div>
|
|
261
|
-
)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Usage
|
|
265
|
-
<DataList
|
|
266
|
-
data={projects}
|
|
267
|
-
loading={isLoading}
|
|
268
|
-
loadingSkeleton={<ProjectsSkeleton />}
|
|
269
|
-
emptyState={
|
|
270
|
-
<EmptyState
|
|
271
|
-
icon={<Folder />}
|
|
272
|
-
title="No projects"
|
|
273
|
-
description="Create a project to get started"
|
|
274
|
-
action={{ label: 'New Project', onClick: handleCreate }}
|
|
275
|
-
/>
|
|
276
|
-
}
|
|
277
|
-
renderItem={(project) => <ProjectCard project={project} />}
|
|
278
|
-
/>
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
## Filter Empty State
|
|
282
|
-
|
|
283
|
-
```tsx
|
|
284
|
-
// components/ui/FilterEmptyState.tsx
|
|
285
|
-
import { Filter } from 'lucide-react'
|
|
286
|
-
|
|
287
|
-
interface Props {
|
|
288
|
-
filterCount: number
|
|
289
|
-
onClearFilters: () => void
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
export function FilterEmptyState({ filterCount, onClearFilters }: Props) {
|
|
293
|
-
return (
|
|
294
|
-
<div className="flex flex-col items-center py-12 text-center">
|
|
295
|
-
<div className="mb-4 rounded-full bg-gray-100 p-4">
|
|
296
|
-
<Filter className="h-8 w-8 text-gray-400" />
|
|
297
|
-
</div>
|
|
298
|
-
|
|
299
|
-
<h3 className="mb-2 text-lg font-medium">No matching results</h3>
|
|
300
|
-
<p className="mb-6 text-gray-500">
|
|
301
|
-
{filterCount} active {filterCount === 1 ? 'filter' : 'filters'} may be too restrictive.
|
|
302
|
-
</p>
|
|
303
|
-
|
|
304
|
-
<button
|
|
305
|
-
onClick={onClearFilters}
|
|
306
|
-
className="rounded-lg border px-4 py-2 hover:bg-gray-50"
|
|
307
|
-
>
|
|
308
|
-
Clear all filters
|
|
309
|
-
</button>
|
|
310
|
-
</div>
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
## When to Use
|
|
316
|
-
|
|
317
|
-
- Empty lists
|
|
318
|
-
- Search results
|
|
319
|
-
- Error recovery
|
|
320
|
-
- Onboarding
|
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
# Form Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for building forms with React Hook Form and Zod.
|
|
4
|
-
|
|
5
|
-
## Basic Form
|
|
6
|
-
|
|
7
|
-
```tsx
|
|
8
|
-
// components/forms/ContactForm.tsx
|
|
9
|
-
'use client'
|
|
10
|
-
|
|
11
|
-
import { useForm } from 'react-hook-form'
|
|
12
|
-
import { zodResolver } from '@hookform/resolvers/zod'
|
|
13
|
-
import { z } from 'zod'
|
|
14
|
-
|
|
15
|
-
const formSchema = z.object({
|
|
16
|
-
name: z.string().min(1, 'Name is required'),
|
|
17
|
-
email: z.string().email('Invalid email'),
|
|
18
|
-
message: z.string().min(10, 'Message must be at least 10 characters')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
type FormData = z.infer<typeof formSchema>
|
|
22
|
-
|
|
23
|
-
export function ContactForm() {
|
|
24
|
-
const {
|
|
25
|
-
register,
|
|
26
|
-
handleSubmit,
|
|
27
|
-
formState: { errors, isSubmitting },
|
|
28
|
-
reset
|
|
29
|
-
} = useForm<FormData>({
|
|
30
|
-
resolver: zodResolver(formSchema)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
async function onSubmit(data: FormData) {
|
|
34
|
-
await fetch('/api/contact', {
|
|
35
|
-
method: 'POST',
|
|
36
|
-
body: JSON.stringify(data)
|
|
37
|
-
})
|
|
38
|
-
reset()
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
|
43
|
-
<div>
|
|
44
|
-
<label htmlFor="name">Name</label>
|
|
45
|
-
<input
|
|
46
|
-
id="name"
|
|
47
|
-
{...register('name')}
|
|
48
|
-
className="w-full rounded border p-2"
|
|
49
|
-
/>
|
|
50
|
-
{errors.name && (
|
|
51
|
-
<p className="text-sm text-red-500">{errors.name.message}</p>
|
|
52
|
-
)}
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<div>
|
|
56
|
-
<label htmlFor="email">Email</label>
|
|
57
|
-
<input
|
|
58
|
-
id="email"
|
|
59
|
-
type="email"
|
|
60
|
-
{...register('email')}
|
|
61
|
-
className="w-full rounded border p-2"
|
|
62
|
-
/>
|
|
63
|
-
{errors.email && (
|
|
64
|
-
<p className="text-sm text-red-500">{errors.email.message}</p>
|
|
65
|
-
)}
|
|
66
|
-
</div>
|
|
67
|
-
|
|
68
|
-
<div>
|
|
69
|
-
<label htmlFor="message">Message</label>
|
|
70
|
-
<textarea
|
|
71
|
-
id="message"
|
|
72
|
-
{...register('message')}
|
|
73
|
-
rows={4}
|
|
74
|
-
className="w-full rounded border p-2"
|
|
75
|
-
/>
|
|
76
|
-
{errors.message && (
|
|
77
|
-
<p className="text-sm text-red-500">{errors.message.message}</p>
|
|
78
|
-
)}
|
|
79
|
-
</div>
|
|
80
|
-
|
|
81
|
-
<button
|
|
82
|
-
type="submit"
|
|
83
|
-
disabled={isSubmitting}
|
|
84
|
-
className="rounded bg-black px-4 py-2 text-white disabled:opacity-50"
|
|
85
|
-
>
|
|
86
|
-
{isSubmitting ? 'Sending...' : 'Send'}
|
|
87
|
-
</button>
|
|
88
|
-
</form>
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Server Action Form
|
|
94
|
-
|
|
95
|
-
```tsx
|
|
96
|
-
// app/actions.ts
|
|
97
|
-
'use server'
|
|
98
|
-
|
|
99
|
-
import { z } from 'zod'
|
|
100
|
-
import { revalidatePath } from 'next/cache'
|
|
101
|
-
|
|
102
|
-
const schema = z.object({
|
|
103
|
-
title: z.string().min(1),
|
|
104
|
-
content: z.string().min(1)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
export async function createPost(formData: FormData) {
|
|
108
|
-
const parsed = schema.safeParse({
|
|
109
|
-
title: formData.get('title'),
|
|
110
|
-
content: formData.get('content')
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
if (!parsed.success) {
|
|
114
|
-
return { error: parsed.error.flatten().fieldErrors }
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
await prisma.post.create({ data: parsed.data })
|
|
118
|
-
revalidatePath('/posts')
|
|
119
|
-
|
|
120
|
-
return { success: true }
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// components/CreatePostForm.tsx
|
|
124
|
-
'use client'
|
|
125
|
-
|
|
126
|
-
import { useFormState, useFormStatus } from 'react-dom'
|
|
127
|
-
import { createPost } from '@/app/actions'
|
|
128
|
-
|
|
129
|
-
function SubmitButton() {
|
|
130
|
-
const { pending } = useFormStatus()
|
|
131
|
-
|
|
132
|
-
return (
|
|
133
|
-
<button type="submit" disabled={pending}>
|
|
134
|
-
{pending ? 'Creating...' : 'Create Post'}
|
|
135
|
-
</button>
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function CreatePostForm() {
|
|
140
|
-
const [state, action] = useFormState(createPost, null)
|
|
141
|
-
|
|
142
|
-
return (
|
|
143
|
-
<form action={action} className="space-y-4">
|
|
144
|
-
<div>
|
|
145
|
-
<input name="title" placeholder="Title" />
|
|
146
|
-
{state?.error?.title && (
|
|
147
|
-
<p className="text-red-500">{state.error.title}</p>
|
|
148
|
-
)}
|
|
149
|
-
</div>
|
|
150
|
-
|
|
151
|
-
<div>
|
|
152
|
-
<textarea name="content" placeholder="Content" />
|
|
153
|
-
{state?.error?.content && (
|
|
154
|
-
<p className="text-red-500">{state.error.content}</p>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
|
|
158
|
-
<SubmitButton />
|
|
159
|
-
</form>
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Dynamic Form Fields
|
|
165
|
-
|
|
166
|
-
```tsx
|
|
167
|
-
// components/forms/DynamicForm.tsx
|
|
168
|
-
'use client'
|
|
169
|
-
|
|
170
|
-
import { useForm, useFieldArray } from 'react-hook-form'
|
|
171
|
-
import { zodResolver } from '@hookform/resolvers/zod'
|
|
172
|
-
import { z } from 'zod'
|
|
173
|
-
|
|
174
|
-
const schema = z.object({
|
|
175
|
-
items: z.array(z.object({
|
|
176
|
-
name: z.string().min(1),
|
|
177
|
-
quantity: z.number().min(1)
|
|
178
|
-
})).min(1, 'Add at least one item')
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
type FormData = z.infer<typeof schema>
|
|
182
|
-
|
|
183
|
-
export function DynamicForm() {
|
|
184
|
-
const { register, control, handleSubmit, formState: { errors } } = useForm<FormData>({
|
|
185
|
-
resolver: zodResolver(schema),
|
|
186
|
-
defaultValues: { items: [{ name: '', quantity: 1 }] }
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const { fields, append, remove } = useFieldArray({
|
|
190
|
-
control,
|
|
191
|
-
name: 'items'
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<form onSubmit={handleSubmit(console.log)} className="space-y-4">
|
|
196
|
-
{fields.map((field, index) => (
|
|
197
|
-
<div key={field.id} className="flex gap-2">
|
|
198
|
-
<input
|
|
199
|
-
{...register(`items.${index}.name`)}
|
|
200
|
-
placeholder="Item name"
|
|
201
|
-
/>
|
|
202
|
-
<input
|
|
203
|
-
type="number"
|
|
204
|
-
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
|
|
205
|
-
placeholder="Qty"
|
|
206
|
-
/>
|
|
207
|
-
<button type="button" onClick={() => remove(index)}>
|
|
208
|
-
Remove
|
|
209
|
-
</button>
|
|
210
|
-
</div>
|
|
211
|
-
))}
|
|
212
|
-
|
|
213
|
-
{errors.items && (
|
|
214
|
-
<p className="text-red-500">{errors.items.message}</p>
|
|
215
|
-
)}
|
|
216
|
-
|
|
217
|
-
<div className="flex gap-2">
|
|
218
|
-
<button
|
|
219
|
-
type="button"
|
|
220
|
-
onClick={() => append({ name: '', quantity: 1 })}
|
|
221
|
-
>
|
|
222
|
-
Add Item
|
|
223
|
-
</button>
|
|
224
|
-
<button type="submit">Submit</button>
|
|
225
|
-
</div>
|
|
226
|
-
</form>
|
|
227
|
-
)
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
## Multi-Step Form
|
|
232
|
-
|
|
233
|
-
```tsx
|
|
234
|
-
// components/forms/MultiStepForm.tsx
|
|
235
|
-
'use client'
|
|
236
|
-
|
|
237
|
-
import { useState } from 'react'
|
|
238
|
-
import { useForm, FormProvider } from 'react-hook-form'
|
|
239
|
-
import { zodResolver } from '@hookform/resolvers/zod'
|
|
240
|
-
import { z } from 'zod'
|
|
241
|
-
|
|
242
|
-
const stepSchemas = {
|
|
243
|
-
personal: z.object({
|
|
244
|
-
name: z.string().min(1),
|
|
245
|
-
email: z.string().email()
|
|
246
|
-
}),
|
|
247
|
-
address: z.object({
|
|
248
|
-
street: z.string().min(1),
|
|
249
|
-
city: z.string().min(1),
|
|
250
|
-
zip: z.string().min(5)
|
|
251
|
-
}),
|
|
252
|
-
payment: z.object({
|
|
253
|
-
cardNumber: z.string().min(16),
|
|
254
|
-
expiry: z.string().min(5)
|
|
255
|
-
})
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const fullSchema = z.object({
|
|
259
|
-
...stepSchemas.personal.shape,
|
|
260
|
-
...stepSchemas.address.shape,
|
|
261
|
-
...stepSchemas.payment.shape
|
|
262
|
-
})
|
|
263
|
-
|
|
264
|
-
type FormData = z.infer<typeof fullSchema>
|
|
265
|
-
|
|
266
|
-
const steps = ['personal', 'address', 'payment'] as const
|
|
267
|
-
|
|
268
|
-
export function MultiStepForm() {
|
|
269
|
-
const [step, setStep] = useState(0)
|
|
270
|
-
const currentStep = steps[step]
|
|
271
|
-
|
|
272
|
-
const methods = useForm<FormData>({
|
|
273
|
-
resolver: zodResolver(fullSchema),
|
|
274
|
-
mode: 'onChange'
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
async function nextStep() {
|
|
278
|
-
const fields = Object.keys(stepSchemas[currentStep].shape) as (keyof FormData)[]
|
|
279
|
-
const isValid = await methods.trigger(fields)
|
|
280
|
-
|
|
281
|
-
if (isValid) {
|
|
282
|
-
setStep(s => Math.min(s + 1, steps.length - 1))
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function prevStep() {
|
|
287
|
-
setStep(s => Math.max(s - 1, 0))
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
return (
|
|
291
|
-
<FormProvider {...methods}>
|
|
292
|
-
<form onSubmit={methods.handleSubmit(console.log)}>
|
|
293
|
-
{/* Progress indicator */}
|
|
294
|
-
<div className="mb-8 flex justify-between">
|
|
295
|
-
{steps.map((s, i) => (
|
|
296
|
-
<div
|
|
297
|
-
key={s}
|
|
298
|
-
className={`h-2 flex-1 ${i <= step ? 'bg-blue-500' : 'bg-gray-200'}`}
|
|
299
|
-
/>
|
|
300
|
-
))}
|
|
301
|
-
</div>
|
|
302
|
-
|
|
303
|
-
{/* Step content */}
|
|
304
|
-
{currentStep === 'personal' && <PersonalStep />}
|
|
305
|
-
{currentStep === 'address' && <AddressStep />}
|
|
306
|
-
{currentStep === 'payment' && <PaymentStep />}
|
|
307
|
-
|
|
308
|
-
{/* Navigation */}
|
|
309
|
-
<div className="mt-4 flex justify-between">
|
|
310
|
-
<button
|
|
311
|
-
type="button"
|
|
312
|
-
onClick={prevStep}
|
|
313
|
-
disabled={step === 0}
|
|
314
|
-
>
|
|
315
|
-
Back
|
|
316
|
-
</button>
|
|
317
|
-
|
|
318
|
-
{step < steps.length - 1 ? (
|
|
319
|
-
<button type="button" onClick={nextStep}>
|
|
320
|
-
Next
|
|
321
|
-
</button>
|
|
322
|
-
) : (
|
|
323
|
-
<button type="submit">Submit</button>
|
|
324
|
-
)}
|
|
325
|
-
</div>
|
|
326
|
-
</form>
|
|
327
|
-
</FormProvider>
|
|
328
|
-
)
|
|
329
|
-
}
|
|
330
|
-
```
|
|
331
|
-
|
|
332
|
-
## File Upload Form
|
|
333
|
-
|
|
334
|
-
```tsx
|
|
335
|
-
// components/forms/FileUploadForm.tsx
|
|
336
|
-
'use client'
|
|
337
|
-
|
|
338
|
-
import { useState, useCallback } from 'react'
|
|
339
|
-
import { useDropzone } from 'react-dropzone'
|
|
340
|
-
|
|
341
|
-
export function FileUploadForm() {
|
|
342
|
-
const [files, setFiles] = useState<File[]>([])
|
|
343
|
-
const [uploading, setUploading] = useState(false)
|
|
344
|
-
|
|
345
|
-
const onDrop = useCallback((acceptedFiles: File[]) => {
|
|
346
|
-
setFiles(prev => [...prev, ...acceptedFiles])
|
|
347
|
-
}, [])
|
|
348
|
-
|
|
349
|
-
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
350
|
-
onDrop,
|
|
351
|
-
accept: { 'image/*': ['.png', '.jpg', '.jpeg'] },
|
|
352
|
-
maxSize: 5 * 1024 * 1024 // 5MB
|
|
353
|
-
})
|
|
354
|
-
|
|
355
|
-
async function handleUpload() {
|
|
356
|
-
setUploading(true)
|
|
357
|
-
|
|
358
|
-
const formData = new FormData()
|
|
359
|
-
files.forEach(file => formData.append('files', file))
|
|
360
|
-
|
|
361
|
-
await fetch('/api/upload', { method: 'POST', body: formData })
|
|
362
|
-
|
|
363
|
-
setFiles([])
|
|
364
|
-
setUploading(false)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return (
|
|
368
|
-
<div className="space-y-4">
|
|
369
|
-
<div
|
|
370
|
-
{...getRootProps()}
|
|
371
|
-
className={`border-2 border-dashed p-8 text-center ${
|
|
372
|
-
isDragActive ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
|
|
373
|
-
}`}
|
|
374
|
-
>
|
|
375
|
-
<input {...getInputProps()} />
|
|
376
|
-
<p>Drag files here, or click to select</p>
|
|
377
|
-
</div>
|
|
378
|
-
|
|
379
|
-
{files.length > 0 && (
|
|
380
|
-
<div className="space-y-2">
|
|
381
|
-
{files.map((file, i) => (
|
|
382
|
-
<div key={i} className="flex items-center justify-between">
|
|
383
|
-
<span>{file.name}</span>
|
|
384
|
-
<button onClick={() => setFiles(f => f.filter((_, j) => j !== i))}>
|
|
385
|
-
Remove
|
|
386
|
-
</button>
|
|
387
|
-
</div>
|
|
388
|
-
))}
|
|
389
|
-
</div>
|
|
390
|
-
)}
|
|
391
|
-
|
|
392
|
-
<button onClick={handleUpload} disabled={!files.length || uploading}>
|
|
393
|
-
{uploading ? 'Uploading...' : 'Upload'}
|
|
394
|
-
</button>
|
|
395
|
-
</div>
|
|
396
|
-
)
|
|
397
|
-
}
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
## When to Use
|
|
401
|
-
|
|
402
|
-
- User input
|
|
403
|
-
- Data collection
|
|
404
|
-
- File uploads
|
|
405
|
-
- Multi-step processes
|