@markopolo_ai_inc/markopolo-email-editor 1.0.3
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 +63 -0
- package/build/asset-manifest.json +12 -0
- package/build/favicon.ico +0 -0
- package/build/index.html +1 -0
- package/build/logo192.png +0 -0
- package/build/logo512.png +0 -0
- package/build/manifest.json +25 -0
- package/build/robots.txt +3 -0
- package/build/static/css/main.588cb535.css +9 -0
- package/build/static/js/206.a4343501.chunk.js +1 -0
- package/build/static/js/main.053d366a.js +2 -0
- package/build/static/js/main.053d366a.js.LICENSE.txt +56 -0
- package/package.json +64 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +50 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/robots.txt +3 -0
- package/src/App.js +15 -0
- package/src/components/EmailEditor/assets/App.css +339 -0
- package/src/components/EmailEditor/assets/Columns.css +309 -0
- package/src/components/EmailEditor/assets/Header.css +174 -0
- package/src/components/EmailEditor/assets/ImageBlock.css +12 -0
- package/src/components/EmailEditor/assets/Preview.css +30 -0
- package/src/components/EmailEditor/assets/RichText.css +199 -0
- package/src/components/EmailEditor/assets/RightSettings.css +520 -0
- package/src/components/EmailEditor/assets/Sidebar.css +195 -0
- package/src/components/EmailEditor/components/BlockItems/ButtonBlock.js +25 -0
- package/src/components/EmailEditor/components/BlockItems/DividerBlock.js +19 -0
- package/src/components/EmailEditor/components/BlockItems/HeadingBlock.js +16 -0
- package/src/components/EmailEditor/components/BlockItems/ImageBlock.js +28 -0
- package/src/components/EmailEditor/components/BlockItems/MenuBlock.js +52 -0
- package/src/components/EmailEditor/components/BlockItems/SocialLinkBlocks.js +26 -0
- package/src/components/EmailEditor/components/BlockItems/SpacerBlock.js +23 -0
- package/src/components/EmailEditor/components/BlockItems/TextBlock.js +16 -0
- package/src/components/EmailEditor/components/BlockItems/index.js +25 -0
- package/src/components/EmailEditor/components/ColorPicker/index.js +26 -0
- package/src/components/EmailEditor/components/Column/index.js +253 -0
- package/src/components/EmailEditor/components/Header/index.js +243 -0
- package/src/components/EmailEditor/components/LeftSideBar/index.js +253 -0
- package/src/components/EmailEditor/components/Main/index.js +281 -0
- package/src/components/EmailEditor/components/Preview/index.js +97 -0
- package/src/components/EmailEditor/components/RichText/Bold.js +37 -0
- package/src/components/EmailEditor/components/RichText/FontColor.js +39 -0
- package/src/components/EmailEditor/components/RichText/InsertOrderedList.js +36 -0
- package/src/components/EmailEditor/components/RichText/InsertUnorderedList.js +36 -0
- package/src/components/EmailEditor/components/RichText/Italic.js +36 -0
- package/src/components/EmailEditor/components/RichText/Link.js +99 -0
- package/src/components/EmailEditor/components/RichText/RichTextLayout.js +53 -0
- package/src/components/EmailEditor/components/RichText/Strikethrough.js +36 -0
- package/src/components/EmailEditor/components/RichText/TextAlign.js +58 -0
- package/src/components/EmailEditor/components/RichText/Underline.js +36 -0
- package/src/components/EmailEditor/components/RichText/index.js +210 -0
- package/src/components/EmailEditor/components/RightSetting/index.js +126 -0
- package/src/components/EmailEditor/components/StyleSettings/ButtonStyleSettings.js +141 -0
- package/src/components/EmailEditor/components/StyleSettings/ColumnStyleSettings.js +241 -0
- package/src/components/EmailEditor/components/StyleSettings/DividerStyleSettings.js +111 -0
- package/src/components/EmailEditor/components/StyleSettings/HeadingStyleSettings.js +162 -0
- package/src/components/EmailEditor/components/StyleSettings/ImageStyleSettings.js +217 -0
- package/src/components/EmailEditor/components/StyleSettings/MenuStyleSettings.js +177 -0
- package/src/components/EmailEditor/components/StyleSettings/PaddingSettings.js +30 -0
- package/src/components/EmailEditor/components/StyleSettings/SocialLinkSettings.js +250 -0
- package/src/components/EmailEditor/components/StyleSettings/SpacerStyleSettings.js +38 -0
- package/src/components/EmailEditor/components/StyleSettings/TextStyleSettings.js +108 -0
- package/src/components/EmailEditor/components/StyleSettings/index.js +32 -0
- package/src/components/EmailEditor/configs/getBlockConfigsList.js +263 -0
- package/src/components/EmailEditor/configs/getColumnConfigFunc.js +59 -0
- package/src/components/EmailEditor/configs/getColumnsSettings.js +246 -0
- package/src/components/EmailEditor/configs/useDataSource.js +19 -0
- package/src/components/EmailEditor/index.js +93 -0
- package/src/components/EmailEditor/reducers/index.js +173 -0
- package/src/components/EmailEditor/translation/en.js +166 -0
- package/src/components/EmailEditor/translation/index.js +39 -0
- package/src/components/EmailEditor/translation/zh.js +166 -0
- package/src/components/EmailEditor/utils/classNames.js +5 -0
- package/src/components/EmailEditor/utils/dataToHTML.js +323 -0
- package/src/components/EmailEditor/utils/exportValidation.js +296 -0
- package/src/components/EmailEditor/utils/helpers.js +48 -0
- package/src/components/EmailEditor/utils/pexels.js +20 -0
- package/src/components/EmailEditor/utils/useSection.js +24 -0
- package/src/components/EmailEditor/utils/useStyleLayout.js +82 -0
- package/src/index.css +99 -0
- package/src/index.js +15 -0
- package/src/logo.svg +1 -0
- package/src/pages/AppPage/index.js +10 -0
- package/src/pages/Dashboard/Header.js +192 -0
- package/src/pages/Dashboard/defaultBlockList.json +1758 -0
- package/src/pages/Dashboard/index.js +48 -0
- package/src/reportWebVitals.js +13 -0
- package/src/setupTests.js +5 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export validation: pure function over { blockList, bodySettings }.
|
|
3
|
+
* Returns issues (errors, warnings, info) to show before export.
|
|
4
|
+
* No async; no React/refs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DEFAULT_BODY_SETTINGS = {
|
|
8
|
+
preHeader: "",
|
|
9
|
+
contentWidth: 600,
|
|
10
|
+
styles: {
|
|
11
|
+
backgroundColor: "#f5f5f5",
|
|
12
|
+
color: "#152b2a",
|
|
13
|
+
fontFamily: "Arial",
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function isValidUrl(value, allowEmpty = false) {
|
|
18
|
+
if (value == null || String(value).trim() === "") return allowEmpty;
|
|
19
|
+
const s = String(value).trim();
|
|
20
|
+
try {
|
|
21
|
+
if (/^https?:\/\//i.test(s)) {
|
|
22
|
+
new URL(s);
|
|
23
|
+
} else {
|
|
24
|
+
new URL("https://" + s);
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeBodySettings(bodySettings) {
|
|
33
|
+
if (bodySettings == null || typeof bodySettings !== "object") {
|
|
34
|
+
return { ...DEFAULT_BODY_SETTINGS, styles: { ...DEFAULT_BODY_SETTINGS.styles } };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
preHeader: bodySettings.preHeader != null ? bodySettings.preHeader : DEFAULT_BODY_SETTINGS.preHeader,
|
|
38
|
+
contentWidth: Number(bodySettings.contentWidth) || DEFAULT_BODY_SETTINGS.contentWidth,
|
|
39
|
+
styles: {
|
|
40
|
+
backgroundColor:
|
|
41
|
+
bodySettings.styles?.backgroundColor != null
|
|
42
|
+
? bodySettings.styles.backgroundColor
|
|
43
|
+
: DEFAULT_BODY_SETTINGS.styles.backgroundColor,
|
|
44
|
+
color:
|
|
45
|
+
bodySettings.styles?.color != null ? bodySettings.styles.color : DEFAULT_BODY_SETTINGS.styles.color,
|
|
46
|
+
fontFamily:
|
|
47
|
+
bodySettings.styles?.fontFamily != null
|
|
48
|
+
? bodySettings.styles.fontFamily
|
|
49
|
+
: DEFAULT_BODY_SETTINGS.styles.fontFamily,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function locationString(path, blockKey, label) {
|
|
55
|
+
const parts = [];
|
|
56
|
+
if (path.length >= 1) parts.push(`column ${path[0] + 1}`);
|
|
57
|
+
if (path.length >= 2) parts.push(`content ${path[1] + 1}`);
|
|
58
|
+
if (path.length >= 3) parts.push(`block ${path[2] + 1}`);
|
|
59
|
+
const where = parts.length ? ` (${parts.join(", ")})` : "";
|
|
60
|
+
const blockLabel = blockKey.replace(/_/g, " ");
|
|
61
|
+
return label ? `${blockLabel}: ${label}${where}` : `${blockLabel}${where}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validateBodySettings(bodySettings, issues) {
|
|
65
|
+
const resolved = normalizeBodySettings(bodySettings);
|
|
66
|
+
|
|
67
|
+
if (typeof resolved.contentWidth !== "number" || resolved.contentWidth <= 0) {
|
|
68
|
+
issues.push({
|
|
69
|
+
severity: "error",
|
|
70
|
+
code: "body_content_width_invalid",
|
|
71
|
+
message: "Body content width must be a positive number.",
|
|
72
|
+
location: "Theme settings",
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const s = resolved.styles;
|
|
77
|
+
if (s.backgroundColor == null || String(s.backgroundColor).trim() === "") {
|
|
78
|
+
issues.push({
|
|
79
|
+
severity: "error",
|
|
80
|
+
code: "body_background_color_missing",
|
|
81
|
+
message: "Theme background color is missing.",
|
|
82
|
+
location: "Theme settings",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (s.color == null || String(s.color).trim() === "") {
|
|
86
|
+
issues.push({
|
|
87
|
+
severity: "error",
|
|
88
|
+
code: "body_text_color_missing",
|
|
89
|
+
message: "Theme text color is missing.",
|
|
90
|
+
location: "Theme settings",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (s.fontFamily == null || String(s.fontFamily).trim() === "") {
|
|
94
|
+
issues.push({
|
|
95
|
+
severity: "error",
|
|
96
|
+
code: "body_font_family_missing",
|
|
97
|
+
message: "Theme font family is missing.",
|
|
98
|
+
location: "Theme settings",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateImageBlock(item, path, issues) {
|
|
104
|
+
const src = item.src;
|
|
105
|
+
const loc = () => locationString(path, "image");
|
|
106
|
+
|
|
107
|
+
if (src == null || String(src).trim() === "") {
|
|
108
|
+
issues.push({
|
|
109
|
+
severity: "error",
|
|
110
|
+
code: "image_empty_src",
|
|
111
|
+
message: "Image URL is empty; the image will not display.",
|
|
112
|
+
location: loc(),
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!isValidUrl(src, false)) {
|
|
118
|
+
issues.push({
|
|
119
|
+
severity: "warning",
|
|
120
|
+
code: "image_invalid_src",
|
|
121
|
+
message: "Image URL format is invalid.",
|
|
122
|
+
location: loc(),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (item.alt == null || String(item.alt).trim() === "") {
|
|
127
|
+
issues.push({
|
|
128
|
+
severity: "info",
|
|
129
|
+
code: "image_empty_alt",
|
|
130
|
+
message: "Image alt text is empty; consider adding for accessibility.",
|
|
131
|
+
location: loc(),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateButtonBlock(item, path, issues) {
|
|
137
|
+
const linkURL = item.linkURL;
|
|
138
|
+
const loc = () => locationString(path, "button");
|
|
139
|
+
|
|
140
|
+
if (linkURL == null || String(linkURL).trim() === "") {
|
|
141
|
+
issues.push({
|
|
142
|
+
severity: "warning",
|
|
143
|
+
code: "button_empty_link",
|
|
144
|
+
message: "Button link URL is empty; will export as href=\"https://\".",
|
|
145
|
+
location: loc(),
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!isValidUrl(linkURL, false)) {
|
|
151
|
+
issues.push({
|
|
152
|
+
severity: "warning",
|
|
153
|
+
code: "button_invalid_link",
|
|
154
|
+
message: "Button link URL format is invalid.",
|
|
155
|
+
location: loc(),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function validateMenuBlock(item, path, issues) {
|
|
161
|
+
const list = item.list || [];
|
|
162
|
+
list.forEach((entry, idx) => {
|
|
163
|
+
const loc = (lbl) => locationString(path, "menu", lbl || `item ${idx + 1}`);
|
|
164
|
+
|
|
165
|
+
if (entry.label == null || String(entry.label).trim() === "") {
|
|
166
|
+
issues.push({
|
|
167
|
+
severity: "info",
|
|
168
|
+
code: "menu_empty_label",
|
|
169
|
+
message: "Menu item label is empty; will show as \"Link\".",
|
|
170
|
+
location: loc(`item ${idx + 1}`),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const url = entry.url;
|
|
175
|
+
if (url == null || String(url).trim() === "") {
|
|
176
|
+
issues.push({
|
|
177
|
+
severity: "warning",
|
|
178
|
+
code: "menu_empty_url",
|
|
179
|
+
message: "Menu item URL is empty; will export as href=\"#\".",
|
|
180
|
+
location: loc(entry.label || `item ${idx + 1}`),
|
|
181
|
+
});
|
|
182
|
+
} else if (!isValidUrl(url, false)) {
|
|
183
|
+
issues.push({
|
|
184
|
+
severity: "warning",
|
|
185
|
+
code: "menu_invalid_url",
|
|
186
|
+
message: "Menu item URL format is invalid.",
|
|
187
|
+
location: loc(entry.label || `item ${idx + 1}`),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (list.length === 0) {
|
|
193
|
+
issues.push({
|
|
194
|
+
severity: "info",
|
|
195
|
+
code: "menu_no_items",
|
|
196
|
+
message: "Menu block has no items.",
|
|
197
|
+
location: locationString(path, "menu"),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function validateSocialLinkBlock(item, path, issues) {
|
|
203
|
+
const list = item.list || [];
|
|
204
|
+
list.forEach((entry, idx) => {
|
|
205
|
+
const title = entry.title || entry.image || `link ${idx + 1}`;
|
|
206
|
+
const loc = (lbl) => locationString(path, "social_link", lbl || title);
|
|
207
|
+
|
|
208
|
+
const linkURL = entry.linkURL;
|
|
209
|
+
if (linkURL == null || String(linkURL).trim() === "") {
|
|
210
|
+
issues.push({
|
|
211
|
+
severity: "warning",
|
|
212
|
+
code: "social_empty_link",
|
|
213
|
+
message: "Social link URL is empty; will export as href=\"https://\".",
|
|
214
|
+
location: loc(title),
|
|
215
|
+
});
|
|
216
|
+
} else if (!isValidUrl(linkURL, false)) {
|
|
217
|
+
issues.push({
|
|
218
|
+
severity: "warning",
|
|
219
|
+
code: "social_invalid_link",
|
|
220
|
+
message: "Social link URL format is invalid.",
|
|
221
|
+
location: loc(title),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const image = entry.image;
|
|
226
|
+
if (image == null || String(image).trim() === "") {
|
|
227
|
+
issues.push({
|
|
228
|
+
severity: "warning",
|
|
229
|
+
code: "social_empty_icon",
|
|
230
|
+
message: "Social link icon URL is empty; icon will not display.",
|
|
231
|
+
location: loc(title),
|
|
232
|
+
});
|
|
233
|
+
} else if (!isValidUrl(image, false)) {
|
|
234
|
+
issues.push({
|
|
235
|
+
severity: "warning",
|
|
236
|
+
code: "social_invalid_icon",
|
|
237
|
+
message: "Social link icon URL format is invalid.",
|
|
238
|
+
location: loc(title),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (list.length === 0) {
|
|
244
|
+
issues.push({
|
|
245
|
+
severity: "info",
|
|
246
|
+
code: "social_no_links",
|
|
247
|
+
message: "Social link block has no links.",
|
|
248
|
+
location: locationString(path, "social_link"),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function walkBlocks(blockList, path, issues, bodySettings) {
|
|
254
|
+
if (!Array.isArray(blockList)) return;
|
|
255
|
+
blockList.forEach((item, index) => {
|
|
256
|
+
const p = [...path, index];
|
|
257
|
+
if (!item || typeof item !== "object") return;
|
|
258
|
+
|
|
259
|
+
const key = item.key;
|
|
260
|
+
if (key === "column" || key === "content") {
|
|
261
|
+
if (Array.isArray(item.children)) walkBlocks(item.children, p, issues, bodySettings);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
switch (key) {
|
|
266
|
+
case "image":
|
|
267
|
+
validateImageBlock(item, p, issues);
|
|
268
|
+
break;
|
|
269
|
+
case "button":
|
|
270
|
+
validateButtonBlock(item, p, issues);
|
|
271
|
+
break;
|
|
272
|
+
case "menu":
|
|
273
|
+
validateMenuBlock(item, p, issues);
|
|
274
|
+
break;
|
|
275
|
+
case "social_link":
|
|
276
|
+
validateSocialLinkBlock(item, p, issues);
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validates the export payload. Pure function; no side effects.
|
|
286
|
+
* @param {{ blockList: array, bodySettings: object|null }} payload - Same shape as export (Header.js).
|
|
287
|
+
* @returns {{ severity: string, code: string, message: string, location: string }[]}
|
|
288
|
+
*/
|
|
289
|
+
export function validateExportPayload({ blockList, bodySettings }) {
|
|
290
|
+
const issues = [];
|
|
291
|
+
|
|
292
|
+
validateBodySettings(bodySettings, issues);
|
|
293
|
+
walkBlocks(Array.isArray(blockList) ? blockList : [], [], issues, bodySettings);
|
|
294
|
+
|
|
295
|
+
return issues;
|
|
296
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const deepClone = (source) => {
|
|
2
|
+
const targetObj = source.constructor === Array ? [] : {}; // Determine if copy target is array or object
|
|
3
|
+
for (let keys in source) {
|
|
4
|
+
// Iterate over source
|
|
5
|
+
if (source.hasOwnProperty(keys)) {
|
|
6
|
+
if (source[keys] && typeof source[keys] === "object") {
|
|
7
|
+
// If value is object, recurse
|
|
8
|
+
targetObj[keys] = source[keys].constructor === Array ? [] : {};
|
|
9
|
+
targetObj[keys] = deepClone(source[keys]);
|
|
10
|
+
} else {
|
|
11
|
+
// Otherwise assign directly
|
|
12
|
+
targetObj[keys] = source[keys];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return targetObj;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Throttle
|
|
20
|
+
export const throttle = (fn, delay) => {
|
|
21
|
+
let timer = null;
|
|
22
|
+
return function () {
|
|
23
|
+
if (timer) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
fn.apply(this, arguments);
|
|
28
|
+
timer = null;
|
|
29
|
+
}, delay);
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Debounce
|
|
34
|
+
export const debounce = (fn, delay) => {
|
|
35
|
+
let timer = null;
|
|
36
|
+
|
|
37
|
+
return function () {
|
|
38
|
+
if (timer) {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
timer = setTimeout(() => {
|
|
43
|
+
fn.apply(this, arguments);
|
|
44
|
+
|
|
45
|
+
timer = null;
|
|
46
|
+
}, delay);
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// const pexelsKey = "563492ad6f91700001000001efd3eadbee21486a9756a5576e4e3b73";
|
|
2
|
+
|
|
3
|
+
import { createClient } from "pexels";
|
|
4
|
+
const client = createClient("563492ad6f91700001000001efd3eadbee21486a9756a5576e4e3b73");
|
|
5
|
+
|
|
6
|
+
const fetchPhotos = (pagination, pages, query) => {
|
|
7
|
+
if (!query) {
|
|
8
|
+
query = "fun";
|
|
9
|
+
}
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
client.photos
|
|
12
|
+
.search({ query, per_page: pages, page: pagination })
|
|
13
|
+
.then((photos) => {
|
|
14
|
+
resolve(photos);
|
|
15
|
+
})
|
|
16
|
+
.catch((error) => reject(error));
|
|
17
|
+
});
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default fetchPhotos;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
|
|
3
|
+
const useSection = () => {
|
|
4
|
+
const getSelectionNode = (node, tagName) => {
|
|
5
|
+
if (!node) return null;
|
|
6
|
+
if (node.classList?.contains("text-content_editable")) return null;
|
|
7
|
+
|
|
8
|
+
if (node && node.tagName?.toLocaleLowerCase() === tagName) {
|
|
9
|
+
return node;
|
|
10
|
+
}
|
|
11
|
+
return getSelectionNode(node.parentNode, tagName);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const getSectionStyle = useCallback((node, styleName) => {
|
|
15
|
+
if (!node) return null;
|
|
16
|
+
if (node.classList?.contains("text-content_editable")) return null;
|
|
17
|
+
if (node && node.style?.[styleName]) return node.style[styleName];
|
|
18
|
+
return getSectionStyle(node.parentNode, styleName);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return { getSelectionNode, getSectionStyle };
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default useSection;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { GlobalContext } from "../reducers";
|
|
3
|
+
import { deepClone } from "./helpers";
|
|
4
|
+
|
|
5
|
+
const useLayout = () => {
|
|
6
|
+
const { previewMode, currentItem, blockList, setBlockList, setCurrentItem } = useContext(GlobalContext);
|
|
7
|
+
|
|
8
|
+
const findStyleItem = (styles, key) => {
|
|
9
|
+
let styleItem = styles[previewMode][key];
|
|
10
|
+
if (!styleItem) {
|
|
11
|
+
styleItem = styles["desktop"][key];
|
|
12
|
+
}
|
|
13
|
+
return styleItem;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const cardItemElement = (title, dom) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className="card-item">
|
|
19
|
+
<div className="card-item-title">{title}</div>
|
|
20
|
+
<div>{dom}</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const colorChange =
|
|
26
|
+
(key) =>
|
|
27
|
+
({ hex }) => {
|
|
28
|
+
const newCurrentItem = deepClone(currentItem);
|
|
29
|
+
newCurrentItem.data.styles[previewMode][key] = hex;
|
|
30
|
+
updateItemStyles(newCurrentItem.data);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const paddingChange = (padding) => {
|
|
34
|
+
const newData = deepClone(currentItem.data);
|
|
35
|
+
newData.styles[previewMode] = {
|
|
36
|
+
...newData.styles[previewMode],
|
|
37
|
+
...padding,
|
|
38
|
+
};
|
|
39
|
+
updateItemStyles(newData);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const inputChange = (key) => (value) => {
|
|
43
|
+
const newData = deepClone(currentItem.data);
|
|
44
|
+
newData.styles[previewMode][key] = value;
|
|
45
|
+
updateItemStyles(newData);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const otherStylesChange = (key, value) => {
|
|
49
|
+
const newData = deepClone(currentItem.data);
|
|
50
|
+
newData.styles[previewMode][key] = value;
|
|
51
|
+
updateItemStyles(newData);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const updateItemStyles = (newData) => {
|
|
55
|
+
const newCurrentItem = deepClone(currentItem);
|
|
56
|
+
const newBlockList = deepClone(blockList);
|
|
57
|
+
newCurrentItem.data = {
|
|
58
|
+
...newData,
|
|
59
|
+
};
|
|
60
|
+
if (newData.key === "column") {
|
|
61
|
+
newBlockList[currentItem.index] = newData;
|
|
62
|
+
} else {
|
|
63
|
+
const indexArr = currentItem.index.split("-");
|
|
64
|
+
newBlockList[indexArr[0]].children[indexArr[1]].children[indexArr[2]] = newData;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
setBlockList(newBlockList, `edit_${new Date().getTime()}`);
|
|
68
|
+
setCurrentItem(newCurrentItem);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
findStyleItem,
|
|
73
|
+
cardItemElement,
|
|
74
|
+
updateItemStyles,
|
|
75
|
+
colorChange,
|
|
76
|
+
paddingChange,
|
|
77
|
+
otherStylesChange,
|
|
78
|
+
inputChange,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default useLayout;
|
package/src/index.css
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
@import '~antd/dist/antd.compact.css';
|
|
2
|
+
|
|
3
|
+
/* Dashboard uses Nabiq dark theme (aligned with globals.scss .dark) */
|
|
4
|
+
:root {
|
|
5
|
+
color-scheme: dark;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
#root {
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
background-color: #0a0a0a;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
17
|
+
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
18
|
+
sans-serif;
|
|
19
|
+
-webkit-font-smoothing: antialiased;
|
|
20
|
+
-moz-osx-font-smoothing: grayscale;
|
|
21
|
+
background-color: #0a0a0a;
|
|
22
|
+
color: #fafafa;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
code {
|
|
26
|
+
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
|
27
|
+
monospace;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.App {
|
|
31
|
+
height: calc(100vh - 60px);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.dashboard {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
height: 100%;
|
|
38
|
+
background-color: #0a0a0a;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.dashboard-content {
|
|
42
|
+
flex: 1;
|
|
43
|
+
overflow: auto;
|
|
44
|
+
background-color: #0a0a0a;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.dashboard-header {
|
|
48
|
+
padding: 8px 12px;
|
|
49
|
+
background-color: #171717;
|
|
50
|
+
border-bottom: 1px solid #282828;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.2);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.dashboard-header-title {
|
|
58
|
+
color: #fafafa;
|
|
59
|
+
font-size: 24px;
|
|
60
|
+
font-weight: 700;
|
|
61
|
+
font-family: 'Inter', sans-serif;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.dashboard-header-subtitle {
|
|
65
|
+
padding: 8px 16px;
|
|
66
|
+
color: #171717;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
border-radius: 0.5rem;
|
|
70
|
+
background-color: #d8fe91;
|
|
71
|
+
transition: all 0.3s;
|
|
72
|
+
border: none;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.15);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.dashboard-header-subtitle:hover {
|
|
78
|
+
background-color: #b8e67a;
|
|
79
|
+
box-shadow: 0px 1px 3px 0px rgba(0, 0, 0, 0.2);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.dashboard-header-feature {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: 24px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.dashboard-header-language {
|
|
89
|
+
color: #fafafa;
|
|
90
|
+
font-size: 14px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.dashboard-header-language span {
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.dashboard-header-language span:hover {
|
|
98
|
+
text-decoration: underline;
|
|
99
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import reportWebVitals from "./reportWebVitals";
|
|
4
|
+
import "./index.css";
|
|
5
|
+
import App from "./App";
|
|
6
|
+
|
|
7
|
+
const container = document.getElementById("root");
|
|
8
|
+
const root = createRoot(container);
|
|
9
|
+
|
|
10
|
+
root.render(<App />);
|
|
11
|
+
|
|
12
|
+
// If you want to start measuring performance in your app, pass a function
|
|
13
|
+
// to log results (for example: reportWebVitals(console.log))
|
|
14
|
+
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
|
15
|
+
reportWebVitals();
|
package/src/logo.svg
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Route, Switch, useLocation } from "react-router-dom";
|
|
2
|
+
import Dashboard from "../Dashboard";
|
|
3
|
+
export default function AppPage() {
|
|
4
|
+
const location = useLocation();
|
|
5
|
+
return (
|
|
6
|
+
<Switch location={location} key={location.pathname}>
|
|
7
|
+
<Route exact path="/" component={Dashboard} />
|
|
8
|
+
</Switch>
|
|
9
|
+
);
|
|
10
|
+
}
|