├── .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 |
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 |
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 | You need to enable JavaScript to run this app.
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 |
12 |
this.props.onPushBlock(this.props.blockId)}>
13 |
14 |
{this.props.name}
15 |
Add block
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 | props.onChangeActiveTab(0)}>
11 | edit
12 |
13 | props.onChangeActiveTab(1)}>
17 | search
18 |
19 | props.onChangeActiveTab(2)}>
23 | view_quilt
24 |
25 | props.onChangeActiveTab(3)}>
29 | subject
30 |
31 | props.onChangeActiveTab(4)}>
35 | insert_photo
36 |
37 | props.onChangeActiveTab(5)}>
41 | featured_video
42 |
43 | props.onChangeActiveTab(6)}>
47 | perm_contact_calendar
48 |
49 | props.onChangeActiveTab(7)}>
53 | table_chart
54 |
55 | props.onChangeActiveTab(8)}>
59 | view_agenda
60 |
61 |
62 |
63 | props.onChangeActiveTab(9)}>
67 | save
68 |
69 | props.onChangeActiveTab(10)}>
73 | settings
74 |
75 | props.onChangeActiveTab(11)}>
79 | help_outline
80 |
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 | this.handleDeleteBlock(blockUuid)}>Delete block
49 |
50 |
51 | {Object.keys(config).map((el, index) => {
52 | if (config[el].type === 'string') {
53 | return
54 | {config[el].name}
55 | this.handleChangeBlockData(blockUuid, el, e.target.value)}
62 | />
63 |
64 | } else if (config[el].type === 'color') {
65 | return
66 | {config[el].name}
67 | this.handleChangeBlockData(blockUuid, el, e.target.value)}
74 | />
75 |
76 | } else if (config[el].type === 'boolean') {
77 | return
78 |
79 | this.handleChangeBlockData(blockUuid, el, e.target.checked)}/>
80 | {config[el].name}
81 |
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 | Output HTML
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 | props.onChangePreviewMode(0)} className={`btn btn-sm btn-preview-toolbar d-flex align-items-center ${props.previewMode === 0 ? 'active' : ''}`}>
14 | devices
15 |
16 | props.onChangePreviewMode(1)} className={`btn btn-sm btn-preview-toolbar d-flex align-items-center ${props.previewMode === 1 ? 'active' : ''}`}>
17 | tv
18 |
19 | props.onChangePreviewMode(2)} className={`btn btn-sm btn-preview-toolbar d-flex align-items-center ${props.previewMode === 2 ? 'active' : ''}`}>
20 | tablet
21 |
22 | props.onChangePreviewMode(3)} className={`btn btn-sm btn-preview-toolbar d-flex align-items-center ${props.previewMode === 3 ? 'active' : ''}`}>
23 | smartphone
24 |
25 |
26 |
27 |
29 |
;
30 | }
31 |
--------------------------------------------------------------------------------
/src/containers/Search.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react';
2 | import blocks from '../views/blocks';
3 | import BlockPreview from "../components/BlockPreview";
4 |
5 | class Search extends Component {
6 | constructor(props) {
7 | super(props);
8 |
9 | this.state = {
10 | searchValue: ''
11 | }
12 |
13 | this.handleChangeSearchValue = this.handleChangeSearchValue.bind(this);
14 | }
15 |
16 | handleChangeSearchValue(newValue) {
17 | this.setState({
18 | searchValue: newValue
19 | });
20 | }
21 |
22 | render() {
23 | if (!this.props.display) return null;
24 | return (
25 |
26 |
this.handleChangeSearchValue(e.target.value)}/>
32 |
33 |
34 | {Object.keys(blocks).map((blockId, index) => {
35 | const block = blocks[blockId];
36 | if (this.state.searchValue !== "" && index < 10 && block.name.toLowerCase().indexOf(this.state.searchValue.toLowerCase()) > -1) {
37 | return
43 | } else {
44 | return null;
45 | }
46 | })}
47 |
48 |
49 | );
50 | }
51 | }
52 |
53 | export default Search;
54 |
--------------------------------------------------------------------------------
/src/containers/Settings.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 Settings extends Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.handleDocumentChange = this.handleDocumentChange.bind(this);
12 | }
13 |
14 | handleDocumentChange(documentId) {
15 | console.log(documentId)
16 | this.props.dispatch({
17 | type: actionTypes.CHANGE_DOCUMENT_ID,
18 | documentId
19 | });
20 | }
21 |
22 | render() {
23 | if (!this.props.display) return null;
24 |
25 | return (
26 |
27 |
28 |
Settings
29 |
30 |
31 |
32 | Document template
33 | this.handleDocumentChange(e.target.value)} value={this.props.layout.documentId}>
34 | {Object.keys(documents).map(documentId => {documents[documentId].name} )}
35 |
36 |
37 |
38 | );
39 | }
40 | }
41 |
42 | Settings.propTypes = {
43 | layout: PropTypes.object,
44 | config: PropTypes.object,
45 | display: PropTypes.bool,
46 | };
47 |
48 | const mapStateToProps = state => {
49 | return {
50 | config: state.config,
51 | layout: state.layout,
52 | };
53 | };
54 |
55 | const mapDispatchToProps = dispatch => ({ dispatch });
56 |
57 | export default connect(mapStateToProps, mapDispatchToProps)(Settings);
58 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Provider } from 'react-redux';
4 |
5 | import reportWebVitals from './utils/reportWebVitals';
6 | import store from './utils/store';
7 | import App from './containers/App';
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 |
14 | ,
15 | document.getElementById('root')
16 | );
17 |
18 | reportWebVitals();
19 |
--------------------------------------------------------------------------------
/src/reducers/config.js:
--------------------------------------------------------------------------------
1 | import actionTypes from "../constants/actionTypes";
2 |
3 | const initialState = {
4 | activeTab: 0,
5 | previewMode: 0
6 | };
7 |
8 | export default function reducer(state = initialState, action) {
9 | switch (action.type) {
10 | case actionTypes.CHANGE_ACTIVE_TAB:
11 | return {
12 | ...state,
13 | activeTab: action.index,
14 | };
15 | case actionTypes.CHANGE_PREVIEW_MODE:
16 | return {
17 | ...state,
18 | previewMode: action.mode,
19 | };
20 | default:
21 | return state;
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers} from "redux";
2 |
3 | import config from './config';
4 | import layout from './layout';
5 |
6 | export default combineReducers({
7 | config,
8 | layout
9 | });
10 |
--------------------------------------------------------------------------------
/src/reducers/layout.js:
--------------------------------------------------------------------------------
1 | import actionTypes from "../constants/actionTypes";
2 | import blocks from "../views/blocks/";
3 | import {v4 as uuidv4} from 'uuid';
4 |
5 | const initialState = {
6 | blocks: [],
7 | selectedBlockUuid: '',
8 | documentId: 'document1'
9 | };
10 |
11 | export default function reducer(state = initialState, action) {
12 | switch (action.type) {
13 | case actionTypes.PUSH_BLOCK:
14 | return {
15 | ...state,
16 | blocks: [...state.blocks, {
17 | uuid: uuidv4(),
18 | blockId: action.blockId,
19 | data: {
20 | ...blocks[action.blockId].defaultData
21 | }
22 | }]
23 | }
24 | case actionTypes.SET_SELECTED_BLOCK:
25 | return {
26 | ...state,
27 | selectedBlockUuid: action.blockUuid
28 | }
29 | case actionTypes.REORDER_LAYOUT:
30 | return {
31 | ...state,
32 | blocks: [
33 | ...action.newBlocksLayout
34 | ]
35 | }
36 | case actionTypes.CHANGE_BLOCK_DATA:
37 | const index = state.blocks.findIndex(el => {
38 | return el.uuid === action.blockUuid;
39 | });
40 | const newBlocks = [...state.blocks];
41 | newBlocks[index].data[action.key] = action.value;
42 | return {
43 | ...state,
44 | blocks: [...newBlocks]
45 | }
46 | case actionTypes.DELETE_BLOCK:
47 | const newArr = state.blocks.filter(block => {
48 | return block.uuid != action.blockUuid;
49 | });
50 | return {
51 | ...state,
52 | blocks: [...newArr],
53 | selectedBlockUuid: ''
54 | }
55 | case actionTypes.CHANGE_DOCUMENT_ID:
56 | return {
57 | ...state,
58 | documentId: action.documentId
59 | }
60 | default:
61 | return state;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/sagas/index.js:
--------------------------------------------------------------------------------
1 | export default function* rootSaga() {}
2 |
--------------------------------------------------------------------------------
/src/utils/renderHandlebars.js:
--------------------------------------------------------------------------------
1 | import handlebars from "handlebars";
2 | import documents from "../views/documents";
3 | import section from "../views/section";
4 | import blocks from "../views/blocks";
5 |
6 | function render(layoutBlocks, documentId) {
7 | const innerHTML = layoutBlocks.reduce((acc, layoutBlock) => {
8 | const blockHbs = blocks[layoutBlock.blockId].hbs;
9 | const blockTemplate = handlebars.compile(blockHbs);
10 | const blockHTML = blockTemplate(layoutBlock.data);
11 |
12 | const sectionTemplate = handlebars.compile(section);
13 | const sectionHTML = sectionTemplate({
14 | content: blockHTML,
15 | uuid: layoutBlock.uuid
16 | });
17 |
18 | return `${acc}${sectionHTML}`;
19 | }, ``);
20 |
21 | return handlebars.compile(documents[documentId].hbs)({
22 | content: innerHTML
23 | });
24 | }
25 |
26 | export default render;
27 |
--------------------------------------------------------------------------------
/src/utils/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/utils/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/utils/store.js:
--------------------------------------------------------------------------------
1 | import createSagaMiddleware from 'redux-saga';
2 | import { createStore, applyMiddleware, compose } from 'redux';
3 | import rootReducer from '../reducers';
4 | import rootSaga from '../sagas';
5 | const sagaMiddleware = createSagaMiddleware();
6 |
7 | const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
8 |
9 | const globalStore = createStore(
10 | rootReducer,
11 | composeEnhancers(
12 | applyMiddleware(sagaMiddleware)
13 | ),
14 | );
15 |
16 | sagaMiddleware.run(rootSaga);
17 |
18 | export default globalStore;
19 |
20 |
--------------------------------------------------------------------------------
/src/views/blocks/article1.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
{{title}}
7 |
{{description}}
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
{{articleTitle1}}
16 |
{{text1}}
17 |
18 |
19 |
20 |
21 |
22 |
{{articleTitle2}}
23 |
{{text2}}
24 |
25 |
26 |
27 |
28 |
29 |
{{articleTitle3}}
30 |
{{text3}}
31 |
32 |
33 |
34 |
35 | `;
36 |
37 | const block = {
38 | hbs,
39 | name: 'Article #1',
40 | previewImageUrl: 'https://i.imgur.com/6QUsWtK.png',
41 | category: 'article',
42 | defaultData: {
43 | title: 'Sample section',
44 | description: 'Lorem ipsum dolor sit amet.',
45 | image1: 'https://via.placeholder.com/450x450',
46 | image2: 'https://via.placeholder.com/450x450',
47 | image3: 'https://via.placeholder.com/450x450',
48 | articleTitle1: 'Hello World',
49 | articleTitle2: 'Hello World',
50 | articleTitle3: 'Hello World',
51 | text1: 'Aenean tortor est, vulputate quis leo in, vehicula rhoncus lacus. Praesent aliquam in tellus eu gravida. Aliquam varius finibus est, interdum justo suscipit id.',
52 | text2: 'Aenean tortor est, vulputate quis leo in, vehicula rhoncus lacus. Praesent aliquam in tellus eu gravida. Aliquam varius finibus est, interdum justo suscipit id.',
53 | text3: 'Aenean tortor est, vulputate quis leo in, vehicula rhoncus lacus. Praesent aliquam in tellus eu gravida. Aliquam varius finibus est, interdum justo suscipit id.',
54 | },
55 | config: {
56 | title: {
57 | type: "string",
58 | name: 'Section title',
59 | },
60 | description: {
61 | type: "string",
62 | name: 'Section description',
63 | },
64 | image1: {
65 | type: "string",
66 | name: 'Url to image #1',
67 | },
68 | image2: {
69 | type: "string",
70 | name: 'Url to image #2',
71 | },
72 | image3: {
73 | type: "string",
74 | name: 'Url to image #3',
75 | },
76 | articleTitle1: {
77 | type: "string",
78 | name: 'Title for the article #1',
79 | },
80 | articleTitle2: {
81 | type: "string",
82 | name: 'Title for the article #2',
83 | },
84 | articleTitle3: {
85 | type: "string",
86 | name: 'Title for the article #3',
87 | },
88 | text1: {
89 | type: "string",
90 | name: 'Content for the article #1',
91 | },
92 | text2: {
93 | type: "string",
94 | name: 'Content for the article #2',
95 | },
96 | text3: {
97 | type: "string",
98 | name: 'Content for the article #3',
99 | },
100 | }
101 | };
102 |
103 | export default block;
104 |
--------------------------------------------------------------------------------
/src/views/blocks/article2.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
{{title}}
7 |
{{description}}
8 |
9 |
10 |
11 |
12 |
{{articleTitle1}}
13 |
{{text1}}
14 |
15 |
16 |
{{articleTitle2}}
17 |
{{text2}}
18 |
19 |
20 |
{{articleTitle3}}
21 |
{{text3}}
22 |
23 |
24 |
25 |
26 | `;
27 |
28 | const block = {
29 | hbs,
30 | name: 'Article #2',
31 | previewImageUrl: 'https://i.imgur.com/xljS5RC.png',
32 | category: 'article',
33 | defaultData: {
34 | title: 'Sample section',
35 | description: 'Lorem ipsum dolor sit amet.',
36 | articleTitle1: 'Hello World',
37 | articleTitle2: 'Hello World',
38 | articleTitle3: 'Hello World',
39 | text1: 'Aenean tortor est, vulputate quis leo in, vehicula rhoncus lacus. Praesent aliquam in tellus eu gravida. Aliquam varius finibus est, interdum justo suscipit id.',
40 | text2: 'Aenean tortor est, vulputate quis leo in, vehicula rhoncus lacus. Praesent aliquam in tellus eu gravida. Aliquam varius finibus est, interdum justo suscipit id.',
41 | text3: 'Aenean tortor est, vulputate quis leo in, vehicula rhoncus lacus. Praesent aliquam in tellus eu gravida. Aliquam varius finibus est, interdum justo suscipit id.',
42 | },
43 | config: {
44 | title: {
45 | type: "string",
46 | name: 'Section title',
47 | },
48 | description: {
49 | type: "string",
50 | name: 'Section description',
51 | },
52 | articleTitle1: {
53 | type: "string",
54 | name: 'Title for the article #1',
55 | },
56 | articleTitle2: {
57 | type: "string",
58 | name: 'Title for the article #2',
59 | },
60 | articleTitle3: {
61 | type: "string",
62 | name: 'Title for the article #3',
63 | },
64 | text1: {
65 | type: "string",
66 | name: 'Content for the article #1',
67 | },
68 | text2: {
69 | type: "string",
70 | name: 'Content for the article #2',
71 | },
72 | text3: {
73 | type: "string",
74 | name: 'Content for the article #3',
75 | },
76 | }
77 | };
78 |
79 | export default block;
80 |
--------------------------------------------------------------------------------
/src/views/blocks/gallery2.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
16 | `;
17 |
18 | const block = {
19 | hbs,
20 | name: '2 column gallery',
21 | previewImageUrl: 'https://i.imgur.com/3hpsgRt.png',
22 | category: 'gallery',
23 | defaultData: {
24 | img1: "https://via.placeholder.com/450x450",
25 | img2: "https://via.placeholder.com/450x450.",
26 | alt1: "Sample image",
27 | alt2: "Sample image",
28 | },
29 | config: {
30 | img1: {
31 | type: "string",
32 | name: 'Url to image #1',
33 | },
34 | img2: {
35 | type: "string",
36 | name: 'Url to image #2',
37 | },
38 | alt1: {
39 | type: "string",
40 | name: 'Alt for image #1',
41 | },
42 | alt2: {
43 | type: "string",
44 | name: 'Alt for image #2',
45 | },
46 | }
47 | };
48 |
49 | export default block;
50 |
--------------------------------------------------------------------------------
/src/views/blocks/gallery3.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
21 | `;
22 |
23 | const block = {
24 | hbs,
25 | name: '3 column gallery',
26 | previewImageUrl: 'https://i.imgur.com/L0eQWAp.png',
27 | category: 'gallery',
28 | defaultData: {
29 | img1: "https://via.placeholder.com/450x450",
30 | img2: "https://via.placeholder.com/450x450.",
31 | img3: "https://via.placeholder.com/450x450",
32 | alt1: "Sample image",
33 | alt2: "Sample image",
34 | alt3: "Sample image",
35 | },
36 | config: {
37 | img1: {
38 | type: "string",
39 | name: 'Url to image #1',
40 | },
41 | img2: {
42 | type: "string",
43 | name: 'Url to image #2',
44 | },
45 | img3: {
46 | type: "string",
47 | name: 'Url to image #3',
48 | },
49 | alt1: {
50 | type: "string",
51 | name: 'Alt for image #1',
52 | },
53 | alt2: {
54 | type: "string",
55 | name: 'Alt for image #2',
56 | },
57 | alt3: {
58 | type: "string",
59 | name: 'Alt for image #3',
60 | },
61 | }
62 | };
63 |
64 | export default block;
65 |
--------------------------------------------------------------------------------
/src/views/blocks/gallery4.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
26 | `;
27 |
28 | const block = {
29 | hbs,
30 | name: '4 column gallery',
31 | previewImageUrl: 'https://i.imgur.com/jFFAd28.png',
32 | category: 'gallery',
33 | defaultData: {
34 | img1: "https://via.placeholder.com/450x450",
35 | img2: "https://via.placeholder.com/450x450.",
36 | img3: "https://via.placeholder.com/450x450",
37 | img4: "https://via.placeholder.com/450x450",
38 | alt1: "Sample image",
39 | alt2: "Sample image",
40 | alt3: "Sample image",
41 | alt4: "Sample image",
42 | },
43 | config: {
44 | img1: {
45 | type: "string",
46 | name: 'Url to image #1',
47 | },
48 | img2: {
49 | type: "string",
50 | name: 'Url to image #2',
51 | },
52 | img3: {
53 | type: "string",
54 | name: 'Url to image #3',
55 | },
56 | img4: {
57 | type: "string",
58 | name: 'Url to image #4',
59 | },
60 | alt1: {
61 | type: "string",
62 | name: 'Alt for image #1',
63 | },
64 | alt2: {
65 | type: "string",
66 | name: 'Alt for image #2',
67 | },
68 | alt3: {
69 | type: "string",
70 | name: 'Alt for image #3',
71 | },
72 | alt4: {
73 | type: "string",
74 | name: 'Alt for image #4',
75 | },
76 | }
77 | };
78 |
79 | export default block;
80 |
--------------------------------------------------------------------------------
/src/views/blocks/header1.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
{{title}}
4 |
{{tagline}}
5 |
{{link}}
6 |
7 | `;
8 |
9 | const block = {
10 | hbs,
11 | name: 'Simple Header #1',
12 | previewImageUrl: 'https://i.imgur.com/IXz7LZ5.png',
13 | category: 'header',
14 | defaultData: {
15 | title: "Hello World",
16 | tagline: "Lorem ipsum dolor sit amet.",
17 | link: "Read more",
18 | },
19 | config: {
20 | title: {
21 | type: "string",
22 | name: 'Title',
23 | },
24 | tagline: {
25 | type: "string",
26 | name: 'Tag Line',
27 | },
28 | link: {
29 | type: "string",
30 | name: 'Text on the link',
31 | }
32 | }
33 | };
34 |
35 | export default block;
36 |
--------------------------------------------------------------------------------
/src/views/blocks/header2.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
10 | `;
11 |
12 | const block = {
13 | hbs,
14 | name: 'Simple Header #2',
15 | previewImageUrl: 'https://i.imgur.com/1bYEKB4.png',
16 | category: 'header',
17 | defaultData: {
18 | title: "Hello World",
19 | tagline: "Lorem ipsum dolor sit amet.",
20 | button: "Click here",
21 | link: "Read more",
22 | },
23 | config: {
24 | title: {
25 | type: "string",
26 | name: 'Title',
27 | },
28 | tagline: {
29 | type: "string",
30 | name: 'Tag Line',
31 | },
32 | button: {
33 | type: "string",
34 | name: 'Text on the button',
35 | },
36 | link: {
37 | type: "string",
38 | name: 'Text on the link',
39 | }
40 | }
41 | };
42 |
43 | export default block;
44 |
--------------------------------------------------------------------------------
/src/views/blocks/index.js:
--------------------------------------------------------------------------------
1 | import header1 from './header1';
2 | import header2 from './header2';
3 | import navbar1 from './navbar1';
4 |
5 | import gallery3 from './gallery3';
6 | import gallery4 from './gallery4';
7 | import gallery2 from './gallery2';
8 |
9 | import article1 from './article1';
10 | import article2 from './article2';
11 |
12 | const blocks = {
13 | header1,
14 | header2,
15 | navbar1,
16 | gallery3,
17 | gallery4,
18 | gallery2,
19 | article1,
20 | article2,
21 | };
22 |
23 | export default blocks;
24 |
--------------------------------------------------------------------------------
/src/views/blocks/navbar1.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 | {{brand}}
4 |
5 |
6 |
7 |
8 |
9 |
25 | {{#if showSearch}}
26 |
30 | {{/if}}
31 |
32 |
33 | `;
34 |
35 | const block = {
36 | hbs,
37 | name: 'Navbar #1',
38 | previewImageUrl: 'https://i.imgur.com/awrvf3d.png',
39 | category: 'header',
40 | defaultData: {
41 | brand: "Reynholm",
42 | link1: "Home",
43 | link2: "Feature",
44 | link3: "Pricing",
45 | link4: "About",
46 | showSearch: true,
47 | useDarkTheme: false,
48 | },
49 | config: {
50 | title: {
51 | type: "string",
52 | name: 'Brand name',
53 | },
54 | link1: {
55 | type: "string",
56 | name: 'Link #1',
57 | },
58 | link2: {
59 | type: "string",
60 | name: 'Link #2',
61 | },
62 | link3: {
63 | type: "string",
64 | name: 'Link #3',
65 | },
66 | link4: {
67 | type: "string",
68 | name: 'Link #4',
69 | },
70 | showSearch: {
71 | type: "boolean",
72 | name: 'Show search',
73 | },
74 | useDarkTheme: {
75 | type: "boolean",
76 | name: 'Use dark theme',
77 | },
78 | }
79 | };
80 |
81 | export default block;
82 |
--------------------------------------------------------------------------------
/src/views/documents/document1.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hello, world!
9 |
35 |
36 |
37 |
38 |
39 | {{{content}}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
89 |
90 |
100 |
101 |
102 | `;
103 |
104 | const document = {
105 | hbs,
106 | name: 'Default (Bootstrap 4.5)'
107 | }
108 |
109 | export default document;
110 |
--------------------------------------------------------------------------------
/src/views/documents/document2.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
7 | Hello, world!
8 |
33 |
34 |
35 |
36 |
37 | {{{content}}}
38 |
39 |
40 |
41 |
42 |
85 |
86 |
96 |
97 |
98 | `;
99 |
100 | const document = {
101 | hbs,
102 | name: 'Basic document'
103 | }
104 |
105 | export default document;
106 |
--------------------------------------------------------------------------------
/src/views/documents/document3.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hello, world!
9 |
35 |
36 |
37 |
38 |
39 | {{{content}}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
89 |
90 |
100 |
101 |
102 | `;
103 |
104 | const document = {
105 | hbs,
106 | name: 'Bootswatch Cosmo (Bootstrap 4.5)'
107 | }
108 |
109 | export default document;
110 |
--------------------------------------------------------------------------------
/src/views/documents/document4.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hello, world!
9 |
35 |
36 |
37 |
38 |
39 | {{{content}}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
89 |
90 |
100 |
101 |
102 | `;
103 |
104 | const document = {
105 | hbs,
106 | name: 'Bootswatch Journal (Bootstrap 4.5)'
107 | }
108 |
109 | export default document;
110 |
--------------------------------------------------------------------------------
/src/views/documents/document5.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hello, world!
9 |
35 |
36 |
37 |
38 |
39 | {{{content}}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
89 |
90 |
100 |
101 |
102 | `;
103 |
104 | const document = {
105 | hbs,
106 | name: 'Bootswatch Yeti (Bootstrap 4.5)'
107 | }
108 |
109 | export default document;
110 |
--------------------------------------------------------------------------------
/src/views/documents/document6.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hello, world!
9 |
35 |
36 |
37 |
38 |
39 | {{{content}}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
89 |
90 |
100 |
101 |
102 | `;
103 |
104 | const document = {
105 | hbs,
106 | name: 'Bootswatch Slate (Bootstrap 4.5)'
107 | }
108 |
109 | export default document;
110 |
--------------------------------------------------------------------------------
/src/views/documents/document7.js:
--------------------------------------------------------------------------------
1 | const hbs = `
2 |
3 |
4 |
5 |
6 |
7 |
8 | Hello, world!
9 |
35 |
36 |
37 |
38 |
39 | {{{content}}}
40 |
41 |
42 |
43 |
44 |
45 |
46 |
89 |
90 |
100 |
101 |
102 | `;
103 |
104 | const document = {
105 | hbs,
106 | name: 'Bootswatch Spacelab (Bootstrap 4.5)'
107 | }
108 |
109 | export default document;
110 |
--------------------------------------------------------------------------------
/src/views/documents/index.js:
--------------------------------------------------------------------------------
1 | import document1 from './document1';
2 | import document2 from './document2';
3 | import document3 from './document3';
4 | import document4 from './document4';
5 | import document5 from './document5';
6 | import document6 from './document6';
7 | import document7 from './document7';
8 |
9 | const documents = {
10 | document1,
11 | document2,
12 | document3,
13 | document4,
14 | document5,
15 | document6,
16 | document7,
17 | }
18 |
19 | export default documents;
20 |
--------------------------------------------------------------------------------
/src/views/section.js:
--------------------------------------------------------------------------------
1 | const section = `
2 |
5 | `;
6 |
7 | export default section;
8 |
--------------------------------------------------------------------------------