@orion-studios/payload-studio 0.4.0-beta.0 → 0.4.0-beta.2
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 +9 -0
- package/dist/admin-app/styles.css +5 -0
- package/dist/studio-pages/builder.css +16 -3
- package/dist/studio-pages/client.js +246 -17
- package/dist/studio-pages/client.mjs +246 -17
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,9 @@ npm install @orion-studios/payload-studio
|
|
|
13
13
|
- `@orion-studios/payload-studio/admin`
|
|
14
14
|
- `@orion-studios/payload-studio/admin/client`
|
|
15
15
|
- `@orion-studios/payload-studio/admin.css`
|
|
16
|
+
- `@orion-studios/payload-studio/admin-app`
|
|
17
|
+
- `@orion-studios/payload-studio/admin-app/client`
|
|
18
|
+
- `@orion-studios/payload-studio/admin-app/styles.css`
|
|
16
19
|
- `@orion-studios/payload-studio/blocks`
|
|
17
20
|
- `@orion-studios/payload-studio/nextjs`
|
|
18
21
|
- `@orion-studios/payload-studio/studio`
|
|
@@ -35,6 +38,12 @@ import { BuilderPageEditor } from '@orion-studios/payload-studio/studio-pages/cl
|
|
|
35
38
|
import '@orion-studios/payload-studio/studio-pages/builder.css'
|
|
36
39
|
```
|
|
37
40
|
|
|
41
|
+
```ts
|
|
42
|
+
import { AdminPage } from '@orion-studios/payload-studio/admin-app'
|
|
43
|
+
import { AdminShellClient } from '@orion-studios/payload-studio/admin-app/client'
|
|
44
|
+
import '@orion-studios/payload-studio/admin-app/styles.css'
|
|
45
|
+
```
|
|
46
|
+
|
|
38
47
|
## Build
|
|
39
48
|
|
|
40
49
|
```bash
|
|
@@ -112,8 +112,13 @@ body {
|
|
|
112
112
|
.features {
|
|
113
113
|
border-radius: var(--orion-studio-radius-lg);
|
|
114
114
|
padding: 1.25rem;
|
|
115
|
-
background: #
|
|
115
|
+
background: linear-gradient(135deg, #124a37 0%, #1f684f 100%);
|
|
116
116
|
border: 1px solid var(--orion-studio-border);
|
|
117
|
+
color: #fff;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.features .inner {
|
|
121
|
+
padding: 1.8rem;
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
.feature-grid {
|
|
@@ -124,12 +129,20 @@ body {
|
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
.feature-item {
|
|
127
|
-
background:
|
|
128
|
-
border: 1px solid rgba(
|
|
132
|
+
background: rgba(255, 255, 255, 0.1);
|
|
133
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
129
134
|
border-radius: 14px;
|
|
130
135
|
padding: 0.9rem;
|
|
131
136
|
}
|
|
132
137
|
|
|
138
|
+
.feature-item h3 {
|
|
139
|
+
color: #fff;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.feature-item p {
|
|
143
|
+
color: rgba(255, 255, 255, 0.88);
|
|
144
|
+
}
|
|
145
|
+
|
|
133
146
|
.feature-icon {
|
|
134
147
|
width: 44px;
|
|
135
148
|
height: 44px;
|
|
@@ -518,6 +518,37 @@ function parseColor(value, fallback) {
|
|
|
518
518
|
}
|
|
519
519
|
return fallback;
|
|
520
520
|
}
|
|
521
|
+
function getRelationID(value) {
|
|
522
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
if (!value || typeof value !== "object") {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
if ("id" in value) {
|
|
529
|
+
const id = value.id;
|
|
530
|
+
if (typeof id === "number" || typeof id === "string") {
|
|
531
|
+
return id;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
function extractUploadedMedia(value) {
|
|
537
|
+
const candidate = value && typeof value === "object" && "doc" in value ? value.doc : value;
|
|
538
|
+
if (!candidate || typeof candidate !== "object") {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
const id = getRelationID(candidate);
|
|
542
|
+
if (id === null) {
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
alt: typeof candidate.alt === "string" ? candidate.alt : "",
|
|
547
|
+
filename: typeof candidate.filename === "string" ? candidate.filename : "",
|
|
548
|
+
id,
|
|
549
|
+
url: typeof candidate.url === "string" ? candidate.url : ""
|
|
550
|
+
};
|
|
551
|
+
}
|
|
521
552
|
function InlineText({
|
|
522
553
|
as = "p",
|
|
523
554
|
className,
|
|
@@ -807,6 +838,13 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
807
838
|
const [selectedIndex, setSelectedIndex] = (0, import_react.useState)(null);
|
|
808
839
|
const [dragIndex, setDragIndex] = (0, import_react.useState)(null);
|
|
809
840
|
const [sidebarOpen, setSidebarOpen] = (0, import_react.useState)(true);
|
|
841
|
+
const [savingStatus, setSavingStatus] = (0, import_react.useState)(null);
|
|
842
|
+
const [saveMessage, setSaveMessage] = (0, import_react.useState)("");
|
|
843
|
+
const [saveError, setSaveError] = (0, import_react.useState)("");
|
|
844
|
+
const [uploadingTarget, setUploadingTarget] = (0, import_react.useState)(null);
|
|
845
|
+
const [uploadError, setUploadError] = (0, import_react.useState)("");
|
|
846
|
+
const [uploadMessage, setUploadMessage] = (0, import_react.useState)("");
|
|
847
|
+
const [uploadAltText, setUploadAltText] = (0, import_react.useState)("");
|
|
810
848
|
const selectedBlock = (0, import_react.useMemo)(
|
|
811
849
|
() => selectedIndex !== null ? layout[selectedIndex] : null,
|
|
812
850
|
[layout, selectedIndex]
|
|
@@ -873,6 +911,76 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
873
911
|
const currentItems = Array.isArray(selectedBlock[fieldName]) ? selectedBlock[fieldName] : [];
|
|
874
912
|
updateSelectedArray(fieldName, [...currentItems, item]);
|
|
875
913
|
};
|
|
914
|
+
const uploadMediaForSelected = async (target, file) => {
|
|
915
|
+
if (selectedIndex === null) {
|
|
916
|
+
setUploadError("Select a section first.");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
setUploadingTarget(target);
|
|
920
|
+
setUploadError("");
|
|
921
|
+
setUploadMessage("");
|
|
922
|
+
try {
|
|
923
|
+
const formData = new FormData();
|
|
924
|
+
const fallbackAlt = file.name.replace(/\.[^/.]+$/, "").trim();
|
|
925
|
+
const resolvedAlt = uploadAltText.trim() || fallbackAlt || "Uploaded image";
|
|
926
|
+
formData.set("_payload", JSON.stringify({ alt: resolvedAlt }));
|
|
927
|
+
formData.set("file", file);
|
|
928
|
+
const response = await fetch("/api/media", {
|
|
929
|
+
body: formData,
|
|
930
|
+
credentials: "include",
|
|
931
|
+
method: "POST"
|
|
932
|
+
});
|
|
933
|
+
if (!response.ok) {
|
|
934
|
+
const body = await response.text();
|
|
935
|
+
throw new Error(body || "Upload failed");
|
|
936
|
+
}
|
|
937
|
+
const json = await response.json();
|
|
938
|
+
const uploaded = extractUploadedMedia(json);
|
|
939
|
+
if (!uploaded) {
|
|
940
|
+
throw new Error("Upload succeeded but returned media data was malformed.");
|
|
941
|
+
}
|
|
942
|
+
const nextLayout = cloneBlockLayout(layout);
|
|
943
|
+
const block = nextLayout[selectedIndex];
|
|
944
|
+
if (target === "hero") {
|
|
945
|
+
nextLayout[selectedIndex] = {
|
|
946
|
+
...block,
|
|
947
|
+
backgroundImageURL: "",
|
|
948
|
+
media: uploaded
|
|
949
|
+
};
|
|
950
|
+
} else {
|
|
951
|
+
nextLayout[selectedIndex] = {
|
|
952
|
+
...block,
|
|
953
|
+
image: uploaded
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
setLayout(nextLayout);
|
|
957
|
+
setUploadMessage("Image uploaded and attached to this section.");
|
|
958
|
+
} catch (error) {
|
|
959
|
+
setUploadError(error instanceof Error ? error.message : "Upload failed.");
|
|
960
|
+
} finally {
|
|
961
|
+
setUploadingTarget(null);
|
|
962
|
+
}
|
|
963
|
+
};
|
|
964
|
+
const toPersistedLayout = (sourceLayout) => sourceLayout.map((block) => {
|
|
965
|
+
if (!block || typeof block !== "object") {
|
|
966
|
+
return block;
|
|
967
|
+
}
|
|
968
|
+
const nextBlock = { ...block };
|
|
969
|
+
const blockType = normalizeText(nextBlock.blockType);
|
|
970
|
+
if (blockType === "hero") {
|
|
971
|
+
const mediaID = getRelationID(nextBlock.media);
|
|
972
|
+
if (mediaID !== null) {
|
|
973
|
+
nextBlock.media = mediaID;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (blockType === "media") {
|
|
977
|
+
const imageID = getRelationID(nextBlock.image);
|
|
978
|
+
if (imageID !== null) {
|
|
979
|
+
nextBlock.image = imageID;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return nextBlock;
|
|
983
|
+
});
|
|
876
984
|
const sidebarSectionStyle = {
|
|
877
985
|
border: "1px solid rgba(13, 74, 55, 0.15)",
|
|
878
986
|
borderRadius: 12,
|
|
@@ -921,12 +1029,16 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
921
1029
|
});
|
|
922
1030
|
};
|
|
923
1031
|
const saveLayout = async (status) => {
|
|
1032
|
+
setSavingStatus(status);
|
|
1033
|
+
setSaveError("");
|
|
1034
|
+
setSaveMessage("");
|
|
924
1035
|
try {
|
|
1036
|
+
const persistedLayout = toPersistedLayout(layout);
|
|
925
1037
|
const response = await fetch(`/api/pages/${pageID}`, {
|
|
926
1038
|
body: JSON.stringify({
|
|
927
1039
|
_status: status,
|
|
928
|
-
layout,
|
|
929
|
-
studioDocument: layoutToStudioDocument(
|
|
1040
|
+
layout: persistedLayout,
|
|
1041
|
+
studioDocument: layoutToStudioDocument(persistedLayout, title),
|
|
930
1042
|
title
|
|
931
1043
|
}),
|
|
932
1044
|
headers: {
|
|
@@ -938,6 +1050,7 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
938
1050
|
const body = await response.text();
|
|
939
1051
|
throw new Error(body || "Failed to save page");
|
|
940
1052
|
}
|
|
1053
|
+
setSaveMessage(status === "published" ? "Published." : "Draft saved.");
|
|
941
1054
|
window.parent?.postMessage(
|
|
942
1055
|
{
|
|
943
1056
|
message: status === "published" ? "Published." : "Draft saved.",
|
|
@@ -948,8 +1061,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
948
1061
|
},
|
|
949
1062
|
"*"
|
|
950
1063
|
);
|
|
1064
|
+
return true;
|
|
951
1065
|
} catch (error) {
|
|
952
1066
|
console.error(error);
|
|
1067
|
+
setSaveError("Could not save. Check permissions/session and retry.");
|
|
953
1068
|
window.parent?.postMessage(
|
|
954
1069
|
{
|
|
955
1070
|
message: "Could not save. Check permissions/session and retry.",
|
|
@@ -960,6 +1075,9 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
960
1075
|
},
|
|
961
1076
|
"*"
|
|
962
1077
|
);
|
|
1078
|
+
return false;
|
|
1079
|
+
} finally {
|
|
1080
|
+
setSavingStatus(null);
|
|
963
1081
|
}
|
|
964
1082
|
};
|
|
965
1083
|
(0, import_react.useEffect)(() => {
|
|
@@ -1632,6 +1750,48 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1632
1750
|
}
|
|
1633
1751
|
),
|
|
1634
1752
|
sidebarOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "grid", gap: 12, maxHeight: "calc(100vh - 90px)", overflowY: "auto", padding: 12 }, children: [
|
|
1753
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: sidebarSectionStyle, children: [
|
|
1754
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Save & Publish" }),
|
|
1755
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
|
|
1756
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1757
|
+
"button",
|
|
1758
|
+
{
|
|
1759
|
+
disabled: savingStatus !== null,
|
|
1760
|
+
onClick: () => void saveLayout("draft"),
|
|
1761
|
+
style: {
|
|
1762
|
+
borderRadius: 999,
|
|
1763
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1764
|
+
fontSize: 12,
|
|
1765
|
+
fontWeight: 700,
|
|
1766
|
+
padding: "7px 10px"
|
|
1767
|
+
},
|
|
1768
|
+
type: "button",
|
|
1769
|
+
children: savingStatus === "draft" ? "Saving..." : "Save Draft"
|
|
1770
|
+
}
|
|
1771
|
+
),
|
|
1772
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1773
|
+
"button",
|
|
1774
|
+
{
|
|
1775
|
+
disabled: savingStatus !== null,
|
|
1776
|
+
onClick: () => void saveLayout("published"),
|
|
1777
|
+
style: {
|
|
1778
|
+
background: "#0f7d52",
|
|
1779
|
+
border: "none",
|
|
1780
|
+
borderRadius: 999,
|
|
1781
|
+
color: "#fff",
|
|
1782
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1783
|
+
fontSize: 12,
|
|
1784
|
+
fontWeight: 700,
|
|
1785
|
+
padding: "7px 10px"
|
|
1786
|
+
},
|
|
1787
|
+
type: "button",
|
|
1788
|
+
children: savingStatus === "published" ? "Publishing..." : "Publish"
|
|
1789
|
+
}
|
|
1790
|
+
)
|
|
1791
|
+
] }),
|
|
1792
|
+
saveMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveMessage }) : null,
|
|
1793
|
+
saveError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveError }) : null
|
|
1794
|
+
] }),
|
|
1635
1795
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: sidebarSectionStyle, children: [
|
|
1636
1796
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Add Sections" }),
|
|
1637
1797
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -1788,6 +1948,38 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1788
1948
|
value: parseColor(selectedBlock.backgroundColor, "#124a37")
|
|
1789
1949
|
}
|
|
1790
1950
|
)
|
|
1951
|
+
] }),
|
|
1952
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
1953
|
+
"Upload Alt Text",
|
|
1954
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1955
|
+
"input",
|
|
1956
|
+
{
|
|
1957
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
1958
|
+
placeholder: "Describe the image",
|
|
1959
|
+
style: sidebarInputStyle,
|
|
1960
|
+
type: "text",
|
|
1961
|
+
value: uploadAltText
|
|
1962
|
+
}
|
|
1963
|
+
)
|
|
1964
|
+
] }),
|
|
1965
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
1966
|
+
"Upload Hero Background Image",
|
|
1967
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1968
|
+
"input",
|
|
1969
|
+
{
|
|
1970
|
+
accept: "image/*",
|
|
1971
|
+
disabled: uploadingTarget !== null,
|
|
1972
|
+
onChange: (event) => {
|
|
1973
|
+
const file = event.currentTarget.files?.[0];
|
|
1974
|
+
if (file) {
|
|
1975
|
+
void uploadMediaForSelected("hero", file);
|
|
1976
|
+
}
|
|
1977
|
+
event.currentTarget.value = "";
|
|
1978
|
+
},
|
|
1979
|
+
style: sidebarInputStyle,
|
|
1980
|
+
type: "file"
|
|
1981
|
+
}
|
|
1982
|
+
)
|
|
1791
1983
|
] })
|
|
1792
1984
|
] }) : null,
|
|
1793
1985
|
selectedType === "featureGrid" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
@@ -1861,20 +2053,54 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1861
2053
|
)
|
|
1862
2054
|
] })
|
|
1863
2055
|
] }) : null,
|
|
1864
|
-
selectedType === "media" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2056
|
+
selectedType === "media" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
2057
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
2058
|
+
"Image Size",
|
|
2059
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2060
|
+
"select",
|
|
2061
|
+
{
|
|
2062
|
+
onChange: (event) => updateSelectedField("size", event.target.value),
|
|
2063
|
+
style: sidebarInputStyle,
|
|
2064
|
+
value: normalizeText(selectedBlock.size, "default"),
|
|
2065
|
+
children: [
|
|
2066
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "default", children: "Default" }),
|
|
2067
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "wide", children: "Wide" })
|
|
2068
|
+
]
|
|
2069
|
+
}
|
|
2070
|
+
)
|
|
2071
|
+
] }),
|
|
2072
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
2073
|
+
"Upload Alt Text",
|
|
2074
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2075
|
+
"input",
|
|
2076
|
+
{
|
|
2077
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
2078
|
+
placeholder: "Describe the image",
|
|
2079
|
+
style: sidebarInputStyle,
|
|
2080
|
+
type: "text",
|
|
2081
|
+
value: uploadAltText
|
|
2082
|
+
}
|
|
2083
|
+
)
|
|
2084
|
+
] }),
|
|
2085
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
2086
|
+
"Upload Section Image",
|
|
2087
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2088
|
+
"input",
|
|
2089
|
+
{
|
|
2090
|
+
accept: "image/*",
|
|
2091
|
+
disabled: uploadingTarget !== null,
|
|
2092
|
+
onChange: (event) => {
|
|
2093
|
+
const file = event.currentTarget.files?.[0];
|
|
2094
|
+
if (file) {
|
|
2095
|
+
void uploadMediaForSelected("media", file);
|
|
2096
|
+
}
|
|
2097
|
+
event.currentTarget.value = "";
|
|
2098
|
+
},
|
|
2099
|
+
style: sidebarInputStyle,
|
|
2100
|
+
type: "file"
|
|
2101
|
+
}
|
|
2102
|
+
)
|
|
2103
|
+
] })
|
|
1878
2104
|
] }) : null,
|
|
1879
2105
|
selectedType === "richText" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
1880
2106
|
"Content Width",
|
|
@@ -1928,7 +2154,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1928
2154
|
children: "Add Testimonial"
|
|
1929
2155
|
}
|
|
1930
2156
|
) : null,
|
|
1931
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." })
|
|
2157
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." }),
|
|
2158
|
+
uploadingTarget ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Uploading image..." }) : null,
|
|
2159
|
+
uploadMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700 }, children: uploadMessage }) : null,
|
|
2160
|
+
uploadError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700 }, children: uploadError }) : null
|
|
1932
2161
|
] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "var(--ink-700)", fontSize: 12, margin: 0 }, children: "Click a section on the page preview to edit its options here." })
|
|
1933
2162
|
] })
|
|
1934
2163
|
] }) : null
|
|
@@ -490,6 +490,37 @@ function parseColor(value, fallback) {
|
|
|
490
490
|
}
|
|
491
491
|
return fallback;
|
|
492
492
|
}
|
|
493
|
+
function getRelationID(value) {
|
|
494
|
+
if (typeof value === "number" || typeof value === "string") {
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
if (!value || typeof value !== "object") {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
if ("id" in value) {
|
|
501
|
+
const id = value.id;
|
|
502
|
+
if (typeof id === "number" || typeof id === "string") {
|
|
503
|
+
return id;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
function extractUploadedMedia(value) {
|
|
509
|
+
const candidate = value && typeof value === "object" && "doc" in value ? value.doc : value;
|
|
510
|
+
if (!candidate || typeof candidate !== "object") {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const id = getRelationID(candidate);
|
|
514
|
+
if (id === null) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
alt: typeof candidate.alt === "string" ? candidate.alt : "",
|
|
519
|
+
filename: typeof candidate.filename === "string" ? candidate.filename : "",
|
|
520
|
+
id,
|
|
521
|
+
url: typeof candidate.url === "string" ? candidate.url : ""
|
|
522
|
+
};
|
|
523
|
+
}
|
|
493
524
|
function InlineText({
|
|
494
525
|
as = "p",
|
|
495
526
|
className,
|
|
@@ -779,6 +810,13 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
779
810
|
const [selectedIndex, setSelectedIndex] = useState(null);
|
|
780
811
|
const [dragIndex, setDragIndex] = useState(null);
|
|
781
812
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
813
|
+
const [savingStatus, setSavingStatus] = useState(null);
|
|
814
|
+
const [saveMessage, setSaveMessage] = useState("");
|
|
815
|
+
const [saveError, setSaveError] = useState("");
|
|
816
|
+
const [uploadingTarget, setUploadingTarget] = useState(null);
|
|
817
|
+
const [uploadError, setUploadError] = useState("");
|
|
818
|
+
const [uploadMessage, setUploadMessage] = useState("");
|
|
819
|
+
const [uploadAltText, setUploadAltText] = useState("");
|
|
782
820
|
const selectedBlock = useMemo(
|
|
783
821
|
() => selectedIndex !== null ? layout[selectedIndex] : null,
|
|
784
822
|
[layout, selectedIndex]
|
|
@@ -845,6 +883,76 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
845
883
|
const currentItems = Array.isArray(selectedBlock[fieldName]) ? selectedBlock[fieldName] : [];
|
|
846
884
|
updateSelectedArray(fieldName, [...currentItems, item]);
|
|
847
885
|
};
|
|
886
|
+
const uploadMediaForSelected = async (target, file) => {
|
|
887
|
+
if (selectedIndex === null) {
|
|
888
|
+
setUploadError("Select a section first.");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
setUploadingTarget(target);
|
|
892
|
+
setUploadError("");
|
|
893
|
+
setUploadMessage("");
|
|
894
|
+
try {
|
|
895
|
+
const formData = new FormData();
|
|
896
|
+
const fallbackAlt = file.name.replace(/\.[^/.]+$/, "").trim();
|
|
897
|
+
const resolvedAlt = uploadAltText.trim() || fallbackAlt || "Uploaded image";
|
|
898
|
+
formData.set("_payload", JSON.stringify({ alt: resolvedAlt }));
|
|
899
|
+
formData.set("file", file);
|
|
900
|
+
const response = await fetch("/api/media", {
|
|
901
|
+
body: formData,
|
|
902
|
+
credentials: "include",
|
|
903
|
+
method: "POST"
|
|
904
|
+
});
|
|
905
|
+
if (!response.ok) {
|
|
906
|
+
const body = await response.text();
|
|
907
|
+
throw new Error(body || "Upload failed");
|
|
908
|
+
}
|
|
909
|
+
const json = await response.json();
|
|
910
|
+
const uploaded = extractUploadedMedia(json);
|
|
911
|
+
if (!uploaded) {
|
|
912
|
+
throw new Error("Upload succeeded but returned media data was malformed.");
|
|
913
|
+
}
|
|
914
|
+
const nextLayout = cloneBlockLayout(layout);
|
|
915
|
+
const block = nextLayout[selectedIndex];
|
|
916
|
+
if (target === "hero") {
|
|
917
|
+
nextLayout[selectedIndex] = {
|
|
918
|
+
...block,
|
|
919
|
+
backgroundImageURL: "",
|
|
920
|
+
media: uploaded
|
|
921
|
+
};
|
|
922
|
+
} else {
|
|
923
|
+
nextLayout[selectedIndex] = {
|
|
924
|
+
...block,
|
|
925
|
+
image: uploaded
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
setLayout(nextLayout);
|
|
929
|
+
setUploadMessage("Image uploaded and attached to this section.");
|
|
930
|
+
} catch (error) {
|
|
931
|
+
setUploadError(error instanceof Error ? error.message : "Upload failed.");
|
|
932
|
+
} finally {
|
|
933
|
+
setUploadingTarget(null);
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
const toPersistedLayout = (sourceLayout) => sourceLayout.map((block) => {
|
|
937
|
+
if (!block || typeof block !== "object") {
|
|
938
|
+
return block;
|
|
939
|
+
}
|
|
940
|
+
const nextBlock = { ...block };
|
|
941
|
+
const blockType = normalizeText(nextBlock.blockType);
|
|
942
|
+
if (blockType === "hero") {
|
|
943
|
+
const mediaID = getRelationID(nextBlock.media);
|
|
944
|
+
if (mediaID !== null) {
|
|
945
|
+
nextBlock.media = mediaID;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (blockType === "media") {
|
|
949
|
+
const imageID = getRelationID(nextBlock.image);
|
|
950
|
+
if (imageID !== null) {
|
|
951
|
+
nextBlock.image = imageID;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return nextBlock;
|
|
955
|
+
});
|
|
848
956
|
const sidebarSectionStyle = {
|
|
849
957
|
border: "1px solid rgba(13, 74, 55, 0.15)",
|
|
850
958
|
borderRadius: 12,
|
|
@@ -893,12 +1001,16 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
893
1001
|
});
|
|
894
1002
|
};
|
|
895
1003
|
const saveLayout = async (status) => {
|
|
1004
|
+
setSavingStatus(status);
|
|
1005
|
+
setSaveError("");
|
|
1006
|
+
setSaveMessage("");
|
|
896
1007
|
try {
|
|
1008
|
+
const persistedLayout = toPersistedLayout(layout);
|
|
897
1009
|
const response = await fetch(`/api/pages/${pageID}`, {
|
|
898
1010
|
body: JSON.stringify({
|
|
899
1011
|
_status: status,
|
|
900
|
-
layout,
|
|
901
|
-
studioDocument: layoutToStudioDocument(
|
|
1012
|
+
layout: persistedLayout,
|
|
1013
|
+
studioDocument: layoutToStudioDocument(persistedLayout, title),
|
|
902
1014
|
title
|
|
903
1015
|
}),
|
|
904
1016
|
headers: {
|
|
@@ -910,6 +1022,7 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
910
1022
|
const body = await response.text();
|
|
911
1023
|
throw new Error(body || "Failed to save page");
|
|
912
1024
|
}
|
|
1025
|
+
setSaveMessage(status === "published" ? "Published." : "Draft saved.");
|
|
913
1026
|
window.parent?.postMessage(
|
|
914
1027
|
{
|
|
915
1028
|
message: status === "published" ? "Published." : "Draft saved.",
|
|
@@ -920,8 +1033,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
920
1033
|
},
|
|
921
1034
|
"*"
|
|
922
1035
|
);
|
|
1036
|
+
return true;
|
|
923
1037
|
} catch (error) {
|
|
924
1038
|
console.error(error);
|
|
1039
|
+
setSaveError("Could not save. Check permissions/session and retry.");
|
|
925
1040
|
window.parent?.postMessage(
|
|
926
1041
|
{
|
|
927
1042
|
message: "Could not save. Check permissions/session and retry.",
|
|
@@ -932,6 +1047,9 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
932
1047
|
},
|
|
933
1048
|
"*"
|
|
934
1049
|
);
|
|
1050
|
+
return false;
|
|
1051
|
+
} finally {
|
|
1052
|
+
setSavingStatus(null);
|
|
935
1053
|
}
|
|
936
1054
|
};
|
|
937
1055
|
useEffect(() => {
|
|
@@ -1604,6 +1722,48 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1604
1722
|
}
|
|
1605
1723
|
),
|
|
1606
1724
|
sidebarOpen ? /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12, maxHeight: "calc(100vh - 90px)", overflowY: "auto", padding: 12 }, children: [
|
|
1725
|
+
/* @__PURE__ */ jsxs("section", { style: sidebarSectionStyle, children: [
|
|
1726
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Save & Publish" }),
|
|
1727
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6 }, children: [
|
|
1728
|
+
/* @__PURE__ */ jsx(
|
|
1729
|
+
"button",
|
|
1730
|
+
{
|
|
1731
|
+
disabled: savingStatus !== null,
|
|
1732
|
+
onClick: () => void saveLayout("draft"),
|
|
1733
|
+
style: {
|
|
1734
|
+
borderRadius: 999,
|
|
1735
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1736
|
+
fontSize: 12,
|
|
1737
|
+
fontWeight: 700,
|
|
1738
|
+
padding: "7px 10px"
|
|
1739
|
+
},
|
|
1740
|
+
type: "button",
|
|
1741
|
+
children: savingStatus === "draft" ? "Saving..." : "Save Draft"
|
|
1742
|
+
}
|
|
1743
|
+
),
|
|
1744
|
+
/* @__PURE__ */ jsx(
|
|
1745
|
+
"button",
|
|
1746
|
+
{
|
|
1747
|
+
disabled: savingStatus !== null,
|
|
1748
|
+
onClick: () => void saveLayout("published"),
|
|
1749
|
+
style: {
|
|
1750
|
+
background: "#0f7d52",
|
|
1751
|
+
border: "none",
|
|
1752
|
+
borderRadius: 999,
|
|
1753
|
+
color: "#fff",
|
|
1754
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1755
|
+
fontSize: 12,
|
|
1756
|
+
fontWeight: 700,
|
|
1757
|
+
padding: "7px 10px"
|
|
1758
|
+
},
|
|
1759
|
+
type: "button",
|
|
1760
|
+
children: savingStatus === "published" ? "Publishing..." : "Publish"
|
|
1761
|
+
}
|
|
1762
|
+
)
|
|
1763
|
+
] }),
|
|
1764
|
+
saveMessage ? /* @__PURE__ */ jsx("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveMessage }) : null,
|
|
1765
|
+
saveError ? /* @__PURE__ */ jsx("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveError }) : null
|
|
1766
|
+
] }),
|
|
1607
1767
|
/* @__PURE__ */ jsxs("section", { style: sidebarSectionStyle, children: [
|
|
1608
1768
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Add Sections" }),
|
|
1609
1769
|
/* @__PURE__ */ jsxs(
|
|
@@ -1760,6 +1920,38 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1760
1920
|
value: parseColor(selectedBlock.backgroundColor, "#124a37")
|
|
1761
1921
|
}
|
|
1762
1922
|
)
|
|
1923
|
+
] }),
|
|
1924
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
1925
|
+
"Upload Alt Text",
|
|
1926
|
+
/* @__PURE__ */ jsx(
|
|
1927
|
+
"input",
|
|
1928
|
+
{
|
|
1929
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
1930
|
+
placeholder: "Describe the image",
|
|
1931
|
+
style: sidebarInputStyle,
|
|
1932
|
+
type: "text",
|
|
1933
|
+
value: uploadAltText
|
|
1934
|
+
}
|
|
1935
|
+
)
|
|
1936
|
+
] }),
|
|
1937
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
1938
|
+
"Upload Hero Background Image",
|
|
1939
|
+
/* @__PURE__ */ jsx(
|
|
1940
|
+
"input",
|
|
1941
|
+
{
|
|
1942
|
+
accept: "image/*",
|
|
1943
|
+
disabled: uploadingTarget !== null,
|
|
1944
|
+
onChange: (event) => {
|
|
1945
|
+
const file = event.currentTarget.files?.[0];
|
|
1946
|
+
if (file) {
|
|
1947
|
+
void uploadMediaForSelected("hero", file);
|
|
1948
|
+
}
|
|
1949
|
+
event.currentTarget.value = "";
|
|
1950
|
+
},
|
|
1951
|
+
style: sidebarInputStyle,
|
|
1952
|
+
type: "file"
|
|
1953
|
+
}
|
|
1954
|
+
)
|
|
1763
1955
|
] })
|
|
1764
1956
|
] }) : null,
|
|
1765
1957
|
selectedType === "featureGrid" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1833,20 +2025,54 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1833
2025
|
)
|
|
1834
2026
|
] })
|
|
1835
2027
|
] }) : null,
|
|
1836
|
-
selectedType === "media" ? /* @__PURE__ */ jsxs(
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2028
|
+
selectedType === "media" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2029
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
2030
|
+
"Image Size",
|
|
2031
|
+
/* @__PURE__ */ jsxs(
|
|
2032
|
+
"select",
|
|
2033
|
+
{
|
|
2034
|
+
onChange: (event) => updateSelectedField("size", event.target.value),
|
|
2035
|
+
style: sidebarInputStyle,
|
|
2036
|
+
value: normalizeText(selectedBlock.size, "default"),
|
|
2037
|
+
children: [
|
|
2038
|
+
/* @__PURE__ */ jsx("option", { value: "default", children: "Default" }),
|
|
2039
|
+
/* @__PURE__ */ jsx("option", { value: "wide", children: "Wide" })
|
|
2040
|
+
]
|
|
2041
|
+
}
|
|
2042
|
+
)
|
|
2043
|
+
] }),
|
|
2044
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
2045
|
+
"Upload Alt Text",
|
|
2046
|
+
/* @__PURE__ */ jsx(
|
|
2047
|
+
"input",
|
|
2048
|
+
{
|
|
2049
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
2050
|
+
placeholder: "Describe the image",
|
|
2051
|
+
style: sidebarInputStyle,
|
|
2052
|
+
type: "text",
|
|
2053
|
+
value: uploadAltText
|
|
2054
|
+
}
|
|
2055
|
+
)
|
|
2056
|
+
] }),
|
|
2057
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
2058
|
+
"Upload Section Image",
|
|
2059
|
+
/* @__PURE__ */ jsx(
|
|
2060
|
+
"input",
|
|
2061
|
+
{
|
|
2062
|
+
accept: "image/*",
|
|
2063
|
+
disabled: uploadingTarget !== null,
|
|
2064
|
+
onChange: (event) => {
|
|
2065
|
+
const file = event.currentTarget.files?.[0];
|
|
2066
|
+
if (file) {
|
|
2067
|
+
void uploadMediaForSelected("media", file);
|
|
2068
|
+
}
|
|
2069
|
+
event.currentTarget.value = "";
|
|
2070
|
+
},
|
|
2071
|
+
style: sidebarInputStyle,
|
|
2072
|
+
type: "file"
|
|
2073
|
+
}
|
|
2074
|
+
)
|
|
2075
|
+
] })
|
|
1850
2076
|
] }) : null,
|
|
1851
2077
|
selectedType === "richText" ? /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
1852
2078
|
"Content Width",
|
|
@@ -1900,7 +2126,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1900
2126
|
children: "Add Testimonial"
|
|
1901
2127
|
}
|
|
1902
2128
|
) : null,
|
|
1903
|
-
/* @__PURE__ */ jsx("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." })
|
|
2129
|
+
/* @__PURE__ */ jsx("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." }),
|
|
2130
|
+
uploadingTarget ? /* @__PURE__ */ jsx("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Uploading image..." }) : null,
|
|
2131
|
+
uploadMessage ? /* @__PURE__ */ jsx("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700 }, children: uploadMessage }) : null,
|
|
2132
|
+
uploadError ? /* @__PURE__ */ jsx("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700 }, children: uploadError }) : null
|
|
1904
2133
|
] }) : /* @__PURE__ */ jsx("p", { style: { color: "var(--ink-700)", fontSize: 12, margin: 0 }, children: "Click a section on the page preview to edit its options here." })
|
|
1905
2134
|
] })
|
|
1906
2135
|
] }) : null
|