@chiselandco/nexus 2.2.6

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 ADDED
@@ -0,0 +1,676 @@
1
+ # project-portfolio
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 it needs with zero client-side waterfall requests.
4
+
5
+ ## Requirements
6
+
7
+ - Next.js 13+ (App Router)
8
+ - React 18+
9
+
10
+ No other dependencies required.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install project-portfolio
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Quick Start
21
+
22
+ Here is the most common full setup — a projects grid page, a detail page with similar projects, and a megamenu in the nav.
23
+
24
+ ```tsx
25
+ // app/projects/page.tsx
26
+ import { ProjectPortfolio } from "project-portfolio"
27
+
28
+ export default async function ProjectsPage({
29
+ searchParams,
30
+ }: {
31
+ searchParams: { [key: string]: string | string[] | undefined }
32
+ }) {
33
+ return (
34
+ <ProjectPortfolio
35
+ clientSlug="your-client-slug"
36
+ apiBase="https://your-api.com"
37
+ basePath="/projects"
38
+ searchParams={searchParams}
39
+ />
40
+ )
41
+ }
42
+ ```
43
+
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
+ ---
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`.
111
+
112
+ ```tsx
113
+ // app/projects/page.tsx
114
+ import { ProjectPortfolio } from "project-portfolio"
115
+
116
+ export default async function ProjectsPage({
117
+ searchParams,
118
+ }: {
119
+ searchParams: { [key: string]: string | string[] | undefined }
120
+ }) {
121
+ return (
122
+ <ProjectPortfolio
123
+ clientSlug="your-client-slug"
124
+ apiBase="https://your-api.com"
125
+ basePath="/projects"
126
+ searchParams={searchParams}
127
+ />
128
+ )
129
+ }
130
+ ```
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
+
144
+ Filter URLs follow this pattern — the key matches the custom field key in the schema:
145
+
146
+ ```
147
+ /projects?filter[type]=commercial
148
+ /projects?filter[type]=educational-facilities
149
+ ```
150
+
151
+ When active filters are applied, a filter banner is shown above the grid with a "Clear filters" link back to `basePath`.
152
+
153
+ ---
154
+
155
+ ### `ProjectPortfolioClient`
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 "project-portfolio"
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
+ ```
173
+
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.
177
+
178
+ ```tsx
179
+ "use client"
180
+ import { useState } from "react"
181
+ import { ProjectPortfolioClient } from "project-portfolio"
182
+
183
+ export default function ProjectsPage() {
184
+ const [filters, setFilters] = useState<Record<string, string>>({})
185
+
186
+ return (
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
+ }
204
+ ```
205
+
206
+ | Prop | Type | Required | Default | Description |
207
+ |---|---|---|---|---|
208
+ | `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
209
+ | `apiBase` | `string` | Yes | — | Base URL of the projects API |
210
+ | `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
211
+ | `filters` | `Record<string, string>` | No | `{}` | Active filters keyed by custom field key — filtering is instant, no API call on change |
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 |
214
+
215
+ ---
216
+
217
+ ### `ProjectDetail`
218
+
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.
220
+
221
+ ```tsx
222
+ // app/projects/[slug]/page.tsx
223
+ import { ProjectDetail } from "project-portfolio"
224
+
225
+ export default async function ProjectPage({ params }: { params: { slug: string } }) {
226
+ return (
227
+ <ProjectDetail
228
+ slug={params.slug}
229
+ clientSlug="your-client-slug"
230
+ apiBase="https://your-api.com"
231
+ backPath="/projects"
232
+ backLabel="All Projects"
233
+ />
234
+ )
235
+ }
236
+ ```
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
+
268
+ | Prop | Type | Required | Default | Description |
269
+ |---|---|---|---|---|
270
+ | `slug` | `string` | Yes | — | The project slug to load |
271
+ | `clientSlug` | `string` | Yes | — | The client slug that owns this project |
272
+ | `apiBase` | `string` | Yes | — | Base URL of the projects API |
273
+ | `backPath` | `string` | No | `"/projects"` | Path for the back navigation link |
274
+ | `backLabel` | `string` | No | `"All Projects"` | Label for the back navigation link |
275
+ | `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
276
+ | `noCache` | `boolean` | No | `false` | Bypasses the Next.js Data Cache and sets `cache: "no-store"`. Useful during development or for frequently updated projects. |
277
+
278
+ ---
279
+
280
+ ### `GalleryCarousel`
281
+
282
+ 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.
283
+
284
+ ```tsx
285
+ "use client"
286
+ import { GalleryCarousel } from "project-portfolio"
287
+
288
+ // `media` is the array of image objects returned by the projects API
289
+ export function ProjectGallery({ media, title }: { media: Media[]; title: string }) {
290
+ return (
291
+ <GalleryCarousel
292
+ images={media}
293
+ projectTitle={title}
294
+ />
295
+ )
296
+ }
297
+ ```
298
+
299
+ The main image and thumbnails are standardised to a `16/9` aspect ratio — no external CSS required.
300
+
301
+ | Prop | Type | Required | Default | Description |
302
+ |---|---|---|---|---|
303
+ | `images` | `Media[]` | Yes | — | Array of media objects from the projects API |
304
+ | `projectTitle` | `string` | Yes | — | Used as the alt text fallback for the main image |
305
+
306
+ ---
307
+
308
+ ### `SimilarProjects`
309
+
310
+ 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.
311
+
312
+ ```tsx
313
+ // app/projects/[slug]/page.tsx
314
+ import { ProjectDetail, SimilarProjects } from "project-portfolio"
315
+
316
+ export default async function ProjectPage({ params }: { params: { slug: string } }) {
317
+ return (
318
+ <>
319
+ <ProjectDetail
320
+ slug={params.slug}
321
+ clientSlug="your-client-slug"
322
+ apiBase="https://your-api.com"
323
+ />
324
+ <SimilarProjects
325
+ filters={{ type: "commercial" }}
326
+ excludeSlug={params.slug}
327
+ clientSlug="your-client-slug"
328
+ apiBase="https://your-api.com"
329
+ basePath="/projects"
330
+ />
331
+ </>
332
+ )
333
+ }
334
+ ```
335
+
336
+ #### Deriving filters from the current project
337
+
338
+ 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:
339
+
340
+ ```tsx
341
+ // app/projects/[slug]/page.tsx
342
+ import { ProjectDetail, SimilarProjects } from "project-portfolio"
343
+
344
+ async function getProject(slug: string, clientSlug: string, apiBase: string) {
345
+ const res = await fetch(
346
+ `${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=YOUR_API_KEY`,
347
+ { next: { revalidate: 86400 } }
348
+ )
349
+ return res.ok ? (await res.json())?.data : null
350
+ }
351
+
352
+ export default async function ProjectPage({ params }: { params: { slug: string } }) {
353
+ const project = await getProject(params.slug, "your-client-slug", "https://your-api.com")
354
+ const projectType = project?.custom_field_values?.type ?? null
355
+
356
+ return (
357
+ <>
358
+ <ProjectDetail slug={params.slug} clientSlug="your-client-slug" apiBase="https://your-api.com" />
359
+ {projectType && (
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
+ }
371
+ ```
372
+
373
+ #### Manually specifying projects (recommended for quick setup)
374
+
375
+ 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.
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.
378
+
379
+ ```tsx
380
+ // app/projects/[slug]/page.tsx
381
+ import { ProjectDetail, SimilarProjects } from "project-portfolio"
382
+
383
+ export default async function ProjectPage({ params }: { params: { slug: string } }) {
384
+ return (
385
+ <>
386
+ {/* All your other page content above */}
387
+ <ProjectDetail
388
+ slug={params.slug}
389
+ clientSlug="your-client-slug"
390
+ apiBase="https://your-api.com"
391
+ backPath="/projects"
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
+ }
409
+ ```
410
+
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
+ #### Card variant
416
+
417
+ Use `variant="card"` to render the same baseball-card style used in `ProjectPortfolio` instead of the default list style:
418
+
419
+ ```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
+ />
428
+ ```
429
+
430
+ | Prop | Type | Required | Default | Description |
431
+ |---|---|---|---|---|
432
+ | `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
433
+ | `apiBase` | `string` | Yes | — | Base URL of the projects API |
434
+ | `filters` | `Record<string, string>` | No | `{}` | Key/value pairs to filter projects by custom field values. All filters must match (AND logic) |
435
+ | `excludeSlug` | `string` | No | — | Slug of a project to exclude from results (e.g. the current project) |
436
+ | `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
437
+ | `projectSlugs` | `string[]` | No | — | Explicit list of project slugs to show, in order. When provided, overrides `filters` entirely. e.g. `["jacob-javits", "tillamook-bay"]` |
438
+ | `maxItems` | `number` | No | `3` | Maximum number of projects to show |
439
+ | `title` | `string` | No | `"Similar Projects"` | Heading text for the section. e.g. `"Featured Projects"` |
440
+ | `subtitle` | `string` | No | `"More Work"` | Small uppercase label above the heading. e.g. `"Our Work"` |
441
+ | `variant` | `"list" \| "card"` | No | `"list"` | `"list"` uses the border-bottom separator style; `"card"` renders full baseball-card style matching `ProjectPortfolio` |
442
+ | `font` | `string` | No | System font stack | Font family string applied to all inline styles |
443
+ | `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
444
+ | `noCache` | `boolean` | No | `false` | Bypasses the Next.js Data Cache and sets `cache: "no-store"`. Useful during development or for frequently updated projects. |
445
+
446
+ ---
447
+
448
+ ### `ProjectMenu`
449
+
450
+ 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.
451
+
452
+ ```tsx
453
+ // components/MegaMenu.tsx
454
+ import { ProjectMenu } from "project-portfolio"
455
+
456
+ // Must be a Server Component — do NOT add "use client"
457
+ export async function ProjectsMegaMenu() {
458
+ return (
459
+ <ProjectMenu
460
+ clientSlug="your-client-slug"
461
+ apiBase="https://your-api.com"
462
+ basePath="/projects"
463
+ subtitle="Our systems are installed in every geographic region of the U.S."
464
+ maxProjects={6}
465
+ />
466
+ )
467
+ }
468
+ ```
469
+
470
+ #### With a curated menu
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.
473
+
474
+ ```tsx
475
+ <ProjectMenu
476
+ clientSlug="your-client-slug"
477
+ apiBase="https://your-api.com"
478
+ menuId="main-nav"
479
+ basePath="/projects"
480
+ />
481
+ ```
482
+
483
+ | Prop | Type | Required | Default | Description |
484
+ |---|---|---|---|---|
485
+ | `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
486
+ | `apiBase` | `string` | Yes | — | Base URL of the projects API |
487
+ | `menuId` | `string` | No | — | Slug of a curated menu. When provided, fetches from `/menus/{slug}` instead of all projects. Filters always shown regardless. |
488
+ | `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
489
+ | `viewAllPath` | `string` | No | Same as `basePath` | Path for the "View All Projects" link |
490
+ | `subtitle` | `string` | No | — | Description paragraph shown above the project cards |
491
+ | `font` | `string` | No | System font stack | Font family string applied to all inline styles |
492
+ | `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
493
+ | `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
494
+ | `noCache` | `boolean` | No | `false` | Bypasses the Next.js Data Cache and sets `cache: "no-store"`. Useful during development or for frequently updated projects. |
495
+
496
+ ---
497
+
498
+ ### `ProjectMenuClient` + `createMenuHandler`
499
+
500
+ `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.
501
+
502
+ There are two ways to set it up:
503
+
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.
509
+
510
+ ```ts
511
+ // app/api/chisel-menu/route.ts
512
+ import { createMenuHandler } from "project-portfolio"
513
+
514
+ export const GET = createMenuHandler({
515
+ clientSlug: "your-client-slug",
516
+ apiBase: "https://your-api.com",
517
+ })
518
+ ```
519
+
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
+ ```tsx
534
+ // components/Nav.tsx
535
+ "use client"
536
+ import { ProjectMenuClient } from "project-portfolio"
537
+
538
+ export function Nav() {
539
+ return (
540
+ <ProjectMenuClient
541
+ dataUrl="/api/chisel-menu"
542
+ basePath="/projects"
543
+ viewAllPath="/projects"
544
+ subtitle="Explore our portfolio of projects."
545
+ maxProjects={6}
546
+ />
547
+ )
548
+ }
549
+ ```
550
+
551
+ ---
552
+
553
+ #### Option 2 — Direct fetch (quick setup / non-Next.js environments)
554
+
555
+ 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.
556
+
557
+ ```tsx
558
+ "use client"
559
+ import { ProjectMenuClient } from "project-portfolio"
560
+
561
+ export function Nav() {
562
+ return (
563
+ <ProjectMenuClient
564
+ clientSlug="your-client-slug"
565
+ apiBase="https://your-api.com"
566
+ basePath="/projects"
567
+ viewAllPath="/projects"
568
+ />
569
+ )
570
+ }
571
+ ```
572
+
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
+ | Prop | Type | Required | Default | Description |
588
+ |---|---|---|---|---|
589
+ | `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()`. Recommended for production |
590
+ | `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
591
+ | `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
592
+ | `menuId` | `string` | No | — | Slug of a curated menu. Fetches from `/menus/{slug}` for projects. Filters are always shown regardless. |
593
+ | `noCache` | `boolean` | No | `false` | Bypasses the module-level data cache and sets `cache: "no-store"` on all fetch calls. Useful during development or when projects update frequently. |
594
+ | `basePath` | `string` | Yes | — | Base path for project detail links |
595
+ | `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
596
+ | `subtitle` | `string` | No | — | Description shown above the project cards (hidden on mobile) |
597
+ | `font` | `string` | No | System font stack | Font family string |
598
+ | `maxProjects` | `number` | No | `6` | Maximum number of projects to display (capped at 3 on mobile) |
599
+
600
+ *One of `dataUrl` or `clientSlug + apiBase` must be provided.
601
+
602
+ ---
603
+
604
+ ## Server vs Client Components
605
+
606
+ All top-level components except `ProjectMenuClient`, `ProjectPortfolioClient`, and `GalleryCarousel` are **async Server Components**. They must be rendered in a server context:
607
+
608
+ ```tsx
609
+ // CORRECT
610
+ export default async function Page() {
611
+ return <ProjectPortfolio clientSlug="..." apiBase="..." />
612
+ }
613
+
614
+ // WRONG — causes a runtime error
615
+ "use client"
616
+ export default function Page() {
617
+ return <ProjectPortfolio clientSlug="..." apiBase="..." />
618
+ }
619
+ ```
620
+
621
+ 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.
622
+
623
+ | Component | Type | Use when |
624
+ |---|---|---|
625
+ | `ProjectPortfolio` | Server | Page-level grid, URL-driven filters |
626
+ | `ProjectPortfolioClient` | Client | Inside a client tree, custom filter UI |
627
+ | `ProjectDetail` | Server | Project detail page |
628
+ | `GalleryCarousel` | Client | Standalone image gallery |
629
+ | `ProjectMenu` | Server | Server-rendered nav dropdown |
630
+ | `ProjectMenuClient` | Client | Client-rendered nav/header |
631
+ | `SimilarProjects` | Server | After `ProjectDetail` on detail pages |
632
+
633
+ ---
634
+
635
+ ## Caching
636
+
637
+ | Component | Server Cache | Client Cache |
638
+ |---|---|---|
639
+ | `ProjectMenu` (RSC) | 24h via `next.revalidate` | — |
640
+ | `ProjectMenuClient` + `createMenuHandler` | 24h (route handler) | Per-session module cache |
641
+ | `ProjectMenuClient` (direct fetch) | None | Per-session module cache |
642
+ | `ProjectPortfolio` (RSC) | 24h via `next.revalidate` | — |
643
+ | `ProjectPortfolioClient` | None | Per-session module cache |
644
+ | `ProjectDetail` (RSC) | 24h via `next.revalidate` | — |
645
+ | `SimilarProjects` (RSC) | 24h via `next.revalidate` | — |
646
+
647
+ #### Bypassing the cache
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 |
655
+
656
+ ```ts
657
+ // CMS webhook — invalidate server cache on publish
658
+ import { revalidateTag } from "next/cache"
659
+ revalidateTag("chisel-menu-your-client-slug")
660
+
661
+ // For a curated menu, the tag includes the menuId
662
+ revalidateTag("chisel-menu-your-client-slug-main-nav")
663
+ ```
664
+
665
+ ---
666
+
667
+ ## Publishing to npm
668
+
669
+ ```bash
670
+ npm login
671
+ cd package
672
+ npm run build
673
+ npm publish --access public
674
+ ```
675
+
676
+ To release an update, bump the `version` field in `package/package.json` then run `npm publish` again.
@@ -0,0 +1,7 @@
1
+ import type { Media } from "./types";
2
+ export interface GalleryCarouselProps {
3
+ images: Media[];
4
+ projectTitle: string;
5
+ }
6
+ export declare function GalleryCarousel({ images, projectTitle, }: GalleryCarouselProps): import("react/jsx-runtime").JSX.Element | null;
7
+ //# sourceMappingURL=GalleryCarousel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,YAAY,GACb,EAAE,oBAAoB,kDAwJtB"}