@chiselandco/nexus 2.2.7 → 2.5.0
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 +274 -286
- package/dist/FilterSidebar.d.ts +20 -0
- package/dist/FilterSidebar.d.ts.map +1 -0
- package/dist/FilterSidebar.js +266 -0
- package/dist/FilteredPortfolio.d.ts +45 -0
- package/dist/FilteredPortfolio.d.ts.map +1 -0
- package/dist/FilteredPortfolio.js +134 -0
- package/dist/GalleryCarousel.d.ts +9 -2
- package/dist/GalleryCarousel.d.ts.map +1 -1
- package/dist/GalleryCarousel.js +363 -63
- package/dist/ProjectDetail.d.ts +3 -1
- package/dist/ProjectDetail.d.ts.map +1 -1
- package/dist/ProjectDetail.js +33 -18
- package/dist/ProjectMenu.d.ts +8 -3
- package/dist/ProjectMenu.d.ts.map +1 -1
- package/dist/ProjectMenu.js +12 -16
- package/dist/ProjectMenuClient.d.ts +4 -2
- package/dist/ProjectMenuClient.d.ts.map +1 -1
- package/dist/ProjectMenuClient.js +6 -7
- package/dist/ProjectPortfolio.d.ts +3 -1
- package/dist/ProjectPortfolio.d.ts.map +1 -1
- package/dist/ProjectPortfolio.js +4 -4
- package/dist/ProjectPortfolioClient.d.ts +3 -1
- package/dist/ProjectPortfolioClient.d.ts.map +1 -1
- package/dist/ProjectPortfolioClient.js +4 -6
- package/dist/SimilarProjects.d.ts +3 -1
- package/dist/SimilarProjects.d.ts.map +1 -1
- package/dist/SimilarProjects.js +11 -9
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/dist/ProjectFilters.d.ts +0 -11
- package/dist/ProjectFilters.d.ts.map +0 -1
- package/dist/ProjectFilters.js +0 -49
- package/dist/ProjectGrid.d.ts +0 -10
- package/dist/ProjectGrid.d.ts.map +0 -1
- package/dist/ProjectGrid.js +0 -8
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# @chiselandco/nexus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Self-contained project portfolio components for Next.js App Router. Pass a `clientSlug`, `apiBase`, and `apiKey` — each component fetches, caches, and renders everything it needs with no client-side waterfall requests.
|
|
4
|
+
|
|
5
|
+
**Version:** 2.4.0
|
|
6
|
+
|
|
7
|
+
---
|
|
4
8
|
|
|
5
9
|
## Requirements
|
|
6
10
|
|
|
@@ -9,6 +13,8 @@ A suite of self-contained project portfolio components for Next.js App Router. D
|
|
|
9
13
|
|
|
10
14
|
No other dependencies required.
|
|
11
15
|
|
|
16
|
+
---
|
|
17
|
+
|
|
12
18
|
## Installation
|
|
13
19
|
|
|
14
20
|
```bash
|
|
@@ -19,23 +25,24 @@ npm install @chiselandco/nexus
|
|
|
19
25
|
|
|
20
26
|
## Quick Start
|
|
21
27
|
|
|
22
|
-
|
|
28
|
+
The most common full setup — a filterable projects grid, a detail page with similar projects, and a megamenu in the nav.
|
|
23
29
|
|
|
24
30
|
```tsx
|
|
25
31
|
// app/projects/page.tsx
|
|
26
|
-
import {
|
|
32
|
+
import { FilteredPortfolio } from "@chiselandco/nexus"
|
|
27
33
|
|
|
28
34
|
export default async function ProjectsPage({
|
|
29
35
|
searchParams,
|
|
30
36
|
}: {
|
|
31
|
-
searchParams:
|
|
37
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
32
38
|
}) {
|
|
33
39
|
return (
|
|
34
|
-
<
|
|
40
|
+
<FilteredPortfolio
|
|
35
41
|
clientSlug="your-client-slug"
|
|
36
42
|
apiBase="https://your-api.com"
|
|
43
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
37
44
|
basePath="/projects"
|
|
38
|
-
searchParams={searchParams}
|
|
45
|
+
searchParams={await searchParams}
|
|
39
46
|
/>
|
|
40
47
|
)
|
|
41
48
|
}
|
|
@@ -45,26 +52,29 @@ export default async function ProjectsPage({
|
|
|
45
52
|
// app/projects/[slug]/page.tsx
|
|
46
53
|
import { ProjectDetail, SimilarProjects } from "@chiselandco/nexus"
|
|
47
54
|
|
|
48
|
-
export default async function ProjectPage({
|
|
55
|
+
export default async function ProjectPage({
|
|
56
|
+
params,
|
|
57
|
+
}: {
|
|
58
|
+
params: Promise<{ slug: string }>
|
|
59
|
+
}) {
|
|
60
|
+
const { slug } = await params
|
|
61
|
+
const apiKey = process.env.YOUR_CLIENT_API_KEY!
|
|
62
|
+
|
|
49
63
|
return (
|
|
50
64
|
<>
|
|
51
65
|
<ProjectDetail
|
|
52
|
-
slug={
|
|
66
|
+
slug={slug}
|
|
53
67
|
clientSlug="your-client-slug"
|
|
54
68
|
apiBase="https://your-api.com"
|
|
69
|
+
apiKey={apiKey}
|
|
55
70
|
backPath="/projects"
|
|
71
|
+
backLabel="All Projects"
|
|
56
72
|
/>
|
|
57
|
-
|
|
58
|
-
{/* Hardcode the slugs you want shown — update per client request */}
|
|
59
73
|
<SimilarProjects
|
|
60
|
-
|
|
61
|
-
"jacob-javits-convention-center",
|
|
62
|
-
"tillamook-bay-community-college",
|
|
63
|
-
"lcisd-liberty-hill-high-school",
|
|
64
|
-
]}
|
|
65
|
-
excludeSlug={params.slug}
|
|
74
|
+
excludeSlug={slug}
|
|
66
75
|
clientSlug="your-client-slug"
|
|
67
76
|
apiBase="https://your-api.com"
|
|
77
|
+
apiKey={apiKey}
|
|
68
78
|
basePath="/projects"
|
|
69
79
|
/>
|
|
70
80
|
</>
|
|
@@ -79,18 +89,18 @@ import { createMenuHandler } from "@chiselandco/nexus"
|
|
|
79
89
|
export const GET = createMenuHandler({
|
|
80
90
|
clientSlug: "your-client-slug",
|
|
81
91
|
apiBase: "https://your-api.com",
|
|
92
|
+
apiKey: process.env.YOUR_CLIENT_API_KEY!,
|
|
82
93
|
})
|
|
83
94
|
```
|
|
84
95
|
|
|
85
96
|
```tsx
|
|
86
|
-
// components/Nav.tsx
|
|
97
|
+
// components/Nav.tsx
|
|
87
98
|
"use client"
|
|
88
99
|
import { ProjectMenuClient } from "@chiselandco/nexus"
|
|
89
100
|
|
|
90
101
|
export function Nav() {
|
|
91
102
|
return (
|
|
92
103
|
<nav>
|
|
93
|
-
{/* ... other nav items ... */}
|
|
94
104
|
<ProjectMenuClient
|
|
95
105
|
dataUrl="/api/chisel-menu"
|
|
96
106
|
basePath="/projects"
|
|
@@ -105,25 +115,25 @@ export function Nav() {
|
|
|
105
115
|
|
|
106
116
|
## Components
|
|
107
117
|
|
|
108
|
-
### `
|
|
118
|
+
### `FilteredPortfolio`
|
|
109
119
|
|
|
110
|
-
|
|
120
|
+
Server component. The recommended primary projects grid. Fetches all projects once, reads `filter[key]=` URL params server-side to narrow results using AND-across-fields / OR-within-field logic, then renders a project count toolbar with a `FilterSidebar` trigger above a responsive card grid.
|
|
111
121
|
|
|
112
122
|
```tsx
|
|
113
|
-
|
|
114
|
-
import { ProjectPortfolio } from "@chiselandco/nexus"
|
|
123
|
+
import { FilteredPortfolio } from "@chiselandco/nexus"
|
|
115
124
|
|
|
116
125
|
export default async function ProjectsPage({
|
|
117
126
|
searchParams,
|
|
118
127
|
}: {
|
|
119
|
-
searchParams:
|
|
128
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
120
129
|
}) {
|
|
121
130
|
return (
|
|
122
|
-
<
|
|
131
|
+
<FilteredPortfolio
|
|
123
132
|
clientSlug="your-client-slug"
|
|
124
133
|
apiBase="https://your-api.com"
|
|
134
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
125
135
|
basePath="/projects"
|
|
126
|
-
searchParams={searchParams}
|
|
136
|
+
searchParams={await searchParams}
|
|
127
137
|
/>
|
|
128
138
|
)
|
|
129
139
|
}
|
|
@@ -131,103 +141,71 @@ export default async function ProjectsPage({
|
|
|
131
141
|
|
|
132
142
|
| Prop | Type | Required | Default | Description |
|
|
133
143
|
|---|---|---|---|---|
|
|
134
|
-
| `clientSlug` | `string` | Yes | — |
|
|
135
|
-
| `apiBase` | `string` | Yes | — | Base URL of the
|
|
136
|
-
| `
|
|
137
|
-
| `searchParams` | `Record<string, string \| string[] \| undefined>` |
|
|
138
|
-
| `
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
| `clientSlug` | `string` | Yes | — | Client identifier passed to the API |
|
|
145
|
+
| `apiBase` | `string` | Yes | — | Base URL of the Chisel API |
|
|
146
|
+
| `apiKey` | `string` | Yes | — | Client API key — always pass via environment variable, never hardcode |
|
|
147
|
+
| `searchParams` | `Record<string, string \| string[] \| undefined>` | Yes | — | Resolved Next.js `searchParams` — await it before passing in Next.js 16+ |
|
|
148
|
+
| `basePath` | `string` | No | `"/projects"` | Base path for project detail card links |
|
|
149
|
+
| `filterKeys` | `string[]` | No | All eligible fields | Ordered list of field keys to expose in the filter drawer |
|
|
150
|
+
| `font` | `string` | No | System font | Font family string |
|
|
151
|
+
| `noCache` | `boolean` | No | `false` | Sets `cache: "no-store"` — useful during development |
|
|
152
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds |
|
|
141
153
|
|
|
142
|
-
|
|
154
|
+
#### URL param format
|
|
143
155
|
|
|
144
|
-
|
|
156
|
+
Filters are written to and read from URL params in the form `filter[fieldKey]=slug1,slug2`. Multiple values within a field are OR'd; multiple fields are AND'd.
|
|
145
157
|
|
|
146
158
|
```
|
|
147
|
-
/projects?filter[
|
|
148
|
-
/projects?filter[type]=educational-facilities
|
|
159
|
+
/projects?filter[application]=hospitality,education&filter[systems]=spacematic
|
|
149
160
|
```
|
|
150
161
|
|
|
151
|
-
When active filters are applied, a filter banner is shown above the grid with a "Clear filters" link back to `basePath`.
|
|
152
|
-
|
|
153
162
|
---
|
|
154
163
|
|
|
155
|
-
### `
|
|
156
|
-
|
|
157
|
-
A `"use client"` version of the project grid. Fetches all projects once on mount (module-level cached — no re-fetch on remount) and filters them locally in memory. Use this when you need the grid inside a client component tree, or when you want to build your own custom filter UI.
|
|
158
|
-
|
|
159
|
-
```tsx
|
|
160
|
-
// Works in any component — no RSC required
|
|
161
|
-
import { ProjectPortfolioClient } from "@chiselandco/nexus"
|
|
162
|
-
|
|
163
|
-
export default function ProjectsPage() {
|
|
164
|
-
return (
|
|
165
|
-
<ProjectPortfolioClient
|
|
166
|
-
clientSlug="your-client-slug"
|
|
167
|
-
apiBase="https://your-api.com"
|
|
168
|
-
basePath="/projects"
|
|
169
|
-
/>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
```
|
|
164
|
+
### `FilterSidebar`
|
|
173
165
|
|
|
174
|
-
|
|
166
|
+
Client component (`"use client"`). Renders an "Advanced Filters" trigger that opens a right-side drawer with one section per filterable field. Pills are solid black when active and outlined when inactive. Filter state is written to URL params so filtered views are shareable and survive page refresh.
|
|
175
167
|
|
|
176
|
-
|
|
168
|
+
Used internally by `FilteredPortfolio` — only import it standalone if you need to build a custom layout around it.
|
|
177
169
|
|
|
178
170
|
```tsx
|
|
179
|
-
|
|
180
|
-
import { useState } from "react"
|
|
181
|
-
import { ProjectPortfolioClient } from "@chiselandco/nexus"
|
|
171
|
+
import { FilterSidebar } from "@chiselandco/nexus"
|
|
182
172
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
{/* Your own filter UI */}
|
|
189
|
-
<select onChange={(e) => setFilters({ type: e.target.value })}>
|
|
190
|
-
<option value="">All Types</option>
|
|
191
|
-
<option value="commercial">Commercial</option>
|
|
192
|
-
<option value="educational-facilities">Educational</option>
|
|
193
|
-
</select>
|
|
194
|
-
|
|
195
|
-
<ProjectPortfolioClient
|
|
196
|
-
clientSlug="your-client-slug"
|
|
197
|
-
apiBase="https://your-api.com"
|
|
198
|
-
basePath="/projects"
|
|
199
|
-
filters={filters}
|
|
200
|
-
/>
|
|
201
|
-
</>
|
|
202
|
-
)
|
|
203
|
-
}
|
|
173
|
+
<FilterSidebar
|
|
174
|
+
schema={schema}
|
|
175
|
+
filterKeys={["application", "systems", "material"]}
|
|
176
|
+
triggerLabel="Advanced Filters"
|
|
177
|
+
/>
|
|
204
178
|
```
|
|
205
179
|
|
|
206
180
|
| Prop | Type | Required | Default | Description |
|
|
207
181
|
|---|---|---|---|---|
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
212
|
-
| `columns` | `2 \| 3` | No | `3` | Number of columns in the project grid |
|
|
213
|
-
| `font` | `string` | No | System font stack | Font family string applied to all text |
|
|
182
|
+
| `schema` | `CustomFieldSchema[]` | Yes | — | Field schema from the API — only `select` and `multi-select` fields with options are used |
|
|
183
|
+
| `filterKeys` | `string[]` | No | All eligible fields | Ordered list of field keys to show in the drawer |
|
|
184
|
+
| `triggerLabel` | `string` | No | `"Advanced Filters"` | Label for the trigger link |
|
|
185
|
+
| `font` | `string` | No | `"inherit"` | Font family string |
|
|
214
186
|
|
|
215
187
|
---
|
|
216
188
|
|
|
217
189
|
### `ProjectDetail`
|
|
218
190
|
|
|
219
|
-
|
|
191
|
+
Server component. Fetches a single project by slug and renders a hero image, a stats bar, a project overview section with description and specs sidebar, and a `GalleryCarousel` with filterable media tag pills.
|
|
220
192
|
|
|
221
193
|
```tsx
|
|
222
194
|
// app/projects/[slug]/page.tsx
|
|
223
195
|
import { ProjectDetail } from "@chiselandco/nexus"
|
|
224
196
|
|
|
225
|
-
export default async function ProjectPage({
|
|
197
|
+
export default async function ProjectPage({
|
|
198
|
+
params,
|
|
199
|
+
}: {
|
|
200
|
+
params: Promise<{ slug: string }>
|
|
201
|
+
}) {
|
|
202
|
+
const { slug } = await params
|
|
226
203
|
return (
|
|
227
204
|
<ProjectDetail
|
|
228
|
-
slug={
|
|
205
|
+
slug={slug}
|
|
229
206
|
clientSlug="your-client-slug"
|
|
230
207
|
apiBase="https://your-api.com"
|
|
208
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
231
209
|
backPath="/projects"
|
|
232
210
|
backLabel="All Projects"
|
|
233
211
|
/>
|
|
@@ -237,24 +215,22 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
237
215
|
|
|
238
216
|
#### Stats bar
|
|
239
217
|
|
|
240
|
-
The stats bar below the hero is driven
|
|
218
|
+
The stats bar below the hero is schema-driven. Fields are shown in this order when present and populated:
|
|
241
219
|
|
|
242
|
-
| Schema key | Label
|
|
220
|
+
| Schema key | Label |
|
|
243
221
|
|---|---|
|
|
244
222
|
| `location` | Location |
|
|
245
|
-
| `type`
|
|
223
|
+
| `type` | field `name` from schema |
|
|
246
224
|
| `coverage` | Coverage |
|
|
247
225
|
| `year-completed` | Completed |
|
|
248
226
|
| `architect` | Architect |
|
|
249
227
|
| `general-contractor` | General Contractor |
|
|
250
228
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
#### Project Overview specs sidebar
|
|
229
|
+
#### Specs sidebar
|
|
254
230
|
|
|
255
|
-
The "Project Overview" section renders
|
|
231
|
+
The "Project Overview" section renders a specs sidebar when any of these fields are populated:
|
|
256
232
|
|
|
257
|
-
| Schema key | Label
|
|
233
|
+
| Schema key | Label |
|
|
258
234
|
|---|---|
|
|
259
235
|
| `systems-used` | Systems Used |
|
|
260
236
|
| `systems` | Track Systems |
|
|
@@ -263,69 +239,93 @@ The "Project Overview" section renders the project description on the left and a
|
|
|
263
239
|
| `finishes` | Finishes |
|
|
264
240
|
| `specifications` | Specifications |
|
|
265
241
|
|
|
266
|
-
|
|
242
|
+
Both the stats bar and specs sidebar are fully schema-driven — fields not present in a client's schema are silently omitted, making `ProjectDetail` safe to use across clients with different field configurations.
|
|
243
|
+
|
|
244
|
+
#### Media enrichment
|
|
245
|
+
|
|
246
|
+
`ProjectDetail` automatically fetches `custom_field_values` from the list endpoint (where they are available) and merges them onto the single-project media items before passing them to `GalleryCarousel`. No extra work is needed.
|
|
267
247
|
|
|
268
248
|
| Prop | Type | Required | Default | Description |
|
|
269
249
|
|---|---|---|---|---|
|
|
270
250
|
| `slug` | `string` | Yes | — | The project slug to load |
|
|
271
251
|
| `clientSlug` | `string` | Yes | — | The client slug that owns this project |
|
|
272
252
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
253
|
+
| `apiKey` | `string` | Yes | — | Client API key — always pass via environment variable, never hardcode |
|
|
273
254
|
| `backPath` | `string` | No | `"/projects"` | Path for the back navigation link |
|
|
274
255
|
| `backLabel` | `string` | No | `"All Projects"` | Label for the back navigation link |
|
|
275
|
-
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds
|
|
276
|
-
| `noCache` | `boolean` | No | `false` |
|
|
256
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds |
|
|
257
|
+
| `noCache` | `boolean` | No | `false` | Sets `cache: "no-store"` — useful during development |
|
|
277
258
|
|
|
278
259
|
---
|
|
279
260
|
|
|
280
261
|
### `GalleryCarousel`
|
|
281
262
|
|
|
282
|
-
|
|
263
|
+
Client component (`"use client"`). Image carousel with previous/next arrows, a counter badge, a scrollable thumbnail strip, URL-synced image filters, and automatic media tag pills. Used internally by `ProjectDetail` but can be used standalone.
|
|
283
264
|
|
|
284
265
|
```tsx
|
|
285
266
|
"use client"
|
|
286
267
|
import { GalleryCarousel } from "@chiselandco/nexus"
|
|
287
268
|
|
|
288
|
-
|
|
289
|
-
export function ProjectGallery({ media, title }: { media: Media[]; title: string }) {
|
|
269
|
+
export function ProjectGallery({ media, schema, title }) {
|
|
290
270
|
return (
|
|
291
271
|
<GalleryCarousel
|
|
292
272
|
images={media}
|
|
293
273
|
projectTitle={title}
|
|
274
|
+
schema={schema}
|
|
294
275
|
/>
|
|
295
276
|
)
|
|
296
277
|
}
|
|
297
278
|
```
|
|
298
279
|
|
|
299
|
-
|
|
280
|
+
#### Media tag pills
|
|
281
|
+
|
|
282
|
+
When a media item has `custom_field_values` set, `GalleryCarousel` renders frosted-glass pills in the bottom-left corner of the active image. Each pill shows the field name and resolved value — e.g. `System: Speed-Rail with Mesh Infill`, `Finish: Black Anodized`. Pills update as the user navigates between images.
|
|
283
|
+
|
|
284
|
+
#### Image filtering
|
|
285
|
+
|
|
286
|
+
When images have `custom_field_values`, a filter bar appears above the gallery. Each field that appears on at least one image is shown as a row of pill buttons. Selecting a pill narrows both the main image and the thumbnail strip to only matching images. Multiple fields can be filtered simultaneously (AND logic). Filter state is written to the URL so filtered views are shareable:
|
|
287
|
+
|
|
288
|
+
```
|
|
289
|
+
/projects/jacob-javits?filter[system]=Structural Glass&filter[finish]=Black Anodized
|
|
290
|
+
```
|
|
300
291
|
|
|
301
292
|
| Prop | Type | Required | Default | Description |
|
|
302
293
|
|---|---|---|---|---|
|
|
303
294
|
| `images` | `Media[]` | Yes | — | Array of media objects from the projects API |
|
|
304
295
|
| `projectTitle` | `string` | Yes | — | Used as the alt text fallback for the main image |
|
|
296
|
+
| `schema` | `CustomFieldSchema[]` | No | `[]` | Client custom fields schema — used to resolve slug values to labels for pills and filter options |
|
|
305
297
|
|
|
306
298
|
---
|
|
307
299
|
|
|
308
300
|
### `SimilarProjects`
|
|
309
301
|
|
|
310
|
-
|
|
302
|
+
Server component. Fetches all projects for a client, optionally filters to those matching provided field values, excludes the current project, and renders a section of matching results.
|
|
311
303
|
|
|
312
304
|
```tsx
|
|
313
305
|
// app/projects/[slug]/page.tsx
|
|
314
306
|
import { ProjectDetail, SimilarProjects } from "@chiselandco/nexus"
|
|
315
307
|
|
|
316
|
-
export default async function ProjectPage({
|
|
308
|
+
export default async function ProjectPage({
|
|
309
|
+
params,
|
|
310
|
+
}: {
|
|
311
|
+
params: Promise<{ slug: string }>
|
|
312
|
+
}) {
|
|
313
|
+
const { slug } = await params
|
|
314
|
+
const apiKey = process.env.YOUR_CLIENT_API_KEY!
|
|
315
|
+
|
|
317
316
|
return (
|
|
318
317
|
<>
|
|
319
318
|
<ProjectDetail
|
|
320
|
-
slug={
|
|
319
|
+
slug={slug}
|
|
321
320
|
clientSlug="your-client-slug"
|
|
322
321
|
apiBase="https://your-api.com"
|
|
322
|
+
apiKey={apiKey}
|
|
323
323
|
/>
|
|
324
324
|
<SimilarProjects
|
|
325
|
-
|
|
326
|
-
excludeSlug={params.slug}
|
|
325
|
+
excludeSlug={slug}
|
|
327
326
|
clientSlug="your-client-slug"
|
|
328
327
|
apiBase="https://your-api.com"
|
|
328
|
+
apiKey={apiKey}
|
|
329
329
|
basePath="/projects"
|
|
330
330
|
/>
|
|
331
331
|
</>
|
|
@@ -333,133 +333,93 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
333
333
|
}
|
|
334
334
|
```
|
|
335
335
|
|
|
336
|
-
####
|
|
336
|
+
#### Filtering by field value
|
|
337
337
|
|
|
338
|
-
|
|
338
|
+
Pass `filters` to match projects that share a field value with the current project. The field values from the current project can be derived from the API response:
|
|
339
339
|
|
|
340
340
|
```tsx
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
341
|
+
const apiKey = process.env.YOUR_CLIENT_API_KEY!
|
|
342
|
+
const res = await fetch(
|
|
343
|
+
`${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=${apiKey}`,
|
|
344
|
+
{ next: { revalidate: 86400 } }
|
|
345
|
+
)
|
|
346
|
+
const project = res.ok ? (await res.json())?.data : null
|
|
347
|
+
// type may be a plain string or single-element array
|
|
348
|
+
const typeVal = project?.custom_field_values?.type
|
|
349
|
+
const projectType = Array.isArray(typeVal) ? typeVal[0] : typeVal ?? null
|
|
351
350
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
<SimilarProjects
|
|
361
|
-
filters={{ type: projectType }}
|
|
362
|
-
excludeSlug={params.slug}
|
|
363
|
-
clientSlug="your-client-slug"
|
|
364
|
-
apiBase="https://your-api.com"
|
|
365
|
-
basePath="/projects"
|
|
366
|
-
/>
|
|
367
|
-
)}
|
|
368
|
-
</>
|
|
369
|
-
)
|
|
370
|
-
}
|
|
351
|
+
<SimilarProjects
|
|
352
|
+
filters={projectType ? { type: projectType } : {}}
|
|
353
|
+
excludeSlug={slug}
|
|
354
|
+
clientSlug="your-client-slug"
|
|
355
|
+
apiBase="https://your-api.com"
|
|
356
|
+
apiKey={apiKey}
|
|
357
|
+
basePath="/projects"
|
|
358
|
+
/>
|
|
371
359
|
```
|
|
372
360
|
|
|
373
|
-
#### Manually specifying projects
|
|
361
|
+
#### Manually specifying projects
|
|
374
362
|
|
|
375
|
-
Pass `projectSlugs` to hand-pick exactly which projects appear
|
|
376
|
-
|
|
377
|
-
This is the recommended approach for most client sites. The slugs are hardcoded in the page file. If a client wants different projects shown, update the slugs and redeploy.
|
|
363
|
+
Pass `projectSlugs` to hand-pick exactly which projects appear. This overrides `filters` entirely and is the simplest approach when you want curated results. `excludeSlug` is still respected.
|
|
378
364
|
|
|
379
365
|
```tsx
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
/>
|
|
393
|
-
|
|
394
|
-
{/* Similar projects hardcoded — update slugs per client request */}
|
|
395
|
-
<SimilarProjects
|
|
396
|
-
projectSlugs={[
|
|
397
|
-
"jacob-javits-convention-center",
|
|
398
|
-
"tillamook-bay-community-college",
|
|
399
|
-
"lcisd-liberty-hill-high-school",
|
|
400
|
-
]}
|
|
401
|
-
excludeSlug={params.slug}
|
|
402
|
-
clientSlug="your-client-slug"
|
|
403
|
-
apiBase="https://your-api.com"
|
|
404
|
-
basePath="/projects"
|
|
405
|
-
/>
|
|
406
|
-
</>
|
|
407
|
-
)
|
|
408
|
-
}
|
|
366
|
+
<SimilarProjects
|
|
367
|
+
projectSlugs={[
|
|
368
|
+
"jacob-javits-convention-center",
|
|
369
|
+
"tillamook-bay-community-college",
|
|
370
|
+
"lcisd-liberty-hill-high-school",
|
|
371
|
+
]}
|
|
372
|
+
excludeSlug={slug}
|
|
373
|
+
clientSlug="your-client-slug"
|
|
374
|
+
apiBase="https://your-api.com"
|
|
375
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
376
|
+
basePath="/projects"
|
|
377
|
+
/>
|
|
409
378
|
```
|
|
410
379
|
|
|
411
|
-
> `excludeSlug` is still respected even in `projectSlugs` mode — if the current page's slug appears in the list it is automatically removed so a project never links to itself.
|
|
412
|
-
|
|
413
|
-
To update which projects appear, find the `projectSlugs` array in the page file and swap in the new slugs. Project slugs are visible in the URL when browsing the portfolio: `/projects/jacob-javits-convention-center` → slug is `jacob-javits-convention-center`.
|
|
414
|
-
|
|
415
380
|
#### Card variant
|
|
416
381
|
|
|
417
|
-
Use `variant="card"` to render
|
|
382
|
+
Use `variant="card"` to render baseball-card style instead of the default list style:
|
|
418
383
|
|
|
419
384
|
```tsx
|
|
420
|
-
<SimilarProjects
|
|
421
|
-
filters={{ type: "commercial" }}
|
|
422
|
-
excludeSlug={params.slug}
|
|
423
|
-
clientSlug="your-client-slug"
|
|
424
|
-
apiBase="https://your-api.com"
|
|
425
|
-
basePath="/projects"
|
|
426
|
-
variant="card"
|
|
427
|
-
/>
|
|
385
|
+
<SimilarProjects variant="card" ... />
|
|
428
386
|
```
|
|
429
387
|
|
|
430
388
|
| Prop | Type | Required | Default | Description |
|
|
431
389
|
|---|---|---|---|---|
|
|
432
390
|
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
433
391
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
434
|
-
| `
|
|
435
|
-
| `
|
|
392
|
+
| `apiKey` | `string` | Yes | — | Client API key — always pass via environment variable, never hardcode |
|
|
393
|
+
| `filters` | `Record<string, string>` | No | `{}` | Key/value pairs to filter by. All filters must match (AND logic) |
|
|
394
|
+
| `excludeSlug` | `string` | No | — | Project slug to exclude from results |
|
|
436
395
|
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
437
|
-
| `projectSlugs` | `string[]` | No | — | Explicit list of
|
|
396
|
+
| `projectSlugs` | `string[]` | No | — | Explicit ordered list of slugs to show. Overrides `filters` when provided |
|
|
438
397
|
| `maxItems` | `number` | No | `3` | Maximum number of projects to show |
|
|
439
|
-
| `title` | `string` | No | `"Similar Projects"` |
|
|
440
|
-
| `subtitle` | `string` | No | `"More Work"` | Small uppercase label above the heading
|
|
441
|
-
| `variant` | `"list" \| "card"` | No | `"list"` |
|
|
442
|
-
| `font` | `string` | No | System font stack | Font family string
|
|
443
|
-
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds
|
|
444
|
-
| `noCache` | `boolean` | No | `false` |
|
|
398
|
+
| `title` | `string` | No | `"Similar Projects"` | Section heading |
|
|
399
|
+
| `subtitle` | `string` | No | `"More Work"` | Small uppercase label above the heading |
|
|
400
|
+
| `variant` | `"list" \| "card"` | No | `"list"` | Display style |
|
|
401
|
+
| `font` | `string` | No | System font stack | Font family string |
|
|
402
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds |
|
|
403
|
+
| `noCache` | `boolean` | No | `false` | Sets `cache: "no-store"` — useful during development |
|
|
445
404
|
|
|
446
405
|
---
|
|
447
406
|
|
|
448
407
|
### `ProjectMenu`
|
|
449
408
|
|
|
450
|
-
|
|
409
|
+
Server component. Megamenu that shows featured projects as compact cards on the left and "Browse By" filter links on the right. Drop it directly into a navigation dropdown.
|
|
451
410
|
|
|
452
411
|
```tsx
|
|
453
|
-
// components/MegaMenu.tsx
|
|
412
|
+
// components/MegaMenu.tsx — must be a Server Component
|
|
454
413
|
import { ProjectMenu } from "@chiselandco/nexus"
|
|
455
414
|
|
|
456
|
-
// Must be a Server Component — do NOT add "use client"
|
|
457
415
|
export async function ProjectsMegaMenu() {
|
|
458
416
|
return (
|
|
459
417
|
<ProjectMenu
|
|
460
418
|
clientSlug="your-client-slug"
|
|
461
419
|
apiBase="https://your-api.com"
|
|
420
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
462
421
|
basePath="/projects"
|
|
422
|
+
viewAllPath="/projects"
|
|
463
423
|
subtitle="Our systems are installed in every geographic region of the U.S."
|
|
464
424
|
maxProjects={6}
|
|
465
425
|
/>
|
|
@@ -467,14 +427,13 @@ export async function ProjectsMegaMenu() {
|
|
|
467
427
|
}
|
|
468
428
|
```
|
|
469
429
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
Pass `menuId` to show a specific curated set of projects instead of all projects. The value should be the menu **slug** (not the UUID) — available slugs can be retrieved from `GET /api/v1/clients/{clientSlug}/menus`. The Browse By filters on the right always reflect the full schema regardless of which menu is active.
|
|
430
|
+
Pass `menuId` to show a specific curated set of projects instead of all projects:
|
|
473
431
|
|
|
474
432
|
```tsx
|
|
475
433
|
<ProjectMenu
|
|
476
434
|
clientSlug="your-client-slug"
|
|
477
435
|
apiBase="https://your-api.com"
|
|
436
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
478
437
|
menuId="main-nav"
|
|
479
438
|
basePath="/projects"
|
|
480
439
|
/>
|
|
@@ -484,28 +443,25 @@ Pass `menuId` to show a specific curated set of projects instead of all projects
|
|
|
484
443
|
|---|---|---|---|---|
|
|
485
444
|
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
486
445
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
487
|
-
| `
|
|
446
|
+
| `apiKey` | `string` | Yes | — | Client API key — always pass via environment variable, never hardcode |
|
|
447
|
+
| `menuId` | `string` | No | — | Slug of a curated menu. When provided fetches from `/menus/{slug}`. Browse By filters always reflect the full schema. |
|
|
488
448
|
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
489
449
|
| `viewAllPath` | `string` | No | Same as `basePath` | Path for the "View All Projects" link |
|
|
490
|
-
| `subtitle` | `string` | No | — | Description
|
|
491
|
-
| `font` | `string` | No | System font stack | Font family string
|
|
450
|
+
| `subtitle` | `string` | No | — | Description shown above the project cards |
|
|
451
|
+
| `font` | `string` | No | System font stack | Font family string |
|
|
492
452
|
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
|
|
493
|
-
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds
|
|
494
|
-
| `noCache` | `boolean` | No | `false` |
|
|
453
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds |
|
|
454
|
+
| `noCache` | `boolean` | No | `false` | Sets `cache: "no-store"` — useful during development |
|
|
495
455
|
|
|
496
456
|
---
|
|
497
457
|
|
|
498
458
|
### `ProjectMenuClient` + `createMenuHandler`
|
|
499
459
|
|
|
500
|
-
|
|
460
|
+
Client component (`"use client"`). Use when your nav or header is a client component. Fetches and caches data on first mount — the API is never called twice on re-hover or remount.
|
|
501
461
|
|
|
502
|
-
|
|
462
|
+
#### Option 1 — `dataUrl` + `createMenuHandler` (recommended)
|
|
503
463
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
#### Option 1 — `dataUrl` + `createMenuHandler` (recommended for production)
|
|
507
|
-
|
|
508
|
-
Create one API route once. The data is server-cached for 24 hours — most users never trigger a call to the upstream API at all.
|
|
464
|
+
Create one API route. Data is server-cached for 24 hours.
|
|
509
465
|
|
|
510
466
|
```ts
|
|
511
467
|
// app/api/chisel-menu/route.ts
|
|
@@ -514,22 +470,10 @@ import { createMenuHandler } from "@chiselandco/nexus"
|
|
|
514
470
|
export const GET = createMenuHandler({
|
|
515
471
|
clientSlug: "your-client-slug",
|
|
516
472
|
apiBase: "https://your-api.com",
|
|
473
|
+
apiKey: process.env.YOUR_CLIENT_API_KEY!,
|
|
517
474
|
})
|
|
518
475
|
```
|
|
519
476
|
|
|
520
|
-
For a curated menu, pass `menuId` to the handler:
|
|
521
|
-
|
|
522
|
-
```ts
|
|
523
|
-
// app/api/chisel-menu/route.ts
|
|
524
|
-
export const GET = createMenuHandler({
|
|
525
|
-
clientSlug: "your-client-slug",
|
|
526
|
-
apiBase: "https://your-api.com",
|
|
527
|
-
menuId: "main-nav",
|
|
528
|
-
})
|
|
529
|
-
```
|
|
530
|
-
|
|
531
|
-
Then pass `dataUrl` to the component:
|
|
532
|
-
|
|
533
477
|
```tsx
|
|
534
478
|
// components/Nav.tsx
|
|
535
479
|
"use client"
|
|
@@ -541,18 +485,16 @@ export function Nav() {
|
|
|
541
485
|
dataUrl="/api/chisel-menu"
|
|
542
486
|
basePath="/projects"
|
|
543
487
|
viewAllPath="/projects"
|
|
544
|
-
subtitle="Explore our portfolio
|
|
488
|
+
subtitle="Explore our portfolio."
|
|
545
489
|
maxProjects={6}
|
|
546
490
|
/>
|
|
547
491
|
)
|
|
548
492
|
}
|
|
549
493
|
```
|
|
550
494
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
#### Option 2 — Direct fetch (quick setup / non-Next.js environments)
|
|
495
|
+
#### Option 2 — Direct fetch (quick setup)
|
|
554
496
|
|
|
555
|
-
No API route needed. The component fetches directly from the upstream API on first mount
|
|
497
|
+
No API route needed. The component fetches directly from the upstream API on first mount. Note: this exposes the API call to the client browser.
|
|
556
498
|
|
|
557
499
|
```tsx
|
|
558
500
|
"use client"
|
|
@@ -563,6 +505,7 @@ export function Nav() {
|
|
|
563
505
|
<ProjectMenuClient
|
|
564
506
|
clientSlug="your-client-slug"
|
|
565
507
|
apiBase="https://your-api.com"
|
|
508
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
566
509
|
basePath="/projects"
|
|
567
510
|
viewAllPath="/projects"
|
|
568
511
|
/>
|
|
@@ -570,101 +513,146 @@ export function Nav() {
|
|
|
570
513
|
}
|
|
571
514
|
```
|
|
572
515
|
|
|
573
|
-
With a curated menu:
|
|
574
|
-
|
|
575
|
-
```tsx
|
|
576
|
-
<ProjectMenuClient
|
|
577
|
-
clientSlug="your-client-slug"
|
|
578
|
-
apiBase="https://your-api.com"
|
|
579
|
-
menuId="main-nav"
|
|
580
|
-
basePath="/projects"
|
|
581
|
-
viewAllPath="/projects"
|
|
582
|
-
/>
|
|
583
|
-
```
|
|
584
|
-
|
|
585
|
-
---
|
|
586
|
-
|
|
587
516
|
| Prop | Type | Required | Default | Description |
|
|
588
517
|
|---|---|---|---|---|
|
|
589
|
-
| `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()
|
|
518
|
+
| `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()` — recommended for production |
|
|
590
519
|
| `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
|
|
591
520
|
| `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
|
|
592
|
-
| `
|
|
593
|
-
| `
|
|
521
|
+
| `apiKey` | `string` | No* | — | Client API key for direct fetch mode |
|
|
522
|
+
| `menuId` | `string` | No | — | Slug of a curated menu |
|
|
594
523
|
| `basePath` | `string` | Yes | — | Base path for project detail links |
|
|
595
524
|
| `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
|
|
596
|
-
| `subtitle` | `string` | No | — | Description shown above the project cards
|
|
525
|
+
| `subtitle` | `string` | No | — | Description shown above the project cards |
|
|
597
526
|
| `font` | `string` | No | System font stack | Font family string |
|
|
598
|
-
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display
|
|
527
|
+
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
|
|
528
|
+
| `noCache` | `boolean` | No | `false` | Bypasses the module-level data cache |
|
|
599
529
|
|
|
600
|
-
*One of `dataUrl` or `clientSlug + apiBase` must be provided.
|
|
530
|
+
*One of `dataUrl` or `clientSlug + apiBase + apiKey` must be provided.
|
|
601
531
|
|
|
602
532
|
---
|
|
603
533
|
|
|
604
|
-
|
|
534
|
+
### `ProjectPortfolio`
|
|
605
535
|
|
|
606
|
-
|
|
536
|
+
Server component. Fetches all projects and renders a responsive baseball card grid (1 col mobile / 2 col tablet / 3 col desktop). Supports URL-driven filtering via `searchParams`. Prefer `FilteredPortfolio` for new projects — it includes the full filter sidebar and is the recommended default.
|
|
607
537
|
|
|
608
538
|
```tsx
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
-
|
|
539
|
+
// app/projects/page.tsx
|
|
540
|
+
import { ProjectPortfolio } from "@chiselandco/nexus"
|
|
541
|
+
|
|
542
|
+
export default async function ProjectsPage({
|
|
543
|
+
searchParams,
|
|
544
|
+
}: {
|
|
545
|
+
searchParams: Promise<Record<string, string | string[] | undefined>>
|
|
546
|
+
}) {
|
|
547
|
+
return (
|
|
548
|
+
<ProjectPortfolio
|
|
549
|
+
clientSlug="your-client-slug"
|
|
550
|
+
apiBase="https://your-api.com"
|
|
551
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
552
|
+
basePath="/projects"
|
|
553
|
+
searchParams={await searchParams}
|
|
554
|
+
/>
|
|
555
|
+
)
|
|
612
556
|
}
|
|
557
|
+
```
|
|
613
558
|
|
|
614
|
-
|
|
559
|
+
| Prop | Type | Required | Default | Description |
|
|
560
|
+
|---|---|---|---|---|
|
|
561
|
+
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
562
|
+
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
563
|
+
| `apiKey` | `string` | Yes | — | Client API key — always pass via environment variable, never hardcode |
|
|
564
|
+
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
565
|
+
| `searchParams` | `Record<string, string \| string[] \| undefined>` | No | `{}` | Filter params — pass Next.js `searchParams` directly |
|
|
566
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds |
|
|
567
|
+
| `noCache` | `boolean` | No | `false` | Sets `cache: "no-store"` — useful during development |
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
### `ProjectPortfolioClient`
|
|
572
|
+
|
|
573
|
+
Client component (`"use client"`). Same grid as `ProjectPortfolio` but renders client-side. Fetches all projects once on mount (module-level cached). Use this inside a client component tree or when you want to build a custom filter UI that filters in memory.
|
|
574
|
+
|
|
575
|
+
```tsx
|
|
615
576
|
"use client"
|
|
616
|
-
|
|
617
|
-
|
|
577
|
+
import { useState } from "react"
|
|
578
|
+
import { ProjectPortfolioClient } from "@chiselandco/nexus"
|
|
579
|
+
|
|
580
|
+
export default function ProjectsPage() {
|
|
581
|
+
const [filters, setFilters] = useState<Record<string, string>>({})
|
|
582
|
+
|
|
583
|
+
return (
|
|
584
|
+
<>
|
|
585
|
+
<select onChange={(e) => setFilters({ type: e.target.value })}>
|
|
586
|
+
<option value="">All Types</option>
|
|
587
|
+
<option value="commercial">Commercial</option>
|
|
588
|
+
</select>
|
|
589
|
+
<ProjectPortfolioClient
|
|
590
|
+
clientSlug="your-client-slug"
|
|
591
|
+
apiBase="https://your-api.com"
|
|
592
|
+
apiKey={process.env.YOUR_CLIENT_API_KEY!}
|
|
593
|
+
basePath="/projects"
|
|
594
|
+
filters={filters}
|
|
595
|
+
/>
|
|
596
|
+
</>
|
|
597
|
+
)
|
|
618
598
|
}
|
|
619
599
|
```
|
|
620
600
|
|
|
621
|
-
|
|
601
|
+
| Prop | Type | Required | Default | Description |
|
|
602
|
+
|---|---|---|---|---|
|
|
603
|
+
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
604
|
+
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
605
|
+
| `apiKey` | `string` | Yes | — | Client API key — always pass via environment variable, never hardcode |
|
|
606
|
+
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
607
|
+
| `filters` | `Record<string, string>` | No | `{}` | Active filters — filtering is instant, no API call on change |
|
|
608
|
+
| `columns` | `2 \| 3` | No | `3` | Number of grid columns |
|
|
609
|
+
| `font` | `string` | No | System font stack | Font family string |
|
|
622
610
|
|
|
623
|
-
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
## Server vs Client components
|
|
614
|
+
|
|
615
|
+
| Component | Type | Notes |
|
|
624
616
|
|---|---|---|
|
|
625
|
-
| `
|
|
626
|
-
| `
|
|
627
|
-
| `
|
|
628
|
-
| `
|
|
629
|
-
| `
|
|
630
|
-
| `
|
|
617
|
+
| `FilteredPortfolio` | Server | Recommended default for project grids |
|
|
618
|
+
| `FilterSidebar` | Client | Used internally by `FilteredPortfolio` |
|
|
619
|
+
| `ProjectPortfolio` | Server | Simpler grid without sidebar |
|
|
620
|
+
| `ProjectPortfolioClient` | Client | For use inside client trees |
|
|
621
|
+
| `ProjectDetail` | Server | Full project detail page |
|
|
622
|
+
| `GalleryCarousel` | Client | Used internally by `ProjectDetail` |
|
|
631
623
|
| `SimilarProjects` | Server | After `ProjectDetail` on detail pages |
|
|
624
|
+
| `ProjectMenu` | Server | Server-rendered nav megamenu |
|
|
625
|
+
| `ProjectMenuClient` | Client | Client-rendered nav megamenu |
|
|
626
|
+
|
|
627
|
+
All server components must be rendered in a server context. If your parent component uses `"use client"`, use the client variants or pass server components as `children` from a server parent.
|
|
632
628
|
|
|
633
629
|
---
|
|
634
630
|
|
|
635
631
|
## Caching
|
|
636
632
|
|
|
637
|
-
| Component | Server
|
|
633
|
+
| Component | Server cache | Client cache |
|
|
638
634
|
|---|---|---|
|
|
639
|
-
| `
|
|
635
|
+
| `FilteredPortfolio` | 24h via `next.revalidate` | — |
|
|
636
|
+
| `ProjectPortfolio` | 24h via `next.revalidate` | — |
|
|
637
|
+
| `ProjectDetail` | 24h via `next.revalidate` | — |
|
|
638
|
+
| `SimilarProjects` | 24h via `next.revalidate` | — |
|
|
639
|
+
| `ProjectMenu` | 24h via `next.revalidate` | — |
|
|
640
640
|
| `ProjectMenuClient` + `createMenuHandler` | 24h (route handler) | Per-session module cache |
|
|
641
641
|
| `ProjectMenuClient` (direct fetch) | None | Per-session module cache |
|
|
642
|
-
| `ProjectPortfolio` (RSC) | 24h via `next.revalidate` | — |
|
|
643
642
|
| `ProjectPortfolioClient` | None | Per-session module cache |
|
|
644
|
-
| `ProjectDetail` (RSC) | 24h via `next.revalidate` | — |
|
|
645
|
-
| `SimilarProjects` (RSC) | 24h via `next.revalidate` | — |
|
|
646
643
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
| Method | Scope | Use case |
|
|
650
|
-
|---|---|---|
|
|
651
|
-
| `?bust=1` on `/api/chisel-menu` | Single request | Dev/testing after a CMS change |
|
|
652
|
-
| `CHISEL_CACHE_BYPASS=true` env var | Entire deployment | Staging environments |
|
|
653
|
-
| `revalidateTag("chisel-menu-{clientSlug}")` | Server cache | CMS webhook on content publish |
|
|
654
|
-
| `noCache: true` on `ProjectMenu` | Single render | Debug during development |
|
|
644
|
+
Pass `noCache={true}` on any server component to bypass the cache during development. To invalidate the server cache from a CMS webhook:
|
|
655
645
|
|
|
656
646
|
```ts
|
|
657
|
-
// CMS webhook — invalidate server cache on publish
|
|
658
647
|
import { revalidateTag } from "next/cache"
|
|
659
648
|
revalidateTag("chisel-menu-your-client-slug")
|
|
660
|
-
|
|
661
|
-
// For a curated menu, the tag includes the menuId
|
|
649
|
+
// For a curated menu:
|
|
662
650
|
revalidateTag("chisel-menu-your-client-slug-main-nav")
|
|
663
651
|
```
|
|
664
652
|
|
|
665
653
|
---
|
|
666
654
|
|
|
667
|
-
## Publishing
|
|
655
|
+
## Publishing
|
|
668
656
|
|
|
669
657
|
```bash
|
|
670
658
|
npm login
|