@chiselandco/nexus 2.2.6
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 +676 -0
- package/dist/GalleryCarousel.d.ts +7 -0
- package/dist/GalleryCarousel.d.ts.map +1 -0
- package/dist/GalleryCarousel.js +84 -0
- package/dist/ProjectCard.d.ts +15 -0
- package/dist/ProjectCard.d.ts.map +1 -0
- package/dist/ProjectCard.js +159 -0
- package/dist/ProjectDetail.d.ts +25 -0
- package/dist/ProjectDetail.d.ts.map +1 -0
- package/dist/ProjectDetail.js +179 -0
- package/dist/ProjectFilters.d.ts +11 -0
- package/dist/ProjectFilters.d.ts.map +1 -0
- package/dist/ProjectFilters.js +49 -0
- package/dist/ProjectGrid.d.ts +10 -0
- package/dist/ProjectGrid.d.ts.map +1 -0
- package/dist/ProjectGrid.js +8 -0
- package/dist/ProjectMenu.d.ts +79 -0
- package/dist/ProjectMenu.d.ts.map +1 -0
- package/dist/ProjectMenu.js +170 -0
- package/dist/ProjectMenuClient.d.ts +44 -0
- package/dist/ProjectMenuClient.d.ts.map +1 -0
- package/dist/ProjectMenuClient.js +386 -0
- package/dist/ProjectPortfolio.d.ts +42 -0
- package/dist/ProjectPortfolio.d.ts.map +1 -0
- package/dist/ProjectPortfolio.js +153 -0
- 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 +46 -0
- package/dist/SimilarProjects.d.ts.map +1 -0
- package/dist/SimilarProjects.js +125 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
|
@@ -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%", aspectRatio: "16/9", backgroundColor: "#f4f4f5", overflow: "hidden" }, children: [_jsx("img", { src: active.url, alt: active.alt || projectTitle, style: { position: "absolute", inset: 0, 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", overflowX: "auto", WebkitOverflowScrolling: "touch", paddingBottom: "4px" }, children: images.map((img, i) => {
|
|
69
|
+
var _a;
|
|
70
|
+
return (_jsx("button", { onClick: () => setCurrent(i), "aria-label": `View image ${i + 1}`, style: {
|
|
71
|
+
width: "88px",
|
|
72
|
+
aspectRatio: "16/9",
|
|
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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Project, CustomFieldSchema } from "./types";
|
|
2
|
+
export type CardVariant = "card" | "compact";
|
|
3
|
+
interface ProjectCardProps {
|
|
4
|
+
project: Project;
|
|
5
|
+
schema: CustomFieldSchema[];
|
|
6
|
+
/** Map of fieldKey -> { id: label, label: label } for normalizing mixed id/label values */
|
|
7
|
+
fieldOptionsMap?: Record<string, Record<string, string>>;
|
|
8
|
+
priority?: boolean;
|
|
9
|
+
variant?: CardVariant;
|
|
10
|
+
/** Base path for project detail links. Defaults to "/projects" */
|
|
11
|
+
basePath?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function ProjectCard({ project, schema, fieldOptionsMap, priority, variant, basePath, }: ProjectCardProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=ProjectCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectCard.d.ts","sourceRoot":"","sources":["../src/ProjectCard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAmC,MAAM,SAAS,CAAA;AAE1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,CAAA;AAE5C,UAAU,gBAAgB;IACxB,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,2FAA2F;IAC3F,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,OAAO,CAAC,EAAE,WAAW,CAAA;IACrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAeD,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,MAAM,EACN,eAAoB,EACpB,QAAQ,EACR,OAAgB,EAChB,QAAsB,GACvB,EAAE,gBAAgB,2CA2QlB"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
function parseMultiValue(raw) {
|
|
3
|
+
if (Array.isArray(raw))
|
|
4
|
+
return raw.map(String).map((s) => s.replace(/`/g, "").trim()).filter(Boolean);
|
|
5
|
+
if (typeof raw === "string") {
|
|
6
|
+
return raw.replace(/`/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
7
|
+
}
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
function parseSingleValue(raw) {
|
|
11
|
+
if (typeof raw === "string")
|
|
12
|
+
return raw.replace(/`/g, "").trim();
|
|
13
|
+
return String(raw !== null && raw !== void 0 ? raw : "");
|
|
14
|
+
}
|
|
15
|
+
export function ProjectCard({ project, schema, fieldOptionsMap = {}, priority, variant = "card", basePath = "/projects", }) {
|
|
16
|
+
var _a, _b, _c, _d, _e, _f;
|
|
17
|
+
const imageUrl = (_d = (_a = project.image_url) !== null && _a !== void 0 ? _a : (_c = (_b = project.media) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c.url) !== null && _d !== void 0 ? _d : null;
|
|
18
|
+
const badgeField = schema.find((f) => f.display_position === "badge_overlay");
|
|
19
|
+
const tagFields = schema
|
|
20
|
+
.filter((f) => f.display_position === "tags" && f.type !== "number")
|
|
21
|
+
.filter((f, i, arr) => arr.findIndex((x) => x.key === f.key) === i);
|
|
22
|
+
// Coverage/sq ft field for footer stat — matched by key or name regardless of type
|
|
23
|
+
const numericField = schema.find((f) => /sq|sqft|square|coverage/i.test(f.key + f.name));
|
|
24
|
+
const locationField = schema.find((f) => f.type === "location");
|
|
25
|
+
const badgeRaw = badgeField
|
|
26
|
+
? parseSingleValue(project.custom_field_values[badgeField.key])
|
|
27
|
+
: null;
|
|
28
|
+
const badgeOptMap = badgeField ? ((_e = fieldOptionsMap[badgeField.key]) !== null && _e !== void 0 ? _e : {}) : {};
|
|
29
|
+
const badgeValue = badgeRaw ? ((_f = badgeOptMap[badgeRaw]) !== null && _f !== void 0 ? _f : badgeRaw) : null;
|
|
30
|
+
const locationValue = locationField
|
|
31
|
+
? project.custom_field_values[locationField.key]
|
|
32
|
+
: null;
|
|
33
|
+
const locationString = locationValue
|
|
34
|
+
? [locationValue.city, locationValue.state].filter(Boolean).join(", ")
|
|
35
|
+
: null;
|
|
36
|
+
const footerStat = (() => {
|
|
37
|
+
if (!numericField)
|
|
38
|
+
return null;
|
|
39
|
+
const val = project.custom_field_values[numericField.key];
|
|
40
|
+
if (val === undefined || val === null || val === "")
|
|
41
|
+
return null;
|
|
42
|
+
const label = numericField.name.replace(/\s*\([^)]+\)/, "");
|
|
43
|
+
// Coverage is a pre-formatted string from the API (e.g. "186,446 SF") — render as-is.
|
|
44
|
+
// For numeric fields, format with commas.
|
|
45
|
+
const formatted = typeof val === "string"
|
|
46
|
+
? val
|
|
47
|
+
: typeof val === "number"
|
|
48
|
+
? Math.round(val).toLocaleString()
|
|
49
|
+
: String(val);
|
|
50
|
+
return { label, formatted };
|
|
51
|
+
})();
|
|
52
|
+
const compactTags = tagFields.flatMap((field) => {
|
|
53
|
+
var _a;
|
|
54
|
+
const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
|
|
55
|
+
const hasOptions = Object.keys(optMap).length > 0;
|
|
56
|
+
const vals = parseMultiValue(project.custom_field_values[field.key]);
|
|
57
|
+
const active = hasOptions ? vals.filter((v) => optMap[v] !== undefined) : vals;
|
|
58
|
+
return active.map((v) => { var _a; return (_a = optMap[v]) !== null && _a !== void 0 ? _a : v; });
|
|
59
|
+
});
|
|
60
|
+
const href = `${basePath}/${project.slug}`;
|
|
61
|
+
if (variant === "compact") {
|
|
62
|
+
return (_jsxs("a", { href: href, style: {
|
|
63
|
+
all: "revert",
|
|
64
|
+
display: "flex",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
gap: "1rem",
|
|
67
|
+
padding: "1rem",
|
|
68
|
+
backgroundColor: "#fff",
|
|
69
|
+
border: "1px solid #e4e4e7",
|
|
70
|
+
borderRadius: "2px",
|
|
71
|
+
textDecoration: "none",
|
|
72
|
+
transition: "border-color 0.2s, box-shadow 0.2s",
|
|
73
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
74
|
+
boxSizing: "border-box",
|
|
75
|
+
color: "inherit",
|
|
76
|
+
}, children: [_jsx("div", { style: {
|
|
77
|
+
position: "relative",
|
|
78
|
+
width: "80px",
|
|
79
|
+
height: "80px",
|
|
80
|
+
flexShrink: 0,
|
|
81
|
+
borderRadius: "2px",
|
|
82
|
+
overflow: "hidden",
|
|
83
|
+
backgroundColor: "#f4f4f5",
|
|
84
|
+
}, children: imageUrl ? (_jsx("img", { src: imageUrl, alt: project.title, style: { width: "100%", height: "100%", objectFit: "cover" } })) : (_jsx("div", { style: { width: "100%", height: "100%", backgroundColor: "#e4e4e7" } })) }), _jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "4px", minWidth: 0 }, children: [_jsx("p", { style: { fontWeight: 700, color: "#18181b", fontSize: "14px", margin: 0, lineHeight: 1.4, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }, children: project.title }), badgeValue && (_jsx("p", { style: { fontSize: "12px", color: "#71717a", margin: 0 }, children: badgeValue })), compactTags.length > 0 && (_jsx("p", { style: { fontSize: "12px", color: "#f18a00", margin: 0, lineHeight: 1.4 }, children: compactTags.join(" · ") }))] })] }));
|
|
85
|
+
}
|
|
86
|
+
return (_jsxs("article", { style: {
|
|
87
|
+
all: "revert",
|
|
88
|
+
backgroundColor: "#fff",
|
|
89
|
+
borderRadius: "2px",
|
|
90
|
+
boxShadow: "0 2px 8px 0 rgba(0,0,0,0.10), 0 0 0 1px rgba(0,0,0,0.07)",
|
|
91
|
+
overflow: "hidden",
|
|
92
|
+
display: "flex",
|
|
93
|
+
flexDirection: "column",
|
|
94
|
+
transition: "box-shadow 0.3s",
|
|
95
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
96
|
+
boxSizing: "border-box",
|
|
97
|
+
}, children: [_jsxs("div", { className: "chisel-project-card-img", style: { position: "relative", width: "100%", 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: {
|
|
98
|
+
position: "absolute",
|
|
99
|
+
inset: 0,
|
|
100
|
+
background: "linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.25) 50%, transparent 100%)",
|
|
101
|
+
} }), project.is_featured && (_jsx("span", { style: {
|
|
102
|
+
position: "absolute",
|
|
103
|
+
top: "1rem",
|
|
104
|
+
left: "1rem",
|
|
105
|
+
backgroundColor: "#fbbf24",
|
|
106
|
+
color: "#451a03",
|
|
107
|
+
fontSize: "11px",
|
|
108
|
+
fontWeight: 700,
|
|
109
|
+
textTransform: "uppercase",
|
|
110
|
+
letterSpacing: "0.05em",
|
|
111
|
+
padding: "3px 10px",
|
|
112
|
+
zIndex: 10,
|
|
113
|
+
}, children: "Featured" })), _jsxs("div", { style: {
|
|
114
|
+
position: "absolute",
|
|
115
|
+
bottom: 0,
|
|
116
|
+
left: 0,
|
|
117
|
+
right: 0,
|
|
118
|
+
padding: "1.25rem",
|
|
119
|
+
display: "flex",
|
|
120
|
+
flexDirection: "column",
|
|
121
|
+
gap: "6px",
|
|
122
|
+
zIndex: 10,
|
|
123
|
+
}, children: [badgeValue && (_jsx("span", { style: {
|
|
124
|
+
alignSelf: "flex-start",
|
|
125
|
+
backgroundColor: "rgba(255,255,255,0.85)",
|
|
126
|
+
backdropFilter: "blur(4px)",
|
|
127
|
+
color: "#27272a",
|
|
128
|
+
fontSize: "11px",
|
|
129
|
+
fontWeight: 600,
|
|
130
|
+
padding: "3px 10px",
|
|
131
|
+
}, 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) => {
|
|
132
|
+
var _a;
|
|
133
|
+
const vals = parseMultiValue(project.custom_field_values[field.key]);
|
|
134
|
+
if (vals.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
|
|
137
|
+
const hasOptions = Object.keys(optMap).length > 0;
|
|
138
|
+
// Filter out archived values — if optMap is populated and the value isn't in it, it's archived
|
|
139
|
+
const activeVals = hasOptions ? vals.filter((val) => optMap[val] !== undefined) : vals;
|
|
140
|
+
if (activeVals.length === 0)
|
|
141
|
+
return null;
|
|
142
|
+
return (_jsxs("div", { style: { marginBottom: "16px" }, children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 600, letterSpacing: "0.1em", textTransform: "uppercase", color: "#a1a1aa", marginBottom: "8px" }, children: "Systems Used:" }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: activeVals.map((val, i) => {
|
|
143
|
+
var _a, _b, _c, _d;
|
|
144
|
+
// Normalize: look up by id or label, always display the label
|
|
145
|
+
const label = (_a = optMap[val]) !== null && _a !== void 0 ? _a : val;
|
|
146
|
+
const url = (_c = (_b = field.option_urls) === null || _b === void 0 ? void 0 : _b[val]) !== null && _c !== void 0 ? _c : (_d = field.option_urls) === null || _d === void 0 ? void 0 : _d[label];
|
|
147
|
+
const tagStyle = {
|
|
148
|
+
border: "1px solid #d4d4d8",
|
|
149
|
+
padding: "4px 12px",
|
|
150
|
+
fontSize: "14px",
|
|
151
|
+
color: "#3f3f46",
|
|
152
|
+
textDecoration: "none",
|
|
153
|
+
backgroundColor: "transparent",
|
|
154
|
+
cursor: url ? "pointer" : "default",
|
|
155
|
+
};
|
|
156
|
+
return url ? (_jsx("a", { href: url, style: tagStyle, children: label }, `${project.id}-${field.key}-${i}`)) : (_jsx("span", { style: tagStyle, children: label }, `${project.id}-${field.key}-${i}`));
|
|
157
|
+
}) })] }, `${project.id}-${field.key}`));
|
|
158
|
+
}), _jsx("div", { style: { flex: 1 } }), _jsxs("div", { style: { marginTop: "16px", borderTop: "1px solid #e4e4e7", paddingTop: "12px", display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: "16px" }, children: [footerStat ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "2px" }, children: [_jsx("span", { style: { fontSize: "12px", color: "#a1a1aa" }, children: footerStat.label }), _jsx("span", { style: { color: "#18181b", fontWeight: 700, fontSize: "16px" }, children: footerStat.formatted })] })) : _jsx("div", {}), _jsx("a", { href: href, style: { all: "revert", color: "#f18a00", fontWeight: 600, fontSize: "14px", textDecoration: "none", flexShrink: 0, fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" }, children: "Details \u2192" })] })] })] }));
|
|
159
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 and "View All" 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
|
+
* When true, bypasses the Next.js Data Cache and always fetches fresh data.
|
|
20
|
+
* Sets fetch cache to "no-store". Useful during development or for frequently updated projects.
|
|
21
|
+
*/
|
|
22
|
+
noCache?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function ProjectDetail({ slug, clientSlug, apiBase, backPath, backLabel, revalidate, noCache, }: ProjectDetailProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
25
|
+
//# sourceMappingURL=ProjectDetail.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectDetail.d.ts","sourceRoot":"","sources":["../src/ProjectDetail.tsx"],"names":[],"mappings":"AAWA,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,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAoED,wBAAsB,aAAa,CAAC,EAClC,IAAI,EACJ,UAAU,EACV,OAAO,EACP,QAAsB,EACtB,SAA0B,EAC1B,UAAe,EACf,OAAe,GAChB,EAAE,kBAAkB,oDAoQpB"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cache } from "react";
|
|
3
|
+
// LocationValue is used in locationValue cast below
|
|
4
|
+
import { GalleryCarousel } from "./GalleryCarousel";
|
|
5
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
6
|
+
function parseMultiValue(raw) {
|
|
7
|
+
if (Array.isArray(raw))
|
|
8
|
+
return raw.map(String).map((s) => s.replace(/`/g, "").trim()).filter(Boolean);
|
|
9
|
+
if (typeof raw === "string")
|
|
10
|
+
return raw.replace(/`/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
11
|
+
return [];
|
|
12
|
+
}
|
|
13
|
+
function dedupeByKey(arr) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
return arr.filter((f) => {
|
|
16
|
+
if (seen.has(f.key))
|
|
17
|
+
return false;
|
|
18
|
+
seen.add(f.key);
|
|
19
|
+
return true;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// ─── Data fetching ───────────────────────────────────────────────────────────
|
|
23
|
+
const fetchProjectDetail = cache(async (apiBase, clientSlug, slug, revalidate, noCache) => {
|
|
24
|
+
var _a, _b, _c, _d;
|
|
25
|
+
const fetchOpts = noCache
|
|
26
|
+
? { cache: "no-store" }
|
|
27
|
+
: revalidate > 0
|
|
28
|
+
? { next: { revalidate } }
|
|
29
|
+
: {};
|
|
30
|
+
try {
|
|
31
|
+
// Single call — /projects/{slug} returns the project AND client.custom_fields_schema
|
|
32
|
+
// with full options embedded. No need for a separate /fields call.
|
|
33
|
+
const projectRes = await fetch(`${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=pk_live_crmsuTIm7NNfb9uEWBCyv88F6kj2YQUR`, fetchOpts);
|
|
34
|
+
const projectJson = projectRes.ok ? await projectRes.json() : null;
|
|
35
|
+
const project = (_a = projectJson === null || projectJson === void 0 ? void 0 : projectJson.data) !== null && _a !== void 0 ? _a : null;
|
|
36
|
+
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 : []);
|
|
37
|
+
// Build fieldOptionsMap directly from schema options — identical data to /fields
|
|
38
|
+
const fieldOptionsMap = {};
|
|
39
|
+
for (const field of schema) {
|
|
40
|
+
const map = {};
|
|
41
|
+
for (const opt of ((_d = field.options) !== null && _d !== void 0 ? _d : [])) {
|
|
42
|
+
if (typeof opt === "object" && opt.id && opt.label) {
|
|
43
|
+
map[opt.id] = opt.label;
|
|
44
|
+
map[opt.label] = opt.label;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
fieldOptionsMap[field.key] = map;
|
|
48
|
+
}
|
|
49
|
+
return { project, schema, fieldOptionsMap };
|
|
50
|
+
}
|
|
51
|
+
catch (_e) {
|
|
52
|
+
return { project: null, schema: [], fieldOptionsMap: {} };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
56
|
+
export async function ProjectDetail({ slug, clientSlug, apiBase, backPath = "/projects", backLabel = "All Projects", revalidate = 60, noCache = false, }) {
|
|
57
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
|
|
58
|
+
const { project, schema, fieldOptionsMap } = await fetchProjectDetail(apiBase, clientSlug, slug, revalidate, noCache);
|
|
59
|
+
if (!project) {
|
|
60
|
+
return (_jsx("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." }) }));
|
|
61
|
+
}
|
|
62
|
+
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;
|
|
63
|
+
const galleryImages = project.image_url
|
|
64
|
+
? ((_e = project.media) !== null && _e !== void 0 ? _e : [])
|
|
65
|
+
: ((_g = (_f = project.media) === null || _f === void 0 ? void 0 : _f.slice(1)) !== null && _g !== void 0 ? _g : []);
|
|
66
|
+
const badgeField = schema.find((f) => f.display_position === "badge_overlay");
|
|
67
|
+
const locationField = schema.find((f) => f.type === "location");
|
|
68
|
+
const badgeRaw = badgeField
|
|
69
|
+
? ((_h = parseMultiValue(project.custom_field_values[badgeField.key])[0]) !== null && _h !== void 0 ? _h : null)
|
|
70
|
+
: null;
|
|
71
|
+
const badgeOptMap = badgeField ? ((_j = fieldOptionsMap[badgeField.key]) !== null && _j !== void 0 ? _j : {}) : {};
|
|
72
|
+
const badgeValue = badgeRaw ? ((_k = badgeOptMap[badgeRaw]) !== null && _k !== void 0 ? _k : badgeRaw) : null;
|
|
73
|
+
const locationValue = locationField
|
|
74
|
+
? project.custom_field_values[locationField.key]
|
|
75
|
+
: null;
|
|
76
|
+
const locationString = locationValue
|
|
77
|
+
? [locationValue.city, locationValue.state].filter(Boolean).join(", ")
|
|
78
|
+
: null;
|
|
79
|
+
// Helper: format a raw field value to a display string, resolving slugs via fieldOptionsMap
|
|
80
|
+
function formatFieldValue(field, raw) {
|
|
81
|
+
var _a, _b;
|
|
82
|
+
if (raw === null || raw === undefined || raw === "")
|
|
83
|
+
return null;
|
|
84
|
+
const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
|
|
85
|
+
if (typeof raw === "string")
|
|
86
|
+
return (_b = optMap[raw]) !== null && _b !== void 0 ? _b : raw;
|
|
87
|
+
if (typeof raw === "number") {
|
|
88
|
+
if (/year/i.test(field.key + field.name))
|
|
89
|
+
return String(Math.round(raw));
|
|
90
|
+
return Math.round(raw).toLocaleString();
|
|
91
|
+
}
|
|
92
|
+
if (Array.isArray(raw)) {
|
|
93
|
+
const hasOptions = Object.keys(optMap).length > 0;
|
|
94
|
+
const vals = raw.map(String).filter(Boolean);
|
|
95
|
+
const active = hasOptions ? vals.filter((v) => optMap[v] !== undefined) : vals;
|
|
96
|
+
const labels = active.map((v) => { var _a; return (_a = optMap[v]) !== null && _a !== void 0 ? _a : v; });
|
|
97
|
+
return labels.length > 0 ? labels.join(", ") : null;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// ── Stats bar: explicit ordered list of fields to show ────────────────────
|
|
102
|
+
// Location and badge/type are handled first from their detected fields,
|
|
103
|
+
// then remaining stat bar fields are matched by key in the schema.
|
|
104
|
+
const STAT_BAR_KEYS = ["coverage", "year-completed", "architect", "general-contractor"];
|
|
105
|
+
const metadataStats = [];
|
|
106
|
+
if (locationString) {
|
|
107
|
+
metadataStats.push({ label: "Location", value: locationString });
|
|
108
|
+
}
|
|
109
|
+
if (badgeValue) {
|
|
110
|
+
metadataStats.push({ label: badgeField.name, value: badgeValue });
|
|
111
|
+
}
|
|
112
|
+
for (const key of STAT_BAR_KEYS) {
|
|
113
|
+
const field = schema.find((f) => f.key === key);
|
|
114
|
+
if (!field)
|
|
115
|
+
continue;
|
|
116
|
+
const raw = project.custom_field_values[field.key];
|
|
117
|
+
const formatted = formatFieldValue(field, raw);
|
|
118
|
+
// Use "Completed" as the label for year-completed
|
|
119
|
+
const label = key === "year-completed" ? "Completed" : field.name;
|
|
120
|
+
if (formatted)
|
|
121
|
+
metadataStats.push({ label, value: formatted });
|
|
122
|
+
}
|
|
123
|
+
// ── Specs sidebar: explicit ordered list of fields ────────────────────────
|
|
124
|
+
const SPEC_SIDEBAR_KEYS = [
|
|
125
|
+
"systems-used",
|
|
126
|
+
"systems",
|
|
127
|
+
"series-used",
|
|
128
|
+
"operation-type",
|
|
129
|
+
"finishes",
|
|
130
|
+
"specifications",
|
|
131
|
+
];
|
|
132
|
+
const specFields = SPEC_SIDEBAR_KEYS
|
|
133
|
+
.map((key) => schema.find((f) => f.key === key))
|
|
134
|
+
.filter((f) => f !== undefined);
|
|
135
|
+
const font = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif";
|
|
136
|
+
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] }))] })] }), metadataStats.length > 0 && (_jsxs("section", { style: { borderBottom: "1px solid #e4e4e7", backgroundColor: "#fff" }, children: [_jsx("style", { children: `
|
|
137
|
+
.chisel-stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; }
|
|
138
|
+
@media (min-width: 640px) { .chisel-stats-grid { grid-template-columns: repeat(3, 1fr); } }
|
|
139
|
+
@media (min-width: 1024px) { .chisel-stats-grid { grid-template-columns: repeat(${Math.min(metadataStats.length, 6)}, 1fr); } }
|
|
140
|
+
.chisel-stat-item { padding: 1.5rem 1.25rem; border-right: 1px solid #e4e4e7; border-bottom: 1px solid #e4e4e7; }
|
|
141
|
+
.chisel-stat-item:last-child { border-right: none; }
|
|
142
|
+
.chisel-gallery-placeholder { display: grid; grid-template-columns: 1fr; gap: 12px; }
|
|
143
|
+
@media (min-width: 640px) { .chisel-gallery-placeholder { grid-template-columns: repeat(3, 1fr); } }
|
|
144
|
+
.chisel-hero-img { height: 50vw; min-height: 220px; max-height: 560px; }
|
|
145
|
+
.chisel-overview-grid { display: grid; grid-template-columns: 1fr; gap: 2.5rem; }
|
|
146
|
+
@media (min-width: 1024px) { .chisel-overview-grid { grid-template-columns: 1fr 300px; gap: 4rem; align-items: start; } }
|
|
147
|
+
` }), _jsx("div", { style: { maxWidth: "1280px", margin: "0 auto", boxSizing: "border-box", padding: "0 0 0 0" }, className: "chisel-stats-grid", children: metadataStats.map(({ label, value }) => (_jsxs("div", { className: "chisel-stat-item", children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: "#a1a1aa", margin: "0 0 8px 0" }, children: label }), _jsx("p", { style: { color: "#18181b", fontWeight: 600, fontSize: "15px", margin: 0, lineHeight: 1.4 }, children: value })] }, label))) })] })), _jsxs("article", { style: { maxWidth: "1280px", margin: "0 auto", padding: "3rem 1.5rem", boxSizing: "border-box" }, children: [(project.blurb || project.description || specFields.length > 0) && (_jsxs("section", { style: { marginBottom: "3rem" }, children: [_jsx("div", { style: { borderBottom: "1px solid #e4e4e7", marginBottom: "2rem", paddingBottom: "1rem", display: "flex", alignItems: "baseline", justifyContent: "space-between" }, children: _jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "20px", margin: 0, letterSpacing: "-0.01em", fontFamily: font }, children: "Project Overview" }) }), _jsxs("div", { className: "chisel-overview-grid", children: [_jsxs("div", { children: [project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", fontWeight: 400, lineHeight: 1.85, margin: "0 0 20px 0" }, children: project.blurb })), project.description && project.description !== project.blurb && (_jsx("p", { style: { color: "#3f3f46", fontSize: "16px", fontWeight: 400, lineHeight: 1.85, margin: 0 }, children: project.description }))] }), specFields.length > 0 && (_jsx("aside", { style: { borderLeft: "3px solid #f18a00", paddingLeft: "2rem" }, children: _jsx("div", { style: { display: "flex", flexDirection: "column", gap: "2rem" }, children: specFields.map((field) => {
|
|
148
|
+
var _a;
|
|
149
|
+
const raw = project.custom_field_values[field.key];
|
|
150
|
+
const optMap = (_a = fieldOptionsMap[field.key]) !== null && _a !== void 0 ? _a : {};
|
|
151
|
+
const hasOptions = Object.keys(optMap).length > 0;
|
|
152
|
+
const rawVals = Array.isArray(raw)
|
|
153
|
+
? raw.map(String).filter(Boolean)
|
|
154
|
+
: typeof raw === "string"
|
|
155
|
+
? [raw]
|
|
156
|
+
: raw !== null && raw !== undefined
|
|
157
|
+
? [String(raw)]
|
|
158
|
+
: [];
|
|
159
|
+
// Filter out archived values and resolve slugs to labels
|
|
160
|
+
const vals = hasOptions
|
|
161
|
+
? rawVals.filter((v) => optMap[v] !== undefined).map((v) => { var _a; return (_a = optMap[v]) !== null && _a !== void 0 ? _a : v; })
|
|
162
|
+
: rawVals;
|
|
163
|
+
if (vals.length === 0)
|
|
164
|
+
return null;
|
|
165
|
+
return (_jsxs("div", { children: [_jsx("p", { style: { fontSize: "10px", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.12em", color: "#a1a1aa", margin: "0 0 12px 0" }, children: field.name }), _jsx("div", { style: { display: "flex", flexWrap: "wrap", gap: "8px" }, children: vals.map((val) => (_jsx("span", { style: {
|
|
166
|
+
display: "inline-block",
|
|
167
|
+
padding: "5px 14px",
|
|
168
|
+
backgroundColor: "#fafafa",
|
|
169
|
+
border: "1px solid #e4e4e7",
|
|
170
|
+
color: "#3f3f46",
|
|
171
|
+
fontSize: "13px",
|
|
172
|
+
fontWeight: 500,
|
|
173
|
+
borderRadius: "2px",
|
|
174
|
+
lineHeight: 1.5,
|
|
175
|
+
}, children: val }, val))) })] }, field.key));
|
|
176
|
+
}) }) }))] })] })), _jsxs("section", { children: [_jsx("div", { style: { borderBottom: "1px solid #e4e4e7", marginBottom: "2rem", paddingBottom: "1rem" }, children: _jsx("h2", { style: { color: "#18181b", fontWeight: 700, fontSize: "20px", margin: 0, letterSpacing: "-0.01em", fontFamily: font }, children: "Project Gallery" }) }), galleryImages.length > 0 ? (_jsx(GalleryCarousel, { images: galleryImages, projectTitle: project.title })) : (
|
|
177
|
+
/* Placeholder */
|
|
178
|
+
_jsx("div", { className: "chisel-gallery-placeholder", children: [0, 1, 2].map((i) => (_jsxs("div", { style: { aspectRatio: "16/9", 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))) }))] })] })] }));
|
|
179
|
+
}
|
|
@@ -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,2CAgGrB"}
|
|
@@ -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((o) => typeof o === "string" ? o : o.label))].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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Project, CustomFieldSchema } from "./types";
|
|
2
|
+
interface ProjectGridProps {
|
|
3
|
+
projects: Project[];
|
|
4
|
+
schema: CustomFieldSchema[];
|
|
5
|
+
/** Base path for project detail links. Defaults to "/projects" */
|
|
6
|
+
basePath?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function ProjectGrid({ projects, schema, basePath }: ProjectGridProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=ProjectGrid.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectGrid.d.ts","sourceRoot":"","sources":["../src/ProjectGrid.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAEzD,UAAU,gBAAgB;IACxB,QAAQ,EAAE,OAAO,EAAE,CAAA;IACnB,MAAM,EAAE,iBAAiB,EAAE,CAAA;IAC3B,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAsB,EAAE,EAAE,gBAAgB,2CAsBzF"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ProjectCard } from "./ProjectCard";
|
|
3
|
+
export function ProjectGrid({ projects, schema, basePath = "/projects" }) {
|
|
4
|
+
if (projects.length === 0) {
|
|
5
|
+
return (_jsx("div", { style: { textAlign: "center", padding: "4rem 0" }, children: _jsx("p", { style: { color: "#71717a" }, children: "No projects found." }) }));
|
|
6
|
+
}
|
|
7
|
+
return (_jsx("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))", gap: "2rem" }, children: projects.map((project, index) => (_jsx(ProjectCard, { project: project, schema: schema, priority: index === 0, basePath: basePath }, project.id))) }));
|
|
8
|
+
}
|