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 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 | `60` | Cache revalidation period in seconds |
48
+ | `revalidate` | `number` | No | `86400` | Cache revalidation period in seconds (24 hours) |
49
49
 
50
- #### Filter Integration
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 | `60` | Cache revalidation period in seconds |
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 declare function GalleryCarousel({ images, projectTitle, }: {
2
+ export interface GalleryCarouselProps {
3
3
  images: Media[];
4
4
  projectTitle: string;
5
- }): import("react/jsx-runtime").JSX.Element | null;
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,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,YAAY,GACb,EAAE;IACD,MAAM,EAAE,KAAK,EAAE,CAAA;IACf,YAAY,EAAE,MAAM,CAAA;CACrB,kDAwJA"}
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"}
@@ -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", flexWrap: "wrap" }, children: images.map((img, i) => {
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",
@@ -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 2.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: `
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,2CAscxB"}
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.5rem 1rem;
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.5rem;
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.5rem;
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: 360px;
168
+ width: 220px;
165
169
  flex-shrink: 0;
166
170
  padding-left: 2.5rem;
167
171
  padding-top: 0;
168
- border-top: none;
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: 10px;
186
+ gap: 6px;
176
187
  }
177
- @media (min-width: 540px) {
188
+ @media (min-width: 768px) {
178
189
  .chisel-menu-card-grid {
179
190
  grid-template-columns: repeat(2, 1fr);
180
- gap: 12px;
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
- .chisel-menu-filters-mobile-toggle {
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
- justify-content: space-between;
188
- background: none;
189
- border: none;
190
- padding: 0;
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
- width: 100%;
193
- margin-bottom: 10px;
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-filters-mobile-toggle {
197
- display: none;
229
+ .chisel-menu-card-thumb {
230
+ width: 72px;
231
+ height: 54px;
198
232
  }
199
233
  }
200
234
 
201
- .chisel-menu-filters-list {
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-filters-list {
211
- display: flex !important;
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: "13px",
275
+ fontSize: "11px",
218
276
  fontWeight: 700,
219
277
  textTransform: "uppercase",
220
278
  letterSpacing: "0.12em",
221
279
  color: "#71717a",
222
- margin: "0 0 14px 0",
280
+ margin: "0 0 12px 0",
223
281
  }, children: "Featured Projects" }), subtitle && (_jsx("p", { style: {
224
- fontSize: "16px",
282
+ fontSize: "14px",
225
283
  color: "#52525b",
226
- lineHeight: 1.6,
227
- margin: "0 0 24px 0",
228
- }, 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) => {
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.06)" : "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", { style: { display: "flex", flexDirection: "column", gap: "3px", minWidth: 0, flex: 1 }, children: [_jsx("p", { style: {
262
- fontSize: "14px",
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: "13px", color: "#52525b", margin: 0, fontFamily: font }, children: badge })), tags.length > 0 && (() => {
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
- const shown = tagLabels.slice(0, 3);
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" }), filterOptions.length > 0 && (_jsxs("div", { className: "chisel-menu-right", children: [_jsx("p", { style: {
280
- fontSize: "11px",
281
- fontWeight: 700,
282
- textTransform: "uppercase",
283
- letterSpacing: "0.12em",
284
- color: "#71717a",
285
- margin: "0 0 14px 0",
286
- }, children: "Browse By" }), _jsx("p", { style: {
287
- fontSize: "15px",
288
- fontWeight: 700,
289
- color: "#18181b",
290
- margin: "0 0 10px 0",
291
- fontFamily: font,
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,2DA6GtB"}
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"}
@@ -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", paddingTop: "3rem", maxWidth: "1280px", margin: "0 auto", padding: "3rem 1rem 2rem", boxSizing: "border-box", fontFamily: font }, children: [_jsx("style", { children: `
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
- ` }), _jsxs("div", { style: { display: "flex", alignItems: "flex-end", justifyContent: "space-between", marginBottom: "2rem" }, 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: "28px", margin: 0, fontFamily: font }, children: "Similar Projects" })] }), _jsxs("a", { href: basePath, style: { color: "#18181b", fontWeight: 600, fontSize: "15px", textDecoration: "none", display: "flex", alignItems: "center", gap: "6px", fontFamily: font }, 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) => {
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%", height: "220px", 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: {
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";
@@ -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": "1.9.2",
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
- "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",