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