project-portfolio 1.0.8 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ import type { Media } from "./types";
2
+ export declare function GalleryCarousel({ images, projectTitle, }: {
3
+ images: Media[];
4
+ projectTitle: string;
5
+ }): import("react/jsx-runtime").JSX.Element | null;
6
+ //# sourceMappingURL=GalleryCarousel.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
4
+ const font = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
5
+ export function GalleryCarousel({ images, projectTitle, }) {
6
+ const [current, setCurrent] = useState(0);
7
+ if (images.length === 0)
8
+ return null;
9
+ // caption may come back as `caption`, `description`, or `alt` depending on the API
10
+ const active = images[current];
11
+ const caption = active.caption || active.description || null;
12
+ const total = images.length;
13
+ function prev() {
14
+ setCurrent((c) => (c - 1 + total) % total);
15
+ }
16
+ function next() {
17
+ setCurrent((c) => (c + 1) % total);
18
+ }
19
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "12px", fontFamily: font }, children: [_jsxs("div", { style: { position: "relative", width: "100%", height: "520px", backgroundColor: "#f4f4f5", overflow: "hidden" }, children: [_jsx("img", { src: active.url, alt: active.alt || projectTitle, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }), total > 1 && (_jsx("button", { onClick: prev, "aria-label": "Previous image", style: {
20
+ position: "absolute",
21
+ left: "16px",
22
+ top: "50%",
23
+ transform: "translateY(-50%)",
24
+ width: "40px",
25
+ height: "40px",
26
+ borderRadius: "50%",
27
+ backgroundColor: "rgba(255,255,255,0.92)",
28
+ border: "none",
29
+ cursor: "pointer",
30
+ display: "flex",
31
+ alignItems: "center",
32
+ justifyContent: "center",
33
+ boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
34
+ }, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "#18181b", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M15 18l-6-6 6-6" }) }) })), total > 1 && (_jsx("button", { onClick: next, "aria-label": "Next image", style: {
35
+ position: "absolute",
36
+ right: "16px",
37
+ top: "50%",
38
+ transform: "translateY(-50%)",
39
+ width: "40px",
40
+ height: "40px",
41
+ borderRadius: "50%",
42
+ backgroundColor: "rgba(255,255,255,0.92)",
43
+ border: "none",
44
+ cursor: "pointer",
45
+ display: "flex",
46
+ alignItems: "center",
47
+ justifyContent: "center",
48
+ boxShadow: "0 1px 4px rgba(0,0,0,0.18)",
49
+ }, children: _jsx("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "#18181b", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M9 18l6-6-6-6" }) }) })), total > 1 && (_jsxs("div", { style: {
50
+ position: "absolute",
51
+ bottom: "14px",
52
+ right: "14px",
53
+ backgroundColor: "rgba(0,0,0,0.65)",
54
+ color: "#fff",
55
+ fontSize: "13px",
56
+ fontWeight: 600,
57
+ padding: "4px 10px",
58
+ borderRadius: "9999px",
59
+ fontFamily: font,
60
+ }, children: [current + 1, " / ", total] }))] }), caption && (_jsx("p", { style: {
61
+ textAlign: "center",
62
+ fontSize: "14px",
63
+ fontStyle: "italic",
64
+ color: "#6b7280",
65
+ margin: 0,
66
+ padding: "0 1rem",
67
+ fontFamily: font,
68
+ }, children: caption })), total > 1 && (_jsx("div", { style: { display: "flex", gap: "8px", flexWrap: "wrap" }, children: images.map((img, i) => {
69
+ var _a;
70
+ return (_jsx("button", { onClick: () => setCurrent(i), "aria-label": `View image ${i + 1}`, style: {
71
+ width: "72px",
72
+ height: "52px",
73
+ padding: 0,
74
+ border: i === current ? "2px solid #f18a00" : "2px solid transparent",
75
+ borderRadius: "2px",
76
+ overflow: "hidden",
77
+ cursor: "pointer",
78
+ flexShrink: 0,
79
+ backgroundColor: "#f4f4f5",
80
+ opacity: i === current ? 1 : 0.65,
81
+ transition: "opacity 0.15s, border-color 0.15s",
82
+ }, children: _jsx("img", { src: img.url, alt: img.alt || `Image ${i + 1}`, style: { width: "100%", height: "100%", objectFit: "cover", display: "block" } }) }, (_a = img.id) !== null && _a !== void 0 ? _a : i));
83
+ }) }))] }));
84
+ }
@@ -79,7 +79,7 @@ export function ProjectCard({ project, schema, priority, variant = "card", baseP
79
79
  transition: "box-shadow 0.3s",
80
80
  fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
81
81
  boxSizing: "border-box",
82
- }, children: [_jsxs("div", { style: { position: "relative", aspectRatio: "16/10", overflow: "hidden", flexShrink: 0 }, children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, loading: priority ? "eager" : "lazy", style: { width: "100%", height: "100%", objectFit: "cover" } })) : (_jsx("div", { style: { position: "absolute", inset: 0, backgroundColor: "#f4f4f5" } })), _jsx("div", { style: {
82
+ }, children: [_jsxs("div", { style: { position: "relative", width: "100%", height: "220px", overflow: "hidden", flexShrink: 0 }, children: [imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, loading: priority ? "eager" : "lazy", style: { width: "100%", height: "100%", objectFit: "cover" } })) : (_jsx("div", { style: { position: "absolute", inset: 0, backgroundColor: "#f4f4f5" } })), _jsx("div", { style: {
83
83
  position: "absolute",
84
84
  inset: 0,
85
85
  background: "linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.25) 50%, transparent 100%)",
@@ -113,7 +113,7 @@ export function ProjectCard({ project, schema, priority, variant = "card", baseP
113
113
  fontSize: "11px",
114
114
  fontWeight: 600,
115
115
  padding: "3px 10px",
116
- }, children: badgeValue })), _jsx("h3", { style: { all: "revert", color: "#fff", fontWeight: 700, fontSize: "20px", lineHeight: 1.3, margin: 0, fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, children: project.title }), locationString && (_jsx("p", { style: { color: "rgba(255,255,255,0.8)", fontSize: "14px", margin: 0 }, children: locationString }))] })] }), _jsxs("div", { style: { display: "flex", flexDirection: "column", flex: 1, padding: "1.5rem" }, children: [(project.blurb || project.description) && (_jsx("p", { style: { fontSize: "14px", color: "#3f3f46", lineHeight: 1.6, margin: "0 0 16px 0" }, children: project.blurb || project.description })), tagFields.map((field) => {
116
+ }, children: badgeValue })), _jsx("h3", { style: { color: "#fff", fontWeight: 700, fontSize: "20px", lineHeight: 1.3, margin: 0, padding: 0, fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, children: project.title }), locationString && (_jsx("p", { style: { color: "rgba(255,255,255,0.8)", fontSize: "14px", margin: 0 }, children: locationString }))] })] }), _jsxs("div", { style: { display: "flex", flexDirection: "column", flex: 1, padding: "1.5rem" }, children: [(project.blurb || project.description) && (_jsx("p", { style: { fontSize: "14px", color: "#3f3f46", lineHeight: 1.6, margin: "0 0 16px 0" }, children: project.blurb || project.description })), tagFields.map((field) => {
117
117
  const vals = parseMultiValue(project.custom_field_values[field.key]);
118
118
  if (vals.length === 0)
119
119
  return null;
@@ -0,0 +1,20 @@
1
+ export interface ProjectDetailProps {
2
+ /** The project slug to load */
3
+ slug: string;
4
+ /** The client slug identifying which client owns this project */
5
+ clientSlug: string;
6
+ /** Base URL of the projects API */
7
+ apiBase: string;
8
+ /** Base path for the "back" link. Defaults to "/projects" */
9
+ backPath?: string;
10
+ /** Label for the "back" link. Defaults to "All Projects" */
11
+ backLabel?: string;
12
+ /**
13
+ * Seconds to cache via Next.js Data Cache on production deployments.
14
+ * React.cache() always deduplicates within a single render in all environments.
15
+ * Defaults to 60.
16
+ */
17
+ revalidate?: number;
18
+ }
19
+ export declare function ProjectDetail({ slug, clientSlug, apiBase, backPath, backLabel, revalidate, }: ProjectDetailProps): Promise<import("react/jsx-runtime").JSX.Element>;
20
+ //# sourceMappingURL=ProjectDetail.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectDetail.d.ts","sourceRoot":"","sources":["../src/ProjectDetail.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IACjC,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,iEAAiE;IACjE,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAkLD,wBAAsB,aAAa,CAAC,EAClC,IAAI,EACJ,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,SAA0B,EAC1B,UAAe,GAChB,EAAE,kBAAkB,oDAgLpB"}
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cache } from "react";
3
+ import { GalleryCarousel } from "./GalleryCarousel";
4
+ // ─── Helpers ────────────────────────────────────────────────────────────────
5
+ function parseMultiValue(raw) {
6
+ if (Array.isArray(raw))
7
+ return raw.map(String).map((s) => s.replace(/`/g, "").trim()).filter(Boolean);
8
+ if (typeof raw === "string")
9
+ return raw.replace(/`/g, "").split(",").map((s) => s.trim()).filter(Boolean);
10
+ return [];
11
+ }
12
+ function dedupeByKey(arr) {
13
+ const seen = new Set();
14
+ return arr.filter((f) => {
15
+ if (seen.has(f.key))
16
+ return false;
17
+ seen.add(f.key);
18
+ return true;
19
+ });
20
+ }
21
+ // ─── Data fetching ───────────────────────────────────────────────────────────
22
+ const fetchProjectDetail = cache(async (apiBase, clientSlug, slug, revalidate) => {
23
+ var _a, _b, _c, _d;
24
+ const fetchOpts = revalidate > 0
25
+ ? { next: { revalidate } }
26
+ : {};
27
+ try {
28
+ const [projectRes, allProjectsRes] = await Promise.all([
29
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts),
30
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts),
31
+ ]);
32
+ // Per API docs, single project response is { client: {..., custom_fields_schema}, data: {...} }
33
+ const projectJson = projectRes.ok ? await projectRes.json() : null;
34
+ const project = (_a = projectJson === null || projectJson === void 0 ? void 0 : projectJson.data) !== null && _a !== void 0 ? _a : null;
35
+ const schema = dedupeByKey((_c = (_b = projectJson === null || projectJson === void 0 ? void 0 : projectJson.client) === null || _b === void 0 ? void 0 : _b.custom_fields_schema) !== null && _c !== void 0 ? _c : []);
36
+ // All projects response is { client: {...}, data: [...] }
37
+ const allProjectsJson = allProjectsRes.ok ? await allProjectsRes.json() : null;
38
+ const allProjects = (_d = allProjectsJson === null || allProjectsJson === void 0 ? void 0 : allProjectsJson.data) !== null && _d !== void 0 ? _d : [];
39
+ return { project, schema, allProjects };
40
+ }
41
+ catch (_e) {
42
+ return { project: null, schema: [], allProjects: [] };
43
+ }
44
+ });
45
+ // ─── Similar Projects sub-component ─────────────────────────────────────────
46
+ function SimilarProjects({ allProjects, currentSlug, badgeValue, badgeField, locationField, basePath, font, }) {
47
+ const similar = allProjects
48
+ .filter((p) => {
49
+ var _a;
50
+ if (p.slug === currentSlug)
51
+ return false;
52
+ if (!badgeValue || !badgeField)
53
+ return true;
54
+ const val = (_a = parseMultiValue(p.custom_field_values[badgeField.key])[0]) !== null && _a !== void 0 ? _a : null;
55
+ return val === badgeValue;
56
+ })
57
+ .slice(0, 3);
58
+ if (similar.length === 0)
59
+ return null;
60
+ return (_jsxs("section", { style: { borderTop: "1px solid #e4e4e7", paddingTop: "3rem" }, children: [_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: "#f18a00", 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", { style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "1.5rem" }, children: similar.map((p) => {
61
+ var _a, _b, _c, _d, _e;
62
+ 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
+ const badge = badgeField
64
+ ? ((_e = parseMultiValue(p.custom_field_values[badgeField.key])[0]) !== null && _e !== void 0 ? _e : null)
65
+ : null;
66
+ const loc = locationField
67
+ ? p.custom_field_values[locationField.key]
68
+ : null;
69
+ 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: {
71
+ position: "absolute",
72
+ top: "12px",
73
+ left: "12px",
74
+ backgroundColor: "#f18a00",
75
+ color: "#fff",
76
+ fontSize: "10px",
77
+ fontWeight: 700,
78
+ textTransform: "uppercase",
79
+ letterSpacing: "0.1em",
80
+ padding: "4px 10px",
81
+ }, 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));
82
+ }) })] }));
83
+ }
84
+ // ─── Component ───────────────────────────────────────────────────────────────
85
+ export async function ProjectDetail({ slug, clientSlug, apiBase, backPath = "/projects", backLabel = "All Projects", revalidate = 60, }) {
86
+ var _a, _b, _c, _d, _e, _f, _g, _h;
87
+ const { project, schema, allProjects } = await fetchProjectDetail(apiBase, clientSlug, slug, revalidate);
88
+ if (!project) {
89
+ return (_jsxs("div", { style: { textAlign: "center", padding: "6rem 1.5rem", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, children: [_jsx("p", { style: { color: "#71717a", fontSize: "18px" }, children: "Project not found." }), _jsxs("a", { href: backPath, style: { color: "#f18a00", fontWeight: 600, fontSize: "14px", textDecoration: "none", marginTop: "1rem", display: "inline-block" }, children: ["\u2190 ", backLabel] })] }));
90
+ }
91
+ 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;
92
+ const galleryImages = project.image_url
93
+ ? ((_e = project.media) !== null && _e !== void 0 ? _e : [])
94
+ : ((_g = (_f = project.media) === null || _f === void 0 ? void 0 : _f.slice(1)) !== null && _g !== void 0 ? _g : []);
95
+ const badgeField = schema.find((f) => f.display_position === "badge_overlay");
96
+ const locationField = schema.find((f) => f.type === "location");
97
+ const numericField = schema.find((f) => f.type === "number" && f.display_position !== "hidden");
98
+ const badgeValue = badgeField
99
+ ? ((_h = parseMultiValue(project.custom_field_values[badgeField.key])[0]) !== null && _h !== void 0 ? _h : null)
100
+ : null;
101
+ const locationValue = locationField
102
+ ? project.custom_field_values[locationField.key]
103
+ : null;
104
+ const locationString = locationValue
105
+ ? [locationValue.city, locationValue.state].filter(Boolean).join(", ")
106
+ : null;
107
+ const rawCoverage = numericField ? project.custom_field_values[numericField.key] : null;
108
+ const coverageFormatted = (() => {
109
+ if (rawCoverage === null || rawCoverage === undefined)
110
+ return null;
111
+ const n = typeof rawCoverage === "number" ? rawCoverage : parseFloat(String(rawCoverage));
112
+ return isNaN(n) ? String(rawCoverage) : `${n.toLocaleString()} SF`;
113
+ })();
114
+ const completedValue = (() => {
115
+ var _a;
116
+ const entries = Object.entries(project.custom_field_values);
117
+ const preferred = entries.find(([key]) => /(completed|completion|year|date)/i.test(key));
118
+ const candidate = (_a = preferred === null || preferred === void 0 ? void 0 : preferred[1]) !== null && _a !== void 0 ? _a : null;
119
+ if (candidate === null || candidate === undefined)
120
+ return null;
121
+ if (typeof candidate === "number")
122
+ return String(candidate);
123
+ if (typeof candidate === "string")
124
+ return candidate.trim() || null;
125
+ return null;
126
+ })();
127
+ const font = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
128
+ return (_jsxs("main", { style: { minHeight: "100vh", backgroundColor: "#fff", fontFamily: font }, children: [_jsxs("section", { style: { position: "relative", width: "100%", height: "55vh", minHeight: "320px", maxHeight: "560px", 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%)" } }), _jsx("div", { style: { position: "absolute", top: "1.5rem", left: 0, right: 0, padding: "0 1.5rem" }, children: _jsxs("a", { href: backPath, style: { display: "inline-flex", alignItems: "center", gap: "6px", color: "rgba(255,255,255,0.7)", fontSize: "14px", textDecoration: "none", fontFamily: font }, children: [_jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "M19 12H5M12 5l-7 7 7 7" }) }), backLabel] }) }), _jsxs("div", { style: { position: "absolute", bottom: 0, left: 0, right: 0, padding: "0 1.5rem 2.5rem", maxWidth: "900px" }, children: [(badgeValue || completedValue) && (_jsx("p", { style: { color: "#f18a00", fontSize: "11px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", margin: "0 0 10px 0" }, children: [badgeValue, completedValue].filter(Boolean).join(" · ") })), _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] }))] })] }), _jsx("section", { style: { borderBottom: "1px solid #e4e4e7", backgroundColor: "#fff" }, children: _jsx("div", { style: { maxWidth: "1280px", margin: "0 auto", padding: "2rem 1.5rem", display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: "2rem", boxSizing: "border-box" }, children: [
129
+ { label: "Location", value: locationString !== null && locationString !== void 0 ? locationString : "—" },
130
+ { label: "Project Type", value: badgeValue !== null && badgeValue !== void 0 ? badgeValue : "—" },
131
+ { label: "Sq. Footage", value: coverageFormatted !== null && coverageFormatted !== void 0 ? coverageFormatted : "—" },
132
+ { label: "Completed", value: completedValue !== null && completedValue !== void 0 ? completedValue : "—" },
133
+ ].map(({ label, value }) => (_jsxs("div", { children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.1em", color: "#a1a1aa", margin: "0 0 6px 0" }, children: label }), _jsx("p", { style: { color: "#18181b", fontWeight: 600, fontSize: "15px", margin: 0 }, children: value })] }, label))) }) }), _jsxs("article", { style: { maxWidth: "1280px", margin: "0 auto", padding: "3rem 1.5rem", boxSizing: "border-box", display: "flex", flexDirection: "column", gap: "2.5rem" }, children: [(project.blurb || project.description) && (_jsxs("section", { style: { maxWidth: "720px" }, children: [_jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "22px", margin: "0 0 16px 0", fontFamily: font }, children: "Project Overview" }), project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", lineHeight: 1.7, margin: "0 0 16px 0" }, children: project.blurb })), project.description && project.description !== project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", lineHeight: 1.7, margin: 0 }, children: project.description }))] })), _jsxs("section", { children: [_jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "22px", margin: "0 0 1.5rem 0", fontFamily: font }, children: "Project Gallery" }), galleryImages.length > 0 ? (_jsx(GalleryCarousel, { images: galleryImages, projectTitle: project.title })) : (
134
+ /* Placeholder */
135
+ _jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "12px" }, children: [0, 1, 2].map((i) => (_jsxs("div", { style: { aspectRatio: "4/3", borderRadius: "4px", backgroundColor: "#f9f9f9", border: "1px solid #e4e4e7", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "8px" }, children: [_jsx("svg", { width: "24", height: "24", viewBox: "0 0 24 24", fill: "none", stroke: "#a1a1aa", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909M3.75 19.5h16.5" }) }), _jsx("p", { style: { color: "#a1a1aa", fontSize: "12px", margin: 0 }, children: "Photos coming soon" })] }, i))) }))] }), _jsx(SimilarProjects, { allProjects: allProjects, currentSlug: slug, badgeValue: badgeValue, badgeField: badgeField !== null && badgeField !== void 0 ? badgeField : null, locationField: locationField !== null && locationField !== void 0 ? locationField : null, basePath: backPath, font: font })] })] }));
136
+ }
@@ -0,0 +1,11 @@
1
+ import type { CustomFieldSchema } from "./types";
2
+ interface ProjectFiltersProps {
3
+ fields: CustomFieldSchema[];
4
+ filters: Record<string, string>;
5
+ onFilterChange: (key: string, value: string) => void;
6
+ onClearFilters: () => void;
7
+ hasActiveFilters: boolean;
8
+ }
9
+ export declare function ProjectFilters({ fields, filters, onFilterChange, onClearFilters, hasActiveFilters, }: ProjectFiltersProps): import("react/jsx-runtime").JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=ProjectFilters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectFilters.d.ts","sourceRoot":"","sources":["../src/ProjectFilters.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAEhD,UAAU,mBAAmB;IAC3B,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,cAAc,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACpD,cAAc,EAAE,MAAM,IAAI,CAAA;IAC1B,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,wBAAgB,cAAc,CAAC,EAC7B,MAAM,EACN,OAAO,EACP,cAAc,EACd,cAAc,EACd,gBAAgB,GACjB,EAAE,mBAAmB,2CA8FrB"}
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function ProjectFilters({ fields, filters, onFilterChange, onClearFilters, hasActiveFilters, }) {
3
+ const deduped = fields.filter((f, i, arr) => arr.findIndex((x) => x.key === f.key) === i);
4
+ return (_jsxs("div", { style: {
5
+ marginBottom: "2rem",
6
+ paddingBottom: "2rem",
7
+ borderBottom: "1px solid #e4e4e7",
8
+ display: "flex",
9
+ flexWrap: "wrap",
10
+ alignItems: "flex-end",
11
+ gap: "1rem",
12
+ }, children: [deduped.map((field, fieldIndex) => {
13
+ if (field.type === "select" || field.type === "multi-select") {
14
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [_jsx("label", { style: { fontSize: "13px", fontWeight: 500, color: "#71717a" }, children: field.name }), _jsxs("select", { value: filters[field.key] || "all", onChange: (e) => onFilterChange(field.key, e.target.value === "all" ? "" : e.target.value), style: {
15
+ width: "180px",
16
+ padding: "8px 12px",
17
+ border: "1px solid #e4e4e7",
18
+ borderRadius: "6px",
19
+ fontSize: "14px",
20
+ backgroundColor: "#fff",
21
+ color: "#18181b",
22
+ outline: "none",
23
+ cursor: "pointer",
24
+ }, children: [_jsxs("option", { value: "all", children: ["All ", field.name] }), [...new Set(field.options)].map((option, i) => (_jsx("option", { value: option, children: option }, `${field.key}-opt-${i}`)))] })] }, `filter-${fieldIndex}-${field.key}`));
25
+ }
26
+ if (field.type === "text" || field.type === "location") {
27
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "6px" }, children: [_jsx("label", { style: { fontSize: "13px", fontWeight: 500, color: "#71717a" }, children: field.name }), _jsx("input", { type: "text", placeholder: `Search ${field.name.toLowerCase()}...`, value: filters[field.key] || "", onChange: (e) => onFilterChange(field.key, e.target.value), style: {
28
+ width: "180px",
29
+ padding: "8px 12px",
30
+ border: "1px solid #e4e4e7",
31
+ borderRadius: "6px",
32
+ fontSize: "14px",
33
+ color: "#18181b",
34
+ outline: "none",
35
+ } })] }, `filter-${fieldIndex}-${field.key}`));
36
+ }
37
+ return null;
38
+ }), hasActiveFilters && (_jsx("button", { onClick: onClearFilters, style: {
39
+ display: "flex",
40
+ alignItems: "center",
41
+ gap: "4px",
42
+ padding: "8px 12px",
43
+ background: "none",
44
+ border: "none",
45
+ fontSize: "14px",
46
+ color: "#71717a",
47
+ cursor: "pointer",
48
+ }, children: "\u00D7 Clear filters" }))] }));
49
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ProjectPortfolio.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAuED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,UAAe,GAChB,EAAE,qBAAqB,oDA8BvB"}
1
+ {"version":3,"file":"ProjectPortfolio.d.ts","sourceRoot":"","sources":["../src/ProjectPortfolio.tsx"],"names":[],"mappings":"AAIA,MAAM,WAAW,qBAAqB;IACpC,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAA;IAClB,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAuED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,gBAAgB,CAAC,EACrC,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,UAAe,GAChB,EAAE,qBAAqB,oDAwCvB"}
@@ -14,8 +14,8 @@ const fetchPortfolioData = cache(async (apiBase, clientSlug, revalidate) => {
14
14
  // Either way React.cache() above ensures no duplicate fetches within one render.
15
15
  const fetchOpts = { next: { revalidate } };
16
16
  const [clientRes, projectsRes] = await Promise.all([
17
- fetch(`${apiBase}/api/v1/clients/${clientSlug}`, fetchOpts),
18
- fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects`, fetchOpts),
17
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts),
18
+ fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts),
19
19
  ]);
20
20
  const clientJson = clientRes.ok
21
21
  ? await clientRes.json()
@@ -59,8 +59,14 @@ export async function ProjectPortfolio({ clientSlug, apiBase, basePath = "/proje
59
59
  return (_jsx("div", { style: { textAlign: "center", padding: "4rem 0" }, children: _jsx("p", { style: { color: "#71717a" }, children: "No projects found." }) }));
60
60
  }
61
61
  return (_jsx("div", { style: {
62
- display: "grid",
63
- gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
64
- gap: "2rem",
65
- }, children: projects.map((project, index) => (_jsx(ProjectCard, { project: project, schema: schema, basePath: basePath, priority: index === 0 }, project.id))) }));
62
+ width: "100%",
63
+ maxWidth: "1280px",
64
+ margin: "0 auto",
65
+ padding: "2rem 1.5rem",
66
+ boxSizing: "border-box",
67
+ }, children: _jsx("div", { style: {
68
+ display: "grid",
69
+ gridTemplateColumns: "repeat(3, 1fr)",
70
+ gap: "2rem",
71
+ }, children: projects.map((project, index) => (_jsx(ProjectCard, { project: project, schema: schema, basePath: basePath, priority: index === 0 }, project.id))) }) }));
66
72
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { ProjectPortfolio } from "./ProjectPortfolio";
2
2
  export type { ProjectPortfolioProps } from "./ProjectPortfolio";
3
- export { ProjectGrid } from "./ProjectGrid";
3
+ export { ProjectDetail } from "./ProjectDetail";
4
+ export type { ProjectDetailProps } from "./ProjectDetail";
5
+ export { GalleryCarousel } from "./GalleryCarousel";
4
6
  export { ProjectCard } from "./ProjectCard";
5
7
  export type { CardVariant } from "./ProjectCard";
6
8
  export type { Project, CustomFieldSchema, CustomFieldValue, LocationValue, Media, } from "./types";
@@ -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,WAAW,EAAE,MAAM,eAAe,CAAA;AAC3C,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,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,YAAY,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,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,3 +1,4 @@
1
1
  export { ProjectPortfolio } from "./ProjectPortfolio";
2
- export { ProjectGrid } from "./ProjectGrid";
2
+ export { ProjectDetail } from "./ProjectDetail";
3
+ export { GalleryCarousel } from "./GalleryCarousel";
3
4
  export { ProjectCard } from "./ProjectCard";
package/dist/types.d.ts CHANGED
@@ -13,6 +13,8 @@ export interface Media {
13
13
  project_id: string;
14
14
  url: string;
15
15
  alt: string;
16
+ caption?: string | null;
17
+ description?: string | null;
16
18
  is_primary: boolean;
17
19
  sort_order: number;
18
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,GAAG,cAAc,GAAG,UAAU,CAAA;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACpC,aAAa,EAAE,OAAO,CAAA;IACtB,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,eAAe,GAAG,MAAM,GAAG,UAAU,GAAG,QAAQ,CAAA;CACpE;AAED,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAClB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,UAAU,EAAE,OAAO,CAAA;IACnB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,aAAa,GAAG,IAAI,CAAA;AAEhF,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,WAAW,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACrD,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,KAAK,EAAE,CAAA;CACf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "project-portfolio",
3
- "version": "1.0.8",
3
+ "version": "1.3.5",
4
4
  "description": "Self-contained project portfolio component for Next.js App Router. Drop in one component, pass a clientSlug and apiBase, done.",
5
5
  "keywords": ["nextjs", "react", "portfolio", "projects"],
6
6
  "license": "MIT",