project-portfolio 2.1.0 → 2.2.1

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 CHANGED
@@ -216,7 +216,7 @@ export default function ProjectsPage() {
216
216
 
217
217
  ### `ProjectDetail`
218
218
 
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.
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
220
 
221
221
  ```tsx
222
222
  // app/projects/[slug]/page.tsx
@@ -235,6 +235,36 @@ export default async function ProjectPage({ params }: { params: { slug: string }
235
235
  }
236
236
  ```
237
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
+
238
268
  | Prop | Type | Required | Default | Description |
239
269
  |---|---|---|---|---|
240
270
  | `slug` | `string` | Yes | — | The project slug to load |
@@ -265,18 +295,7 @@ export function ProjectGallery({ media, title }: { media: Media[]; title: string
265
295
  }
266
296
  ```
267
297
 
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
- ```
298
+ The main image and thumbnails are standardised to a `16/9` aspect ratio no external CSS required.
280
299
 
281
300
  | Prop | Type | Required | Default | Description |
282
301
  |---|---|---|---|---|
@@ -446,7 +465,7 @@ export async function ProjectsMegaMenu() {
446
465
 
447
466
  #### With a curated menu
448
467
 
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`.
468
+ 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.
450
469
 
451
470
  ```tsx
452
471
  <ProjectMenu
@@ -461,7 +480,7 @@ Pass `menuId` to show a specific curated set of projects instead of all projects
461
480
  |---|---|---|---|---|
462
481
  | `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
463
482
  | `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. |
483
+ | `menuId` | `string` | No | — | Slug of a curated menu. When provided, fetches from `/menus/{slug}` instead of all projects. Filters always shown regardless. |
465
484
  | `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
466
485
  | `viewAllPath` | `string` | No | Same as `basePath` | Path for the "View All Projects" link |
467
486
  | `subtitle` | `string` | No | — | Description paragraph shown above the project cards |
@@ -566,7 +585,7 @@ With a curated menu:
566
585
  | `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()`. Recommended for production |
567
586
  | `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
568
587
  | `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. |
588
+ | `menuId` | `string` | No | — | Slug of a curated menu. Fetches from `/menus/{slug}` for projects. Filters are always shown regardless. |
570
589
  | `basePath` | `string` | Yes | — | Base path for project detail links |
571
590
  | `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
572
591
  | `subtitle` | `string` | No | — | Description shown above the project cards (hidden on mobile) |
@@ -16,7 +16,7 @@ export function GalleryCarousel({ images, projectTitle, }) {
16
16
  function next() {
17
17
  setCurrent((c) => (c + 1) % total);
18
18
  }
19
- return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px", fontFamily: font }, children: [_jsxs("div", { className: "chisel-gallery-main-img", style: { position: "relative", width: "100%", backgroundColor: "#f4f4f5", overflow: "hidden" }, children: [_jsx("img", { src: active.url, alt: active.alt || projectTitle, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }), total > 1 && (_jsx("button", { onClick: prev, "aria-label": "Previous image", style: {
19
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px", fontFamily: font }, children: [_jsxs("div", { style: { position: "relative", width: "100%", aspectRatio: "16/9", backgroundColor: "#f4f4f5", overflow: "hidden" }, children: [_jsx("img", { src: active.url, alt: active.alt || projectTitle, style: { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", display: "block" } }), total > 1 && (_jsx("button", { onClick: prev, "aria-label": "Previous image", style: {
20
20
  position: "absolute",
21
21
  left: "16px",
22
22
  top: "50%",
@@ -68,8 +68,8 @@ export function GalleryCarousel({ images, projectTitle, }) {
68
68
  }, children: caption })), total > 1 && (_jsx("div", { style: { display: "flex", gap: "8px", overflowX: "auto", WebkitOverflowScrolling: "touch", paddingBottom: "4px" }, children: images.map((img, i) => {
69
69
  var _a;
70
70
  return (_jsx("button", { onClick: () => setCurrent(i), "aria-label": `View image ${i + 1}`, style: {
71
- width: "72px",
72
- height: "52px",
71
+ width: "88px",
72
+ aspectRatio: "16/9",
73
73
  padding: 0,
74
74
  border: i === current ? "2px solid #f18a00" : "2px solid transparent",
75
75
  borderRadius: "2px",
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectCard.d.ts","sourceRoot":"","sources":["../src/ProjectCard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAmC,MAAM,SAAS,CAAA;AAE1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAA;AAE5C,UAAU,gBAAgB;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,2FAA2F;IAC3F,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAeD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,MAAM,EACN,eAAoB,EACpB,QAAQ,EACR,OAAgB,EAChB,QAAsB,GACvB,EAAE,gBAAgB,2CA6PlB"}
1
+ {"version":3,"file":"ProjectCard.d.ts","sourceRoot":"","sources":["../src/ProjectCard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAmC,MAAM,SAAS,CAAA;AAE1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAA;AAE5C,UAAU,gBAAgB;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,2FAA2F;IAC3F,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAeD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,MAAM,EACN,eAAoB,EACpB,QAAQ,EACR,OAAgB,EAChB,QAAsB,GACvB,EAAE,gBAAgB,2CA2QlB"}
@@ -13,18 +13,20 @@ function parseSingleValue(raw) {
13
13
  return String(raw !== null && raw !== void 0 ? raw : "");
14
14
  }
15
15
  export function ProjectCard({ project, schema, fieldOptionsMap = {}, priority, variant = "card", basePath = "/projects", }) {
16
- var _a, _b, _c, _d;
16
+ var _a, _b, _c, _d, _e, _f;
17
17
  const imageUrl = (_d = (_a = project.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = project.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : null;
18
18
  const badgeField = schema.find((f) => f.display_position === "badge_overlay");
19
19
  const tagFields = schema
20
20
  .filter((f) => f.display_position === "tags" && f.type !== "number")
21
21
  .filter((f, i, arr) => arr.findIndex((x) => x.key === f.key) === i);
22
- // Numeric field for footer stat — the first visible number field (year completed)
23
- const numericField = schema.find((f) => f.type === "number" && f.display_position !== "hidden");
22
+ // Coverage/sq ft field for footer stat — matched by key or name regardless of type
23
+ const numericField = schema.find((f) => /sq|sqft|square|coverage/i.test(f.key + f.name));
24
24
  const locationField = schema.find((f) => f.type === "location");
25
- const badgeValue = badgeField
25
+ const badgeRaw = badgeField
26
26
  ? parseSingleValue(project.custom_field_values[badgeField.key])
27
27
  : null;
28
+ const badgeOptMap = badgeField ? ((_e = fieldOptionsMap[badgeField.key]) !== null && _e !== void 0 ? _e : {}) : {};
29
+ const badgeValue = badgeRaw ? ((_f = badgeOptMap[badgeRaw]) !== null && _f !== void 0 ? _f : badgeRaw) : null;
28
30
  const locationValue = locationField
29
31
  ? project.custom_field_values[locationField.key]
30
32
  : null;
@@ -38,11 +40,23 @@ export function ProjectCard({ project, schema, fieldOptionsMap = {}, priority, v
38
40
  if (val === undefined || val === null || val === "")
39
41
  return null;
40
42
  const label = numericField.name.replace(/\s*\([^)]+\)/, "");
41
- const numVal = typeof val === "number" ? val : parseFloat(String(val));
42
- const formatted = isNaN(numVal) ? String(val) : String(Math.round(numVal));
43
+ // Coverage is a pre-formatted string from the API (e.g. "186,446 SF") render as-is.
44
+ // For numeric fields, format with commas.
45
+ const formatted = typeof val === "string"
46
+ ? val
47
+ : typeof val === "number"
48
+ ? Math.round(val).toLocaleString()
49
+ : String(val);
43
50
  return { label, formatted };
44
51
  })();
45
- const compactTags = tagFields.flatMap((field) => parseMultiValue(project.custom_field_values[field.key]));
52
+ const compactTags = tagFields.flatMap((field) => {
53
+ var _a;
54
+ const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
55
+ const hasOptions = Object.keys(optMap).length > 0;
56
+ const vals = parseMultiValue(project.custom_field_values[field.key]);
57
+ const active = hasOptions ? vals.filter((v) => optMap[v] !== undefined) : vals;
58
+ return active.map((v) => { var _a; return (_a = optMap[v]) !== null && _a !== void 0 ? _a : v; });
59
+ });
46
60
  const href = `${basePath}/${project.slug}`;
47
61
  if (variant === "compact") {
48
62
  return (_jsxs("a", { href: href, style: {
@@ -120,7 +134,12 @@ export function ProjectCard({ project, schema, fieldOptionsMap = {}, priority, v
120
134
  if (vals.length === 0)
121
135
  return null;
122
136
  const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
123
- return (_jsxs("div", { style: { marginBottom: "16px" }, children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 600, letterSpacing: "0.1em", textTransform: "uppercase", color: "#a1a1aa", marginBottom: "8px" }, children: "Systems Used:" }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: vals.map((val, i) => {
137
+ const hasOptions = Object.keys(optMap).length > 0;
138
+ // Filter out archived values — if optMap is populated and the value isn't in it, it's archived
139
+ const activeVals = hasOptions ? vals.filter((val) => optMap[val] !== undefined) : vals;
140
+ if (activeVals.length === 0)
141
+ return null;
142
+ return (_jsxs("div", { style: { marginBottom: "16px" }, children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 600, letterSpacing: "0.1em", textTransform: "uppercase", color: "#a1a1aa", marginBottom: "8px" }, children: "Systems Used:" }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: activeVals.map((val, i) => {
124
143
  var _a, _b, _c, _d;
125
144
  // Normalize: look up by id or label, always display the label
126
145
  const label = (_a = optMap[val]) !== null && _a !== void 0 ? _a : val;
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectDetail.d.ts","sourceRoot":"","sources":["../src/ProjectDetail.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAmDD,wBAAsB,aAAa,CAAC,EAClC,IAAI,EACJ,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,SAA0B,EAC1B,UAAe,GAChB,EAAE,kBAAkB,oDAyKpB"}
1
+ {"version":3,"file":"ProjectDetail.d.ts","sourceRoot":"","sources":["../src/ProjectDetail.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAiED,wBAAsB,aAAa,CAAC,EAClC,IAAI,EACJ,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,SAA0B,EAC1B,UAAe,GAChB,EAAE,kBAAkB,oDAoQpB"}
@@ -21,25 +21,39 @@ function dedupeByKey(arr) {
21
21
  }
22
22
  // ─── Data fetching ───────────────────────────────────────────────────────────
23
23
  const fetchProjectDetail = cache(async (apiBase, clientSlug, slug, revalidate) => {
24
- var _a, _b, _c;
24
+ var _a, _b, _c, _d;
25
25
  const fetchOpts = revalidate > 0
26
26
  ? { next: { revalidate } }
27
27
  : {};
28
28
  try {
29
+ // Single call — /projects/{slug} returns the project AND client.custom_fields_schema
30
+ // with full options embedded. No need for a separate /fields call.
29
31
  const projectRes = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts);
30
32
  const projectJson = projectRes.ok ? await projectRes.json() : null;
31
33
  const project = (_a = projectJson === null || projectJson === void 0 ? void 0 : projectJson.data) !== null && _a !== void 0 ? _a : null;
32
34
  const schema = dedupeByKey((_c = (_b = projectJson === null || projectJson === void 0 ? void 0 : projectJson.client) === null || _b === void 0 ? void 0 : _b.custom_fields_schema) !== null && _c !== void 0 ? _c : []);
33
- return { project, schema };
35
+ // Build fieldOptionsMap directly from schema options — identical data to /fields
36
+ const fieldOptionsMap = {};
37
+ for (const field of schema) {
38
+ const map = {};
39
+ for (const opt of ((_d = field.options) !== null && _d !== void 0 ? _d : [])) {
40
+ if (typeof opt === "object" && opt.id && opt.label) {
41
+ map[opt.id] = opt.label;
42
+ map[opt.label] = opt.label;
43
+ }
44
+ }
45
+ fieldOptionsMap[field.key] = map;
46
+ }
47
+ return { project, schema, fieldOptionsMap };
34
48
  }
35
- catch (_d) {
36
- return { project: null, schema: [] };
49
+ catch (_e) {
50
+ return { project: null, schema: [], fieldOptionsMap: {} };
37
51
  }
38
52
  });
39
53
  // ─── Component ───────────────────────────────────────────────────────────────
40
54
  export async function ProjectDetail({ slug, clientSlug, apiBase, backPath = "/projects", backLabel = "All Projects", revalidate = 60, }) {
41
- var _a, _b, _c, _d, _e, _f, _g, _h, _j;
42
- const { project, schema } = await fetchProjectDetail(apiBase, clientSlug, slug, revalidate);
55
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
56
+ const { project, schema, fieldOptionsMap } = await fetchProjectDetail(apiBase, clientSlug, slug, revalidate);
43
57
  if (!project) {
44
58
  return (_jsx("div", { style: { textAlign: "center", padding: "6rem 1.5rem", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, children: _jsx("p", { style: { color: "#71717a", fontSize: "18px" }, children: "Project not found." }) }));
45
59
  }
@@ -49,53 +63,115 @@ export async function ProjectDetail({ slug, clientSlug, apiBase, backPath = "/pr
49
63
  : ((_g = (_f = project.media) === null || _f === void 0 ? void 0 : _f.slice(1)) !== null && _g !== void 0 ? _g : []);
50
64
  const badgeField = schema.find((f) => f.display_position === "badge_overlay");
51
65
  const locationField = schema.find((f) => f.type === "location");
52
- // Year field: any number field whose key or name contains "year"
53
- const yearField = schema.find((f) => f.type === "number" && f.display_position !== "hidden" &&
54
- (/year/i.test(f.key) || /year/i.test(f.name)));
55
- // Sq footage field: any field (text or number) whose key or name matches common area/coverage terms
56
- // Falls back to the first non-year number field so it works regardless of client field naming
57
- const sqftField = (_h = schema.find((f) => f.display_position !== "hidden" &&
58
- (/coverage|sq.?ft|square.?foot|area|footage/i.test(f.key) || /coverage|sq.?ft|square.?foot|area|footage/i.test(f.name)))) !== null && _h !== void 0 ? _h : schema.find((f) => f.type === "number" && f.display_position !== "hidden" &&
59
- !(/year/i.test(f.key) || /year/i.test(f.name)));
60
- const badgeValue = badgeField
61
- ? ((_j = parseMultiValue(project.custom_field_values[badgeField.key])[0]) !== null && _j !== void 0 ? _j : null)
66
+ const badgeRaw = badgeField
67
+ ? ((_h = parseMultiValue(project.custom_field_values[badgeField.key])[0]) !== null && _h !== void 0 ? _h : null)
62
68
  : null;
69
+ const badgeOptMap = badgeField ? ((_j = fieldOptionsMap[badgeField.key]) !== null && _j !== void 0 ? _j : {}) : {};
70
+ const badgeValue = badgeRaw ? ((_k = badgeOptMap[badgeRaw]) !== null && _k !== void 0 ? _k : badgeRaw) : null;
63
71
  const locationValue = locationField
64
72
  ? project.custom_field_values[locationField.key]
65
73
  : null;
66
74
  const locationString = locationValue
67
75
  ? [locationValue.city, locationValue.state].filter(Boolean).join(", ")
68
76
  : null;
69
- const coverageFormatted = (() => {
70
- const raw = sqftField ? project.custom_field_values[sqftField.key] : null;
77
+ // Helper: format a raw field value to a display string, resolving slugs via fieldOptionsMap
78
+ function formatFieldValue(field, raw) {
79
+ var _a, _b;
71
80
  if (raw === null || raw === undefined || raw === "")
72
81
  return null;
82
+ const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
73
83
  if (typeof raw === "string")
74
- return raw; // already formatted e.g. "225 SF"
75
- const n = typeof raw === "number" ? raw : parseFloat(String(raw));
76
- return isNaN(n) ? String(raw) : `${n.toLocaleString()} SF`;
77
- })();
78
- const yearCompleted = (() => {
79
- const raw = yearField ? project.custom_field_values[yearField.key] : null;
80
- if (raw === null || raw === undefined)
81
- return null;
82
- const n = typeof raw === "number" ? raw : parseFloat(String(raw));
83
- return isNaN(n) ? String(raw) : String(Math.round(n));
84
- })();
84
+ return (_b = optMap[raw]) !== null && _b !== void 0 ? _b : raw;
85
+ if (typeof raw === "number") {
86
+ if (/year/i.test(field.key + field.name))
87
+ return String(Math.round(raw));
88
+ return Math.round(raw).toLocaleString();
89
+ }
90
+ if (Array.isArray(raw)) {
91
+ const hasOptions = Object.keys(optMap).length > 0;
92
+ const vals = raw.map(String).filter(Boolean);
93
+ const active = hasOptions ? vals.filter((v) => optMap[v] !== undefined) : vals;
94
+ const labels = active.map((v) => { var _a; return (_a = optMap[v]) !== null && _a !== void 0 ? _a : v; });
95
+ return labels.length > 0 ? labels.join(", ") : null;
96
+ }
97
+ return null;
98
+ }
99
+ // ── Stats bar: explicit ordered list of fields to show ────────────────────
100
+ // Location and badge/type are handled first from their detected fields,
101
+ // then remaining stat bar fields are matched by key in the schema.
102
+ const STAT_BAR_KEYS = ["coverage", "year-completed", "architect", "general-contractor"];
103
+ const metadataStats = [];
104
+ if (locationString) {
105
+ metadataStats.push({ label: "Location", value: locationString });
106
+ }
107
+ if (badgeValue) {
108
+ metadataStats.push({ label: badgeField.name, value: badgeValue });
109
+ }
110
+ for (const key of STAT_BAR_KEYS) {
111
+ const field = schema.find((f) => f.key === key);
112
+ if (!field)
113
+ continue;
114
+ const raw = project.custom_field_values[field.key];
115
+ const formatted = formatFieldValue(field, raw);
116
+ // Use "Completed" as the label for year-completed
117
+ const label = key === "year-completed" ? "Completed" : field.name;
118
+ if (formatted)
119
+ metadataStats.push({ label, value: formatted });
120
+ }
121
+ // ── Specs sidebar: explicit ordered list of fields ────────────────────────
122
+ const SPEC_SIDEBAR_KEYS = [
123
+ "systems-used",
124
+ "systems",
125
+ "series-used",
126
+ "operation-type",
127
+ "finishes",
128
+ "specifications",
129
+ ];
130
+ const specFields = SPEC_SIDEBAR_KEYS
131
+ .map((key) => schema.find((f) => f.key === key))
132
+ .filter((f) => f !== undefined);
85
133
  const font = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
86
- return (_jsxs("main", { style: { minHeight: "100vh", backgroundColor: "#fff", fontFamily: font }, children: [_jsxs("section", { className: "chisel-hero-img", style: { position: "relative", width: "100%", overflow: "hidden" }, children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })) : (_jsx("div", { style: { position: "absolute", inset: 0, backgroundColor: "#27272a" } })), _jsx("div", { style: { position: "absolute", inset: 0, background: "linear-gradient(to top, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.38) 50%, rgba(0,0,0,0.12) 100%)" } }), _jsxs("div", { style: { position: "absolute", bottom: 0, left: 0, right: 0, padding: "0 1rem 1.5rem", maxWidth: "900px" }, children: [badgeValue && (_jsx("p", { style: { color: "#f18a00", fontSize: "11px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", margin: "0 0 10px 0" }, children: badgeValue })), _jsx("h1", { style: { color: "#fff", fontWeight: 700, fontSize: "clamp(28px, 4vw, 52px)", lineHeight: 1.1, margin: "0 0 12px 0", fontFamily: font }, children: project.title }), locationString && (_jsxs("p", { style: { display: "flex", alignItems: "center", gap: "6px", color: "rgba(255,255,255,0.75)", fontSize: "15px", margin: 0 }, children: [_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0 }, children: [_jsx("path", { d: "M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" }), _jsx("circle", { cx: "12", cy: "10", r: "3" })] }), locationString] }))] })] }), _jsxs("section", { style: { borderBottom: "1px solid #e4e4e7", backgroundColor: "#fff" }, children: [_jsx("style", { children: `
87
- .chisel-stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.25rem; }
88
- @media (min-width: 768px) { .chisel-stats-grid { grid-template-columns: repeat(4, 1fr); gap: 2rem; } }
89
- .chisel-gallery-placeholder { display: grid; grid-template-columns: 1fr; gap: 12px; }
90
- @media (min-width: 640px) { .chisel-gallery-placeholder { grid-template-columns: repeat(3, 1fr); } }
91
- .chisel-hero-img { height: 50vw; min-height: 220px; max-height: 560px; }
92
- .chisel-gallery-main-img { height: 60vw; min-height: 240px; max-height: 520px; }
93
- ` }), _jsx("div", { style: { maxWidth: "1280px", margin: "0 auto", padding: "1.5rem 1rem", boxSizing: "border-box" }, className: "chisel-stats-grid", children: [
94
- { label: "Location", value: locationString !== null && locationString !== void 0 ? locationString : "—" },
95
- { label: "Project Type", value: badgeValue !== null && badgeValue !== void 0 ? badgeValue : "—" },
96
- { label: "Sq. Footage", value: coverageFormatted !== null && coverageFormatted !== void 0 ? coverageFormatted : "—" },
97
- { label: "Completed", value: yearCompleted !== null && yearCompleted !== void 0 ? yearCompleted : "" },
98
- ].map(({ label, value }) => (_jsxs("div", { children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.1em", color: "#a1a1aa", margin: "0 0 6px 0" }, children: label }), _jsx("p", { style: { color: "#18181b", fontWeight: 600, fontSize: "15px", margin: 0 }, children: value })] }, label))) })] }), _jsxs("article", { style: { maxWidth: "1280px", margin: "0 auto", padding: "2rem 1rem", boxSizing: "border-box", display: "flex", flexDirection: "column", gap: "2.5rem" }, children: [(project.blurb || project.description) && (_jsxs("section", { style: { maxWidth: "900px" }, children: [_jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "22px", margin: "0 0 16px 0", fontFamily: font }, children: "Project Overview" }), project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", lineHeight: 1.7, margin: "0 0 16px 0" }, children: project.blurb })), project.description && project.description !== project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", lineHeight: 1.7, margin: 0 }, children: project.description }))] })), _jsxs("section", { children: [_jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "22px", margin: "0 0 1.5rem 0", fontFamily: font }, children: "Project Gallery" }), galleryImages.length > 0 ? (_jsx(GalleryCarousel, { images: galleryImages, projectTitle: project.title })) : (
134
+ return (_jsxs("main", { style: { minHeight: "100vh", backgroundColor: "#fff", fontFamily: font }, children: [_jsxs("section", { className: "chisel-hero-img", style: { position: "relative", width: "100%", overflow: "hidden" }, children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })) : (_jsx("div", { style: { position: "absolute", inset: 0, backgroundColor: "#27272a" } })), _jsx("div", { style: { position: "absolute", inset: 0, background: "linear-gradient(to top, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.38) 50%, rgba(0,0,0,0.12) 100%)" } }), _jsxs("div", { style: { position: "absolute", bottom: 0, left: 0, right: 0, padding: "0 1rem 1.5rem", maxWidth: "900px" }, children: [badgeValue && (_jsx("p", { style: { color: "#f18a00", fontSize: "11px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", margin: "0 0 10px 0" }, children: badgeValue })), _jsx("h1", { style: { color: "#fff", fontWeight: 700, fontSize: "clamp(28px, 4vw, 52px)", lineHeight: 1.1, margin: "0 0 12px 0", fontFamily: font }, children: project.title }), locationString && (_jsxs("p", { style: { display: "flex", alignItems: "center", gap: "6px", color: "rgba(255,255,255,0.75)", fontSize: "15px", margin: 0 }, children: [_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0 }, children: [_jsx("path", { d: "M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" }), _jsx("circle", { cx: "12", cy: "10", r: "3" })] }), locationString] }))] })] }), metadataStats.length > 0 && (_jsxs("section", { style: { borderBottom: "1px solid #e4e4e7", backgroundColor: "#fff" }, children: [_jsx("style", { children: `
135
+ .chisel-stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; }
136
+ @media (min-width: 640px) { .chisel-stats-grid { grid-template-columns: repeat(3, 1fr); } }
137
+ @media (min-width: 1024px) { .chisel-stats-grid { grid-template-columns: repeat(${Math.min(metadataStats.length, 6)}, 1fr); } }
138
+ .chisel-stat-item { padding: 1.5rem 1.25rem; border-right: 1px solid #e4e4e7; border-bottom: 1px solid #e4e4e7; }
139
+ .chisel-stat-item:last-child { border-right: none; }
140
+ .chisel-gallery-placeholder { display: grid; grid-template-columns: 1fr; gap: 12px; }
141
+ @media (min-width: 640px) { .chisel-gallery-placeholder { grid-template-columns: repeat(3, 1fr); } }
142
+ .chisel-hero-img { height: 50vw; min-height: 220px; max-height: 560px; }
143
+ .chisel-overview-grid { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
144
+ @media (min-width: 1024px) { .chisel-overview-grid { grid-template-columns: 1fr 300px; gap: 4rem; align-items: start; } }
145
+ ` }), _jsx("div", { style: { maxWidth: "1280px", margin: "0 auto", boxSizing: "border-box", padding: "0 0 0 0" }, className: "chisel-stats-grid", children: metadataStats.map(({ label, value }) => (_jsxs("div", { className: "chisel-stat-item", children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: "#a1a1aa", margin: "0 0 8px 0" }, children: label }), _jsx("p", { style: { color: "#18181b", fontWeight: 600, fontSize: "15px", margin: 0, lineHeight: 1.4 }, children: value })] }, label))) })] })), _jsxs("article", { style: { maxWidth: "1280px", margin: "0 auto", padding: "3rem 1.5rem", boxSizing: "border-box" }, children: [(project.blurb || project.description || specFields.length > 0) && (_jsxs("section", { style: { marginBottom: "3rem" }, children: [_jsx("div", { style: { borderBottom: "1px solid #e4e4e7", marginBottom: "2rem", paddingBottom: "1rem", display: "flex", alignItems: "baseline", justifyContent: "space-between" }, children: _jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "20px", margin: 0, letterSpacing: "-0.01em", fontFamily: font }, children: "Project Overview" }) }), _jsxs("div", { className: "chisel-overview-grid", children: [_jsxs("div", { children: [project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", fontWeight: 400, lineHeight: 1.85, margin: "0 0 20px 0" }, children: project.blurb })), project.description && project.description !== project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", fontWeight: 400, lineHeight: 1.85, margin: 0 }, children: project.description }))] }), specFields.length > 0 && (_jsx("aside", { style: { borderLeft: "3px solid #f18a00", paddingLeft: "2rem" }, children: _jsx("div", { style: { display: "flex", flexDirection: "column", gap: "2rem" }, children: specFields.map((field) => {
146
+ var _a;
147
+ const raw = project.custom_field_values[field.key];
148
+ const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
149
+ const hasOptions = Object.keys(optMap).length > 0;
150
+ const rawVals = Array.isArray(raw)
151
+ ? raw.map(String).filter(Boolean)
152
+ : typeof raw === "string"
153
+ ? [raw]
154
+ : raw !== null && raw !== undefined
155
+ ? [String(raw)]
156
+ : [];
157
+ // Filter out archived values and resolve slugs to labels
158
+ const vals = hasOptions
159
+ ? rawVals.filter((v) => optMap[v] !== undefined).map((v) => { var _a; return (_a = optMap[v]) !== null && _a !== void 0 ? _a : v; })
160
+ : rawVals;
161
+ if (vals.length === 0)
162
+ return null;
163
+ return (_jsxs("div", { children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: "#a1a1aa", margin: "0 0 12px 0" }, children: field.name }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: vals.map((val) => (_jsx("span", { style: {
164
+ display: "inline-block",
165
+ padding: "5px 14px",
166
+ backgroundColor: "#fafafa",
167
+ border: "1px solid #e4e4e7",
168
+ color: "#3f3f46",
169
+ fontSize: "13px",
170
+ fontWeight: 500,
171
+ borderRadius: "2px",
172
+ lineHeight: 1.5,
173
+ }, children: val }, val))) })] }, field.key));
174
+ }) }) }))] })] })), _jsxs("section", { children: [_jsx("div", { style: { borderBottom: "1px solid #e4e4e7", marginBottom: "2rem", paddingBottom: "1rem" }, children: _jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "20px", margin: 0, letterSpacing: "-0.01em", fontFamily: font }, children: "Project Gallery" }) }), galleryImages.length > 0 ? (_jsx(GalleryCarousel, { images: galleryImages, projectTitle: project.title })) : (
99
175
  /* Placeholder */
100
- _jsx("div", { className: "chisel-gallery-placeholder", children: [0, 1, 2].map((i) => (_jsxs("div", { style: { aspectRatio: "4/3", borderRadius: "4px", backgroundColor: "#f9f9f9", border: "1px solid #e4e4e7", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "8px" }, children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#a1a1aa", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909M3.75 19.5h16.5" }) }), _jsx("p", { style: { color: "#a1a1aa", fontSize: "12px", margin: 0 }, children: "Photos coming soon" })] }, i))) }))] })] })] }));
176
+ _jsx("div", { className: "chisel-gallery-placeholder", children: [0, 1, 2].map((i) => (_jsxs("div", { style: { aspectRatio: "16/9", borderRadius: "4px", backgroundColor: "#f9f9f9", border: "1px solid #e4e4e7", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "8px" }, children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#a1a1aa", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909M3.75 19.5h16.5" }) }), _jsx("p", { style: { color: "#a1a1aa", fontSize: "12px", margin: 0 }, children: "Photos coming soon" })] }, i))) }))] })] })] }));
101
177
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectMenu.d.ts","sourceRoot":"","sources":["../src/ProjectMenu.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAKzD,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,UAAU,EACV,OAAO,EACP,MAAM,EACN,UAAkB,GACnB,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,aACoC,OAAO,uBAsE3C;AAED,wBAAsB,oBAAoB,CAAC,EACzC,OAAO,EACP,UAAU,EACV,MAAM,EACN,UAAkB,EAClB,OAAe,GAChB,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG,OAAO,CAAC;IACV,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,aAAa,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC9C,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACxD,CAAC,CAqBD;AA8DD,wBAAsB,WAAW,CAAC,EAChC,UAAU,EACV,OAAO,EACP,MAAM,EACN,QAAsB,EACtB,WAAW,EACX,QAAQ,EACR,IAA0E,EAC1E,WAAe,EACf,UAAkB,EAClB,OAAe,GAChB,EAAE,gBAAgB,oDAiClB"}
1
+ {"version":3,"file":"ProjectMenu.d.ts","sourceRoot":"","sources":["../src/ProjectMenu.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAKzD,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,OAAO,EAAE,MAAM,CAAA;IACf;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4DAA4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,UAAU,EACV,OAAO,EACP,MAAM,EACN,UAAkB,GACnB,EAAE;IACD,UAAU,EAAE,MAAM,CAAA;IAClB,OAAO,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,aACoC,OAAO,uBAmE3C;AAED,wBAAsB,oBAAoB,CAAC,EACzC,OAAO,EACP,UAAU,EACV,MAAM,EACN,UAAkB,EAClB,OAAe,GAChB,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,GAAG,OAAO,CAAC;IACV,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,aAAa,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC9C,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,eAAe,EAAE,MAAM,CAAA;IACvB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CACxD,CAAC,CAqBD;AA0DD,wBAAsB,WAAW,CAAC,EAChC,UAAU,EACV,OAAO,EACP,MAAM,EACN,QAAsB,EACtB,WAAW,EACX,QAAQ,EACR,IAA0E,EAC1E,WAAe,EACf,UAAkB,EAClB,OAAe,GAChB,EAAE,gBAAgB,oDAiClB"}
@@ -28,7 +28,7 @@ const API_KEY = "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR";
28
28
  */
29
29
  export function createMenuHandler({ clientSlug, apiBase, menuId, revalidate = 86400, }) {
30
30
  return async function GET(request) {
31
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
31
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
32
32
  const bypass = process.env.CHISEL_CACHE_BYPASS === "true" ||
33
33
  new URL(request.url).searchParams.has("bust");
34
34
  const cacheTag = menuId ? `chisel-menu-${clientSlug}-${menuId}` : `chisel-menu-${clientSlug}`;
@@ -36,31 +36,27 @@ export function createMenuHandler({ clientSlug, apiBase, menuId, revalidate = 86
36
36
  ? { cache: "no-store" }
37
37
  : { next: { revalidate, tags: [cacheTag] } };
38
38
  try {
39
- // Always fetch /projects for schema, and /fields for options
40
- // If menuId is provided, also fetch the menu endpoint for curated projects
41
- const fetches = [
42
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
43
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`, fetchOpts),
44
- ];
45
- if (menuId) {
46
- fetches.push(fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`, fetchOpts));
47
- }
48
- const responses = await Promise.all(fetches);
49
- const [projectsRes, fieldsRes] = responses;
50
- const menuRes = menuId ? responses[2] : null;
51
- const projectsJson = projectsRes.ok ? await projectsRes.json() : {};
52
- const menuJson = menuRes && menuRes.ok ? await menuRes.json() : null;
53
- // If menuId provided, use menu projects; otherwise use all projects
54
- const projects = menuJson
55
- ? ((_b = (_a = menuJson.projects) !== null && _a !== void 0 ? _a : menuJson.data) !== null && _b !== void 0 ? _b : [])
56
- : ((_c = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.data) !== null && _c !== void 0 ? _c : []);
57
- // Always get schema from /projects response
58
- const schema = (_e = (_d = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.client) === null || _d === void 0 ? void 0 : _d.custom_fields_schema) !== null && _e !== void 0 ? _e : [];
59
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
39
+ // /projects always returns schema + options. Use it in both cases.
40
+ // When menuId provided: 2 calls /menus/{slug} for projects, /projects for schema.
41
+ // Without menuId: 1 call — /projects for everything.
42
+ const fetches = menuId
43
+ ? [
44
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`, fetchOpts),
45
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
46
+ ]
47
+ : [fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts)];
48
+ const [primaryRes, schemaRes] = await Promise.all(fetches);
49
+ const primaryJson = primaryRes.ok ? await primaryRes.json() : {};
50
+ const schemaJson = schemaRes ? (schemaRes.ok ? await schemaRes.json() : {}) : primaryJson;
51
+ const rawProjects = menuId
52
+ ? ((_a = primaryJson.projects) !== null && _a !== void 0 ? _a : [])
53
+ : ((_b = primaryJson.data) !== null && _b !== void 0 ? _b : []);
54
+ const projects = rawProjects.filter((p) => p.is_published !== false);
55
+ const schema = (_d = (_c = schemaJson === null || schemaJson === void 0 ? void 0 : schemaJson.client) === null || _c === void 0 ? void 0 : _c.custom_fields_schema) !== null && _d !== void 0 ? _d : [];
60
56
  const fieldOptionsMap = {};
61
- for (const field of ((_f = fieldsJson.fields) !== null && _f !== void 0 ? _f : [])) {
57
+ for (const field of schema) {
62
58
  const map = {};
63
- for (const opt of ((_g = field.options) !== null && _g !== void 0 ? _g : [])) {
59
+ for (const opt of ((_e = field.options) !== null && _e !== void 0 ? _e : [])) {
64
60
  if (typeof opt === "object" && opt.id && opt.label) {
65
61
  map[opt.id] = opt.label;
66
62
  map[opt.label] = opt.label;
@@ -68,9 +64,9 @@ export function createMenuHandler({ clientSlug, apiBase, menuId, revalidate = 86
68
64
  }
69
65
  fieldOptionsMap[field.key] = map;
70
66
  }
71
- const filterField = (_h = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _h !== void 0 ? _h : null;
67
+ const filterField = (_f = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _f !== void 0 ? _f : null;
72
68
  const filterOptions = filterField
73
- ? ((_j = filterField.options) !== null && _j !== void 0 ? _j : []).map((opt) => {
69
+ ? ((_g = filterField.options) !== null && _g !== void 0 ? _g : []).map((opt) => {
74
70
  var _a, _b;
75
71
  if (typeof opt === "string")
76
72
  return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
@@ -81,12 +77,12 @@ export function createMenuHandler({ clientSlug, apiBase, menuId, revalidate = 86
81
77
  projects,
82
78
  schema,
83
79
  filterOptions,
84
- filterFieldKey: (_k = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _k !== void 0 ? _k : null,
85
- filterFieldName: (_l = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _l !== void 0 ? _l : "Project Type",
80
+ filterFieldKey: (_h = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _h !== void 0 ? _h : null,
81
+ filterFieldName: (_j = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _j !== void 0 ? _j : "Project Type",
86
82
  fieldOptionsMap,
87
83
  });
88
84
  }
89
- catch (_m) {
85
+ catch (_k) {
90
86
  return Response.json({ projects: [], schema: [], filterOptions: [], filterFieldKey: null, filterFieldName: "Project Type", fieldOptionsMap: {} }, { status: 500 });
91
87
  }
92
88
  };
@@ -114,39 +110,34 @@ export async function fetchProjectMenuData({ apiBase, clientSlug, menuId, revali
114
110
  };
115
111
  }
116
112
  const _fetchMenuData = cache(async (apiBase, clientSlug, menuId, revalidate, noCache = false) => {
117
- var _a, _b, _c, _d, _e, _f, _g;
113
+ var _a, _b, _c, _d, _e;
118
114
  const fetchOpts = noCache
119
115
  ? { cache: "no-store" }
120
116
  : { next: { revalidate } };
121
117
  try {
122
- // Always fetch /projects for schema, and /fields for options
123
- // If menuId is provided, also fetch the menu endpoint for curated projects
124
- const fetches = [
125
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
126
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`, fetchOpts),
127
- ];
128
- if (menuId) {
129
- fetches.push(fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`, fetchOpts));
130
- }
131
- const responses = await Promise.all(fetches);
132
- const [projectsRes, fieldsRes] = responses;
133
- const menuRes = menuId ? responses[2] : null;
134
- if (!projectsRes.ok)
118
+ // /projects always returns schema + options. Use it in both cases.
119
+ // When menuId provided: 2 calls /menus/{slug} for projects, /projects for schema.
120
+ // Without menuId: 1 call — /projects for everything.
121
+ const fetches = menuId
122
+ ? [
123
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`, fetchOpts),
124
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
125
+ ]
126
+ : [fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts)];
127
+ const [primaryRes, schemaRes] = await Promise.all(fetches);
128
+ if (!primaryRes.ok)
135
129
  return { projects: [], schema: [], fieldOptionsMap: {} };
136
- const projectsJson = await projectsRes.json();
137
- const menuJson = menuRes && menuRes.ok ? await menuRes.json() : null;
138
- // If menuId provided, use menu projects; otherwise use all projects
139
- const projects = menuJson
140
- ? ((_b = (_a = menuJson.projects) !== null && _a !== void 0 ? _a : menuJson.data) !== null && _b !== void 0 ? _b : [])
141
- : ((_c = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.data) !== null && _c !== void 0 ? _c : []);
142
- // Always get schema from /projects response
143
- const schema = (_e = (_d = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.client) === null || _d === void 0 ? void 0 : _d.custom_fields_schema) !== null && _e !== void 0 ? _e : [];
144
- // Build fieldOptionsMap: { fieldKey: { id: label, label: label } }
145
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
130
+ const primaryJson = await primaryRes.json();
131
+ const schemaJson = schemaRes ? (schemaRes.ok ? await schemaRes.json() : {}) : primaryJson;
132
+ const rawProjects = menuId
133
+ ? ((_a = primaryJson.projects) !== null && _a !== void 0 ? _a : [])
134
+ : ((_b = primaryJson.data) !== null && _b !== void 0 ? _b : []);
135
+ const projects = rawProjects.filter((p) => p.is_published !== false);
136
+ const schema = (_d = (_c = schemaJson === null || schemaJson === void 0 ? void 0 : schemaJson.client) === null || _c === void 0 ? void 0 : _c.custom_fields_schema) !== null && _d !== void 0 ? _d : [];
146
137
  const fieldOptionsMap = {};
147
- for (const field of ((_f = fieldsJson.fields) !== null && _f !== void 0 ? _f : [])) {
138
+ for (const field of schema) {
148
139
  const map = {};
149
- for (const opt of ((_g = field.options) !== null && _g !== void 0 ? _g : [])) {
140
+ for (const opt of ((_e = field.options) !== null && _e !== void 0 ? _e : [])) {
150
141
  if (typeof opt === "object" && opt.id && opt.label) {
151
142
  map[opt.id] = opt.label;
152
143
  map[opt.label] = opt.label;
@@ -156,7 +147,7 @@ const _fetchMenuData = cache(async (apiBase, clientSlug, menuId, revalidate, noC
156
147
  }
157
148
  return { projects, schema, fieldOptionsMap };
158
149
  }
159
- catch (_h) {
150
+ catch (_f) {
160
151
  return { projects: [], schema: [], fieldOptionsMap: {} };
161
152
  }
162
153
  });
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAa3E,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC/C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAmBD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,iBAAiB,EAChC,cAAc,EAAE,kBAAkB,EAClC,eAAe,EAAE,mBAAoC,EACrD,eAAe,EAAE,mBAAwB,EACzC,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAmB,EACnB,WAAe,GAChB,EAAE,sBAAsB,2CAwfxB"}
1
+ {"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAa3E,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC/C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAmBD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,UAAU,EACV,OAAO,EACP,MAAM,EACN,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,iBAAiB,EAChC,cAAc,EAAE,kBAAkB,EAClC,eAAe,EAAE,mBAAoC,EACrD,eAAe,EAAE,mBAAwB,EACzC,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAmB,EACnB,WAAe,GAChB,EAAE,sBAAsB,2CA4fxB"}
@@ -56,7 +56,7 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
56
56
  ]);
57
57
  const menuJson = menuRes.ok ? await menuRes.json() : {};
58
58
  const projectsJson = projectsRes.ok ? await projectsRes.json() : {};
59
- const projects = (_h = (_g = menuJson === null || menuJson === void 0 ? void 0 : menuJson.projects) !== null && _g !== void 0 ? _g : menuJson === null || menuJson === void 0 ? void 0 : menuJson.data) !== null && _h !== void 0 ? _h : [];
59
+ const projects = ((_h = (_g = menuJson === null || menuJson === void 0 ? void 0 : menuJson.projects) !== null && _g !== void 0 ? _g : menuJson === null || menuJson === void 0 ? void 0 : menuJson.data) !== null && _h !== void 0 ? _h : []).filter((p) => p.is_published !== false);
60
60
  // Get schema from menu response first, fallback to projects response
61
61
  const schema = (_o = (_l = (_k = (_j = menuJson === null || menuJson === void 0 ? void 0 : menuJson.client) === null || _j === void 0 ? void 0 : _j.custom_fields_schema) !== null && _k !== void 0 ? _k : menuJson === null || menuJson === void 0 ? void 0 : menuJson.schema) !== null && _l !== void 0 ? _l : (_m = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.client) === null || _m === void 0 ? void 0 : _m.custom_fields_schema) !== null && _o !== void 0 ? _o : [];
62
62
  const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
@@ -95,7 +95,7 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
95
95
  fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`),
96
96
  ]);
97
97
  const json = projectsRes.ok ? await projectsRes.json() : {};
98
- const projects = (_v = json === null || json === void 0 ? void 0 : json.data) !== null && _v !== void 0 ? _v : [];
98
+ const projects = ((_v = json === null || json === void 0 ? void 0 : json.data) !== null && _v !== void 0 ? _v : []).filter((p) => p.is_published !== false);
99
99
  const schema = (_x = (_w = json === null || json === void 0 ? void 0 : json.client) === null || _w === void 0 ? void 0 : _w.custom_fields_schema) !== null && _x !== void 0 ? _x : [];
100
100
  const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
101
101
  const fieldOptionsMap = {};
@@ -348,7 +348,12 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
348
348
  }, children: project.title }), badge && (_jsx("p", { style: { fontSize: "12px", color: "#71717a", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (() => {
349
349
  var _a;
350
350
  const optMap = tagsField ? ((_a = fieldOptionsMap[tagsField.key]) !== null && _a !== void 0 ? _a : {}) : {};
351
- const tagLabels = tags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; }).slice(0, 2);
351
+ const hasOptions = Object.keys(optMap).length > 0;
352
+ // Filter out archived values, then resolve to display labels
353
+ const activeTags = hasOptions ? tags.filter((t) => optMap[t] !== undefined) : tags;
354
+ const tagLabels = activeTags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; }).slice(0, 2);
355
+ if (tagLabels.length === 0)
356
+ return null;
352
357
  return (_jsx("p", { style: { fontSize: "11px", color: ACCENT, margin: 0, fontFamily: font, lineHeight: 1.4 }, children: tagLabels.join(" · ") }));
353
358
  })()] })] }, project.id));
354
359
  }) }))] }), _jsx("div", { className: "chisel-menu-divider-v" }), _jsxs("div", { className: "chisel-menu-right", children: [filterOptions.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { style: {
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectPortfolio.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;IAC5D;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AA6ED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,YAAiB,EACjB,UAAe,EACf,OAAe,GAChB,EAAE,qBAAqB,oDA+HvB"}
1
+ {"version":3,"file":"ProjectPortfolio.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;IAC5D;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAyED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,YAAiB,EACjB,UAAe,EACf,OAAe,GAChB,EAAE,qBAAqB,oDA+HvB"}
@@ -7,7 +7,7 @@ import { ProjectCard } from "./ProjectCard";
7
7
  // 2. next: { revalidate } — Next.js Data Cache, caches across multiple requests
8
8
  // on production deployments. Silently ignored in preview/local.
9
9
  const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate, filtersKey = "{}", noCache = false) => {
10
- var _a, _b, _c, _d, _e, _f, _g;
10
+ var _a, _b, _c, _d, _e, _f;
11
11
  const fetchOpts = noCache
12
12
  ? { cache: "no-store" }
13
13
  : { next: { revalidate } };
@@ -18,10 +18,8 @@ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate, filters
18
18
  if (val)
19
19
  params.append(`filter[${key}]`, val);
20
20
  });
21
- const [res, fieldsRes] = await Promise.all([
22
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?${params.toString()}`, fetchOpts),
23
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts),
24
- ]);
21
+ // Single call /projects returns projects AND client.custom_fields_schema with full options.
22
+ const res = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?${params.toString()}`, fetchOpts);
25
23
  const json = res.ok
26
24
  ? await res.json()
27
25
  : { client: { name: "Projects", description: null, custom_fields_schema: [] }, data: [] };
@@ -33,13 +31,11 @@ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate, filters
33
31
  seen.add(f.key);
34
32
  return true;
35
33
  });
36
- // Build fieldOptionsMap: { fieldKey: { id: label, label: label } }
37
- // Both id and label key to label so we normalize whichever shape is stored
38
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
34
+ // Build fieldOptionsMap directly from schema options no separate /fields call needed
39
35
  const fieldOptionsMap = {};
40
- for (const field of ((_c = fieldsJson.fields) !== null && _c !== void 0 ? _c : [])) {
36
+ for (const field of schema) {
41
37
  const map = {};
42
- for (const opt of ((_d = field.options) !== null && _d !== void 0 ? _d : [])) {
38
+ for (const opt of ((_c = field.options) !== null && _c !== void 0 ? _c : [])) {
43
39
  if (typeof opt === "object" && opt.id && opt.label) {
44
40
  map[opt.id] = opt.label;
45
41
  map[opt.label] = opt.label;
@@ -48,8 +44,8 @@ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate, filters
48
44
  fieldOptionsMap[field.key] = map;
49
45
  }
50
46
  return {
51
- clientName: (_f = (_e = json.client) === null || _e === void 0 ? void 0 : _e.name) !== null && _f !== void 0 ? _f : "Projects",
52
- projects: (_g = json.data) !== null && _g !== void 0 ? _g : [],
47
+ clientName: (_e = (_d = json.client) === null || _d === void 0 ? void 0 : _d.name) !== null && _e !== void 0 ? _e : "Projects",
48
+ projects: ((_f = json.data) !== null && _f !== void 0 ? _f : []).filter((p) => p.is_published !== false),
53
49
  schema,
54
50
  fieldOptionsMap,
55
51
  };
@@ -52,7 +52,7 @@ export function ProjectPortfolioClient({ clientSlug, apiBase, basePath = "/proje
52
52
  fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`),
53
53
  ]);
54
54
  const json = projectsRes.ok ? await projectsRes.json() : {};
55
- const projects = (_a = json === null || json === void 0 ? void 0 : json.data) !== null && _a !== void 0 ? _a : [];
55
+ const projects = ((_a = json === null || json === void 0 ? void 0 : json.data) !== null && _a !== void 0 ? _a : []).filter((p) => p.is_published !== false);
56
56
  // Deduplicate schema keys
57
57
  const seen = new Set();
58
58
  const schema = ((_c = (_b = json === null || json === void 0 ? void 0 : json.client) === null || _b === void 0 ? void 0 : _b.custom_fields_schema) !== null && _c !== void 0 ? _c : []).filter((f) => {
@@ -1 +1 @@
1
- {"version":3,"file":"SimilarProjects.d.ts","sourceRoot":"","sources":["../src/SimilarProjects.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AA6DD,wBAAsB,eAAe,CAAC,EACpC,OAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,QAAY,EACZ,UAAe,EACf,OAAgB,EAChB,YAAY,GACb,EAAE,oBAAoB,2DA8JtB"}
1
+ {"version":3,"file":"SimilarProjects.d.ts","sourceRoot":"","sources":["../src/SimilarProjects.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACzB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;CACxB;AA0DD,wBAAsB,eAAe,CAAC,EACpC,OAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,QAAY,EACZ,UAAe,EACf,OAAgB,EAChB,YAAY,GACb,EAAE,oBAAoB,2DA+JtB"}
@@ -20,22 +20,19 @@ function dedupeByKey(arr) {
20
20
  }
21
21
  // ─── Data fetching ────────────────────────────────────────────────────────────
22
22
  const fetchSimilarData = cache(async (apiBase, clientSlug, revalidate) => {
23
- var _a, _b, _c, _d, _e;
23
+ var _a, _b, _c, _d;
24
24
  const fetchOpts = revalidate > 0 ? { next: { revalidate } } : {};
25
25
  const API_KEY = "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR";
26
26
  try {
27
- const [res, fieldsRes] = await Promise.all([
28
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
29
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`, fetchOpts),
30
- ]);
27
+ // Single call /projects returns projects AND client.custom_fields_schema with full options.
28
+ const res = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts);
31
29
  const json = res.ok ? await res.json() : null;
32
- const allProjects = (_a = json === null || json === void 0 ? void 0 : json.data) !== null && _a !== void 0 ? _a : [];
30
+ const allProjects = ((_a = json === null || json === void 0 ? void 0 : json.data) !== null && _a !== void 0 ? _a : []).filter((p) => p.is_published !== false);
33
31
  const schema = dedupeByKey((_c = (_b = json === null || json === void 0 ? void 0 : json.client) === null || _b === void 0 ? void 0 : _b.custom_fields_schema) !== null && _c !== void 0 ? _c : []);
34
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
35
32
  const fieldOptionsMap = {};
36
- for (const field of ((_d = fieldsJson.fields) !== null && _d !== void 0 ? _d : [])) {
33
+ for (const field of schema) {
37
34
  const map = {};
38
- for (const opt of ((_e = field.options) !== null && _e !== void 0 ? _e : [])) {
35
+ for (const opt of ((_d = field.options) !== null && _d !== void 0 ? _d : [])) {
39
36
  if (typeof opt === "object" && opt.id && opt.label) {
40
37
  map[opt.id] = opt.label;
41
38
  map[opt.label] = opt.label;
@@ -45,7 +42,7 @@ const fetchSimilarData = cache(async (apiBase, clientSlug, revalidate) => {
45
42
  }
46
43
  return { allProjects, schema, fieldOptionsMap };
47
44
  }
48
- catch (_f) {
45
+ catch (_e) {
49
46
  return { allProjects: [], schema: [], fieldOptionsMap: {} };
50
47
  }
51
48
  });
@@ -82,28 +79,27 @@ export async function SimilarProjects({ filters = {}, excludeSlug, clientSlug, a
82
79
  if (similar.length === 0)
83
80
  return null;
84
81
  const header = (_jsxs("div", { className: "chisel-similar-header", children: [_jsxs("div", { children: [_jsx("p", { style: { fontSize: "11px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: "oklch(0.78 0.16 85)", margin: "0 0 6px 0" }, children: "More Work" }), _jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "clamp(20px, 4vw, 28px)", margin: 0, fontFamily: font }, children: "Similar Projects" })] }), _jsxs("a", { href: basePath, style: { color: "#18181b", fontWeight: 600, fontSize: "15px", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "6px", fontFamily: font, flexShrink: 0 }, children: ["View All", _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M9 18l6-6-6-6" }) })] })] }));
82
+ // Shared styles injected regardless of variant so .chisel-project-card-img is always defined
83
+ const sharedStyles = (_jsx("style", { children: `
84
+ .chisel-similar-header { display: flex; flex-direction: column; gap: 12px; margin-bottom: 2rem; }
85
+ @media (min-width: 640px) { .chisel-similar-header { flex-direction: row; align-items: flex-end; justify-content: space-between; } }
86
+ .chisel-similar-card-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
87
+ @media (min-width: 640px) { .chisel-similar-card-grid { grid-template-columns: repeat(2, 1fr); } }
88
+ @media (min-width: 1024px) { .chisel-similar-card-grid { grid-template-columns: repeat(3, 1fr); } }
89
+ .chisel-project-card-img { height: 180px; }
90
+ @media (min-width: 640px) { .chisel-project-card-img { height: 200px; } }
91
+ @media (min-width: 1024px) { .chisel-project-card-img { height: 220px; } }
92
+ .chisel-similar-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
93
+ @media (min-width: 640px) { .chisel-similar-grid { grid-template-columns: repeat(2, 1fr); } }
94
+ @media (min-width: 1024px) { .chisel-similar-grid { grid-template-columns: repeat(3, 1fr); } }
95
+ .chisel-similar-img { height: 56vw; min-height: 160px; max-height: 220px; }
96
+ ` }));
85
97
  // ── Card variant ─────────────────────────────────────────────────────────────
86
98
  if (variant === "card") {
87
- return (_jsxs("section", { style: { borderTop: "1px solid #e4e4e7", maxWidth: "1280px", margin: "0 auto", padding: "3rem 1rem 2rem", boxSizing: "border-box", fontFamily: font }, children: [_jsx("style", { children: `
88
- .chisel-similar-header { display: flex; flex-direction: column; gap: 12px; margin-bottom: 2rem; }
89
- @media (min-width: 640px) { .chisel-similar-header { flex-direction: row; align-items: flex-end; justify-content: space-between; } }
90
- .chisel-similar-card-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
91
- @media (min-width: 640px) { .chisel-similar-card-grid { grid-template-columns: repeat(2, 1fr); } }
92
- @media (min-width: 1024px) { .chisel-similar-card-grid { grid-template-columns: repeat(3, 1fr); } }
93
- .chisel-project-card-img { height: 180px; }
94
- @media (min-width: 640px) { .chisel-project-card-img { height: 200px; } }
95
- @media (min-width: 1024px) { .chisel-project-card-img { height: 220px; } }
96
- ` }), header, _jsx("div", { className: "chisel-similar-card-grid", children: similar.map((p, i) => (_jsx(ProjectCard, { project: p, schema: schema, fieldOptionsMap: fieldOptionsMap, basePath: basePath, priority: i === 0, variant: "card" }, p.id))) })] }));
99
+ return (_jsxs("section", { style: { borderTop: "1px solid #e4e4e7", maxWidth: "1280px", margin: "0 auto", padding: "3rem 1rem 2rem", boxSizing: "border-box", fontFamily: font }, children: [sharedStyles, header, _jsx("div", { className: "chisel-similar-card-grid", children: similar.map((p, i) => (_jsx(ProjectCard, { project: p, schema: schema, fieldOptionsMap: fieldOptionsMap, basePath: basePath, priority: i === 0, variant: "card" }, p.id))) })] }));
97
100
  }
98
101
  // ── List variant (default) ────────────────────────────────────────────────────
99
- return (_jsxs("section", { style: { borderTop: "1px solid #e4e4e7", maxWidth: "1280px", margin: "0 auto", padding: "3rem 1rem 2rem", boxSizing: "border-box", fontFamily: font }, children: [_jsx("style", { children: `
100
- .chisel-similar-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
101
- @media (min-width: 640px) { .chisel-similar-grid { grid-template-columns: repeat(2, 1fr); } }
102
- @media (min-width: 1024px) { .chisel-similar-grid { grid-template-columns: repeat(3, 1fr); } }
103
- .chisel-similar-header { display: flex; flex-direction: column; gap: 12px; margin-bottom: 2rem; }
104
- @media (min-width: 640px) { .chisel-similar-header { flex-direction: row; align-items: flex-end; justify-content: space-between; } }
105
- .chisel-similar-img { height: 56vw; min-height: 160px; max-height: 220px; }
106
- ` }), header, _jsx("div", { className: "chisel-similar-grid", children: similar.map((p) => {
102
+ return (_jsxs("section", { style: { borderTop: "1px solid #e4e4e7", maxWidth: "1280px", margin: "0 auto", padding: "3rem 1rem 2rem", boxSizing: "border-box", fontFamily: font }, children: [sharedStyles, header, _jsx("div", { className: "chisel-similar-grid", children: similar.map((p) => {
107
103
  var _a, _b, _c, _d, _e;
108
104
  const imgUrl = (_d = (_a = p.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = p.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : null;
109
105
  const badge = badgeField
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-portfolio",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectPortfolioClient (with built-in filtering), ProjectDetail, SimilarProjects, ProjectMenu, ProjectMenuClient, and GalleryCarousel. Pass a clientSlug and apiBase — done.",
5
5
  "keywords": ["nextjs", "react", "portfolio", "projects", "megamenu", "gallery", "filtering"],
6
6
  "license": "MIT",