project-portfolio 2.0.1 → 2.2.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 +376 -91
- package/dist/ProjectCard.d.ts.map +1 -1
- package/dist/ProjectCard.js +15 -5
- package/dist/ProjectDetail.d.ts.map +1 -1
- package/dist/ProjectDetail.js +84 -36
- package/dist/ProjectMenu.d.ts +11 -3
- package/dist/ProjectMenu.d.ts.map +1 -1
- package/dist/ProjectMenu.js +61 -30
- package/dist/ProjectMenuClient.d.ts +6 -1
- package/dist/ProjectMenuClient.d.ts.map +1 -1
- package/dist/ProjectMenuClient.js +72 -24
- package/dist/ProjectPortfolio.js +1 -1
- package/dist/ProjectPortfolioClient.js +1 -1
- package/dist/SimilarProjects.d.ts +13 -1
- package/dist/SimilarProjects.d.ts.map +1 -1
- package/dist/SimilarProjects.js +65 -22
- 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
|
+
```
|
|
49
74
|
|
|
50
|
-
|
|
75
|
+
```ts
|
|
76
|
+
// app/api/chisel-menu/route.ts
|
|
77
|
+
import { createMenuHandler } from "project-portfolio"
|
|
51
78
|
|
|
52
|
-
|
|
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
|
+
---
|
|
105
|
+
|
|
106
|
+
## Components
|
|
107
|
+
|
|
108
|
+
### `ProjectPortfolio`
|
|
109
|
+
|
|
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
|
|
219
|
+
A full server-rendered project detail page. Fetches a single project by slug and renders a hero image, a dynamic stats bar, a "Project Overview" section with description and specs sidebar, a photo gallery, 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
|
|
@@ -168,6 +235,36 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
168
235
|
}
|
|
169
236
|
```
|
|
170
237
|
|
|
238
|
+
#### Stats bar
|
|
239
|
+
|
|
240
|
+
The stats bar below the hero is driven by an explicit ordered key list. Fields are shown in this order when present in the schema and populated on the project:
|
|
241
|
+
|
|
242
|
+
| Schema key | Label shown |
|
|
243
|
+
|---|---|
|
|
244
|
+
| `location` | Location |
|
|
245
|
+
| `type` (badge field) | field `name` from schema |
|
|
246
|
+
| `coverage` | Coverage |
|
|
247
|
+
| `year-completed` | Completed |
|
|
248
|
+
| `architect` | Architect |
|
|
249
|
+
| `general-contractor` | General Contractor |
|
|
250
|
+
|
|
251
|
+
If a field doesn't exist in the schema for a given client, or the project has no value for it, that stat is silently omitted. The column count adjusts automatically — 2 columns on mobile, 3 on tablet, up to 6 on desktop.
|
|
252
|
+
|
|
253
|
+
#### Project Overview specs sidebar
|
|
254
|
+
|
|
255
|
+
The "Project Overview" section renders the project description on the left and a specs sidebar on the right (amber accent border). The sidebar shows the following fields when populated, in this order:
|
|
256
|
+
|
|
257
|
+
| Schema key | Label shown |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `systems-used` | Systems Used |
|
|
260
|
+
| `systems` | Track Systems |
|
|
261
|
+
| `series-used` | Series Used |
|
|
262
|
+
| `operation-type` | Operation Type |
|
|
263
|
+
| `finishes` | Finishes |
|
|
264
|
+
| `specifications` | Specifications |
|
|
265
|
+
|
|
266
|
+
Each group renders values as outlined pills. Fields with no value for the current project are omitted. Both the stats bar and the specs sidebar are fully schema-driven — if a key doesn't exist in a client's schema it is simply not shown, making `ProjectDetail` safe to reuse across clients with wildly different field configurations.
|
|
267
|
+
|
|
171
268
|
| Prop | Type | Required | Default | Description |
|
|
172
269
|
|---|---|---|---|---|
|
|
173
270
|
| `slug` | `string` | Yes | — | The project slug to load |
|
|
@@ -179,47 +276,48 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
179
276
|
|
|
180
277
|
---
|
|
181
278
|
|
|
182
|
-
### `
|
|
279
|
+
### `GalleryCarousel`
|
|
183
280
|
|
|
184
|
-
A
|
|
281
|
+
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
282
|
|
|
186
283
|
```tsx
|
|
187
|
-
|
|
188
|
-
import {
|
|
284
|
+
"use client"
|
|
285
|
+
import { GalleryCarousel } from "project-portfolio"
|
|
189
286
|
|
|
190
|
-
//
|
|
191
|
-
export
|
|
287
|
+
// `media` is the array of image objects returned by the projects API
|
|
288
|
+
export function ProjectGallery({ media, title }: { media: Media[]; title: string }) {
|
|
192
289
|
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}
|
|
290
|
+
<GalleryCarousel
|
|
291
|
+
images={media}
|
|
292
|
+
projectTitle={title}
|
|
199
293
|
/>
|
|
200
294
|
)
|
|
201
295
|
}
|
|
202
296
|
```
|
|
203
297
|
|
|
298
|
+
The parent component is responsible for injecting the `.chisel-gallery-main-img` CSS class to control the main image height:
|
|
299
|
+
|
|
300
|
+
```css
|
|
301
|
+
.chisel-gallery-main-img {
|
|
302
|
+
height: 400px; /* adjust to your layout */
|
|
303
|
+
}
|
|
304
|
+
@media (min-width: 768px) {
|
|
305
|
+
.chisel-gallery-main-img {
|
|
306
|
+
height: 560px;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
204
311
|
| Prop | Type | Required | Default | Description |
|
|
205
312
|
|---|---|---|---|---|
|
|
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.
|
|
313
|
+
| `images` | `Media[]` | Yes | — | Array of media objects from the projects API |
|
|
314
|
+
| `projectTitle` | `string` | Yes | — | Used as the alt text fallback for the main image |
|
|
217
315
|
|
|
218
316
|
---
|
|
219
317
|
|
|
220
318
|
### `SimilarProjects`
|
|
221
319
|
|
|
222
|
-
A
|
|
320
|
+
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
321
|
|
|
224
322
|
```tsx
|
|
225
323
|
// app/projects/[slug]/page.tsx
|
|
@@ -245,32 +343,176 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
245
343
|
}
|
|
246
344
|
```
|
|
247
345
|
|
|
248
|
-
|
|
346
|
+
#### Deriving filters from the current project
|
|
347
|
+
|
|
348
|
+
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:
|
|
349
|
+
|
|
350
|
+
```tsx
|
|
351
|
+
// app/projects/[slug]/page.tsx
|
|
352
|
+
import { ProjectDetail, SimilarProjects } from "project-portfolio"
|
|
353
|
+
|
|
354
|
+
async function getProject(slug: string, clientSlug: string, apiBase: string) {
|
|
355
|
+
const res = await fetch(
|
|
356
|
+
`${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=YOUR_API_KEY`,
|
|
357
|
+
{ next: { revalidate: 86400 } }
|
|
358
|
+
)
|
|
359
|
+
return res.ok ? (await res.json())?.data : null
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export default async function ProjectPage({ params }: { params: { slug: string } }) {
|
|
363
|
+
const project = await getProject(params.slug, "your-client-slug", "https://your-api.com")
|
|
364
|
+
const projectType = project?.custom_field_values?.type ?? null
|
|
365
|
+
|
|
366
|
+
return (
|
|
367
|
+
<>
|
|
368
|
+
<ProjectDetail slug={params.slug} clientSlug="your-client-slug" apiBase="https://your-api.com" />
|
|
369
|
+
{projectType && (
|
|
370
|
+
<SimilarProjects
|
|
371
|
+
filters={{ type: projectType }}
|
|
372
|
+
excludeSlug={params.slug}
|
|
373
|
+
clientSlug="your-client-slug"
|
|
374
|
+
apiBase="https://your-api.com"
|
|
375
|
+
basePath="/projects"
|
|
376
|
+
/>
|
|
377
|
+
)}
|
|
378
|
+
</>
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### Manually specifying projects (recommended for quick setup)
|
|
384
|
+
|
|
385
|
+
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.
|
|
386
|
+
|
|
387
|
+
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.
|
|
388
|
+
|
|
389
|
+
```tsx
|
|
390
|
+
// app/projects/[slug]/page.tsx
|
|
391
|
+
import { ProjectDetail, SimilarProjects } from "project-portfolio"
|
|
392
|
+
|
|
393
|
+
export default async function ProjectPage({ params }: { params: { slug: string } }) {
|
|
394
|
+
return (
|
|
395
|
+
<>
|
|
396
|
+
{/* All your other page content above */}
|
|
397
|
+
<ProjectDetail
|
|
398
|
+
slug={params.slug}
|
|
399
|
+
clientSlug="your-client-slug"
|
|
400
|
+
apiBase="https://your-api.com"
|
|
401
|
+
backPath="/projects"
|
|
402
|
+
/>
|
|
403
|
+
|
|
404
|
+
{/* Similar projects hardcoded — update slugs per client request */}
|
|
405
|
+
<SimilarProjects
|
|
406
|
+
projectSlugs={[
|
|
407
|
+
"jacob-javits-convention-center",
|
|
408
|
+
"tillamook-bay-community-college",
|
|
409
|
+
"lcisd-liberty-hill-high-school",
|
|
410
|
+
]}
|
|
411
|
+
excludeSlug={params.slug}
|
|
412
|
+
clientSlug="your-client-slug"
|
|
413
|
+
apiBase="https://your-api.com"
|
|
414
|
+
basePath="/projects"
|
|
415
|
+
/>
|
|
416
|
+
</>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
> `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.
|
|
422
|
+
|
|
423
|
+
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`.
|
|
424
|
+
|
|
425
|
+
#### Card variant
|
|
426
|
+
|
|
427
|
+
Use `variant="card"` to render the same baseball-card style used in `ProjectPortfolio` instead of the default list style:
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
<SimilarProjects
|
|
431
|
+
filters={{ type: "commercial" }}
|
|
432
|
+
excludeSlug={params.slug}
|
|
433
|
+
clientSlug="your-client-slug"
|
|
434
|
+
apiBase="https://your-api.com"
|
|
435
|
+
basePath="/projects"
|
|
436
|
+
variant="card"
|
|
437
|
+
/>
|
|
438
|
+
```
|
|
249
439
|
|
|
250
440
|
| Prop | Type | Required | Default | Description |
|
|
251
441
|
|---|---|---|---|---|
|
|
442
|
+
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
443
|
+
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
252
444
|
| `filters` | `Record<string, string>` | No | `{}` | Key/value pairs to filter projects by custom field values. All filters must match (AND logic) |
|
|
253
445
|
| `excludeSlug` | `string` | No | — | Slug of a project to exclude from results (e.g. the current project) |
|
|
446
|
+
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
447
|
+
| `projectSlugs` | `string[]` | No | — | Explicit list of project slugs to show, in order. When provided, overrides `filters` entirely. e.g. `["jacob-javits", "tillamook-bay"]` |
|
|
448
|
+
| `maxItems` | `number` | No | `3` | Maximum number of projects to show |
|
|
449
|
+
| `variant` | `"list" \| "card"` | No | `"list"` | `"list"` uses the border-bottom separator style; `"card"` renders full baseball-card style matching `ProjectPortfolio` |
|
|
450
|
+
| `font` | `string` | No | System font stack | Font family string applied to all inline styles |
|
|
451
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
### `ProjectMenu`
|
|
456
|
+
|
|
457
|
+
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.
|
|
458
|
+
|
|
459
|
+
```tsx
|
|
460
|
+
// components/MegaMenu.tsx
|
|
461
|
+
import { ProjectMenu } from "project-portfolio"
|
|
462
|
+
|
|
463
|
+
// Must be a Server Component — do NOT add "use client"
|
|
464
|
+
export async function ProjectsMegaMenu() {
|
|
465
|
+
return (
|
|
466
|
+
<ProjectMenu
|
|
467
|
+
clientSlug="your-client-slug"
|
|
468
|
+
apiBase="https://your-api.com"
|
|
469
|
+
basePath="/projects"
|
|
470
|
+
subtitle="Our systems are installed in every geographic region of the U.S."
|
|
471
|
+
maxProjects={6}
|
|
472
|
+
/>
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### With a curated menu
|
|
478
|
+
|
|
479
|
+
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`.
|
|
480
|
+
|
|
481
|
+
```tsx
|
|
482
|
+
<ProjectMenu
|
|
483
|
+
clientSlug="your-client-slug"
|
|
484
|
+
apiBase="https://your-api.com"
|
|
485
|
+
menuId="main-nav"
|
|
486
|
+
basePath="/projects"
|
|
487
|
+
/>
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
| Prop | Type | Required | Default | Description |
|
|
491
|
+
|---|---|---|---|---|
|
|
254
492
|
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
255
493
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
494
|
+
| `menuId` | `string` | No | — | ID of a curated menu. When provided, fetches from `/menus/{menuId}` instead of all projects. Filters always shown regardless. |
|
|
256
495
|
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
257
|
-
| `
|
|
496
|
+
| `viewAllPath` | `string` | No | Same as `basePath` | Path for the "View All Projects" link |
|
|
497
|
+
| `subtitle` | `string` | No | — | Description paragraph shown above the project cards |
|
|
258
498
|
| `font` | `string` | No | System font stack | Font family string applied to all inline styles |
|
|
259
|
-
| `
|
|
499
|
+
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
|
|
500
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
501
|
+
| `noCache` | `boolean` | No | `false` | Disable caching entirely — useful for development |
|
|
260
502
|
|
|
261
503
|
---
|
|
262
504
|
|
|
263
505
|
### `ProjectMenuClient` + `createMenuHandler`
|
|
264
506
|
|
|
265
|
-
`ProjectMenuClient` is a `"use client"` component
|
|
507
|
+
`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
508
|
|
|
267
|
-
There are two ways to
|
|
509
|
+
There are two ways to set it up:
|
|
268
510
|
|
|
269
511
|
---
|
|
270
512
|
|
|
271
|
-
|
|
513
|
+
#### Option 1 — `dataUrl` + `createMenuHandler` (recommended for production)
|
|
272
514
|
|
|
273
|
-
|
|
515
|
+
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
516
|
|
|
275
517
|
```ts
|
|
276
518
|
// app/api/chisel-menu/route.ts
|
|
@@ -282,29 +524,66 @@ export const GET = createMenuHandler({
|
|
|
282
524
|
})
|
|
283
525
|
```
|
|
284
526
|
|
|
527
|
+
For a curated menu, pass `menuId` to the handler:
|
|
528
|
+
|
|
529
|
+
```ts
|
|
530
|
+
// app/api/chisel-menu/route.ts
|
|
531
|
+
export const GET = createMenuHandler({
|
|
532
|
+
clientSlug: "your-client-slug",
|
|
533
|
+
apiBase: "https://your-api.com",
|
|
534
|
+
menuId: "main-nav",
|
|
535
|
+
})
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Then pass `dataUrl` to the component:
|
|
539
|
+
|
|
285
540
|
```tsx
|
|
286
|
-
//
|
|
541
|
+
// components/Nav.tsx
|
|
542
|
+
"use client"
|
|
287
543
|
import { ProjectMenuClient } from "project-portfolio"
|
|
288
544
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
545
|
+
export function Nav() {
|
|
546
|
+
return (
|
|
547
|
+
<ProjectMenuClient
|
|
548
|
+
dataUrl="/api/chisel-menu"
|
|
549
|
+
basePath="/projects"
|
|
550
|
+
viewAllPath="/projects"
|
|
551
|
+
subtitle="Explore our portfolio of projects."
|
|
552
|
+
maxProjects={6}
|
|
553
|
+
/>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
294
556
|
```
|
|
295
557
|
|
|
296
558
|
---
|
|
297
559
|
|
|
298
|
-
|
|
560
|
+
#### Option 2 — Direct fetch (quick setup / non-Next.js environments)
|
|
299
561
|
|
|
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.
|
|
562
|
+
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
563
|
|
|
302
564
|
```tsx
|
|
565
|
+
"use client"
|
|
303
566
|
import { ProjectMenuClient } from "project-portfolio"
|
|
304
567
|
|
|
568
|
+
export function Nav() {
|
|
569
|
+
return (
|
|
570
|
+
<ProjectMenuClient
|
|
571
|
+
clientSlug="your-client-slug"
|
|
572
|
+
apiBase="https://your-api.com"
|
|
573
|
+
basePath="/projects"
|
|
574
|
+
viewAllPath="/projects"
|
|
575
|
+
/>
|
|
576
|
+
)
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
With a curated menu:
|
|
581
|
+
|
|
582
|
+
```tsx
|
|
305
583
|
<ProjectMenuClient
|
|
306
584
|
clientSlug="your-client-slug"
|
|
307
585
|
apiBase="https://your-api.com"
|
|
586
|
+
menuId="main-nav"
|
|
308
587
|
basePath="/projects"
|
|
309
588
|
viewAllPath="/projects"
|
|
310
589
|
/>
|
|
@@ -314,22 +593,23 @@ import { ProjectMenuClient } from "project-portfolio"
|
|
|
314
593
|
|
|
315
594
|
| Prop | Type | Required | Default | Description |
|
|
316
595
|
|---|---|---|---|---|
|
|
317
|
-
| `dataUrl` | `string` | No | — | URL of a local API route created with `createMenuHandler()`. Recommended for production |
|
|
596
|
+
| `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()`. Recommended for production |
|
|
318
597
|
| `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
|
|
319
598
|
| `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
|
|
599
|
+
| `menuId` | `string` | No | — | ID of a curated menu. Fetches from `/menus/{menuId}` for projects. Filters are always shown regardless. |
|
|
320
600
|
| `basePath` | `string` | Yes | — | Base path for project detail links |
|
|
321
601
|
| `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
|
|
322
602
|
| `subtitle` | `string` | No | — | Description shown above the project cards (hidden on mobile) |
|
|
323
603
|
| `font` | `string` | No | System font stack | Font family string |
|
|
324
604
|
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display (capped at 3 on mobile) |
|
|
325
605
|
|
|
326
|
-
*One of `dataUrl` or `clientSlug + apiBase` must be provided.
|
|
606
|
+
*One of `dataUrl` or `clientSlug + apiBase` must be provided.
|
|
327
607
|
|
|
328
608
|
---
|
|
329
609
|
|
|
330
|
-
##
|
|
610
|
+
## Server vs Client Components
|
|
331
611
|
|
|
332
|
-
All top-level components
|
|
612
|
+
All top-level components except `ProjectMenuClient`, `ProjectPortfolioClient`, and `GalleryCarousel` are **async Server Components**. They must be rendered in a server context:
|
|
333
613
|
|
|
334
614
|
```tsx
|
|
335
615
|
// CORRECT
|
|
@@ -337,58 +617,63 @@ export default async function Page() {
|
|
|
337
617
|
return <ProjectPortfolio clientSlug="..." apiBase="..." />
|
|
338
618
|
}
|
|
339
619
|
|
|
340
|
-
// WRONG — causes
|
|
620
|
+
// WRONG — causes a runtime error
|
|
341
621
|
"use client"
|
|
342
622
|
export default function Page() {
|
|
343
623
|
return <ProjectPortfolio clientSlug="..." apiBase="..." />
|
|
344
624
|
}
|
|
345
625
|
```
|
|
346
626
|
|
|
347
|
-
If
|
|
627
|
+
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.
|
|
628
|
+
|
|
629
|
+
| Component | Type | Use when |
|
|
630
|
+
|---|---|---|
|
|
631
|
+
| `ProjectPortfolio` | Server | Page-level grid, URL-driven filters |
|
|
632
|
+
| `ProjectPortfolioClient` | Client | Inside a client tree, custom filter UI |
|
|
633
|
+
| `ProjectDetail` | Server | Project detail page |
|
|
634
|
+
| `GalleryCarousel` | Client | Standalone image gallery |
|
|
635
|
+
| `ProjectMenu` | Server | Server-rendered nav dropdown |
|
|
636
|
+
| `ProjectMenuClient` | Client | Client-rendered nav/header |
|
|
637
|
+
| `SimilarProjects` | Server | After `ProjectDetail` on detail pages |
|
|
348
638
|
|
|
349
639
|
---
|
|
350
640
|
|
|
351
641
|
## Caching
|
|
352
642
|
|
|
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
|
|
643
|
+
| Component | Server Cache | Client Cache |
|
|
644
|
+
|---|---|---|
|
|
645
|
+
| `ProjectMenu` (RSC) | 24h via `next.revalidate` | — |
|
|
646
|
+
| `ProjectMenuClient` + `createMenuHandler` | 24h (route handler) | Per-session module cache |
|
|
647
|
+
| `ProjectMenuClient` (direct fetch) | None | Per-session module cache |
|
|
648
|
+
| `ProjectPortfolio` (RSC) | 24h via `next.revalidate` | — |
|
|
649
|
+
| `ProjectPortfolioClient` | None | Per-session module cache |
|
|
650
|
+
| `ProjectDetail` (RSC) | 24h via `next.revalidate` | — |
|
|
651
|
+
| `SimilarProjects` (RSC) | 24h via `next.revalidate` | — |
|
|
365
652
|
|
|
366
|
-
|
|
653
|
+
#### Bypassing the cache
|
|
367
654
|
|
|
368
655
|
| Method | Scope | Use case |
|
|
369
656
|
|---|---|---|
|
|
370
657
|
| `?bust=1` on `/api/chisel-menu` | Single request | Dev/testing after a CMS change |
|
|
371
658
|
| `CHISEL_CACHE_BYPASS=true` env var | Entire deployment | Staging environments |
|
|
372
|
-
| `revalidateTag("chisel-menu-{clientSlug}")` | Server cache
|
|
659
|
+
| `revalidateTag("chisel-menu-{clientSlug}")` | Server cache | CMS webhook on content publish |
|
|
373
660
|
| `noCache: true` on `ProjectMenu` | Single render | Debug during development |
|
|
374
661
|
|
|
375
662
|
```ts
|
|
376
|
-
// CMS webhook
|
|
663
|
+
// CMS webhook — invalidate server cache on publish
|
|
377
664
|
import { revalidateTag } from "next/cache"
|
|
378
665
|
revalidateTag("chisel-menu-your-client-slug")
|
|
379
|
-
```
|
|
380
666
|
|
|
381
|
-
|
|
667
|
+
// For a curated menu, the tag includes the menuId
|
|
668
|
+
revalidateTag("chisel-menu-your-client-slug-main-nav")
|
|
669
|
+
```
|
|
382
670
|
|
|
383
671
|
---
|
|
384
672
|
|
|
385
673
|
## Publishing to npm
|
|
386
674
|
|
|
387
675
|
```bash
|
|
388
|
-
# Log in to npm
|
|
389
676
|
npm login
|
|
390
|
-
|
|
391
|
-
# Build and publish
|
|
392
677
|
cd package
|
|
393
678
|
npm run build
|
|
394
679
|
npm publish --access public
|