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 +35 -0
- package/dist/ProjectCard.d.ts +3 -1
- package/dist/ProjectCard.d.ts.map +1 -1
- package/dist/ProjectCard.js +8 -4
- package/dist/ProjectFilters.d.ts.map +1 -1
- package/dist/ProjectFilters.js +14 -1
- package/dist/ProjectMenu.d.ts +6 -1
- package/dist/ProjectMenu.d.ts.map +1 -1
- package/dist/ProjectMenu.js +30 -13
- package/dist/ProjectMenuClient.d.ts +3 -1
- package/dist/ProjectMenuClient.d.ts.map +1 -1
- package/dist/ProjectMenuClient.js +45 -32
- package/dist/ProjectPortfolio.d.ts +12 -1
- package/dist/ProjectPortfolio.d.ts.map +1 -1
- package/dist/ProjectPortfolio.js +81 -12
- package/dist/types.d.ts +7 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
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`
|
package/dist/ProjectCard.d.ts
CHANGED
|
@@ -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,
|
|
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"}
|
package/dist/ProjectCard.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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;
|
|
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"}
|
package/dist/ProjectFilters.js
CHANGED
|
@@ -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] }),
|
|
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: {
|
package/dist/ProjectMenu.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/ProjectMenu.js
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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":"
|
|
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
|
-
|
|
17
|
-
|
|
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
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
154
|
+
border: `1px solid ${isHovered ? ACCENT : "#e4e4e7"}`,
|
|
160
155
|
textDecoration: "none",
|
|
161
156
|
boxSizing: "border-box",
|
|
162
|
-
transition: "border-color 0.
|
|
163
|
-
},
|
|
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: {
|
|
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
|
-
|
|
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" }),
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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:
|
|
198
|
-
fontWeight:
|
|
210
|
+
color: "#52525b",
|
|
211
|
+
fontWeight: 400,
|
|
199
212
|
padding: "5px 0",
|
|
200
|
-
|
|
213
|
+
textDecoration: "none",
|
|
201
214
|
fontFamily: font,
|
|
202
215
|
transition: "color 0.15s",
|
|
203
|
-
|
|
204
|
-
|
|
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:
|
|
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;
|
|
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"}
|
package/dist/ProjectPortfolio.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 =
|
|
12
|
-
|
|
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: (
|
|
26
|
-
projects: (
|
|
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
|
-
|
|
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 (
|
|
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: [
|
|
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
|
|
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;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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",
|