@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,435 +0,0 @@
|
|
|
1
|
-
# Date Handling Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for working with dates, times, and timezones.
|
|
4
|
-
|
|
5
|
-
## date-fns Setup
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/dates.ts
|
|
9
|
-
import {
|
|
10
|
-
format,
|
|
11
|
-
formatDistanceToNow,
|
|
12
|
-
parseISO,
|
|
13
|
-
isValid,
|
|
14
|
-
startOfDay,
|
|
15
|
-
endOfDay,
|
|
16
|
-
addDays,
|
|
17
|
-
subDays,
|
|
18
|
-
differenceInDays,
|
|
19
|
-
isBefore,
|
|
20
|
-
isAfter,
|
|
21
|
-
isSameDay
|
|
22
|
-
} from 'date-fns'
|
|
23
|
-
import { formatInTimeZone, toZonedTime } from 'date-fns-tz'
|
|
24
|
-
|
|
25
|
-
// Format date for display
|
|
26
|
-
export function formatDate(date: Date | string, pattern = 'MMM d, yyyy'): string {
|
|
27
|
-
const d = typeof date === 'string' ? parseISO(date) : date
|
|
28
|
-
return isValid(d) ? format(d, pattern) : 'Invalid date'
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Format datetime
|
|
32
|
-
export function formatDateTime(date: Date | string): string {
|
|
33
|
-
return formatDate(date, 'MMM d, yyyy h:mm a')
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Relative time (e.g., "2 hours ago")
|
|
37
|
-
export function timeAgo(date: Date | string): string {
|
|
38
|
-
const d = typeof date === 'string' ? parseISO(date) : date
|
|
39
|
-
return formatDistanceToNow(d, { addSuffix: true })
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Format with timezone
|
|
43
|
-
export function formatWithTimezone(
|
|
44
|
-
date: Date | string,
|
|
45
|
-
timezone: string,
|
|
46
|
-
pattern = 'MMM d, yyyy h:mm a zzz'
|
|
47
|
-
): string {
|
|
48
|
-
const d = typeof date === 'string' ? parseISO(date) : date
|
|
49
|
-
return formatInTimeZone(d, timezone, pattern)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Parse user input
|
|
53
|
-
export function parseDate(input: string): Date | null {
|
|
54
|
-
const parsed = parseISO(input)
|
|
55
|
-
return isValid(parsed) ? parsed : null
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Date range helpers
|
|
59
|
-
export function getDateRange(
|
|
60
|
-
range: 'today' | 'yesterday' | 'last7days' | 'last30days' | 'thisMonth'
|
|
61
|
-
): { start: Date; end: Date } {
|
|
62
|
-
const now = new Date()
|
|
63
|
-
const end = endOfDay(now)
|
|
64
|
-
|
|
65
|
-
switch (range) {
|
|
66
|
-
case 'today':
|
|
67
|
-
return { start: startOfDay(now), end }
|
|
68
|
-
case 'yesterday':
|
|
69
|
-
return {
|
|
70
|
-
start: startOfDay(subDays(now, 1)),
|
|
71
|
-
end: endOfDay(subDays(now, 1))
|
|
72
|
-
}
|
|
73
|
-
case 'last7days':
|
|
74
|
-
return { start: startOfDay(subDays(now, 6)), end }
|
|
75
|
-
case 'last30days':
|
|
76
|
-
return { start: startOfDay(subDays(now, 29)), end }
|
|
77
|
-
case 'thisMonth':
|
|
78
|
-
return {
|
|
79
|
-
start: startOfDay(new Date(now.getFullYear(), now.getMonth(), 1)),
|
|
80
|
-
end
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
## Date Input Component
|
|
87
|
-
|
|
88
|
-
```tsx
|
|
89
|
-
// components/DateInput.tsx
|
|
90
|
-
'use client'
|
|
91
|
-
|
|
92
|
-
import { useState } from 'react'
|
|
93
|
-
import { format, parse, isValid } from 'date-fns'
|
|
94
|
-
|
|
95
|
-
interface DateInputProps {
|
|
96
|
-
value?: Date
|
|
97
|
-
onChange: (date: Date | null) => void
|
|
98
|
-
min?: Date
|
|
99
|
-
max?: Date
|
|
100
|
-
disabled?: boolean
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function DateInput({
|
|
104
|
-
value,
|
|
105
|
-
onChange,
|
|
106
|
-
min,
|
|
107
|
-
max,
|
|
108
|
-
disabled
|
|
109
|
-
}: DateInputProps) {
|
|
110
|
-
const [inputValue, setInputValue] = useState(
|
|
111
|
-
value ? format(value, 'yyyy-MM-dd') : ''
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
115
|
-
const newValue = e.target.value
|
|
116
|
-
setInputValue(newValue)
|
|
117
|
-
|
|
118
|
-
if (newValue === '') {
|
|
119
|
-
onChange(null)
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
const parsed = parse(newValue, 'yyyy-MM-dd', new Date())
|
|
124
|
-
if (isValid(parsed)) {
|
|
125
|
-
if (min && parsed < min) return
|
|
126
|
-
if (max && parsed > max) return
|
|
127
|
-
onChange(parsed)
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return (
|
|
132
|
-
<input
|
|
133
|
-
type="date"
|
|
134
|
-
value={inputValue}
|
|
135
|
-
onChange={handleChange}
|
|
136
|
-
min={min ? format(min, 'yyyy-MM-dd') : undefined}
|
|
137
|
-
max={max ? format(max, 'yyyy-MM-dd') : undefined}
|
|
138
|
-
disabled={disabled}
|
|
139
|
-
className="rounded-md border px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
140
|
-
/>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
## Date Range Picker
|
|
146
|
-
|
|
147
|
-
```tsx
|
|
148
|
-
// components/DateRangePicker.tsx
|
|
149
|
-
'use client'
|
|
150
|
-
|
|
151
|
-
import { useState } from 'react'
|
|
152
|
-
import { format } from 'date-fns'
|
|
153
|
-
import { Calendar as CalendarIcon } from 'lucide-react'
|
|
154
|
-
import { DayPicker, DateRange } from 'react-day-picker'
|
|
155
|
-
import {
|
|
156
|
-
Popover,
|
|
157
|
-
PopoverContent,
|
|
158
|
-
PopoverTrigger
|
|
159
|
-
} from '@/components/ui/Popover'
|
|
160
|
-
import { cn } from '@/lib/utils'
|
|
161
|
-
|
|
162
|
-
interface DateRangePickerProps {
|
|
163
|
-
value?: DateRange
|
|
164
|
-
onChange: (range: DateRange | undefined) => void
|
|
165
|
-
placeholder?: string
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export function DateRangePicker({
|
|
169
|
-
value,
|
|
170
|
-
onChange,
|
|
171
|
-
placeholder = 'Select date range'
|
|
172
|
-
}: DateRangePickerProps) {
|
|
173
|
-
const [open, setOpen] = useState(false)
|
|
174
|
-
|
|
175
|
-
const formatRange = (range?: DateRange) => {
|
|
176
|
-
if (!range?.from) return placeholder
|
|
177
|
-
if (!range.to) return format(range.from, 'MMM d, yyyy')
|
|
178
|
-
return `${format(range.from, 'MMM d')} - ${format(range.to, 'MMM d, yyyy')}`
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return (
|
|
182
|
-
<Popover open={open} onOpenChange={setOpen}>
|
|
183
|
-
<PopoverTrigger asChild>
|
|
184
|
-
<button
|
|
185
|
-
className={cn(
|
|
186
|
-
'flex items-center gap-2 rounded-md border px-3 py-2 text-sm',
|
|
187
|
-
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
|
188
|
-
!value?.from && 'text-gray-500'
|
|
189
|
-
)}
|
|
190
|
-
>
|
|
191
|
-
<CalendarIcon className="h-4 w-4" />
|
|
192
|
-
{formatRange(value)}
|
|
193
|
-
</button>
|
|
194
|
-
</PopoverTrigger>
|
|
195
|
-
<PopoverContent className="w-auto p-0" align="start">
|
|
196
|
-
<DayPicker
|
|
197
|
-
mode="range"
|
|
198
|
-
selected={value}
|
|
199
|
-
onSelect={onChange}
|
|
200
|
-
numberOfMonths={2}
|
|
201
|
-
/>
|
|
202
|
-
</PopoverContent>
|
|
203
|
-
</Popover>
|
|
204
|
-
)
|
|
205
|
-
}
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
## Timezone Handling
|
|
209
|
-
|
|
210
|
-
```typescript
|
|
211
|
-
// lib/timezone.ts
|
|
212
|
-
import { toZonedTime, fromZonedTime, formatInTimeZone } from 'date-fns-tz'
|
|
213
|
-
|
|
214
|
-
// Get user's timezone
|
|
215
|
-
export function getUserTimezone(): string {
|
|
216
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Convert UTC to user's timezone
|
|
220
|
-
export function toUserTime(utcDate: Date, timezone?: string): Date {
|
|
221
|
-
const tz = timezone ?? getUserTimezone()
|
|
222
|
-
return toZonedTime(utcDate, tz)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Convert user's local time to UTC
|
|
226
|
-
export function toUTC(localDate: Date, timezone?: string): Date {
|
|
227
|
-
const tz = timezone ?? getUserTimezone()
|
|
228
|
-
return fromZonedTime(localDate, tz)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Format in specific timezone
|
|
232
|
-
export function formatInTz(
|
|
233
|
-
date: Date,
|
|
234
|
-
timezone: string,
|
|
235
|
-
pattern = 'yyyy-MM-dd HH:mm:ss zzz'
|
|
236
|
-
): string {
|
|
237
|
-
return formatInTimeZone(date, timezone, pattern)
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Timezone select options
|
|
241
|
-
export const timezones = Intl.supportedValuesOf('timeZone').map(tz => ({
|
|
242
|
-
value: tz,
|
|
243
|
-
label: tz.replace(/_/g, ' ')
|
|
244
|
-
}))
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
## Relative Time Component
|
|
248
|
-
|
|
249
|
-
```tsx
|
|
250
|
-
// components/RelativeTime.tsx
|
|
251
|
-
'use client'
|
|
252
|
-
|
|
253
|
-
import { useEffect, useState } from 'react'
|
|
254
|
-
import { formatDistanceToNow } from 'date-fns'
|
|
255
|
-
import { SimpleTooltip } from '@/components/ui/Tooltip'
|
|
256
|
-
import { formatDateTime } from '@/lib/dates'
|
|
257
|
-
|
|
258
|
-
interface RelativeTimeProps {
|
|
259
|
-
date: Date | string
|
|
260
|
-
updateInterval?: number // ms
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export function RelativeTime({
|
|
264
|
-
date,
|
|
265
|
-
updateInterval = 60000
|
|
266
|
-
}: RelativeTimeProps) {
|
|
267
|
-
const dateObj = typeof date === 'string' ? new Date(date) : date
|
|
268
|
-
const [relative, setRelative] = useState(
|
|
269
|
-
formatDistanceToNow(dateObj, { addSuffix: true })
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
useEffect(() => {
|
|
273
|
-
const timer = setInterval(() => {
|
|
274
|
-
setRelative(formatDistanceToNow(dateObj, { addSuffix: true }))
|
|
275
|
-
}, updateInterval)
|
|
276
|
-
|
|
277
|
-
return () => clearInterval(timer)
|
|
278
|
-
}, [dateObj, updateInterval])
|
|
279
|
-
|
|
280
|
-
return (
|
|
281
|
-
<SimpleTooltip content={formatDateTime(dateObj)}>
|
|
282
|
-
<time dateTime={dateObj.toISOString()}>{relative}</time>
|
|
283
|
-
</SimpleTooltip>
|
|
284
|
-
)
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
## Calendar Component
|
|
289
|
-
|
|
290
|
-
```tsx
|
|
291
|
-
// components/Calendar.tsx
|
|
292
|
-
'use client'
|
|
293
|
-
|
|
294
|
-
import { useState } from 'react'
|
|
295
|
-
import {
|
|
296
|
-
format,
|
|
297
|
-
startOfMonth,
|
|
298
|
-
endOfMonth,
|
|
299
|
-
eachDayOfInterval,
|
|
300
|
-
isSameMonth,
|
|
301
|
-
isSameDay,
|
|
302
|
-
addMonths,
|
|
303
|
-
subMonths,
|
|
304
|
-
startOfWeek,
|
|
305
|
-
endOfWeek
|
|
306
|
-
} from 'date-fns'
|
|
307
|
-
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
308
|
-
import { cn } from '@/lib/utils'
|
|
309
|
-
|
|
310
|
-
interface CalendarProps {
|
|
311
|
-
selected?: Date
|
|
312
|
-
onSelect: (date: Date) => void
|
|
313
|
-
events?: { date: Date; label: string }[]
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function Calendar({ selected, onSelect, events = [] }: CalendarProps) {
|
|
317
|
-
const [currentMonth, setCurrentMonth] = useState(selected ?? new Date())
|
|
318
|
-
|
|
319
|
-
const monthStart = startOfMonth(currentMonth)
|
|
320
|
-
const monthEnd = endOfMonth(currentMonth)
|
|
321
|
-
const calendarStart = startOfWeek(monthStart)
|
|
322
|
-
const calendarEnd = endOfWeek(monthEnd)
|
|
323
|
-
|
|
324
|
-
const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd })
|
|
325
|
-
|
|
326
|
-
const hasEvent = (date: Date) =>
|
|
327
|
-
events.some(e => isSameDay(e.date, date))
|
|
328
|
-
|
|
329
|
-
return (
|
|
330
|
-
<div className="w-full max-w-sm rounded-lg border p-4">
|
|
331
|
-
{/* Header */}
|
|
332
|
-
<div className="mb-4 flex items-center justify-between">
|
|
333
|
-
<button
|
|
334
|
-
onClick={() => setCurrentMonth(subMonths(currentMonth, 1))}
|
|
335
|
-
className="rounded p-1 hover:bg-gray-100"
|
|
336
|
-
>
|
|
337
|
-
<ChevronLeft className="h-5 w-5" />
|
|
338
|
-
</button>
|
|
339
|
-
<h2 className="font-semibold">
|
|
340
|
-
{format(currentMonth, 'MMMM yyyy')}
|
|
341
|
-
</h2>
|
|
342
|
-
<button
|
|
343
|
-
onClick={() => setCurrentMonth(addMonths(currentMonth, 1))}
|
|
344
|
-
className="rounded p-1 hover:bg-gray-100"
|
|
345
|
-
>
|
|
346
|
-
<ChevronRight className="h-5 w-5" />
|
|
347
|
-
</button>
|
|
348
|
-
</div>
|
|
349
|
-
|
|
350
|
-
{/* Weekday headers */}
|
|
351
|
-
<div className="mb-2 grid grid-cols-7 gap-1 text-center text-xs font-medium text-gray-500">
|
|
352
|
-
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map(day => (
|
|
353
|
-
<div key={day}>{day}</div>
|
|
354
|
-
))}
|
|
355
|
-
</div>
|
|
356
|
-
|
|
357
|
-
{/* Days grid */}
|
|
358
|
-
<div className="grid grid-cols-7 gap-1">
|
|
359
|
-
{days.map(day => (
|
|
360
|
-
<button
|
|
361
|
-
key={day.toISOString()}
|
|
362
|
-
onClick={() => onSelect(day)}
|
|
363
|
-
className={cn(
|
|
364
|
-
'relative aspect-square rounded-full p-2 text-sm',
|
|
365
|
-
'hover:bg-gray-100 focus:outline-none focus:ring-2',
|
|
366
|
-
!isSameMonth(day, currentMonth) && 'text-gray-300',
|
|
367
|
-
selected && isSameDay(day, selected) && 'bg-blue-600 text-white hover:bg-blue-700',
|
|
368
|
-
isSameDay(day, new Date()) && !selected?.valueOf() && 'bg-gray-100'
|
|
369
|
-
)}
|
|
370
|
-
>
|
|
371
|
-
{format(day, 'd')}
|
|
372
|
-
{hasEvent(day) && (
|
|
373
|
-
<span className="absolute bottom-1 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-blue-500" />
|
|
374
|
-
)}
|
|
375
|
-
</button>
|
|
376
|
-
))}
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
)
|
|
380
|
-
}
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
## Date Validation
|
|
384
|
-
|
|
385
|
-
```typescript
|
|
386
|
-
// lib/validation/dates.ts
|
|
387
|
-
import { z } from 'zod'
|
|
388
|
-
import { parseISO, isValid, isFuture, isPast, isAfter, isBefore } from 'date-fns'
|
|
389
|
-
|
|
390
|
-
// Zod date string schema
|
|
391
|
-
export const dateStringSchema = z.string().refine(
|
|
392
|
-
val => isValid(parseISO(val)),
|
|
393
|
-
{ message: 'Invalid date format' }
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
// Future date schema
|
|
397
|
-
export const futureDateSchema = dateStringSchema.refine(
|
|
398
|
-
val => isFuture(parseISO(val)),
|
|
399
|
-
{ message: 'Date must be in the future' }
|
|
400
|
-
)
|
|
401
|
-
|
|
402
|
-
// Past date schema
|
|
403
|
-
export const pastDateSchema = dateStringSchema.refine(
|
|
404
|
-
val => isPast(parseISO(val)),
|
|
405
|
-
{ message: 'Date must be in the past' }
|
|
406
|
-
)
|
|
407
|
-
|
|
408
|
-
// Date range schema
|
|
409
|
-
export const dateRangeSchema = z.object({
|
|
410
|
-
from: dateStringSchema,
|
|
411
|
-
to: dateStringSchema
|
|
412
|
-
}).refine(
|
|
413
|
-
({ from, to }) => isAfter(parseISO(to), parseISO(from)),
|
|
414
|
-
{ message: 'End date must be after start date' }
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
// Age validation
|
|
418
|
-
export function validateAge(birthDate: Date, minAge: number): boolean {
|
|
419
|
-
const today = new Date()
|
|
420
|
-
const age = today.getFullYear() - birthDate.getFullYear()
|
|
421
|
-
const monthDiff = today.getMonth() - birthDate.getMonth()
|
|
422
|
-
|
|
423
|
-
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
|
|
424
|
-
return age - 1 >= minAge
|
|
425
|
-
}
|
|
426
|
-
return age >= minAge
|
|
427
|
-
}
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
## When to Use
|
|
431
|
-
|
|
432
|
-
- Date display formatting
|
|
433
|
-
- Calendar components
|
|
434
|
-
- Date range selection
|
|
435
|
-
- Timezone conversions
|