project-portfolio 2.0.0 → 2.1.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 +349 -94
- package/dist/GalleryCarousel.d.ts +3 -2
- package/dist/GalleryCarousel.d.ts.map +1 -1
- package/dist/GalleryCarousel.js +1 -1
- package/dist/ProjectDetail.js +1 -1
- package/dist/ProjectMenu.d.ts +11 -3
- package/dist/ProjectMenu.d.ts.map +1 -1
- package/dist/ProjectMenu.js +57 -30
- package/dist/ProjectMenuClient.d.ts +6 -1
- package/dist/ProjectMenuClient.d.ts.map +1 -1
- package/dist/ProjectMenuClient.js +167 -103
- package/dist/ProjectPortfolioClient.d.ts.map +1 -1
- package/dist/ProjectPortfolioClient.js +6 -1
- package/dist/SimilarProjects.d.ts +13 -1
- package/dist/SimilarProjects.d.ts.map +1 -1
- package/dist/SimilarProjects.js +70 -24
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# project-portfolio
|
|
2
2
|
|
|
3
|
-
A suite of self-contained project portfolio components for Next.js App Router. Drop in a `clientSlug` and `apiBase` — each component fetches, caches, and renders everything
|
|
3
|
+
A suite of self-contained project portfolio components for Next.js App Router. Drop in a `clientSlug` and `apiBase` — each component fetches, caches, and renders everything it needs with zero client-side waterfall requests.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
@@ -17,39 +17,97 @@ npm install project-portfolio
|
|
|
17
17
|
|
|
18
18
|
---
|
|
19
19
|
|
|
20
|
-
##
|
|
21
|
-
|
|
22
|
-
### `ProjectPortfolio`
|
|
20
|
+
## Quick Start
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
Here is the most common full setup — a projects grid page, a detail page with similar projects, and a megamenu in the nav.
|
|
25
23
|
|
|
26
24
|
```tsx
|
|
27
25
|
// app/projects/page.tsx
|
|
28
26
|
import { ProjectPortfolio } from "project-portfolio"
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
export default async function ProjectsPage({
|
|
29
|
+
searchParams,
|
|
30
|
+
}: {
|
|
31
|
+
searchParams: { [key: string]: string | string[] | undefined }
|
|
32
|
+
}) {
|
|
32
33
|
return (
|
|
33
34
|
<ProjectPortfolio
|
|
34
35
|
clientSlug="your-client-slug"
|
|
35
36
|
apiBase="https://your-api.com"
|
|
36
37
|
basePath="/projects"
|
|
38
|
+
searchParams={searchParams}
|
|
37
39
|
/>
|
|
38
40
|
)
|
|
39
41
|
}
|
|
40
42
|
```
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
```tsx
|
|
45
|
+
// app/projects/[slug]/page.tsx
|
|
46
|
+
import { ProjectDetail, SimilarProjects } from "project-portfolio"
|
|
47
|
+
|
|
48
|
+
export default async function ProjectPage({ params }: { params: { slug: string } }) {
|
|
49
|
+
return (
|
|
50
|
+
<>
|
|
51
|
+
<ProjectDetail
|
|
52
|
+
slug={params.slug}
|
|
53
|
+
clientSlug="your-client-slug"
|
|
54
|
+
apiBase="https://your-api.com"
|
|
55
|
+
backPath="/projects"
|
|
56
|
+
/>
|
|
57
|
+
|
|
58
|
+
{/* Hardcode the slugs you want shown — update per client request */}
|
|
59
|
+
<SimilarProjects
|
|
60
|
+
projectSlugs={[
|
|
61
|
+
"jacob-javits-convention-center",
|
|
62
|
+
"tillamook-bay-community-college",
|
|
63
|
+
"lcisd-liberty-hill-high-school",
|
|
64
|
+
]}
|
|
65
|
+
excludeSlug={params.slug}
|
|
66
|
+
clientSlug="your-client-slug"
|
|
67
|
+
apiBase="https://your-api.com"
|
|
68
|
+
basePath="/projects"
|
|
69
|
+
/>
|
|
70
|
+
</>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// app/api/chisel-menu/route.ts
|
|
77
|
+
import { createMenuHandler } from "project-portfolio"
|
|
78
|
+
|
|
79
|
+
export const GET = createMenuHandler({
|
|
80
|
+
clientSlug: "your-client-slug",
|
|
81
|
+
apiBase: "https://your-api.com",
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
// components/Nav.tsx — "use client" component in your header
|
|
87
|
+
"use client"
|
|
88
|
+
import { ProjectMenuClient } from "project-portfolio"
|
|
89
|
+
|
|
90
|
+
export function Nav() {
|
|
91
|
+
return (
|
|
92
|
+
<nav>
|
|
93
|
+
{/* ... other nav items ... */}
|
|
94
|
+
<ProjectMenuClient
|
|
95
|
+
dataUrl="/api/chisel-menu"
|
|
96
|
+
basePath="/projects"
|
|
97
|
+
viewAllPath="/projects"
|
|
98
|
+
/>
|
|
99
|
+
</nav>
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
49
105
|
|
|
50
|
-
|
|
106
|
+
## Components
|
|
107
|
+
|
|
108
|
+
### `ProjectPortfolio`
|
|
51
109
|
|
|
52
|
-
|
|
110
|
+
A full server-rendered project grid page. Fetches all projects for a client and renders them as responsive baseball cards (1 column on mobile, 2 on tablet, 3 on desktop). Supports URL-driven filtering via `searchParams`.
|
|
53
111
|
|
|
54
112
|
```tsx
|
|
55
113
|
// app/projects/page.tsx
|
|
@@ -71,12 +129,23 @@ export default async function ProjectsPage({
|
|
|
71
129
|
}
|
|
72
130
|
```
|
|
73
131
|
|
|
132
|
+
| Prop | Type | Required | Default | Description |
|
|
133
|
+
|---|---|---|---|---|
|
|
134
|
+
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
135
|
+
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
136
|
+
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
137
|
+
| `searchParams` | `Record<string, string \| string[] \| undefined>` | No | `{}` | Filter params forwarded to the API — pass Next.js `searchParams` directly |
|
|
138
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
139
|
+
|
|
140
|
+
#### URL-driven filtering
|
|
141
|
+
|
|
142
|
+
When a user clicks a "Browse By" filter link in the menu, they land on the grid with query params in the URL. Pass `searchParams` from the page and `ProjectPortfolio` forwards them to the API automatically.
|
|
143
|
+
|
|
74
144
|
Filter URLs follow this pattern — the key matches the custom field key in the schema:
|
|
75
145
|
|
|
76
146
|
```
|
|
77
|
-
/projects?type=commercial
|
|
78
|
-
/projects?type=educational-facilities
|
|
79
|
-
/projects?system=spacematic
|
|
147
|
+
/projects?filter[type]=commercial
|
|
148
|
+
/projects?filter[type]=educational-facilities
|
|
80
149
|
```
|
|
81
150
|
|
|
82
151
|
When active filters are applied, a filter banner is shown above the grid with a "Clear filters" link back to `basePath`.
|
|
@@ -85,7 +154,7 @@ When active filters are applied, a filter banner is shown above the grid with a
|
|
|
85
154
|
|
|
86
155
|
### `ProjectPortfolioClient`
|
|
87
156
|
|
|
88
|
-
A `"use client"`
|
|
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.
|
|
89
158
|
|
|
90
159
|
```tsx
|
|
91
160
|
// Works in any component — no RSC required
|
|
@@ -102,7 +171,9 @@ export default function ProjectsPage() {
|
|
|
102
171
|
}
|
|
103
172
|
```
|
|
104
173
|
|
|
105
|
-
|
|
174
|
+
#### With a custom filter UI
|
|
175
|
+
|
|
176
|
+
Wire up your own dropdowns, buttons, or search inputs using the `filters` prop. Filter changes are instant — no API call on each change, all filtering happens in memory.
|
|
106
177
|
|
|
107
178
|
```tsx
|
|
108
179
|
"use client"
|
|
@@ -121,7 +192,6 @@ export default function ProjectsPage() {
|
|
|
121
192
|
<option value="educational-facilities">Educational</option>
|
|
122
193
|
</select>
|
|
123
194
|
|
|
124
|
-
{/* Component receives filters and applies them in memory */}
|
|
125
195
|
<ProjectPortfolioClient
|
|
126
196
|
clientSlug="your-client-slug"
|
|
127
197
|
apiBase="https://your-api.com"
|
|
@@ -142,19 +212,16 @@ export default function ProjectsPage() {
|
|
|
142
212
|
| `columns` | `2 \| 3` | No | `3` | Number of columns in the project grid |
|
|
143
213
|
| `font` | `string` | No | System font stack | Font family string applied to all text |
|
|
144
214
|
|
|
145
|
-
**How filtering works:** Pass any `Record<string, string>` of field key/value pairs and the component filters the already-fetched project list in memory. All filter logic uses AND matching with case-insensitive comparison. The field keys must match the custom field keys in the API schema.
|
|
146
|
-
|
|
147
215
|
---
|
|
148
216
|
|
|
149
217
|
### `ProjectDetail`
|
|
150
218
|
|
|
151
|
-
A full project detail page. Fetches a single project by slug and renders its gallery, custom fields,
|
|
219
|
+
A full server-rendered project detail page. Fetches a single project by slug and renders its image gallery, custom fields, and a back link.
|
|
152
220
|
|
|
153
221
|
```tsx
|
|
154
222
|
// app/projects/[slug]/page.tsx
|
|
155
223
|
import { ProjectDetail } from "project-portfolio"
|
|
156
224
|
|
|
157
|
-
// Must be a Server Component — do NOT add "use client"
|
|
158
225
|
export default async function ProjectPage({ params }: { params: { slug: string } }) {
|
|
159
226
|
return (
|
|
160
227
|
<ProjectDetail
|
|
@@ -175,51 +242,52 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
175
242
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
176
243
|
| `backPath` | `string` | No | `"/projects"` | Path for the back navigation link |
|
|
177
244
|
| `backLabel` | `string` | No | `"All Projects"` | Label for the back navigation link |
|
|
178
|
-
| `revalidate` | `number` | No | `
|
|
245
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
179
246
|
|
|
180
247
|
---
|
|
181
248
|
|
|
182
|
-
### `
|
|
249
|
+
### `GalleryCarousel`
|
|
183
250
|
|
|
184
|
-
A
|
|
251
|
+
A `"use client"` image carousel with previous/next arrows, a counter badge, and a scrollable thumbnail strip. Used internally by `ProjectDetail` but can also be used standalone if you fetch your own project data.
|
|
185
252
|
|
|
186
253
|
```tsx
|
|
187
|
-
|
|
188
|
-
import {
|
|
254
|
+
"use client"
|
|
255
|
+
import { GalleryCarousel } from "project-portfolio"
|
|
189
256
|
|
|
190
|
-
//
|
|
191
|
-
export
|
|
257
|
+
// `media` is the array of image objects returned by the projects API
|
|
258
|
+
export function ProjectGallery({ media, title }: { media: Media[]; title: string }) {
|
|
192
259
|
return (
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
basePath="/projects"
|
|
197
|
-
subtitle="Our systems are installed in every geographic region of the U.S. and in a variety of applications."
|
|
198
|
-
maxProjects={6}
|
|
260
|
+
<GalleryCarousel
|
|
261
|
+
images={media}
|
|
262
|
+
projectTitle={title}
|
|
199
263
|
/>
|
|
200
264
|
)
|
|
201
265
|
}
|
|
202
266
|
```
|
|
203
267
|
|
|
268
|
+
The parent component is responsible for injecting the `.chisel-gallery-main-img` CSS class to control the main image height:
|
|
269
|
+
|
|
270
|
+
```css
|
|
271
|
+
.chisel-gallery-main-img {
|
|
272
|
+
height: 400px; /* adjust to your layout */
|
|
273
|
+
}
|
|
274
|
+
@media (min-width: 768px) {
|
|
275
|
+
.chisel-gallery-main-img {
|
|
276
|
+
height: 560px;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
204
281
|
| Prop | Type | Required | Default | Description |
|
|
205
282
|
|---|---|---|---|---|
|
|
206
|
-
| `
|
|
207
|
-
| `
|
|
208
|
-
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
209
|
-
| `viewAllPath` | `string` | No | Same as `basePath` | Path for the "View All Projects" link |
|
|
210
|
-
| `subtitle` | `string` | No | — | Description paragraph shown above the project cards |
|
|
211
|
-
| `font` | `string` | No | System font stack | Font family string applied to all inline styles |
|
|
212
|
-
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
|
|
213
|
-
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
214
|
-
| `noCache` | `boolean` | No | `false` | Disable caching entirely — useful for development |
|
|
215
|
-
|
|
216
|
-
The filter options in the right sidebar are driven automatically by the `is_filterable` fields returned from the API schema — no manual configuration needed.
|
|
283
|
+
| `images` | `Media[]` | Yes | — | Array of media objects from the projects API |
|
|
284
|
+
| `projectTitle` | `string` | Yes | — | Used as the alt text fallback for the main image |
|
|
217
285
|
|
|
218
286
|
---
|
|
219
287
|
|
|
220
288
|
### `SimilarProjects`
|
|
221
289
|
|
|
222
|
-
A
|
|
290
|
+
A server-rendered similar projects section. Fetches all projects for a client, filters to those matching the provided field values, excludes the current project, and renders up to 3 matching results. Designed to be placed after `ProjectDetail` on a project detail page.
|
|
223
291
|
|
|
224
292
|
```tsx
|
|
225
293
|
// app/projects/[slug]/page.tsx
|
|
@@ -245,32 +313,176 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
245
313
|
}
|
|
246
314
|
```
|
|
247
315
|
|
|
248
|
-
|
|
316
|
+
#### Deriving filters from the current project
|
|
317
|
+
|
|
318
|
+
In most real-world cases you want to match similar projects based on the current project's own field values. Fetch the project first, then pass its field values as filters:
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// app/projects/[slug]/page.tsx
|
|
322
|
+
import { ProjectDetail, SimilarProjects } from "project-portfolio"
|
|
323
|
+
|
|
324
|
+
async function getProject(slug: string, clientSlug: string, apiBase: string) {
|
|
325
|
+
const res = await fetch(
|
|
326
|
+
`${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=YOUR_API_KEY`,
|
|
327
|
+
{ next: { revalidate: 86400 } }
|
|
328
|
+
)
|
|
329
|
+
return res.ok ? (await res.json())?.data : null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export default async function ProjectPage({ params }: { params: { slug: string } }) {
|
|
333
|
+
const project = await getProject(params.slug, "your-client-slug", "https://your-api.com")
|
|
334
|
+
const projectType = project?.custom_field_values?.type ?? null
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<>
|
|
338
|
+
<ProjectDetail slug={params.slug} clientSlug="your-client-slug" apiBase="https://your-api.com" />
|
|
339
|
+
{projectType && (
|
|
340
|
+
<SimilarProjects
|
|
341
|
+
filters={{ type: projectType }}
|
|
342
|
+
excludeSlug={params.slug}
|
|
343
|
+
clientSlug="your-client-slug"
|
|
344
|
+
apiBase="https://your-api.com"
|
|
345
|
+
basePath="/projects"
|
|
346
|
+
/>
|
|
347
|
+
)}
|
|
348
|
+
</>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### Manually specifying projects (recommended for quick setup)
|
|
354
|
+
|
|
355
|
+
Pass `projectSlugs` to hand-pick exactly which projects appear, in the order you specify. This overrides `filters` entirely — useful when you want to curate the section rather than rely on field matching, or when you need a reliable result quickly without worrying about field values matching.
|
|
356
|
+
|
|
357
|
+
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.
|
|
358
|
+
|
|
359
|
+
```tsx
|
|
360
|
+
// app/projects/[slug]/page.tsx
|
|
361
|
+
import { ProjectDetail, SimilarProjects } from "project-portfolio"
|
|
362
|
+
|
|
363
|
+
export default async function ProjectPage({ params }: { params: { slug: string } }) {
|
|
364
|
+
return (
|
|
365
|
+
<>
|
|
366
|
+
{/* All your other page content above */}
|
|
367
|
+
<ProjectDetail
|
|
368
|
+
slug={params.slug}
|
|
369
|
+
clientSlug="your-client-slug"
|
|
370
|
+
apiBase="https://your-api.com"
|
|
371
|
+
backPath="/projects"
|
|
372
|
+
/>
|
|
373
|
+
|
|
374
|
+
{/* Similar projects hardcoded — update slugs per client request */}
|
|
375
|
+
<SimilarProjects
|
|
376
|
+
projectSlugs={[
|
|
377
|
+
"jacob-javits-convention-center",
|
|
378
|
+
"tillamook-bay-community-college",
|
|
379
|
+
"lcisd-liberty-hill-high-school",
|
|
380
|
+
]}
|
|
381
|
+
excludeSlug={params.slug}
|
|
382
|
+
clientSlug="your-client-slug"
|
|
383
|
+
apiBase="https://your-api.com"
|
|
384
|
+
basePath="/projects"
|
|
385
|
+
/>
|
|
386
|
+
</>
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
> `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.
|
|
392
|
+
|
|
393
|
+
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`.
|
|
394
|
+
|
|
395
|
+
#### Card variant
|
|
396
|
+
|
|
397
|
+
Use `variant="card"` to render the same baseball-card style used in `ProjectPortfolio` instead of the default list style:
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
<SimilarProjects
|
|
401
|
+
filters={{ type: "commercial" }}
|
|
402
|
+
excludeSlug={params.slug}
|
|
403
|
+
clientSlug="your-client-slug"
|
|
404
|
+
apiBase="https://your-api.com"
|
|
405
|
+
basePath="/projects"
|
|
406
|
+
variant="card"
|
|
407
|
+
/>
|
|
408
|
+
```
|
|
249
409
|
|
|
250
410
|
| Prop | Type | Required | Default | Description |
|
|
251
411
|
|---|---|---|---|---|
|
|
412
|
+
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
413
|
+
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
252
414
|
| `filters` | `Record<string, string>` | No | `{}` | Key/value pairs to filter projects by custom field values. All filters must match (AND logic) |
|
|
253
415
|
| `excludeSlug` | `string` | No | — | Slug of a project to exclude from results (e.g. the current project) |
|
|
416
|
+
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
417
|
+
| `projectSlugs` | `string[]` | No | — | Explicit list of project slugs to show, in order. When provided, overrides `filters` entirely. e.g. `["jacob-javits", "tillamook-bay"]` |
|
|
418
|
+
| `maxItems` | `number` | No | `3` | Maximum number of projects to show |
|
|
419
|
+
| `variant` | `"list" \| "card"` | No | `"list"` | `"list"` uses the border-bottom separator style; `"card"` renders full baseball-card style matching `ProjectPortfolio` |
|
|
420
|
+
| `font` | `string` | No | System font stack | Font family string applied to all inline styles |
|
|
421
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### `ProjectMenu`
|
|
426
|
+
|
|
427
|
+
A server-rendered megamenu component. Shows featured projects as compact cards on the left and "Browse By" filter links on the right. Designed to be dropped directly into a navigation dropdown.
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
// components/MegaMenu.tsx
|
|
431
|
+
import { ProjectMenu } from "project-portfolio"
|
|
432
|
+
|
|
433
|
+
// Must be a Server Component — do NOT add "use client"
|
|
434
|
+
export async function ProjectsMegaMenu() {
|
|
435
|
+
return (
|
|
436
|
+
<ProjectMenu
|
|
437
|
+
clientSlug="your-client-slug"
|
|
438
|
+
apiBase="https://your-api.com"
|
|
439
|
+
basePath="/projects"
|
|
440
|
+
subtitle="Our systems are installed in every geographic region of the U.S."
|
|
441
|
+
maxProjects={6}
|
|
442
|
+
/>
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
#### With a curated menu
|
|
448
|
+
|
|
449
|
+
Pass `menuId` to show a specific curated set of projects instead of all projects. The Browse By filters on the right always reflect the full schema regardless of which menu is active. Available menu IDs can be retrieved from `GET /api/v1/clients/{clientSlug}/menus`.
|
|
450
|
+
|
|
451
|
+
```tsx
|
|
452
|
+
<ProjectMenu
|
|
453
|
+
clientSlug="your-client-slug"
|
|
454
|
+
apiBase="https://your-api.com"
|
|
455
|
+
menuId="main-nav"
|
|
456
|
+
basePath="/projects"
|
|
457
|
+
/>
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
| Prop | Type | Required | Default | Description |
|
|
461
|
+
|---|---|---|---|---|
|
|
254
462
|
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
255
463
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
464
|
+
| `menuId` | `string` | No | — | ID of a curated menu. When provided, fetches from `/menus/{menuId}` instead of all projects. Filters always shown regardless. |
|
|
256
465
|
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
257
|
-
| `
|
|
466
|
+
| `viewAllPath` | `string` | No | Same as `basePath` | Path for the "View All Projects" link |
|
|
467
|
+
| `subtitle` | `string` | No | — | Description paragraph shown above the project cards |
|
|
258
468
|
| `font` | `string` | No | System font stack | Font family string applied to all inline styles |
|
|
259
|
-
| `
|
|
469
|
+
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
|
|
470
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
471
|
+
| `noCache` | `boolean` | No | `false` | Disable caching entirely — useful for development |
|
|
260
472
|
|
|
261
473
|
---
|
|
262
474
|
|
|
263
475
|
### `ProjectMenuClient` + `createMenuHandler`
|
|
264
476
|
|
|
265
|
-
`ProjectMenuClient` is a `"use client"` component
|
|
477
|
+
`ProjectMenuClient` is a `"use client"` megamenu component. Use it when your nav or header is a client component. It fetches and caches data on first mount so the API is never called twice on re-hover or remount.
|
|
266
478
|
|
|
267
|
-
There are two ways to
|
|
479
|
+
There are two ways to set it up:
|
|
268
480
|
|
|
269
481
|
---
|
|
270
482
|
|
|
271
|
-
|
|
483
|
+
#### Option 1 — `dataUrl` + `createMenuHandler` (recommended for production)
|
|
272
484
|
|
|
273
|
-
|
|
485
|
+
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.
|
|
274
486
|
|
|
275
487
|
```ts
|
|
276
488
|
// app/api/chisel-menu/route.ts
|
|
@@ -282,29 +494,66 @@ export const GET = createMenuHandler({
|
|
|
282
494
|
})
|
|
283
495
|
```
|
|
284
496
|
|
|
497
|
+
For a curated menu, pass `menuId` to the handler:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
// app/api/chisel-menu/route.ts
|
|
501
|
+
export const GET = createMenuHandler({
|
|
502
|
+
clientSlug: "your-client-slug",
|
|
503
|
+
apiBase: "https://your-api.com",
|
|
504
|
+
menuId: "main-nav",
|
|
505
|
+
})
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Then pass `dataUrl` to the component:
|
|
509
|
+
|
|
285
510
|
```tsx
|
|
286
|
-
//
|
|
511
|
+
// components/Nav.tsx
|
|
512
|
+
"use client"
|
|
287
513
|
import { ProjectMenuClient } from "project-portfolio"
|
|
288
514
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
515
|
+
export function Nav() {
|
|
516
|
+
return (
|
|
517
|
+
<ProjectMenuClient
|
|
518
|
+
dataUrl="/api/chisel-menu"
|
|
519
|
+
basePath="/projects"
|
|
520
|
+
viewAllPath="/projects"
|
|
521
|
+
subtitle="Explore our portfolio of projects."
|
|
522
|
+
maxProjects={6}
|
|
523
|
+
/>
|
|
524
|
+
)
|
|
525
|
+
}
|
|
294
526
|
```
|
|
295
527
|
|
|
296
528
|
---
|
|
297
529
|
|
|
298
|
-
|
|
530
|
+
#### Option 2 — Direct fetch (quick setup / non-Next.js environments)
|
|
299
531
|
|
|
300
|
-
No API route needed. The component fetches directly from the upstream API on first mount and caches the result in memory for the session.
|
|
532
|
+
No API route needed. The component fetches directly from the upstream API on first mount and caches the result in memory for the session. Note: this exposes the API call to the client browser.
|
|
301
533
|
|
|
302
534
|
```tsx
|
|
535
|
+
"use client"
|
|
303
536
|
import { ProjectMenuClient } from "project-portfolio"
|
|
304
537
|
|
|
538
|
+
export function Nav() {
|
|
539
|
+
return (
|
|
540
|
+
<ProjectMenuClient
|
|
541
|
+
clientSlug="your-client-slug"
|
|
542
|
+
apiBase="https://your-api.com"
|
|
543
|
+
basePath="/projects"
|
|
544
|
+
viewAllPath="/projects"
|
|
545
|
+
/>
|
|
546
|
+
)
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
With a curated menu:
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
305
553
|
<ProjectMenuClient
|
|
306
554
|
clientSlug="your-client-slug"
|
|
307
555
|
apiBase="https://your-api.com"
|
|
556
|
+
menuId="main-nav"
|
|
308
557
|
basePath="/projects"
|
|
309
558
|
viewAllPath="/projects"
|
|
310
559
|
/>
|
|
@@ -314,22 +563,23 @@ import { ProjectMenuClient } from "project-portfolio"
|
|
|
314
563
|
|
|
315
564
|
| Prop | Type | Required | Default | Description |
|
|
316
565
|
|---|---|---|---|---|
|
|
317
|
-
| `dataUrl` | `string` | No | — | URL of a local API route created with `createMenuHandler()`. Recommended for production |
|
|
566
|
+
| `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()`. Recommended for production |
|
|
318
567
|
| `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
|
|
319
568
|
| `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
|
|
569
|
+
| `menuId` | `string` | No | — | ID of a curated menu. Fetches from `/menus/{menuId}` for projects. Filters are always shown regardless. |
|
|
320
570
|
| `basePath` | `string` | Yes | — | Base path for project detail links |
|
|
321
571
|
| `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
|
|
322
|
-
| `subtitle` | `string` | No | — | Description shown above the project cards |
|
|
572
|
+
| `subtitle` | `string` | No | — | Description shown above the project cards (hidden on mobile) |
|
|
323
573
|
| `font` | `string` | No | System font stack | Font family string |
|
|
324
|
-
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
|
|
574
|
+
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display (capped at 3 on mobile) |
|
|
325
575
|
|
|
326
|
-
*One of `dataUrl` or `clientSlug + apiBase` must be provided.
|
|
576
|
+
*One of `dataUrl` or `clientSlug + apiBase` must be provided.
|
|
327
577
|
|
|
328
578
|
---
|
|
329
579
|
|
|
330
|
-
##
|
|
580
|
+
## Server vs Client Components
|
|
331
581
|
|
|
332
|
-
All top-level components
|
|
582
|
+
All top-level components except `ProjectMenuClient`, `ProjectPortfolioClient`, and `GalleryCarousel` are **async Server Components**. They must be rendered in a server context:
|
|
333
583
|
|
|
334
584
|
```tsx
|
|
335
585
|
// CORRECT
|
|
@@ -337,58 +587,63 @@ export default async function Page() {
|
|
|
337
587
|
return <ProjectPortfolio clientSlug="..." apiBase="..." />
|
|
338
588
|
}
|
|
339
589
|
|
|
340
|
-
// WRONG — causes
|
|
590
|
+
// WRONG — causes a runtime error
|
|
341
591
|
"use client"
|
|
342
592
|
export default function Page() {
|
|
343
593
|
return <ProjectPortfolio clientSlug="..." apiBase="..." />
|
|
344
594
|
}
|
|
345
595
|
```
|
|
346
596
|
|
|
347
|
-
If
|
|
597
|
+
If your parent component uses `"use client"`, use the client variants instead (`ProjectMenuClient`, `ProjectPortfolioClient`) or pass the server components as `children` from a server parent.
|
|
598
|
+
|
|
599
|
+
| Component | Type | Use when |
|
|
600
|
+
|---|---|---|
|
|
601
|
+
| `ProjectPortfolio` | Server | Page-level grid, URL-driven filters |
|
|
602
|
+
| `ProjectPortfolioClient` | Client | Inside a client tree, custom filter UI |
|
|
603
|
+
| `ProjectDetail` | Server | Project detail page |
|
|
604
|
+
| `GalleryCarousel` | Client | Standalone image gallery |
|
|
605
|
+
| `ProjectMenu` | Server | Server-rendered nav dropdown |
|
|
606
|
+
| `ProjectMenuClient` | Client | Client-rendered nav/header |
|
|
607
|
+
| `SimilarProjects` | Server | After `ProjectDetail` on detail pages |
|
|
348
608
|
|
|
349
609
|
---
|
|
350
610
|
|
|
351
611
|
## Caching
|
|
352
612
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
**`ProjectMenuClient` with direct fetch / `ProjectPortfolioClient`**
|
|
364
|
-
4. **Module-level client cache only** — the upstream API is called once per session per user (on first mount), then cached in memory for the rest of that page session. Filter changes on `ProjectPortfolioClient` never trigger a fetch — filtering is done entirely in memory
|
|
613
|
+
| Component | Server Cache | Client Cache |
|
|
614
|
+
|---|---|---|
|
|
615
|
+
| `ProjectMenu` (RSC) | 24h via `next.revalidate` | — |
|
|
616
|
+
| `ProjectMenuClient` + `createMenuHandler` | 24h (route handler) | Per-session module cache |
|
|
617
|
+
| `ProjectMenuClient` (direct fetch) | None | Per-session module cache |
|
|
618
|
+
| `ProjectPortfolio` (RSC) | 24h via `next.revalidate` | — |
|
|
619
|
+
| `ProjectPortfolioClient` | None | Per-session module cache |
|
|
620
|
+
| `ProjectDetail` (RSC) | 24h via `next.revalidate` | — |
|
|
621
|
+
| `SimilarProjects` (RSC) | 24h via `next.revalidate` | — |
|
|
365
622
|
|
|
366
|
-
|
|
623
|
+
#### Bypassing the cache
|
|
367
624
|
|
|
368
625
|
| Method | Scope | Use case |
|
|
369
626
|
|---|---|---|
|
|
370
627
|
| `?bust=1` on `/api/chisel-menu` | Single request | Dev/testing after a CMS change |
|
|
371
628
|
| `CHISEL_CACHE_BYPASS=true` env var | Entire deployment | Staging environments |
|
|
372
|
-
| `revalidateTag("chisel-menu-{clientSlug}")` | Server cache
|
|
629
|
+
| `revalidateTag("chisel-menu-{clientSlug}")` | Server cache | CMS webhook on content publish |
|
|
373
630
|
| `noCache: true` on `ProjectMenu` | Single render | Debug during development |
|
|
374
631
|
|
|
375
632
|
```ts
|
|
376
|
-
// CMS webhook
|
|
633
|
+
// CMS webhook — invalidate server cache on publish
|
|
377
634
|
import { revalidateTag } from "next/cache"
|
|
378
635
|
revalidateTag("chisel-menu-your-client-slug")
|
|
379
|
-
```
|
|
380
636
|
|
|
381
|
-
|
|
637
|
+
// For a curated menu, the tag includes the menuId
|
|
638
|
+
revalidateTag("chisel-menu-your-client-slug-main-nav")
|
|
639
|
+
```
|
|
382
640
|
|
|
383
641
|
---
|
|
384
642
|
|
|
385
643
|
## Publishing to npm
|
|
386
644
|
|
|
387
645
|
```bash
|
|
388
|
-
# Log in to npm
|
|
389
646
|
npm login
|
|
390
|
-
|
|
391
|
-
# Build and publish
|
|
392
647
|
cd package
|
|
393
648
|
npm run build
|
|
394
649
|
npm publish --access public
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Media } from "./types";
|
|
2
|
-
export
|
|
2
|
+
export interface GalleryCarouselProps {
|
|
3
3
|
images: Media[];
|
|
4
4
|
projectTitle: string;
|
|
5
|
-
}
|
|
5
|
+
}
|
|
6
|
+
export declare function GalleryCarousel({ images, projectTitle, }: GalleryCarouselProps): import("react/jsx-runtime").JSX.Element | null;
|
|
6
7
|
//# sourceMappingURL=GalleryCarousel.d.ts.map
|