project-portfolio 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,8 +44,43 @@ export default async function ProjectsPage() {
44
44
  | `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
45
45
  | `apiBase` | `string` | Yes | — | Base URL of the projects API |
46
46
  | `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
47
+ | `searchParams` | `Record<string, string \| string[] \| undefined>` | No | `{}` | Filter params forwarded to the API — pass Next.js `searchParams` directly |
47
48
  | `revalidate` | `number` | No | `60` | Cache revalidation period in seconds |
48
49
 
50
+ #### Filter Integration
51
+
52
+ `ProjectPortfolio` works together with `ProjectMenu` to form a complete filter flow. When a user clicks a filter link in the megamenu, they are navigated to the project grid with filter query params appended to the URL. Pass Next.js `searchParams` to `ProjectPortfolio` so it can forward them to the API:
53
+
54
+ ```tsx
55
+ // app/projects/page.tsx
56
+ import { ProjectPortfolio } from "project-portfolio"
57
+
58
+ export default async function ProjectsPage({
59
+ searchParams,
60
+ }: {
61
+ searchParams: { [key: string]: string | string[] | undefined }
62
+ }) {
63
+ return (
64
+ <ProjectPortfolio
65
+ clientSlug="your-client-slug"
66
+ apiBase="https://your-api.com"
67
+ basePath="/projects"
68
+ searchParams={searchParams}
69
+ />
70
+ )
71
+ }
72
+ ```
73
+
74
+ Filter URLs follow this pattern — the key matches the custom field key in the schema:
75
+
76
+ ```
77
+ /projects?type=commercial
78
+ /projects?type=educational-facilities
79
+ /projects?system=spacematic
80
+ ```
81
+
82
+ When active filters are applied, a filter banner is shown above the grid with a "Clear filters" link back to `basePath`.
83
+
49
84
  ---
50
85
 
51
86
  ### `ProjectDetail`
@@ -3,11 +3,13 @@ export type CardVariant = "card" | "compact";
3
3
  interface ProjectCardProps {
4
4
  project: Project;
5
5
  schema: CustomFieldSchema[];
6
+ /** Map of fieldKey -> { id: label, label: label } for normalizing mixed id/label values */
7
+ fieldOptionsMap?: Record<string, Record<string, string>>;
6
8
  priority?: boolean;
7
9
  variant?: CardVariant;
8
10
  /** Base path for project detail links. Defaults to "/projects" */
9
11
  basePath?: string;
10
12
  }
11
- export declare function ProjectCard({ project, schema, priority, variant, basePath, }: ProjectCardProps): import("react/jsx-runtime").JSX.Element;
13
+ export declare function ProjectCard({ project, schema, fieldOptionsMap, priority, variant, basePath, }: ProjectCardProps): import("react/jsx-runtime").JSX.Element;
12
14
  export {};
13
15
  //# sourceMappingURL=ProjectCard.d.ts.map
@@ -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,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,QAAQ,EACR,OAAgB,EAChB,QAAsB,GACvB,EAAE,gBAAgB,2CAyPlB"}
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,2CA4PlB"}
@@ -12,7 +12,7 @@ function parseSingleValue(raw) {
12
12
  return raw.replace(/`/g, "").trim();
13
13
  return String(raw !== null && raw !== void 0 ? raw : "");
14
14
  }
15
- export function ProjectCard({ project, schema, priority, variant = "card", basePath = "/projects", }) {
15
+ export function ProjectCard({ project, schema, fieldOptionsMap = {}, priority, variant = "card", basePath = "/projects", }) {
16
16
  var _a, _b, _c, _d;
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");
@@ -114,12 +114,16 @@ export function ProjectCard({ project, schema, priority, variant = "card", baseP
114
114
  fontWeight: 600,
115
115
  padding: "3px 10px",
116
116
  }, children: badgeValue })), _jsx("h3", { style: { color: "#fff", fontWeight: 700, fontSize: "20px", lineHeight: 1.3, margin: 0, padding: 0, fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, children: project.title }), locationString && (_jsx("p", { style: { color: "rgba(255,255,255,0.8)", fontSize: "14px", margin: 0 }, children: locationString }))] })] }), _jsxs("div", { style: { display: "flex", flexDirection: "column", flex: 1, padding: "1.5rem" }, children: [(project.blurb || project.description) && (_jsx("p", { style: { fontSize: "14px", color: "#3f3f46", lineHeight: 1.6, margin: "0 0 16px 0" }, children: project.blurb || project.description })), tagFields.map((field) => {
117
+ var _a;
117
118
  const vals = parseMultiValue(project.custom_field_values[field.key]);
118
119
  if (vals.length === 0)
119
120
  return null;
121
+ const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
120
122
  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: vals.map((val, i) => {
121
- var _a;
122
- const url = (_a = field.option_urls) === null || _a === void 0 ? void 0 : _a[val];
123
+ var _a, _b, _c, _d;
124
+ // Normalize: look up by id or label, always display the label
125
+ const label = (_a = optMap[val]) !== null && _a !== void 0 ? _a : val;
126
+ const url = (_c = (_b = field.option_urls) === null || _b === void 0 ? void 0 : _b[val]) !== null && _c !== void 0 ? _c : (_d = field.option_urls) === null || _d === void 0 ? void 0 : _d[label];
123
127
  const tagStyle = {
124
128
  border: "1px solid #d4d4d8",
125
129
  padding: "4px 12px",
@@ -129,7 +133,7 @@ export function ProjectCard({ project, schema, priority, variant = "card", baseP
129
133
  backgroundColor: "transparent",
130
134
  cursor: url ? "pointer" : "default",
131
135
  };
132
- return url ? (_jsx("a", { href: url, style: tagStyle, children: val }, `${project.id}-${field.key}-${i}`)) : (_jsx("span", { style: tagStyle, children: val }, `${project.id}-${field.key}-${i}`));
136
+ return url ? (_jsx("a", { href: url, style: tagStyle, children: label }, `${project.id}-${field.key}-${i}`)) : (_jsx("span", { style: tagStyle, children: label }, `${project.id}-${field.key}-${i}`));
133
137
  }) })] }, `${project.id}-${field.key}`));
134
138
  }), _jsx("div", { style: { flex: 1 } }), _jsxs("div", { style: { marginTop: "16px", borderTop: "1px solid #e4e4e7", paddingTop: "12px", display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: "16px" }, children: [footerStat ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [_jsx("span", { style: { fontSize: "12px", color: "#a1a1aa" }, children: footerStat.label }), _jsx("span", { style: { color: "#18181b", fontWeight: 700, fontSize: "16px" }, children: footerStat.formatted })] })) : _jsx("div", {}), _jsx("a", { href: href, style: { all: "revert", color: "#f18a00", fontWeight: 600, fontSize: "14px", textDecoration: "none", flexShrink: 0, fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, children: "Details \u2192" })] })] })] }));
135
139
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectFilters.d.ts","sourceRoot":"","sources":["../src/ProjectFilters.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAEhD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpD,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAO,EACP,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,EAAE,mBAAmB,2CA8FrB"}
1
+ {"version":3,"file":"ProjectFilters.d.ts","sourceRoot":"","sources":["../src/ProjectFilters.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAehD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpD,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAO,EACP,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,EAAE,mBAAmB,2CA8FrB"}
@@ -1,4 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ function selectOptionsForField(field) {
3
+ const out = [];
4
+ const seen = new Set();
5
+ for (const opt of field.options) {
6
+ const value = typeof opt === "string" ? opt : opt.id;
7
+ const label = typeof opt === "string" ? opt : opt.label;
8
+ if (seen.has(value))
9
+ continue;
10
+ seen.add(value);
11
+ out.push({ value, label });
12
+ }
13
+ return out;
14
+ }
2
15
  export function ProjectFilters({ fields, filters, onFilterChange, onClearFilters, hasActiveFilters, }) {
3
16
  const deduped = fields.filter((f, i, arr) => arr.findIndex((x) => x.key === f.key) === i);
4
17
  return (_jsxs("div", { style: {
@@ -21,7 +34,7 @@ export function ProjectFilters({ fields, filters, onFilterChange, onClearFilters
21
34
  color: "#18181b",
22
35
  outline: "none",
23
36
  cursor: "pointer",
24
- }, children: [_jsxs("option", { value: "all", children: ["All ", field.name] }), [...new Set(field.options)].map((option, i) => (_jsx("option", { value: option, children: option }, `${field.key}-opt-${i}`)))] })] }, `filter-${fieldIndex}-${field.key}`));
37
+ }, children: [_jsxs("option", { value: "all", children: ["All ", field.name] }), selectOptionsForField(field).map(({ value, label }, i) => (_jsx("option", { value: value, children: label }, `${field.key}-opt-${i}`)))] })] }, `filter-${fieldIndex}-${field.key}`));
25
38
  }
26
39
  if (field.type === "text" || field.type === "location") {
27
40
  return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [_jsx("label", { style: { fontSize: "13px", fontWeight: 500, color: "#71717a" }, children: field.name }), _jsx("input", { type: "text", placeholder: `Search ${field.name.toLowerCase()}...`, value: filters[field.key] || "", onChange: (e) => onFilterChange(field.key, e.target.value), style: {
@@ -15,6 +15,11 @@ export interface ProjectMenuProps {
15
15
  maxProjects?: number;
16
16
  /** Revalidation period in seconds. Defaults to 60 */
17
17
  revalidate?: number;
18
+ /**
19
+ * Disable all caching. Every request hits the API fresh.
20
+ * Useful for debugging — do not use in production.
21
+ */
22
+ noCache?: boolean;
18
23
  }
19
- export declare function ProjectMenu({ clientSlug, apiBase, basePath, viewAllPath, subtitle, font, maxProjects, revalidate, }: ProjectMenuProps): Promise<import("react/jsx-runtime").JSX.Element>;
24
+ export declare function ProjectMenu({ clientSlug, apiBase, basePath, viewAllPath, subtitle, font, maxProjects, revalidate, noCache, }: ProjectMenuProps): Promise<import("react/jsx-runtime").JSX.Element>;
20
25
  //# sourceMappingURL=ProjectMenu.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectMenu.d.ts","sourceRoot":"","sources":["../src/ProjectMenu.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,OAAO,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,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA+BD,wBAAsB,WAAW,CAAC,EAChC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,WAAW,EACX,QAAQ,EACR,IAA0E,EAC1E,WAAe,EACf,UAAe,GAChB,EAAE,gBAAgB,oDA+BlB"}
1
+ {"version":3,"file":"ProjectMenu.d.ts","sourceRoot":"","sources":["../src/ProjectMenu.tsx"],"names":[],"mappings":"AAMA,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,UAAU,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,OAAO,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,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AA6CD,wBAAsB,WAAW,CAAC,EAChC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,WAAW,EACX,QAAQ,EACR,IAA0E,EAC1E,WAAe,EACf,UAAe,EACf,OAAe,GAChB,EAAE,gBAAgB,oDAiClB"}
@@ -2,26 +2,43 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cache } from "react";
3
3
  import { ProjectMenuClient } from "./ProjectMenuClient";
4
4
  const API_KEY = "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR";
5
- const fetchMenuData = cache(async (apiBase, clientSlug, revalidate) => {
6
- var _a, _b, _c;
7
- const fetchOpts = revalidate > 0 ? { next: { revalidate } } : {};
5
+ const fetchMenuData = cache(async (apiBase, clientSlug, revalidate, noCache = false) => {
6
+ var _a, _b, _c, _d, _e;
7
+ const fetchOpts = noCache
8
+ ? { cache: "no-store" }
9
+ : { next: { revalidate } };
8
10
  try {
9
- const res = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts);
11
+ const [res, fieldsRes] = await Promise.all([
12
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`, fetchOpts),
13
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`, fetchOpts),
14
+ ]);
10
15
  if (!res.ok)
11
- return { projects: [], schema: [] };
16
+ return { projects: [], schema: [], fieldOptionsMap: {} };
12
17
  const json = await res.json();
13
- // Per API docs: single project response is { client: { custom_fields_schema }, data: [...] }
14
18
  const projects = (_a = json === null || json === void 0 ? void 0 : json.data) !== null && _a !== void 0 ? _a : [];
15
19
  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 : [];
16
- return { projects, schema };
20
+ // Build fieldOptionsMap: { fieldKey: { id: label, label: label } }
21
+ const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
22
+ const fieldOptionsMap = {};
23
+ for (const field of ((_d = fieldsJson.fields) !== null && _d !== void 0 ? _d : [])) {
24
+ const map = {};
25
+ for (const opt of ((_e = field.options) !== null && _e !== void 0 ? _e : [])) {
26
+ if (typeof opt === "object" && opt.id && opt.label) {
27
+ map[opt.id] = opt.label;
28
+ map[opt.label] = opt.label;
29
+ }
30
+ }
31
+ fieldOptionsMap[field.key] = map;
32
+ }
33
+ return { projects, schema, fieldOptionsMap };
17
34
  }
18
- catch (_d) {
19
- return { projects: [], schema: [] };
35
+ catch (_f) {
36
+ return { projects: [], schema: [], fieldOptionsMap: {} };
20
37
  }
21
38
  });
22
- export async function ProjectMenu({ clientSlug, apiBase, basePath = "/projects", viewAllPath, subtitle, font = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", maxProjects = 6, revalidate = 60, }) {
23
- var _a, _b, _c;
24
- const { projects, schema } = await fetchMenuData(apiBase, clientSlug, revalidate);
39
+ export async function ProjectMenu({ clientSlug, apiBase, basePath = "/projects", viewAllPath, subtitle, font = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif", maxProjects = 6, revalidate = 60, noCache = false, }) {
40
+ var _a, _b, _c, _d;
41
+ const { projects, schema, fieldOptionsMap } = await fetchMenuData(apiBase, clientSlug, revalidate, noCache);
25
42
  // Find the filterable select field (badge_overlay is our category field)
26
43
  const filterField = (_a = schema.find((f) => f.is_filterable && (f.type === "select" || f.type === "multi-select"))) !== null && _a !== void 0 ? _a : null;
27
44
  // Build filter option list from schema
@@ -34,5 +51,5 @@ export async function ProjectMenu({ clientSlug, apiBase, basePath = "/projects",
34
51
  return { id: (_a = opt.id) !== null && _a !== void 0 ? _a : String((_b = opt.label) !== null && _b !== void 0 ? _b : "").toLowerCase().replace(/\s+/g, "-"), label: (_c = opt.label) !== null && _c !== void 0 ? _c : String(opt) };
35
52
  })
36
53
  : [];
37
- return (_jsx(ProjectMenuClient, { projects: projects, schema: schema, filterOptions: filterOptions, filterFieldKey: (_c = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _c !== void 0 ? _c : null, subtitle: subtitle, basePath: basePath, viewAllPath: viewAllPath !== null && viewAllPath !== void 0 ? viewAllPath : basePath, font: font, maxProjects: maxProjects }));
54
+ return (_jsx(ProjectMenuClient, { projects: projects, schema: schema, filterOptions: filterOptions, filterFieldKey: (_c = filterField === null || filterField === void 0 ? void 0 : filterField.key) !== null && _c !== void 0 ? _c : null, filterFieldName: (_d = filterField === null || filterField === void 0 ? void 0 : filterField.name) !== null && _d !== void 0 ? _d : "Project Type", fieldOptionsMap: fieldOptionsMap, subtitle: subtitle, basePath: basePath, viewAllPath: viewAllPath !== null && viewAllPath !== void 0 ? viewAllPath : basePath, font: font, maxProjects: maxProjects }));
38
55
  }
@@ -7,12 +7,14 @@ interface ProjectMenuClientProps {
7
7
  label: string;
8
8
  }[];
9
9
  filterFieldKey: string | null;
10
+ filterFieldName?: string;
11
+ fieldOptionsMap?: Record<string, Record<string, string>>;
10
12
  subtitle?: string;
11
13
  basePath: string;
12
14
  viewAllPath: string;
13
15
  font: string;
14
16
  maxProjects?: number;
15
17
  }
16
- export declare function ProjectMenuClient({ projects, schema, filterOptions, filterFieldKey, subtitle, basePath, viewAllPath, font, maxProjects, }: ProjectMenuClientProps): import("react/jsx-runtime").JSX.Element;
18
+ export declare function ProjectMenuClient({ projects, schema, filterOptions, filterFieldKey, filterFieldName, fieldOptionsMap, subtitle, basePath, viewAllPath, font, maxProjects, }: ProjectMenuClientProps): import("react/jsx-runtime").JSX.Element;
17
19
  export {};
18
20
  //# sourceMappingURL=ProjectMenuClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAa3E,UAAU,sBAAsB;IAC9B,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,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,MAAM,EACN,aAAa,EACb,cAAc,EACd,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAI,EACJ,WAAe,GAChB,EAAE,sBAAsB,2CAqVxB"}
1
+ {"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAa3E,UAAU,sBAAsB;IAC9B,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,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,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAID,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,MAAM,EACN,aAAa,EACb,cAAc,EACd,eAAgC,EAChC,eAAoB,EACpB,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAI,EACJ,WAAe,GAChB,EAAE,sBAAsB,2CA4VxB"}
@@ -13,19 +13,11 @@ function parseMultiValue(raw) {
13
13
  return raw.replace(/`/g, "").split(",").map((s) => s.trim()).filter(Boolean);
14
14
  return [];
15
15
  }
16
- export function ProjectMenuClient({ projects, schema, filterOptions, filterFieldKey, subtitle, basePath, viewAllPath, font, maxProjects = 6, }) {
17
- const [activeFilter, setActiveFilter] = useState(null);
16
+ const ACCENT = "oklch(0.78 0.16 85)";
17
+ export function ProjectMenuClient({ projects, schema, filterOptions, filterFieldKey, filterFieldName = "Project Type", fieldOptionsMap = {}, subtitle, basePath, viewAllPath, font, maxProjects = 6, }) {
18
18
  const [filtersOpen, setFiltersOpen] = useState(false);
19
- const filtered = filterFieldKey && activeFilter
20
- ? projects.filter((p) => {
21
- const raw = p.custom_field_values[filterFieldKey];
22
- const vals = parseMultiValue(raw).length > 0
23
- ? parseMultiValue(raw)
24
- : [parseSingleValue(raw)];
25
- return vals.some((v) => v.toLowerCase() === activeFilter.toLowerCase());
26
- })
27
- : projects;
28
- const displayed = filtered.slice(0, maxProjects);
19
+ const [hoveredCard, setHoveredCard] = useState(null);
20
+ const displayed = projects.slice(0, maxProjects);
29
21
  const badgeField = schema.find((f) => f.display_position === "badge_overlay");
30
22
  const tagsField = schema.find((f) => f.display_position === "tags");
31
23
  return (_jsxs(_Fragment, { children: [_jsx("style", { children: `
@@ -144,36 +136,56 @@ export function ProjectMenuClient({ projects, schema, filterOptions, filterField
144
136
  lineHeight: 1.6,
145
137
  margin: "0 0 24px 0",
146
138
  }, 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) => {
147
- var _a, _b, _c, _d;
139
+ var _a, _b, _c, _d, _e, _f;
148
140
  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;
149
- const badge = badgeField ? parseSingleValue(project.custom_field_values[badgeField.key]) : null;
141
+ const badgeRaw = badgeField ? parseSingleValue(project.custom_field_values[badgeField.key]) : null;
142
+ const badgeOptMap = badgeField ? ((_e = fieldOptionsMap[badgeField.key]) !== null && _e !== void 0 ? _e : {}) : {};
143
+ const badge = badgeRaw ? ((_f = badgeOptMap[badgeRaw]) !== null && _f !== void 0 ? _f : badgeRaw) : null;
150
144
  const tags = tagsField ? parseMultiValue(project.custom_field_values[tagsField.key]) : [];
151
145
  const href = `${basePath}/${project.slug}`;
152
- return (_jsxs("a", { href: href, style: {
146
+ const isHovered = hoveredCard === project.id;
147
+ return (_jsxs("a", { href: href, onMouseEnter: () => setHoveredCard(project.id), onMouseLeave: () => setHoveredCard(null), style: {
153
148
  all: "unset",
154
149
  cursor: "pointer",
155
150
  display: "flex",
156
151
  alignItems: "flex-start",
157
152
  gap: "12px",
158
153
  padding: "12px",
159
- border: "1px solid #e4e4e7",
154
+ border: `1px solid ${isHovered ? ACCENT : "#e4e4e7"}`,
160
155
  textDecoration: "none",
161
156
  boxSizing: "border-box",
162
- transition: "border-color 0.15s",
163
- }, onMouseEnter: (e) => (e.currentTarget.style.borderColor = "#d4d4d8"), onMouseLeave: (e) => (e.currentTarget.style.borderColor = "#e4e4e7"), children: [_jsx("div", { style: {
157
+ transition: "border-color 0.2s",
158
+ }, children: [_jsx("div", { style: {
164
159
  width: "80px",
165
160
  height: "64px",
166
161
  flexShrink: 0,
167
162
  overflow: "hidden",
168
163
  backgroundColor: "#f4f4f5",
169
- }, children: imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px", minWidth: 0, flex: 1 }, children: [_jsx("p", { style: {
164
+ }, children: imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: {
165
+ width: "100%",
166
+ height: "100%",
167
+ objectFit: "cover",
168
+ display: "block",
169
+ transform: isHovered ? "scale(1.06)" : "scale(1)",
170
+ transition: "transform 0.3s ease",
171
+ } })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "3px", minWidth: 0, flex: 1 }, children: [_jsx("p", { style: {
170
172
  fontSize: "14px",
171
173
  fontWeight: 700,
172
- color: "#18181b",
174
+ color: isHovered ? ACCENT : "#18181b",
173
175
  margin: 0,
174
176
  lineHeight: 1.3,
175
177
  fontFamily: font,
176
- }, children: project.title }), badge && (_jsx("p", { style: { fontSize: "13px", color: "#52525b", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (_jsx("p", { style: { fontSize: "12px", color: "#f18a00", margin: 0, fontFamily: font, lineHeight: 1.4 }, children: tags.join(" · ") }))] })] }, project.id));
178
+ transition: "color 0.2s",
179
+ }, children: project.title }), badge && (_jsx("p", { style: { fontSize: "13px", color: "#52525b", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (() => {
180
+ var _a;
181
+ const optMap = tagsField ? ((_a = fieldOptionsMap[tagsField.key]) !== null && _a !== void 0 ? _a : {}) : {};
182
+ const tagLabels = tags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; });
183
+ const shown = tagLabels.slice(0, 3);
184
+ const display = tagLabels.length > 3
185
+ ? shown.join(" · ") + " · ..."
186
+ : shown.join(" · ");
187
+ return (_jsx("p", { style: { fontSize: "12px", color: ACCENT, margin: 0, fontFamily: font, lineHeight: 1.4 }, children: display }));
188
+ })()] })] }, project.id));
177
189
  }) }))] }), _jsx("div", { className: "chisel-menu-divider-v" }), filterOptions.length > 0 && (_jsxs("div", { className: "chisel-menu-right", children: [_jsx("p", { style: {
178
190
  fontSize: "11px",
179
191
  fontWeight: 700,
@@ -181,27 +193,28 @@ export function ProjectMenuClient({ projects, schema, filterOptions, filterField
181
193
  letterSpacing: "0.12em",
182
194
  color: "#71717a",
183
195
  margin: "0 0 14px 0",
184
- }, children: "Browse By" }), _jsxs("button", { className: "chisel-menu-filters-mobile-toggle", onClick: () => setFiltersOpen((o) => !o), children: [_jsx("p", { style: { fontSize: "15px", fontWeight: 700, color: "#18181b", margin: 0, fontFamily: font }, children: "Project Type" }), _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#18181b", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", style: { transform: filtersOpen ? "rotate(180deg)" : "rotate(0deg)", transition: "transform 0.2s", flexShrink: 0 }, children: _jsx("path", { d: "M6 9l6 6 6-6" }) })] }), _jsx("p", { style: {
196
+ }, children: "Browse By" }), _jsx("p", { style: {
185
197
  fontSize: "15px",
186
198
  fontWeight: 700,
187
199
  color: "#18181b",
188
200
  margin: "0 0 10px 0",
189
201
  fontFamily: font,
190
- display: "none",
191
- }, className: "chisel-menu-desktop-heading", children: "Project Type" }), _jsx("div", { className: `chisel-menu-filters-list${filtersOpen ? " open" : ""}`, children: filterOptions.map((opt) => {
192
- const isActive = activeFilter === opt.label;
193
- return (_jsx("button", { onClick: () => setActiveFilter(isActive ? null : opt.label), style: {
202
+ }, children: filterFieldName }), _jsx("button", { className: "chisel-menu-filters-mobile-toggle", onClick: () => setFiltersOpen((o) => !o), style: { marginTop: "-10px" }, children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "#18181b", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", style: { transform: filtersOpen ? "rotate(180deg)" : "rotate(0deg)", transition: "transform 0.2s", flexShrink: 0 }, children: _jsx("path", { d: "M6 9l6 6 6-6" }) }) }), _jsx("div", { className: `chisel-menu-filters-list${filtersOpen ? " open" : ""}`, children: filterOptions.map((opt) => {
203
+ const href = filterFieldKey
204
+ ? `${basePath}?filter[${filterFieldKey}]=${encodeURIComponent(opt.id)}`
205
+ : basePath;
206
+ return (_jsx("a", { href: href, style: {
194
207
  all: "unset",
195
208
  cursor: "pointer",
196
209
  fontSize: "14px",
197
- color: isActive ? "#18181b" : "#52525b",
198
- fontWeight: isActive ? 600 : 400,
210
+ color: "#52525b",
211
+ fontWeight: 400,
199
212
  padding: "5px 0",
200
- textAlign: "left",
213
+ textDecoration: "none",
201
214
  fontFamily: font,
202
215
  transition: "color 0.15s",
203
- }, onMouseEnter: (e) => (e.currentTarget.style.color = "#18181b"), onMouseLeave: (e) => { if (!isActive)
204
- e.currentTarget.style.color = "#52525b"; }, children: opt.label }, opt.id));
216
+ display: "block",
217
+ }, onMouseEnter: (e) => (e.currentTarget.style.color = "#18181b"), onMouseLeave: (e) => (e.currentTarget.style.color = "#52525b"), children: opt.label }, opt.id));
205
218
  }) }), _jsx("div", { style: { height: "1px", backgroundColor: "#e4e4e7", margin: "20px 0" } }), _jsxs("a", { href: viewAllPath, style: {
206
219
  all: "unset",
207
220
  cursor: "pointer",
@@ -210,7 +223,7 @@ export function ProjectMenuClient({ projects, schema, filterOptions, filterField
210
223
  gap: "6px",
211
224
  fontSize: "14px",
212
225
  fontWeight: 600,
213
- color: "#f18a00",
226
+ color: ACCENT,
214
227
  textDecoration: "none",
215
228
  fontFamily: font,
216
229
  }, 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" }) })] })] }))] })] }));
@@ -5,12 +5,23 @@ export interface ProjectPortfolioProps {
5
5
  apiBase: string;
6
6
  /** Base path for project detail links. Defaults to "/projects" */
7
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>;
8
14
  /**
9
15
  * Seconds to cache via Next.js Data Cache on production deployments.
10
16
  * React.cache() always deduplicates within a single render in all environments.
11
17
  * Defaults to 60.
12
18
  */
13
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;
14
25
  }
15
26
  /**
16
27
  * ProjectPortfolio — pure self-fetching card grid.
@@ -27,5 +38,5 @@ export interface ProjectPortfolioProps {
27
38
  * apiBase="https://your-api.com"
28
39
  * />
29
40
  */
30
- export declare function ProjectPortfolio({ clientSlug, apiBase, basePath, revalidate, }: ProjectPortfolioProps): Promise<import("react/jsx-runtime").JSX.Element>;
41
+ export declare function ProjectPortfolio({ clientSlug, apiBase, basePath, searchParams, revalidate, noCache, }: ProjectPortfolioProps): Promise<import("react/jsx-runtime").JSX.Element>;
31
42
  //# sourceMappingURL=ProjectPortfolio.d.ts.map
@@ -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,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAiDD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,UAAe,GAChB,EAAE,qBAAqB,oDAkEvB"}
1
+ {"version":3,"file":"ProjectPortfolio.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAA;IAC5D;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AA6ED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,YAAiB,EACjB,UAAe,EACf,OAAe,GAChB,EAAE,qBAAqB,oDA+HvB"}
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cache } from "react";
3
3
  import { ProjectCard } from "./ProjectCard";
4
4
  // Two-layer caching strategy:
@@ -6,10 +6,22 @@ import { ProjectCard } from "./ProjectCard";
6
6
  // If this component appears twice on one page, the API is only hit once.
7
7
  // 2. next: { revalidate } — Next.js Data Cache, caches across multiple requests
8
8
  // on production deployments. Silently ignored in preview/local.
9
- const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate) => {
10
- var _a, _b, _c, _d, _e;
11
- const fetchOpts = { next: { revalidate } };
12
- const res = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts);
9
+ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate, filtersKey = "{}", noCache = false) => {
10
+ var _a, _b, _c, _d, _e, _f, _g;
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
+ const [res, fieldsRes] = await Promise.all([
22
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?${params.toString()}`, fetchOpts),
23
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts),
24
+ ]);
13
25
  const json = res.ok
14
26
  ? await res.json()
15
27
  : { client: { name: "Projects", description: null, custom_fields_schema: [] }, data: [] };
@@ -21,10 +33,25 @@ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate) => {
21
33
  seen.add(f.key);
22
34
  return true;
23
35
  });
36
+ // Build fieldOptionsMap: { fieldKey: { id: label, label: label } }
37
+ // Both id and label key to label so we normalize whichever shape is stored
38
+ const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
39
+ const fieldOptionsMap = {};
40
+ for (const field of ((_c = fieldsJson.fields) !== null && _c !== void 0 ? _c : [])) {
41
+ const map = {};
42
+ for (const opt of ((_d = field.options) !== null && _d !== void 0 ? _d : [])) {
43
+ if (typeof opt === "object" && opt.id && opt.label) {
44
+ map[opt.id] = opt.label;
45
+ map[opt.label] = opt.label;
46
+ }
47
+ }
48
+ fieldOptionsMap[field.key] = map;
49
+ }
24
50
  return {
25
- clientName: (_d = (_c = json.client) === null || _c === void 0 ? void 0 : _c.name) !== null && _d !== void 0 ? _d : "Projects",
26
- projects: (_e = json.data) !== null && _e !== void 0 ? _e : [],
51
+ clientName: (_f = (_e = json.client) === null || _e === void 0 ? void 0 : _e.name) !== null && _f !== void 0 ? _f : "Projects",
52
+ projects: (_g = json.data) !== null && _g !== void 0 ? _g : [],
27
53
  schema,
54
+ fieldOptionsMap,
28
55
  };
29
56
  });
30
57
  /**
@@ -42,10 +69,30 @@ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate) => {
42
69
  * apiBase="https://your-api.com"
43
70
  * />
44
71
  */
45
- export async function ProjectPortfolio({ clientSlug, apiBase, basePath = "/projects", revalidate = 60, }) {
46
- const { projects, schema } = await fetchPortfolioData(apiBase, clientSlug, revalidate);
72
+ export async function ProjectPortfolio({ clientSlug, apiBase, basePath = "/projects", searchParams = {}, revalidate = 60, noCache = false, }) {
73
+ // Parse filter[key]=value from searchParams into { key: value }
74
+ const filters = {};
75
+ Object.entries(searchParams).forEach(([key, val]) => {
76
+ if (val === undefined)
77
+ return;
78
+ const match = key.match(/^filter\[(.+)\]$/);
79
+ if (match)
80
+ filters[match[1]] = Array.isArray(val) ? val[0] : val;
81
+ });
82
+ const filtersKey = JSON.stringify(filters);
83
+ const { projects, schema, fieldOptionsMap } = await fetchPortfolioData(apiBase, clientSlug, revalidate, filtersKey, noCache);
84
+ const hasFilters = Object.keys(filters).length > 0;
85
+ const activeFilterLabels = Object.entries(filters)
86
+ .map(([key, val]) => {
87
+ var _a;
88
+ const field = schema.find((f) => f.key === key);
89
+ // Try to find the display label from schema options
90
+ 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);
91
+ return optLabel && typeof optLabel === "object" ? optLabel.label : val;
92
+ })
93
+ .filter(Boolean);
47
94
  if (projects.length === 0) {
48
- return (_jsx("div", { style: { textAlign: "center", padding: "4rem 0" }, children: _jsx("p", { style: { color: "#71717a" }, children: "No projects found." }) }));
95
+ 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" }))] }));
49
96
  }
50
97
  return (_jsxs("div", { style: {
51
98
  width: "100%",
@@ -53,7 +100,29 @@ export async function ProjectPortfolio({ clientSlug, apiBase, basePath = "/proje
53
100
  margin: "0 auto",
54
101
  padding: "2rem 1rem",
55
102
  boxSizing: "border-box",
56
- }, children: [_jsx("style", { children: `
103
+ }, children: [hasFilters && (_jsxs("div", { style: {
104
+ display: "flex",
105
+ alignItems: "center",
106
+ gap: "12px",
107
+ marginBottom: "1.5rem",
108
+ padding: "10px 14px",
109
+ backgroundColor: "#fafafa",
110
+ border: "1px solid #e4e4e7",
111
+ flexWrap: "wrap",
112
+ }, children: [_jsx("span", { style: { fontSize: "13px", color: "#71717a" }, children: "Filtered by:" }), activeFilterLabels.map((label, i) => (_jsx("span", { style: {
113
+ fontSize: "13px",
114
+ fontWeight: 600,
115
+ color: "#18181b",
116
+ backgroundColor: "#f4f4f5",
117
+ padding: "2px 10px",
118
+ borderRadius: "999px",
119
+ }, children: label }, i))), _jsx("a", { href: basePath, style: {
120
+ marginLeft: "auto",
121
+ fontSize: "13px",
122
+ color: "#f18a00",
123
+ textDecoration: "none",
124
+ fontWeight: 500,
125
+ }, children: "Clear filters" })] })), _jsx("style", { children: `
57
126
  .chisel-project-grid {
58
127
  display: grid;
59
128
  grid-template-columns: 1fr;
@@ -84,5 +153,5 @@ export async function ProjectPortfolio({ clientSlug, apiBase, basePath = "/proje
84
153
  height: 220px;
85
154
  }
86
155
  }
87
- ` }), _jsx("div", { className: "chisel-project-grid", children: projects.map((project, index) => (_jsx(ProjectCard, { project: project, schema: schema, basePath: basePath, priority: index === 0 }, project.id))) })] }));
156
+ ` }), _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))) })] }));
88
157
  }
package/dist/types.d.ts CHANGED
@@ -1,8 +1,13 @@
1
+ export interface FieldOption {
2
+ id: string;
3
+ label: string;
4
+ url?: string | null;
5
+ }
1
6
  export interface CustomFieldSchema {
2
7
  key: string;
3
8
  name: string;
4
9
  type: "text" | "number" | "select" | "multi-select" | "location";
5
- options: string[];
10
+ options: string[] | FieldOption[];
6
11
  option_urls?: Record<string, string>;
7
12
  is_filterable: boolean;
8
13
  sort_order: number;
@@ -32,7 +37,7 @@ export interface Project {
32
37
  image_url: string | null;
33
38
  is_featured: boolean;
34
39
  is_published?: boolean;
35
- /** Completion year when returned by the API */
40
+ /** Completion or project year when provided by the API */
36
41
  year?: number | string | null;
37
42
  custom_field_values: Record<string, CustomFieldValue>;
38
43
  created_at: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC7B,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IAC7B,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-portfolio",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectDetail, ProjectMenu (megamenu), and GalleryCarousel. Pass a clientSlug and apiBase — done.",
5
5
  "keywords": ["nextjs", "react", "portfolio", "projects", "megamenu", "gallery"],
6
6
  "license": "MIT",