@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.
Files changed (91) hide show
  1. package/README.md +63 -0
  2. package/build/asset-manifest.json +12 -0
  3. package/build/favicon.ico +0 -0
  4. package/build/index.html +1 -0
  5. package/build/logo192.png +0 -0
  6. package/build/logo512.png +0 -0
  7. package/build/manifest.json +25 -0
  8. package/build/robots.txt +3 -0
  9. package/build/static/css/main.588cb535.css +9 -0
  10. package/build/static/js/206.a4343501.chunk.js +1 -0
  11. package/build/static/js/main.053d366a.js +2 -0
  12. package/build/static/js/main.053d366a.js.LICENSE.txt +56 -0
  13. package/package.json +64 -0
  14. package/public/favicon.ico +0 -0
  15. package/public/index.html +50 -0
  16. package/public/logo192.png +0 -0
  17. package/public/logo512.png +0 -0
  18. package/public/manifest.json +25 -0
  19. package/public/robots.txt +3 -0
  20. package/src/App.js +15 -0
  21. package/src/components/EmailEditor/assets/App.css +339 -0
  22. package/src/components/EmailEditor/assets/Columns.css +309 -0
  23. package/src/components/EmailEditor/assets/Header.css +174 -0
  24. package/src/components/EmailEditor/assets/ImageBlock.css +12 -0
  25. package/src/components/EmailEditor/assets/Preview.css +30 -0
  26. package/src/components/EmailEditor/assets/RichText.css +199 -0
  27. package/src/components/EmailEditor/assets/RightSettings.css +520 -0
  28. package/src/components/EmailEditor/assets/Sidebar.css +195 -0
  29. package/src/components/EmailEditor/components/BlockItems/ButtonBlock.js +25 -0
  30. package/src/components/EmailEditor/components/BlockItems/DividerBlock.js +19 -0
  31. package/src/components/EmailEditor/components/BlockItems/HeadingBlock.js +16 -0
  32. package/src/components/EmailEditor/components/BlockItems/ImageBlock.js +28 -0
  33. package/src/components/EmailEditor/components/BlockItems/MenuBlock.js +52 -0
  34. package/src/components/EmailEditor/components/BlockItems/SocialLinkBlocks.js +26 -0
  35. package/src/components/EmailEditor/components/BlockItems/SpacerBlock.js +23 -0
  36. package/src/components/EmailEditor/components/BlockItems/TextBlock.js +16 -0
  37. package/src/components/EmailEditor/components/BlockItems/index.js +25 -0
  38. package/src/components/EmailEditor/components/ColorPicker/index.js +26 -0
  39. package/src/components/EmailEditor/components/Column/index.js +253 -0
  40. package/src/components/EmailEditor/components/Header/index.js +243 -0
  41. package/src/components/EmailEditor/components/LeftSideBar/index.js +253 -0
  42. package/src/components/EmailEditor/components/Main/index.js +281 -0
  43. package/src/components/EmailEditor/components/Preview/index.js +97 -0
  44. package/src/components/EmailEditor/components/RichText/Bold.js +37 -0
  45. package/src/components/EmailEditor/components/RichText/FontColor.js +39 -0
  46. package/src/components/EmailEditor/components/RichText/InsertOrderedList.js +36 -0
  47. package/src/components/EmailEditor/components/RichText/InsertUnorderedList.js +36 -0
  48. package/src/components/EmailEditor/components/RichText/Italic.js +36 -0
  49. package/src/components/EmailEditor/components/RichText/Link.js +99 -0
  50. package/src/components/EmailEditor/components/RichText/RichTextLayout.js +53 -0
  51. package/src/components/EmailEditor/components/RichText/Strikethrough.js +36 -0
  52. package/src/components/EmailEditor/components/RichText/TextAlign.js +58 -0
  53. package/src/components/EmailEditor/components/RichText/Underline.js +36 -0
  54. package/src/components/EmailEditor/components/RichText/index.js +210 -0
  55. package/src/components/EmailEditor/components/RightSetting/index.js +126 -0
  56. package/src/components/EmailEditor/components/StyleSettings/ButtonStyleSettings.js +141 -0
  57. package/src/components/EmailEditor/components/StyleSettings/ColumnStyleSettings.js +241 -0
  58. package/src/components/EmailEditor/components/StyleSettings/DividerStyleSettings.js +111 -0
  59. package/src/components/EmailEditor/components/StyleSettings/HeadingStyleSettings.js +162 -0
  60. package/src/components/EmailEditor/components/StyleSettings/ImageStyleSettings.js +217 -0
  61. package/src/components/EmailEditor/components/StyleSettings/MenuStyleSettings.js +177 -0
  62. package/src/components/EmailEditor/components/StyleSettings/PaddingSettings.js +30 -0
  63. package/src/components/EmailEditor/components/StyleSettings/SocialLinkSettings.js +250 -0
  64. package/src/components/EmailEditor/components/StyleSettings/SpacerStyleSettings.js +38 -0
  65. package/src/components/EmailEditor/components/StyleSettings/TextStyleSettings.js +108 -0
  66. package/src/components/EmailEditor/components/StyleSettings/index.js +32 -0
  67. package/src/components/EmailEditor/configs/getBlockConfigsList.js +263 -0
  68. package/src/components/EmailEditor/configs/getColumnConfigFunc.js +59 -0
  69. package/src/components/EmailEditor/configs/getColumnsSettings.js +246 -0
  70. package/src/components/EmailEditor/configs/useDataSource.js +19 -0
  71. package/src/components/EmailEditor/index.js +93 -0
  72. package/src/components/EmailEditor/reducers/index.js +173 -0
  73. package/src/components/EmailEditor/translation/en.js +166 -0
  74. package/src/components/EmailEditor/translation/index.js +39 -0
  75. package/src/components/EmailEditor/translation/zh.js +166 -0
  76. package/src/components/EmailEditor/utils/classNames.js +5 -0
  77. package/src/components/EmailEditor/utils/dataToHTML.js +323 -0
  78. package/src/components/EmailEditor/utils/exportValidation.js +296 -0
  79. package/src/components/EmailEditor/utils/helpers.js +48 -0
  80. package/src/components/EmailEditor/utils/pexels.js +20 -0
  81. package/src/components/EmailEditor/utils/useSection.js +24 -0
  82. package/src/components/EmailEditor/utils/useStyleLayout.js +82 -0
  83. package/src/index.css +99 -0
  84. package/src/index.js +15 -0
  85. package/src/logo.svg +1 -0
  86. package/src/pages/AppPage/index.js +10 -0
  87. package/src/pages/Dashboard/Header.js +192 -0
  88. package/src/pages/Dashboard/defaultBlockList.json +1758 -0
  89. package/src/pages/Dashboard/index.js +48 -0
  90. package/src/reportWebVitals.js +13 -0
  91. package/src/setupTests.js +5 -0
@@ -0,0 +1,217 @@
1
+ import { useContext, useRef } from "react";
2
+ import { GlobalContext } from "../../reducers";
3
+ import classNames from "../../utils/classNames";
4
+ import { Switch, Slider, Input } from "antd";
5
+ import { deepClone } from "../../utils/helpers";
6
+ import PaddingSettings from "./PaddingSettings";
7
+ import useLayout from "../../utils/useStyleLayout";
8
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
9
+ import { faAlignCenter, faAlignLeft, faAlignRight, faAlignJustify, faCloudUploadAlt } from "@fortawesome/free-solid-svg-icons";
10
+ import useTranslation from "../../translation";
11
+
12
+ const ImageStyleSettings = () => {
13
+ const { currentItem, previewMode } = useContext(GlobalContext);
14
+ const { t } = useTranslation();
15
+ const { findStyleItem, cardItemElement, inputChange, updateItemStyles } = useLayout();
16
+ const fileInputRef = useRef(null);
17
+
18
+ const actionSettings = () => {
19
+ const { linkURL } = currentItem.data;
20
+
21
+ const linkChange = (event) => {
22
+ const newValue = event.target.value;
23
+ const newCurrentItem = deepClone(currentItem);
24
+ currentItem.data.linkURL = newValue;
25
+
26
+ updateItemStyles(newCurrentItem.data);
27
+ };
28
+
29
+ return (
30
+ <>
31
+ <div className="right-setting-block-item-title">{t("image_action")}</div>
32
+ {cardItemElement(t("action_type"), <div className="link-tag">{t("link")}</div>)}
33
+ <div className="card-item-title">{t("link_url")}</div>
34
+ <div className="margin-top-6">
35
+ <Input addonBefore="https://" value={linkURL} onChange={linkChange} />
36
+ </div>
37
+ </>
38
+ );
39
+ };
40
+
41
+ const imageSettings = () => {
42
+ const { src, alt } = currentItem.data;
43
+
44
+ const linkChange = (key) => (event) => {
45
+ const newCurrentItem = deepClone(currentItem);
46
+ newCurrentItem.data[key] = event.target.value;
47
+ updateItemStyles(newCurrentItem.data);
48
+ };
49
+
50
+ // Reserved for future upload: call with uploaded URL when implementing S3 upload
51
+ // eslint-disable-next-line no-unused-vars
52
+ const setImageSrc = (url) => {
53
+ const newCurrentItem = deepClone(currentItem);
54
+ newCurrentItem.data.src = url;
55
+ updateItemStyles(newCurrentItem.data);
56
+ };
57
+
58
+ const handleUploadClick = () => {
59
+ fileInputRef.current?.click();
60
+ };
61
+
62
+ const handleFileChange = (e) => {
63
+ const file = e.target.files?.[0];
64
+ if (!file) return;
65
+ // Future: upload file to S3, then call setImageSrc(returnedUrl)
66
+ // setImageSrc(await uploadToS3(file));
67
+ e.target.value = "";
68
+ };
69
+
70
+ const handleDrop = (e) => {
71
+ e.preventDefault();
72
+ e.stopPropagation();
73
+ const file = e.dataTransfer?.files?.[0];
74
+ if (file?.type?.startsWith("image/")) {
75
+ // Future: upload file to S3, then call setImageSrc(returnedUrl)
76
+ // setImageSrc(await uploadToS3(file));
77
+ }
78
+ fileInputRef.current.value = "";
79
+ };
80
+
81
+ const handleDragOver = (e) => {
82
+ e.preventDefault();
83
+ e.stopPropagation();
84
+ };
85
+
86
+ return (
87
+ <>
88
+ <div className="right-setting-block-item-title">{t("image_settings")}</div>
89
+ {/* Upload drop box – future: upload to S3 and set returned URL into image link */}
90
+ <div className="card-item">
91
+ <div className="width-full">
92
+ <div className="card-item-title">{t("image_upload")}</div>
93
+ <input
94
+ ref={fileInputRef}
95
+ type="file"
96
+ accept="image/*"
97
+ className="ee-image-upload-input-hidden"
98
+ onChange={handleFileChange}
99
+ />
100
+ <div
101
+ role="button"
102
+ tabIndex={0}
103
+ className="ee-image-upload-trigger"
104
+ onClick={handleUploadClick}
105
+ onDrop={handleDrop}
106
+ onDragOver={handleDragOver}
107
+ >
108
+ <FontAwesomeIcon icon={faCloudUploadAlt} className="ee-image-upload-icon" />
109
+ <span>{t("image_upload_click")}</span>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ {/* Image link – paste URL here or filled by upload (S3 URL) when implemented */}
114
+ <div className="card-item">
115
+ <div className="width-full">
116
+ <div className="card-item-title">{t("image_url")}</div>
117
+ <div className="margin-top-6">
118
+ <Input
119
+ value={src || ""}
120
+ onChange={linkChange("src")}
121
+ placeholder="https://..."
122
+ />
123
+ </div>
124
+ </div>
125
+ </div>
126
+ <div className="card-item">
127
+ <div className="width-full">
128
+ <div className="card-item-title">{t("image_alt")}</div>
129
+ <div className="margin-top-6">
130
+ <Input value={alt || ""} onChange={linkChange("alt")} />
131
+ </div>
132
+ </div>
133
+ </div>
134
+ </>
135
+ );
136
+ };
137
+
138
+ const updateContentStylesPadding = (padding) => {
139
+ const newData = deepClone(currentItem.data);
140
+ newData.contentStyles[previewMode] = {
141
+ ...newData.contentStyles[previewMode],
142
+ ...padding,
143
+ };
144
+ updateItemStyles(newData);
145
+ };
146
+
147
+ const updateContentTextAlign = (textAlign) => {
148
+ const newData = deepClone(currentItem.data);
149
+ newData.contentStyles[previewMode] = {
150
+ ...newData.contentStyles[previewMode],
151
+ textAlign,
152
+ };
153
+ updateItemStyles(newData);
154
+ };
155
+
156
+ const imageStyleSettings = () => {
157
+ const width = findStyleItem(currentItem.data.styles, "width");
158
+ const textAlign = findStyleItem(currentItem.data.contentStyles, "textAlign");
159
+ return (
160
+ <>
161
+ <div className="right-setting-block-item-title">{t("image_styles")}</div>
162
+ {cardItemElement(
163
+ t("width_auto"),
164
+ <Switch
165
+ checked={width === "auto"}
166
+ className={classNames(width === "auto" ? "bg-sky-500" : "bg-gray-400")}
167
+ onChange={() => {
168
+ const value = width === "auto" ? "100%" : "auto";
169
+ inputChange("width")(value);
170
+ }}
171
+ />
172
+ )}
173
+ {width !== "auto" && <Slider value={Number(width.replace("%", ""))} onChange={(value) => inputChange("width")(value + "%")} />}
174
+ {cardItemElement(
175
+ t("align"),
176
+ <div className="flex justify-center items-center">
177
+ {[
178
+ { icon: faAlignLeft, value: "left" },
179
+ { icon: faAlignCenter, value: "center" },
180
+ { icon: faAlignRight, value: "right" },
181
+ { icon: faAlignJustify, value: "justify" },
182
+ ].map(({ icon, value }) => {
183
+ return (
184
+ <div
185
+ key={value}
186
+ className={classNames(textAlign === value ? "align-style-item-active" : "align-style-item-un_active", "align-style-item")}
187
+ onClick={() => updateContentTextAlign(value)}
188
+ >
189
+ <FontAwesomeIcon icon={icon} className="tag-style-size" />
190
+ </div>
191
+ );
192
+ })}
193
+ </div>
194
+ )}
195
+ <div className="card-item-title">{t("padding_settings")}</div>
196
+ <PaddingSettings
197
+ padding={{
198
+ paddingTop: findStyleItem(currentItem.data.contentStyles, "paddingTop"),
199
+ paddingRight: findStyleItem(currentItem.data.contentStyles, "paddingRight"),
200
+ paddingLeft: findStyleItem(currentItem.data.contentStyles, "paddingLeft"),
201
+ paddingBottom: findStyleItem(currentItem.data.contentStyles, "paddingBottom"),
202
+ }}
203
+ setPadding={updateContentStylesPadding}
204
+ />
205
+ </>
206
+ );
207
+ };
208
+ return (
209
+ <div className="margin-y-30">
210
+ {actionSettings()}
211
+ {imageSettings()}
212
+ {imageStyleSettings()}
213
+ </div>
214
+ );
215
+ };
216
+
217
+ export default ImageStyleSettings;
@@ -0,0 +1,177 @@
1
+ import { useContext, useCallback, useRef, useState } from "react";
2
+ import { GlobalContext } from "../../reducers";
3
+ import classNames from "../../utils/classNames";
4
+ import { Input, InputNumber, Select } from "antd";
5
+ import { throttle, deepClone } from "../../utils/helpers";
6
+ import ColorPicker from "../ColorPicker";
7
+ import PaddingSettings from "./PaddingSettings";
8
+ import useLayout from "../../utils/useStyleLayout";
9
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
10
+ import { faTrash, faGripVertical, faAlignCenter, faAlignLeft, faAlignRight } from "@fortawesome/free-solid-svg-icons";
11
+ import useTranslation from "../../translation";
12
+
13
+ const MenuStyleSettings = () => {
14
+ const { currentItem, previewMode } = useContext(GlobalContext);
15
+ const { t } = useTranslation();
16
+ const { findStyleItem, paddingChange, updateItemStyles, cardItemElement, colorChange, inputChange } = useLayout();
17
+
18
+ const { list = [], separator = " | " } = currentItem.data;
19
+ const [sourceNode, setSourceNode] = useState(null);
20
+ const [isDragStart, setIsDragStart] = useState(false);
21
+ const [dragIndex, setDragIndex] = useState(null);
22
+ const menuListsRef = useRef(null);
23
+
24
+ const updateMenuSettings = (key, value) => {
25
+ const newData = deepClone(currentItem.data);
26
+ newData[key] = value;
27
+ updateItemStyles(newData);
28
+ };
29
+
30
+ const contentPaddingChange = (padding) => {
31
+ const newData = deepClone(currentItem.data);
32
+ newData.contentStyles[previewMode] = {
33
+ ...newData.contentStyles[previewMode],
34
+ ...padding,
35
+ };
36
+ updateItemStyles(newData);
37
+ };
38
+
39
+ const updateContentTextAlign = (textAlign) => {
40
+ const newData = deepClone(currentItem.data);
41
+ newData.contentStyles[previewMode] = {
42
+ ...newData.contentStyles[previewMode],
43
+ textAlign,
44
+ };
45
+ updateItemStyles(newData);
46
+ };
47
+
48
+ // throttle() returns a new function reference
49
+ // eslint-disable-next-line react-hooks/exhaustive-deps
50
+ const dragOver = useCallback(
51
+ throttle((event) => {
52
+ event.preventDefault();
53
+ const listsDom = menuListsRef.current;
54
+ if (!listsDom) return;
55
+ const overEl = event.target.closest("[data-index]");
56
+ if (!overEl) return;
57
+ const children = Array.from(listsDom.children);
58
+ const overIndex = children.indexOf(overEl);
59
+ if (overIndex === -1) return;
60
+ const sourceIndex = parseInt(dragIndex, 10);
61
+ if (sourceIndex === overIndex) return;
62
+ const newList = deepClone(list);
63
+ [newList[overIndex], newList[sourceIndex]] = [newList[sourceIndex], newList[overIndex]];
64
+ setDragIndex(String(overIndex));
65
+ updateMenuSettings("list", newList);
66
+ }, 50),
67
+ [list, dragIndex]
68
+ );
69
+
70
+ const fontFamilyList = ["sans-serif", "Arial", "Verdana", "Times New Roman", "Georgia", "Courier New"];
71
+ const targetOptions = [
72
+ { label: t("same_tab"), value: "_self" },
73
+ { label: t("new_tab"), value: "_blank" },
74
+ ];
75
+
76
+ return (
77
+ <div className="margin-y-30">
78
+ <div className="right-setting-block-item-title">{t("menu_items")}</div>
79
+ <div
80
+ ref={menuListsRef}
81
+ onDragOver={dragOver}
82
+ className="margin-top-12"
83
+ onDragStart={(e) => {
84
+ const row = e.target.closest("[data-index]");
85
+ const idx = row?.getAttribute("data-index");
86
+ if (idx != null) {
87
+ row.classList.add("social-link-item-current");
88
+ setSourceNode(row);
89
+ setIsDragStart(true);
90
+ setDragIndex(idx);
91
+ }
92
+ }}
93
+ onDragEnd={() => {
94
+ const node = sourceNode?.closest?.("[data-index]");
95
+ if (node) node.classList.remove("social-link-item-current");
96
+ setSourceNode(null);
97
+ setIsDragStart(false);
98
+ setDragIndex(null);
99
+ }}
100
+ >
101
+ {list.map((item, index) => (
102
+ <div
103
+ key={index}
104
+ data-index={index}
105
+ draggable
106
+ className={classNames(
107
+ "social-link-item",
108
+ isDragStart && "social-link-item-drag_start",
109
+ dragIndex === String(index) ? "social-link-item-current" : "border-transparent"
110
+ )}
111
+ >
112
+ <div className="social-link-item-content">
113
+ <div className="flex items-center justify-between margin-bottom-8">
114
+ <div className="flex items-center gap-2 cursor-grab">
115
+ <FontAwesomeIcon icon={faGripVertical} className="tag-style-size" />
116
+ <span className="font-semibold">{t("menu_item")} {index + 1}</span>
117
+ </div>
118
+ <div
119
+ className="social-link-item-icon social-link-item-icon-slate"
120
+ onClick={() => updateMenuSettings("list", list.filter((_, i) => i !== index))}
121
+ >
122
+ <FontAwesomeIcon icon={faTrash} className="social-link-item-icon-svg social-link-item-icon-svg-deep" />
123
+ </div>
124
+ </div>
125
+ {cardItemElement(t("label"), <Input value={item.label || ""} onChange={(e) => updateMenuSettings("list", list.map((it, i) => (i === index ? { ...it, label: e.target.value } : it)))} placeholder="Page" />)}
126
+ {cardItemElement(
127
+ t("url"),
128
+ <Input addonBefore="https://" value={(item.url || "").replace(/^https?:\/\//, "")} onChange={(e) => updateMenuSettings("list", list.map((it, i) => (i === index ? { ...it, url: e.target.value } : it)))} placeholder="example.com" />
129
+ )}
130
+ {cardItemElement(
131
+ t("target"),
132
+ <Select className="input-width" value={item.target || "_self"} onChange={(v) => updateMenuSettings("list", list.map((it, i) => (i === index ? { ...it, target: v } : it)))} getPopupContainer={() => document.querySelector(".right-settings") || document.body}>
133
+ {targetOptions.map((opt) => (
134
+ <Select.Option key={opt.value} value={opt.value}>{opt.label}</Select.Option>
135
+ ))}
136
+ </Select>
137
+ )}
138
+ </div>
139
+ </div>
140
+ ))}
141
+ </div>
142
+ <button type="button" className="menu-add-new-item margin-top-12" onClick={() => updateMenuSettings("list", list.concat({ label: t("page"), url: "", target: "_self" }))}>
143
+ <span className="menu-add-new-item-icon">+</span> {t("add_new_item")}
144
+ </button>
145
+
146
+ <div className="right-setting-block-item-title margin-top-18">{t("menu_styles")}</div>
147
+ {cardItemElement(t("font_family"), <Select className="input-width" value={findStyleItem(currentItem.data.styles, "fontFamily")} onChange={inputChange("fontFamily")} getPopupContainer={() => document.querySelector(".right-settings") || document.body}>{fontFamilyList.map((f) => (<Select.Option key={f} value={f}>{f}</Select.Option>))}</Select>)}
148
+ {cardItemElement(t("font_size"), <InputNumber min={8} max={72} className="input-width" addonAfter="px" value={findStyleItem(currentItem.data.styles, "fontSize")} onChange={inputChange("fontSize")} />)}
149
+ {cardItemElement(t("letter_spacing"), <InputNumber className="input-width" addonAfter="px" value={findStyleItem(currentItem.data.styles, "letterSpacing")} onChange={inputChange("letterSpacing")} />)}
150
+ {cardItemElement(t("text_color"), <ColorPicker color={findStyleItem(currentItem.data.styles, "color")} setColor={colorChange("color")} />)}
151
+ {cardItemElement(t("link_color"), <ColorPicker color={findStyleItem(currentItem.data.styles, "linkColor")} setColor={colorChange("linkColor")} />)}
152
+ {cardItemElement(
153
+ t("align"),
154
+ <div className="flex justify-center items-center">
155
+ {[
156
+ { icon: faAlignLeft, value: "left" },
157
+ { icon: faAlignCenter, value: "center" },
158
+ { icon: faAlignRight, value: "right" },
159
+ ].map(({ icon, value }) => (
160
+ <div key={value} className={classNames(findStyleItem(currentItem.data.contentStyles, "textAlign") === value ? "align-style-item-active" : "align-style-item-un_active", "align-style-item")} onClick={() => updateContentTextAlign(value)}>
161
+ <FontAwesomeIcon icon={icon} className="tag-style-size" />
162
+ </div>
163
+ ))}
164
+ </div>
165
+ )}
166
+ {cardItemElement(t("separator"), <Input value={separator} onChange={(e) => updateMenuSettings("separator", e.target.value)} placeholder=" | " />)}
167
+
168
+ <div className="right-setting-block-item-title margin-top-18">{t("padding_settings")}</div>
169
+ <PaddingSettings padding={{ paddingTop: findStyleItem(currentItem.data.contentStyles, "paddingTop"), paddingRight: findStyleItem(currentItem.data.contentStyles, "paddingRight"), paddingLeft: findStyleItem(currentItem.data.contentStyles, "paddingLeft"), paddingBottom: findStyleItem(currentItem.data.contentStyles, "paddingBottom") }} setPadding={contentPaddingChange} />
170
+
171
+ <div className="card-item-title margin-top-18">{t("spacing")}</div>
172
+ <PaddingSettings padding={{ paddingTop: findStyleItem(currentItem.data.styles, "paddingTop"), paddingRight: findStyleItem(currentItem.data.styles, "paddingRight"), paddingLeft: findStyleItem(currentItem.data.styles, "paddingLeft"), paddingBottom: findStyleItem(currentItem.data.styles, "paddingBottom") }} setPadding={paddingChange} />
173
+ </div>
174
+ );
175
+ };
176
+
177
+ export default MenuStyleSettings;
@@ -0,0 +1,30 @@
1
+ import { InputNumber } from "antd";
2
+ import useTranslation from "../../translation";
3
+
4
+ const PaddingSettings = ({ padding, setPadding }) => {
5
+ const { t } = useTranslation();
6
+ const paddingChange = (key) => (value) => {
7
+ const newPadding = { ...padding, [key]: value };
8
+ setPadding(newPadding);
9
+ };
10
+ return (
11
+ <div className="padding-settings">
12
+ {[
13
+ { name: t("top"), value: "paddingTop" },
14
+ { name: t("right"), value: "paddingRight" },
15
+ { name: t("left"), value: "paddingLeft" },
16
+ { name: t("bottom"), value: "paddingBottom" },
17
+ ].map(({ name, value }) => {
18
+ const style = padding[value];
19
+ return (
20
+ <div key={value}>
21
+ <div>{name}</div>
22
+ <InputNumber className="width-full" addonAfter="px" min={0} value={style} onChange={paddingChange(value)} />
23
+ </div>
24
+ );
25
+ })}
26
+ </div>
27
+ );
28
+ };
29
+
30
+ export default PaddingSettings;
@@ -0,0 +1,250 @@
1
+ import { useContext, useCallback, useRef, useState } from "react";
2
+ import { GlobalContext } from "../../reducers";
3
+ import classNames from "../../utils/classNames";
4
+ import { Input, InputNumber } from "antd";
5
+ import { throttle, deepClone } from "../../utils/helpers";
6
+ import PaddingSettings from "./PaddingSettings";
7
+ import useLayout from "../../utils/useStyleLayout";
8
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
9
+ import { faTrash, faAlignCenter, faAlignLeft, faAlignRight, faAlignJustify } from "@fortawesome/free-solid-svg-icons";
10
+ import useTranslation from "../../translation";
11
+
12
+ const SOCIAL_ICON_CDN = "https://s.magecdn.com/social";
13
+
14
+ const SocialLinkSettings = () => {
15
+ const { currentItem, previewMode } = useContext(GlobalContext);
16
+ const { t } = useTranslation();
17
+ const { findStyleItem, paddingChange, updateItemStyles, cardItemElement } = useLayout();
18
+
19
+ const { list } = currentItem.data;
20
+ const [sourceNode, setSourceNode] = useState(null);
21
+ const [isDragStart, setIsDragStart] = useState(false);
22
+ const [dragIndex, setDragIndex] = useState(null);
23
+ const socialLists = useRef(null);
24
+
25
+ const socialList = [
26
+ { image: `${SOCIAL_ICON_CDN}/tc-facebook.svg`, title: "Facebook", linkURL: "" },
27
+ { image: `${SOCIAL_ICON_CDN}/tc-github.svg`, title: "Github", linkURL: "" },
28
+ { image: `${SOCIAL_ICON_CDN}/tc-linkedin.svg`, title: "LinkedIn", linkURL: "" },
29
+ { image: `${SOCIAL_ICON_CDN}/tc-instagram.svg`, title: "Instagram", linkURL: "" },
30
+ { image: `${SOCIAL_ICON_CDN}/tc-tiktok.svg`, title: "TikTok", linkURL: "" },
31
+ { image: `${SOCIAL_ICON_CDN}/tc-x.svg`, title: "Twitter", linkURL: "" },
32
+ { image: `${SOCIAL_ICON_CDN}/tc-youtube.svg`, title: "YouTube", linkURL: "" },
33
+ ];
34
+
35
+ const updateSocialSettings = (key, value) => {
36
+ const newCurrentItem = deepClone(currentItem);
37
+ newCurrentItem.data = {
38
+ ...newCurrentItem.data,
39
+ [key]: value,
40
+ };
41
+ updateItemStyles(newCurrentItem.data);
42
+ };
43
+
44
+ // throttle() returns a new function reference
45
+ // eslint-disable-next-line react-hooks/exhaustive-deps
46
+ const dragOver = useCallback(
47
+ throttle((event) => {
48
+ event.preventDefault();
49
+ const listsDom = socialLists.current;
50
+ const children = Array.from(listsDom.childNodes);
51
+ const overIndex = children.indexOf(event.target);
52
+
53
+ if (overIndex === -1) return;
54
+ const sourceIndex = dragIndex;
55
+ const newSocialLists = deepClone(list);
56
+ if (sourceIndex === overIndex) {
57
+ return;
58
+ }
59
+
60
+ [newSocialLists[overIndex], newSocialLists[sourceIndex]] = [newSocialLists[sourceIndex], newSocialLists[overIndex]];
61
+ setDragIndex(overIndex);
62
+ updateSocialSettings("list", newSocialLists);
63
+ }, 50),
64
+ [sourceNode, dragIndex]
65
+ );
66
+
67
+ const contentPaddingChange = (padding) => {
68
+ const newData = deepClone(currentItem.data);
69
+ newData.contentStyles[previewMode] = {
70
+ ...newData.contentStyles[previewMode],
71
+ ...padding,
72
+ };
73
+ updateItemStyles(newData);
74
+ };
75
+
76
+ const updateContentTextAlign = (textAlign) => {
77
+ const newData = deepClone(currentItem.data);
78
+ newData.contentStyles[previewMode] = {
79
+ ...newData.contentStyles[previewMode],
80
+ textAlign,
81
+ };
82
+ updateItemStyles(newData);
83
+ };
84
+
85
+ const imageWidthChange = (value) => {
86
+ const newData = deepClone(currentItem.data);
87
+ newData.imageWidth = value;
88
+ updateItemStyles(newData);
89
+ };
90
+
91
+ const PaddingStylesElement = () => {
92
+ return (
93
+ <>
94
+ <div className="right-setting-block-item-title"> {t("padding_settings")}</div>
95
+ <PaddingSettings
96
+ padding={{
97
+ paddingTop: findStyleItem(currentItem.data.contentStyles, "paddingTop"),
98
+ paddingRight: findStyleItem(currentItem.data.contentStyles, "paddingRight"),
99
+ paddingLeft: findStyleItem(currentItem.data.contentStyles, "paddingLeft"),
100
+ paddingBottom: findStyleItem(currentItem.data.contentStyles, "paddingBottom"),
101
+ }}
102
+ setPadding={contentPaddingChange}
103
+ />
104
+ </>
105
+ );
106
+ };
107
+
108
+ const socialLinkLayoutElement = () => {
109
+ const textAlign = findStyleItem(currentItem.data.contentStyles, "textAlign");
110
+ const width = currentItem.data.imageWidth;
111
+ return (
112
+ <>
113
+ <div className="right-setting-block-item-title"> {t("social_link_settings")}</div>
114
+ {cardItemElement(
115
+ t("align"),
116
+ <div className="flex justify-center items-center">
117
+ {[
118
+ { icon: faAlignLeft, value: "left" },
119
+ { icon: faAlignCenter, value: "center" },
120
+ { icon: faAlignRight, value: "right" },
121
+ { icon: faAlignJustify, value: "justify" },
122
+ ].map(({ icon, value }) => {
123
+ return (
124
+ <div
125
+ key={value}
126
+ className={classNames(textAlign === value ? "align-style-item-active" : "align-style-item-un_active", "align-style-item")}
127
+ onClick={() => updateContentTextAlign(value)}
128
+ >
129
+ <FontAwesomeIcon icon={icon} className="tag-style-size" />
130
+ </div>
131
+ );
132
+ })}
133
+ </div>
134
+ )}
135
+ {cardItemElement(
136
+ t("social_link_size"),
137
+ <InputNumber min={0} className="input-width" addonAfter="px" value={width} onChange={imageWidthChange} />
138
+ )}
139
+ <div className="card-item-title margin-top-18">{t("social_links")}</div>
140
+ <div
141
+ ref={socialLists}
142
+ onDragOver={dragOver}
143
+ className="margin-top-12"
144
+ onDragStart={(event) => {
145
+ setSourceNode(event.target);
146
+ setIsDragStart(true);
147
+ setDragIndex(event.target.dataset.index);
148
+ }}
149
+ onDragEnd={() => {
150
+ sourceNode.childNodes[0].classList.remove("social-link-item-current");
151
+ setSourceNode(null);
152
+ setIsDragStart(false);
153
+ setDragIndex(null);
154
+ }}
155
+ >
156
+ {list.map((item, index) => {
157
+ const { image, title, linkURL } = item;
158
+ return (
159
+ <div
160
+ key={index}
161
+ data-index={index}
162
+ draggable
163
+ className={classNames(
164
+ "social-link-item",
165
+ "cursor-grab",
166
+ isDragStart && "social-link-item-drag_start",
167
+ dragIndex === index ? "social-link-item-current" : "border-transparent"
168
+ )}
169
+ >
170
+ <div className="social-link-item-content">
171
+ <div className="flex items-center justify-between">
172
+ <div className="flex items-center">
173
+ <img src={image} alt={title} className="social-link-item-img" />
174
+ <div className="font-semibold">{title}</div>
175
+ </div>
176
+ <div className="flex items-center">
177
+ <div
178
+ className="social-link-item-icon social-link-item-icon-slate"
179
+ onClick={() => {
180
+ updateSocialSettings(
181
+ "list",
182
+ list.filter((item, idx) => idx !== index)
183
+ );
184
+ }}
185
+ >
186
+ <FontAwesomeIcon icon={faTrash} className="social-link-item-icon-svg social-link-item-icon-svg-deep" />
187
+ </div>
188
+ </div>
189
+ </div>
190
+ <div className="margin-top-12 relative">
191
+ <Input
192
+ addonBefore="https://"
193
+ value={linkURL || ""}
194
+ onChange={(e) =>
195
+ updateSocialSettings(
196
+ "list",
197
+ list.map((item, idx) => {
198
+ if (idx === index) {
199
+ return { ...item, linkURL: e.target.value };
200
+ } else {
201
+ return item;
202
+ }
203
+ })
204
+ )
205
+ }
206
+ />
207
+ </div>
208
+ </div>
209
+ </div>
210
+ );
211
+ })}
212
+ </div>
213
+ <div className="card-item-title margin-top-18">{t("add_social_link")}</div>
214
+ <div className="social-link-add margin-top-12">
215
+ {socialList.map((item, index) => {
216
+ const { image, title } = item;
217
+ return (
218
+ <img
219
+ src={image}
220
+ key={index}
221
+ alt={title}
222
+ className="social-link-add-img"
223
+ onClick={() => updateSocialSettings("list", list.concat(item))}
224
+ />
225
+ );
226
+ })}
227
+ </div>
228
+ <div className="card-item-title margin-top-18"> {t("spacing")}</div>
229
+ <PaddingSettings
230
+ padding={{
231
+ paddingTop: findStyleItem(currentItem.data.styles, "paddingTop"),
232
+ paddingRight: findStyleItem(currentItem.data.styles, "paddingRight"),
233
+ paddingLeft: findStyleItem(currentItem.data.styles, "paddingLeft"),
234
+ paddingBottom: findStyleItem(currentItem.data.styles, "paddingBottom"),
235
+ }}
236
+ setPadding={paddingChange}
237
+ />
238
+ </>
239
+ );
240
+ };
241
+
242
+ return (
243
+ <div className="margin-y-30">
244
+ {socialLinkLayoutElement()}
245
+ {PaddingStylesElement()}
246
+ </div>
247
+ );
248
+ };
249
+
250
+ export default SocialLinkSettings;