@chiselandco/nexus 2.2.6

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.
@@ -0,0 +1,386 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useState, useEffect } from "react";
4
+ function parseSingleValue(raw) {
5
+ if (typeof raw === "string")
6
+ return raw.replace(/`/g, "").trim();
7
+ return String(raw !== null && raw !== void 0 ? raw : "");
8
+ }
9
+ function parseMultiValue(raw) {
10
+ if (Array.isArray(raw))
11
+ return raw.map(String).map((s) => s.replace(/`/g, "").trim()).filter(Boolean);
12
+ if (typeof raw === "string")
13
+ return raw.replace(/`/g, "").split(",").map((s) => s.trim()).filter(Boolean);
14
+ return [];
15
+ }
16
+ const ACCENT = "oklch(0.78 0.16 85)";
17
+ const API_KEY = "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR";
18
+ const DEFAULT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
19
+ const menuDataCache = new Map();
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
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
22
+ const [filtersOpen, setFiltersOpen] = useState(false);
23
+ const [hoveredCard, setHoveredCard] = useState(null);
24
+ const [fetched, setFetched] = useState(null);
25
+ // Self-fetch mode: fires when dataUrl OR (clientSlug + apiBase) are provided.
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.
28
+ useEffect(() => {
29
+ const hasDataUrl = !!dataUrl;
30
+ const hasDirectFetch = !!(clientSlug && apiBase);
31
+ if (!hasDataUrl && !hasDirectFetch)
32
+ return;
33
+ let cancelled = false;
34
+ const fetchOpts = noCache ? { cache: "no-store" } : {};
35
+ const cacheKey = dataUrl !== null && dataUrl !== void 0 ? dataUrl : `${clientSlug}:${apiBase}:${menuId !== null && menuId !== void 0 ? menuId : "all"}`;
36
+ async function fetchAndCache() {
37
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
38
+ // dataUrl mode: fetch from local API route (server-cached, no API key exposed)
39
+ if (dataUrl) {
40
+ const res = await fetch(dataUrl, fetchOpts);
41
+ const json = res.ok ? await res.json() : {};
42
+ return {
43
+ projects: (_a = json.projects) !== null && _a !== void 0 ? _a : [],
44
+ schema: (_b = json.schema) !== null && _b !== void 0 ? _b : [],
45
+ filterOptions: (_c = json.filterOptions) !== null && _c !== void 0 ? _c : [],
46
+ filterFieldKey: (_d = json.filterFieldKey) !== null && _d !== void 0 ? _d : null,
47
+ filterFieldName: (_e = json.filterFieldName) !== null && _e !== void 0 ? _e : "Project Type",
48
+ fieldOptionsMap: (_f = json.fieldOptionsMap) !== null && _f !== void 0 ? _f : {},
49
+ };
50
+ }
51
+ // Menu endpoint mode: /menus/{slug} for projects, /projects for schema+options.
52
+ if (menuId) {
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
+ ]);
57
+ const menuJson = menuRes.ok ? await menuRes.json() : {};
58
+ const projectsJson = projectsRes.ok ? await projectsRes.json() : {};
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 : [];
61
+ const fieldOptionsMap = {};
62
+ for (const field of schema) {
63
+ const map = {};
64
+ for (const opt of ((_k = field.options) !== null && _k !== void 0 ? _k : [])) {
65
+ if (typeof opt === "object" && opt.id && opt.label) {
66
+ map[opt.id] = opt.label;
67
+ map[opt.label] = opt.label;
68
+ }
69
+ }
70
+ fieldOptionsMap[field.key] = map;
71
+ }
72
+ const filterField = (_l = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _l !== void 0 ? _l : null;
73
+ const filterOptions = filterField
74
+ ? ((_m = filterField.options) !== null && _m !== void 0 ? _m : []).map((opt) => {
75
+ var _a, _b;
76
+ if (typeof opt === "string")
77
+ return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
78
+ return { id: (_a = opt.id) !== null && _a !== void 0 ? _a : "", label: (_b = opt.label) !== null && _b !== void 0 ? _b : "" };
79
+ })
80
+ : [];
81
+ return {
82
+ projects,
83
+ schema,
84
+ filterOptions,
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",
87
+ fieldOptionsMap,
88
+ };
89
+ }
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 : [];
95
+ const fieldOptionsMap = {};
96
+ for (const field of schema) {
97
+ const map = {};
98
+ for (const opt of ((_t = field.options) !== null && _t !== void 0 ? _t : [])) {
99
+ if (typeof opt === "object" && opt.id && opt.label) {
100
+ map[opt.id] = opt.label;
101
+ map[opt.label] = opt.label;
102
+ }
103
+ }
104
+ fieldOptionsMap[field.key] = map;
105
+ }
106
+ const filterField = (_u = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _u !== void 0 ? _u : null;
107
+ const filterOptions = filterField
108
+ ? ((_v = filterField.options) !== null && _v !== void 0 ? _v : []).map((opt) => {
109
+ var _a, _b;
110
+ if (typeof opt === "string")
111
+ return { id: opt.toLowerCase().replace(/\s+/g, "-"), label: opt };
112
+ return { id: (_a = opt.id) !== null && _a !== void 0 ? _a : "", label: (_b = opt.label) !== null && _b !== void 0 ? _b : "" };
113
+ })
114
+ : [];
115
+ return {
116
+ projects,
117
+ schema,
118
+ filterOptions,
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",
121
+ fieldOptionsMap,
122
+ };
123
+ }
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) => {
132
+ if (!cancelled)
133
+ setFetched(data);
134
+ }).catch(() => {
135
+ // silently fail — render nothing
136
+ });
137
+ return () => { cancelled = true; };
138
+ }, [dataUrl, clientSlug, apiBase, menuId, noCache]);
139
+ // Resolve data: prefer self-fetched, fall back to props
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 : [];
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 : [];
142
+ const filterOptions = (_f = (_e = fetched === null || fetched === void 0 ? void 0 : fetched.filterOptions) !== null && _e !== void 0 ? _e : filterOptionsProp) !== null && _f !== void 0 ? _f : [];
143
+ const filterFieldKey = (_h = (_g = fetched === null || fetched === void 0 ? void 0 : fetched.filterFieldKey) !== null && _g !== void 0 ? _g : filterFieldKeyProp) !== null && _h !== void 0 ? _h : null;
144
+ const filterFieldName = (_j = fetched === null || fetched === void 0 ? void 0 : fetched.filterFieldName) !== null && _j !== void 0 ? _j : filterFieldNameProp;
145
+ const fieldOptionsMap = (_k = fetched === null || fetched === void 0 ? void 0 : fetched.fieldOptionsMap) !== null && _k !== void 0 ? _k : fieldOptionsMapProp;
146
+ // Show a minimal skeleton while self-fetching
147
+ if ((dataUrl || (clientSlug && apiBase)) && !fetched) {
148
+ return (_jsx("div", { style: { padding: "2rem 2.5rem", fontFamily: font, color: "#a1a1aa", fontSize: "14px" }, children: "Loading projects..." }));
149
+ }
150
+ const displayed = projects.slice(0, maxProjects);
151
+ const badgeField = schema.find((f) => f.display_position === "badge_overlay");
152
+ const tagsField = schema.find((f) => f.display_position === "tags");
153
+ return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
154
+ .chisel-menu-outer {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: 0;
158
+ font-family: ${font};
159
+ width: 100%;
160
+ box-sizing: border-box;
161
+ padding: 1.25rem 1rem;
162
+ }
163
+ @media (min-width: 768px) {
164
+ .chisel-menu-outer {
165
+ flex-direction: row;
166
+ padding: 2rem 2.5rem;
167
+ }
168
+ }
169
+
170
+ /* ── Left column ── */
171
+ .chisel-menu-left {
172
+ flex: 1;
173
+ min-width: 0;
174
+ padding-right: 0;
175
+ padding-bottom: 1.25rem;
176
+ border-bottom: 1px solid #e4e4e7;
177
+ }
178
+ @media (min-width: 768px) {
179
+ .chisel-menu-left {
180
+ padding-right: 2.5rem;
181
+ padding-bottom: 0;
182
+ border-bottom: none;
183
+ }
184
+ }
185
+
186
+ /* ── Vertical divider — desktop only ── */
187
+ .chisel-menu-divider-v {
188
+ display: none;
189
+ }
190
+ @media (min-width: 768px) {
191
+ .chisel-menu-divider-v {
192
+ display: block;
193
+ width: 1px;
194
+ background-color: #e4e4e7;
195
+ flex-shrink: 0;
196
+ align-self: stretch;
197
+ }
198
+ }
199
+
200
+ /* ── Right column ── */
201
+ .chisel-menu-right {
202
+ width: 100%;
203
+ padding-left: 0;
204
+ padding-top: 1.25rem;
205
+ }
206
+ @media (min-width: 768px) {
207
+ .chisel-menu-right {
208
+ width: 220px;
209
+ flex-shrink: 0;
210
+ padding-left: 2.5rem;
211
+ padding-top: 0;
212
+ }
213
+ }
214
+ @media (min-width: 1024px) {
215
+ .chisel-menu-right {
216
+ width: 280px;
217
+ }
218
+ }
219
+
220
+ /* ── Project card grid ──
221
+ Mobile: 1 column — cards get full width, titles don't wrap
222
+ Desktop: 2 columns */
223
+ .chisel-menu-card-grid {
224
+ display: grid;
225
+ grid-template-columns: 1fr;
226
+ gap: 6px;
227
+ }
228
+ @media (min-width: 768px) {
229
+ .chisel-menu-card-grid {
230
+ grid-template-columns: repeat(2, 1fr);
231
+ gap: 10px;
232
+ }
233
+ }
234
+
235
+ /* Hide cards beyond the 3rd on mobile */
236
+ @media (max-width: 767px) {
237
+ .chisel-menu-card:nth-child(n+4) {
238
+ display: none;
239
+ }
240
+ }
241
+
242
+ /* Card — always horizontal side-by-side */
243
+ .chisel-menu-card {
244
+ display: flex;
245
+ flex-direction: row;
246
+ align-items: center;
247
+ gap: 12px;
248
+ padding: 10px 12px;
249
+ border: 1px solid #e4e4e7;
250
+ text-decoration: none;
251
+ color: inherit;
252
+ box-sizing: border-box;
253
+ transition: border-color 0.2s;
254
+ cursor: pointer;
255
+ overflow: hidden;
256
+ }
257
+ .chisel-menu-card:hover {
258
+ border-color: ${ACCENT};
259
+ }
260
+
261
+ .chisel-menu-card-thumb {
262
+ width: 72px;
263
+ height: 54px;
264
+ overflow: hidden;
265
+ background-color: #f4f4f5;
266
+ flex-shrink: 0;
267
+ }
268
+ @media (min-width: 768px) {
269
+ .chisel-menu-card-thumb {
270
+ width: 72px;
271
+ height: 54px;
272
+ }
273
+ }
274
+
275
+ .chisel-menu-card-body {
276
+ display: flex;
277
+ flex-direction: column;
278
+ gap: 2px;
279
+ flex: 1;
280
+ min-width: 0;
281
+ }
282
+
283
+ /* ── Subtitle — hidden on mobile, shown on desktop ── */
284
+ .chisel-menu-subtitle {
285
+ display: none !important;
286
+ }
287
+ @media (min-width: 768px) {
288
+ .chisel-menu-subtitle {
289
+ display: block !important;
290
+ }
291
+ }
292
+
293
+ /* ── Filter links — always visible, no toggle needed ── */
294
+ .chisel-menu-filter-link {
295
+ all: unset;
296
+ cursor: pointer;
297
+ display: block;
298
+ font-size: 14px;
299
+ color: #52525b;
300
+ font-weight: 400;
301
+ padding: 7px 0;
302
+ text-decoration: none;
303
+ transition: color 0.15s;
304
+ }
305
+ .chisel-menu-filter-link:hover {
306
+ color: #18181b;
307
+ }
308
+ ` }), _jsxs("div", { className: "chisel-menu-outer", children: [_jsxs("div", { className: "chisel-menu-left", children: [_jsx("p", { style: {
309
+ fontSize: "11px",
310
+ fontWeight: 700,
311
+ textTransform: "uppercase",
312
+ letterSpacing: "0.12em",
313
+ color: "#71717a",
314
+ margin: "0 0 12px 0",
315
+ }, children: "Featured Projects" }), subtitle && (_jsx("p", { style: {
316
+ fontSize: "14px",
317
+ color: "#52525b",
318
+ lineHeight: 1.5,
319
+ margin: "0 0 16px 0",
320
+ display: "none",
321
+ }, className: "chisel-menu-subtitle", children: subtitle })), displayed.length === 0 ? (_jsx("p", { style: { fontSize: "14px", color: "#a1a1aa", margin: 0 }, children: "No projects found." })) : (_jsx("div", { className: "chisel-menu-card-grid", children: displayed.map((project) => {
322
+ var _a, _b, _c, _d, _e, _f;
323
+ 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;
324
+ const badgeRaw = badgeField ? parseSingleValue(project.custom_field_values[badgeField.key]) : null;
325
+ const badgeOptMap = badgeField ? ((_e = fieldOptionsMap[badgeField.key]) !== null && _e !== void 0 ? _e : {}) : {};
326
+ const badge = badgeRaw ? ((_f = badgeOptMap[badgeRaw]) !== null && _f !== void 0 ? _f : badgeRaw) : null;
327
+ const tags = tagsField ? parseMultiValue(project.custom_field_values[tagsField.key]) : [];
328
+ const href = `${basePath}/${project.slug}`;
329
+ const isHovered = hoveredCard === project.id;
330
+ return (_jsxs("a", { href: href, className: "chisel-menu-card", onMouseEnter: () => setHoveredCard(project.id), onMouseLeave: () => setHoveredCard(null), children: [_jsx("div", { className: "chisel-menu-card-thumb", children: imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: {
331
+ width: "100%",
332
+ height: "100%",
333
+ objectFit: "cover",
334
+ display: "block",
335
+ transform: isHovered ? "scale(1.05)" : "scale(1)",
336
+ transition: "transform 0.3s ease",
337
+ } })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", { className: "chisel-menu-card-body", children: [_jsx("p", { style: {
338
+ fontSize: "13px",
339
+ fontWeight: 700,
340
+ color: isHovered ? ACCENT : "#18181b",
341
+ margin: 0,
342
+ lineHeight: 1.3,
343
+ fontFamily: font,
344
+ transition: "color 0.2s",
345
+ }, children: project.title }), badge && (_jsx("p", { style: { fontSize: "12px", color: "#71717a", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (() => {
346
+ var _a;
347
+ const optMap = tagsField ? ((_a = fieldOptionsMap[tagsField.key]) !== null && _a !== void 0 ? _a : {}) : {};
348
+ const hasOptions = Object.keys(optMap).length > 0;
349
+ // Filter out archived values, then resolve to display labels
350
+ const activeTags = hasOptions ? tags.filter((t) => optMap[t] !== undefined) : tags;
351
+ const tagLabels = activeTags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; }).slice(0, 2);
352
+ if (tagLabels.length === 0)
353
+ return null;
354
+ return (_jsx("p", { style: { fontSize: "11px", color: ACCENT, margin: 0, fontFamily: font, lineHeight: 1.4 }, children: tagLabels.join(" · ") }));
355
+ })()] })] }, project.id));
356
+ }) }))] }), _jsx("div", { className: "chisel-menu-divider-v" }), _jsxs("div", { className: "chisel-menu-right", children: [filterOptions.length > 0 && (_jsxs(_Fragment, { children: [_jsx("p", { style: {
357
+ fontSize: "11px",
358
+ fontWeight: 700,
359
+ textTransform: "uppercase",
360
+ letterSpacing: "0.12em",
361
+ color: "#71717a",
362
+ margin: "0 0 10px 0",
363
+ }, children: "Browse By" }), _jsx("p", { style: {
364
+ fontSize: "15px",
365
+ fontWeight: 700,
366
+ color: "#18181b",
367
+ margin: "0 0 4px 0",
368
+ fontFamily: font,
369
+ }, children: filterFieldName }), _jsx("div", { style: { display: "flex", flexDirection: "column", marginBottom: "20px" }, children: filterOptions.map((opt) => {
370
+ const href = filterFieldKey
371
+ ? `${basePath}?filter[${filterFieldKey}]=${encodeURIComponent(opt.id)}`
372
+ : basePath;
373
+ return (_jsx("a", { href: href, className: "chisel-menu-filter-link", style: { fontFamily: font }, children: opt.label }, opt.id));
374
+ }) }), _jsx("div", { style: { height: "1px", backgroundColor: "#e4e4e7", marginBottom: "20px" } })] })), _jsxs("a", { href: viewAllPath, style: {
375
+ all: "unset",
376
+ cursor: "pointer",
377
+ display: "inline-flex",
378
+ alignItems: "center",
379
+ gap: "6px",
380
+ fontSize: "14px",
381
+ fontWeight: 600,
382
+ color: ACCENT,
383
+ textDecoration: "none",
384
+ fontFamily: font,
385
+ }, children: ["View All Projects", _jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M5 12h14M12 5l7 7-7 7" }) })] })] })] })] }));
386
+ }
@@ -0,0 +1,42 @@
1
+ export interface ProjectPortfolioProps {
2
+ /** Client slug identifying which client's projects to load */
3
+ clientSlug: string;
4
+ /** Base URL of the projects API */
5
+ apiBase: string;
6
+ /** Base path for project detail links. Defaults to "/projects" */
7
+ basePath?: string;
8
+ /**
9
+ * Filter params forwarded to the API as query string params.
10
+ * Pass Next.js searchParams here — e.g. { type: "commercial" }
11
+ * These map directly to custom field keys defined in the schema.
12
+ */
13
+ searchParams?: Record<string, string | string[] | undefined>;
14
+ /**
15
+ * Seconds to cache via Next.js Data Cache on production deployments.
16
+ * React.cache() always deduplicates within a single render in all environments.
17
+ * Defaults to 60.
18
+ */
19
+ revalidate?: number;
20
+ /**
21
+ * Disable all caching. Every request hits the API fresh with no deduplication.
22
+ * Useful for debugging — do not use in production.
23
+ */
24
+ noCache?: boolean;
25
+ }
26
+ /**
27
+ * ProjectPortfolio — pure self-fetching card grid.
28
+ *
29
+ * Caching works in all environments:
30
+ * - Everywhere: React.cache() deduplicates fetches within a single render
31
+ * - Production: next.revalidate caches across requests for `revalidate` seconds
32
+ *
33
+ * Usage:
34
+ * import { ProjectPortfolio } from "chisel-project-portfolio"
35
+ *
36
+ * <ProjectPortfolio
37
+ * clientSlug="my-client"
38
+ * apiBase="https://your-api.com"
39
+ * />
40
+ */
41
+ export declare function ProjectPortfolio({ clientSlug, apiBase, basePath, searchParams, revalidate, noCache, }: ProjectPortfolioProps): Promise<import("react/jsx-runtime").JSX.Element>;
42
+ //# sourceMappingURL=ProjectPortfolio.d.ts.map
@@ -0,0 +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;AAyED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,YAAiB,EACjB,UAAe,EACf,OAAe,GAChB,EAAE,qBAAqB,oDA+HvB"}
@@ -0,0 +1,153 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { cache } from "react";
3
+ import { ProjectCard } from "./ProjectCard";
4
+ // Two-layer caching strategy:
5
+ // 1. React.cache() — deduplicates within a single render in ALL environments.
6
+ // If this component appears twice on one page, the API is only hit once.
7
+ // 2. next: { revalidate } — Next.js Data Cache, caches across multiple requests
8
+ // on production deployments. Silently ignored in preview/local.
9
+ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate, filtersKey = "{}", noCache = false) => {
10
+ var _a, _b, _c, _d, _e, _f;
11
+ const fetchOpts = noCache
12
+ ? { cache: "no-store" }
13
+ : { next: { revalidate } };
14
+ // Build URL with filter[key]=value as required by the API
15
+ const filters = JSON.parse(filtersKey);
16
+ const params = new URLSearchParams({ api_key: "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR" });
17
+ Object.entries(filters).forEach(([key, val]) => {
18
+ if (val)
19
+ params.append(`filter[${key}]`, val);
20
+ });
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);
23
+ const json = res.ok
24
+ ? await res.json()
25
+ : { client: { name: "Projects", description: null, custom_fields_schema: [] }, data: [] };
26
+ // Deduplicate schema keys
27
+ const seen = new Set();
28
+ const schema = ((_b = (_a = json.client) === null || _a === void 0 ? void 0 : _a.custom_fields_schema) !== null && _b !== void 0 ? _b : []).filter((f) => {
29
+ if (seen.has(f.key))
30
+ return false;
31
+ seen.add(f.key);
32
+ return true;
33
+ });
34
+ // Build fieldOptionsMap directly from schema options — no separate /fields call needed
35
+ const fieldOptionsMap = {};
36
+ for (const field of schema) {
37
+ const map = {};
38
+ for (const opt of ((_c = field.options) !== null && _c !== void 0 ? _c : [])) {
39
+ if (typeof opt === "object" && opt.id && opt.label) {
40
+ map[opt.id] = opt.label;
41
+ map[opt.label] = opt.label;
42
+ }
43
+ }
44
+ fieldOptionsMap[field.key] = map;
45
+ }
46
+ return {
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),
49
+ schema,
50
+ fieldOptionsMap,
51
+ };
52
+ });
53
+ /**
54
+ * ProjectPortfolio — pure self-fetching card grid.
55
+ *
56
+ * Caching works in all environments:
57
+ * - Everywhere: React.cache() deduplicates fetches within a single render
58
+ * - Production: next.revalidate caches across requests for `revalidate` seconds
59
+ *
60
+ * Usage:
61
+ * import { ProjectPortfolio } from "chisel-project-portfolio"
62
+ *
63
+ * <ProjectPortfolio
64
+ * clientSlug="my-client"
65
+ * apiBase="https://your-api.com"
66
+ * />
67
+ */
68
+ export async function ProjectPortfolio({ clientSlug, apiBase, basePath = "/projects", searchParams = {}, revalidate = 60, noCache = false, }) {
69
+ // Parse filter[key]=value from searchParams into { key: value }
70
+ const filters = {};
71
+ Object.entries(searchParams).forEach(([key, val]) => {
72
+ if (val === undefined)
73
+ return;
74
+ const match = key.match(/^filter\[(.+)\]$/);
75
+ if (match)
76
+ filters[match[1]] = Array.isArray(val) ? val[0] : val;
77
+ });
78
+ const filtersKey = JSON.stringify(filters);
79
+ const { projects, schema, fieldOptionsMap } = await fetchPortfolioData(apiBase, clientSlug, revalidate, filtersKey, noCache);
80
+ const hasFilters = Object.keys(filters).length > 0;
81
+ const activeFilterLabels = Object.entries(filters)
82
+ .map(([key, val]) => {
83
+ var _a;
84
+ const field = schema.find((f) => f.key === key);
85
+ // Try to find the display label from schema options
86
+ const optLabel = (_a = field === null || field === void 0 ? void 0 : field.options) === null || _a === void 0 ? void 0 : _a.find((o) => typeof o === "object" ? o.id === val : false);
87
+ return optLabel && typeof optLabel === "object" ? optLabel.label : val;
88
+ })
89
+ .filter(Boolean);
90
+ if (projects.length === 0) {
91
+ return (_jsxs("div", { style: { textAlign: "center", padding: "4rem 0" }, children: [_jsxs("p", { style: { color: "#71717a" }, children: ["No projects found", hasFilters ? " matching the selected filters" : "", "."] }), hasFilters && (_jsx("a", { href: basePath, style: { color: "#f18a00", fontSize: "14px" }, children: "Clear filters" }))] }));
92
+ }
93
+ return (_jsxs("div", { style: {
94
+ width: "100%",
95
+ maxWidth: "1280px",
96
+ margin: "0 auto",
97
+ padding: "2rem 1rem",
98
+ boxSizing: "border-box",
99
+ }, children: [hasFilters && (_jsxs("div", { style: {
100
+ display: "flex",
101
+ alignItems: "center",
102
+ gap: "12px",
103
+ marginBottom: "1.5rem",
104
+ padding: "10px 14px",
105
+ backgroundColor: "#fafafa",
106
+ border: "1px solid #e4e4e7",
107
+ flexWrap: "wrap",
108
+ }, children: [_jsx("span", { style: { fontSize: "13px", color: "#71717a" }, children: "Filtered by:" }), activeFilterLabels.map((label, i) => (_jsx("span", { style: {
109
+ fontSize: "13px",
110
+ fontWeight: 600,
111
+ color: "#18181b",
112
+ backgroundColor: "#f4f4f5",
113
+ padding: "2px 10px",
114
+ borderRadius: "999px",
115
+ }, children: label }, i))), _jsx("a", { href: basePath, style: {
116
+ marginLeft: "auto",
117
+ fontSize: "13px",
118
+ color: "#f18a00",
119
+ textDecoration: "none",
120
+ fontWeight: 500,
121
+ }, children: "Clear filters" })] })), _jsx("style", { children: `
122
+ .chisel-project-grid {
123
+ display: grid;
124
+ grid-template-columns: 1fr;
125
+ gap: 1.5rem;
126
+ }
127
+ @media (min-width: 640px) {
128
+ .chisel-project-grid {
129
+ grid-template-columns: repeat(2, 1fr);
130
+ gap: 1.5rem;
131
+ }
132
+ }
133
+ @media (min-width: 1024px) {
134
+ .chisel-project-grid {
135
+ grid-template-columns: repeat(3, 1fr);
136
+ gap: 2rem;
137
+ }
138
+ }
139
+ .chisel-project-card-img {
140
+ height: 180px;
141
+ }
142
+ @media (min-width: 640px) {
143
+ .chisel-project-card-img {
144
+ height: 200px;
145
+ }
146
+ }
147
+ @media (min-width: 1024px) {
148
+ .chisel-project-card-img {
149
+ height: 220px;
150
+ }
151
+ }
152
+ ` }), _jsx("div", { className: "chisel-project-grid", children: projects.map((project, index) => (_jsx(ProjectCard, { project: project, schema: schema, fieldOptionsMap: fieldOptionsMap, basePath: basePath, priority: index === 0 }, project.id))) })] }));
153
+ }
@@ -0,0 +1,21 @@
1
+ export interface ProjectPortfolioClientProps {
2
+ /** Client slug identifying which client's projects to load */
3
+ clientSlug: string;
4
+ /** Base URL of the projects API */
5
+ apiBase: string;
6
+ /** Base path for project detail links. Defaults to "/projects" */
7
+ basePath?: string;
8
+ /**
9
+ * Active filters to apply to the project list.
10
+ * Pass a Record<string, string> of field key/value pairs.
11
+ * Filtering happens in memory — no API call on change.
12
+ * e.g. { type: "commercial" }
13
+ */
14
+ filters?: Record<string, string>;
15
+ /** Font family string applied to all text. Defaults to system font stack */
16
+ font?: string;
17
+ /** Max columns in the grid. 2 or 3. Defaults to 3 */
18
+ columns?: 2 | 3;
19
+ }
20
+ export declare function ProjectPortfolioClient({ clientSlug, apiBase, basePath, filters, font, columns, }: ProjectPortfolioClientProps): import("react/jsx-runtime").JSX.Element;
21
+ //# sourceMappingURL=ProjectPortfolioClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectPortfolioClient.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolioClient.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,2BAA2B;IAC1C,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,OAAO,CAAC,EAAE,CAAC,GAAG,CAAC,CAAA;CAChB;AA0DD,wBAAgB,sBAAsB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,OAAY,EACZ,IAAmB,EACnB,OAAW,GACZ,EAAE,2BAA2B,2CAoJ7B"}