project-portfolio 1.9.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -7
- package/dist/GalleryCarousel.d.ts +3 -2
- package/dist/GalleryCarousel.d.ts.map +1 -1
- package/dist/GalleryCarousel.js +1 -1
- package/dist/ProjectDetail.js +1 -1
- package/dist/ProjectMenuClient.d.ts.map +1 -1
- package/dist/ProjectMenuClient.js +112 -91
- package/dist/ProjectPortfolioClient.d.ts +21 -0
- package/dist/ProjectPortfolioClient.d.ts.map +1 -0
- package/dist/ProjectPortfolioClient.js +141 -0
- package/dist/SimilarProjects.d.ts.map +1 -1
- package/dist/SimilarProjects.js +7 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -45,9 +45,9 @@ export default async function ProjectsPage() {
|
|
|
45
45
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
46
46
|
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
47
47
|
| `searchParams` | `Record<string, string \| string[] \| undefined>` | No | `{}` | Filter params forwarded to the API — pass Next.js `searchParams` directly |
|
|
48
|
-
| `revalidate` | `number` | No | `
|
|
48
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
49
49
|
|
|
50
|
-
####
|
|
50
|
+
#### URL-driven filter integration
|
|
51
51
|
|
|
52
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
53
|
|
|
@@ -83,6 +83,69 @@ When active filters are applied, a filter banner is shown above the grid with a
|
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
86
|
+
### `ProjectPortfolioClient`
|
|
87
|
+
|
|
88
|
+
A `"use client"` component that fetches all projects once on mount (module-level cached — no re-fetch on remount) and filters them locally in memory. It ships with **no filter UI** — build your own dropdowns, buttons, or search inputs and drive the component with the `filters` prop.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
// Works in any component — no RSC required
|
|
92
|
+
import { ProjectPortfolioClient } from "project-portfolio"
|
|
93
|
+
|
|
94
|
+
export default function ProjectsPage() {
|
|
95
|
+
return (
|
|
96
|
+
<ProjectPortfolioClient
|
|
97
|
+
clientSlug="your-client-slug"
|
|
98
|
+
apiBase="https://your-api.com"
|
|
99
|
+
basePath="/projects"
|
|
100
|
+
/>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Wire up your own filter UI with the `filters` prop:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
"use client"
|
|
109
|
+
import { useState } from "react"
|
|
110
|
+
import { ProjectPortfolioClient } from "project-portfolio"
|
|
111
|
+
|
|
112
|
+
export default function ProjectsPage() {
|
|
113
|
+
const [filters, setFilters] = useState<Record<string, string>>({})
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<>
|
|
117
|
+
{/* Your own filter UI */}
|
|
118
|
+
<select onChange={(e) => setFilters({ type: e.target.value })}>
|
|
119
|
+
<option value="">All Types</option>
|
|
120
|
+
<option value="commercial">Commercial</option>
|
|
121
|
+
<option value="educational-facilities">Educational</option>
|
|
122
|
+
</select>
|
|
123
|
+
|
|
124
|
+
{/* Component receives filters and applies them in memory */}
|
|
125
|
+
<ProjectPortfolioClient
|
|
126
|
+
clientSlug="your-client-slug"
|
|
127
|
+
apiBase="https://your-api.com"
|
|
128
|
+
basePath="/projects"
|
|
129
|
+
filters={filters}
|
|
130
|
+
/>
|
|
131
|
+
</>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
| Prop | Type | Required | Default | Description |
|
|
137
|
+
|---|---|---|---|---|
|
|
138
|
+
| `clientSlug` | `string` | Yes | — | Identifies which client's projects to load |
|
|
139
|
+
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
140
|
+
| `basePath` | `string` | No | `"/projects"` | Base path for project detail links |
|
|
141
|
+
| `filters` | `Record<string, string>` | No | `{}` | Active filters keyed by custom field key — filtering is instant, no API call on change |
|
|
142
|
+
| `columns` | `2 \| 3` | No | `3` | Number of columns in the project grid |
|
|
143
|
+
| `font` | `string` | No | System font stack | Font family string applied to all text |
|
|
144
|
+
|
|
145
|
+
**How filtering works:** Pass any `Record<string, string>` of field key/value pairs and the component filters the already-fetched project list in memory. All filter logic uses AND matching with case-insensitive comparison. The field keys must match the custom field keys in the API schema.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
86
149
|
### `ProjectDetail`
|
|
87
150
|
|
|
88
151
|
A full project detail page. Fetches a single project by slug and renders its gallery, custom fields, related projects, and a back link.
|
|
@@ -112,7 +175,7 @@ export default async function ProjectPage({ params }: { params: { slug: string }
|
|
|
112
175
|
| `apiBase` | `string` | Yes | — | Base URL of the projects API |
|
|
113
176
|
| `backPath` | `string` | No | `"/projects"` | Path for the back navigation link |
|
|
114
177
|
| `backLabel` | `string` | No | `"All Projects"` | Label for the back navigation link |
|
|
115
|
-
| `revalidate` | `number` | No | `
|
|
178
|
+
| `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
|
|
116
179
|
|
|
117
180
|
---
|
|
118
181
|
|
|
@@ -256,9 +319,9 @@ import { ProjectMenuClient } from "project-portfolio"
|
|
|
256
319
|
| `apiBase` | `string` | No* | — | API base URL for direct fetch mode |
|
|
257
320
|
| `basePath` | `string` | Yes | — | Base path for project detail links |
|
|
258
321
|
| `viewAllPath` | `string` | Yes | — | Path for the "View All Projects" link |
|
|
259
|
-
| `subtitle` | `string` | No | — | Description shown above the project cards |
|
|
322
|
+
| `subtitle` | `string` | No | — | Description shown above the project cards (hidden on mobile) |
|
|
260
323
|
| `font` | `string` | No | System font stack | Font family string |
|
|
261
|
-
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display |
|
|
324
|
+
| `maxProjects` | `number` | No | `6` | Maximum number of projects to display (capped at 3 on mobile) |
|
|
262
325
|
|
|
263
326
|
*One of `dataUrl` or `clientSlug + apiBase` must be provided. Pre-fetched `projects`, `schema`, `filterOptions`, `filterFieldKey`, and `fieldOptionsMap` can also be passed directly for SSR usage.
|
|
264
327
|
|
|
@@ -297,8 +360,8 @@ Data is cached at multiple levels depending on which component is used.
|
|
|
297
360
|
3. **Server-side route cache** — the `/api/chisel-menu` route is cached by Next.js for 24 hours. The upstream API is called at most once per day regardless of traffic
|
|
298
361
|
4. **Module-level client cache** — after the first fetch within a session, the result is held in memory. Re-hovering the menu or remounting the component never triggers another network request
|
|
299
362
|
|
|
300
|
-
**`ProjectMenuClient` with direct fetch
|
|
301
|
-
4. **Module-level client cache only** — the upstream API is called once per session per user (on first mount), then cached in memory for the rest of that page session
|
|
363
|
+
**`ProjectMenuClient` with direct fetch / `ProjectPortfolioClient`**
|
|
364
|
+
4. **Module-level client cache only** — the upstream API is called once per session per user (on first mount), then cached in memory for the rest of that page session. Filter changes on `ProjectPortfolioClient` never trigger a fetch — filtering is done entirely in memory
|
|
302
365
|
|
|
303
366
|
**Bypassing the cache**
|
|
304
367
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Media } from "./types";
|
|
2
|
-
export
|
|
2
|
+
export interface GalleryCarouselProps {
|
|
3
3
|
images: Media[];
|
|
4
4
|
projectTitle: string;
|
|
5
|
-
}
|
|
5
|
+
}
|
|
6
|
+
export declare function GalleryCarousel({ images, projectTitle, }: GalleryCarouselProps): import("react/jsx-runtime").JSX.Element | null;
|
|
6
7
|
//# sourceMappingURL=GalleryCarousel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,
|
|
1
|
+
{"version":3,"file":"GalleryCarousel.d.ts","sourceRoot":"","sources":["../src/GalleryCarousel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAIpC,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,YAAY,GACb,EAAE,oBAAoB,kDAwJtB"}
|
package/dist/GalleryCarousel.js
CHANGED
|
@@ -65,7 +65,7 @@ export function GalleryCarousel({ images, projectTitle, }) {
|
|
|
65
65
|
margin: 0,
|
|
66
66
|
padding: "0 1rem",
|
|
67
67
|
fontFamily: font,
|
|
68
|
-
}, children: caption })), total > 1 && (_jsx("div", { style: { display: "flex", gap: "8px",
|
|
68
|
+
}, children: caption })), total > 1 && (_jsx("div", { style: { display: "flex", gap: "8px", overflowX: "auto", WebkitOverflowScrolling: "touch", paddingBottom: "4px" }, children: images.map((img, i) => {
|
|
69
69
|
var _a;
|
|
70
70
|
return (_jsx("button", { onClick: () => setCurrent(i), "aria-label": `View image ${i + 1}`, style: {
|
|
71
71
|
width: "72px",
|
package/dist/ProjectDetail.js
CHANGED
|
@@ -83,7 +83,7 @@ export async function ProjectDetail({ slug, clientSlug, apiBase, backPath = "/pr
|
|
|
83
83
|
return isNaN(n) ? String(raw) : String(Math.round(n));
|
|
84
84
|
})();
|
|
85
85
|
const font = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
86
|
-
return (_jsxs("main", { style: { minHeight: "100vh", backgroundColor: "#fff", fontFamily: font }, children: [_jsxs("section", { className: "chisel-hero-img", style: { position: "relative", width: "100%", overflow: "hidden" }, children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })) : (_jsx("div", { style: { position: "absolute", inset: 0, backgroundColor: "#27272a" } })), _jsx("div", { style: { position: "absolute", inset: 0, background: "linear-gradient(to top, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.38) 50%, rgba(0,0,0,0.12) 100%)" } }), _jsxs("div", { style: { position: "absolute", bottom: 0, left: 0, right: 0, padding: "0 1.5rem
|
|
86
|
+
return (_jsxs("main", { style: { minHeight: "100vh", backgroundColor: "#fff", fontFamily: font }, children: [_jsxs("section", { className: "chisel-hero-img", style: { position: "relative", width: "100%", overflow: "hidden" }, children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })) : (_jsx("div", { style: { position: "absolute", inset: 0, backgroundColor: "#27272a" } })), _jsx("div", { style: { position: "absolute", inset: 0, background: "linear-gradient(to top, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.38) 50%, rgba(0,0,0,0.12) 100%)" } }), _jsxs("div", { style: { position: "absolute", bottom: 0, left: 0, right: 0, padding: "0 1rem 1.5rem", maxWidth: "900px" }, children: [badgeValue && (_jsx("p", { style: { color: "#f18a00", fontSize: "11px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", margin: "0 0 10px 0" }, children: badgeValue })), _jsx("h1", { style: { color: "#fff", fontWeight: 700, fontSize: "clamp(28px, 4vw, 52px)", lineHeight: 1.1, margin: "0 0 12px 0", fontFamily: font }, children: project.title }), locationString && (_jsxs("p", { style: { display: "flex", alignItems: "center", gap: "6px", color: "rgba(255,255,255,0.75)", fontSize: "15px", margin: 0 }, children: [_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0 }, children: [_jsx("path", { d: "M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" }), _jsx("circle", { cx: "12", cy: "10", r: "3" })] }), locationString] }))] })] }), _jsxs("section", { style: { borderBottom: "1px solid #e4e4e7", backgroundColor: "#fff" }, children: [_jsx("style", { children: `
|
|
87
87
|
.chisel-stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1.25rem; }
|
|
88
88
|
@media (min-width: 768px) { .chisel-stats-grid { grid-template-columns: repeat(4, 1fr); gap: 2rem; } }
|
|
89
89
|
.chisel-gallery-placeholder { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAa3E,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC/C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAmBD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,UAAU,EACV,OAAO,EACP,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,iBAAiB,EAChC,cAAc,EAAE,kBAAkB,EAClC,eAAe,EAAE,mBAAoC,EACrD,eAAe,EAAE,mBAAwB,EACzC,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAmB,EACnB,WAAe,GAChB,EAAE,sBAAsB,
|
|
1
|
+
{"version":3,"file":"ProjectMenuClient.d.ts","sourceRoot":"","sources":["../src/ProjectMenuClient.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAoB,MAAM,SAAS,CAAA;AAa3E,MAAM,WAAW,sBAAsB;IACrC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAA;IACpB,MAAM,CAAC,EAAE,iBAAiB,EAAE,CAAA;IAC5B,aAAa,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IAC/C,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAmBD,wBAAgB,iBAAiB,CAAC,EAChC,OAAO,EACP,UAAU,EACV,OAAO,EACP,QAAQ,EAAE,YAAY,EACtB,MAAM,EAAE,UAAU,EAClB,aAAa,EAAE,iBAAiB,EAChC,cAAc,EAAE,kBAAkB,EAClC,eAAe,EAAE,mBAAoC,EACrD,eAAe,EAAE,mBAAwB,EACzC,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,IAAmB,EACnB,WAAe,GAChB,EAAE,sBAAsB,2CAqcxB"}
|
|
@@ -118,7 +118,7 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
|
|
|
118
118
|
font-family: ${font};
|
|
119
119
|
width: 100%;
|
|
120
120
|
box-sizing: border-box;
|
|
121
|
-
padding: 1.
|
|
121
|
+
padding: 1.25rem 1rem;
|
|
122
122
|
}
|
|
123
123
|
@media (min-width: 768px) {
|
|
124
124
|
.chisel-menu-outer {
|
|
@@ -127,19 +127,23 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/* ── Left column ── */
|
|
130
131
|
.chisel-menu-left {
|
|
131
132
|
flex: 1;
|
|
132
133
|
min-width: 0;
|
|
133
134
|
padding-right: 0;
|
|
134
|
-
padding-bottom: 1.
|
|
135
|
+
padding-bottom: 1.25rem;
|
|
136
|
+
border-bottom: 1px solid #e4e4e7;
|
|
135
137
|
}
|
|
136
138
|
@media (min-width: 768px) {
|
|
137
139
|
.chisel-menu-left {
|
|
138
140
|
padding-right: 2.5rem;
|
|
139
141
|
padding-bottom: 0;
|
|
142
|
+
border-bottom: none;
|
|
140
143
|
}
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
/* ── Vertical divider — desktop only ── */
|
|
143
147
|
.chisel-menu-divider-v {
|
|
144
148
|
display: none;
|
|
145
149
|
}
|
|
@@ -153,79 +157,134 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
|
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
/* ── Right column ── */
|
|
156
161
|
.chisel-menu-right {
|
|
157
162
|
width: 100%;
|
|
158
163
|
padding-left: 0;
|
|
159
|
-
padding-top: 1.
|
|
160
|
-
border-top: 1px solid #e4e4e7;
|
|
164
|
+
padding-top: 1.25rem;
|
|
161
165
|
}
|
|
162
166
|
@media (min-width: 768px) {
|
|
163
167
|
.chisel-menu-right {
|
|
164
|
-
width:
|
|
168
|
+
width: 220px;
|
|
165
169
|
flex-shrink: 0;
|
|
166
170
|
padding-left: 2.5rem;
|
|
167
171
|
padding-top: 0;
|
|
168
|
-
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
@media (min-width: 1024px) {
|
|
175
|
+
.chisel-menu-right {
|
|
176
|
+
width: 280px;
|
|
169
177
|
}
|
|
170
178
|
}
|
|
171
179
|
|
|
180
|
+
/* ── Project card grid ──
|
|
181
|
+
Mobile: 1 column — cards get full width, titles don't wrap
|
|
182
|
+
Desktop: 2 columns */
|
|
172
183
|
.chisel-menu-card-grid {
|
|
173
184
|
display: grid;
|
|
174
185
|
grid-template-columns: 1fr;
|
|
175
|
-
gap:
|
|
186
|
+
gap: 6px;
|
|
176
187
|
}
|
|
177
|
-
@media (min-width:
|
|
188
|
+
@media (min-width: 768px) {
|
|
178
189
|
.chisel-menu-card-grid {
|
|
179
190
|
grid-template-columns: repeat(2, 1fr);
|
|
180
|
-
gap:
|
|
191
|
+
gap: 10px;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Hide cards beyond the 3rd on mobile */
|
|
196
|
+
@media (max-width: 767px) {
|
|
197
|
+
.chisel-menu-card:nth-child(n+4) {
|
|
198
|
+
display: none;
|
|
181
199
|
}
|
|
182
200
|
}
|
|
183
201
|
|
|
184
|
-
|
|
202
|
+
/* Card — always horizontal side-by-side */
|
|
203
|
+
.chisel-menu-card {
|
|
185
204
|
display: flex;
|
|
205
|
+
flex-direction: row;
|
|
186
206
|
align-items: center;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
border:
|
|
190
|
-
|
|
207
|
+
gap: 12px;
|
|
208
|
+
padding: 10px 12px;
|
|
209
|
+
border: 1px solid #e4e4e7;
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
color: inherit;
|
|
212
|
+
box-sizing: border-box;
|
|
213
|
+
transition: border-color 0.2s;
|
|
191
214
|
cursor: pointer;
|
|
192
|
-
|
|
193
|
-
|
|
215
|
+
overflow: hidden;
|
|
216
|
+
}
|
|
217
|
+
.chisel-menu-card:hover {
|
|
218
|
+
border-color: ${ACCENT};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.chisel-menu-card-thumb {
|
|
222
|
+
width: 72px;
|
|
223
|
+
height: 54px;
|
|
224
|
+
overflow: hidden;
|
|
225
|
+
background-color: #f4f4f5;
|
|
226
|
+
flex-shrink: 0;
|
|
194
227
|
}
|
|
195
228
|
@media (min-width: 768px) {
|
|
196
|
-
.chisel-menu-
|
|
197
|
-
|
|
229
|
+
.chisel-menu-card-thumb {
|
|
230
|
+
width: 72px;
|
|
231
|
+
height: 54px;
|
|
198
232
|
}
|
|
199
233
|
}
|
|
200
234
|
|
|
201
|
-
.chisel-menu-
|
|
202
|
-
display: none;
|
|
203
|
-
}
|
|
204
|
-
.chisel-menu-filters-list.open {
|
|
235
|
+
.chisel-menu-card-body {
|
|
205
236
|
display: flex;
|
|
206
237
|
flex-direction: column;
|
|
207
238
|
gap: 2px;
|
|
239
|
+
flex: 1;
|
|
240
|
+
min-width: 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/* ── Subtitle — hidden on mobile, shown on desktop ── */
|
|
244
|
+
.chisel-menu-subtitle {
|
|
245
|
+
display: none !important;
|
|
208
246
|
}
|
|
209
247
|
@media (min-width: 768px) {
|
|
210
|
-
.chisel-menu-
|
|
211
|
-
display:
|
|
212
|
-
flex-direction: column;
|
|
213
|
-
gap: 2px;
|
|
248
|
+
.chisel-menu-subtitle {
|
|
249
|
+
display: block !important;
|
|
214
250
|
}
|
|
215
251
|
}
|
|
252
|
+
|
|
253
|
+
/* ── Filter links — always visible, no toggle needed ── */
|
|
254
|
+
.chisel-menu-filter-link {
|
|
255
|
+
all: unset;
|
|
256
|
+
cursor: pointer;
|
|
257
|
+
display: flex;
|
|
258
|
+
align-items: center;
|
|
259
|
+
justify-content: space-between;
|
|
260
|
+
font-size: 14px;
|
|
261
|
+
color: #52525b;
|
|
262
|
+
font-weight: 400;
|
|
263
|
+
padding: 7px 0;
|
|
264
|
+
text-decoration: none;
|
|
265
|
+
transition: color 0.15s;
|
|
266
|
+
border-bottom: 1px solid #f4f4f5;
|
|
267
|
+
}
|
|
268
|
+
.chisel-menu-filter-link:last-child {
|
|
269
|
+
border-bottom: none;
|
|
270
|
+
}
|
|
271
|
+
.chisel-menu-filter-link:hover {
|
|
272
|
+
color: #18181b;
|
|
273
|
+
}
|
|
216
274
|
` }), _jsxs("div", { className: "chisel-menu-outer", children: [_jsxs("div", { className: "chisel-menu-left", children: [_jsx("p", { style: {
|
|
217
|
-
fontSize: "
|
|
275
|
+
fontSize: "11px",
|
|
218
276
|
fontWeight: 700,
|
|
219
277
|
textTransform: "uppercase",
|
|
220
278
|
letterSpacing: "0.12em",
|
|
221
279
|
color: "#71717a",
|
|
222
|
-
margin: "0 0
|
|
280
|
+
margin: "0 0 12px 0",
|
|
223
281
|
}, children: "Featured Projects" }), subtitle && (_jsx("p", { style: {
|
|
224
|
-
fontSize: "
|
|
282
|
+
fontSize: "14px",
|
|
225
283
|
color: "#52525b",
|
|
226
|
-
lineHeight: 1.
|
|
227
|
-
margin: "0 0
|
|
228
|
-
|
|
284
|
+
lineHeight: 1.5,
|
|
285
|
+
margin: "0 0 16px 0",
|
|
286
|
+
display: "none",
|
|
287
|
+
}, 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) => {
|
|
229
288
|
var _a, _b, _c, _d, _e, _f;
|
|
230
289
|
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;
|
|
231
290
|
const badgeRaw = badgeField ? parseSingleValue(project.custom_field_values[badgeField.key]) : null;
|
|
@@ -234,78 +293,40 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
|
|
|
234
293
|
const tags = tagsField ? parseMultiValue(project.custom_field_values[tagsField.key]) : [];
|
|
235
294
|
const href = `${basePath}/${project.slug}`;
|
|
236
295
|
const isHovered = hoveredCard === project.id;
|
|
237
|
-
return (_jsxs("a", { href: href, onMouseEnter: () => setHoveredCard(project.id), onMouseLeave: () => setHoveredCard(null), style: {
|
|
238
|
-
all: "unset",
|
|
239
|
-
cursor: "pointer",
|
|
240
|
-
display: "flex",
|
|
241
|
-
alignItems: "flex-start",
|
|
242
|
-
gap: "12px",
|
|
243
|
-
padding: "12px",
|
|
244
|
-
border: `1px solid ${isHovered ? ACCENT : "#e4e4e7"}`,
|
|
245
|
-
textDecoration: "none",
|
|
246
|
-
boxSizing: "border-box",
|
|
247
|
-
transition: "border-color 0.2s",
|
|
248
|
-
}, children: [_jsx("div", { style: {
|
|
249
|
-
width: "80px",
|
|
250
|
-
height: "64px",
|
|
251
|
-
flexShrink: 0,
|
|
252
|
-
overflow: "hidden",
|
|
253
|
-
backgroundColor: "#f4f4f5",
|
|
254
|
-
}, children: imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: {
|
|
296
|
+
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: {
|
|
255
297
|
width: "100%",
|
|
256
298
|
height: "100%",
|
|
257
299
|
objectFit: "cover",
|
|
258
300
|
display: "block",
|
|
259
|
-
transform: isHovered ? "scale(1.
|
|
301
|
+
transform: isHovered ? "scale(1.05)" : "scale(1)",
|
|
260
302
|
transition: "transform 0.3s ease",
|
|
261
|
-
} })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", {
|
|
262
|
-
fontSize: "
|
|
303
|
+
} })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", { className: "chisel-menu-card-body", children: [_jsx("p", { style: {
|
|
304
|
+
fontSize: "13px",
|
|
263
305
|
fontWeight: 700,
|
|
264
306
|
color: isHovered ? ACCENT : "#18181b",
|
|
265
307
|
margin: 0,
|
|
266
308
|
lineHeight: 1.3,
|
|
267
309
|
fontFamily: font,
|
|
268
310
|
transition: "color 0.2s",
|
|
269
|
-
}, children: project.title }), badge && (_jsx("p", { style: { fontSize: "
|
|
311
|
+
}, children: project.title }), badge && (_jsx("p", { style: { fontSize: "12px", color: "#71717a", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (() => {
|
|
270
312
|
var _a;
|
|
271
313
|
const optMap = tagsField ? ((_a = fieldOptionsMap[tagsField.key]) !== null && _a !== void 0 ? _a : {}) : {};
|
|
272
|
-
const tagLabels = tags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; });
|
|
273
|
-
|
|
274
|
-
const display = tagLabels.length > 3
|
|
275
|
-
? shown.join(" · ") + " · ..."
|
|
276
|
-
: shown.join(" · ");
|
|
277
|
-
return (_jsx("p", { style: { fontSize: "12px", color: ACCENT, margin: 0, fontFamily: font, lineHeight: 1.4 }, children: display }));
|
|
314
|
+
const tagLabels = tags.map((t) => { var _a; return (_a = optMap[t]) !== null && _a !== void 0 ? _a : t; }).slice(0, 2);
|
|
315
|
+
return (_jsx("p", { style: { fontSize: "11px", color: ACCENT, margin: 0, fontFamily: font, lineHeight: 1.4 }, children: tagLabels.join(" · ") }));
|
|
278
316
|
})()] })] }, project.id));
|
|
279
|
-
}) }))] }), _jsx("div", { className: "chisel-menu-divider-v" }),
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
}, 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) => {
|
|
293
|
-
const href = filterFieldKey
|
|
294
|
-
? `${basePath}?filter[${filterFieldKey}]=${encodeURIComponent(opt.id)}`
|
|
295
|
-
: basePath;
|
|
296
|
-
return (_jsx("a", { href: href, style: {
|
|
297
|
-
all: "unset",
|
|
298
|
-
cursor: "pointer",
|
|
299
|
-
fontSize: "14px",
|
|
300
|
-
color: "#52525b",
|
|
301
|
-
fontWeight: 400,
|
|
302
|
-
padding: "5px 0",
|
|
303
|
-
textDecoration: "none",
|
|
304
|
-
fontFamily: font,
|
|
305
|
-
transition: "color 0.15s",
|
|
306
|
-
display: "block",
|
|
307
|
-
}, onMouseEnter: (e) => (e.currentTarget.style.color = "#18181b"), onMouseLeave: (e) => (e.currentTarget.style.color = "#52525b"), children: opt.label }, opt.id));
|
|
308
|
-
}) }), _jsx("div", { style: { height: "1px", backgroundColor: "#e4e4e7", margin: "20px 0" } }), _jsxs("a", { href: viewAllPath, style: {
|
|
317
|
+
}) }))] }), _jsx("div", { className: "chisel-menu-divider-v" }), _jsxs("div", { className: "chisel-menu-right", children: [filterOptions.length > 0 && (_jsxs(_Fragment, { children: [_jsxs("p", { style: {
|
|
318
|
+
fontSize: "11px",
|
|
319
|
+
fontWeight: 700,
|
|
320
|
+
textTransform: "uppercase",
|
|
321
|
+
letterSpacing: "0.12em",
|
|
322
|
+
color: "#71717a",
|
|
323
|
+
margin: "0 0 4px 0",
|
|
324
|
+
}, children: ["Browse By ", filterFieldName] }), _jsx("div", { style: { display: "flex", flexDirection: "column", marginBottom: "20px" }, children: filterOptions.map((opt) => {
|
|
325
|
+
const href = filterFieldKey
|
|
326
|
+
? `${basePath}?filter[${filterFieldKey}]=${encodeURIComponent(opt.id)}`
|
|
327
|
+
: basePath;
|
|
328
|
+
return (_jsxs("a", { href: href, className: "chisel-menu-filter-link", style: { fontFamily: font }, children: [opt.label, _jsx("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0, opacity: 0.4 }, children: _jsx("path", { d: "M9 18l6-6-6-6" }) })] }, opt.id));
|
|
329
|
+
}) }), _jsx("div", { style: { height: "1px", backgroundColor: "#e4e4e7", marginBottom: "20px" } })] })), _jsxs("a", { href: viewAllPath, style: {
|
|
309
330
|
all: "unset",
|
|
310
331
|
cursor: "pointer",
|
|
311
332
|
display: "inline-flex",
|
|
@@ -316,5 +337,5 @@ export function ProjectMenuClient({ dataUrl, clientSlug, apiBase, projects: proj
|
|
|
316
337
|
color: ACCENT,
|
|
317
338
|
textDecoration: "none",
|
|
318
339
|
fontFamily: font,
|
|
319
|
-
}, 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" }) })] })] })
|
|
340
|
+
}, 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" }) })] })] })] })] }));
|
|
320
341
|
}
|
|
@@ -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"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect, useMemo } from "react";
|
|
4
|
+
import { ProjectCard } from "./ProjectCard";
|
|
5
|
+
const portfolioDataCache = new Map();
|
|
6
|
+
const API_KEY = "pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR";
|
|
7
|
+
const DEFAULT_FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
8
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
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
|
+
function matchesFilters(project, filters, schema, fieldOptionsMap) {
|
|
17
|
+
return Object.entries(filters).every(([key, value]) => {
|
|
18
|
+
var _a;
|
|
19
|
+
if (!value)
|
|
20
|
+
return true;
|
|
21
|
+
const field = schema.find((f) => f.key === key);
|
|
22
|
+
if (!field)
|
|
23
|
+
return true;
|
|
24
|
+
const raw = project.custom_field_values[key];
|
|
25
|
+
if (field.type === "location") {
|
|
26
|
+
const loc = raw;
|
|
27
|
+
if (!loc)
|
|
28
|
+
return false;
|
|
29
|
+
const locStr = [loc.city, loc.state].filter(Boolean).join(", ").toLowerCase();
|
|
30
|
+
return locStr.includes(value.toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
const values = parseMultiValue(raw);
|
|
33
|
+
const optMap = (_a = fieldOptionsMap[key]) !== null && _a !== void 0 ? _a : {};
|
|
34
|
+
return values.some((v) => {
|
|
35
|
+
var _a;
|
|
36
|
+
const normalizedV = ((_a = optMap[v]) !== null && _a !== void 0 ? _a : v).toLowerCase();
|
|
37
|
+
const normalizedFilter = value.toLowerCase();
|
|
38
|
+
return normalizedV === normalizedFilter || normalizedV.includes(normalizedFilter);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
43
|
+
export function ProjectPortfolioClient({ clientSlug, apiBase, basePath = "/projects", filters = {}, font = DEFAULT_FONT, columns = 3, }) {
|
|
44
|
+
const [data, setData] = useState(null);
|
|
45
|
+
// Self-fetch on mount — uses module-level cache so the API is only called once per page load
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const cacheKey = `${clientSlug}:${apiBase}`;
|
|
48
|
+
async function fetchAndCache() {
|
|
49
|
+
var _a, _b, _c, _d, _e;
|
|
50
|
+
const [projectsRes, fieldsRes] = await Promise.all([
|
|
51
|
+
fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=${API_KEY}`),
|
|
52
|
+
fetch(`${apiBase}/api/v1/clients/${clientSlug}/fields?api_key=${API_KEY}`),
|
|
53
|
+
]);
|
|
54
|
+
const json = projectsRes.ok ? await projectsRes.json() : {};
|
|
55
|
+
const projects = (_a = json === null || json === void 0 ? void 0 : json.data) !== null && _a !== void 0 ? _a : [];
|
|
56
|
+
// Deduplicate schema keys
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const schema = ((_c = (_b = json === null || json === void 0 ? void 0 : json.client) === null || _b === void 0 ? void 0 : _b.custom_fields_schema) !== null && _c !== void 0 ? _c : []).filter((f) => {
|
|
59
|
+
if (seen.has(f.key))
|
|
60
|
+
return false;
|
|
61
|
+
seen.add(f.key);
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
const fieldsJson = fieldsRes.ok ? await fieldsRes.json() : { fields: [] };
|
|
65
|
+
const fieldOptionsMap = {};
|
|
66
|
+
for (const field of ((_d = fieldsJson.fields) !== null && _d !== void 0 ? _d : [])) {
|
|
67
|
+
const map = {};
|
|
68
|
+
for (const opt of ((_e = field.options) !== null && _e !== void 0 ? _e : [])) {
|
|
69
|
+
if (typeof opt === "object" && opt.id && opt.label) {
|
|
70
|
+
map[opt.id] = opt.label;
|
|
71
|
+
map[opt.label] = opt.label;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
fieldOptionsMap[field.key] = map;
|
|
75
|
+
}
|
|
76
|
+
return { projects, schema, fieldOptionsMap };
|
|
77
|
+
}
|
|
78
|
+
if (!portfolioDataCache.has(cacheKey)) {
|
|
79
|
+
portfolioDataCache.set(cacheKey, fetchAndCache());
|
|
80
|
+
}
|
|
81
|
+
let cancelled = false;
|
|
82
|
+
portfolioDataCache.get(cacheKey).then((result) => {
|
|
83
|
+
if (!cancelled)
|
|
84
|
+
setData(result);
|
|
85
|
+
}).catch(() => {
|
|
86
|
+
if (!cancelled)
|
|
87
|
+
setData({ projects: [], schema: [], fieldOptionsMap: {} });
|
|
88
|
+
});
|
|
89
|
+
return () => { cancelled = true; };
|
|
90
|
+
}, [clientSlug, apiBase]);
|
|
91
|
+
// Filter projects locally — instant, no API call on filter change
|
|
92
|
+
const filteredProjects = useMemo(() => {
|
|
93
|
+
if (!data)
|
|
94
|
+
return [];
|
|
95
|
+
const hasActiveFilters = Object.values(filters).some(Boolean);
|
|
96
|
+
if (!hasActiveFilters)
|
|
97
|
+
return data.projects;
|
|
98
|
+
return data.projects.filter((p) => matchesFilters(p, filters, data.schema, data.fieldOptionsMap));
|
|
99
|
+
}, [data, filters]);
|
|
100
|
+
const gridCols = columns === 2
|
|
101
|
+
? "repeat(2, 1fr)"
|
|
102
|
+
: "repeat(3, 1fr)";
|
|
103
|
+
// Loading state
|
|
104
|
+
if (!data) {
|
|
105
|
+
return (_jsxs("div", { style: {
|
|
106
|
+
width: "100%",
|
|
107
|
+
maxWidth: "1280px",
|
|
108
|
+
margin: "0 auto",
|
|
109
|
+
padding: "2rem 1rem",
|
|
110
|
+
fontFamily: font,
|
|
111
|
+
}, children: [_jsx("style", { children: `
|
|
112
|
+
@keyframes pulse { 0%, 100% { opacity: 1 } 50% { opacity: 0.5 } }
|
|
113
|
+
.chisel-skeleton-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
|
|
114
|
+
@media (min-width: 640px) { .chisel-skeleton-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
115
|
+
@media (min-width: 1024px) { .chisel-skeleton-grid { grid-template-columns: ${gridCols}; gap: 2rem; } }
|
|
116
|
+
` }), _jsx("div", { className: "chisel-skeleton-grid", children: Array.from({ length: 6 }).map((_, i) => (_jsx("div", { style: { backgroundColor: "#f4f4f5", borderRadius: 2, height: 320, animation: "pulse 1.5s ease-in-out infinite" } }, i))) })] }));
|
|
117
|
+
}
|
|
118
|
+
return (_jsxs("div", { style: {
|
|
119
|
+
width: "100%",
|
|
120
|
+
maxWidth: "1280px",
|
|
121
|
+
margin: "0 auto",
|
|
122
|
+
padding: "2rem 1rem",
|
|
123
|
+
boxSizing: "border-box",
|
|
124
|
+
fontFamily: font,
|
|
125
|
+
}, children: [filteredProjects.length === 0 && (_jsx("div", { style: { textAlign: "center", padding: "4rem 0" }, children: _jsx("p", { style: { color: "#71717a", fontFamily: font }, children: "No projects found." }) })), filteredProjects.length > 0 && (_jsxs(_Fragment, { children: [_jsx("style", { children: `
|
|
126
|
+
.chisel-portfolio-grid {
|
|
127
|
+
display: grid;
|
|
128
|
+
grid-template-columns: 1fr;
|
|
129
|
+
gap: 1.5rem;
|
|
130
|
+
}
|
|
131
|
+
@media (min-width: 640px) {
|
|
132
|
+
.chisel-portfolio-grid { grid-template-columns: repeat(2, 1fr); }
|
|
133
|
+
}
|
|
134
|
+
@media (min-width: 1024px) {
|
|
135
|
+
.chisel-portfolio-grid { grid-template-columns: ${gridCols}; gap: 2rem; }
|
|
136
|
+
}
|
|
137
|
+
.chisel-project-card-img { height: 180px; }
|
|
138
|
+
@media (min-width: 640px) { .chisel-project-card-img { height: 200px; } }
|
|
139
|
+
@media (min-width: 1024px) { .chisel-project-card-img { height: 220px; } }
|
|
140
|
+
` }), _jsx("div", { className: "chisel-portfolio-grid", children: filteredProjects.map((project, index) => (_jsx(ProjectCard, { project: project, schema: data.schema, fieldOptionsMap: data.fieldOptionsMap, basePath: basePath, priority: index === 0 }, project.id))) })] }))] }));
|
|
141
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SimilarProjects.d.ts","sourceRoot":"","sources":["../src/SimilarProjects.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA8CD,wBAAsB,eAAe,CAAC,EACpC,OAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,QAAY,EACZ,UAAe,GAChB,EAAE,oBAAoB,
|
|
1
|
+
{"version":3,"file":"SimilarProjects.d.ts","sourceRoot":"","sources":["../src/SimilarProjects.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,wDAAwD;IACxD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AA8CD,wBAAsB,eAAe,CAAC,EACpC,OAAY,EACZ,WAAW,EACX,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,QAAY,EACZ,UAAe,GAChB,EAAE,oBAAoB,2DAgHtB"}
|
package/dist/SimilarProjects.js
CHANGED
|
@@ -53,11 +53,14 @@ export async function SimilarProjects({ filters = {}, excludeSlug, clientSlug, a
|
|
|
53
53
|
.slice(0, maxItems);
|
|
54
54
|
if (similar.length === 0)
|
|
55
55
|
return null;
|
|
56
|
-
return (_jsxs("section", { style: { borderTop: "1px solid #e4e4e7",
|
|
56
|
+
return (_jsxs("section", { style: { borderTop: "1px solid #e4e4e7", maxWidth: "1280px", margin: "0 auto", padding: "3rem 1rem 2rem", boxSizing: "border-box", fontFamily: font }, children: [_jsx("style", { children: `
|
|
57
57
|
.chisel-similar-grid { display: grid; grid-template-columns: 1fr; gap: 1.5rem; }
|
|
58
58
|
@media (min-width: 640px) { .chisel-similar-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
59
59
|
@media (min-width: 1024px) { .chisel-similar-grid { grid-template-columns: repeat(3, 1fr); } }
|
|
60
|
-
|
|
60
|
+
.chisel-similar-header { display: flex; flex-direction: column; gap: 12px; margin-bottom: 2rem; }
|
|
61
|
+
@media (min-width: 640px) { .chisel-similar-header { flex-direction: row; align-items: flex-end; justify-content: space-between; } }
|
|
62
|
+
.chisel-similar-img { height: 56vw; min-height: 160px; max-height: 220px; }
|
|
63
|
+
` }), _jsxs("div", { className: "chisel-similar-header", children: [_jsxs("div", { children: [_jsx("p", { style: { fontSize: "11px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: "oklch(0.78 0.16 85)", margin: "0 0 6px 0" }, children: "More Work" }), _jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "clamp(20px, 4vw, 28px)", margin: 0, fontFamily: font }, children: "Similar Projects" })] }), _jsxs("a", { href: basePath, style: { color: "#18181b", fontWeight: 600, fontSize: "15px", textDecoration: "none", display: "inline-flex", alignItems: "center", gap: "6px", fontFamily: font, flexShrink: 0 }, children: ["View All", _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M9 18l6-6-6-6" }) })] })] }), _jsx("div", { className: "chisel-similar-grid", children: similar.map((p) => {
|
|
61
64
|
var _a, _b, _c, _d, _e;
|
|
62
65
|
const imgUrl = (_d = (_a = p.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = p.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : null;
|
|
63
66
|
const badge = badgeField
|
|
@@ -67,11 +70,11 @@ export async function SimilarProjects({ filters = {}, excludeSlug, clientSlug, a
|
|
|
67
70
|
? p.custom_field_values[locationField.key]
|
|
68
71
|
: null;
|
|
69
72
|
const locStr = loc ? [loc.city, loc.state].filter(Boolean).join(", ") : null;
|
|
70
|
-
return (_jsxs("a", { href: `${basePath}/${p.slug}`, style: { textDecoration: "none", color: "inherit", display: "block", borderBottom: "1px solid #e4e4e7", fontFamily: font }, children: [_jsxs("div", { style: { position: "relative", width: "100%",
|
|
73
|
+
return (_jsxs("a", { href: `${basePath}/${p.slug}`, style: { textDecoration: "none", color: "inherit", display: "block", borderBottom: "1px solid #e4e4e7", fontFamily: font }, children: [_jsxs("div", { className: "chisel-similar-img", style: { position: "relative", width: "100%", overflow: "hidden", backgroundColor: "#f4f4f5", marginBottom: "1rem" }, children: [imgUrl && (_jsx("img", { src: imgUrl, alt: p.title, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } })), badge && (_jsx("span", { style: {
|
|
71
74
|
position: "absolute", top: "12px", left: "12px",
|
|
72
75
|
backgroundColor: "oklch(0.78 0.16 85)", color: "#fff",
|
|
73
76
|
fontSize: "10px", fontWeight: 700, textTransform: "uppercase",
|
|
74
77
|
letterSpacing: "0.1em", padding: "4px 10px",
|
|
75
|
-
}, children: badge }))] }), _jsxs("div", { style: { paddingBottom: "1.25rem" }, children: [_jsx("h3", { style: { color: "#18181b", fontWeight: 700, fontSize: "17px", lineHeight: 1.3, margin: "0 0 8px 0", fontFamily: font }, children: p.title }), locStr && (_jsxs("p", { style: { display: "flex", alignItems: "center", gap: "5px", color: "#71717a", fontSize: "14px", margin: 0 }, children: [_jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0 }, children: [_jsx("path", { d: "M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" }), _jsx("circle", { cx: "12", cy: "10", r: "3" })] }), locStr] }))] })] }, p.id));
|
|
78
|
+
}, children: badge }))] }), _jsxs("div", { style: { paddingBottom: "1.25rem" }, children: [_jsx("h3", { style: { color: "#18181b", fontWeight: 700, fontSize: "clamp(15px, 2.5vw, 17px)", lineHeight: 1.3, margin: "0 0 8px 0", fontFamily: font }, children: p.title }), locStr && (_jsxs("p", { style: { display: "flex", alignItems: "center", gap: "5px", color: "#71717a", fontSize: "14px", margin: 0 }, children: [_jsxs("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", style: { flexShrink: 0 }, children: [_jsx("path", { d: "M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" }), _jsx("circle", { cx: "12", cy: "10", r: "3" })] }), locStr] }))] })] }, p.id));
|
|
76
79
|
}) })] }));
|
|
77
80
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export { ProjectPortfolio } from "./ProjectPortfolio";
|
|
2
2
|
export type { ProjectPortfolioProps } from "./ProjectPortfolio";
|
|
3
|
+
export { ProjectPortfolioClient } from "./ProjectPortfolioClient";
|
|
4
|
+
export type { ProjectPortfolioClientProps } from "./ProjectPortfolioClient";
|
|
3
5
|
export { ProjectDetail } from "./ProjectDetail";
|
|
4
6
|
export type { ProjectDetailProps } from "./ProjectDetail";
|
|
5
7
|
export { SimilarProjects } from "./SimilarProjects";
|
|
6
8
|
export type { SimilarProjectsProps } from "./SimilarProjects";
|
|
7
9
|
export { GalleryCarousel } from "./GalleryCarousel";
|
|
10
|
+
export type { GalleryCarouselProps } from "./GalleryCarousel";
|
|
8
11
|
export { ProjectMenu, fetchProjectMenuData, createMenuHandler } from "./ProjectMenu";
|
|
9
12
|
export type { ProjectMenuProps } from "./ProjectMenu";
|
|
10
13
|
export { ProjectMenuClient } from "./ProjectMenuClient";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,KAAK,GACN,MAAM,SAAS,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA;AACrD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAC/D,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,YAAY,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAA;AAC3E,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AACpF,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,YAAY,EACV,OAAO,EACP,iBAAiB,EACjB,gBAAgB,EAChB,aAAa,EACb,KAAK,GACN,MAAM,SAAS,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { ProjectPortfolio } from "./ProjectPortfolio";
|
|
2
|
+
export { ProjectPortfolioClient } from "./ProjectPortfolioClient";
|
|
2
3
|
export { ProjectDetail } from "./ProjectDetail";
|
|
3
4
|
export { SimilarProjects } from "./SimilarProjects";
|
|
4
5
|
export { GalleryCarousel } from "./GalleryCarousel";
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "project-portfolio",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectDetail, ProjectMenu
|
|
5
|
-
"keywords": ["nextjs", "react", "portfolio", "projects", "megamenu", "gallery"],
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectPortfolioClient (with built-in filtering), ProjectDetail, SimilarProjects, ProjectMenu, ProjectMenuClient, and GalleryCarousel. Pass a clientSlug and apiBase — done.",
|
|
5
|
+
"keywords": ["nextjs", "react", "portfolio", "projects", "megamenu", "gallery", "filtering"],
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "./dist/index.js",
|