project-portfolio 2.2.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
@@ -295,18 +295,7 @@ export function ProjectGallery({ media, title }: { media: Media[]; title: string
295
295
  }
296
296
  ```
297
297
 
298
- The parent component is responsible for injecting the `.chisel-gallery-main-img` CSS class to control the main image height:
299
-
300
- ```css
301
- .chisel-gallery-main-img {
302
- height: 400px; /* adjust to your layout */
303
- }
304
- @media (min-width: 768px) {
305
- .chisel-gallery-main-img {
306
- height: 560px;
307
- }
308
- }
309
- ```
298
+ The main image and thumbnails are standardised to a `16/9` aspect ratio no external CSS required.
310
299
 
311
300
  | Prop | Type | Required | Default | Description |
312
301
  |---|---|---|---|---|
@@ -476,7 +465,7 @@ export async function ProjectsMegaMenu() {
476
465
 
477
466
  #### With a curated menu
478
467
 
479
- Pass `menuId` to show a specific curated set of projects instead of all projects. The Browse By filters on the right always reflect the full schema regardless of which menu is active. Available menu IDs can be retrieved from `GET /api/v1/clients/{clientSlug}/menus`.
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.
480
469
 
481
470
  ```tsx
482
471
  <ProjectMenu
@@ -491,7 +480,7 @@ Pass `menuId` to show a specific curated set of projects instead of all projects
491
480
  |---|---|---|---|---|
492
481
  | `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
493
482
  | `apiBase` | `string` | Yes | — | Base URL of the projects API |
494
- | `menuId` | `string` | No | — | ID of a curated menu. When provided, fetches from `/menus/{menuId}` instead of all projects. Filters always shown regardless. |
483
+ | `menuId` | `string` | No | — | Slug of a curated menu. When provided, fetches from `/menus/{slug}` instead of all projects. Filters always shown regardless. |
495
484
  | `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
496
485
  | `viewAllPath` | `string` | No | Same as `basePath` | Path for the "View All Projects" link |
497
486
  | `subtitle` | `string` | No | — | Description paragraph shown above the project cards |
@@ -596,7 +585,7 @@ With a curated menu:
596
585
  | `dataUrl` | `string` | No* | — | URL of a local API route created with `createMenuHandler()`. Recommended for production |
597
586
  | `clientSlug` | `string` | No* | — | Client slug for direct fetch mode |
598
587
  | `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
599
- | `menuId` | `string` | No | — | ID of a curated menu. Fetches from `/menus/{menuId}` for projects. Filters are always shown regardless. |
588
+ | `menuId` | `string` | No | — | Slug of a curated menu. Fetches from `/menus/{slug}` for projects. Filters are always shown regardless. |
600
589
  | `basePath` | `string` | Yes | — | Base path for project detail links |
601
590
  | `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
602
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,2CAqQlB"}
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,7 +13,7 @@ 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
@@ -22,9 +22,11 @@ export function ProjectCard({ project, schema, fieldOptionsMap = {}, priority, v
22
22
  // Coverage/sq ft field for footer stat — matched by key or name regardless of type
23
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;
@@ -47,7 +49,14 @@ export function ProjectCard({ project, schema, fieldOptionsMap = {}, priority, v
47
49
  : String(val);
48
50
  return { label, formatted };
49
51
  })();
50
- 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
+ });
51
60
  const href = `${basePath}/${project.slug}`;
52
61
  if (variant === "compact") {
53
62
  return (_jsxs("a", { href: href, style: {
@@ -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,oDAwPpB"}
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;
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,29 +63,36 @@ 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
- const badgeValue = badgeField
66
+ const badgeRaw = badgeField
53
67
  ? ((_h = parseMultiValue(project.custom_field_values[badgeField.key])[0]) !== null && _h !== void 0 ? _h : null)
54
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;
55
71
  const locationValue = locationField
56
72
  ? project.custom_field_values[locationField.key]
57
73
  : null;
58
74
  const locationString = locationValue
59
75
  ? [locationValue.city, locationValue.state].filter(Boolean).join(", ")
60
76
  : null;
61
- // Helper: format a raw field value to a display string
77
+ // Helper: format a raw field value to a display string, resolving slugs via fieldOptionsMap
62
78
  function formatFieldValue(field, raw) {
79
+ var _a, _b;
63
80
  if (raw === null || raw === undefined || raw === "")
64
81
  return null;
82
+ const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
65
83
  if (typeof raw === "string")
66
- return raw;
84
+ return (_b = optMap[raw]) !== null && _b !== void 0 ? _b : raw;
67
85
  if (typeof raw === "number") {
68
86
  if (/year/i.test(field.key + field.name))
69
87
  return String(Math.round(raw));
70
88
  return Math.round(raw).toLocaleString();
71
89
  }
72
90
  if (Array.isArray(raw)) {
91
+ const hasOptions = Object.keys(optMap).length > 0;
73
92
  const vals = raw.map(String).filter(Boolean);
74
- return vals.length > 0 ? vals.join(", ") : null;
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;
75
96
  }
76
97
  return null;
77
98
  }
@@ -122,14 +143,21 @@ export async function ProjectDetail({ slug, clientSlug, apiBase, backPath = "/pr
122
143
  .chisel-overview-grid { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
123
144
  @media (min-width: 1024px) { .chisel-overview-grid { grid-template-columns: 1fr 300px; gap: 4rem; align-items: start; } }
124
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;
125
147
  const raw = project.custom_field_values[field.key];
126
- const vals = Array.isArray(raw)
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)
127
151
  ? raw.map(String).filter(Boolean)
128
152
  : typeof raw === "string"
129
153
  ? [raw]
130
154
  : raw !== null && raw !== undefined
131
155
  ? [String(raw)]
132
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;
133
161
  if (vals.length === 0)
134
162
  return null;
135
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: {
@@ -145,5 +173,5 @@ export async function ProjectDetail({ slug, clientSlug, apiBase, backPath = "/pr
145
173
  }, children: val }, val))) })] }, field.key));
146
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 })) : (
147
175
  /* Placeholder */
148
- _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))) }))] })] })] }));
149
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,uBAwE3C;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;AAgED,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,33 +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
- // Filter out archived (is_published === false) projects in both cases
55
- const rawProjects = menuJson
56
- ? ((_b = (_a = menuJson.projects) !== null && _a !== void 0 ? _a : menuJson.data) !== null && _b !== void 0 ? _b : [])
57
- : ((_c = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.data) !== null && _c !== void 0 ? _c : []);
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 : []);
58
54
  const projects = rawProjects.filter((p) => p.is_published !== false);
59
- // Always get schema from /projects response
60
- 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 : [];
61
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
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 : [];
62
56
  const fieldOptionsMap = {};
63
- for (const field of ((_f = fieldsJson.fields) !== null && _f !== void 0 ? _f : [])) {
57
+ for (const field of schema) {
64
58
  const map = {};
65
- 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 : [])) {
66
60
  if (typeof opt === "object" && opt.id && opt.label) {
67
61
  map[opt.id] = opt.label;
68
62
  map[opt.label] = opt.label;
@@ -70,9 +64,9 @@ export function createMenuHandler({ clientSlug, apiBase, menuId, revalidate = 86
70
64
  }
71
65
  fieldOptionsMap[field.key] = map;
72
66
  }
73
- 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;
74
68
  const filterOptions = filterField
75
- ? ((_j = filterField.options) !== null && _j !== void 0 ? _j : []).map((opt) => {
69
+ ? ((_g = filterField.options) !== null && _g !== void 0 ? _g : []).map((opt) => {
76
70
  var _a, _b;
77
71
  if (typeof opt === "string")
78
72
  return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
@@ -83,12 +77,12 @@ export function createMenuHandler({ clientSlug, apiBase, menuId, revalidate = 86
83
77
  projects,
84
78
  schema,
85
79
  filterOptions,
86
- filterFieldKey: (_k = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _k !== void 0 ? _k : null,
87
- 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",
88
82
  fieldOptionsMap,
89
83
  });
90
84
  }
91
- catch (_m) {
85
+ catch (_k) {
92
86
  return Response.json({ projects: [], schema: [], filterOptions: [], filterFieldKey: null, filterFieldName: "Project Type", fieldOptionsMap: {} }, { status: 500 });
93
87
  }
94
88
  };
@@ -116,41 +110,34 @@ export async function fetchProjectMenuData({ apiBase, clientSlug, menuId, revali
116
110
  };
117
111
  }
118
112
  const _fetchMenuData = cache(async (apiBase, clientSlug, menuId, revalidate, noCache = false) => {
119
- var _a, _b, _c, _d, _e, _f, _g;
113
+ var _a, _b, _c, _d, _e;
120
114
  const fetchOpts = noCache
121
115
  ? { cache: "no-store" }
122
116
  : { next: { revalidate } };
123
117
  try {
124
- // Always fetch /projects for schema, and /fields for options
125
- // If menuId is provided, also fetch the menu endpoint for curated projects
126
- const fetches = [
127
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
128
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`, fetchOpts),
129
- ];
130
- if (menuId) {
131
- fetches.push(fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`, fetchOpts));
132
- }
133
- const responses = await Promise.all(fetches);
134
- const [projectsRes, fieldsRes] = responses;
135
- const menuRes = menuId ? responses[2] : null;
136
- 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)
137
129
  return { projects: [], schema: [], fieldOptionsMap: {} };
138
- const projectsJson = await projectsRes.json();
139
- const menuJson = menuRes && menuRes.ok ? await menuRes.json() : null;
140
- // If menuId provided, use menu projects; otherwise use all projects
141
- // Filter out archived (is_published === false) projects in both cases
142
- const rawProjects = menuJson
143
- ? ((_b = (_a = menuJson.projects) !== null && _a !== void 0 ? _a : menuJson.data) !== null && _b !== void 0 ? _b : [])
144
- : ((_c = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.data) !== null && _c !== void 0 ? _c : []);
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 : []);
145
135
  const projects = rawProjects.filter((p) => p.is_published !== false);
146
- // Always get schema from /projects response
147
- 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 : [];
148
- // Build fieldOptionsMap: { fieldKey: { id: label, label: label } }
149
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
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 : [];
150
137
  const fieldOptionsMap = {};
151
- for (const field of ((_f = fieldsJson.fields) !== null && _f !== void 0 ? _f : [])) {
138
+ for (const field of schema) {
152
139
  const map = {};
153
- 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 : [])) {
154
141
  if (typeof opt === "object" && opt.id && opt.label) {
155
142
  map[opt.id] = opt.label;
156
143
  map[opt.label] = opt.label;
@@ -160,7 +147,7 @@ const _fetchMenuData = cache(async (apiBase, clientSlug, menuId, revalidate, noC
160
147
  }
161
148
  return { projects, schema, fieldOptionsMap };
162
149
  }
163
- catch (_h) {
150
+ catch (_f) {
164
151
  return { projects: [], schema: [], fieldOptionsMap: {} };
165
152
  }
166
153
  });
@@ -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 : []).filter((p) => p.is_published !== false),
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
  };
@@ -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
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.2.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",