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 +5 -15
- package/dist/GalleryCarousel.js +3 -3
- package/dist/ProjectCard.d.ts.map +1 -1
- package/dist/ProjectCard.js +12 -3
- package/dist/ProjectDetail.d.ts.map +1 -1
- package/dist/ProjectDetail.js +40 -12
- package/dist/ProjectMenu.d.ts.map +1 -1
- package/dist/ProjectMenu.js +45 -58
- package/dist/ProjectMenuClient.d.ts +9 -3
- package/dist/ProjectMenuClient.d.ts.map +1 -1
- package/dist/ProjectMenuClient.js +37 -40
- package/dist/ProjectPortfolio.d.ts.map +1 -1
- package/dist/ProjectPortfolio.js +8 -12
- package/dist/SimilarProjects.d.ts.map +1 -1
- package/dist/SimilarProjects.js +27 -29
- package/package.json +1 -1
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
|
|
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
|
|
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 | — |
|
|
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 | — |
|
|
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) |
|
package/dist/GalleryCarousel.js
CHANGED
|
@@ -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", {
|
|
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: "
|
|
72
|
-
|
|
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,
|
|
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"}
|
package/dist/ProjectCard.js
CHANGED
|
@@ -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
|
|
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) =>
|
|
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;
|
|
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"}
|
package/dist/ProjectDetail.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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,
|
|
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"}
|
package/dist/ProjectMenu.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
57
|
+
for (const field of schema) {
|
|
64
58
|
const map = {};
|
|
65
|
-
for (const opt of ((
|
|
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 = (
|
|
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
|
-
? ((
|
|
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: (
|
|
87
|
-
filterFieldName: (
|
|
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 (
|
|
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
|
|
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
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
|
|
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
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
|
138
|
+
for (const field of schema) {
|
|
152
139
|
const map = {};
|
|
153
|
-
for (const opt of ((
|
|
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 (
|
|
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
|
|
19
|
-
* When provided, fetches from /api/v1/clients/{clientSlug}/menus/{
|
|
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;
|
|
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
|
|
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:
|
|
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
|
|
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 = ((
|
|
60
|
-
|
|
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
|
|
62
|
+
for (const field of schema) {
|
|
65
63
|
const map = {};
|
|
66
|
-
for (const opt of ((
|
|
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 = (
|
|
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
|
-
? ((
|
|
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: (
|
|
88
|
-
filterFieldName: (
|
|
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
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
96
|
+
for (const field of schema) {
|
|
103
97
|
const map = {};
|
|
104
|
-
for (const opt of ((
|
|
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 = (
|
|
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
|
-
? ((
|
|
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: (
|
|
126
|
-
filterFieldName: (
|
|
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
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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;
|
|
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"}
|
package/dist/ProjectPortfolio.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
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
|
|
36
|
+
for (const field of schema) {
|
|
41
37
|
const map = {};
|
|
42
|
-
for (const opt of ((
|
|
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: (
|
|
52
|
-
projects: ((
|
|
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;
|
|
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"}
|
package/dist/SimilarProjects.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
28
|
-
|
|
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
|
|
33
|
+
for (const field of schema) {
|
|
37
34
|
const map = {};
|
|
38
|
-
for (const opt of ((
|
|
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 (
|
|
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("
|
|
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("
|
|
100
|
-
|
|
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
|
|
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.
|
|
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",
|