├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── bootstrap.css ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── styles.css ├── src ├── components │ ├── BlockPreview.js │ ├── NarrowSidebar.js │ └── WideSidebar.js ├── constants │ └── actionTypes.js ├── containers │ ├── App.js │ ├── BlocksGallery.js │ ├── Inspector.js │ ├── Output.js │ ├── Preview.js │ ├── Search.js │ └── Settings.js ├── index.js ├── reducers │ ├── config.js │ ├── index.js │ └── layout.js ├── sagas │ └── index.js ├── utils │ ├── renderHandlebars.js │ ├── reportWebVitals.js │ ├── setupTests.js │ └── store.js └── views │ ├── blocks │ ├── article1.js │ ├── article2.js │ ├── gallery2.js │ ├── gallery3.js │ ├── gallery4.js │ ├── header1.js │ ├── header2.js │ ├── index.js │ └── navbar1.js │ ├── documents │ ├── document1.js │ ├── document2.js │ ├── document3.js │ ├── document4.js │ ├── document5.js │ ├── document6.js │ ├── document7.js │ └── index.js │ └── section.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .idea/ 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 pilotpirxie 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Visual Editor Logo 3 |

4 | 5 | # visual-editor 6 | Website editor built with React. Make a modern website in seconds with predefined blocks and drag and drop. 7 | 8 | Demo: https://pilotpirxie.github.io/visual-editor/ 9 | 10 | ## Features 11 | * Drag and drop editor built with React 12 | * Live preview with different responsive modes 13 | * Easily add new blocks and sections 14 | * Works offline without a backend server 15 | * Search blocks by the name or with categories 16 | * The built-in inspector and preferences editor 17 | * Easily to write blocks with handlebars syntax 18 | * Works with every CSS framework 19 | 20 |

21 | Visual Editor 22 |

23 | 24 | ## Installation 25 | ```shell script 26 | git clone 27 | yarn 28 | yarn start 29 | ``` 30 | 31 | ## License 32 | visual-editor is licensed under the MIT. 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "visual-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://pilotpirxie.github.io/visual-editor", 6 | "repository": "https://github.com/pilotpirxie/visual-editor.git", 7 | "dependencies": { 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "gh-pages": "^4.0.0", 12 | "handlebars": "^4.7.6", 13 | "prop-types": "^15.7.2", 14 | "react": "^17.0.1", 15 | "react-debounce-input": "^3.2.3", 16 | "react-dom": "^17.0.1", 17 | "react-redux": "^7.2.2", 18 | "react-router-dom": "^5.2.0", 19 | "react-scripts": "4.0.0", 20 | "redux": "^4.0.5", 21 | "redux-saga": "^1.1.3", 22 | "uuid": "^8.3.1", 23 | "web-vitals": "^0.2.4" 24 | }, 25 | "scripts": { 26 | "start": "react-scripts start", 27 | "build": "react-scripts build", 28 | "deploy" : "gh-pages -d build", 29 | "test": "react-scripts test", 30 | "eject": "react-scripts eject" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ] 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotpirxie/visual-editor/dee8fc6c8863ef6b3221b65c4c29566697edd983/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 19 | 20 | 22 | 31 | React App 32 | 33 | 34 | 35 |
36 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotpirxie/visual-editor/dee8fc6c8863ef6b3221b65c4c29566697edd983/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pilotpirxie/visual-editor/dee8fc6c8863ef6b3221b65c4c29566697edd983/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #faf8ff !important; 3 | width: 100vw; 4 | height: 100%; 5 | padding: 0; 6 | margin: 0; 7 | overflow-x: hidden; 8 | } 9 | 10 | .overflow-x-scroll { 11 | overflow-x: scroll; 12 | } 13 | 14 | .cursor-pointer { 15 | cursor: pointer; 16 | } 17 | 18 | .btn-sidebar:hover { 19 | background: #232323; 20 | } 21 | 22 | .active-button { 23 | background: #232323; 24 | color: #fff!important; 25 | } 26 | 27 | .btn-sidebar { 28 | color: #8d8d8d; 29 | padding: 0.75rem; 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | } 34 | 35 | .btn-sidebar:hover { 36 | color: #fff; 37 | } 38 | 39 | .shadow { 40 | -webkit-box-shadow: 0px 0px 22px -1px rgba(0,0,0,0.25); 41 | -moz-box-shadow: 0px 0px 22px -1px rgba(0,0,0,0.25); 42 | box-shadow: 0px 0px 22px -1px rgba(0,0,0,0.25); 43 | } 44 | 45 | 46 | .icons-wrapper { 47 | width: 4rem; 48 | min-width: 32px; 49 | overflow-y: hidden; 50 | height: 100vh; 51 | background: rgb(52, 62, 75) none repeat scroll 0% 0%; 52 | } 53 | 54 | .inspector-wrapper { 55 | overflow-y: scroll; 56 | height: 100vh; 57 | width: 600px; 58 | background: white; 59 | } 60 | 61 | .page-content-wrapper { 62 | width: 100vw; 63 | padding-top: 32px; 64 | padding-bottom: 8px; 65 | } 66 | 67 | .visual-iframe { 68 | width: 100%; 69 | flex: 1; 70 | border: none; 71 | margin: 0; 72 | padding: 0; 73 | } 74 | 75 | .preview-window { 76 | background-color: white; 77 | border: 1px solid #ddd; 78 | display: flex; 79 | flex-direction: column; 80 | } 81 | 82 | .preview-mode-0 { 83 | width: 95%; 84 | } 85 | 86 | .preview-mode-1 { 87 | width: 1200px; 88 | } 89 | 90 | .preview-mode-2 { 91 | width: 800px; 92 | } 93 | 94 | .preview-mode-3 { 95 | width: 450px; 96 | } 97 | 98 | .preview-toolbar { 99 | width: 100%; 100 | padding: 4px 16px; 101 | background-color: #f7f5fb; 102 | border-bottom: 1px solid #ddd; 103 | } 104 | 105 | .preview-toolbar-dot { 106 | font-size: 1.2rem!important; 107 | color: #e4e2ea; 108 | margin-right: 4px; 109 | cursor: default; 110 | } 111 | 112 | .btn-preview-toolbar { 113 | color: #A2A2A2; 114 | } 115 | 116 | .btn-preview-toolbar.active, 117 | .btn-preview-toolbar:hover { 118 | background-color: #e4e2ea; 119 | } 120 | 121 | .block-entry { 122 | cursor: pointer; 123 | } 124 | 125 | .block-entry > .prompt { 126 | left: 0; 127 | top: 0; 128 | width: 100%; 129 | height: 100%; 130 | margin: 0; 131 | padding: 0; 132 | position: absolute; 133 | opacity: 0; 134 | transition: 100ms linear 0s; 135 | } 136 | 137 | .block-entry > .prompt > .prompt-inside { 138 | background: rgb(52, 62, 75) none repeat scroll 0% 0%; 139 | color: white; 140 | display: flex; 141 | flex-direction: row; 142 | height: 100%; 143 | justify-content: center; 144 | align-items: center; 145 | } 146 | 147 | .block-entry:hover > .prompt { 148 | opacity: 0.95; 149 | } 150 | 151 | 152 | -------------------------------------------------------------------------------- /src/components/BlockPreview.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class BlockPreview extends Component { 5 | render() { 6 | return ( 7 |
8 | {this.props.name} 12 |
this.props.onPushBlock(this.props.blockId)}> 13 |
14 |
{this.props.name}
15 | 16 |
17 |
18 |
19 | ); 20 | } 21 | } 22 | 23 | BlockPreview.propTypes = { 24 | blockId: PropTypes.string, 25 | image: PropTypes.string, 26 | name: PropTypes.string, 27 | onPushBlock: PropTypes.func, 28 | }; 29 | 30 | export default BlockPreview; 31 | -------------------------------------------------------------------------------- /src/components/NarrowSidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export default function NarrowSidebar(props) { 5 | return
6 |
7 | 13 | 19 | 25 | 31 | 37 | 43 | 49 | 55 | 61 |
62 |
63 | 69 | 75 | 81 |
82 |
; 83 | } 84 | 85 | NarrowSidebar.propTypes = { 86 | onChangeActiveTab: PropTypes.func.isRequired 87 | }; 88 | -------------------------------------------------------------------------------- /src/components/WideSidebar.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function WideSidebar({ children }) { 4 | return
5 |
6 | {children} 7 |
8 |
; 9 | } 10 | -------------------------------------------------------------------------------- /src/constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | const actionTypes = { 2 | CHANGE_ACTIVE_TAB: 'CONFIG/CHANGE_ACTIVE_TAB', 3 | CHANGE_PREVIEW_MODE: 'CONFIG/CHANGE_PREVIEW_MODE', 4 | SET_SELECTED_BLOCK: 'LAYOUT/SET_SELECTED_BLOCK', 5 | PUSH_BLOCK: 'LAYOUT/PUSH_BLOCK', 6 | CHANGE_BLOCK_DATA: 'LAYOUT/CHANGE_BLOCK_DATA', 7 | REORDER_LAYOUT: 'LAYOUT/REORDER_LAYOUT', 8 | DELETE_BLOCK: 'LAYOUT/DELETE_BLOCK', 9 | CHANGE_DOCUMENT_ID: 'LAYOUT/CHANGE_DOCUMENT_ID', 10 | }; 11 | 12 | export default actionTypes; 13 | -------------------------------------------------------------------------------- /src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from "react-redux"; 3 | import { 4 | BrowserRouter as Router, 5 | Switch, 6 | Route, 7 | } from "react-router-dom"; 8 | 9 | import renderHandlebars from '../utils/renderHandlebars'; 10 | import NarrowSidebar from "../components/NarrowSidebar"; 11 | import WideSidebar from "../components/WideSidebar"; 12 | 13 | import Preview from "./Preview"; 14 | import BlocksGallery from "./BlocksGallery"; 15 | import Search from "./Search"; 16 | import Inspector from "./Inspector"; 17 | import Settings from "./Settings"; 18 | 19 | import actionTypes from "../constants/actionTypes"; 20 | import Output from "./Output"; 21 | 22 | class App extends React.Component { 23 | constructor(props) { 24 | super(props); 25 | 26 | this.handleChangeActiveTab = this.handleChangeActiveTab.bind(this); 27 | this.handleChangePreviewMode = this.handleChangePreviewMode.bind(this); 28 | this.handlePushBlock = this.handlePushBlock.bind(this); 29 | this.handleMessage = this.handleMessage.bind(this); 30 | this.handleSetSelectedBlock = this.handleSetSelectedBlock.bind(this); 31 | this.handleReorderLayout = this.handleReorderLayout.bind(this); 32 | } 33 | 34 | componentDidMount() { 35 | window.addEventListener("message", this.handleMessage) 36 | } 37 | 38 | componentWillUnmount() { 39 | window.removeEventListener("message", this.handleMessage) 40 | } 41 | 42 | handleMessage(event) { 43 | console.log(event.data) 44 | if (event.data.event) { 45 | if (event.data.blockId && event.data.event === 'click') { 46 | this.handleChangeActiveTab(0); 47 | this.handleSetSelectedBlock(event.data.blockId); 48 | } else if (event.data.newOrder && event.data.event === 'sorted') { 49 | this.handleReorderLayout(event.data.newOrder); 50 | } 51 | } 52 | } 53 | 54 | handleChangeActiveTab(index) { 55 | this.props.dispatch({ 56 | type: actionTypes.CHANGE_ACTIVE_TAB, 57 | index 58 | }); 59 | } 60 | 61 | handleChangePreviewMode(mode) { 62 | this.props.dispatch({ 63 | type: actionTypes.CHANGE_PREVIEW_MODE, 64 | mode 65 | }); 66 | } 67 | 68 | handlePushBlock(blockId) { 69 | this.props.dispatch({ 70 | type: actionTypes.PUSH_BLOCK, 71 | blockId 72 | }); 73 | } 74 | 75 | handleSetSelectedBlock(blockUuid) { 76 | this.props.dispatch({ 77 | type: actionTypes.SET_SELECTED_BLOCK, 78 | blockUuid 79 | }); 80 | } 81 | 82 | handleReorderLayout(newOrder) { 83 | const newBlocksLayout = []; 84 | newOrder.forEach(blockUuid => { 85 | const block = this.props.layout.blocks.find(el => { 86 | return el.uuid === blockUuid; 87 | }) 88 | newBlocksLayout.push(block); 89 | }); 90 | 91 | this.props.dispatch({ 92 | type: actionTypes.REORDER_LAYOUT, 93 | newBlocksLayout 94 | }); 95 | } 96 | 97 | render() { 98 | const innerHTML = renderHandlebars(this.props.layout.blocks, this.props.layout.documentId); 99 | const {activeTab, previewMode} = this.props.config; 100 | 101 | return ( 102 | 103 |
104 | 105 | 106 | 109 | 110 | 112 | 115 | 119 | 123 | 127 | 131 | 134 | 136 | 137 | 141 | 142 | 143 |
144 |
145 | ); 146 | } 147 | } 148 | 149 | const mapStateToProps = state => { 150 | return { 151 | config: state.config, 152 | layout: state.layout, 153 | }; 154 | }; 155 | 156 | const mapDispatchToProps = dispatch => ({ dispatch }); 157 | 158 | export default connect(mapStateToProps, mapDispatchToProps)(App); 159 | -------------------------------------------------------------------------------- /src/containers/BlocksGallery.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import BlockPreview from "../components/BlockPreview"; 4 | import blocks from '../views/blocks'; 5 | 6 | class BlocksGallery extends Component { 7 | render() { 8 | if (!this.props.display) return null; 9 | return ( 10 |
11 |
Category: {this.props.category}
12 |
13 | {Object.keys(blocks).map(blockId => { 14 | const block = blocks[blockId]; 15 | if (block.category === this.props.category) { 16 | return 22 | } else { 23 | return null; 24 | } 25 | })} 26 |
27 | ); 28 | } 29 | } 30 | 31 | BlocksGallery.propTypes = { 32 | onPushBlock: PropTypes.func, 33 | block: PropTypes.object, 34 | display: PropTypes.bool 35 | } 36 | 37 | export default BlocksGallery; 38 | -------------------------------------------------------------------------------- /src/containers/Inspector.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from "react-redux"; 4 | import {DebounceInput} from 'react-debounce-input'; 5 | import actionTypes from "../constants/actionTypes"; 6 | import blocks from "../views/blocks"; 7 | 8 | class Inspector extends Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.handleChangeBlockData = this.handleChangeBlockData.bind(this); 13 | this.handleDeleteBlock = this.handleDeleteBlock.bind(this); 14 | } 15 | 16 | handleChangeBlockData(blockUuid, key, value) { 17 | this.props.dispatch({ 18 | type: actionTypes.CHANGE_BLOCK_DATA, 19 | blockUuid, 20 | key, 21 | value 22 | }); 23 | } 24 | 25 | handleDeleteBlock(blockUuid) { 26 | this.props.dispatch({ 27 | type: actionTypes.DELETE_BLOCK, 28 | blockUuid, 29 | }); 30 | } 31 | 32 | render() { 33 | if (!this.props.display) return null; 34 | 35 | const blockUuid = this.props.layout.selectedBlockUuid; 36 | const block = this.props.layout.blocks.find(el => { 37 | return el.uuid === blockUuid; 38 | }); 39 | 40 | if (!block) return
First add and select block section
; 41 | 42 | const config = blocks[block.blockId].config; 43 | 44 | return ( 45 |
46 |
47 |
Inspector
48 | 49 |
50 |
51 | {Object.keys(config).map((el, index) => { 52 | if (config[el].type === 'string') { 53 | return
54 | 55 | this.handleChangeBlockData(blockUuid, el, e.target.value)} 62 | /> 63 |
64 | } else if (config[el].type === 'color') { 65 | return
66 | 67 | this.handleChangeBlockData(blockUuid, el, e.target.value)} 74 | /> 75 |
76 | } else if (config[el].type === 'boolean') { 77 | return
78 | 82 |
83 | } else { 84 | return null; 85 | } 86 | })} 87 |
88 | ); 89 | } 90 | } 91 | 92 | Inspector.propTypes = { 93 | layout: PropTypes.object, 94 | display: PropTypes.bool 95 | }; 96 | 97 | const mapStateToProps = state => { 98 | return { 99 | layout: state.layout, 100 | }; 101 | }; 102 | 103 | const mapDispatchToProps = dispatch => ({ dispatch }); 104 | 105 | export default connect(mapStateToProps, mapDispatchToProps)(Inspector); 106 | -------------------------------------------------------------------------------- /src/containers/Output.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import {connect} from "react-redux"; 4 | import documents from "../views/documents"; 5 | import actionTypes from "../constants/actionTypes"; 6 | 7 | class Output extends Component { 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | if (!this.props.display) return null; 14 | 15 | return ( 16 |
17 |
18 |
Export
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | } 29 | 30 | Output.propTypes = { 31 | display: PropTypes.bool, 32 | html: PropTypes.bool, 33 | }; 34 | 35 | export default Output; 36 | -------------------------------------------------------------------------------- /src/containers/Preview.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Preview(props) { 4 | return
5 |
6 |
7 |
8 | stop_circle 9 | stop_circle 10 | stop_circle 11 |
12 |
13 | 16 | 19 | 22 | 25 |
26 |
27 |