project-portfolio 2.2.0 → 2.2.2

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,8 @@ 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. |
589
+ | `noCache` | `boolean` | No | `false` | Bypasses the module-level data cache and sets `cache: "no-store"` on all fetch calls. Useful during development or when projects update frequently. |
600
590
  | `basePath` | `string` | Yes | — | Base path for project detail links |
601
591
  | `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
602
592
  | `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
  });
@@ -15,10 +15,16 @@ export interface ProjectMenuClientProps {
15
15
  clientSlug?: string;
16
16
  apiBase?: string;
17
17
  /**
18
- * Optional menu ID to fetch a specific curated menu instead of all projects.
19
- * When provided, fetches from /api/v1/clients/{clientSlug}/menus/{menuId}
18
+ * Optional menu slug to fetch a specific curated menu instead of all projects.
19
+ * When provided, fetches from /api/v1/clients/{clientSlug}/menus/{slug}
20
20
  */
21
21
  menuId?: string;
22
+ /**
23
+ * When true, bypasses the module-level data cache and always fetches fresh data.
24
+ * Also sets fetch cache to "no-store" on all underlying requests.
25
+ * Useful during development or when projects update frequently.
26
+ */
27
+ noCache?: boolean;
22
28
  projects?: Project[];
23
29
  schema?: CustomFieldSchema[];
24
30
  filterOptions?: {
@@ -34,5 +40,5 @@ export interface ProjectMenuClientProps {
34
40
  font?: string;
35
41
  maxProjects?: number;
36
42
  }
37
- export declare function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projects: projectsProp, schema: schemaProp, filterOptions: filterOptionsProp, filterFieldKey: filterFieldKeyProp, filterFieldName: filterFieldNameProp, fieldOptionsMap: fieldOptionsMapProp, subtitle, basePath, viewAllPath, font, maxProjects, }: ProjectMenuClientProps): import("react/jsx-runtime").JSX.Element;
43
+ export declare function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, noCache, projects: projectsProp, schema: schemaProp, filterOptions: filterOptionsProp, filterFieldKey: filterFieldKeyProp, filterFieldName: filterFieldNameProp, fieldOptionsMap: fieldOptionsMapProp, subtitle, basePath, viewAllPath, font, maxProjects, }: ProjectMenuClientProps): import("react/jsx-runtime").JSX.Element;
38
44
  //# sourceMappingURL=ProjectMenuClient.d.ts.map
@@ -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,2CA4fxB"}
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;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB,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,OAAe,EACf,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,2CAqfxB"}
@@ -17,25 +17,27 @@ const ACCENT = "oklch(0.78 0.16 85)";
17
17
  const API_KEY = "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR";
18
18
  const DEFAULT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
19
19
  const menuDataCache = new Map();
20
- export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projects: projectsProp, schema: schemaProp, filterOptions: filterOptionsProp, filterFieldKey: filterFieldKeyProp, filterFieldName: filterFieldNameProp = "Project Type", fieldOptionsMap: fieldOptionsMapProp = {}, subtitle, basePath, viewAllPath, font = DEFAULT_FONT, maxProjects = 6, }) {
20
+ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, noCache = false, projects: projectsProp, schema: schemaProp, filterOptions: filterOptionsProp, filterFieldKey: filterFieldKeyProp, filterFieldName: filterFieldNameProp = "Project Type", fieldOptionsMap: fieldOptionsMapProp = {}, subtitle, basePath, viewAllPath, font = DEFAULT_FONT, maxProjects = 6, }) {
21
21
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
22
22
  const [filtersOpen, setFiltersOpen] = useState(false);
23
23
  const [hoveredCard, setHoveredCard] = useState(null);
24
24
  const [fetched, setFetched] = useState(null);
25
25
  // Self-fetch mode: fires when dataUrl OR (clientSlug + apiBase) are provided.
26
26
  // Uses a module-level Promise cache so repeated mounts never re-hit the API.
27
+ // Pass noCache=true to bypass the module cache and always fetch fresh data.
27
28
  useEffect(() => {
28
29
  const hasDataUrl = !!dataUrl;
29
30
  const hasDirectFetch = !!(clientSlug && apiBase);
30
31
  if (!hasDataUrl && !hasDirectFetch)
31
32
  return;
32
33
  let cancelled = false;
34
+ const fetchOpts = noCache ? { cache: "no-store" } : {};
33
35
  const cacheKey = dataUrl !== null && dataUrl !== void 0 ? dataUrl : `${clientSlug}:${apiBase}:${menuId !== null && menuId !== void 0 ? menuId : "all"}`;
34
36
  async function fetchAndCache() {
35
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3;
37
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
36
38
  // dataUrl mode: fetch from local API route (server-cached, no API key exposed)
37
39
  if (dataUrl) {
38
- const res = await fetch(dataUrl);
40
+ const res = await fetch(dataUrl, fetchOpts);
39
41
  const json = res.ok ? await res.json() : {};
40
42
  return {
41
43
  projects: (_a = json.projects) !== null && _a !== void 0 ? _a : [],
@@ -46,24 +48,20 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
46
48
  fieldOptionsMap: (_f = json.fieldOptionsMap) !== null && _f !== void 0 ? _f : {},
47
49
  };
48
50
  }
49
- // Menu endpoint mode: fetch from /menus/{menuId} when menuId is provided
50
- // Also fetch /projects to get schema for filters (menu endpoint may not return it)
51
+ // Menu endpoint mode: /menus/{slug} for projects, /projects for schema+options.
51
52
  if (menuId) {
52
- const [menuRes, projectsRes, fieldsRes] = await Promise.all([
53
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`),
54
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`),
55
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`),
53
+ const [menuRes, projectsRes] = await Promise.all([
54
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/menus/${menuId}?api_key=${API_KEY}`, fetchOpts),
55
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
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 : []).filter((p) => p.is_published !== false);
60
- // Get schema from menu response first, fallback to projects response
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
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
59
+ const projects = ((_g = menuJson === null || menuJson === void 0 ? void 0 : menuJson.projects) !== null && _g !== void 0 ? _g : []).filter((p) => p.is_published !== false);
60
+ const schema = (_j = (_h = projectsJson === null || projectsJson === void 0 ? void 0 : projectsJson.client) === null || _h === void 0 ? void 0 : _h.custom_fields_schema) !== null && _j !== void 0 ? _j : [];
63
61
  const fieldOptionsMap = {};
64
- for (const field of ((_p = fieldsJson.fields) !== null && _p !== void 0 ? _p : [])) {
62
+ for (const field of schema) {
65
63
  const map = {};
66
- for (const opt of ((_q = field.options) !== null && _q !== void 0 ? _q : [])) {
64
+ for (const opt of ((_k = field.options) !== null && _k !== void 0 ? _k : [])) {
67
65
  if (typeof opt === "object" && opt.id && opt.label) {
68
66
  map[opt.id] = opt.label;
69
67
  map[opt.label] = opt.label;
@@ -71,9 +69,9 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
71
69
  }
72
70
  fieldOptionsMap[field.key] = map;
73
71
  }
74
- const filterField = (_r = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _r !== void 0 ? _r : null;
72
+ const filterField = (_l = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _l !== void 0 ? _l : null;
75
73
  const filterOptions = filterField
76
- ? ((_s = filterField.options) !== null && _s !== void 0 ? _s : []).map((opt) => {
74
+ ? ((_m = filterField.options) !== null && _m !== void 0 ? _m : []).map((opt) => {
77
75
  var _a, _b;
78
76
  if (typeof opt === "string")
79
77
  return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
@@ -84,24 +82,20 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
84
82
  projects,
85
83
  schema,
86
84
  filterOptions,
87
- filterFieldKey: (_t = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _t !== void 0 ? _t : null,
88
- filterFieldName: (_u = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _u !== void 0 ? _u : "Project Type",
85
+ filterFieldKey: (_o = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _o !== void 0 ? _o : null,
86
+ filterFieldName: (_p = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _p !== void 0 ? _p : "Project Type",
89
87
  fieldOptionsMap,
90
88
  };
91
89
  }
92
- // Direct fetch mode: fetch from the upstream API directly (all projects)
93
- const [projectsRes, fieldsRes] = await Promise.all([
94
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`),
95
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`),
96
- ]);
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 : []).filter((p) => p.is_published !== false);
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
- const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
90
+ // Direct fetch mode single call, /projects returns everything.
91
+ const res = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts);
92
+ const json = res.ok ? await res.json() : {};
93
+ const projects = ((_q = json === null || json === void 0 ? void 0 : json.data) !== null && _q !== void 0 ? _q : []).filter((p) => p.is_published !== false);
94
+ const schema = (_s = (_r = json === null || json === void 0 ? void 0 : json.client) === null || _r === void 0 ? void 0 : _r.custom_fields_schema) !== null && _s !== void 0 ? _s : [];
101
95
  const fieldOptionsMap = {};
102
- for (const field of ((_y = fieldsJson.fields) !== null && _y !== void 0 ? _y : [])) {
96
+ for (const field of schema) {
103
97
  const map = {};
104
- for (const opt of ((_z = field.options) !== null && _z !== void 0 ? _z : [])) {
98
+ for (const opt of ((_t = field.options) !== null && _t !== void 0 ? _t : [])) {
105
99
  if (typeof opt === "object" && opt.id && opt.label) {
106
100
  map[opt.id] = opt.label;
107
101
  map[opt.label] = opt.label;
@@ -109,9 +103,9 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
109
103
  }
110
104
  fieldOptionsMap[field.key] = map;
111
105
  }
112
- const filterField = (_0 = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _0 !== void 0 ? _0 : null;
106
+ const filterField = (_u = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _u !== void 0 ? _u : null;
113
107
  const filterOptions = filterField
114
- ? ((_1 = filterField.options) !== null && _1 !== void 0 ? _1 : []).map((opt) => {
108
+ ? ((_v = filterField.options) !== null && _v !== void 0 ? _v : []).map((opt) => {
115
109
  var _a, _b;
116
110
  if (typeof opt === "string")
117
111
  return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
@@ -122,23 +116,26 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, menuId, projec
122
116
  projects,
123
117
  schema,
124
118
  filterOptions,
125
- filterFieldKey: (_2 = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _2 !== void 0 ? _2 : null,
126
- filterFieldName: (_3 = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _3 !== void 0 ? _3 : "Project Type",
119
+ filterFieldKey: (_w = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _w !== void 0 ? _w : null,
120
+ filterFieldName: (_x = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _x !== void 0 ? _x : "Project Type",
127
121
  fieldOptionsMap,
128
122
  };
129
123
  }
130
- // Store the Promise on first call reuse it on every subsequent mount
131
- if (!menuDataCache.has(cacheKey)) {
132
- menuDataCache.set(cacheKey, fetchAndCache());
133
- }
134
- menuDataCache.get(cacheKey).then((data) => {
124
+ // When noCache=true, skip the module cache entirely and always fetch fresh.
125
+ // Otherwise store the Promise on first call and reuse it on every subsequent mount.
126
+ const getOrFetch = noCache
127
+ ? fetchAndCache()
128
+ : (menuDataCache.has(cacheKey)
129
+ ? menuDataCache.get(cacheKey)
130
+ : (() => { const p = fetchAndCache(); menuDataCache.set(cacheKey, p); return p; })());
131
+ getOrFetch.then((data) => {
135
132
  if (!cancelled)
136
133
  setFetched(data);
137
134
  }).catch(() => {
138
135
  // silently fail — render nothing
139
136
  });
140
137
  return () => { cancelled = true; };
141
- }, [dataUrl, clientSlug, apiBase, menuId]);
138
+ }, [dataUrl, clientSlug, apiBase, menuId, noCache]);
142
139
  // Resolve data: prefer self-fetched, fall back to props
143
140
  const projects = (_b = (_a = fetched === null || fetched === void 0 ? void 0 : fetched.projects) !== null && _a !== void 0 ? _a : projectsProp) !== null && _b !== void 0 ? _b : [];
144
141
  const schema = (_d = (_c = fetched === null || fetched === void 0 ? void 0 : fetched.schema) !== null && _c !== void 0 ? _c : schemaProp) !== null && _d !== void 0 ? _d : [];
@@ -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,2DAiKtB"}
@@ -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,33 +79,34 @@ 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) => {
107
- var _a, _b, _c, _d, _e;
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) => {
103
+ var _a, _b, _c, _d, _e, _f, _g;
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
- const badge = badgeField
105
+ const badgeRaw = badgeField
110
106
  ? ((_e = parseMultiValue(p.custom_field_values[badgeField.key])[0]) !== null && _e !== void 0 ? _e : null)
111
107
  : null;
108
+ const badgeOptMap = badgeField ? ((_f = fieldOptionsMap[badgeField.key]) !== null && _f !== void 0 ? _f : {}) : {};
109
+ const badge = badgeRaw ? ((_g = badgeOptMap[badgeRaw]) !== null && _g !== void 0 ? _g : badgeRaw) : null;
112
110
  const loc = locationField
113
111
  ? p.custom_field_values[locationField.key]
114
112
  : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-portfolio",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",