@orion-studios/payload-studio 0.4.0-beta.0 → 0.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/admin-app/styles.css +5 -0
- package/dist/index.mjs +6 -6
- package/dist/studio-pages/builder.css +16 -3
- package/dist/studio-pages/client.js +245 -17
- package/dist/studio-pages/client.mjs +245 -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
|
package/dist/index.mjs
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
admin_exports
|
|
3
3
|
} from "./chunk-J7W5EE3B.mjs";
|
|
4
|
-
import {
|
|
5
|
-
admin_app_exports
|
|
6
|
-
} from "./chunk-AAOHJDNS.mjs";
|
|
7
4
|
import {
|
|
8
5
|
studio_exports
|
|
9
6
|
} from "./chunk-WLXZDMK3.mjs";
|
|
10
|
-
import {
|
|
11
|
-
studio_pages_exports
|
|
12
|
-
} from "./chunk-Q76U4Z53.mjs";
|
|
13
7
|
import {
|
|
14
8
|
nextjs_exports
|
|
15
9
|
} from "./chunk-ZLLNO5FM.mjs";
|
|
10
|
+
import {
|
|
11
|
+
admin_app_exports
|
|
12
|
+
} from "./chunk-AAOHJDNS.mjs";
|
|
13
|
+
import {
|
|
14
|
+
studio_pages_exports
|
|
15
|
+
} from "./chunk-Q76U4Z53.mjs";
|
|
16
16
|
import {
|
|
17
17
|
blocks_exports
|
|
18
18
|
} from "./chunk-L62FYT57.mjs";
|
|
@@ -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,75 @@ 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
|
+
formData.set("alt", uploadAltText.trim() || fallbackAlt || "Uploaded image");
|
|
926
|
+
formData.set("file", file);
|
|
927
|
+
const response = await fetch("/api/media", {
|
|
928
|
+
body: formData,
|
|
929
|
+
credentials: "include",
|
|
930
|
+
method: "POST"
|
|
931
|
+
});
|
|
932
|
+
if (!response.ok) {
|
|
933
|
+
const body = await response.text();
|
|
934
|
+
throw new Error(body || "Upload failed");
|
|
935
|
+
}
|
|
936
|
+
const json = await response.json();
|
|
937
|
+
const uploaded = extractUploadedMedia(json);
|
|
938
|
+
if (!uploaded) {
|
|
939
|
+
throw new Error("Upload succeeded but returned media data was malformed.");
|
|
940
|
+
}
|
|
941
|
+
const nextLayout = cloneBlockLayout(layout);
|
|
942
|
+
const block = nextLayout[selectedIndex];
|
|
943
|
+
if (target === "hero") {
|
|
944
|
+
nextLayout[selectedIndex] = {
|
|
945
|
+
...block,
|
|
946
|
+
backgroundImageURL: "",
|
|
947
|
+
media: uploaded
|
|
948
|
+
};
|
|
949
|
+
} else {
|
|
950
|
+
nextLayout[selectedIndex] = {
|
|
951
|
+
...block,
|
|
952
|
+
image: uploaded
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
setLayout(nextLayout);
|
|
956
|
+
setUploadMessage("Image uploaded and attached to this section.");
|
|
957
|
+
} catch (error) {
|
|
958
|
+
setUploadError(error instanceof Error ? error.message : "Upload failed.");
|
|
959
|
+
} finally {
|
|
960
|
+
setUploadingTarget(null);
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
const toPersistedLayout = (sourceLayout) => sourceLayout.map((block) => {
|
|
964
|
+
if (!block || typeof block !== "object") {
|
|
965
|
+
return block;
|
|
966
|
+
}
|
|
967
|
+
const nextBlock = { ...block };
|
|
968
|
+
const blockType = normalizeText(nextBlock.blockType);
|
|
969
|
+
if (blockType === "hero") {
|
|
970
|
+
const mediaID = getRelationID(nextBlock.media);
|
|
971
|
+
if (mediaID !== null) {
|
|
972
|
+
nextBlock.media = mediaID;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (blockType === "media") {
|
|
976
|
+
const imageID = getRelationID(nextBlock.image);
|
|
977
|
+
if (imageID !== null) {
|
|
978
|
+
nextBlock.image = imageID;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return nextBlock;
|
|
982
|
+
});
|
|
876
983
|
const sidebarSectionStyle = {
|
|
877
984
|
border: "1px solid rgba(13, 74, 55, 0.15)",
|
|
878
985
|
borderRadius: 12,
|
|
@@ -921,12 +1028,16 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
921
1028
|
});
|
|
922
1029
|
};
|
|
923
1030
|
const saveLayout = async (status) => {
|
|
1031
|
+
setSavingStatus(status);
|
|
1032
|
+
setSaveError("");
|
|
1033
|
+
setSaveMessage("");
|
|
924
1034
|
try {
|
|
1035
|
+
const persistedLayout = toPersistedLayout(layout);
|
|
925
1036
|
const response = await fetch(`/api/pages/${pageID}`, {
|
|
926
1037
|
body: JSON.stringify({
|
|
927
1038
|
_status: status,
|
|
928
|
-
layout,
|
|
929
|
-
studioDocument: layoutToStudioDocument(
|
|
1039
|
+
layout: persistedLayout,
|
|
1040
|
+
studioDocument: layoutToStudioDocument(persistedLayout, title),
|
|
930
1041
|
title
|
|
931
1042
|
}),
|
|
932
1043
|
headers: {
|
|
@@ -938,6 +1049,7 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
938
1049
|
const body = await response.text();
|
|
939
1050
|
throw new Error(body || "Failed to save page");
|
|
940
1051
|
}
|
|
1052
|
+
setSaveMessage(status === "published" ? "Published." : "Draft saved.");
|
|
941
1053
|
window.parent?.postMessage(
|
|
942
1054
|
{
|
|
943
1055
|
message: status === "published" ? "Published." : "Draft saved.",
|
|
@@ -948,8 +1060,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
948
1060
|
},
|
|
949
1061
|
"*"
|
|
950
1062
|
);
|
|
1063
|
+
return true;
|
|
951
1064
|
} catch (error) {
|
|
952
1065
|
console.error(error);
|
|
1066
|
+
setSaveError("Could not save. Check permissions/session and retry.");
|
|
953
1067
|
window.parent?.postMessage(
|
|
954
1068
|
{
|
|
955
1069
|
message: "Could not save. Check permissions/session and retry.",
|
|
@@ -960,6 +1074,9 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
960
1074
|
},
|
|
961
1075
|
"*"
|
|
962
1076
|
);
|
|
1077
|
+
return false;
|
|
1078
|
+
} finally {
|
|
1079
|
+
setSavingStatus(null);
|
|
963
1080
|
}
|
|
964
1081
|
};
|
|
965
1082
|
(0, import_react.useEffect)(() => {
|
|
@@ -1632,6 +1749,48 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1632
1749
|
}
|
|
1633
1750
|
),
|
|
1634
1751
|
sidebarOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "grid", gap: 12, maxHeight: "calc(100vh - 90px)", overflowY: "auto", padding: 12 }, children: [
|
|
1752
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: sidebarSectionStyle, children: [
|
|
1753
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Save & Publish" }),
|
|
1754
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
|
|
1755
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1756
|
+
"button",
|
|
1757
|
+
{
|
|
1758
|
+
disabled: savingStatus !== null,
|
|
1759
|
+
onClick: () => void saveLayout("draft"),
|
|
1760
|
+
style: {
|
|
1761
|
+
borderRadius: 999,
|
|
1762
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1763
|
+
fontSize: 12,
|
|
1764
|
+
fontWeight: 700,
|
|
1765
|
+
padding: "7px 10px"
|
|
1766
|
+
},
|
|
1767
|
+
type: "button",
|
|
1768
|
+
children: savingStatus === "draft" ? "Saving..." : "Save Draft"
|
|
1769
|
+
}
|
|
1770
|
+
),
|
|
1771
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1772
|
+
"button",
|
|
1773
|
+
{
|
|
1774
|
+
disabled: savingStatus !== null,
|
|
1775
|
+
onClick: () => void saveLayout("published"),
|
|
1776
|
+
style: {
|
|
1777
|
+
background: "#0f7d52",
|
|
1778
|
+
border: "none",
|
|
1779
|
+
borderRadius: 999,
|
|
1780
|
+
color: "#fff",
|
|
1781
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1782
|
+
fontSize: 12,
|
|
1783
|
+
fontWeight: 700,
|
|
1784
|
+
padding: "7px 10px"
|
|
1785
|
+
},
|
|
1786
|
+
type: "button",
|
|
1787
|
+
children: savingStatus === "published" ? "Publishing..." : "Publish"
|
|
1788
|
+
}
|
|
1789
|
+
)
|
|
1790
|
+
] }),
|
|
1791
|
+
saveMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveMessage }) : null,
|
|
1792
|
+
saveError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveError }) : null
|
|
1793
|
+
] }),
|
|
1635
1794
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: sidebarSectionStyle, children: [
|
|
1636
1795
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Add Sections" }),
|
|
1637
1796
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -1788,6 +1947,38 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1788
1947
|
value: parseColor(selectedBlock.backgroundColor, "#124a37")
|
|
1789
1948
|
}
|
|
1790
1949
|
)
|
|
1950
|
+
] }),
|
|
1951
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
1952
|
+
"Upload Alt Text",
|
|
1953
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1954
|
+
"input",
|
|
1955
|
+
{
|
|
1956
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
1957
|
+
placeholder: "Describe the image",
|
|
1958
|
+
style: sidebarInputStyle,
|
|
1959
|
+
type: "text",
|
|
1960
|
+
value: uploadAltText
|
|
1961
|
+
}
|
|
1962
|
+
)
|
|
1963
|
+
] }),
|
|
1964
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
1965
|
+
"Upload Hero Background Image",
|
|
1966
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1967
|
+
"input",
|
|
1968
|
+
{
|
|
1969
|
+
accept: "image/*",
|
|
1970
|
+
disabled: uploadingTarget !== null,
|
|
1971
|
+
onChange: (event) => {
|
|
1972
|
+
const file = event.currentTarget.files?.[0];
|
|
1973
|
+
if (file) {
|
|
1974
|
+
void uploadMediaForSelected("hero", file);
|
|
1975
|
+
}
|
|
1976
|
+
event.currentTarget.value = "";
|
|
1977
|
+
},
|
|
1978
|
+
style: sidebarInputStyle,
|
|
1979
|
+
type: "file"
|
|
1980
|
+
}
|
|
1981
|
+
)
|
|
1791
1982
|
] })
|
|
1792
1983
|
] }) : null,
|
|
1793
1984
|
selectedType === "featureGrid" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
@@ -1861,20 +2052,54 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1861
2052
|
)
|
|
1862
2053
|
] })
|
|
1863
2054
|
] }) : null,
|
|
1864
|
-
selectedType === "media" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
2055
|
+
selectedType === "media" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
2056
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
2057
|
+
"Image Size",
|
|
2058
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2059
|
+
"select",
|
|
2060
|
+
{
|
|
2061
|
+
onChange: (event) => updateSelectedField("size", event.target.value),
|
|
2062
|
+
style: sidebarInputStyle,
|
|
2063
|
+
value: normalizeText(selectedBlock.size, "default"),
|
|
2064
|
+
children: [
|
|
2065
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "default", children: "Default" }),
|
|
2066
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "wide", children: "Wide" })
|
|
2067
|
+
]
|
|
2068
|
+
}
|
|
2069
|
+
)
|
|
2070
|
+
] }),
|
|
2071
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
2072
|
+
"Upload Alt Text",
|
|
2073
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2074
|
+
"input",
|
|
2075
|
+
{
|
|
2076
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
2077
|
+
placeholder: "Describe the image",
|
|
2078
|
+
style: sidebarInputStyle,
|
|
2079
|
+
type: "text",
|
|
2080
|
+
value: uploadAltText
|
|
2081
|
+
}
|
|
2082
|
+
)
|
|
2083
|
+
] }),
|
|
2084
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
2085
|
+
"Upload Section Image",
|
|
2086
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2087
|
+
"input",
|
|
2088
|
+
{
|
|
2089
|
+
accept: "image/*",
|
|
2090
|
+
disabled: uploadingTarget !== null,
|
|
2091
|
+
onChange: (event) => {
|
|
2092
|
+
const file = event.currentTarget.files?.[0];
|
|
2093
|
+
if (file) {
|
|
2094
|
+
void uploadMediaForSelected("media", file);
|
|
2095
|
+
}
|
|
2096
|
+
event.currentTarget.value = "";
|
|
2097
|
+
},
|
|
2098
|
+
style: sidebarInputStyle,
|
|
2099
|
+
type: "file"
|
|
2100
|
+
}
|
|
2101
|
+
)
|
|
2102
|
+
] })
|
|
1878
2103
|
] }) : null,
|
|
1879
2104
|
selectedType === "richText" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
|
|
1880
2105
|
"Content Width",
|
|
@@ -1928,7 +2153,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1928
2153
|
children: "Add Testimonial"
|
|
1929
2154
|
}
|
|
1930
2155
|
) : 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." })
|
|
2156
|
+
/* @__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
|
+
uploadingTarget ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Uploading image..." }) : null,
|
|
2158
|
+
uploadMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700 }, children: uploadMessage }) : null,
|
|
2159
|
+
uploadError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700 }, children: uploadError }) : null
|
|
1932
2160
|
] }) : /* @__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
2161
|
] })
|
|
1934
2162
|
] }) : 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,75 @@ 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
|
+
formData.set("alt", uploadAltText.trim() || fallbackAlt || "Uploaded image");
|
|
898
|
+
formData.set("file", file);
|
|
899
|
+
const response = await fetch("/api/media", {
|
|
900
|
+
body: formData,
|
|
901
|
+
credentials: "include",
|
|
902
|
+
method: "POST"
|
|
903
|
+
});
|
|
904
|
+
if (!response.ok) {
|
|
905
|
+
const body = await response.text();
|
|
906
|
+
throw new Error(body || "Upload failed");
|
|
907
|
+
}
|
|
908
|
+
const json = await response.json();
|
|
909
|
+
const uploaded = extractUploadedMedia(json);
|
|
910
|
+
if (!uploaded) {
|
|
911
|
+
throw new Error("Upload succeeded but returned media data was malformed.");
|
|
912
|
+
}
|
|
913
|
+
const nextLayout = cloneBlockLayout(layout);
|
|
914
|
+
const block = nextLayout[selectedIndex];
|
|
915
|
+
if (target === "hero") {
|
|
916
|
+
nextLayout[selectedIndex] = {
|
|
917
|
+
...block,
|
|
918
|
+
backgroundImageURL: "",
|
|
919
|
+
media: uploaded
|
|
920
|
+
};
|
|
921
|
+
} else {
|
|
922
|
+
nextLayout[selectedIndex] = {
|
|
923
|
+
...block,
|
|
924
|
+
image: uploaded
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
setLayout(nextLayout);
|
|
928
|
+
setUploadMessage("Image uploaded and attached to this section.");
|
|
929
|
+
} catch (error) {
|
|
930
|
+
setUploadError(error instanceof Error ? error.message : "Upload failed.");
|
|
931
|
+
} finally {
|
|
932
|
+
setUploadingTarget(null);
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
const toPersistedLayout = (sourceLayout) => sourceLayout.map((block) => {
|
|
936
|
+
if (!block || typeof block !== "object") {
|
|
937
|
+
return block;
|
|
938
|
+
}
|
|
939
|
+
const nextBlock = { ...block };
|
|
940
|
+
const blockType = normalizeText(nextBlock.blockType);
|
|
941
|
+
if (blockType === "hero") {
|
|
942
|
+
const mediaID = getRelationID(nextBlock.media);
|
|
943
|
+
if (mediaID !== null) {
|
|
944
|
+
nextBlock.media = mediaID;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (blockType === "media") {
|
|
948
|
+
const imageID = getRelationID(nextBlock.image);
|
|
949
|
+
if (imageID !== null) {
|
|
950
|
+
nextBlock.image = imageID;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return nextBlock;
|
|
954
|
+
});
|
|
848
955
|
const sidebarSectionStyle = {
|
|
849
956
|
border: "1px solid rgba(13, 74, 55, 0.15)",
|
|
850
957
|
borderRadius: 12,
|
|
@@ -893,12 +1000,16 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
893
1000
|
});
|
|
894
1001
|
};
|
|
895
1002
|
const saveLayout = async (status) => {
|
|
1003
|
+
setSavingStatus(status);
|
|
1004
|
+
setSaveError("");
|
|
1005
|
+
setSaveMessage("");
|
|
896
1006
|
try {
|
|
1007
|
+
const persistedLayout = toPersistedLayout(layout);
|
|
897
1008
|
const response = await fetch(`/api/pages/${pageID}`, {
|
|
898
1009
|
body: JSON.stringify({
|
|
899
1010
|
_status: status,
|
|
900
|
-
layout,
|
|
901
|
-
studioDocument: layoutToStudioDocument(
|
|
1011
|
+
layout: persistedLayout,
|
|
1012
|
+
studioDocument: layoutToStudioDocument(persistedLayout, title),
|
|
902
1013
|
title
|
|
903
1014
|
}),
|
|
904
1015
|
headers: {
|
|
@@ -910,6 +1021,7 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
910
1021
|
const body = await response.text();
|
|
911
1022
|
throw new Error(body || "Failed to save page");
|
|
912
1023
|
}
|
|
1024
|
+
setSaveMessage(status === "published" ? "Published." : "Draft saved.");
|
|
913
1025
|
window.parent?.postMessage(
|
|
914
1026
|
{
|
|
915
1027
|
message: status === "published" ? "Published." : "Draft saved.",
|
|
@@ -920,8 +1032,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
920
1032
|
},
|
|
921
1033
|
"*"
|
|
922
1034
|
);
|
|
1035
|
+
return true;
|
|
923
1036
|
} catch (error) {
|
|
924
1037
|
console.error(error);
|
|
1038
|
+
setSaveError("Could not save. Check permissions/session and retry.");
|
|
925
1039
|
window.parent?.postMessage(
|
|
926
1040
|
{
|
|
927
1041
|
message: "Could not save. Check permissions/session and retry.",
|
|
@@ -932,6 +1046,9 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
932
1046
|
},
|
|
933
1047
|
"*"
|
|
934
1048
|
);
|
|
1049
|
+
return false;
|
|
1050
|
+
} finally {
|
|
1051
|
+
setSavingStatus(null);
|
|
935
1052
|
}
|
|
936
1053
|
};
|
|
937
1054
|
useEffect(() => {
|
|
@@ -1604,6 +1721,48 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1604
1721
|
}
|
|
1605
1722
|
),
|
|
1606
1723
|
sidebarOpen ? /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12, maxHeight: "calc(100vh - 90px)", overflowY: "auto", padding: 12 }, children: [
|
|
1724
|
+
/* @__PURE__ */ jsxs("section", { style: sidebarSectionStyle, children: [
|
|
1725
|
+
/* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Save & Publish" }),
|
|
1726
|
+
/* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6 }, children: [
|
|
1727
|
+
/* @__PURE__ */ jsx(
|
|
1728
|
+
"button",
|
|
1729
|
+
{
|
|
1730
|
+
disabled: savingStatus !== null,
|
|
1731
|
+
onClick: () => void saveLayout("draft"),
|
|
1732
|
+
style: {
|
|
1733
|
+
borderRadius: 999,
|
|
1734
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1735
|
+
fontSize: 12,
|
|
1736
|
+
fontWeight: 700,
|
|
1737
|
+
padding: "7px 10px"
|
|
1738
|
+
},
|
|
1739
|
+
type: "button",
|
|
1740
|
+
children: savingStatus === "draft" ? "Saving..." : "Save Draft"
|
|
1741
|
+
}
|
|
1742
|
+
),
|
|
1743
|
+
/* @__PURE__ */ jsx(
|
|
1744
|
+
"button",
|
|
1745
|
+
{
|
|
1746
|
+
disabled: savingStatus !== null,
|
|
1747
|
+
onClick: () => void saveLayout("published"),
|
|
1748
|
+
style: {
|
|
1749
|
+
background: "#0f7d52",
|
|
1750
|
+
border: "none",
|
|
1751
|
+
borderRadius: 999,
|
|
1752
|
+
color: "#fff",
|
|
1753
|
+
cursor: savingStatus ? "not-allowed" : "pointer",
|
|
1754
|
+
fontSize: 12,
|
|
1755
|
+
fontWeight: 700,
|
|
1756
|
+
padding: "7px 10px"
|
|
1757
|
+
},
|
|
1758
|
+
type: "button",
|
|
1759
|
+
children: savingStatus === "published" ? "Publishing..." : "Publish"
|
|
1760
|
+
}
|
|
1761
|
+
)
|
|
1762
|
+
] }),
|
|
1763
|
+
saveMessage ? /* @__PURE__ */ jsx("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveMessage }) : null,
|
|
1764
|
+
saveError ? /* @__PURE__ */ jsx("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveError }) : null
|
|
1765
|
+
] }),
|
|
1607
1766
|
/* @__PURE__ */ jsxs("section", { style: sidebarSectionStyle, children: [
|
|
1608
1767
|
/* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Add Sections" }),
|
|
1609
1768
|
/* @__PURE__ */ jsxs(
|
|
@@ -1760,6 +1919,38 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1760
1919
|
value: parseColor(selectedBlock.backgroundColor, "#124a37")
|
|
1761
1920
|
}
|
|
1762
1921
|
)
|
|
1922
|
+
] }),
|
|
1923
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
1924
|
+
"Upload Alt Text",
|
|
1925
|
+
/* @__PURE__ */ jsx(
|
|
1926
|
+
"input",
|
|
1927
|
+
{
|
|
1928
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
1929
|
+
placeholder: "Describe the image",
|
|
1930
|
+
style: sidebarInputStyle,
|
|
1931
|
+
type: "text",
|
|
1932
|
+
value: uploadAltText
|
|
1933
|
+
}
|
|
1934
|
+
)
|
|
1935
|
+
] }),
|
|
1936
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
1937
|
+
"Upload Hero Background Image",
|
|
1938
|
+
/* @__PURE__ */ jsx(
|
|
1939
|
+
"input",
|
|
1940
|
+
{
|
|
1941
|
+
accept: "image/*",
|
|
1942
|
+
disabled: uploadingTarget !== null,
|
|
1943
|
+
onChange: (event) => {
|
|
1944
|
+
const file = event.currentTarget.files?.[0];
|
|
1945
|
+
if (file) {
|
|
1946
|
+
void uploadMediaForSelected("hero", file);
|
|
1947
|
+
}
|
|
1948
|
+
event.currentTarget.value = "";
|
|
1949
|
+
},
|
|
1950
|
+
style: sidebarInputStyle,
|
|
1951
|
+
type: "file"
|
|
1952
|
+
}
|
|
1953
|
+
)
|
|
1763
1954
|
] })
|
|
1764
1955
|
] }) : null,
|
|
1765
1956
|
selectedType === "featureGrid" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -1833,20 +2024,54 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1833
2024
|
)
|
|
1834
2025
|
] })
|
|
1835
2026
|
] }) : null,
|
|
1836
|
-
selectedType === "media" ? /* @__PURE__ */ jsxs(
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2027
|
+
selectedType === "media" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2028
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
2029
|
+
"Image Size",
|
|
2030
|
+
/* @__PURE__ */ jsxs(
|
|
2031
|
+
"select",
|
|
2032
|
+
{
|
|
2033
|
+
onChange: (event) => updateSelectedField("size", event.target.value),
|
|
2034
|
+
style: sidebarInputStyle,
|
|
2035
|
+
value: normalizeText(selectedBlock.size, "default"),
|
|
2036
|
+
children: [
|
|
2037
|
+
/* @__PURE__ */ jsx("option", { value: "default", children: "Default" }),
|
|
2038
|
+
/* @__PURE__ */ jsx("option", { value: "wide", children: "Wide" })
|
|
2039
|
+
]
|
|
2040
|
+
}
|
|
2041
|
+
)
|
|
2042
|
+
] }),
|
|
2043
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
2044
|
+
"Upload Alt Text",
|
|
2045
|
+
/* @__PURE__ */ jsx(
|
|
2046
|
+
"input",
|
|
2047
|
+
{
|
|
2048
|
+
onChange: (event) => setUploadAltText(event.target.value),
|
|
2049
|
+
placeholder: "Describe the image",
|
|
2050
|
+
style: sidebarInputStyle,
|
|
2051
|
+
type: "text",
|
|
2052
|
+
value: uploadAltText
|
|
2053
|
+
}
|
|
2054
|
+
)
|
|
2055
|
+
] }),
|
|
2056
|
+
/* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
2057
|
+
"Upload Section Image",
|
|
2058
|
+
/* @__PURE__ */ jsx(
|
|
2059
|
+
"input",
|
|
2060
|
+
{
|
|
2061
|
+
accept: "image/*",
|
|
2062
|
+
disabled: uploadingTarget !== null,
|
|
2063
|
+
onChange: (event) => {
|
|
2064
|
+
const file = event.currentTarget.files?.[0];
|
|
2065
|
+
if (file) {
|
|
2066
|
+
void uploadMediaForSelected("media", file);
|
|
2067
|
+
}
|
|
2068
|
+
event.currentTarget.value = "";
|
|
2069
|
+
},
|
|
2070
|
+
style: sidebarInputStyle,
|
|
2071
|
+
type: "file"
|
|
2072
|
+
}
|
|
2073
|
+
)
|
|
2074
|
+
] })
|
|
1850
2075
|
] }) : null,
|
|
1851
2076
|
selectedType === "richText" ? /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
|
|
1852
2077
|
"Content Width",
|
|
@@ -1900,7 +2125,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
|
|
|
1900
2125
|
children: "Add Testimonial"
|
|
1901
2126
|
}
|
|
1902
2127
|
) : 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." })
|
|
2128
|
+
/* @__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
|
+
uploadingTarget ? /* @__PURE__ */ jsx("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Uploading image..." }) : null,
|
|
2130
|
+
uploadMessage ? /* @__PURE__ */ jsx("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700 }, children: uploadMessage }) : null,
|
|
2131
|
+
uploadError ? /* @__PURE__ */ jsx("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700 }, children: uploadError }) : null
|
|
1904
2132
|
] }) : /* @__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
2133
|
] })
|
|
1906
2134
|
] }) : null
|