├── .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 |
24 |
{children}
25 |
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 | 14 | 18 | 19 | 23 | 24 | ); 25 | 26 | export const IconLayers = (props) => ( 27 | 37 | 38 | 39 | ); 40 | 41 | export const IconSettings = (props) => ( 42 | 52 | 53 | 54 | ); 55 | 56 | export const IconClose = (props) => ( 57 | 67 | 71 | 72 | ); 73 | 74 | export const IconChevronDown = (props) => ( 75 | 85 | 86 | 87 | ); 88 | 89 | export const IconChevronUp = (props) => ( 90 | 100 | 101 | 102 | ); 103 | 104 | export const IconTriangle = (props) => ( 105 | 115 | 116 | 117 | ); 118 | 119 | export const IconTextLeft = (props) => ( 120 | 130 | 134 | 135 | ); 136 | 137 | export const IconTextRight = (props) => ( 138 | 148 | 152 | 153 | ); 154 | 155 | export const IconTextCenter = (props) => ( 156 | 166 | 170 | 171 | ); 172 | 173 | export const IconTextJustify = (props) => ( 174 | 184 | 188 | 189 | ); 190 | 191 | export const IconPlus = (props) => ( 192 | 202 | 207 | 208 | ); 209 | 210 | export const IconDisplay = (props) => ( 211 | 221 | 222 | 223 | ); 224 | 225 | export const IconLaptop = (props) => ( 226 | 236 | 240 | 244 | 245 | 246 | ); 247 | 248 | export const IconPhone = (props) => ( 249 | 259 | 260 | 261 | 262 | ); 263 | 264 | export const IconTablet = (props) => ( 265 | 275 | 276 | 277 | 278 | ); 279 | 280 | export const IconDownload = (props) => ( 281 | 291 | 292 | 293 | 294 | ); 295 | 296 | export const IconEye = (props) => ( 297 | 307 | 308 | 309 | 310 | ); 311 | 312 | export const IconCode = (props) => ( 313 | 323 | 324 | 325 | ); 326 | 327 | export const IconArrowLeft = (props) => ( 328 | 338 | 342 | 343 | ); 344 | 345 | export const IconArrowRight = (props) => ( 346 | 356 | 360 | 361 | ); 362 | 363 | export const IconEyeSlash = (props) => ( 364 | 374 | 375 | 376 | 377 | 378 | ); 379 | 380 | export const IconMove = (props) => ( 381 | 391 | 395 | 396 | ); 397 | 398 | export const IconArrowLeftShort = (props) => ( 399 | 409 | 413 | 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 | 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 |