├── .babelrc
├── .gitignore
├── MIT-LICENSE
├── README.md
├── dist
├── index.html
├── mainlandJs.js
└── mainlandJs.js.LICENSE.txt
├── package.json
├── postcss.config.js
├── src
├── App.jsx
├── components
│ ├── Breadcrumb
│ │ ├── Breadcrumb.module.scss
│ │ └── index.jsx
│ ├── Buttons
│ │ ├── Buttons.module.scss
│ │ └── index.jsx
│ ├── Canvas
│ │ ├── Actions.jsx
│ │ ├── Card.jsx
│ │ └── index.jsx
│ ├── CollapseMenu
│ │ ├── CollapseMenu.module.scss
│ │ └── index.jsx
│ ├── Header
│ │ ├── Header.module.scss
│ │ ├── MainActions
│ │ │ └── index.jsx
│ │ ├── ResponsiveActions
│ │ │ └── index.jsx
│ │ ├── SidebarActions
│ │ │ ├── SidebarActions.module.scss
│ │ │ └── index.jsx
│ │ └── index.jsx
│ ├── Icons
│ │ └── index.jsx
│ ├── Inputs
│ │ ├── Input
│ │ │ └── index.jsx
│ │ ├── Label
│ │ │ └── index.jsx
│ │ ├── Select
│ │ │ └── index.jsx
│ │ └── TextArea
│ │ │ └── index.jsx
│ ├── Layout
│ │ ├── Layout.module.scss
│ │ └── index.jsx
│ ├── Modals
│ │ ├── AI.jsx
│ │ ├── Export.jsx
│ │ ├── ImageSource
│ │ │ ├── ExternalImages.jsx
│ │ │ ├── UploadImage.jsx
│ │ │ └── index.jsx
│ │ ├── Import.jsx
│ │ ├── MediaLibrary
│ │ │ ├── ExternalImages.jsx
│ │ │ ├── UploadImage.jsx
│ │ │ └── index.jsx
│ │ ├── Modal.jsx
│ │ ├── Modals.module.scss
│ │ ├── SidebarModal.jsx
│ │ └── index.jsx
│ ├── Sidebar
│ │ ├── Blocks
│ │ │ ├── Blocks.module.scss
│ │ │ ├── ButtonBlock.jsx
│ │ │ └── index.jsx
│ │ ├── Layers
│ │ │ ├── Layer.jsx
│ │ │ ├── Layers.module.scss
│ │ │ └── index.jsx
│ │ ├── Settings
│ │ │ ├── Settings.module.scss
│ │ │ └── index.jsx
│ │ ├── Sidebar.module.scss
│ │ ├── StyleManager
│ │ │ ├── Background
│ │ │ │ └── index.jsx
│ │ │ ├── Borders
│ │ │ │ └── index.jsx
│ │ │ ├── BoxShadow
│ │ │ │ └── index.jsx
│ │ │ ├── Classes
│ │ │ │ ├── Classes.module.scss
│ │ │ │ └── index.jsx
│ │ │ ├── Effects
│ │ │ │ └── index.jsx
│ │ │ ├── Flex
│ │ │ │ └── index.jsx
│ │ │ ├── FlexChild
│ │ │ │ └── index.jsx
│ │ │ ├── Grid
│ │ │ │ └── index.jsx
│ │ │ ├── Layout
│ │ │ │ └── index.jsx
│ │ │ ├── Position
│ │ │ │ └── index.jsx
│ │ │ ├── Size
│ │ │ │ └── index.jsx
│ │ │ ├── Spacing
│ │ │ │ └── index.jsx
│ │ │ ├── StyleManager.module.scss
│ │ │ ├── Typography
│ │ │ │ └── index.jsx
│ │ │ └── index.jsx
│ │ └── index.jsx
│ └── StyleManager
│ │ ├── AlignSelector
│ │ └── index.jsx
│ │ ├── BordersSelector
│ │ ├── BordersSelector.module.scss
│ │ ├── Button.jsx
│ │ └── index.jsx
│ │ ├── ClassSelector
│ │ └── index.jsx
│ │ ├── ImageSelector
│ │ └── index.jsx
│ │ ├── PropertySelector
│ │ └── index.jsx
│ │ ├── RangeSelector
│ │ ├── RangeSelector.module.scss
│ │ └── index.jsx
│ │ ├── SpacingSelector
│ │ ├── SpacingSelector.module.scss
│ │ └── index.jsx
│ │ ├── SrcSelector
│ │ └── index.jsx
│ │ └── TagSelector
│ │ └── index.jsx
├── configs
│ ├── index.js
│ └── tailwind.js
├── helpers
│ └── index.js
├── index.html
├── index.js
├── redux
│ ├── classes-reducer.js
│ ├── data-reducer.js
│ ├── layout-reducer.js
│ ├── modals-reducer.js
│ └── store.js
├── render
│ └── template.js
├── styles
│ ├── classes.js
│ ├── index.css
│ ├── mediumTheme.js
│ └── variables.scss
└── utils
│ └── index.js
├── tailwind.config.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env", "@babel/preset-react"],
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 Accomplice AI, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TailwindCSS-powered WYSIWYG page builder
2 |
3 | Mainland is an open source WYSIWYG page builder built exclusively with TailwindCSS in mind.
4 |
5 | ## What is Mainland?
6 |
7 | Mainland is an open source WYSIWYG landing page builder powered by TailwindCSS & enhanced with AI.
8 |
9 | With Mainland you can visually create web pages, landing pages and more using TailwindCSS – the world’s most popular CSS framework – and easily generate images, text and even HTML with AI.
10 |
11 | The key features of Mainland are:
12 |
13 | - **Powered by Tailwind CSS**: The world’s most popular CSS framework, Tailwind CSS makes it easy for hundreds of thousands of developers and teams to build quickly and uniformly. Mainland’s support for Tailwind makes it easy for you and your team to integrate Mainland using the CSS framework you already know and love and run in production.
14 | - **Open Source WYSIWYG**: Mainland is the world’s first open source, WYSIWYG page builder that fully supports Tailwind by default.
15 | - **AI enhanced**: Securely use your Open AI API token to seemlessly add HTML templates, headers, paragraphs and images to your pages.
16 |
17 | https://github.com/Accomplice-AI/mainland/assets/26133/f3d4fdb1-00e3-4999-8558-2271f99b4ed0
18 |
19 | ## Why Mainland?
20 |
21 | Mainland was designed primarily for use inside Content Management Systems to speed up the creation of dynamic templates and replace common WYSIWYG editors, which are good for content editing, but inappropriate for creating HTML structures. It’s especially useful for teams that already use TailwindCSS everywhere else in their dev stack. Using Mainland user generated content can have the same class structure as everything else in your webapp and consume fewer resources.
22 |
23 | ## Usage
24 |
25 | Directly in the browser:
26 |
27 | ```
28 |
29 |
30 |
31 |
32 |
33 |
46 | ```
47 |
48 | ## License
49 |
50 | The software is free for use under the MIT License.
51 |
52 | ## Authors & Contributors
53 |
54 | Developed by Yaroslav Luchenko and Adam Howell.
55 |
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Mainland
10 |
11 |
12 |
13 |
14 |
15 |
16 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/dist/mainlandJs.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*
2 | object-assign
3 | (c) Sindre Sorhus
4 | @license MIT
5 | */
6 |
7 | /*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */
8 |
9 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */
10 |
11 | /**
12 | * react-collapsed v4.0.2
13 | *
14 | * Copyright (c) 2019-2023, Rogin Farrer
15 | *
16 | * This source code is licensed under the MIT license found in the
17 | * LICENSE.md file in the root directory of this source tree.
18 | *
19 | * @license MIT
20 | */
21 |
22 | /**
23 | * @license React
24 | * react-dom-server-legacy.browser.production.min.js
25 | *
26 | * Copyright (c) Facebook, Inc. and its affiliates.
27 | *
28 | * This source code is licensed under the MIT license found in the
29 | * LICENSE file in the root directory of this source tree.
30 | */
31 |
32 | /**
33 | * @license React
34 | * react-dom-server.browser.production.min.js
35 | *
36 | * Copyright (c) Facebook, Inc. and its affiliates.
37 | *
38 | * This source code is licensed under the MIT license found in the
39 | * LICENSE file in the root directory of this source tree.
40 | */
41 |
42 | /**
43 | * @license React
44 | * react-dom.production.min.js
45 | *
46 | * Copyright (c) Facebook, Inc. and its affiliates.
47 | *
48 | * This source code is licensed under the MIT license found in the
49 | * LICENSE file in the root directory of this source tree.
50 | */
51 |
52 | /**
53 | * @license React
54 | * react-is.production.min.js
55 | *
56 | * Copyright (c) Facebook, Inc. and its affiliates.
57 | *
58 | * This source code is licensed under the MIT license found in the
59 | * LICENSE file in the root directory of this source tree.
60 | */
61 |
62 | /**
63 | * @license React
64 | * react-jsx-runtime.production.min.js
65 | *
66 | * Copyright (c) Facebook, Inc. and its affiliates.
67 | *
68 | * This source code is licensed under the MIT license found in the
69 | * LICENSE file in the root directory of this source tree.
70 | */
71 |
72 | /**
73 | * @license React
74 | * react.production.min.js
75 | *
76 | * Copyright (c) Facebook, Inc. and its affiliates.
77 | *
78 | * This source code is licensed under the MIT license found in the
79 | * LICENSE file in the root directory of this source tree.
80 | */
81 |
82 | /**
83 | * @license React
84 | * scheduler.production.min.js
85 | *
86 | * Copyright (c) Facebook, Inc. and its affiliates.
87 | *
88 | * This source code is licensed under the MIT license found in the
89 | * LICENSE file in the root directory of this source tree.
90 | */
91 |
92 | /**
93 | * @license React
94 | * use-sync-external-store-shim.production.min.js
95 | *
96 | * Copyright (c) Facebook, Inc. and its affiliates.
97 | *
98 | * This source code is licensed under the MIT license found in the
99 | * LICENSE file in the root directory of this source tree.
100 | */
101 |
102 | /**
103 | * @license React
104 | * use-sync-external-store-shim/with-selector.production.min.js
105 | *
106 | * Copyright (c) Facebook, Inc. and its affiliates.
107 | *
108 | * This source code is licensed under the MIT license found in the
109 | * LICENSE file in the root directory of this source tree.
110 | */
111 |
112 | /** @license React v16.13.1
113 | * react-is.production.min.js
114 | *
115 | * Copyright (c) Facebook, Inc. and its affiliates.
116 | *
117 | * This source code is licensed under the MIT license found in the
118 | * LICENSE file in the root directory of this source tree.
119 | */
120 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mainland_js",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "start": "webpack serve --mode development",
8 | "build": "webpack --mode production"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/Mainland-AI/mainland_js.git"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "bugs": {
17 | "url": "https://github.com/Mainland-AI/mainland_js/issues"
18 | },
19 | "homepage": "https://github.com/Mainland-AI/mainland_js#readme",
20 | "dependencies": {
21 | "@babel/cli": "^7.21.0",
22 | "@babel/core": "^7.21.4",
23 | "@babel/plugin-proposal-class-properties": "^7.18.6",
24 | "@babel/preset-env": "^7.21.4",
25 | "@babel/preset-react": "^7.18.6",
26 | "babel-loader": "^9.1.2",
27 | "bootstrap-icons": "^1.10.5",
28 | "hex-to-rgba": "^2.0.1",
29 | "html-react-parser": "^3.0.16",
30 | "immutability-helper": "^3.1.1",
31 | "openai": "^3.3.0",
32 | "react": "^18.2.0",
33 | "react-collapsed": "^4.0.2",
34 | "react-contenteditable": "^3.3.7",
35 | "react-dnd": "^16.0.1",
36 | "react-dnd-html5-backend": "^16.0.1",
37 | "react-dnd-multi-backend": "^8.0.1",
38 | "react-dom": "^18.2.0",
39 | "react-dropzone": "^14.2.3",
40 | "react-frame-component": "^5.2.6",
41 | "react-medium-editor": "^1.8.1",
42 | "react-redux": "^8.0.5",
43 | "react-select": "^5.7.3",
44 | "redux": "^4.2.1",
45 | "redux-thunk": "^2.4.2",
46 | "shortid": "^2.2.16"
47 | },
48 | "devDependencies": {
49 | "autoprefixer": "^10.4.14",
50 | "copy-webpack-plugin": "^11.0.0",
51 | "css-loader": "^6.7.3",
52 | "file-loader": "^6.2.0",
53 | "node-sass": "^8.0.0",
54 | "postcss": "^8.4.23",
55 | "postcss-import": "^15.1.0",
56 | "postcss-loader": "^7.2.4",
57 | "postcss-nesting": "^11.2.2",
58 | "sass-loader": "^13.2.2",
59 | "style-loader": "^3.3.2",
60 | "tailwindcss": "^3.3.2",
61 | "url-loader": "^4.1.1",
62 | "webpack": "^5.86.0",
63 | "webpack-cli": "^5.0.2",
64 | "webpack-dev-server": "^4.13.3"
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | }
6 | }
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { defaultConfig } from "./configs";
3 | import Layout from "./components/Layout";
4 | import Header from "./components/Header";
5 | import Sidebar from "./components/Sidebar";
6 | import Canvas from "./components/Canvas";
7 | import Breadcrumb from "./components/Breadcrumb";
8 | import { Provider } from "react-redux";
9 | import { store } from "./redux/store";
10 | import { useDispatch } from "react-redux";
11 | import { setConfig } from "./redux/data-reducer";
12 | import Modals from "./components/Modals";
13 | import { useClassNames } from "./helpers";
14 | import { DndProvider } from "react-dnd";
15 | import { HTML5Backend } from "react-dnd-html5-backend";
16 |
17 | import "./styles/index.css";
18 |
19 | const Init = ({ userConfig }) => {
20 | const dispatch = useDispatch();
21 |
22 | useEffect(() => {
23 | dispatch(
24 | setConfig({
25 | ...defaultConfig,
26 | ...userConfig,
27 | blocks: [...defaultConfig.blocks, ...userConfig.blocks],
28 | })
29 | );
30 | }, [userConfig]);
31 |
32 | return <>>;
33 | };
34 |
35 | const App = ({ userConfig }) => {
36 | return (
37 |
38 |
39 |
40 | }
42 | slotSidebar={}
43 | slotBreadcrumb={}
44 | slotModals={}
45 | />
46 |
47 |
48 | );
49 | };
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/src/components/Breadcrumb/Breadcrumb.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../src/styles/variables.scss";
2 |
3 | .root {
4 | width: 100%;
5 | height: 100%;
6 | overflow: hidden;
7 | display: flex;
8 | align-items: center;
9 | padding-left: 1rem;
10 | }
--------------------------------------------------------------------------------
/src/components/Breadcrumb/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelectedNode } from "../../helpers";
3 | import styles from "./Breadcrumb.module.scss";
4 | import { useSelector, useDispatch } from "react-redux";
5 | import {
6 | setSelectedSection,
7 | setHoveredSection,
8 | } from "../../redux/data-reducer";
9 |
10 | const Breadcrumb = () => {
11 | const selectedNode = useSelectedNode();
12 | const { dom } = useSelector((state) => state.data);
13 | const dispatch = useDispatch();
14 |
15 | const generate = () => {
16 | let path = [];
17 | let isFound = false;
18 |
19 | const id = selectedNode.id;
20 |
21 | const checkNode = (node) => {
22 | let subPath = [];
23 |
24 | if (node.children) {
25 | node.children.forEach((n) => {
26 | if (!isFound) {
27 | if (n.id === id) {
28 | subPath = [n];
29 | isFound = true;
30 | } else {
31 | subPath = [n, ...checkNode(n)];
32 | }
33 | }
34 | });
35 | }
36 |
37 | return subPath;
38 | };
39 |
40 | dom.forEach((node) => {
41 | if (!isFound) {
42 | if (node.id === id) {
43 | path = [node];
44 | isFound = true;
45 | } else {
46 | path = [node, ...checkNode(node)];
47 | }
48 | }
49 | });
50 | return path;
51 | };
52 |
53 | return (
54 |
55 | {selectedNode && (
56 | <>
57 | {generate().map((node, i) => (
58 |
59 | {i !== 0 && > }
60 | dispatch(setSelectedSection(node))}
62 | onMouseEnter={() => dispatch(setHoveredSection(node))}
63 | onMouseLeave={() => dispatch(setHoveredSection(null))}
64 | className="p-1 rounded transition hover:bg-slate-600 cursor-pointer leading-none"
65 | >
66 | {node?.label
67 | ? `${node?.label} (${node?.tagName})`
68 | : node?.tagName}
69 |
70 |
71 | ))}
72 | >
73 | )}
74 |
75 | );
76 | };
77 |
78 | export default Breadcrumb;
79 |
--------------------------------------------------------------------------------
/src/components/Buttons/Buttons.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../styles//variables.scss";
2 |
3 | .root {
4 | background-color: transparent;
5 | outline: none;
6 | display: inline-flex;
7 | align-items: center;
8 | padding: 0 1rem;
9 | cursor: pointer;
10 | transition: all $transition ease;
11 | }
12 |
13 | .active {
14 | opacity: 1;
15 | }
16 |
17 | .md {
18 | height: $button-md;
19 | }
20 |
21 | .sm {
22 | height: $button-sm;
23 | }
24 |
25 | .lg {
26 | height: $button-lg;
27 | }
28 |
29 | .active {
30 | opacity: 1;
31 | }
--------------------------------------------------------------------------------
/src/components/Buttons/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Buttons.module.scss";
3 |
4 | export const Button = (props) => {
5 | const { children, size, active, className, disabled, isUnderline, ...rest } = props;
6 |
7 | const getSize = () => {
8 | switch (size) {
9 | case "md":
10 | return styles.md;
11 | case "sm":
12 | return styles.sm;
13 | case "lg":
14 | return styles.lg;
15 | default:
16 | return "";
17 | }
18 | };
19 |
20 | return (
21 |
29 | {children}
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/components/Canvas/Actions.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { IconClose, IconChevronDown, IconChevronUp } from "../Icons";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import {
5 | removeNode,
6 | setSelectedParent,
7 | setSelectedChild,
8 | } from "../../redux/data-reducer";
9 |
10 | const Actions = ({ node, isBottom, isInner }) => {
11 | const dispatch = useDispatch();
12 | const { id } = node;
13 | const { hoveredSection, selectedSection } = useSelector(
14 | (state) => state.data
15 | );
16 | const [transition, setTransition] = useState(false);
17 |
18 | useEffect(() => {
19 | if (selectedSection?.id === id || hoveredSection?.id === id) {
20 | setTimeout(() => {
21 | setTransition(true);
22 | }, 100);
23 | } else {
24 | setTransition(false);
25 | }
26 | }, [selectedSection, hoveredSection]);
27 |
28 | const onUp = () => {
29 | dispatch(setSelectedParent(id));
30 | };
31 |
32 | const onDown = () => {
33 | dispatch(setSelectedChild(id));
34 | };
35 | const onRemove = () => {
36 | dispatch(removeNode(id));
37 | };
38 |
39 | const isActive = () =>
40 | selectedSection?.id === id || hoveredSection?.id === id;
41 |
42 | return (
43 |
58 | {hoveredSection?.id === id && !(selectedSection?.id === id) ? (
59 |
{node.tagName}
60 | ) : (
61 | <>
62 |
{
64 | if (onUp) onUp();
65 | }}
66 | className="mr-2 text-black opacity-80 hover:opacity-100 transition-opacity"
67 | >
68 |
69 |
70 |
{
73 | if (onDown) onDown();
74 | }}
75 | >
76 |
77 |
78 |
{
81 | if (onRemove) onRemove();
82 | }}
83 | >
84 |
85 |
86 | >
87 | )}
88 |
89 | );
90 | };
91 |
92 | export default Actions;
93 |
--------------------------------------------------------------------------------
/src/components/Canvas/Card.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect, useMemo } from "react";
2 | import { useDrag, useDrop } from "react-dnd";
3 | import {
4 | setSelectedSection,
5 | setHoveredSection,
6 | updateText,
7 | addToNode,
8 | setHighlight,
9 | } from "../../redux/data-reducer";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import Actions from "./Actions";
12 | import {
13 | htmlToJson,
14 | checkAndReturnStyles,
15 | isCanContainsChildren,
16 | getEditableTagName,
17 | replceSpecialCharacters,
18 | getDefaultDisplayClassEditable,
19 | } from "../../utils";
20 | //import ContentEditable from "react-contenteditable";
21 | import { openModal } from "../../redux/modals-reducer";
22 | import ContentEditable from "react-medium-editor";
23 |
24 | export const Card = ({
25 | index,
26 | moveCard,
27 | children,
28 | node,
29 | isEditable,
30 | windowFrame,
31 | }) => {
32 | const { id, backgroundImage, className } = node;
33 | const ref = useRef(null);
34 | const dispatch = useDispatch();
35 | const { hoveredSection, selectedSection, dropHighlight } = useSelector(
36 | (state) => state.data
37 | );
38 | const [isCanEdit, setIsCanEdit] = useState(0);
39 | const { isPreview } = useSelector((state) => state.layout);
40 | const editableRef = useRef();
41 |
42 | useEffect(() => {
43 | if ((!selectedSection && editableRef?.current) || (selectedSection?.id != id && editableRef?.current)) {
44 | editableRef.current.medium.origElements.blur();
45 | if (windowFrame.getSelection) {
46 | if (windowFrame.getSelection().empty) {
47 | windowFrame.getSelection().empty();
48 | } else if (windowFrame.getSelection().removeAllRanges) {
49 | windowFrame.getSelection().removeAllRanges();
50 | }
51 | } else if (windowFrame.document.selection) {
52 | windowFrame.document.selection.empty();
53 | }
54 | }
55 | }, [selectedSection]);
56 |
57 | useEffect(() => {
58 | if (selectedSection?.id != id && editableRef?.current) {
59 | editableRef.current.medium.origElements.blur()
60 | }
61 | }, [hoveredSection]);
62 |
63 | const colorBright = useMemo(
64 | () => (node.tagName === "body" ? "#696969" : "#adadad"),
65 | [node]
66 | );
67 | const colorDark = useMemo(() => "#696969", [node]);
68 |
69 | const style = useMemo(
70 | () => ({
71 | ...(!isPreview
72 | ? {
73 | border: `1px dashed ${colorDark}`,
74 | }
75 | : {}),
76 | }),
77 | [colorDark, isPreview]
78 | );
79 |
80 | useEffect(() => {
81 | if (!hoveredSection) clearHightLight();
82 | }, [hoveredSection]);
83 |
84 | const highlight = (monitor) => {
85 | const hoverBoundingRect = ref.current?.getBoundingClientRect();
86 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
87 | const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
88 | const clientOffset = monitor.getClientOffset();
89 |
90 | if (clientOffset) {
91 | const hoverClientY = clientOffset.y - hoverBoundingRect.top;
92 | const hoverClientX = clientOffset.x - hoverBoundingRect.left;
93 |
94 | const left = (hoverClientX * 100) / hoverMiddleX;
95 | const top = (hoverClientY * 100) / hoverMiddleY;
96 |
97 | const offset = 50;
98 |
99 | const percentages = [
100 | { position: "top", value: top < 100 && top < offset ? top : 0 },
101 | { position: "left", value: left < 100 && left < offset ? left : 0 },
102 | {
103 | position: "right",
104 | value: left > 100 && left - 100 > offset ? 100 - (left - 100) : 0,
105 | },
106 | {
107 | position: "bottom",
108 | value: top > 100 && top - 100 > offset ? 100 - (top - 100) : 0,
109 | },
110 | ];
111 |
112 | const greater = percentages.sort((a, b) => a.value - b.value).pop();
113 |
114 | greater.value > 0
115 | ? dispatch(setHighlight({ id: id, position: greater.position }))
116 | : !node.isClosed
117 | ? dispatch(setHighlight({ id: id, position: "all" }))
118 | : dispatch(setHighlight(null));
119 | }
120 | };
121 |
122 | const [{ handlerId }, drop] = useDrop(
123 | {
124 | accept: ["card", "block"],
125 | collect(monitor) {
126 | return {
127 | handlerId: monitor.getHandlerId(),
128 | id: id,
129 | };
130 | },
131 | canDrop() {
132 | return dropHighlight;
133 | },
134 | drop(item, monitor) {
135 | if (
136 | (!item.data && !node) ||
137 | (!item.data && !hoveredSection) ||
138 | monitor.didDrop() ||
139 | (!item.data && item.id === id)
140 | )
141 | return;
142 | if (!ref.current) {
143 | return;
144 | }
145 |
146 | const dragId = item.id;
147 | const hoverId = id;
148 |
149 | if (!(node.isClosed && !dropHighlight)) {
150 | if (item.data) {
151 | const doc = new DOMParser().parseFromString(
152 | replceSpecialCharacters(item.data.content),
153 | "text/xml"
154 | );
155 | dispatch(
156 | addToNode(
157 | htmlToJson(doc.firstChild, item.data.attributes),
158 | hoverId
159 | )
160 | );
161 | } else {
162 | moveCard(dragId, hoverId, node);
163 | }
164 | }
165 |
166 | dispatch(setHighlight(null));
167 | },
168 | hover(item, monitor) {
169 | if (monitor.isOver({ shallow: true })) {
170 | highlight(monitor);
171 | }
172 | },
173 | },
174 | [node, dropHighlight]
175 | );
176 |
177 | const [dragTargetProps, drag] = useDrag(
178 | {
179 | type: "card",
180 | canDrag:
181 | !isPreview && hoveredSection?.id === id && node.tagName !== "body",
182 | item: () => {
183 | return { id, index };
184 | },
185 | collect: (monitor) => ({
186 | isDragging: monitor.isDragging(),
187 | id: id,
188 | }),
189 | },
190 | [hoveredSection, id, isPreview]
191 | );
192 |
193 | const opacity = dragTargetProps.isDragging ? 0 : 1;
194 | drag(drop(ref));
195 |
196 | const onMouseMove = (e) => {
197 | if (e.target.id === id && hoveredSection?.id !== id)
198 | dispatch(setHoveredSection(node));
199 | };
200 |
201 | const onMouseLeave = () => {
202 | dispatch(setHoveredSection(null));
203 | };
204 |
205 | const onClick = (e) => {
206 | if (e.target.id === id) dispatch(setSelectedSection(node));
207 | };
208 |
209 | const clearHightLight = () => {
210 | dispatch(setHighlight(null));
211 | };
212 |
213 | const onDragLeave = () => {
214 | clearHightLight();
215 | };
216 |
217 | const borderStyles = useMemo(
218 | () => ({
219 | borderWidth: "1px",
220 | borderTopColor:
221 | dropHighlight?.id === id &&
222 | (dropHighlight.position === "top" || dropHighlight.position === "all")
223 | ? "white"
224 | : selectedSection?.id === id || hoveredSection?.id === id
225 | ? colorBright
226 | : colorDark,
227 | borderTopStyle:
228 | dropHighlight?.id === id &&
229 | (dropHighlight.position === "top" || dropHighlight.position === "all")
230 | ? "solid"
231 | : selectedSection?.id === id || hoveredSection?.id === id
232 | ? "solid"
233 | : "dashed",
234 | borderBottomColor:
235 | dropHighlight?.id === id &&
236 | (dropHighlight.position === "bottom" ||
237 | dropHighlight.position === "all")
238 | ? "white"
239 | : selectedSection?.id === id || hoveredSection?.id === id
240 | ? colorBright
241 | : colorDark,
242 | borderBottomStyle:
243 | dropHighlight?.id === id &&
244 | (dropHighlight.position === "bottom" ||
245 | dropHighlight.position === "all")
246 | ? "solid"
247 | : selectedSection?.id === id || hoveredSection?.id === id
248 | ? "solid"
249 | : "dashed",
250 | borderLeftColor:
251 | dropHighlight?.id === id &&
252 | (dropHighlight.position === "left" || dropHighlight.position === "all")
253 | ? "white"
254 | : selectedSection?.id === id || hoveredSection?.id === id
255 | ? colorBright
256 | : colorDark,
257 | borderLeftStyle:
258 | dropHighlight?.id === id &&
259 | (dropHighlight.position === "left" || dropHighlight.position === "all")
260 | ? "solid"
261 | : selectedSection?.id === id || hoveredSection?.id === id
262 | ? "solid"
263 | : "dashed",
264 | borderRightColor:
265 | dropHighlight?.id === id &&
266 | (dropHighlight.position === "right" || dropHighlight.position === "all")
267 | ? "white"
268 | : selectedSection?.id === id || hoveredSection?.id === id
269 | ? colorBright
270 | : colorDark,
271 | borderRightStyle:
272 | dropHighlight?.id === id &&
273 | (dropHighlight.position === "right" || dropHighlight.position === "all")
274 | ? "solid"
275 | : selectedSection?.id === id || hoveredSection?.id === id
276 | ? "solid"
277 | : "dashed",
278 | }),
279 | [selectedSection, hoveredSection, dropHighlight]
280 | );
281 |
282 | const isBottom = () => {
283 | return ref?.current?.getBoundingClientRect().top < 25;
284 | };
285 |
286 | const stylesNotEditable = {
287 | ...style,
288 | ...(node.style ? checkAndReturnStyles(node) : {}),
289 | cursor:
290 | !isPreview && hoveredSection?.id === id && node.tagName !== "body"
291 | ? "move"
292 | : "default",
293 | opacity,
294 | ...(!isPreview ? borderStyles : {}),
295 | ...(backgroundImage ? { backgroundImage: `url(${backgroundImage})` } : {}),
296 | ...(backgroundImage ? { backgroundSize: "cover" } : {}),
297 | ...(!children && isCanContainsChildren(node.tagName)
298 | ? { height: !className?.includes("h-") && !isPreview ? "30px" : "" }
299 | : {}),
300 | zIndex: selectedSection?.id === id && isBottom() ? 2 : 1,
301 | };
302 |
303 | const isInner = () => {
304 | if (node.tagName === "body")
305 | return ref?.current?.getBoundingClientRect().bottom -
306 | windowFrame?.innerHeight <
307 | 25
308 | ? true
309 | : false;
310 | };
311 |
312 | const onDoubleClick = () => {
313 | if (node.tagName === "img") dispatch(openModal("imageSource"));
314 | };
315 |
316 | return isEditable && node.content ? (
317 |
344 | {!isPreview && (
345 |
346 | )}
347 |
setIsCanEdit(false)}
356 | onClick={(e) => {
357 | dispatch(setSelectedSection(node));
358 | setIsCanEdit(true);
359 | }}
360 | options={{
361 | toolbar: { buttons: ["bold", "italic", "underline", "anchor"] },
362 | contentWindow: windowFrame,
363 | ownerDocument: windowFrame.document,
364 | elementsContainer: windowFrame.document.body,
365 | }}
366 | className="w-full block"
367 | onChange={(text) => dispatch(updateText(id, text))}
368 | tag={getEditableTagName(node.tagName)}
369 | />
370 |
371 | ) : children ? (
372 |
385 | {!isPreview && (
386 |
387 | )}
388 | {children}
389 |
390 | ) : (
391 |
410 | {!isPreview && (
411 |
412 | )}
413 |
421 | {children}
422 |
423 |
424 | );
425 | };
426 |
--------------------------------------------------------------------------------
/src/components/Canvas/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect } from "react";
2 | import {
3 | moveNode,
4 | setSelectedSection,
5 | setHoveredSection,
6 | setBackward,
7 | setForward,
8 | removeNode,
9 | save
10 | } from "../../redux/data-reducer";
11 | import { useDispatch, useSelector } from "react-redux";
12 | import { closeAllModals } from "../../redux/modals-reducer";
13 | import { Card } from "./Card";
14 |
15 | const Canvas = ({ windowFrame }) => {
16 | const dispatch = useDispatch();
17 | const { dom, selectedSection } = useSelector((state) => state.data);
18 | const { config } = useSelector((state) => state.data);
19 |
20 | useEffect(()=>{
21 | if(config?.apiURL) dispatch(save())
22 | }, [dom])
23 |
24 | useEffect(() => {
25 | window.addEventListener("keydown", onKeyDown);
26 |
27 | return () => window.removeEventListener("keydown", onKeyDown);
28 | }, []);
29 |
30 | const onKeyDown = (e) => {
31 | const evtobj = window.event ? e : e;
32 | if (evtobj.keyCode == 90 && evtobj.ctrlKey) dispatch(setBackward());
33 | if (evtobj.keyCode == 89 && evtobj.ctrlKey) dispatch(setForward());
34 | if (evtobj.keyCode == 27) {
35 | dispatch(closeAllModals());
36 | dispatch(setSelectedSection(null));
37 | }
38 | if (evtobj.keyCode == 46) dispatch(removeNode());
39 | };
40 |
41 | const moveCard = useCallback((dragId, hoverId, node) => {
42 | dispatch(moveNode(dragId, hoverId, node));
43 | }, []);
44 |
45 | const renderCard = useCallback(
46 | (node, index) => {
47 | return !node.isHidden && node.children?.length && !node.content ? (
48 |
55 | {node.children.map((n, i) => renderCard(n, i))}
56 |
57 | ) : (
58 | !node.isHidden && (
59 |
67 | )
68 | );
69 | },
70 | [selectedSection]
71 | );
72 |
73 | const onCanvasEnter = () => {
74 | dispatch(setHoveredSection(null));
75 | };
76 |
77 | const onCanvasClick = (e) => {
78 | if (e.target.id === "canvas") dispatch(setSelectedSection(null));
79 | };
80 |
81 | return (
82 |
88 |
89 | {dom?.map((item, i) => renderCard(item, i))}
90 |
91 |
92 | );
93 | };
94 |
95 | export default Canvas;
96 |
--------------------------------------------------------------------------------
/src/components/CollapseMenu/CollapseMenu.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../src/styles/variables.scss";
2 |
3 | .root {
4 | display: block;
5 | }
6 |
7 | .toggler {
8 | display: flex;
9 | align-items: center;
10 | }
11 |
12 | .icon {
13 | font-size: 0.4rem;
14 | }
--------------------------------------------------------------------------------
/src/components/CollapseMenu/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./CollapseMenu.module.scss";
3 | import { useCollapse } from "react-collapsed";
4 | import { IconTriangle } from "../Icons";
5 |
6 | const CollapseMenu = ({ children, title }) => {
7 | const { getCollapseProps, getToggleProps, isExpanded } = useCollapse(
8 | {
9 | defaultExpanded: true,
10 | }
11 | );
12 |
13 | return (
14 |
15 |
23 |
26 |
27 | );
28 | };
29 |
30 | export default CollapseMenu;
31 |
--------------------------------------------------------------------------------
/src/components/Header/Header.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../src/styles/variables.scss";
2 |
3 | .root {
4 | width: 100%;
5 | height: 100%;
6 | display: flex;
7 | justify-content: space-between;
8 | }
9 |
10 | .sidebarActionsContainer {
11 | width: $sidebar-width;
12 | height: 100%;
13 | }
14 |
15 | .mainActions {
16 | width: calc(100% - #{$sidebar-width});
17 | display: flex;
18 | align-items: center;
19 | justify-content: space-between;
20 | padding-right: 1rem;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/Header/MainActions/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | IconDownload,
4 | IconCode,
5 | IconEye,
6 | IconArrowLeft,
7 | IconArrowRight,
8 | } from "../../Icons";
9 | import { Button } from "../../Buttons";
10 | import { useDispatch, useSelector } from "react-redux";
11 | import { setIsPreview } from "../../../redux/layout-reducer";
12 | import { openModal } from "../../../redux/modals-reducer";
13 | import { setBackward, setForward } from "../../../redux/data-reducer";
14 |
15 | const MainActions = () => {
16 | const { isPreview } = useSelector((state) => state.layout);
17 | const { past, future } = useSelector((state) => state.data);
18 | const dispatch = useDispatch();
19 |
20 | return (
21 |
22 |
29 |
36 |
42 |
49 |
52 |
53 | );
54 | };
55 |
56 | export default MainActions;
57 |
--------------------------------------------------------------------------------
/src/components/Header/ResponsiveActions/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { IconDisplay, IconLaptop, IconPhone, IconTablet } from "../../Icons";
3 | import { Button } from "../../Buttons";
4 | import { useDispatch, useSelector } from "react-redux";
5 | import { setResponsiveView } from "../../../redux/layout-reducer";
6 |
7 | const responsiveButtons = [
8 | { name: "sm", icon: },
9 | { name: "md", icon: },
10 | { name: "lg", icon: },
11 | { name: "xl", icon: },
12 | ];
13 |
14 | const ResponsiveActions = () => {
15 | const { responsiveView } = useSelector((state) => state.layout);
16 | const dispatch = useDispatch();
17 |
18 | return (
19 |
20 | {responsiveButtons.map((button, i) => (
21 |
30 | ))}
31 |
32 | );
33 | };
34 |
35 | export default ResponsiveActions;
36 |
--------------------------------------------------------------------------------
/src/components/Header/SidebarActions/SidebarActions.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../src/styles/variables.scss";
2 |
3 | .root {
4 | width: 100%;
5 | height: 100%;
6 | display: flex;
7 | align-items: stretch;
8 | justify-content: space-between;
9 | font-size: 1.4rem;
10 | }
11 |
12 | .plus {
13 | font-size: 0.875rem;
14 |
15 | svg {
16 | transform: rotate(-45deg);
17 | }
18 | }
--------------------------------------------------------------------------------
/src/components/Header/SidebarActions/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./SidebarActions.module.scss";
3 | import { IconList, IconLayers, IconSettings, IconClose } from "../../Icons";
4 | import { Button } from "../../Buttons";
5 | import { useDispatch, useSelector } from "react-redux";
6 | import { setActiveTab } from "../../../redux/layout-reducer";
7 |
8 | export const sidebarTabs = [
9 | { label: "Style manager", id: "style-manager", icon: },
10 | { label: "Layers", id: "layers", icon: },
11 | { label: "Settings", id: "settings", icon: },
12 | { label: "Blocks", id: "blocks", icon: , style: styles.plus },
13 | ];
14 |
15 | const Header = () => {
16 | const { activeTab } = useSelector((state) => state.layout);
17 | const dispatch = useDispatch();
18 |
19 | return (
20 |
21 | {sidebarTabs.map((tab, i) => (
22 |
31 | ))}
32 |
33 | );
34 | };
35 |
36 | export default Header;
37 |
--------------------------------------------------------------------------------
/src/components/Header/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Header.module.scss";
3 | import SidebarActions from "./SidebarActions";
4 | import ResponsiveActions from "./ResponsiveActions";
5 | import MainActions from "./MainActions";
6 | import { useSelector } from "react-redux";
7 | import { IconArrowLeftShort } from "../Icons";
8 | import { Button } from "../Buttons";
9 |
10 | const Header = () => {
11 | const { config } = useSelector((state) => state.data);
12 |
13 | return (
14 |
15 |
16 | {config?.redirectURL ? (
17 |
18 |
24 |
25 |
26 | ) : (
27 |
28 | )}
29 |
30 |
31 |
32 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Header;
39 |
--------------------------------------------------------------------------------
/src/components/Icons/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export const IconList = (props) => (
4 |
24 | );
25 |
26 | export const IconLayers = (props) => (
27 |
39 | );
40 |
41 | export const IconSettings = (props) => (
42 |
54 | );
55 |
56 | export const IconClose = (props) => (
57 |
72 | );
73 |
74 | export const IconChevronDown = (props) => (
75 |
87 | );
88 |
89 | export const IconChevronUp = (props) => (
90 |
102 | );
103 |
104 | export const IconTriangle = (props) => (
105 |
117 | );
118 |
119 | export const IconTextLeft = (props) => (
120 |
135 | );
136 |
137 | export const IconTextRight = (props) => (
138 |
153 | );
154 |
155 | export const IconTextCenter = (props) => (
156 |
171 | );
172 |
173 | export const IconTextJustify = (props) => (
174 |
189 | );
190 |
191 | export const IconPlus = (props) => (
192 |
208 | );
209 |
210 | export const IconDisplay = (props) => (
211 |
223 | );
224 |
225 | export const IconLaptop = (props) => (
226 |
246 | );
247 |
248 | export const IconPhone = (props) => (
249 |
262 | );
263 |
264 | export const IconTablet = (props) => (
265 |
278 | );
279 |
280 | export const IconDownload = (props) => (
281 |
294 | );
295 |
296 | export const IconEye = (props) => (
297 |
310 | );
311 |
312 | export const IconCode = (props) => (
313 |
325 | );
326 |
327 | export const IconArrowLeft = (props) => (
328 |
343 | );
344 |
345 | export const IconArrowRight = (props) => (
346 |
361 | );
362 |
363 | export const IconEyeSlash = (props) => (
364 |
378 | );
379 |
380 | export const IconMove = (props) => (
381 |
396 | );
397 |
398 | export const IconArrowLeftShort = (props) => (
399 |
414 | );
415 |
--------------------------------------------------------------------------------
/src/components/Inputs/Input/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { setEnableRemove } from "../../../redux/data-reducer";
4 |
5 | const Input = ({ className, onFocus, onBlur, ...rest }) => {
6 | const dispatch = useDispatch();
7 |
8 | const onFocusInner = (e) => {
9 | if(onFocus) onFocus(e)
10 | dispatch(setEnableRemove(false))
11 | };
12 |
13 | const onBlurInner = () => {
14 | if(onBlur) onBlur(e)
15 | dispatch(setEnableRemove(true))
16 | };
17 |
18 | return (
19 |
27 | );
28 | };
29 |
30 | export default Input;
31 |
--------------------------------------------------------------------------------
/src/components/Inputs/Label/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Label = ({ className, children, ...rest }) => {
4 | return (
5 |
11 | {children}
12 |
13 | );
14 | };
15 |
16 | export default Label;
17 |
--------------------------------------------------------------------------------
/src/components/Inputs/Select/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from "react";
2 | import Select, { components } from "react-select";
3 | import { IconTriangle } from "../../Icons";
4 | import Label from "../Label";
5 |
6 | const colors = require("tailwindcss/colors");
7 |
8 | const SelectComp = (props) => {
9 | const { label, isDefault, isColor, isSimpleColor, ...rest } = props;
10 |
11 | const customStyles = useMemo(
12 | () => ({
13 | control: (base, state) => ({
14 | ...base,
15 | background: colors.slate[600],
16 | borderRadius: state.isFocused ? "8px 8px 0 0" : 8,
17 | borderColor: "transparent",
18 | boxShadow: state.isFocused ? null : null,
19 | color: colors.slate[200],
20 | "&:hover": {
21 | borderColor: state.isFocused ? colors.slate[400] : colors.slate[500],
22 | },
23 | }),
24 | menu: (base) => ({
25 | ...base,
26 | borderRadius: 0,
27 | marginTop: 0,
28 | }),
29 | input: (base) => ({
30 | ...base,
31 | color: colors.slate[200],
32 | paddingLeft: "0.5rem",
33 | }),
34 | singleValue: (base, { data }) => ({
35 | ...base,
36 | color: isDefault ? colors.slate[400] : colors.slate[200],
37 | paddingLeft: isColor && data.value !== "none" ? 0 : "0.5rem",
38 | paddingRight: "0.5rem",
39 | fontSize: "0.875rem"
40 | }),
41 | placeholder: (base) => ({
42 | ...base,
43 | color: colors.slate[400],
44 | paddingLeft: "0.5rem",
45 | }),
46 | multiValue: (base) => ({
47 | ...base,
48 | color: isDefault ? colors.slate[400] : colors.slate[200],
49 | paddingLeft: "0.5rem",
50 | paddingRight: "0.5rem",
51 | fontSize: "0.875rem"
52 | }),
53 | menuList: (base) => ({
54 | ...base,
55 | padding: 0,
56 | background: colors.slate[600],
57 | fontSize: "0.875rem",
58 | overflowX: "hidden"
59 | }),
60 | option: (base, { isFocused, isSelected, data }) => ({
61 | ...base,
62 | background: isFocused
63 | ? colors.slate[500]
64 | : isSelected
65 | ? colors.slate[500]
66 | : undefined,
67 | color: colors.slate[200],
68 | zIndex: 1,
69 | padding: isSimpleColor ? "8px 9px" : "8px 12px",
70 | width: isSimpleColor ? "26px" : "100%"
71 | }),
72 | indicatorsContainer: (base) => ({
73 | ...base,
74 | marginRight: "0.5rem",
75 | }),
76 | }),
77 | [isDefault, isSimpleColor, isColor]
78 | );
79 |
80 | const DropdownIndicator = (props) => {
81 | return (
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | const SingleValue = (props) => {
89 | return (
90 |
91 |
92 | {isColor && props.data.value !== "none" && (
93 |
103 | )}
104 |
{props.data.label}
105 |
106 |
107 | );
108 | };
109 |
110 | const Option = (props) => {
111 | return (
112 |
113 |
114 | {isColor && props.data.value !== "none" && (
115 |
125 | )}
126 | {!isSimpleColor &&
{props.data.label}}
127 |
128 |
129 | );
130 | };
131 |
132 | const MenuList = (props) => {
133 | return (
134 |
135 | {props.children}
136 |
137 | );
138 | };
139 |
140 | return (
141 |
142 | {label ? (
143 |
144 | ) : (
145 | <>>
146 | )}
147 |
160 | );
161 | };
162 |
163 | export default SelectComp;
164 |
--------------------------------------------------------------------------------
/src/components/Inputs/TextArea/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { setEnableRemove } from "../../../redux/data-reducer";
4 |
5 | const TextArea = ({ className, rows, onFocus, onBlur, border, ...rest }) => {
6 | const dispatch = useDispatch();
7 |
8 | const onFocusInner = (e) => {
9 | if (onFocus) onFocus(e);
10 | dispatch(setEnableRemove(false));
11 | };
12 |
13 | const onBlurInner = () => {
14 | if (onBlur) onBlur(e);
15 | dispatch(setEnableRemove(true));
16 | };
17 |
18 | return (
19 |
28 | );
29 | };
30 |
31 | export default TextArea;
32 |
--------------------------------------------------------------------------------
/src/components/Layout/Layout.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../src/styles/variables.scss";
2 |
3 | .root {
4 | width: 100vw;
5 | height: 100vh;
6 | overflow: hidden;
7 |
8 | ::-webkit-scrollbar-track {
9 | background-color: rgba(255, 255, 255, 0.1);
10 | }
11 |
12 | ::-webkit-scrollbar {
13 | width: 6px;
14 | background-color: rgba(255, 255, 255, 0.1);
15 | }
16 |
17 | ::-webkit-scrollbar-thumb {
18 | background-color: rgba(0, 0, 0, 0.4);
19 | }
20 | }
21 |
22 | .header {
23 | height: $header-height;
24 | transition: transform $transition ease;
25 |
26 | &:global(.hide) {
27 | transform: translateY(-100%);
28 | }
29 | }
30 |
31 | .inner {
32 | height: 100%;
33 | max-height: calc(100% - #{$header-height});
34 | display: flex;
35 | align-items: stretch;
36 | transition: all $transition ease;
37 |
38 | &:global(.expand) {
39 | padding-top: 0;
40 | max-height: 100%;
41 | margin-top: -$header-height;
42 | }
43 | }
44 |
45 | .sidebar {
46 | position: absolute;
47 | top: $header-height;
48 | right: 0;
49 | width: $sidebar-width;
50 | height: calc(100vh - #{$header-height});
51 | transition: transform $transition ease;
52 |
53 | &:global(.hide) {
54 | transform: translateX($sidebar-width);
55 | }
56 | }
57 |
58 | .canvas {
59 | width: 100%;
60 | height: 100%;
61 | overflow-y: auto;
62 | padding-bottom: $breadcrumb-height;
63 | transition: all $transition ease;
64 |
65 | &:global(.expand) {
66 | padding-bottom: 0;
67 | }
68 | }
69 |
70 | .canvasContainer {
71 | width: 100%;
72 | padding-right: $sidebar-width;
73 | transition: padding $transition ease;
74 |
75 | &:global(.expand) {
76 | padding-right: 0;
77 | }
78 | }
79 |
80 | .breadcrumb {
81 | height: $breadcrumb-height;
82 | width: 100%;
83 | position: absolute;
84 | bottom: 0;
85 | left: 0;
86 | transition: transform $transition ease;
87 |
88 | &:global(.hide) {
89 | transform: translateY(100%);
90 | }
91 | }
92 |
93 | .previewToggler {
94 | position: absolute;
95 | top: 1rem;
96 | right: 1rem;
97 | font-size: 1.25rem;
98 | cursor: pointer;
99 | color: rgba($white, 0.8);
100 | transform: translateY(-3rem);
101 | transition: all $transition ease;
102 |
103 | &:global(.show) {
104 | transform: translateY(0);
105 | }
106 |
107 | &:hover {
108 | color: rgba($white, 0.5);
109 | }
110 | }
--------------------------------------------------------------------------------
/src/components/Layout/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useContext, useState, useRef } from "react";
2 | import styles from "./Layout.module.scss";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import { IconEyeSlash } from "../Icons";
5 | import { setIsPreview } from "../../redux/layout-reducer";
6 | import {
7 | removeNode,
8 | setBackward,
9 | setForward,
10 | setSelectedSection,
11 | } from "../../redux/data-reducer";
12 | import Frame, {
13 | FrameContextConsumer,
14 | FrameContext,
15 | } from "react-frame-component";
16 | import { DndContext } from "react-dnd";
17 | import { screens } from "../../configs/tailwind";
18 | import Canvas from "../Canvas";
19 | import mediumStyles from "../../styles/mediumTheme.js";
20 |
21 | const Layout = ({ slotHeader, slotSidebar, slotBreadcrumb, slotModals }) => {
22 | const { isPreview, responsiveView } = useSelector((state) => state.layout);
23 | const dispatch = useDispatch();
24 | const [isReady, setIsReady] = useState(false);
25 | const iframeRef = useRef();
26 |
27 | useEffect(() => {
28 | setTimeout(() => {
29 | setIsReady(true);
30 | }, 500);
31 | }, []);
32 |
33 | const DndFrame = ({ children }) => {
34 | const { dragDropManager } = useContext(DndContext);
35 | const { window } = useContext(FrameContext);
36 |
37 | useEffect(() => {
38 | dragDropManager.getBackend().addEventListeners(window);
39 | });
40 |
41 | return children;
42 | };
43 |
44 | const FrameBindingContext = () => (
45 |
46 | {({ document, window }) => (
47 |
48 |
49 |
50 |
51 |
52 | )}
53 |
54 | );
55 |
56 | const CanvasInner = ({ children, document, window }) => {
57 | const onKeyDown = (e) => {
58 | const evtobj = window.event ? e : e;
59 | if (evtobj.keyCode == 90 && evtobj.ctrlKey) dispatch(setBackward());
60 | if (evtobj.keyCode == 89 && evtobj.ctrlKey) dispatch(setForward());
61 | if (evtobj.keyCode == 46) dispatch(removeNode());
62 | if (evtobj.keyCode == 27) dispatch(setSelectedSection(null));
63 | };
64 |
65 | useEffect(() => {
66 | const tw = document.createElement("script");
67 | const twS = document.createElement("link");
68 | const twM = document.createElement("style");
69 | const twElm = document.querySelector("#tailwind");
70 |
71 | if (!twElm) {
72 | tw.setAttribute("src", "https://cdn.tailwindcss.com");
73 | tw.setAttribute("id", "tailwind");
74 |
75 | twS.setAttribute("rel", "stylesheet");
76 | twS.setAttribute("type", "text/css");
77 | twS.setAttribute(
78 | "href",
79 | "//cdn.jsdelivr.net/npm/medium-editor@latest/dist/css/medium-editor.min.css"
80 | );
81 |
82 | twM.innerHTML = mediumStyles;
83 |
84 | document.head.appendChild(twM);
85 | document.head.appendChild(twS);
86 | document.head.appendChild(tw);
87 | }
88 |
89 | window.addEventListener("keydown", onKeyDown);
90 |
91 | return () => window.removeEventListener("keydown", onKeyDown);
92 | }, []);
93 |
94 | return children;
95 | };
96 |
97 | return (
98 |
99 |
dispatch(setIsPreview(false))}
101 | className={`${styles.previewToggler} ${isPreview ? "show" : ""}`}
102 | >
103 |
104 |
105 |
106 | {slotHeader}
107 |
108 |
109 |
114 |
125 |
126 |
127 |
128 |
129 |
130 | {slotBreadcrumb}
131 |
132 |
133 |
134 | {slotSidebar}
135 |
136 |
137 | {slotModals}
138 |
139 | );
140 | };
141 |
142 | export default Layout;
143 |
--------------------------------------------------------------------------------
/src/components/Modals/AI.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Modal from "./Modal";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { closeModal } from "../../redux/modals-reducer";
5 | import Input from "../Inputs/Input";
6 | import Label from "../Inputs/Label";
7 | import styles from "./Modals.module.scss";
8 | import { useEffect } from "react";
9 | import { buttonSimple } from "../../styles/classes";
10 | import { tab } from "../../styles/classes";
11 | import { Configuration, OpenAIApi } from "openai";
12 | import { addToDom } from "../../redux/data-reducer";
13 | import Frame, { FrameContextConsumer } from "react-frame-component";
14 |
15 | const tabs = [{ name: "Preview" }, { name: "Code" }];
16 |
17 | const MediaLibrary = () => {
18 | const { isAI, data } = useSelector((state) => state.modals);
19 | const dispatch = useDispatch();
20 | const [prompt, setPrompt] = useState("");
21 | const [output, setOutput] = useState("");
22 | const [keyInput, setKeyInput] = useState("");
23 | const [key, setKey] = useState("");
24 | const [isLoading, setIsLoading] = useState(false);
25 | const [active, setActive] = useState(0);
26 | const [error, setError] = useState("");
27 | const [tokens, setTokens] = useState(500);
28 |
29 | useEffect(() => {
30 | if (data.prompt) setPrompt(data.prompt);
31 | }, [data]);
32 |
33 | useEffect(() => {
34 | if (error)
35 | setTimeout(() => {
36 | setError("");
37 | }, 5000);
38 | }, [error]);
39 |
40 | useEffect(() => {
41 | const AIKey = localStorage.getItem("AIKey");
42 |
43 | if (AIKey) {
44 | setKey(AIKey);
45 | setKeyInput(AIKey);
46 | }
47 | }, []);
48 |
49 | const onApplyKey = () => {
50 | setKey(keyInput);
51 | localStorage.setItem("AIKey", keyInput);
52 | };
53 |
54 | const onGenerate = () => {
55 | setIsLoading(true);
56 |
57 | const configuration = new Configuration({
58 | apiKey: key,
59 | });
60 |
61 | const openai = new OpenAIApi(configuration);
62 |
63 | if (data.isImage) {
64 | openai
65 | .createImage({
66 | prompt: prompt,
67 | response_format: "b64_json",
68 | n: 1,
69 | size: "512x512",
70 | })
71 | .then((d) => {
72 | setIsLoading(false);
73 | setOutput(`
`);
74 | })
75 | .catch((err) => {
76 | setError(err.message);
77 | console.log(err);
78 | });
79 | } else {
80 | openai
81 | .createChatCompletion({
82 | model: "gpt-4",
83 | messages: [
84 | {
85 | role: "user",
86 | content: `output TailwindCSS markup that ${prompt}. Only respond with code as plain text without code block syntax around it.`,
87 | },
88 | ],
89 | max_tokens: tokens ? tokens : 300,
90 | })
91 | .then((d) => {
92 | setIsLoading(false);
93 | setOutput(d.data.choices[0].message.content);
94 | })
95 | .catch((err) => {
96 | setError(err.message);
97 | console.log(err);
98 | });
99 | }
100 | };
101 |
102 | const onAdd = () => {
103 | dispatch(closeModal("AI"));
104 | dispatch(addToDom({ label: "AI block", content: output }));
105 | };
106 |
107 | const FrameBindingContext = () => (
108 |
109 | {({ document, window }) => (
110 |
111 | )}
112 |
113 | );
114 |
115 | const Preview = ({ document }) => {
116 | useEffect(() => {
117 | const tw = document.createElement("script");
118 | const twElm = document.querySelector("#tailwind");
119 | const twS = document.createElement("style");
120 |
121 | if (!twElm) {
122 | tw.setAttribute("src", "https://cdn.tailwindcss.com");
123 | tw.setAttribute("id", "tailwind");
124 | document.head.appendChild(tw);
125 |
126 | twS.innerHTML =
127 | "::-webkit-scrollbar-track {background-color: rgba(255, 255, 255, 0.1);} ::-webkit-scrollbar {width: 6px;background-color: rgba(255, 255, 255, 0.1);} ::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.2);}";
128 | document.head.appendChild(twS);
129 | }
130 | }, []);
131 |
132 | return (
133 |
134 | {error ? (
135 |
136 | {error}
137 |
138 | ) : (
139 | output && (
140 |
144 | )
145 | )}
146 |
147 | );
148 | };
149 |
150 | const renderTab = () => {
151 | switch (active) {
152 | case 0:
153 | return (
154 |
155 |
156 |
157 |
158 |
159 | );
160 | case 1:
161 | return (
162 |
163 | {error ? (
164 |
165 | {error}
166 |
167 | ) : (
168 | output && (
169 |
175 | )
176 | )}
177 |
178 | );
179 | }
180 | };
181 |
182 | return (
183 | dispatch(closeModal("AI"))} active={isAI}>
184 |
185 |
186 | <>
187 |
OpenAI API Key
188 |
189 | "●")
194 | .join("")}`}
195 | onChange={(e) => setKeyInput(e.target.value)}
196 | placeholder="..."
197 | className={`mt-3 mb-3 bg-slate-700`}
198 | />
199 |
205 |
206 |
207 |
Write prompt
208 |
209 | {
213 | if (e.key === "Enter" && prompt) onGenerate();
214 | }}
215 | onChange={(e) => setPrompt(e.target.value)}
216 | placeholder="Create a blue button"
217 | className={`mt-3 mb-3 bg-slate-700`}
218 | />
219 |
228 |
229 |
230 |
231 | {tabs.map((t, i) => (
232 |
243 | ))}
244 |
245 | {!data.isImage && (
246 |
247 |
248 | setTokens(e.target.value)}
251 | placeholder="Tokens"
252 | type="number"
253 | className={`mt-3 mb-3 bg-slate-700 ml-3 w-28`}
254 | />
255 |
256 | )}
257 |
258 | {renderTab()}
259 |
260 |
269 |
270 |
271 | >
272 |
273 |
274 |
275 | );
276 | };
277 |
278 | export default MediaLibrary;
279 |
--------------------------------------------------------------------------------
/src/components/Modals/Export.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import Modal from "./Modal";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { closeModal } from "../../redux/modals-reducer";
5 | import { htmlTemplate } from "../../render/template";
6 | import ReactDOMServer from "react-dom/server";
7 | import TextArea from "../Inputs/TextArea";
8 | import { checkAndReturnStyles } from "../../utils";
9 |
10 | const Export = () => {
11 | const { dom } = useSelector((state) => state.data);
12 | const { isExport } = useSelector((state) => state.modals);
13 | const dispatch = useDispatch();
14 | const [html, setHtml] = useState("");
15 |
16 | const getNodes = () => {
17 | let reactNodes = [];
18 |
19 | const checkEndReturnNode = (node) => {
20 | if (node.children?.length) {
21 | return (
22 |
29 | {node.children.map((n) => checkEndReturnNode(n))}
30 |
31 | );
32 | } else {
33 | return (
34 |
41 | {node.content && node.content}
42 |
43 | );
44 | }
45 | };
46 |
47 | dom.forEach((node) => {
48 | reactNodes.push(checkEndReturnNode(node));
49 | });
50 |
51 | return reactNodes;
52 | };
53 |
54 | useEffect(() => {
55 | const body = ReactDOMServer.renderToStaticMarkup(getNodes());
56 |
57 | let result = htmlTemplate
58 | .replace(`{Body}`, body)
59 | .replace("{Title}", "Mainland app");
60 | setHtml(result);
61 | }, [dom]);
62 |
63 | return (
64 | dispatch(closeModal("export"))} active={isExport}>
65 |
66 |
67 | );
68 | };
69 |
70 | export default Export;
71 |
--------------------------------------------------------------------------------
/src/components/Modals/ImageSource/ExternalImages.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useSelectedNode } from "../../../helpers";
3 | import { setAttribute } from "../../../redux/data-reducer";
4 | import Input from "../../Inputs/Input";
5 | import { useDispatch } from "react-redux";
6 |
7 | const ExternalImages = () => {
8 | const selectedNode = useSelectedNode();
9 | const dispatch = useDispatch();
10 |
11 | return (
12 | <>
13 | {selectedNode?.src ? "Replace image" : "Add image"}
14 |
20 | dispatch(setAttribute("src", e.target.value))
21 | }
22 | placeholder="https://example.com/images/img.png"
23 | className={`mt-3 mb-3 bg-slate-700`}
24 | />
25 | Preview
26 |
27 | {selectedNode?.src && (
28 |

32 | )}
33 |
34 | >
35 | );
36 | };
37 |
38 | export default ExternalImages;
39 |
--------------------------------------------------------------------------------
/src/components/Modals/ImageSource/UploadImage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { setAttribute, addImage } from "../../../redux/data-reducer";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { closeModal } from "../../../redux/modals-reducer";
5 | import { useDropzone } from "react-dropzone";
6 | import { toBase64 } from "../../../utils";
7 |
8 | const UploadImage = () => {
9 | const { mediaLibrary } = useSelector((state) => state.data);
10 | const onDrop = useCallback((acceptedFiles) => {
11 | acceptedFiles.map((file)=>{
12 | toBase64(file).then((data)=>{
13 | dispatch(addImage(data));
14 | })
15 | })
16 | }, []);
17 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
18 | accept: {
19 | "image/jpeg": [],
20 | "image/png": [],
21 | "image/svg": [],
22 | },
23 | onDrop,
24 | });
25 |
26 | const dispatch = useDispatch();
27 |
28 | return (
29 | <>
30 | Add image
31 |
37 |
38 | {isDragActive ? (
39 |
Drop images here ...
40 | ) : (
41 |
Drag 'n' drop images here, or click to select files
42 | )}
43 |
44 | Images
45 |
46 | {mediaLibrary?.map((image, i) => (
47 |
![]()
{
49 | dispatch(closeModal("imageSource"));
50 | dispatch(setAttribute("src", image));
51 | }}
52 | key={`mdi-${i}`}
53 | className="w-44 h-36 object-cover cursor-pointer border border-slate-500 my-3 transition hover:border-slate-200"
54 | src={image}
55 | />
56 | ))}
57 |
58 | >
59 | );
60 | };
61 |
62 | export default UploadImage;
63 |
--------------------------------------------------------------------------------
/src/components/Modals/ImageSource/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Modal from "../Modal";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { closeModal } from "../../../redux/modals-reducer";
5 | import { tab } from "../../../styles/classes";
6 | import styles from "../Modals.module.scss";
7 |
8 | import UploadImage from "./UploadImage";
9 | import ExternalImages from "./ExternalImages";
10 |
11 | const tabs = [
12 | { name: "Upload image" },
13 | { name: "Paste URL" },
14 | ];
15 |
16 | const ImageSource = () => {
17 | const { isImageSource } = useSelector((state) => state.modals);
18 | const dispatch = useDispatch();
19 | const [active, setActive] = useState(0);
20 |
21 | const renderTab = () => {
22 | switch (active) {
23 | case 0:
24 | return ;
25 | case 1:
26 | return ;
27 | }
28 | };
29 |
30 | return (
31 | dispatch(closeModal("imageSource"))}
33 | active={isImageSource}
34 | >
35 |
36 |
37 | {tabs.map((t, i) => (
38 |
47 | ))}
48 |
49 |
{renderTab()}
50 |
51 |
52 | );
53 | };
54 |
55 | export default ImageSource;
56 |
--------------------------------------------------------------------------------
/src/components/Modals/Import.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Modal from "./Modal";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { closeModal } from "../../redux/modals-reducer";
5 | import TextArea from "../Inputs/TextArea";
6 | import { buttonSimple } from "../../styles/classes";
7 | import { addToDom, replaceDom } from "../../redux/data-reducer";
8 | import { clearHTML } from "../../utils";
9 |
10 | const Export = () => {
11 | const { isImport } = useSelector((state) => state.modals);
12 | const dispatch = useDispatch();
13 | const [html, setHtml] = useState("");
14 |
15 | const onAdd = () => {
16 | dispatch(addToDom({ label: "Import", content: clearHTML(html) }));
17 | dispatch(closeModal("import"));
18 | };
19 |
20 | const onReplace = () => {
21 | dispatch(replaceDom({ label: "Import", content: clearHTML(html) }));
22 | dispatch(closeModal("import"));
23 | };
24 |
25 | return (
26 | dispatch(closeModal("import"))} active={isImport}>
27 | Import HTML
28 |
55 | );
56 | };
57 |
58 | export default Export;
59 |
--------------------------------------------------------------------------------
/src/components/Modals/MediaLibrary/ExternalImages.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useSelectedNode } from "../../../helpers";
3 | import { setAttribute } from "../../../redux/data-reducer";
4 | import Input from "../../Inputs/Input";
5 | import { useDispatch } from "react-redux";
6 |
7 | const ExternalImages = () => {
8 | const selectedNode = useSelectedNode();
9 | const dispatch = useDispatch();
10 |
11 | return (
12 | <>
13 | {selectedNode?.backgroundImage ? "Replace image" : "Add image"}
14 |
20 | dispatch(setAttribute("backgroundImage", e.target.value))
21 | }
22 | placeholder="https://example.com/images/img.png"
23 | className={`mt-3 mb-3 bg-slate-700`}
24 | />
25 | Preview
26 |
27 | {selectedNode?.backgroundImage && (
28 |

32 | )}
33 |
34 | >
35 | );
36 | };
37 |
38 | export default ExternalImages;
39 |
--------------------------------------------------------------------------------
/src/components/Modals/MediaLibrary/UploadImage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import { setAttribute, addImage } from "../../../redux/data-reducer";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { closeModal } from "../../../redux/modals-reducer";
5 | import { useDropzone } from "react-dropzone";
6 | import { toBase64 } from "../../../utils";
7 |
8 | const UploadImage = () => {
9 | const { mediaLibrary } = useSelector((state) => state.data);
10 | const onDrop = useCallback((acceptedFiles) => {
11 | acceptedFiles.map((file)=>{
12 | toBase64(file).then((data)=>{
13 | dispatch(addImage(data));
14 | })
15 | })
16 | }, []);
17 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
18 | accept: {
19 | "image/jpeg": [],
20 | "image/png": [],
21 | "image/svg": [],
22 | },
23 | onDrop,
24 | });
25 |
26 | const dispatch = useDispatch();
27 |
28 | return (
29 | <>
30 | Add image
31 |
37 |
38 | {isDragActive ? (
39 |
Drop images here ...
40 | ) : (
41 |
Drag 'n' drop images here, or click to select files
42 | )}
43 |
44 | Images
45 |
46 | {mediaLibrary?.map((image, i) => (
47 |
![]()
{
49 | dispatch(closeModal("mediaLibrary"));
50 | dispatch(setAttribute("backgroundImage", image));
51 | }}
52 | key={`mdi-${i}`}
53 | className="w-44 h-36 object-cover cursor-pointer border border-slate-500 my-3 transition hover:border-slate-200"
54 | src={image}
55 | />
56 | ))}
57 |
58 | >
59 | );
60 | };
61 |
62 | export default UploadImage;
63 |
--------------------------------------------------------------------------------
/src/components/Modals/MediaLibrary/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import Modal from "../Modal";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { closeModal } from "../../../redux/modals-reducer";
5 | import { tab } from "../../../styles/classes";
6 | import styles from "../Modals.module.scss";
7 |
8 |
9 | import UploadImage from "./UploadImage";
10 | import ExternalImages from "./ExternalImages";
11 |
12 | const tabs = [
13 | { name: "External Images" },
14 | { name: "Generated Images" },
15 | { name: "Upload an image" },
16 | { name: "Browse Unsplash" },
17 | ];
18 |
19 | const MediaLibrary = () => {
20 | const { isMediaLibrary } = useSelector((state) => state.modals);
21 | const dispatch = useDispatch();
22 | const [active, setActive] = useState(0);
23 |
24 | const renderTab = () => {
25 | switch (active) {
26 | case 0:
27 | return ;
28 | case 2:
29 | return ;
30 | }
31 | };
32 |
33 | return (
34 | dispatch(closeModal("mediaLibrary"))}
36 | active={isMediaLibrary}
37 | >
38 |
39 |
40 | {tabs.map((t, i) => (
41 |
50 | ))}
51 |
52 |
{renderTab()}
53 |
54 |
55 | );
56 | };
57 |
58 | export default MediaLibrary;
59 |
--------------------------------------------------------------------------------
/src/components/Modals/Modal.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react";
2 | import styles from "./Modals.module.scss";
3 |
4 | const Modal = ({ children, active, onClose }) => {
5 | const [innerActive, setInnerActive] = useState(false);
6 | const modalContent = useRef();
7 |
8 | useEffect(() => {
9 | active
10 | ? setTimeout(() => {
11 | setInnerActive(active);
12 | }, 50)
13 | : setInnerActive(active);
14 | }, [active]);
15 |
16 | return (
17 | window.innerHeight
24 | ? "items-start"
25 | : "items-center"
26 | } justify-center`}
27 | >
28 |
window.innerHeight
34 | ? `${modalContent?.current?.offsetHeight}px`
35 | : "100vh",
36 | }}
37 | className={`w-full absolute top-0 left-0 h-full bg-slate-900 opacity-60 shrink-0`}
38 | >
39 |
43 | {children}
44 |
45 |
46 | );
47 | };
48 |
49 | export default Modal;
50 |
--------------------------------------------------------------------------------
/src/components/Modals/Modals.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../src/styles/variables.scss";
2 |
3 | .close {
4 | width: 1.3rem;
5 | height: 1.3rem;
6 | border-radius: 50%;
7 | position: absolute;
8 | top: -1.7rem;
9 | display: flex;
10 | align-items: center;
11 | justify-content: center;
12 | right: 0;
13 | font-size: 0.5rem;
14 | cursor: pointer;
15 | }
16 |
17 | .modal {
18 | position: relative;
19 | z-index: 1;
20 | width: $modal-width;
21 | }
22 |
23 | .mediaLibrary {
24 | min-height: 590px;
25 | }
26 |
27 |
28 | .AI {
29 | min-height: 650px;
30 | }
--------------------------------------------------------------------------------
/src/components/Modals/SidebarModal.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { IconClose } from "../Icons";
3 | import styles from "./Modals.module.scss";
4 |
5 | const SidebarModal = ({ active, children, onClose }) => {
6 | return (
7 |
14 |
15 |
16 |
17 | {children}
18 |
19 | );
20 | };
21 |
22 | export default SidebarModal;
23 |
--------------------------------------------------------------------------------
/src/components/Modals/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelector } from "react-redux";
3 | import MediaLibrary from "./MediaLibrary";
4 | import Export from "./Export";
5 | import AI from "./AI";
6 | import Import from "./Import";
7 | import ImageSource from "./ImageSource";
8 |
9 | const Modals = () => {
10 | const { isMediaLibrary, isImageSource, isExport, isAI, isImport } = useSelector((state) => state.modals);
11 |
12 | return (
13 | <>
14 | {isImageSource && }
15 | {isMediaLibrary && }
16 | {isExport && }
17 | {isAI && }
18 | {isImport && }
19 | >
20 | );
21 | };
22 |
23 | export default Modals;
24 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Blocks/Blocks.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../styles//variables.scss";
2 |
3 | .root {
4 | width: 100%;
5 | display: grid;
6 | grid-template-columns: repeat(2, 1fr);
7 | grid-column-gap: $gutter;
8 | grid-row-gap: $gutter;
9 | padding: $gutter;
10 |
11 | > * {
12 | width: 100%;
13 | }
14 | }
15 |
16 | .buttonBlock {
17 | height: $button-block-height;
18 | background-color: rgba($white, 0.1);
19 | padding: 1rem;
20 | border-radius: 6px;
21 | border: transparent;
22 | outline: none;
23 | display: inline-flex;
24 | justify-content: center;
25 | flex-direction: column;
26 | align-items: center;
27 | cursor: pointer;
28 | opacity: 0.6;
29 | transition: all $transition ease;
30 |
31 | svg {
32 | fill: $white;
33 | width: auto;
34 | height: $button-block-height * 0.5;
35 | margin-bottom: 0.5rem;
36 | }
37 |
38 | &:hover {
39 | background-color: rgba($white, 0.2);
40 | }
41 | }
42 |
43 | .active {
44 | opacity: 1;
45 | }
--------------------------------------------------------------------------------
/src/components/Sidebar/Blocks/ButtonBlock.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Blocks.module.scss";
3 | import { useDrag } from 'react-dnd'
4 |
5 | const ButtonBlock = (props) => {
6 | const { children, size, active, className, data, ...rest } = props;
7 |
8 | const [{ opacity }, drag] = useDrag(
9 | () => ({
10 | type:"block",
11 | item: () => {
12 | return { data };
13 | },
14 | collect: (monitor) => ({
15 | opacity: monitor.isDragging() ? 0.2 : 0.6,
16 | }),
17 | })
18 | )
19 |
20 | return (
21 |
29 | {children}
30 |
31 | );
32 | };
33 |
34 | export default ButtonBlock;
35 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Blocks/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Blocks.module.scss";
3 | import { useSelector, useDispatch } from "react-redux";
4 | import ButtonBlock from "./ButtonBlock";
5 | import { addToDom } from "../../../redux/data-reducer";
6 | import { openModal } from "../../../redux/modals-reducer";
7 |
8 | const AIBlocks = [
9 | {
10 | label: "AI Template",
11 | icon: '',
12 | },
13 | {
14 | label: "AI Headline",
15 | prompt:
16 | "one sentence headline about Tailwind, white typography, large, tag h1 and bold",
17 | icon: '',
18 | },
19 | {
20 | label: "AI Paragraph",
21 | prompt: "one random paragraph about Tailwind, 20-30 words, color white",
22 | icon: '',
23 | },
24 | {
25 | label: "AI Image",
26 | prompt: "abstract dark background",
27 | isImage: true,
28 | icon: '',
29 | },
30 | ];
31 |
32 | const StyleManager = () => {
33 | const {
34 | config: { blocks },
35 | } = useSelector((state) => state.data);
36 | const dispatch = useDispatch();
37 |
38 | const onAddSection = (block) => {
39 | dispatch(addToDom(block));
40 | };
41 |
42 | return (
43 |
44 | {AIBlocks.map((block, i) => (
45 |
47 | dispatch(
48 | openModal("AI", { prompt: block.prompt, isImage: block.isImage })
49 | )
50 | }
51 | >
52 |
53 | {block.label}
54 |
55 | ))}
56 | {blocks.map((block, i) => (
57 |
onAddSection(block)}
60 | key={`bi-${i}`}
61 | >
62 |
63 | {block.label}
64 |
65 | ))}
66 |
67 | );
68 | };
69 |
70 | export default StyleManager;
71 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Layers/Layer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useRef, useCallback } from "react";
2 | import styles from "./Layers.module.scss";
3 | import { useDrag, useDrop } from "react-dnd";
4 | import { IconEye, IconTriangle, IconMove, IconEyeSlash } from "../../Icons";
5 | import { useCollapse } from "react-collapsed";
6 | import { useDispatch, useSelector } from "react-redux";
7 | import {
8 | setHighlightLayer,
9 | moveNode,
10 | setHoveredLayer,
11 | setSelectedSection,
12 | setIsHiden,
13 | } from "../../../redux/data-reducer";
14 |
15 | const Layer = (props) => {
16 | const { children, data, active, className, ...rest } = props;
17 | const { getCollapseProps, getToggleProps, isExpanded } = useCollapse({
18 | defaultExpanded: true,
19 | });
20 | const dispatch = useDispatch();
21 | const { dropHighlightLayer, hoveredLayer, selectedSection } = useSelector(
22 | (state) => state.data
23 | );
24 | const ref = useRef(null);
25 |
26 | const [{ handlerId }, drop] = useDrop({
27 | accept: ["layer"],
28 | collect(monitor) {
29 | return {
30 | handlerId: monitor.getHandlerId(),
31 | id: data.id,
32 | };
33 | },
34 | canDrop() {
35 | return !(data.isClosed && !dropHighlightLayer);
36 | },
37 | drop(item, monitor) {
38 | if (
39 | (!item.data && !data) ||
40 | (!item.data && !hoveredLayer) ||
41 | monitor.didDrop() ||
42 | (!item.data && item.id === data.id)
43 | )
44 | return;
45 | if (!ref.current) {
46 | return;
47 | }
48 |
49 | const dragId = item.id;
50 | const hoverId = data.id;
51 |
52 | if (!(data.isClosed && !dropHighlightLayer)) {
53 | if (item.data) {
54 | const doc = new DOMParser().parseFromString(
55 | item.data.content,
56 | "text/xml"
57 | );
58 | dispatch(
59 | addToNode(htmlToJson(doc.firstChild, item.data.attributes), hoverId)
60 | );
61 | } else {
62 | moveCard(dragId, hoverId, data);
63 | }
64 | }
65 |
66 | dispatch(setHighlightLayer(null));
67 | },
68 | hover(item, monitor) {
69 | if (monitor.isOver({ shallow: true })) {
70 | highlight(monitor);
71 | }
72 | },
73 | });
74 |
75 | const [dragTargetProps, drag] = useDrag(
76 | {
77 | type: "layer",
78 | canDrag: hoveredLayer?.id === data.id && data.tagName !== "body",
79 | item: () => {
80 | return { id: data.id };
81 | },
82 | collect: (monitor) => ({
83 | isDragging: monitor.isDragging(),
84 | id: data.id,
85 | }),
86 | },
87 | [hoveredLayer, data.id]
88 | );
89 |
90 | const opacity = dragTargetProps.isDragging ? 0.2 : data.isHidden ? 0.3 : 0.6;
91 |
92 | drag(drop(ref));
93 |
94 | const highlight = (monitor) => {
95 | const hoverBoundingRect = ref.current?.getBoundingClientRect();
96 | const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
97 | const hoverMiddleX = (hoverBoundingRect.right - hoverBoundingRect.left) / 2;
98 | const clientOffset = monitor.getClientOffset();
99 |
100 | if (clientOffset) {
101 | const hoverClientY = clientOffset.y - hoverBoundingRect.top;
102 | const hoverClientX = clientOffset.x - hoverBoundingRect.left;
103 |
104 | const left = (hoverClientX * 100) / hoverMiddleX;
105 | const top = (hoverClientY * 100) / hoverMiddleY;
106 |
107 | const offset = 50;
108 |
109 | const percentages = [
110 | { position: "top", value: top < 100 && top < offset ? top : 0 },
111 | { position: "left", value: left < 100 && left < offset ? left : 0 },
112 | {
113 | position: "right",
114 | value: left > 100 && left - 100 > offset ? 100 - (left - 100) : 0,
115 | },
116 | {
117 | position: "bottom",
118 | value: top > 100 && top - 100 > offset ? 100 - (top - 100) : 0,
119 | },
120 | ];
121 |
122 | const greater = percentages.sort((a, b) => a.value - b.value).pop();
123 |
124 | greater.value > 0
125 | ? dispatch(
126 | setHighlightLayer({ id: data.id, position: greater.position })
127 | )
128 | : !data.isClosed
129 | ? dispatch(setHighlightLayer({ id: data.id, position: "all" }))
130 | : dispatch(setHighlightLayer(null));
131 | }
132 | };
133 |
134 | const moveCard = useCallback((dragId, hoverId, node) => {
135 | dispatch(moveNode(dragId, hoverId, "layer"));
136 | }, []);
137 |
138 | const colorDark = useMemo(() => "#696969", [data]);
139 | const colorBright = useMemo(
140 | () => (data.tagName === "body" ? "#696969" : "#adadad"),
141 | [data]
142 | );
143 |
144 | const borderStyles = useMemo(
145 | () => ({
146 | borderWidth: "1px",
147 | borderTopColor:
148 | dropHighlightLayer?.id === data.id &&
149 | (dropHighlightLayer.position === "top" ||
150 | dropHighlightLayer.position === "all")
151 | ? "white"
152 | : selectedSection?.id === data.id || hoveredLayer?.id === data.id
153 | ? colorBright
154 | : colorDark,
155 | borderBottomColor:
156 | dropHighlightLayer?.id === data.id &&
157 | (dropHighlightLayer.position === "bottom" ||
158 | dropHighlightLayer.position === "all")
159 | ? "white"
160 | : selectedSection?.id === data.id || hoveredLayer?.id === data.id
161 | ? colorBright
162 | : colorDark,
163 | }),
164 | [hoveredLayer, dropHighlightLayer]
165 | );
166 |
167 | const clearHightLight = () => {
168 | dispatch(setHighlightLayer(null));
169 | };
170 |
171 | const onDragLeave = () => {
172 | clearHightLight();
173 | };
174 |
175 | const onMouseEnter = (e) => {
176 | if (e.target.id === data.id) dispatch(setHoveredLayer(data));
177 | };
178 |
179 | const onMouseLeave = () => {
180 | dispatch(setHoveredLayer(null));
181 | };
182 |
183 | const onClick = (e) => {
184 | if (e.target.id === data.id) dispatch(setSelectedSection(data));
185 | };
186 |
187 | return (
188 | <>
189 |
203 |
204 |
dispatch(setIsHiden(data.id, !data.isHidden))}
206 | className="mr-3"
207 | >
208 | {data.isHidden ? : }
209 |
210 | {data.children?.length > 0 && (
211 |
217 | )}
218 |
219 | {data.label ? `${data.label} (${data.tagName})` : data.tagName}
220 |
221 |
222 |
223 | {data.children?.length > 0 && (
224 | {data.children?.length}
225 | )}
226 |
227 |
228 |
229 | {children && (
230 |
234 | {children}
235 |
236 | )}
237 | >
238 | );
239 | };
240 |
241 | export default Layer;
242 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Layers/Layers.module.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 | .icon {
7 | font-size: 0.5rem;
8 | outline: none;
9 | }
--------------------------------------------------------------------------------
/src/components/Sidebar/Layers/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Layers.module.scss";
3 | import { useSelector } from "react-redux";
4 | import Layer from "./Layer";
5 |
6 | const Layers = () => {
7 | const { dom } = useSelector((state) => state.data);
8 |
9 | const renderNode = (node, i) => {
10 | if (node.children) {
11 | return (
12 |
13 | {node.children.map((n, z) => renderNode(n, `${i}-${z}`))}
14 |
15 | );
16 | } else {
17 | return ;
18 | }
19 | };
20 |
21 | return (
22 |
23 | {dom.map((node, i) => renderNode(node, i))}
24 |
25 | );
26 | };
27 |
28 | export default Layers;
29 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Settings/Settings.module.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 100%;
3 | }
--------------------------------------------------------------------------------
/src/components/Sidebar/Settings/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Settings.module.scss";
3 | import CollapseMenu from "../../CollapseMenu";
4 | import PropertySelector from "../../StyleManager/PropertySelector";
5 | import TagSelector from "../../StyleManager/TagSelector";
6 | import { isTagVariants } from "../../../utils";
7 | import { useSelectedNode } from "../../../helpers";
8 | import SrcSelector from "../../StyleManager/SrcSelector";
9 |
10 | const StyleManager = () => {
11 | const selectedNode = useSelectedNode();
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {selectedNode?.tagName === "a" && (
22 |
23 |
24 |
25 | )}
26 | {selectedNode?.tagName === "img" && (
27 |
28 |
29 |
30 |
31 | )}
32 | {selectedNode?.tagName === "iframe" && (
33 |
34 |
35 |
36 |
37 |
38 | )}
39 | {isTagVariants(selectedNode?.tagName) && (
40 |
41 |
42 |
43 | )}
44 |
45 | );
46 | };
47 |
48 | export default StyleManager;
49 |
--------------------------------------------------------------------------------
/src/components/Sidebar/Sidebar.module.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 100%;
3 | height: 100%;
4 | overflow-y: auto;
5 | -ms-overflow-style: none;
6 | scrollbar-width: none;
7 |
8 | &::-webkit-scrollbar {
9 | display: none;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Background/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 | import ImageSelector from "../../../StyleManager/ImageSelector";
4 |
5 | const Background = () => {
6 | return (
7 |
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default Background;
15 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Borders/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import BordersSelector from "../../../StyleManager/BordersSelector";
3 |
4 | const Borders = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default Borders;
13 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/BoxShadow/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import Select from "../../../Inputs/Select";
3 | import { useSelectedNode, useShadowProps } from "../../../../helpers";
4 | import {
5 | combinedColors,
6 | opacityValues,
7 | shadowLengthValues,
8 | shadowBlurValues,
9 | } from "../../../../configs/tailwind";
10 | import hexToRgba from "hex-to-rgba";
11 | import { useDispatch, useSelector } from "react-redux";
12 | import { setAttribute } from "../../../../redux/data-reducer";
13 | import {
14 | clearShadowClassNames,
15 | rgba2hex,
16 | getColorNameByValue,
17 | getResponsivePrefix,
18 | } from "../../../../utils";
19 |
20 | const values = shadowLengthValues.map((c) => ({ value: c, label: c }));
21 | const valuesOpacity = opacityValues.map((c) => ({ value: c, label: c }));
22 | const valuesBlur = shadowBlurValues.map((c) => ({ value: c, label: c }));
23 |
24 | let colors = [];
25 |
26 | Object.keys(combinedColors).map((c) => {
27 | if (typeof combinedColors[c] === "string") {
28 | colors.push({
29 | value: combinedColors[c],
30 | label: c,
31 | color: combinedColors[c],
32 | });
33 | } else {
34 | colors.push({
35 | value: combinedColors[c][500],
36 | label: `${c}-${500}`,
37 | color: combinedColors[c][500],
38 | });
39 | }
40 | });
41 |
42 | const BoxShadow = () => {
43 | const selectedNode = useSelectedNode();
44 | const [isDefault, setIsDefault] = useState(false);
45 | const [lengthH, setLengthH] = useState(null);
46 | const [lengthV, setLengthV] = useState(null);
47 | const [blur, setBlur] = useState(null);
48 | const [spread, setSpread] = useState(null);
49 | const [opacity, setOpacity] = useState(null);
50 | const [color, setColor] = useState(null);
51 | const dispatch = useDispatch();
52 | const {
53 | shadowHorizontalLength,
54 | shadowVerticalLength,
55 | shadowBlur,
56 | shadowSpread,
57 | shadowColor,
58 | } = useShadowProps();
59 | const { responsiveView } = useSelector((state) => state.layout);
60 |
61 | useEffect(() => {
62 | if (shadowHorizontalLength) {
63 | const color = rgba2hex(shadowColor);
64 | setLengthH({
65 | value: shadowHorizontalLength,
66 | label: shadowHorizontalLength,
67 | });
68 | setLengthV({ value: shadowVerticalLength, label: shadowVerticalLength });
69 | setBlur({ value: shadowBlur, label: shadowBlur });
70 | setSpread({ value: shadowSpread, label: shadowSpread });
71 | setOpacity({ value: color.opacity, label: color.opacity });
72 | setColor({
73 | value: color.color,
74 | label: getColorNameByValue(combinedColors, color.color),
75 | color: color.color,
76 | });
77 | } else {
78 | setIsDefault(true);
79 | setLengthH(null);
80 | setLengthV(null);
81 | setBlur(null);
82 | setSpread(null);
83 | setOpacity(null);
84 | setColor(null);
85 | }
86 | }, [
87 | selectedNode,
88 | shadowHorizontalLength,
89 | shadowVerticalLength,
90 | shadowBlur,
91 | shadowSpread,
92 | shadowColor,
93 | responsiveView,
94 | ]);
95 |
96 | useEffect(() => {
97 | const c = shadowColor ? rgba2hex(shadowColor) : {};
98 |
99 | if (
100 | color &&
101 | opacity &&
102 | spread &&
103 | blur &&
104 | lengthV &&
105 | lengthH &&
106 | spread.value != shadowSpread &&
107 | blur.value != shadowBlur &&
108 | lengthV.value != shadowVerticalLength &&
109 | lengthH.value != shadowHorizontalLength &&
110 | color.value != c.color &&
111 | opacity.value != c.opacity
112 | ) {
113 | const className = `${getResponsivePrefix(responsiveView)}shadow-[${
114 | lengthH.value
115 | }_${lengthV.value}_${blur.value}_${spread.value}_${hexToRgba(
116 | color.value,
117 | opacity.value
118 | ).replaceAll(" ", ``)}]`;
119 |
120 | if (
121 | !selectedNode?.className?.includes(
122 | `${getResponsivePrefix(responsiveView)}${className}`
123 | )
124 | ) {
125 | const clearClassNames = clearShadowClassNames(
126 | selectedNode?.className,
127 | getResponsivePrefix(responsiveView)
128 | );
129 |
130 | dispatch(
131 | setAttribute(
132 | "className",
133 | `${
134 | selectedNode?.className
135 | ? `${
136 | clearClassNames
137 | ? `${clearClassNames} ${className}`
138 | : `${className}`
139 | }`
140 | : className
141 | }`
142 | )
143 | );
144 | }
145 | }
146 | }, [color, opacity, spread, blur, lengthV, lengthH]);
147 |
148 | return (
149 |
150 |
151 |
159 |
160 |
161 |
169 |
170 |
171 |
179 |
180 |
181 |
189 |
190 |
191 |
199 |
200 |
201 |
210 |
211 |
212 | );
213 | };
214 |
215 | export default BoxShadow;
216 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Classes/Classes.module.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 100%;
3 | }
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Classes/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Classes.module.scss";
3 | import { useDispatch } from "react-redux";
4 | import TextArea from "../../../Inputs/TextArea";
5 | import { setAttribute } from "../../../../redux/data-reducer";
6 | import { useSelectedNode } from "../../../../helpers";
7 |
8 | const Classes = () => {
9 | const selectedNode = useSelectedNode()
10 | const dispatch = useDispatch();
11 |
12 | return (
13 |
14 |
24 | );
25 | };
26 |
27 | export default Classes;
28 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Effects/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 |
4 | const Effects = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | };
14 |
15 | export default Effects;
16 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Flex/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 | import RangeSelector from "../../../StyleManager/RangeSelector";
4 |
5 | const Flex = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Flex;
21 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/FlexChild/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 |
4 | const FlexChild = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default FlexChild;
13 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Grid/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 | import RangeSelector from "../../../StyleManager/RangeSelector";
4 |
5 | const Flex = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default Flex;
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Layout/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 |
4 | const Layout = () => {
5 |
6 | return (
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default Layout;
14 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Position/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 |
4 | const Position = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default Position;
14 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Size/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 |
4 | const Size = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | export default Size;
19 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Spacing/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import SpacingSelector from "../../../StyleManager/SpacingSelector";
3 |
4 | const Spacing = () => {
5 | return (
6 |
7 |
8 |
9 | );
10 | };
11 |
12 | export default Spacing;
13 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/StyleManager.module.scss:
--------------------------------------------------------------------------------
1 | .root {
2 | width: 100%;
3 | }
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/Typography/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ClassSelector from "../../../StyleManager/ClassSelector";
3 | import AlignSelector from "../../../StyleManager/AlignSelector";
4 |
5 | const Typography = () => {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default Typography;
21 |
--------------------------------------------------------------------------------
/src/components/Sidebar/StyleManager/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useSelectedLayout, useParentLayout } from "../../../helpers";
3 | import styles from "./StyleManager.module.scss";
4 | import CollapseMenu from "../../CollapseMenu";
5 | import Classes from "./Classes";
6 | import Layout from "./Layout";
7 | import Spacing from "./Spacing";
8 | import Size from "./Size";
9 | import Position from "./Position";
10 | import Typography from "./Typography";
11 | import Background from "./Background";
12 | import Effects from "./Effects";
13 | import BoxShadow from "./BoxShadow";
14 | import Borders from "./Borders";
15 | import Flex from "./Flex";
16 | import FlexChild from "./FlexChild";
17 | import Grid from "./Grid";
18 |
19 | const StyleManager = () => {
20 | const selectedLayout = useSelectedLayout();
21 | const parentNodeLayout = useParentLayout();
22 |
23 | return (
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {selectedLayout === "grid" && (
32 |
33 |
34 |
35 | )}
36 | {parentNodeLayout === "flex" && (
37 |
38 |
39 |
40 | )}
41 | {selectedLayout === "flex" && (
42 |
43 |
44 |
45 | )}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | export default StyleManager;
75 |
--------------------------------------------------------------------------------
/src/components/Sidebar/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./Sidebar.module.scss";
3 | import { useSelector } from "react-redux";
4 | import Blocks from "./Blocks";
5 | import Layers from "./Layers";
6 | import Settings from "./Settings";
7 | import StyleManager from "./StyleManager";
8 |
9 | const Sidebar = () => {
10 | const { activeTab } = useSelector((state) => state.layout);
11 |
12 | const renderTab = () => {
13 | switch (activeTab) {
14 | case "style-manager":
15 | return ;
16 | case "blocks":
17 | return ;
18 | case "layers":
19 | return ;
20 | case "settings":
21 | return ;
22 | }
23 | };
24 |
25 | return (
26 | {renderTab()}
27 | );
28 | };
29 |
30 | export default Sidebar;
31 |
--------------------------------------------------------------------------------
/src/components/StyleManager/AlignSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { setAttribute } from "../../../redux/data-reducer";
4 | import { useSelectedNode } from "../../../helpers";
5 | import { clearClassNames, getResponsivePrefix } from "../../../utils";
6 | import { classes } from "../../../configs/tailwind";
7 | import {
8 | IconTextLeft,
9 | IconTextRight,
10 | IconTextCenter,
11 | IconTextJustify,
12 | } from "../../Icons";
13 |
14 | const AlignSelector = ({ title, name, isColor }) => {
15 | const dispatch = useDispatch();
16 | const selectedNode = useSelectedNode();
17 | const { responsiveView } = useSelector((state) => state.layout);
18 |
19 | const buttons = useMemo(()=>[
20 | {
21 | name: `${getResponsivePrefix(responsiveView)}text-left`,
22 | icon: ,
23 | },
24 | {
25 | name: `${getResponsivePrefix(responsiveView)}text-center`,
26 | icon: ,
27 | },
28 | {
29 | name: `${getResponsivePrefix(responsiveView)}text-right`,
30 | icon: ,
31 | },
32 | {
33 | name: `${getResponsivePrefix(responsiveView)}text-justify`,
34 | icon: ,
35 | },
36 | ], [responsiveView]);
37 |
38 | const options = classes[name]
39 | ? classes[name].map((c) => ({
40 | value: `${getResponsivePrefix(responsiveView)}${c}`,
41 | label: `${getResponsivePrefix(responsiveView)}${c}`,
42 | ...(isColor ? { color: getColor(c) } : {}),
43 | }))
44 | : [];
45 |
46 | const isActive = (name) => {
47 | let isActive = false;
48 |
49 | selectedNode?.className?.split(" ").forEach(elm => {
50 | if(elm === name) isActive = true
51 | })
52 |
53 | return isActive
54 | };
55 |
56 | const onClick = (type) => {
57 | if (selectedNode) {
58 | let className = `${clearClassNames(
59 | selectedNode.className ? selectedNode.className : "",
60 | options.map((c) => c.value)
61 | )}`;
62 |
63 | className = `${className?.length ? `${className} ${type}` : type}`;
64 |
65 | dispatch(setAttribute("className", className));
66 | }
67 | };
68 |
69 | return (
70 |
71 |
72 | {title ? (
73 |
74 | {title}
75 |
76 | ) : (
77 | <>>
78 | )}
79 |
84 | {buttons.map((button, i) => (
85 |
96 | ))}
97 |
98 |
99 |
100 | );
101 | };
102 |
103 | export default AlignSelector;
104 |
--------------------------------------------------------------------------------
/src/components/StyleManager/BordersSelector/BordersSelector.module.scss:
--------------------------------------------------------------------------------
1 | $shape-size: 2rem;
2 |
3 | .root {
4 | display: flex;
5 | align-items: center;
6 | }
7 |
8 | .buttonsContainer {
9 | width: calc(#{$shape-size} * 3 - 2px);
10 | height: calc(#{$shape-size} * 3 - 2px);
11 | flex-shrink: 0;
12 | position: relative;
13 | }
14 |
15 | .button {
16 | width: 2rem;
17 | height: 2rem;
18 | position: absolute;
19 | display: flex;
20 | align-items: center;
21 | justify-content: center;
22 | cursor: pointer;
23 |
24 | &:global(.top) {
25 | left: 50%;
26 | transform: translateX(-50%);
27 | top: 0;
28 | }
29 | &:global(.bottom) {
30 | left: 50%;
31 | transform: translateX(-50%);
32 | bottom: 0;
33 | }
34 | &:global(.left) {
35 | left: 0;
36 | top: 50%;
37 | transform: translateY(-50%);
38 | }
39 | &:global(.right) {
40 | right: 0;
41 | top: 50%;
42 | transform: translateY(-50%);
43 | }
44 | &:global(.center) {
45 | left: 50%;
46 | top: 50%;
47 | transform: translateX(-50%) translateY(-50%);
48 | }
49 | }
50 |
51 | .buttonShape {
52 | width: calc(100% - 6px);
53 | height: calc(100% - 6px);
54 | }
--------------------------------------------------------------------------------
/src/components/StyleManager/BordersSelector/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styles from "./BordersSelector.module.scss";
3 |
4 | const Button = ({ position, active, ...rest }) => {
5 | const getBorderStyle = () => {
6 | switch (position) {
7 | case "center":
8 | return "border-2";
9 | case "left":
10 | return "border-l-2";
11 | case "right":
12 | return "border-r-2";
13 | case "top":
14 | return "border-t-2";
15 | case "bottom":
16 | return "border-b-2";
17 | }
18 | };
19 |
20 | return (
21 |
35 | );
36 | };
37 |
38 | export default Button;
39 |
--------------------------------------------------------------------------------
/src/components/StyleManager/BordersSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { setAttribute } from "../../../redux/data-reducer";
4 | import { useSelectedNode } from "../../../helpers";
5 | import { clearClassNames, getResponsivePrefix } from "../../../utils";
6 | import { classes } from "../../../configs/tailwind";
7 | import styles from "./BordersSelector.module.scss";
8 | import Select from "../../Inputs/Select";
9 | import Button from "./Button";
10 | import { combinedColors } from "../../../configs/tailwind";
11 | import { useBordersProps } from "../../../helpers";
12 |
13 | const buttons = [
14 | { position: "top" },
15 | { position: "bottom" },
16 | { position: "left" },
17 | { position: "right" },
18 | { position: "center" },
19 | ];
20 |
21 | const BordersSelector = () => {
22 | const dispatch = useDispatch();
23 | const selectedNode = useSelectedNode();
24 | const [style, setStyle] = useState(null);
25 | const [color, setColor] = useState(null);
26 | const [width, setWidth] = useState(null);
27 | const [active, setActive] = useState("center");
28 | const { borderWidth, borderStyle, borderColor } = useBordersProps();
29 | const { responsiveView } = useSelector((state) => state.layout);
30 |
31 | useEffect(() => {
32 | setWidth(borderWidth ? { value: borderWidth, label: borderWidth } : null);
33 | setStyle(borderStyle ? { value: borderStyle, label: borderStyle } : null);
34 | setColor(
35 | borderColor
36 | ? {
37 | value: borderColor,
38 | label: borderColor,
39 | color: getColor(borderColor),
40 | }
41 | : null
42 | );
43 | if (borderColor) setActive(getPosition(borderColor));
44 | }, [borderWidth, borderStyle, borderColor]);
45 |
46 | useEffect(() => {
47 | if (!selectedNode) setActive("center");
48 | }, [selectedNode, borderColor]);
49 |
50 | useEffect(() => {
51 | if (selectedNode && style) {
52 | let className = `${clearClassNames(
53 | selectedNode.className ? selectedNode.className : "",
54 | classes.borderStyle.map(
55 | (c) => `${getResponsivePrefix(responsiveView)}${c}`
56 | )
57 | )}`;
58 |
59 | className = `${
60 | className?.length ? `${className} ${style.value}` : style.value
61 | }`;
62 |
63 | dispatch(setAttribute("className", className));
64 | }
65 | }, [style]);
66 |
67 | useEffect(() => {
68 | if (selectedNode && width) {
69 | let className = `${clearClassNames(
70 | selectedNode.className ? selectedNode.className : "",
71 | classes.borderWidth.map(
72 | (c) => `${getResponsivePrefix(responsiveView)}${c}`
73 | )
74 | )}`;
75 |
76 | className = `${
77 | className?.length ? `${className} ${width.value}` : width.value
78 | }`;
79 |
80 | dispatch(setAttribute("className", className));
81 | }
82 | }, [width]);
83 |
84 | useEffect(() => {
85 | if (selectedNode && color) {
86 | let className = `${clearClassNames(
87 | selectedNode.className ? selectedNode.className : "",
88 | classes[getColorClass()].map(
89 | (c) => `${getResponsivePrefix(responsiveView)}${c}`
90 | )
91 | )}`;
92 |
93 | className = `${
94 | className?.length ? `${className} ${color.value}` : color.value
95 | }`;
96 |
97 | dispatch(setAttribute("className", className));
98 | }
99 | }, [color]);
100 |
101 | const getPosition = (name) => {
102 | if (name.includes("border-r")) return "right";
103 | if (name.includes("border-l")) return "left";
104 | if (name.includes("border-t")) return "top";
105 | if (name.includes("border-b")) return "bottom";
106 | return "center";
107 | };
108 |
109 | const getColorClass = () => {
110 | switch (active) {
111 | case "center":
112 | return "borderColor";
113 | case "left":
114 | return "borderLColor";
115 | case "right":
116 | return "borderRColor";
117 | case "top":
118 | return "borderTColor";
119 | case "bottom":
120 | return "borderBColor";
121 | }
122 | };
123 |
124 | const getColor = (item) => {
125 | let c = null;
126 |
127 | if (item) {
128 | const colorParts = item
129 | .replace(`${getResponsivePrefix(responsiveView)}border-t-`, "")
130 | .replace(`${getResponsivePrefix(responsiveView)}border-b-`, "")
131 | .replace(`${getResponsivePrefix(responsiveView)}border-l-`, "")
132 | .replace(`${getResponsivePrefix(responsiveView)}border-r-`, "")
133 | .replace(`${getResponsivePrefix(responsiveView)}border-x-`, "")
134 | .replace(`${getResponsivePrefix(responsiveView)}border-y-`, "")
135 | .replace(`${getResponsivePrefix(responsiveView)}border-`, "")
136 | .split("-");
137 |
138 | c =
139 | colorParts.length > 1
140 | ? combinedColors[colorParts[0]][colorParts[1]]
141 | : combinedColors[colorParts[0]];
142 | }
143 |
144 | return c;
145 | };
146 |
147 | return (
148 |
149 |
150 |
151 | {buttons.map((button, i) => (
152 |
160 |
161 |
162 |
202 |
203 | );
204 | };
205 |
206 | export default BordersSelector;
207 |
--------------------------------------------------------------------------------
/src/components/StyleManager/ClassSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useMemo } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import Select from "../../Inputs/Select";
4 | import { setAttribute } from "../../../redux/data-reducer";
5 | import { useSelectedNode } from "../../../helpers";
6 | import { clearClassNames, getResponsivePrefix } from "../../../utils";
7 | import { classes, combinedColors } from "../../../configs/tailwind";
8 |
9 | const ClassSelector = ({ title, name, defaultValue, isColor }) => {
10 | const [selectedOption, setSelectedOption] = useState({
11 | value: defaultValue,
12 | label: defaultValue,
13 | });
14 | const [isDefault, setIsDefault] = useState(true);
15 | const dispatch = useDispatch();
16 | const selectedNode = useSelectedNode();
17 | const { responsiveView } = useSelector((state) => state.layout);
18 |
19 | const getColor = (name) => {
20 | if (name) {
21 | const parts = name.split("-");
22 | return parts.length > 2
23 | ? combinedColors[parts[1]][parts[2]]
24 | : combinedColors[parts[1]];
25 | }
26 | };
27 |
28 | const options = useMemo(
29 | () => [
30 | ...(classes[name]
31 | ? classes[name].map((c) => ({
32 | value: `${getResponsivePrefix(responsiveView)}${c}`,
33 | label: `${getResponsivePrefix(responsiveView)}${c}`,
34 | ...(isColor ? { color: getColor(c) } : {}),
35 | }))
36 | : []),
37 | ],
38 | [responsiveView, classes, name]
39 | );
40 |
41 | useEffect(() => {
42 | if (selectedNode) {
43 | if (selectedNode?.className) {
44 | let option = null;
45 | selectedNode?.className?.split(" ").map((c) => {
46 | const index = options.map((c) => c.value).indexOf(c);
47 | if (index !== -1) option = options[index];
48 | });
49 |
50 | if (option) {
51 | setSelectedOption(option);
52 | setIsDefault(false);
53 | } else {
54 | setSelectedOption({ value: defaultValue, label: defaultValue });
55 | setIsDefault(true);
56 | }
57 | } else {
58 | setSelectedOption({ value: defaultValue, label: defaultValue });
59 | setIsDefault(true);
60 | }
61 | }
62 | }, [selectedNode, responsiveView]);
63 |
64 | const onChange = (e) => {
65 | setSelectedOption(e);
66 |
67 | let className = `${clearClassNames(
68 | selectedNode.className ? selectedNode.className : "",
69 | options.map((c) => c.value)
70 | )}`;
71 |
72 | className = `${className?.length ? `${className} ${e.value}` : e.value}`;
73 |
74 | dispatch(setAttribute("className", className));
75 | };
76 |
77 | return (
78 |
79 |
89 |
90 | );
91 | };
92 |
93 | export default ClassSelector;
94 |
--------------------------------------------------------------------------------
/src/components/StyleManager/ImageSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useSelectedNode } from "../../../helpers";
3 | import { IconPlus } from "../../Icons";
4 | import { button } from "../../../styles/classes";
5 | import { openModal } from "../../../redux/modals-reducer";
6 | import { useDispatch } from "react-redux";
7 |
8 | const ImageSelector = () => {
9 | const selectedNode = useSelectedNode();
10 | const dispatch = useDispatch();
11 |
12 | return (
13 |
16 |
17 | Image
18 |
19 |
27 |
28 | );
29 | };
30 |
31 | export default ImageSelector;
32 |
--------------------------------------------------------------------------------
/src/components/StyleManager/PropertySelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { setAttribute, setSelectedSection } from "../../../redux/data-reducer";
4 | import { useSelectedNode } from "../../../helpers";
5 | import Label from "../../Inputs/Label";
6 | import Input from "../../Inputs/Input";
7 | import TextArea from "../../Inputs/TextArea";
8 |
9 | const PropertySelector = ({ property, isTextArea, label }) => {
10 | const dispatch = useDispatch();
11 | const selectedNode = useSelectedNode();
12 |
13 | return (
14 |
15 |
16 | {isTextArea ? (
17 |
50 | );
51 | };
52 |
53 | export default PropertySelector;
54 |
--------------------------------------------------------------------------------
/src/components/StyleManager/RangeSelector/RangeSelector.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../styles//variables.scss";
2 |
3 | .thumb {
4 | &::-moz-range-thumb {
5 | width: 1rem;
6 | height: 1rem;
7 | border-radius: 50%;
8 | background: $light;
9 | cursor: pointer;
10 | }
11 | &::-webkit-slider-thumb {
12 | -webkit-appearance: none;
13 | appearance: none;
14 | width: 1rem;
15 | height: 1rem;
16 | border-radius: 50%;
17 | background: $light;
18 | cursor: pointer;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/StyleManager/RangeSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useMemo } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { setAttribute } from "../../../redux/data-reducer";
4 | import { useSelectedNode } from "../../../helpers";
5 | import { clearClassNames, getResponsivePrefix } from "../../../utils";
6 | import { classes } from "../../../configs/tailwind";
7 | import Label from "../../Inputs/Label";
8 | import styles from "./RangeSelector.module.scss";
9 |
10 | const RangeSelector = ({ title, name, defaultValue }) => {
11 | const [selectedOption, setSelectedOption] = useState(-1);
12 | const [isDefault, setIsDefault] = useState(true);
13 | const dispatch = useDispatch();
14 | const selectedNode = useSelectedNode();
15 | const { responsiveView } = useSelector((state) => state.layout);
16 |
17 | const options = useMemo(
18 | () => [
19 | ...(classes[name]
20 | ? classes[name].map((c) => ({
21 | value: `${getResponsivePrefix(responsiveView)}${c}`,
22 | label: `${getResponsivePrefix(responsiveView)}${c}`,
23 | }))
24 | : []),
25 | ],
26 | [responsiveView, classes, name]
27 | );
28 |
29 | useEffect(() => {
30 | if (selectedNode) {
31 | if (selectedNode?.className) {
32 | let option = 0;
33 | selectedNode?.className?.split(" ").map((c) => {
34 | const index = options.map((c, i) => c.value).indexOf(c);
35 | if (index !== -1) option = index;
36 | });
37 |
38 | if (option) {
39 | setSelectedOption(option);
40 | setIsDefault(false);
41 | } else {
42 | setSelectedOption(-1);
43 | setIsDefault(true);
44 | }
45 | } else {
46 | setSelectedOption(-1);
47 | setIsDefault(true);
48 | }
49 | }
50 | }, [selectedNode, responsiveView]);
51 |
52 | const onChange = (e) => {
53 | setSelectedOption(e.target.value);
54 | console.log(e.target.value, options);
55 |
56 | let className = `${clearClassNames(
57 | selectedNode.className ? selectedNode.className : "",
58 | options.map((c) => c.value)
59 | )}`;
60 |
61 | if (e.target.value >= 0) {
62 | className = `${
63 | className?.length
64 | ? `${className} ${options[e.target.value].value}`
65 | : options[e.target.value].value
66 | }`;
67 | }
68 |
69 | dispatch(setAttribute("className", className));
70 | };
71 |
72 | return (
73 |
74 | {title ? : <>>}
75 |
84 |
85 | );
86 | };
87 |
88 | export default RangeSelector;
89 |
--------------------------------------------------------------------------------
/src/components/StyleManager/SpacingSelector/SpacingSelector.module.scss:
--------------------------------------------------------------------------------
1 | @import "../../../../src/styles/variables.scss";
2 |
3 | .root {
4 | width: 100%;
5 | }
6 |
7 | .button {
8 | background-color: transparent;
9 | outline: none;
10 | position: absolute;
11 | width: 20px;
12 | text-align: center;
13 |
14 | &:global(.tr-left) {
15 | transform: translateX(-50%);
16 | }
17 | &:global(.tr-top) {
18 | transform: translateY(-50%);
19 | }
20 | }
21 |
22 | .padding {
23 | position: absolute;
24 | width: calc(100% - 60px);
25 | height: calc(100% - 56px);
26 | top: 50%;
27 | left: 50%;
28 | transform: translateX(-50%) translateY(-50%);
29 | }
30 |
31 | .shape {
32 | position: absolute;
33 | top: 50%;
34 | left: 50%;
35 | transform: translateX(-50%) translateY(-50%);
36 | width: 80px;
37 | height: 10px;
38 | border-radius: 20px;
39 | }
--------------------------------------------------------------------------------
/src/components/StyleManager/SpacingSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useMemo } from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { setAttribute } from "../../../redux/data-reducer";
4 | import { useSelectedNode } from "../../../helpers";
5 | import {
6 | clearClassNames,
7 | getClassByPartOfName,
8 | clearClassNamesByPartOfName,
9 | getResponsivePrefix,
10 | } from "../../../utils";
11 | import { classes } from "../../../configs/tailwind";
12 | import styles from "./SpacingSelector.module.scss";
13 | import Select from "../../../components/Inputs/Select";
14 | import SidebarModal from "../../Modals/SidebarModal";
15 |
16 | const cls = {
17 | button:
18 | "focus:bg-slate-500 hover:bg-slate-500 rounded transition text-slate-200 text-sm",
19 | };
20 |
21 | const SpacingSelector = () => {
22 | const dispatch = useDispatch();
23 | const selectedNode = useSelectedNode();
24 | const [active, setActive] = useState(null);
25 | const [selectedOption, setSelectedOption] = useState({
26 | pt: null,
27 | pb: null,
28 | pl: null,
29 | pr: null,
30 | mt: null,
31 | mb: null,
32 | ml: null,
33 | mr: null,
34 | });
35 | const { responsiveView } = useSelector((state) => state.layout);
36 |
37 | const options1 = useMemo(
38 | () => [
39 | ...(active
40 | ? classes[active].map((c) => ({
41 | value: `${getResponsivePrefix(responsiveView)}${c}`,
42 | label: `${getResponsivePrefix(responsiveView)}${c}`,
43 | }))
44 | : []),
45 | ],
46 | [responsiveView, active, classes]
47 | );
48 |
49 | const options2 = useMemo(
50 | () => [
51 | ...(active
52 | ? classes[
53 | active?.includes("margin")
54 | ? active?.includes("marginLeft") ||
55 | active?.includes("marginRight")
56 | ? "marginX"
57 | : "marginY"
58 | : active?.includes("paddingLeft") ||
59 | active?.includes("paddingRight")
60 | ? "paddingX"
61 | : "paddingY"
62 | ].map((c) => ({
63 | value: `${getResponsivePrefix(responsiveView)}${c}`,
64 | label: `${getResponsivePrefix(responsiveView)}${c}`,
65 | }))
66 | : []),
67 | ],
68 | [responsiveView, active, classes]
69 | );
70 |
71 | const options3 = useMemo(
72 | () => [
73 | ...(active
74 | ? classes[active?.includes("margin") ? "margin" : "padding"].map(
75 | (c) => ({
76 | value: `${getResponsivePrefix(responsiveView)}${c}`,
77 | label: `${getResponsivePrefix(responsiveView)}${c}`,
78 | })
79 | )
80 | : []),
81 | ],
82 | [responsiveView, active, classes]
83 | );
84 |
85 | useEffect(() => {
86 | if (selectedNode) {
87 | clear();
88 | if (selectedNode?.className) {
89 | selectedNode?.className?.split(" ").map((c) => {
90 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}pt-`) === 0) {
91 | const v = c.split(`${getResponsivePrefix(responsiveView)}pt-`);
92 | setSelectedOption((c) => ({
93 | ...c,
94 | pt: v[1],
95 | }));
96 | }
97 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}pb-`) === 0) {
98 | const v = c.split(`${getResponsivePrefix(responsiveView)}pb-`);
99 | setSelectedOption((c) => ({
100 | ...c,
101 | pb: v[1],
102 | }));
103 | }
104 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}pl-`) === 0) {
105 | const v = c.split(`${getResponsivePrefix(responsiveView)}pl-`);
106 | setSelectedOption((c) => ({
107 | ...c,
108 | pl: v[1],
109 | }));
110 | }
111 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}pr-`) === 0) {
112 | const v = c.split(`${getResponsivePrefix(responsiveView)}pr-`);
113 | setSelectedOption((c) => ({
114 | ...c,
115 | pr: v[1],
116 | }));
117 | }
118 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}mt-`) === 0) {
119 | const v = c.split(`${getResponsivePrefix(responsiveView)}mt-`);
120 | setSelectedOption((c) => ({
121 | ...c,
122 | mt: v[1],
123 | }));
124 | }
125 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}mb-`) === 0) {
126 | const v = c.split(`${getResponsivePrefix(responsiveView)}mb-`);
127 | setSelectedOption((c) => ({
128 | ...c,
129 | mb: v[1],
130 | }));
131 | }
132 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}ml-`) === 0) {
133 | const v = c.split(`${getResponsivePrefix(responsiveView)}ml-`);
134 | setSelectedOption((c) => ({
135 | ...c,
136 | ml: v[1],
137 | }));
138 | }
139 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}mr-`) === 0) {
140 | const v = c.split(`${getResponsivePrefix(responsiveView)}mr-`);
141 | setSelectedOption((c) => ({
142 | ...c,
143 | mr: v[1],
144 | }));
145 | }
146 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}p-`) === 0) {
147 | const v = c.split(`${getResponsivePrefix(responsiveView)}p-`);
148 | setSelectedOption((c) => ({
149 | ...c,
150 | pt: v[1],
151 | pb: v[1],
152 | pl: v[1],
153 | pr: v[1],
154 | }));
155 | }
156 |
157 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}m-`) === 0) {
158 | const v = c.split(`${getResponsivePrefix(responsiveView)}m-`);
159 | setSelectedOption((c) => ({
160 | ...c,
161 | mt: v[1],
162 | mb: v[1],
163 | ml: v[1],
164 | mr: v[1],
165 | }));
166 | }
167 |
168 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}py-`) === 0) {
169 | const v = c.split(`${getResponsivePrefix(responsiveView)}py-`);
170 | setSelectedOption((c) => ({
171 | ...c,
172 | pt: v[1],
173 | pb: v[1],
174 | }));
175 | }
176 |
177 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}my-`) === 0) {
178 | const v = c.split(`${getResponsivePrefix(responsiveView)}my-`);
179 | setSelectedOption((c) => ({
180 | ...c,
181 | mt: v[1],
182 | mb: v[1],
183 | }));
184 | }
185 |
186 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}px-`) === 0) {
187 | const v = c.split(`${getResponsivePrefix(responsiveView)}px-`);
188 | setSelectedOption((c) => ({
189 | ...c,
190 | pl: v[1],
191 | pr: v[1],
192 | }));
193 | }
194 |
195 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}mx-`) === 0) {
196 | const v = c.split(`${getResponsivePrefix(responsiveView)}mx-`);
197 | setSelectedOption((c) => ({
198 | ...c,
199 | ml: v[1],
200 | mr: v[1],
201 | }));
202 | }
203 | });
204 | } else {
205 | clear();
206 | }
207 | } else {
208 | clear();
209 | }
210 | }, [selectedNode, responsiveView]);
211 |
212 | const clear = () =>
213 | setSelectedOption({
214 | pt: null,
215 | pb: null,
216 | pl: null,
217 | pr: null,
218 | mt: null,
219 | mb: null,
220 | ml: null,
221 | mr: null,
222 | });
223 |
224 | const onChange = (e, partOfName) => {
225 | if (selectedNode) {
226 | let className = clearClassNamesByPartOfName(
227 | selectedNode.className,
228 | partOfName
229 | );
230 |
231 | className = `${className?.length ? `${className} ${e.value}` : e.value}`;
232 |
233 | dispatch(setAttribute("className", className));
234 | }
235 | };
236 |
237 | const isMargin = () => active?.includes("margin");
238 | const removeName = () => active?.replace("margin", "").replace("padding", "");
239 |
240 | const getOption = (name) => {
241 | const className = getClassByPartOfName(
242 | `${selectedNode.className}`,
243 | `${getResponsivePrefix(responsiveView)}${name}`
244 | );
245 | return className
246 | ? {
247 | value: className,
248 | label: className,
249 | }
250 | : null;
251 | };
252 |
253 | const getSelected = (type) => {
254 | switch (type) {
255 | case "Top":
256 | return getOption(isMargin() ? "mt-" : "pt-");
257 | case "Bottom":
258 | return getOption(isMargin() ? "mb-" : "pb-");
259 | case "Left":
260 | return getOption(isMargin() ? "ml-" : "pl-");
261 | case "Right":
262 | return getOption(isMargin() ? "mr-" : "pr-");
263 | case "Y":
264 | return getOption(isMargin() ? "my-" : "py-");
265 | case "X":
266 | return getOption(isMargin() ? "mx-" : "px-");
267 | case "all":
268 | return getOption(isMargin() ? "m-" : "p-");
269 | }
270 |
271 | return null;
272 | };
273 |
274 | const XorY = () => {
275 | return active?.includes("marginLeft") ||
276 | active?.includes("marginRight") ||
277 | active?.includes("paddingLeft") ||
278 | active?.includes("paddingRight")
279 | ? "x"
280 | : "y";
281 | };
282 |
283 | return (
284 |
287 |
setActive(null)}>
288 |
289 |
290 | {active?.includes("margin") ? "Margin" : "Padding"}-{removeName()}
291 |
292 |
296 | onChange(
297 | e,
298 | isMargin()
299 | ? `${getResponsivePrefix(responsiveView)}m${
300 | removeName().toLowerCase()[0]
301 | }-`
302 | : `${getResponsivePrefix(responsiveView)}p${
303 | removeName().toLowerCase()[0]
304 | }-`
305 | )
306 | }
307 | options={options1}
308 | className="w-full"
309 | placeholder={"Select"}
310 | />
311 |
312 |
313 |
314 | {active?.includes("margin") ? "Margin" : "Padding"}{" "}
315 | {XorY().toUpperCase()}
316 | -axis
317 |
318 |
322 | onChange(
323 | e,
324 | isMargin()
325 | ? `${getResponsivePrefix(responsiveView)}m${XorY()}-`
326 | : `${getResponsivePrefix(responsiveView)}p${XorY()}-`
327 | )
328 | }
329 | options={options2}
330 | className="w-full"
331 | placeholder={"Select"}
332 | />
333 |
334 |
335 |
336 | {active?.includes("margin") ? "Margin" : "Padding"} All
337 |
338 |
342 | onChange(
343 | e,
344 | isMargin()
345 | ? `${getResponsivePrefix(responsiveView)}m-`
346 | : `${getResponsivePrefix(responsiveView)}p-`
347 | )
348 | }
349 | options={options3}
350 | className="w-full"
351 | placeholder={"Select"}
352 | />
353 |
354 |
355 |
421 |
422 | );
423 | };
424 |
425 | export default SpacingSelector;
426 |
--------------------------------------------------------------------------------
/src/components/StyleManager/SrcSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from "react";
2 | import { useDispatch } from "react-redux";
3 | import { setAttribute } from "../../../redux/data-reducer";
4 | import { useSelectedNode } from "../../../helpers";
5 | import Label from "../../Inputs/Label";
6 | import { useDropzone } from "react-dropzone";
7 | import { toBase64 } from "../../../utils";
8 |
9 | const SrcSelector = ({ property, label }) => {
10 | const dispatch = useDispatch();
11 | const selectedNode = useSelectedNode();
12 |
13 | const onDrop = useCallback((acceptedFiles) => {
14 | console.log(acceptedFiles)
15 | acceptedFiles.map((file)=>{
16 | toBase64(file).then((data)=>{
17 | dispatch(setAttribute(property, data));
18 | })
19 | })
20 | }, []);
21 | const { getRootProps, getInputProps, isDragActive } = useDropzone({
22 | accept: {
23 | "image/jpeg": [],
24 | "image/png": [],
25 | "image/svg": [],
26 | },
27 | multiple: false,
28 | onDrop,
29 | });
30 |
31 | return (
32 |
33 |
39 |
40 | {isDragActive ? (
41 |
Drop image here ...
42 | ) : (
43 |
Drag 'n' drop image here, or click to select file
44 | )}
45 |
46 |
47 | );
48 | };
49 |
50 | export default SrcSelector;
51 |
--------------------------------------------------------------------------------
/src/components/StyleManager/TagSelector/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Select from "../../Inputs/Select";
3 | import { getMoreTags } from "../../../utils";
4 | import { useSelectedNode } from "../../../helpers";
5 | import { useDispatch } from "react-redux";
6 | import { setAttribute } from "../../../redux/data-reducer";
7 |
8 | const TagSelector = () => {
9 | const selectedNode = useSelectedNode();
10 | const dispatch = useDispatch();
11 |
12 | return (
13 | selectedNode && (
14 | dispatch(setAttribute("tagName", e.value))}
18 | options={getMoreTags(selectedNode.tagName).map((item) => ({
19 | value: item,
20 | label: item,
21 | }))}
22 | />
23 | )
24 | );
25 | };
26 |
27 | export default TagSelector;
28 |
--------------------------------------------------------------------------------
/src/helpers/index.js:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { classes } from "../configs/tailwind";
3 | import { getResponsivePrefix, getResponsivePrefixes } from "../utils";
4 |
5 | const getNode = (dom, id) => {
6 | let resultNode = null;
7 | if (id) {
8 | const checkEndReturnNode = (node) => {
9 | if (node.children)
10 | node.children.forEach((n) => {
11 | n.id !== id ? checkEndReturnNode(n) : (resultNode = n);
12 | });
13 | };
14 |
15 | dom?.forEach((node) => {
16 | node.id !== id ? checkEndReturnNode(node) : (resultNode = node);
17 | });
18 | }
19 |
20 | return resultNode;
21 | };
22 |
23 | const getParentNode = (dom, id) => {
24 | let resultNode = null;
25 | if (id) {
26 | const checkEndReturnNode = (node) => {
27 | if (node.children)
28 | node.children.forEach((n) => {
29 | n.id !== id ? checkEndReturnNode(n) : (resultNode = node);
30 | });
31 | };
32 |
33 | dom?.forEach((node) => {
34 | node.id !== id ? checkEndReturnNode(node) : (resultNode = null);
35 | });
36 | }
37 |
38 | return resultNode;
39 | };
40 |
41 | export const useSelectedNode = () => {
42 | const { dom, selectedSection } = useSelector((state) => state.data);
43 | return getNode(dom, selectedSection?.id);
44 | };
45 |
46 | export const useSelectedLayout = () => {
47 | const { dom, selectedSection } = useSelector((state) => state.data);
48 | const { responsiveView } = useSelector((state) => state.layout);
49 | const node = getNode(dom, selectedSection?.id);
50 |
51 | if (node?.className) {
52 | const cls = node.className.split(" ");
53 | let result = null;
54 |
55 | getResponsivePrefixes(responsiveView).map((view) => {
56 | if (!result?.length)
57 | result = classes.display.filter(
58 | (d) => cls.indexOf(`${view}${d}`) != -1
59 | );
60 | });
61 |
62 | return result.length > 0 ? result[0] : null;
63 | } else {
64 | return null;
65 | }
66 | };
67 |
68 | export const useParentLayout = () => {
69 | const { dom, selectedSection } = useSelector((state) => state.data);
70 | const { responsiveView } = useSelector((state) => state.layout);
71 | const node = getParentNode(dom, selectedSection?.id);
72 |
73 | if (node?.className) {
74 | const cls = node.className.split(" ");
75 | let result = null;
76 |
77 | getResponsivePrefixes(responsiveView).map((view) => {
78 | if (!result?.length)
79 | result = classes.display.filter(
80 | (d) => cls.indexOf(`${view}${d}`) != -1
81 | );
82 | });
83 |
84 | return result.length > 0 ? result[0] : null;
85 | } else {
86 | return null;
87 | }
88 | };
89 |
90 | export const useShadowProps = () => {
91 | const { dom, selectedSection } = useSelector((state) => state.data);
92 | const { responsiveView } = useSelector((state) => state.layout);
93 | let resultNode = getNode(dom, selectedSection?.id);
94 | let isFound = false;
95 |
96 | resultNode?.className?.split(" ").forEach((elm) => {
97 | if (elm.indexOf(`${getResponsivePrefix(responsiveView)}shadow-[`) === 0)
98 | isFound = true;
99 | });
100 |
101 | if (isFound) {
102 | let shadowPropsString = resultNode?.className.split(
103 | `${getResponsivePrefix(responsiveView)}shadow-[`
104 | );
105 | shadowPropsString = shadowPropsString[1].split("]");
106 | shadowPropsString = shadowPropsString[0].split("_");
107 |
108 | return {
109 | shadowHorizontalLength: shadowPropsString[0],
110 | shadowVerticalLength: shadowPropsString[1],
111 | shadowBlur: shadowPropsString[2],
112 | shadowSpread: shadowPropsString[3],
113 | shadowColor: shadowPropsString[4],
114 | };
115 | } else {
116 | return {};
117 | }
118 | };
119 |
120 | export const useBordersProps = () => {
121 | const { dom, selectedSection } = useSelector((state) => state.data);
122 | const { responsiveView } = useSelector((state) => state.layout);
123 | let resultNode = getNode(dom, selectedSection?.id);
124 | let isBorder = false;
125 |
126 | const getClassName = (name) => {
127 | if (name.includes(`${getResponsivePrefix(responsiveView)}border-r-`))
128 | return "borderRColor";
129 | if (name.includes(`${getResponsivePrefix(responsiveView)}border-l-`))
130 | return "borderLColor";
131 | if (name.includes(`${getResponsivePrefix(responsiveView)}border-t-`))
132 | return "borderTColor";
133 | if (name.includes(`${getResponsivePrefix(responsiveView)}border-b-`))
134 | return "borderBColor";
135 | return "borderColor";
136 | };
137 |
138 | resultNode?.className?.split(" ").forEach((c) => {
139 | if (c.indexOf(`${getResponsivePrefix(responsiveView)}border-`) === 0)
140 | isBorder = true;
141 | });
142 |
143 | if (isBorder) {
144 | let borderWidth = null;
145 | let borderStyle = null;
146 | let borderColor = null;
147 |
148 | const cls = resultNode.className
149 | .split(" ")
150 | .filter((item) =>
151 | item.includes(`${getResponsivePrefix(responsiveView)}border-`)
152 | );
153 |
154 | classes.borderWidth.forEach((bW) => {
155 | if (cls.indexOf(`${getResponsivePrefix(responsiveView)}${bW}`) !== -1)
156 | borderWidth = `${getResponsivePrefix(responsiveView)}${bW}`;
157 | });
158 |
159 | classes.borderStyle.forEach((bS) => {
160 | if (cls.indexOf(`${getResponsivePrefix(responsiveView)}${bS}`) !== -1)
161 | borderStyle = `${getResponsivePrefix(responsiveView)}${bS}`;
162 | });
163 |
164 | classes[getClassName(resultNode?.className)].forEach((bC) => {
165 | if (cls.indexOf(`${getResponsivePrefix(responsiveView)}${bC}`) !== -1)
166 | borderColor = `${getResponsivePrefix(responsiveView)}${bC}`;
167 | });
168 |
169 | return {
170 | borderWidth: borderWidth,
171 | borderStyle: borderStyle,
172 | borderColor: borderColor,
173 | };
174 | } else {
175 | return {};
176 | }
177 | };
178 |
179 | export const useClassNames = () => {
180 | const { dom } = useSelector((state) => state.data);
181 | const { previousClassNames } = useSelector((state) => state.classes);
182 | let classNames = [];
183 |
184 | const checkEndReturnNode = (node) => {
185 | node.className?.split(" ").forEach((elm) => {
186 | if (classNames.indexOf(elm) === -1) classNames.push(elm);
187 | });
188 |
189 | if (node.children) {
190 | node.children.forEach((n) => {
191 | checkEndReturnNode(n);
192 | });
193 | }
194 | };
195 |
196 | checkEndReturnNode(dom[0]);
197 |
198 | if (JSON.stringify(classNames) !== JSON.stringify(previousClassNames)) {
199 | return { classNames };
200 | } else {
201 | return {};
202 | }
203 | };
204 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 | Mainland
10 |
11 |
12 |
13 |
14 |
15 |
16 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import App from "./App.jsx";
3 | import { createRoot } from 'react-dom/client';
4 |
5 | export const init = (config) => {
6 | const target = config ? config.target ? config.target : "#mainland-widget" : "#mainland-widget";
7 | const domNode = document.querySelector(target);
8 | const root = createRoot(domNode);
9 | root.render();
10 | }
--------------------------------------------------------------------------------
/src/redux/classes-reducer.js:
--------------------------------------------------------------------------------
1 | const SET_PREVIOUS_CLASSNAMES = "classes-reducer/SET_PREVIOUS_CLASSNAMES";
2 |
3 | const initialState = {
4 | previousClassNames: [],
5 | };
6 |
7 | const classesReducer = (state = initialState, action) => {
8 | switch (action.type) {
9 | case SET_PREVIOUS_CLASSNAMES: {
10 | return { ...state, previousClassNames: action.data };
11 | }
12 | default:
13 | return state;
14 | }
15 | };
16 |
17 | const actions = {
18 | setPreviousClassNames: (data) => ({
19 | type: SET_PREVIOUS_CLASSNAMES,
20 | data: data,
21 | }),
22 | };
23 |
24 | export const setPreviousClassNames = (data) => (dispatch) => {
25 | dispatch(actions.setPreviousClassNames(data));
26 | };
27 |
28 | export default classesReducer;
29 |
--------------------------------------------------------------------------------
/src/redux/layout-reducer.js:
--------------------------------------------------------------------------------
1 | const SET_ACTIVE_TAB = "layout-reducer/SET_ACTIVE_TAB";
2 | const SET_RESPONSIVE_VIEW = "layout-reducer/SET_RESPONSIVE_VIEW";
3 | const SET_IS_PREVIEW = "layout-reducer/SET_IS_PREVIEW";
4 |
5 | const initialState = {
6 | activeTab: "style-manager",
7 | responsiveView: "sm",
8 | isPreview: false,
9 | };
10 |
11 | const layoutReducer = (state = initialState, action) => {
12 | switch (action.type) {
13 | case SET_ACTIVE_TAB: {
14 | return { ...state, activeTab: action.data };
15 | }
16 | case SET_RESPONSIVE_VIEW: {
17 | return { ...state, responsiveView: action.data };
18 | }
19 | case SET_IS_PREVIEW: {
20 | return { ...state, isPreview: action.data };
21 | }
22 | default:
23 | return state;
24 | }
25 | };
26 |
27 | const actions = {
28 | setActiveTab: (data) => ({
29 | type: SET_ACTIVE_TAB,
30 | data: data,
31 | }),
32 | setResponsiveView: (data) => ({
33 | type: SET_RESPONSIVE_VIEW,
34 | data: data,
35 | }),
36 | setIsPreview: (data) => ({
37 | type: SET_IS_PREVIEW,
38 | data: data,
39 | }),
40 | };
41 |
42 | export const setActiveTab = (data) => (dispatch) => {
43 | dispatch(actions.setActiveTab(data));
44 | };
45 |
46 | export const setResponsiveView = (data) => (dispatch) => {
47 | dispatch(actions.setResponsiveView(data));
48 | };
49 |
50 | export const setIsPreview = (data) => (dispatch) => {
51 | dispatch(actions.setIsPreview(data));
52 | };
53 |
54 | export default layoutReducer;
55 |
--------------------------------------------------------------------------------
/src/redux/modals-reducer.js:
--------------------------------------------------------------------------------
1 | const OPEN_MODAL_MEDIA_LIBRARY = "modals-reducer/OPEN_MODAL_MEDIA_LIBRARY";
2 | const CLOSE_MODAL_MEDIA_LIBRARY = "modals-reducer/CLOSE_MODAL_MEDIA_LIBRARY";
3 | const CLOSE_ALL_MODALS = "modals-reducer/CLOSE_ALL_MODALS";
4 | const OPEN_MODAL_EXPORT = "modals-reducer/OPEN_MODAL_EXPORT";
5 | const CLOSE_MODAL_EXPORT = "modals-reducer/CLOSE_MODAL_EXPORT";
6 | const CLOSE_MODAL_AI = "modals-reducer/CLOSE_MODAL_AI";
7 | const OPEN_MODAL_AI = "modals-reducer/OPEN_MODAL_AI";
8 | const OPEN_MODAL_IMAGE_SOURCE = "modals-reducer/OPEN_MODAL_IMAGE_SOURCE";
9 | const CLOSE_MODAL_IMAGE_SOURCE = "modals-reducer/CLOSE_MODAL_IMAGE_SOURCE";
10 | const OPEN_MODAL_IMPORT = "modals-reducer/OPEN_MODAL_IMPORT";
11 | const CLOSE_MODAL_IMPORT = "modals-reducer/CLOSE_MODAL_IMPORT";
12 |
13 | const initialState = {
14 | data: {},
15 | isMediaLibrary: false,
16 | isExport: false,
17 | isAI: false,
18 | isImageSource: false,
19 | isImport: false,
20 | mediaRequestFrom: "",
21 | };
22 |
23 | const modalsReducer = (state = initialState, action) => {
24 | switch (action.type) {
25 | case OPEN_MODAL_MEDIA_LIBRARY: {
26 | return { ...state, isMediaLibrary: true, mediaRequestFrom: action.data };
27 | }
28 | case CLOSE_MODAL_MEDIA_LIBRARY: {
29 | return { ...state, isMediaLibrary: false, mediaRequestFrom: "" };
30 | }
31 | case OPEN_MODAL_EXPORT: {
32 | return { ...state, isExport: true };
33 | }
34 | case CLOSE_MODAL_EXPORT: {
35 | return { ...state, isExport: false };
36 | }
37 | case OPEN_MODAL_AI: {
38 | return { ...state, isAI: true, data: action.data };
39 | }
40 | case CLOSE_MODAL_AI: {
41 | return { ...state, isAI: false, data: {} };
42 | }
43 | case OPEN_MODAL_IMAGE_SOURCE: {
44 | return { ...state, isImageSource: true };
45 | }
46 | case CLOSE_MODAL_IMAGE_SOURCE: {
47 | return { ...state, isImageSource: false };
48 | }
49 | case OPEN_MODAL_IMPORT: {
50 | return { ...state, isImport: true };
51 | }
52 | case CLOSE_MODAL_IMPORT: {
53 | return { ...state, isImport: false };
54 | }
55 | case CLOSE_ALL_MODALS: {
56 | return { ...state, ...initialState };
57 | }
58 | default:
59 | return state;
60 | }
61 | };
62 |
63 | const actions = {
64 | openModal: (modalName, data) => {
65 | switch (modalName) {
66 | case "mediaLibrary":
67 | return {
68 | type: OPEN_MODAL_MEDIA_LIBRARY,
69 | data: data,
70 | };
71 | case "export":
72 | return {
73 | type: OPEN_MODAL_EXPORT,
74 | data: data,
75 | };
76 | case "AI":
77 | return {
78 | type: OPEN_MODAL_AI,
79 | data: data,
80 | };
81 | case "imageSource":
82 | return {
83 | type: OPEN_MODAL_IMAGE_SOURCE,
84 | data: data,
85 | };
86 | case "import":
87 | return {
88 | type: OPEN_MODAL_IMPORT,
89 | data: data,
90 | };
91 | }
92 | },
93 | closeModal: (modalName) => {
94 | switch (modalName) {
95 | case "mediaLibrary":
96 | return {
97 | type: CLOSE_MODAL_MEDIA_LIBRARY,
98 | };
99 | case "export":
100 | return {
101 | type: CLOSE_MODAL_EXPORT,
102 | };
103 | case "AI":
104 | return {
105 | type: CLOSE_MODAL_AI,
106 | };
107 | case "imageSource":
108 | return {
109 | type: CLOSE_MODAL_IMAGE_SOURCE,
110 | };
111 | case "import":
112 | return {
113 | type: CLOSE_MODAL_IMPORT,
114 | };
115 | }
116 | },
117 | closeAllModals: () => ({
118 | type: CLOSE_ALL_MODALS,
119 | }),
120 | };
121 |
122 | export const openModal = (modalName, data) => (dispatch) => {
123 | dispatch(actions.openModal(modalName, data));
124 | };
125 |
126 | export const closeModal = (modalName) => (dispatch) => {
127 | dispatch(actions.closeModal(modalName));
128 | };
129 |
130 | export const closeAllModals = () => (dispatch) => {
131 | dispatch(actions.closeAllModals());
132 | };
133 |
134 | export default modalsReducer;
135 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import {applyMiddleware, combineReducers, createStore,} from "redux"
2 | import thunkMiddleWare from "redux-thunk"
3 | import modalsReducer from "./modals-reducer"
4 | import dataReducer from "./data-reducer"
5 | import layoutReducer from "./layout-reducer"
6 | import classesReducer from "./classes-reducer"
7 |
8 | const reducers = combineReducers(
9 | {
10 | modals: modalsReducer,
11 | data: dataReducer,
12 | layout: layoutReducer,
13 | classes: classesReducer
14 | }
15 | );
16 |
17 | export const store = createStore(reducers, applyMiddleware(thunkMiddleWare));
--------------------------------------------------------------------------------
/src/render/template.js:
--------------------------------------------------------------------------------
1 | export const htmlTemplate = `
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | {Title}
10 |
11 |
14 |
15 |
16 |
17 | {Body}
18 |
19 |
20 |
21 | `
--------------------------------------------------------------------------------
/src/styles/classes.js:
--------------------------------------------------------------------------------
1 | export const button = "bg-slate-600 rounded text-slate-300 hover:bg-slate-700 hover:text-slate-200 transition flex justify-center p-2"
2 | export const tab = "uppercase mr-6 border-b-4 transition-all hover:text-slate-200 transition flex justify-center py-2"
3 | export const buttonSimple = "inline-flex ml-3 bg-slate-500 hover:bg-slate-700 text-white transition py-2 px-4 rounded focus:outline-none focus:shadow-outline"
--------------------------------------------------------------------------------
/src/styles/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | width: 100vw;
7 | overflow: hidden;
8 | height: 100vh;
9 | }
--------------------------------------------------------------------------------
/src/styles/mediumTheme.js:
--------------------------------------------------------------------------------
1 | const styles = `
2 | .medium-toolbar-arrow-under:after {
3 | border-color: #1e293b transparent transparent;
4 | top: 34px;
5 | }
6 | .medium-toolbar-arrow-over:before {
7 | border-color: transparent transparent #1e293b;
8 | top: -8px;
9 | }
10 | .medium-editor-toolbar {
11 | background-color: #1e293b;
12 | background: -webkit-linear-gradient(top, #1e293b, rgba(36, 36, 36, 0.75));
13 | background: linear-gradient(to bottom, #1e293b, rgba(36, 36, 36, 0.75));
14 | border: 1px solid #000;
15 | border-radius: 5px;
16 | box-shadow: 0 0 3px #1e293b;
17 | }
18 | .medium-editor-toolbar li button {
19 | background-color: #1e293b;
20 | padding: 8px !important;
21 | background: -webkit-linear-gradient(top, #475569, #1e293b);
22 | background: linear-gradient(to bottom, #475569, #1e293b);
23 | border: 0;
24 | border-right: 1px solid #000;
25 | border-left: 1px solid #333;
26 | border-left: 1px solid rgba(255, 255, 255, 0.1);
27 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3);
28 | color: #fff;
29 | height: 34px;
30 | min-width: 34px;
31 | -webkit-transition: all 0.2s ease-in;
32 | transition: all 0.2s ease-in;
33 | }
34 | .medium-editor-toolbar li button:hover {
35 | background-color: #000;
36 | color: rgba(255,255,255,0.5);
37 | }
38 | .medium-editor-toolbar li .medium-editor-button-first {
39 | border-bottom-left-radius: 5px;
40 | border-top-left-radius: 5px;
41 | }
42 | .medium-editor-toolbar li .medium-editor-button-last {
43 | border-bottom-right-radius: 5px;
44 | border-top-right-radius: 5px;
45 | }
46 | .medium-editor-toolbar li .medium-editor-button-active {
47 | background-color: #000;
48 | background: -webkit-linear-gradient(top, #1e293b, #1e293b);
49 | background: linear-gradient(to bottom, #1e293b, #1e293b);
50 | color: #fff;
51 | }
52 | .medium-editor-toolbar-form {
53 | background: #1e293b;
54 | border-radius: 5px;
55 | color: #999;
56 | }
57 | .medium-editor-toolbar-form .medium-editor-toolbar-input {
58 | background: #1e293b;
59 | box-sizing: border-box;
60 | color: #ccc;
61 | height: 34px;
62 | }
63 | .medium-editor-toolbar-form a {
64 | color: #fff;
65 | font-size: 18px !important;
66 | }
67 | .medium-editor-toolbar-anchor-preview {
68 | background: #1e293b;
69 | border-radius: 5px;
70 | color: #fff;
71 | }
72 | .medium-editor-placeholder:after {
73 | color: #1e293b;
74 | }
75 | `;
76 |
77 | export default styles;
78 |
--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
1 | $header-height: 50px;
2 | $sidebar-width: 300px;
3 | $breadcrumb-height: 40px;
4 |
5 | $button-sm: 44px;
6 | $button-md: 56px;
7 | $button-lg: 64px;
8 |
9 | $button-block-height: 100px;
10 |
11 | $transition: 300ms;
12 |
13 | $gutter: 15px;
14 |
15 | $container-width: 1440px;
16 |
17 | $modal-width: 800px;
18 |
19 | $white: #ffffff;
20 | $black: #272727;
21 | $light: #e2e8f0;
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import shortid from "shortid";
2 |
3 | export const closedTags = [
4 | "span",
5 | "p",
6 | "i",
7 | "h1",
8 | "h2",
9 | "h3",
10 | "h4",
11 | "h5",
12 | "h6",
13 | "li",
14 | "blockquote",
15 | ];
16 |
17 | export const htmlToJson = (node, attributes, label) => {
18 | const id = shortid.generate();
19 | let tag = {};
20 | tag["id"] = id;
21 | tag["tagName"] = node.tagName;
22 | if (label) tag["label"] = label;
23 | tag["children"] = [];
24 |
25 | if (attributes) {
26 | Object.keys(attributes).forEach((key) => {
27 | tag[key === "class" ? "className" : key] = attributes[key];
28 | });
29 | }
30 |
31 | if (closedTags.indexOf(node.tagName) >= 0) tag["isClosed"] = true;
32 |
33 | if (
34 | (node.innerHTML && !node.innerHTML.includes("<")) ||
35 | node.tagName === "span"
36 | ) {
37 | tag.content = node.textContent;
38 | } else {
39 | for (let i = 0; i < node.children.length; i++) {
40 | tag["children"].push(htmlToJson(node.children[i]));
41 | }
42 | }
43 |
44 | for (let i = 0; i < node.attributes.length; i++) {
45 | let attr = node.attributes[i];
46 | tag[attr.name === "class" ? "className" : attr.name] = attr.value;
47 | }
48 | return tag;
49 | };
50 |
51 | export const clearClassNames = (current, exclude) => {
52 | let result = current;
53 |
54 | if (result) {
55 | result = result
56 | .split(" ")
57 | .filter((c) => exclude.indexOf(c) === -1)
58 | .join(" ");
59 | }
60 |
61 | return result;
62 | };
63 |
64 | export const clearShadowClassNames = (current, prefix) => {
65 | let result = current;
66 |
67 | if (result) {
68 | result = result
69 | .split(" ")
70 | .filter((c) => c.indexOf(`${prefix}shadow-[`) !== 0)
71 | .join(" ");
72 | }
73 |
74 | return result;
75 | };
76 |
77 | export const getWordBoundsAtPosition = (str, position) => {
78 | const isSpace = (c) => /\s/.exec(c);
79 | let start = position - 1;
80 | let end = position;
81 |
82 | while (start >= 0 && !isSpace(str[start])) {
83 | start -= 1;
84 | }
85 | start = Math.max(0, start + 1);
86 |
87 | while (end < str.length && !isSpace(str[end])) {
88 | end += 1;
89 | }
90 | end = Math.max(start, end);
91 |
92 | return [start, end];
93 | };
94 |
95 | export const getDefaultDisplayClass = (tag) => {
96 | switch (tag) {
97 | case "span":
98 | return "inline";
99 | case "button":
100 | return "inline-block";
101 | default:
102 | return "block";
103 | }
104 | };
105 |
106 | export const getDefaultDisplayClassEditable = (tag) => {
107 | switch (tag) {
108 | case "span":
109 | case "button":
110 | return "inline-block";
111 | default:
112 | return "block";
113 | }
114 | };
115 |
116 | export const rgba2hex = (rgba) => {
117 | const [red, green, blue, alpha] = rgba.match(/[\d.]+/g);
118 | const color = `#${Number(red).toString(16).padStart(2, "0")}${Number(green)
119 | .toString(16)
120 | .padStart(2, "0")}${Number(blue).toString(16).padStart(2, "0")}`;
121 | const opacity = parseFloat(alpha);
122 |
123 | return {
124 | color: color,
125 | opacity: opacity,
126 | };
127 | };
128 |
129 | export const getColorNameByValue = (colors, value) => {
130 | let result = null;
131 | Object.keys(colors).forEach((ck) => {
132 | if (typeof colors[ck] === "string") {
133 | if (colors[ck] === value) result = ck;
134 | } else {
135 | Object.keys(colors[ck]).forEach((zk) => {
136 | if (colors[ck][zk] === value) result = `${ck}-${zk}`;
137 | });
138 | }
139 | });
140 |
141 | return result;
142 | };
143 |
144 | export const addStyle = (css) => {
145 | const head = document.head || document.getElementsByTagName("head")[0];
146 | const style = document.createElement("style");
147 | style.appendChild(document.createTextNode(css));
148 |
149 | head.appendChild(style);
150 | };
151 |
152 | export const getClassByPartOfName = (className, partOfName) => {
153 | let result = null;
154 |
155 | className?.split(" ").map((c) => {
156 | if (c.indexOf(partOfName) === 0) result = c;
157 | });
158 |
159 | return result;
160 | };
161 |
162 | export const clearClassNamesByPartOfName = (className, partOfName) =>
163 | className
164 | ?.split(" ")
165 | .filter((c) => c.indexOf(partOfName) !== 0)
166 | .join(" ");
167 |
168 | export const checkAndReturnStyles = (node) => {
169 | const regex = /([\w-]*)\s*:\s*([^;]*)/g;
170 | let match,
171 | properties = {};
172 | while ((match = regex.exec(node.style)))
173 | properties[match[1]] = match[2].trim();
174 |
175 | return properties;
176 | };
177 |
178 | export const getResponsivePrefix = (view) => {
179 | switch (view) {
180 | case "xl":
181 | return "xl:";
182 | case "lg":
183 | return "lg:";
184 | case "md":
185 | return "md:";
186 | case "sm":
187 | return "";
188 | }
189 | };
190 |
191 | export const getResponsivePrefixes = (view) => {
192 | switch (view) {
193 | case "xl":
194 | return ["xl:", "lg:", "md:", ""];
195 | case "lg":
196 | return ["lg:", "md:", ""];
197 | case "md":
198 | return ["md:", ""];
199 | case "sm":
200 | return [""];
201 | }
202 | };
203 |
204 | export const isCanContainsChildren = (name) => {
205 | switch (name) {
206 | case "hr":
207 | case "img":
208 | case "input":
209 | case "li":
210 | return false;
211 | default:
212 | return true;
213 | }
214 | };
215 |
216 | export const getEditableTagName = (tagName) => {
217 | switch (tagName) {
218 | case "h1":
219 | return "h1";
220 | case "h2":
221 | return "h2";
222 | case "h3":
223 | return "h3";
224 | case "h4":
225 | return "h4";
226 | case "h5":
227 | return "h5";
228 | case "h6":
229 | return "h6";
230 | case "li":
231 | return "li";
232 | case "p":
233 | return "p";
234 | default:
235 | return "span";
236 | }
237 | };
238 |
239 | export const getMoreTags = (tagName) => {
240 | switch (tagName) {
241 | case "h1":
242 | case "h2":
243 | case "h3":
244 | case "h4":
245 | case "h5":
246 | case "h6":
247 | return ["h1", "h2", "h3", "h4", "h5", "h6"];
248 | default:
249 | return [];
250 | }
251 | };
252 |
253 | export const isTagVariants = (tagName) => {
254 | switch (tagName) {
255 | case "h1":
256 | case "h2":
257 | case "h3":
258 | case "h4":
259 | case "h5":
260 | case "h6":
261 | return true;
262 | default:
263 | return false;
264 | }
265 | };
266 |
267 | export const replceSpecialCharacters = (string) => {
268 | const replaceChar = [
269 | { reg: "&", replace: "&" },
270 | { reg: "£", replace: "£" },
271 | { reg: "€", replace: "€" },
272 | { reg: "é", replace: "é" },
273 | { reg: "–", replace: "–" },
274 | { reg: "®", replace: "®" },
275 | { reg: "™", replace: "™" },
276 | { reg: "‘", replace: "‘" },
277 | { reg: "’", replace: "’" },
278 | { reg: "“", replace: "“" },
279 | { reg: "”", replace: "”" },
280 | { reg: "#", replace: "#" },
281 | { reg: "©", replace: "©" },
282 | { reg: "@", replace: "@" },
283 | { reg: "$", replace: "$" },
284 | { reg: "\\(", replace: "(" },
285 | { reg: "\\)", replace: ")" },
286 | { reg: "…", replace: "…" },
287 | { reg: "-", replace: "-" },
288 | { reg: "\\*", replace: "*" },
289 | { reg: "required", replace: 'required="true"' },
290 | //{ reg: new RegExp(`\\brequired\\b`, "g"), replace: 'required="true"' },
291 | ];
292 | let s = string;
293 | replaceChar.forEach((obj) => {
294 | s = s.replaceAll(obj.reg, obj.replace);
295 | });
296 |
297 | return s;
298 | };
299 |
300 | export const toBase64 = (file) =>
301 | new Promise((resolve, reject) => {
302 | const reader = new FileReader();
303 | reader.readAsDataURL(file);
304 | reader.onload = () => resolve(reader.result);
305 | reader.onerror = reject;
306 | });
307 |
308 | export const clearHTML = (html) => {
309 | if(html.search("/g
312 | );
313 |
314 | return result[0]
315 | }else {
316 | return html
317 | }
318 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/components/**/*.{html,jsx}",
5 | "./src/styles/**/*.{html,js}"],
6 | theme: {
7 | extend: {
8 |
9 | },
10 | },
11 | plugins: [],
12 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require("path");
2 | const CopyPlugin = require('copy-webpack-plugin');
3 |
4 | let configDev = {
5 | entry: "./src/index.js",
6 | mode: "development",
7 | module: {
8 | rules: [
9 | {
10 | test: /\.(js|jsx)$/,
11 | exclude: /(node_modules|bower_components)/,
12 | loader: "babel-loader",
13 | options: {
14 | presets: [
15 | "@babel/env",
16 | {
17 | plugins: ["@babel/plugin-proposal-class-properties"],
18 | },
19 | ],
20 | },
21 | },
22 | {
23 | test: /\.(scss)$/,
24 | use: [
25 | { loader: "style-loader" },
26 | {
27 | loader: "css-loader",
28 | options: {
29 | sourceMap: true,
30 | modules: {
31 | localIdentName: '[name]__[local]___[hash:base64:5]',
32 | },
33 | },
34 | },
35 | {
36 | loader: "sass-loader",
37 | options: {
38 | sourceMap: true,
39 | },
40 | },
41 | ],
42 | },
43 | {
44 | test: /\.(css)$/,
45 | use: [
46 | { loader: "style-loader" },
47 | { loader: "css-loader" },
48 | { loader: "postcss-loader" },
49 | ],
50 | },
51 | {
52 | test: /\.(png|jpg|gif|mp4|ogg|svg|ico)$/,
53 | use: [
54 | {
55 | loader: "file-loader",
56 | },
57 | ],
58 | },
59 | {
60 | test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
61 | use: [
62 | {
63 | loader: "file-loader",
64 | options: {
65 | name: "[name].[ext]",
66 | outputPath: "./fonts/",
67 | },
68 | },
69 | ],
70 | },
71 | ],
72 | },
73 | resolve: {
74 | extensions: [".*", ".js", ".jsx"],
75 | fallback: {
76 | crypto: false,
77 | },
78 | },
79 | output: {
80 | path: path.resolve("./dist"),
81 | publicPath: "/",
82 | filename: "mainlandJs.js",
83 | library: "mainlandJs",
84 | libraryTarget: "umd",
85 | umdNamedDefine: true,
86 | },
87 | devServer: {
88 | static: "./src/",
89 | port: 3000,
90 | historyApiFallback: true,
91 | hot: true,
92 | open: true,
93 | },
94 | };
95 |
96 | let configProd = {
97 | entry: "./src/index.js",
98 | mode: "production",
99 | module: {
100 | rules: [
101 | {
102 | test: /\.(js|jsx)$/,
103 | exclude: /(node_modules|bower_components)/,
104 | loader: "babel-loader",
105 | options: {
106 | presets: [
107 | "@babel/env",
108 | {
109 | plugins: ["@babel/plugin-proposal-class-properties"],
110 | },
111 | ],
112 | },
113 | },
114 | {
115 | test: /\.(scss)$/,
116 | use: [
117 | { loader: "style-loader" },
118 | {
119 | loader: "css-loader",
120 | options: {
121 | sourceMap: false,
122 | modules: {
123 | localIdentName: '[name]__[local]___[hash:base64:5]',
124 | },
125 | },
126 | },
127 | {
128 | loader: "sass-loader",
129 | options: {
130 | sourceMap: false,
131 | },
132 | },
133 | ],
134 | },
135 | {
136 | test: /\.(css)$/,
137 | use: [
138 | { loader: "style-loader" },
139 | { loader: "css-loader" },
140 | { loader: "postcss-loader" },
141 | ],
142 | },
143 | {
144 | test: /\.(png|jpg|gif|mp4|ogg|svg|ico)$/,
145 | use: [
146 | {
147 | loader: "file-loader",
148 | options: {
149 | name: "[name].[ext]",
150 | outputPath: "./img/",
151 | },
152 | },
153 | ],
154 | },
155 | {
156 | test: /\.(woff(2)?|ttf|eot)(\?v=\d+\.\d+\.\d+)?$/,
157 | use: [
158 | {
159 | loader: "file-loader",
160 | options: {
161 | name: "[name].[ext]",
162 | outputPath: "./fonts/",
163 | },
164 | },
165 | ],
166 | },
167 | ],
168 | },
169 | resolve: { extensions: ["*", ".js", ".jsx"] },
170 | output: {
171 | path: path.resolve("./dist"),
172 | filename: "mainlandJs.js",
173 | library: "mainlandJs",
174 | libraryTarget: "umd",
175 | umdNamedDefine: true,
176 | },
177 | plugins: [
178 | new CopyPlugin({
179 | patterns: [
180 | { from: path.resolve('src/index.html'), to: path.resolve('dist/index.html') },
181 | ],
182 | }),
183 | ]
184 | };
185 |
186 | module.exports = (env, argv) => {
187 | if (argv.mode === "development") {
188 | return configDev;
189 | }
190 |
191 | if (argv.mode === "production") {
192 | return configProd;
193 | }
194 | };
195 |
--------------------------------------------------------------------------------