@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,329 +0,0 @@
|
|
|
1
|
-
# Filtering and Sorting Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for filtering and sorting data.
|
|
4
|
-
|
|
5
|
-
## URL-Based Filters
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/filters.ts
|
|
9
|
-
import { Prisma } from '@prisma/client'
|
|
10
|
-
|
|
11
|
-
interface FilterParams {
|
|
12
|
-
category?: string
|
|
13
|
-
minPrice?: string
|
|
14
|
-
maxPrice?: string
|
|
15
|
-
status?: string
|
|
16
|
-
sort?: string
|
|
17
|
-
order?: 'asc' | 'desc'
|
|
18
|
-
page?: string
|
|
19
|
-
limit?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function buildProductFilters(params: FilterParams) {
|
|
23
|
-
const where: Prisma.ProductWhereInput = {}
|
|
24
|
-
|
|
25
|
-
if (params.category) {
|
|
26
|
-
where.categoryId = params.category
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (params.minPrice || params.maxPrice) {
|
|
30
|
-
where.price = {}
|
|
31
|
-
if (params.minPrice) {
|
|
32
|
-
where.price.gte = parseFloat(params.minPrice)
|
|
33
|
-
}
|
|
34
|
-
if (params.maxPrice) {
|
|
35
|
-
where.price.lte = parseFloat(params.maxPrice)
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (params.status) {
|
|
40
|
-
where.status = params.status
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const orderBy: Prisma.ProductOrderByWithRelationInput = {}
|
|
44
|
-
const sortField = params.sort || 'createdAt'
|
|
45
|
-
const sortOrder = params.order || 'desc'
|
|
46
|
-
orderBy[sortField as keyof typeof orderBy] = sortOrder
|
|
47
|
-
|
|
48
|
-
const page = parseInt(params.page ?? '1')
|
|
49
|
-
const limit = parseInt(params.limit ?? '10')
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
where,
|
|
53
|
-
orderBy,
|
|
54
|
-
skip: (page - 1) * limit,
|
|
55
|
-
take: limit
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Filter API Route
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
// app/api/products/route.ts
|
|
64
|
-
import { buildProductFilters } from '@/lib/filters'
|
|
65
|
-
|
|
66
|
-
export async function GET(request: Request) {
|
|
67
|
-
const { searchParams } = new URL(request.url)
|
|
68
|
-
|
|
69
|
-
const params = {
|
|
70
|
-
category: searchParams.get('category') ?? undefined,
|
|
71
|
-
minPrice: searchParams.get('minPrice') ?? undefined,
|
|
72
|
-
maxPrice: searchParams.get('maxPrice') ?? undefined,
|
|
73
|
-
status: searchParams.get('status') ?? undefined,
|
|
74
|
-
sort: searchParams.get('sort') ?? undefined,
|
|
75
|
-
order: searchParams.get('order') as 'asc' | 'desc' ?? undefined,
|
|
76
|
-
page: searchParams.get('page') ?? undefined,
|
|
77
|
-
limit: searchParams.get('limit') ?? undefined
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const { where, orderBy, skip, take } = buildProductFilters(params)
|
|
81
|
-
|
|
82
|
-
const [products, total] = await Promise.all([
|
|
83
|
-
prisma.product.findMany({
|
|
84
|
-
where,
|
|
85
|
-
orderBy,
|
|
86
|
-
skip,
|
|
87
|
-
take,
|
|
88
|
-
include: { category: true }
|
|
89
|
-
}),
|
|
90
|
-
prisma.product.count({ where })
|
|
91
|
-
])
|
|
92
|
-
|
|
93
|
-
return Response.json({
|
|
94
|
-
products,
|
|
95
|
-
pagination: {
|
|
96
|
-
page: parseInt(params.page ?? '1'),
|
|
97
|
-
limit: take,
|
|
98
|
-
total,
|
|
99
|
-
totalPages: Math.ceil(total / take)
|
|
100
|
-
}
|
|
101
|
-
})
|
|
102
|
-
}
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
## Filter UI Component
|
|
106
|
-
|
|
107
|
-
```tsx
|
|
108
|
-
// components/ProductFilters.tsx
|
|
109
|
-
'use client'
|
|
110
|
-
|
|
111
|
-
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
|
|
112
|
-
import { useState, useCallback } from 'react'
|
|
113
|
-
|
|
114
|
-
interface Category {
|
|
115
|
-
id: string
|
|
116
|
-
name: string
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function ProductFilters({ categories }: { categories: Category[] }) {
|
|
120
|
-
const router = useRouter()
|
|
121
|
-
const pathname = usePathname()
|
|
122
|
-
const searchParams = useSearchParams()
|
|
123
|
-
|
|
124
|
-
const createQueryString = useCallback(
|
|
125
|
-
(updates: Record<string, string | null>) => {
|
|
126
|
-
const params = new URLSearchParams(searchParams)
|
|
127
|
-
|
|
128
|
-
Object.entries(updates).forEach(([key, value]) => {
|
|
129
|
-
if (value === null) {
|
|
130
|
-
params.delete(key)
|
|
131
|
-
} else {
|
|
132
|
-
params.set(key, value)
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// Reset to page 1 when filters change
|
|
137
|
-
if (!updates.page) {
|
|
138
|
-
params.set('page', '1')
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return params.toString()
|
|
142
|
-
},
|
|
143
|
-
[searchParams]
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
const updateFilters = (updates: Record<string, string | null>) => {
|
|
147
|
-
router.push(`${pathname}?${createQueryString(updates)}`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return (
|
|
151
|
-
<div className="space-y-6">
|
|
152
|
-
{/* Category filter */}
|
|
153
|
-
<div>
|
|
154
|
-
<h3 className="font-medium">Category</h3>
|
|
155
|
-
<div className="mt-2 space-y-2">
|
|
156
|
-
{categories.map(category => (
|
|
157
|
-
<label key={category.id} className="flex items-center gap-2">
|
|
158
|
-
<input
|
|
159
|
-
type="radio"
|
|
160
|
-
name="category"
|
|
161
|
-
checked={searchParams.get('category') === category.id}
|
|
162
|
-
onChange={() => updateFilters({ category: category.id })}
|
|
163
|
-
/>
|
|
164
|
-
{category.name}
|
|
165
|
-
</label>
|
|
166
|
-
))}
|
|
167
|
-
<button
|
|
168
|
-
onClick={() => updateFilters({ category: null })}
|
|
169
|
-
className="text-sm text-blue-600"
|
|
170
|
-
>
|
|
171
|
-
Clear
|
|
172
|
-
</button>
|
|
173
|
-
</div>
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
{/* Price range */}
|
|
177
|
-
<div>
|
|
178
|
-
<h3 className="font-medium">Price Range</h3>
|
|
179
|
-
<div className="mt-2 flex gap-2">
|
|
180
|
-
<input
|
|
181
|
-
type="number"
|
|
182
|
-
placeholder="Min"
|
|
183
|
-
defaultValue={searchParams.get('minPrice') ?? ''}
|
|
184
|
-
onBlur={(e) => updateFilters({ minPrice: e.target.value || null })}
|
|
185
|
-
className="w-24 rounded border px-2 py-1"
|
|
186
|
-
/>
|
|
187
|
-
<input
|
|
188
|
-
type="number"
|
|
189
|
-
placeholder="Max"
|
|
190
|
-
defaultValue={searchParams.get('maxPrice') ?? ''}
|
|
191
|
-
onBlur={(e) => updateFilters({ maxPrice: e.target.value || null })}
|
|
192
|
-
className="w-24 rounded border px-2 py-1"
|
|
193
|
-
/>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
{/* Sort */}
|
|
198
|
-
<div>
|
|
199
|
-
<h3 className="font-medium">Sort By</h3>
|
|
200
|
-
<select
|
|
201
|
-
value={`${searchParams.get('sort') ?? 'createdAt'}-${searchParams.get('order') ?? 'desc'}`}
|
|
202
|
-
onChange={(e) => {
|
|
203
|
-
const [sort, order] = e.target.value.split('-')
|
|
204
|
-
updateFilters({ sort, order })
|
|
205
|
-
}}
|
|
206
|
-
className="mt-2 w-full rounded border px-3 py-2"
|
|
207
|
-
>
|
|
208
|
-
<option value="createdAt-desc">Newest</option>
|
|
209
|
-
<option value="createdAt-asc">Oldest</option>
|
|
210
|
-
<option value="price-asc">Price: Low to High</option>
|
|
211
|
-
<option value="price-desc">Price: High to Low</option>
|
|
212
|
-
<option value="name-asc">Name: A-Z</option>
|
|
213
|
-
<option value="name-desc">Name: Z-A</option>
|
|
214
|
-
</select>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
{/* Clear all */}
|
|
218
|
-
<button
|
|
219
|
-
onClick={() => router.push(pathname)}
|
|
220
|
-
className="w-full rounded border py-2 hover:bg-gray-50"
|
|
221
|
-
>
|
|
222
|
-
Clear All Filters
|
|
223
|
-
</button>
|
|
224
|
-
</div>
|
|
225
|
-
)
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
## Active Filters Display
|
|
230
|
-
|
|
231
|
-
```tsx
|
|
232
|
-
// components/ActiveFilters.tsx
|
|
233
|
-
'use client'
|
|
234
|
-
|
|
235
|
-
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
|
|
236
|
-
import { X } from 'lucide-react'
|
|
237
|
-
|
|
238
|
-
export function ActiveFilters() {
|
|
239
|
-
const searchParams = useSearchParams()
|
|
240
|
-
const router = useRouter()
|
|
241
|
-
const pathname = usePathname()
|
|
242
|
-
|
|
243
|
-
const filters = Array.from(searchParams.entries()).filter(
|
|
244
|
-
([key]) => !['page', 'limit'].includes(key)
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
if (filters.length === 0) return null
|
|
248
|
-
|
|
249
|
-
const removeFilter = (key: string) => {
|
|
250
|
-
const params = new URLSearchParams(searchParams)
|
|
251
|
-
params.delete(key)
|
|
252
|
-
router.push(`${pathname}?${params.toString()}`)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return (
|
|
256
|
-
<div className="flex flex-wrap gap-2">
|
|
257
|
-
{filters.map(([key, value]) => (
|
|
258
|
-
<span
|
|
259
|
-
key={key}
|
|
260
|
-
className="inline-flex items-center gap-1 rounded-full bg-gray-100 px-3 py-1 text-sm"
|
|
261
|
-
>
|
|
262
|
-
{key}: {value}
|
|
263
|
-
<button onClick={() => removeFilter(key)}>
|
|
264
|
-
<X className="h-3 w-3" />
|
|
265
|
-
</button>
|
|
266
|
-
</span>
|
|
267
|
-
))}
|
|
268
|
-
</div>
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
## Multi-Select Filter
|
|
274
|
-
|
|
275
|
-
```tsx
|
|
276
|
-
// components/TagFilter.tsx
|
|
277
|
-
'use client'
|
|
278
|
-
|
|
279
|
-
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
|
|
280
|
-
|
|
281
|
-
export function TagFilter({ tags }: { tags: string[] }) {
|
|
282
|
-
const searchParams = useSearchParams()
|
|
283
|
-
const router = useRouter()
|
|
284
|
-
const pathname = usePathname()
|
|
285
|
-
|
|
286
|
-
const selectedTags = searchParams.getAll('tag')
|
|
287
|
-
|
|
288
|
-
const toggleTag = (tag: string) => {
|
|
289
|
-
const params = new URLSearchParams(searchParams)
|
|
290
|
-
const current = params.getAll('tag')
|
|
291
|
-
|
|
292
|
-
params.delete('tag')
|
|
293
|
-
|
|
294
|
-
if (current.includes(tag)) {
|
|
295
|
-
current.filter(t => t !== tag).forEach(t => params.append('tag', t))
|
|
296
|
-
} else {
|
|
297
|
-
current.forEach(t => params.append('tag', t))
|
|
298
|
-
params.append('tag', tag)
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
router.push(`${pathname}?${params.toString()}`)
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return (
|
|
305
|
-
<div className="flex flex-wrap gap-2">
|
|
306
|
-
{tags.map(tag => (
|
|
307
|
-
<button
|
|
308
|
-
key={tag}
|
|
309
|
-
onClick={() => toggleTag(tag)}
|
|
310
|
-
className={`rounded-full px-3 py-1 text-sm ${
|
|
311
|
-
selectedTags.includes(tag)
|
|
312
|
-
? 'bg-blue-600 text-white'
|
|
313
|
-
: 'bg-gray-100 hover:bg-gray-200'
|
|
314
|
-
}`}
|
|
315
|
-
>
|
|
316
|
-
{tag}
|
|
317
|
-
</button>
|
|
318
|
-
))}
|
|
319
|
-
</div>
|
|
320
|
-
)
|
|
321
|
-
}
|
|
322
|
-
```
|
|
323
|
-
|
|
324
|
-
## When to Use
|
|
325
|
-
|
|
326
|
-
- E-commerce listings
|
|
327
|
-
- Search results
|
|
328
|
-
- Admin dashboards
|
|
329
|
-
- Data tables
|
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
# Full-Text Search Patterns
|
|
2
|
-
|
|
3
|
-
Patterns for implementing search functionality.
|
|
4
|
-
|
|
5
|
-
## Prisma Full-Text Search
|
|
6
|
-
|
|
7
|
-
```typescript
|
|
8
|
-
// lib/search.ts
|
|
9
|
-
import { prisma } from '@/lib/db'
|
|
10
|
-
|
|
11
|
-
// PostgreSQL full-text search
|
|
12
|
-
export async function searchPosts(query: string, options?: {
|
|
13
|
-
limit?: number
|
|
14
|
-
offset?: number
|
|
15
|
-
}) {
|
|
16
|
-
const { limit = 10, offset = 0 } = options ?? {}
|
|
17
|
-
|
|
18
|
-
// Convert query to tsquery format
|
|
19
|
-
const searchQuery = query
|
|
20
|
-
.trim()
|
|
21
|
-
.split(/\s+/)
|
|
22
|
-
.map(term => `${term}:*`)
|
|
23
|
-
.join(' & ')
|
|
24
|
-
|
|
25
|
-
const posts = await prisma.$queryRaw`
|
|
26
|
-
SELECT
|
|
27
|
-
id,
|
|
28
|
-
title,
|
|
29
|
-
content,
|
|
30
|
-
ts_rank(
|
|
31
|
-
to_tsvector('english', title || ' ' || content),
|
|
32
|
-
to_tsquery('english', ${searchQuery})
|
|
33
|
-
) as rank
|
|
34
|
-
FROM "Post"
|
|
35
|
-
WHERE to_tsvector('english', title || ' ' || content) @@ to_tsquery('english', ${searchQuery})
|
|
36
|
-
ORDER BY rank DESC
|
|
37
|
-
LIMIT ${limit}
|
|
38
|
-
OFFSET ${offset}
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
return posts
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Prisma native search (preview)
|
|
45
|
-
export async function searchWithPrisma(query: string) {
|
|
46
|
-
return prisma.post.findMany({
|
|
47
|
-
where: {
|
|
48
|
-
OR: [
|
|
49
|
-
{ title: { search: query } },
|
|
50
|
-
{ content: { search: query } }
|
|
51
|
-
]
|
|
52
|
-
},
|
|
53
|
-
orderBy: {
|
|
54
|
-
_relevance: {
|
|
55
|
-
fields: ['title', 'content'],
|
|
56
|
-
search: query,
|
|
57
|
-
sort: 'desc'
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
})
|
|
61
|
-
}
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
## Search API Route
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
// app/api/search/route.ts
|
|
68
|
-
import { searchPosts } from '@/lib/search'
|
|
69
|
-
|
|
70
|
-
export async function GET(request: Request) {
|
|
71
|
-
const { searchParams } = new URL(request.url)
|
|
72
|
-
const query = searchParams.get('q')
|
|
73
|
-
const page = parseInt(searchParams.get('page') ?? '1')
|
|
74
|
-
const limit = parseInt(searchParams.get('limit') ?? '10')
|
|
75
|
-
|
|
76
|
-
if (!query || query.length < 2) {
|
|
77
|
-
return Response.json({ results: [] })
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const offset = (page - 1) * limit
|
|
81
|
-
|
|
82
|
-
const results = await searchPosts(query, { limit, offset })
|
|
83
|
-
|
|
84
|
-
return Response.json({
|
|
85
|
-
results,
|
|
86
|
-
page,
|
|
87
|
-
limit,
|
|
88
|
-
query
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Search Input with Debounce
|
|
94
|
-
|
|
95
|
-
```tsx
|
|
96
|
-
// components/SearchInput.tsx
|
|
97
|
-
'use client'
|
|
98
|
-
|
|
99
|
-
import { useState, useCallback } from 'react'
|
|
100
|
-
import { useDebouncedCallback } from 'use-debounce'
|
|
101
|
-
import { useRouter, useSearchParams } from 'next/navigation'
|
|
102
|
-
|
|
103
|
-
export function SearchInput() {
|
|
104
|
-
const router = useRouter()
|
|
105
|
-
const searchParams = useSearchParams()
|
|
106
|
-
const [query, setQuery] = useState(searchParams.get('q') ?? '')
|
|
107
|
-
|
|
108
|
-
const handleSearch = useDebouncedCallback((term: string) => {
|
|
109
|
-
const params = new URLSearchParams(searchParams)
|
|
110
|
-
|
|
111
|
-
if (term) {
|
|
112
|
-
params.set('q', term)
|
|
113
|
-
} else {
|
|
114
|
-
params.delete('q')
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
router.push(`/search?${params.toString()}`)
|
|
118
|
-
}, 300)
|
|
119
|
-
|
|
120
|
-
return (
|
|
121
|
-
<input
|
|
122
|
-
type="search"
|
|
123
|
-
value={query}
|
|
124
|
-
onChange={(e) => {
|
|
125
|
-
setQuery(e.target.value)
|
|
126
|
-
handleSearch(e.target.value)
|
|
127
|
-
}}
|
|
128
|
-
placeholder="Search..."
|
|
129
|
-
className="w-full rounded-lg border px-4 py-2"
|
|
130
|
-
/>
|
|
131
|
-
)
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Search Results Page
|
|
136
|
-
|
|
137
|
-
```tsx
|
|
138
|
-
// app/search/page.tsx
|
|
139
|
-
import { searchPosts } from '@/lib/search'
|
|
140
|
-
import { SearchInput } from '@/components/SearchInput'
|
|
141
|
-
|
|
142
|
-
export default async function SearchPage({
|
|
143
|
-
searchParams
|
|
144
|
-
}: {
|
|
145
|
-
searchParams: { q?: string; page?: string }
|
|
146
|
-
}) {
|
|
147
|
-
const query = searchParams.q ?? ''
|
|
148
|
-
const page = parseInt(searchParams.page ?? '1')
|
|
149
|
-
|
|
150
|
-
const results = query
|
|
151
|
-
? await searchPosts(query, { limit: 10, offset: (page - 1) * 10 })
|
|
152
|
-
: []
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<div className="mx-auto max-w-2xl py-8">
|
|
156
|
-
<SearchInput />
|
|
157
|
-
|
|
158
|
-
<div className="mt-8">
|
|
159
|
-
{query && (
|
|
160
|
-
<p className="mb-4 text-gray-600">
|
|
161
|
-
Results for "{query}"
|
|
162
|
-
</p>
|
|
163
|
-
)}
|
|
164
|
-
|
|
165
|
-
{results.length === 0 && query && (
|
|
166
|
-
<p className="text-gray-500">No results found</p>
|
|
167
|
-
)}
|
|
168
|
-
|
|
169
|
-
<div className="space-y-4">
|
|
170
|
-
{results.map((post: any) => (
|
|
171
|
-
<article key={post.id} className="rounded-lg border p-4">
|
|
172
|
-
<h2 className="font-semibold">{post.title}</h2>
|
|
173
|
-
<p className="mt-1 text-gray-600">{post.content.slice(0, 150)}...</p>
|
|
174
|
-
</article>
|
|
175
|
-
))}
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
## Algolia Integration
|
|
184
|
-
|
|
185
|
-
```typescript
|
|
186
|
-
// lib/algolia.ts
|
|
187
|
-
import algoliasearch from 'algoliasearch'
|
|
188
|
-
|
|
189
|
-
const client = algoliasearch(
|
|
190
|
-
process.env.ALGOLIA_APP_ID!,
|
|
191
|
-
process.env.ALGOLIA_ADMIN_KEY!
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
const index = client.initIndex('posts')
|
|
195
|
-
|
|
196
|
-
export async function indexPost(post: Post) {
|
|
197
|
-
await index.saveObject({
|
|
198
|
-
objectID: post.id,
|
|
199
|
-
title: post.title,
|
|
200
|
-
content: post.content,
|
|
201
|
-
author: post.author.name,
|
|
202
|
-
createdAt: post.createdAt.getTime()
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export async function removeFromIndex(postId: string) {
|
|
207
|
-
await index.deleteObject(postId)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export async function searchAlgolia(query: string) {
|
|
211
|
-
const { hits } = await index.search(query, {
|
|
212
|
-
hitsPerPage: 10,
|
|
213
|
-
attributesToHighlight: ['title', 'content']
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
return hits
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Sync on create/update
|
|
220
|
-
async function createPost(data: CreatePostData) {
|
|
221
|
-
const post = await prisma.post.create({ data })
|
|
222
|
-
await indexPost(post)
|
|
223
|
-
return post
|
|
224
|
-
}
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
## Search Suggestions
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
// app/api/search/suggestions/route.ts
|
|
231
|
-
export async function GET(request: Request) {
|
|
232
|
-
const { searchParams } = new URL(request.url)
|
|
233
|
-
const query = searchParams.get('q')
|
|
234
|
-
|
|
235
|
-
if (!query || query.length < 2) {
|
|
236
|
-
return Response.json({ suggestions: [] })
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
// Get popular searches matching query
|
|
240
|
-
const suggestions = await prisma.searchQuery.findMany({
|
|
241
|
-
where: {
|
|
242
|
-
query: { startsWith: query.toLowerCase() }
|
|
243
|
-
},
|
|
244
|
-
orderBy: { count: 'desc' },
|
|
245
|
-
take: 5,
|
|
246
|
-
select: { query: true }
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
return Response.json({
|
|
250
|
-
suggestions: suggestions.map(s => s.query)
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
## When to Use
|
|
256
|
-
|
|
257
|
-
- Content search
|
|
258
|
-
- Product search
|
|
259
|
-
- User search
|
|
260
|
-
- Document search
|