)
14 | }
15 | }
16 |
17 | return (
18 |
19 | {BootstrapComponents}
20 |
21 | )
22 | };
23 |
24 | export default BootstrapLibrary;
--------------------------------------------------------------------------------
/public/serviceworker.js:
--------------------------------------------------------------------------------
1 | const CACHE_NAME = "version-1";
2 | const urlsToCache = ['index.html', 'offline.html'];
3 |
4 | const self = this;
5 | // Install SW
6 | self.addEventListener('install', (event) => {
7 | event.waitUntil(
8 | caches.open(CACHE_NAME)
9 | .then(cache => {
10 | console.log('Opened cache');
11 | return cache.addAll(urlsToCache);
12 | })
13 | )
14 | });
15 |
16 | // Listen for requests
17 | self.addEventListener('fetch', (event) => {
18 | event.respondWith(
19 | caches.match(event.request)
20 | .then(() => {
21 | return fetch(event.request)
22 | // if cannot fetch a cached url, send offline.html
23 | .catch(() => caches.match('offline.html'))
24 | })
25 | )
26 | });
27 |
28 | // Actiate the SW
29 | self.addEventListener('activate', (event) => {
30 | const cacheWhiteList = [];
31 | cacheWhiteList.push(CACHE_NAME);
32 |
33 | event.waitUntil(
34 | caches.keys()
35 | .then(cacheNames => Promise.all(
36 | cacheNames.map(cacheName => {
37 | if(!cacheWhiteList.includes(cacheName)) {
38 | return caches.delete(cacheName);
39 | }
40 | })
41 | ))
42 | )
43 | });
--------------------------------------------------------------------------------
/src/components/RightContainer/BodyContainer.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import * as actions from '../../actions/actions.js';
3 | import Canvas from './Canvas.jsx';
4 | import Preview from './Preview.jsx';
5 | import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live'
6 | import { Form, Button, Navbar, Container, Nav, NavDropdown, ListGroup } from 'react-bootstrap';
7 |
8 | const mapStateToProps = (state) => ({
9 | bodyView: state.main.bodyView,
10 | renderedComponents: state.main.renderedComponents,
11 | prototypeCode: state.main.prototypeCode
12 | });
13 |
14 | const mapDispatchToProps = (dispatch) => ({
15 | selectComponent: (component) => dispatch(actions.selectComponent(component))
16 | });
17 |
18 | function BodyContainer(props) {
19 | const scope = { Form, Button, Navbar, Container, Nav, NavDropdown, ListGroup };
20 |
21 | // code string is intentially tabbed this way for formatting on render
22 | const code = `
23 | render (
24 | <>
25 | ${props.prototypeCode}
26 | >
27 | );
28 | `;
29 |
30 | return (
31 |
32 |
33 | { props.bodyView === 'Code Preview' ? : null }
34 |
35 | { props.bodyView === 'Canvas' ? : null }
36 |
37 |
38 | )
39 | };
40 |
41 | export default connect(mapStateToProps, mapDispatchToProps)(BodyContainer);
--------------------------------------------------------------------------------
/src/Pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import { useParams } from 'react-router-dom';
3 | import { connect } from 'react-redux';
4 | import * as actions from '../actions/actions.js';
5 | import '../stylesheets/Dashboard.css';
6 | import RightContainer from '../components/RightContainer/RightContainer';
7 | import ComponentMenu from '../components/ComponentMenu/ComponentMenu';
8 | import ExportCodeModal from '../components/ExportCodeModal';
9 |
10 | const mapStateToProps = (state) => ({
11 | exportModal: state.main.exportModal
12 | })
13 |
14 | const mapDispatchToProps = (dispatch) => ({
15 | setUser: (username) => dispatch(actions.setUser(username)),
16 | });
17 |
18 | function Dashboard(props) {
19 | // useParams parses the url and outputs the params. Using slice on username to extract just the username
20 | const username = useParams().username.slice(9);
21 | // destructure setUser action from props to use inside useEffect (so that useEffect doesn't render on every prop change
22 | const { setUser } = props;
23 |
24 | // invoke setUser action whenever the user changes
25 | useEffect(() => {
26 | setUser(username)
27 | }, [username, setUser])
28 |
29 | return (
30 | <>
31 | {
32 | props.exportModal ? : null
33 | }
34 |
40 | >
41 | )
42 | };
43 |
44 | export default connect(mapStateToProps, mapDispatchToProps)(Dashboard);
--------------------------------------------------------------------------------
/src/Pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import '../stylesheets/Login.css';
2 | import { Icon } from '@iconify/react';
3 | import { useEffect } from 'react';
4 |
5 | function Login() {
6 | return (
7 | useEffect(() => {
8 | document.body.style.overflow = "hidden";
9 | return () => {
10 | document.body.style.overflow = "auto";
11 | };
12 | }, []),
13 |
14 |
15 | {/*
Abstract */}
16 |
40 |
41 | );
42 | }
43 |
44 | export default Login;
45 |
--------------------------------------------------------------------------------
/src/components/ComponentMenu/ComponentMenu.jsx:
--------------------------------------------------------------------------------
1 | import { React, useState } from 'react';
2 | import HTMLLibrary from './HTMLLibrary.jsx';
3 | import ReactRouterLibrary from './ReactRouterLibrary.jsx';
4 | import BootstrapLibrary from './BootstrapLibrary.jsx';
5 | import ComSettings from './ComSettings.jsx';
6 | import { connect } from 'react-redux';
7 | import * as actions from '../../actions/actions.js';
8 |
9 | const mapStateToProps = (state) => ({
10 | componentMenu: state.main.componentMenu
11 | });
12 |
13 | const mapDispatchToProps = (dispatch) => ({
14 | toggleComponentMenu: (componentMenu) => dispatch(actions.toggleComponentMenu(componentMenu))
15 | });
16 |
17 | function ComponentMenu (props) {
18 | // declaring library state to decide which dropdown (HTML, Bootstrap) to render
19 | const [library, setLibrary] = useState('')
20 |
21 | return (
22 |
35 | )
36 | };
37 |
38 | export default connect(mapStateToProps, mapDispatchToProps)(ComponentMenu);
39 |
--------------------------------------------------------------------------------
/src/reducers/reducer.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 |
3 | const initialState = {
4 | username: "oakj",
5 | renderedComponents: [],
6 | bodyView: "Canvas", // Canvas, Code Preview
7 | canvasSize: "iPad Pro", // iPhone X, iPad Pro
8 | componentMenu: true,
9 | selectedComponent: "",
10 | exportModal: false,
11 | // prototype code is used as an input to React-Live inside BodyContainer and as export code to Github
12 | prototypeCode: ``,
13 | };
14 |
15 | const reducer = (state = initialState, action) => {
16 | switch (action.type) {
17 | case types.SET_ACTIVE_USER: {
18 | return {
19 | ...state,
20 | username: action.payload
21 | };
22 | }
23 | case types.ADD_COMPONENT: {
24 | return {
25 | ...state,
26 | prototypeCode: state.prototypeCode + action.payload
27 | };
28 | }
29 | case types.DELETE_COMPONENT: {
30 | return state;
31 | }
32 | case types.TOGGLE_BODY_VIEW: {
33 | return {
34 | ...state,
35 | bodyView: action.payload
36 | };
37 | }
38 | case types.TOGGLE_CANVAS_SIZE: {
39 | return {
40 | ...state,
41 | canvasSize: action.payload
42 | };
43 | }
44 | case types.TOGGLE_COMPONENT_MENU: {
45 | return {
46 | ...state,
47 | componentMenu: !state.componentMenu,
48 | }
49 | }
50 | case types.SELECT_COMPONENT: {
51 | return {
52 | ...state,
53 | selectedComponent: action.payload
54 | };
55 | }
56 | case types.TOGGLE_EXPORT_MODAL: {
57 | return {
58 | ...state,
59 | exportModal: action.payload
60 | }
61 | }
62 | default: {
63 | return state;
64 | }
65 | }
66 | }
67 |
68 | export default reducer
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "abstract",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@iconify/react": "^3.0.1",
7 | "@octokit/core": "^3.5.1",
8 | "@testing-library/jest-dom": "^5.14.1",
9 | "@testing-library/react": "^11.2.7",
10 | "@testing-library/user-event": "^12.8.3",
11 | "bcrypt": "^5.0.1",
12 | "bootstrap": "^5.1.0",
13 | "buffer": "^6.0.3",
14 | "cookie-parser": "^1.4.5",
15 | "cors": "^2.8.5",
16 | "dotenv": "^10.0.0",
17 | "framer-motion": "^4.1.17",
18 | "jsonwebtoken": "^8.5.1",
19 | "jwt-decode": "^3.1.2",
20 | "node-fetch": "^3.0.0",
21 | "nodemon": "^2.0.12",
22 | "octokit": "^1.5.0",
23 | "path": "^0.12.7",
24 | "pg": "^8.7.1",
25 | "react": "^17.0.2",
26 | "react-bootstrap": "^2.0.0-beta.6",
27 | "react-dom": "^17.0.2",
28 | "react-live": "^2.2.3",
29 | "react-redux": "^7.2.5",
30 | "react-router": "^5.2.1",
31 | "react-router-dom": "^5.3.0",
32 | "react-scripts": "4.0.3",
33 | "redux": "^4.1.1",
34 | "redux-devtools-extension": "^2.13.9",
35 | "uuid": "^8.3.2",
36 | "web-vitals": "^1.1.2"
37 | },
38 | "scripts": {
39 | "start": "react-scripts start",
40 | "build": "react-scripts build",
41 | "test": "react-scripts test",
42 | "eject": "react-scripts eject",
43 | "server-dev": "nodemon ./server/server.js "
44 | },
45 | "eslintConfig": {
46 | "extends": [
47 | "react-app",
48 | "react-app/jest"
49 | ]
50 | },
51 | "browserslist": {
52 | "production": [
53 | ">0.2%",
54 | "not dead",
55 | "not op_mini all"
56 | ],
57 | "development": [
58 | "last 1 chrome version",
59 | "last 1 firefox version",
60 | "last 1 safari version"
61 | ]
62 | },
63 | "type": "module",
64 | "devDependencies": {
65 | "@wojtekmaj/enzyme-adapter-react-17": "^0.6.3",
66 | "enzyme": "^3.11.0"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | import accountController from './controllers/accountController.js';
2 | import dotenv from 'dotenv';
3 | import cors from 'cors';
4 | import express from 'express';
5 | import cookieParser from 'cookie-parser';
6 | import path from 'path';
7 |
8 | // intiate __dirname to root path
9 | const __dirname = path.resolve();
10 |
11 | const app = express();
12 | const PORT = process.env.PORT || 5000;
13 |
14 | dotenv.config();
15 |
16 | app.use(express.json());
17 | app.use(express.urlencoded());
18 | app.use(cors());
19 | app.use(cookieParser());
20 |
21 | console.log('testing heroku logs: entered server.js');
22 |
23 | //oauth login
24 | app.get('/oauth',
25 | accountController.handleOAuth);
26 |
27 | // github API to create a repo
28 | app.post('/export',
29 | accountController.createRepo,
30 | (req, res) => {
31 | return res.sendStatus(200)
32 | }
33 | )
34 |
35 | //github API submit files to existing repo
36 | app.put('/export',
37 | accountController.updateRepo,
38 | (req, res) => {
39 | return res.sendStatus(200)
40 | }
41 | )
42 |
43 | // to deploy
44 | app.use(express.static(path.join(__dirname, 'build')));
45 | // enpoint '/*' is needed to cover client routes for '/' and '/dashboard'
46 | app.get('/*', function (req, res) {
47 | res.sendFile(path.join(__dirname, 'build', 'index.html'));
48 | });
49 |
50 | // catch-all route handler for any requests to an unknown route
51 | app.use((req, res) => res.status(404).send('Page not Found'));
52 |
53 | //global err handler
54 | app.use((err, req, res, next) => {
55 | const defaultErr = {
56 | log: 'Express error handler caught unknown middleware error',
57 | status: 500,
58 | message: { err: 'An error occurred' },
59 | };
60 | const errorObj = Object.assign({}, defaultErr, err);
61 | console.log(errorObj.log);
62 | return res.status(errorObj.status).json(errorObj.message);
63 | });
64 |
65 | //server listening
66 | app.listen(PORT, () => {
67 | console.log(`Server listening on port: ${PORT}...`);
68 | });
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 | A B S T R C T
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
30 |
39 | Abstract
40 |
41 |
42 | You need to enable JavaScript to run this app.
43 |
44 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/stylesheets/Login.css:
--------------------------------------------------------------------------------
1 | .logInContainer {
2 | text-align: center;
3 | background-color: #F0F8FF;
4 |
5 | }
6 |
7 | #Abstract {
8 | color: white;
9 | position: relative;
10 | bottom: -30px;
11 | }
12 |
13 | #abstractLogo {
14 | opacity: 100%;
15 | width: 50px;
16 | height: 50px;
17 | }
18 |
19 | .logInHeader {
20 | min-height: 100vh;
21 | display: flex;
22 | flex-direction: column;
23 | justify-content: center;
24 | align-items: center;
25 | font-size: calc(6px + 2vmin);
26 | color: black;
27 | }
28 |
29 | .App-link {
30 | color: #61dafb;
31 | }
32 |
33 | #welcomeBorderBox {
34 | box-shadow: deepskyblue 0px 3px 8px;
35 | border-radius: 4px;
36 | background-color: white;
37 | width: 40%;
38 | padding: 70px;;
39 | }
40 |
41 | #loginButtonDiv {
42 | display:flex;
43 | justify-content: center;
44 | align-items: center;
45 | }
46 | #frontPageLogInButton {
47 | border-radius: 5px;
48 | font-size: 1.25rem;
49 | box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
50 | height: 90px;
51 | display:flex;
52 | justify-content: center;
53 | align-items: center;
54 | background-color: #f5fbff;
55 | border: 1px solid #093A7B;
56 | margin:0px;
57 | }
58 |
59 | #frontPageLogInButton:hover {
60 | box-shadow: 0 0 11px rgba(33,33,33,.2);
61 | }
62 |
63 | #loginText {
64 | margin: 20px;
65 | }
66 |
67 | #frontPageSignUpButton {
68 | box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
69 | border-radius: 6px;
70 | height: 60px;
71 | width: auto;
72 | font-size: 1.25rem;
73 | background-color: #f5fbff;
74 | border: 1px solid #093A7B;
75 | }
76 |
77 |
78 | #frontPageSignUpButton:hover {
79 | box-shadow: 0 0 11px rgba(33,33,33,.2);
80 | }
81 |
82 | #logIn {
83 | border-radius: 15px;
84 | width: 270px;
85 | height: 70px;
86 | font-size: 30px;
87 | word-wrap: break-word;
88 | }
89 |
90 | #gitHubLogo {
91 | width: 50px;
92 | height: 50px;
93 | padding: 1px;
94 |
95 | }
96 |
97 | #hexagon {
98 | color: white;
99 | }
100 |
101 | #welcomeMessage {
102 | font-size: larger;
103 | font-weight: 350;
104 | }
105 | #noAccount {
106 | font-size: large;
107 | font-weight: 400;
108 | }
109 |
110 | #logo {
111 | position: absolute;
112 | top: 0;
113 | width: 40%;
114 | height: 40px;
115 | /* display: flex;
116 | flex-direction: column;
117 | justify-content: center;
118 | align-items: center; */
119 | /* position: absolute;
120 | top: 0; */
121 | /* margin: 0%; */
122 | /* width: 100px;
123 | height: 100px; */
124 | }
125 |
126 |
127 | @media (max-width: 820px) {
128 | #welcomeBorderBox {
129 | width:100%;
130 | height:fit-content;
131 | padding: 0;
132 | }
133 | #loginButtonDiv {
134 | width: 100%
135 | }
136 | #frontPageLogInButton {
137 | width: 100%;
138 | /* font-size: 1rem; */
139 | }
140 | #frontPageSignUpButton {
141 | width:auto;
142 | }
143 | #abstractlogo {
144 | width: 80%;
145 | height: auto;
146 | }
147 | }
--------------------------------------------------------------------------------
/src/actions/actions.js:
--------------------------------------------------------------------------------
1 | import * as types from '../constants/actionTypes';
2 | import * as componentTypes from '../constants/componentTypes';
3 |
4 | // action for SET_ACTIVE_USER action type
5 | export const setUser = (username) => {
6 | return {
7 | type: types.SET_ACTIVE_USER,
8 | payload: username
9 | }
10 | };
11 |
12 | // action for ADD_COMPONENT action type
13 | export const addComponent = (selectedComponent) => {
14 | switch (selectedComponent) {
15 | case 'bootstrapForm':
16 | return {
17 | type: types.ADD_COMPONENT,
18 | payload: componentTypes.bootstrapForm
19 | }
20 | case 'bootstrapButton':
21 | return {
22 | type: types.ADD_COMPONENT,
23 | payload: componentTypes.bootstrapButton
24 | }
25 | case 'bootstrapNavbar':
26 | return {
27 | type: types.ADD_COMPONENT,
28 | payload: componentTypes.bootstrapNavbar
29 | }
30 | case 'bootstrapNav':
31 | return {
32 | type: types.ADD_COMPONENT,
33 | payload: componentTypes.bootstrapNav
34 | }
35 | case 'bootstrapList':
36 | return {
37 | type: types.ADD_COMPONENT,
38 | payload: componentTypes.bootstrapList
39 | }
40 | case 'htmlDiv':
41 | return {
42 | type: types.ADD_COMPONENT,
43 | payload: componentTypes.htmlDiv
44 | }
45 | case 'htmlLink':
46 | return {
47 | type: types.ADD_COMPONENT,
48 | payload: componentTypes.htmlLink
49 | }
50 | case 'htmlImage':
51 | return {
52 | type: types.ADD_COMPONENT,
53 | payload: componentTypes.htmlImage
54 | }
55 | case 'htmlParagraph':
56 | return {
57 | type: types.ADD_COMPONENT,
58 | payload: componentTypes.htmlParagraph
59 | }
60 | default:
61 | return {
62 | type: types.ADD_COMPONENT,
63 | payload: 'Not a component type.'
64 | }
65 | }
66 | }
67 |
68 | //action for DELETE_COMPONENT action type
69 | export const deleteComponent = (selectedComponent) => ({
70 | type: types.DELETE_COMPONENT,
71 | payload: selectedComponent
72 | })
73 |
74 | // action for TOGGLE_BODY_VIEW action type
75 | export const toggleBodyView = (bodyView) => ({
76 | type: types.TOGGLE_BODY_VIEW,
77 | payload: bodyView
78 | })
79 |
80 | //action for TOGGLE_CANVAS_SIZE action type
81 | export const toggleCanvasSize = (canvasSize) => ({
82 | type: types.TOGGLE_CANVAS_SIZE,
83 | payload: canvasSize
84 | })
85 |
86 | //action for TOGGLE_COMPONENT_MENU action type
87 | export const toggleComponentMenu = (componentMenu) => {
88 | return {
89 | type: types.TOGGLE_COMPONENT_MENU,
90 | payload: componentMenu
91 | }
92 | }
93 |
94 | //action for SELECT_COMPONENT action type
95 | export const selectComponent = (component) => ({
96 | type: types.SELECT_COMPONENT,
97 | payload: component
98 | })
99 |
100 | //action for TOGGLE EXPORT MODAL
101 | export const toggleExportModal = (toggle) => ({
102 | type: types.TOGGLE_EXPORT_MODAL,
103 | payload: toggle
104 | })
105 |
--------------------------------------------------------------------------------
/src/components/ExportCodeModal.jsx:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux';
2 | import { motion } from 'framer-motion';
3 | import * as actions from '../actions/actions.js';
4 | import ExportModalBackdrop from "./ExportModalBackdrop";
5 |
6 |
7 | const mapStateToProps = (state) => ({
8 | exportModal: state.main.exportModal,
9 | prototypeCode: state.main.prototypeCode,
10 | username: state.main.username,
11 | bodyView: state.main.bodyView
12 | })
13 |
14 | const mapDispatchToProps = (dispatch) => ({
15 | toggleExportModal: (toggle) => dispatch(actions.toggleExportModal(toggle)),
16 | });
17 |
18 | function ExportCodeModal ({ toggleExportModal, exportModal, ...props}) {
19 | const handleFormSubmit = async (e) => {
20 | e.preventDefault();
21 | const { repository_name, commit_message } = e.target.elements
22 | // create github repo
23 | const createRepo = await fetch('https://abstractreact.herokuapp.com/export', {
24 | method: 'POST',
25 | mode: 'cors',
26 | body: JSON.stringify({
27 | "repository_name": repository_name.value,
28 | }),
29 | headers: {
30 | 'Accept': 'application/json',
31 | 'Content-Type': 'application/json'
32 | },
33 | })
34 | .catch(e => console.log('create err :', e));
35 |
36 | //update files
37 | const updateRepo = await fetch('https://abstractreact.herokuapp.com/export', {
38 | method: 'PUT',
39 | mode: 'cors',
40 | body: JSON.stringify({
41 | "prototypeCode": props.prototypeCode,
42 | "repository_name": repository_name.value,
43 | "commit_message": commit_message.value,
44 | "username": props.username,
45 | }),
46 | headers: {
47 | 'Accept' : 'application/json',
48 | 'Content-Type': 'application/json' }
49 | })
50 | .then(toggleExportModal(!exportModal))
51 | .then(alert('Code successfully exported! Please check your Github repos.'))
52 | .catch(e => console.log('update err: ', e));
53 | }
54 |
55 | return (
56 | toggleExportModal(!exportModal)}>
57 | e.stopPropagation()}
59 | className="abstract_modal orange-gradient"
60 | initial='hidden'
61 | animate='visible'
62 | exit='exit'
63 | >
64 | toggleExportModal(!exportModal)}>x
65 |
66 |
Export Code
67 | Create a Repository
68 |
76 |
77 |
78 |
79 | )
80 | }
81 |
82 | export default connect(mapStateToProps, mapDispatchToProps)(ExportCodeModal);
--------------------------------------------------------------------------------
/src/constants/componentTypes.js:
--------------------------------------------------------------------------------
1 | export const bootstrapForm =
2 | `
3 |
5 | Email address
6 |
7 |
8 | We'll never share your email with anyone else.
9 |
10 |
11 |
12 |
13 | Password
14 |
15 |
16 |
17 |
18 |
19 |
20 | Submit
21 |
22 |
23 | `;
24 |
25 | export const bootstrapButton =
26 | `
27 |
31 | Small button
32 |
33 | `;
34 |
35 | export const bootstrapNavbar =
36 | `
37 |
38 |
39 | React-Bootstrap
40 |
41 |
42 |
43 | Home
44 | Link
45 |
46 | Action
47 | Another action
48 | Something
49 |
50 | Separated link
51 |
52 |
53 |
54 |
55 |
56 | `;
57 |
58 | export const bootstrapNav =
59 | `
60 | alert(\`selected \${selectedKey}\`)} */
63 | >
64 |
65 | Active
66 |
67 |
68 | Link
69 |
70 |
71 | Link
72 |
73 |
74 |
75 | Disabled
76 |
77 |
78 |
79 | `;
80 |
81 | export const bootstrapList =
82 | `
83 |
84 | Cras justo odio
85 | Dapibus ac facilisis in
86 | Morbi leo risus
87 | Porta ac consectetur ac
88 | Vestibulum at eros
89 |
90 | `;
91 |
92 | export const htmlDiv =
93 | `
94 | html div
95 | `;
96 |
97 | export const htmlLink =
98 | `
99 | html anchor link
100 | `;
101 |
102 | export const htmlImage =
103 | `
104 |
105 | `;
106 |
107 | export const htmlParagraph =
108 | `
109 | html paragraph
110 | `;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Abstract Beta is a mobile first application focused on improving developer experience. Create mobile first application prototypes in real-time with proven UI frameworks such as React Bootstrap.
14 |
15 |
16 |
17 | ## Table of Contents
18 | * [Demo](#demo)
19 | + [Select Device Resolution for the Canvas](#select-device-resolution-for-the-canvas)
20 | + [Render Components on the Canvas](#render-components-on-the-canvas)
21 | + [Review Code](#review-code)
22 | + [Export Code](#export-code)
23 | * [Live Features](#live-features)
24 | * [How To Use](#how-to-use)
25 | + [Web Based](#web-based)
26 | + [Running Local](#running-local)
27 | * [Contribute](#how-to-use)
28 | + [Contributors](#contributors)
29 | + [How to contribute](#how-to-contribute)
30 | + [New feature log](#new-feature-log)
31 | * [License](#license)
32 |
33 | ## Demo
34 | ### Select Device Resolution for the Canvas
35 |
36 |
37 | ### Render Components on the Canvas
38 |
39 |
40 | ### Review Code
41 |
42 |
43 | ### Export Code
44 |
45 |
46 | ## Live Features
47 | 1. Real-time live preview of your prototype including rendered components and styling.
48 | 2. Prototype on multiple canvas sizes (e.g. iPad Pro and iPhone X).
49 | 3. Code preview formatted and synchronized with the prototype render view.
50 | 4. React-Bootstrap component integration.
51 | 5. Export your prototype code to your Github account.
52 | 6. Secure signup and login using Github OAuth.
53 | 7. Installable progressive web application
54 |
55 | ## How To Use
56 | ### Web-based
57 | 1. Visit [abstractreact.herokuapp.com](https://abstractreact.herokuapp.com)
58 | 2. Login with a GitHub account
59 | ### Running local
60 | 1. Clone this repo
61 | ```
62 | git clone https://github.com/oslabs-beta/Abstract.git
63 | ```
64 | 2. Navigate to your local directory
65 | 3. Install dependencies
66 | ```
67 | npm install
68 | ```
69 | 4. Run the client (verify that localhost port 3000 is not being used)
70 | ```
71 | npm start
72 | ```
73 |
74 | ## Contribute
75 | Abstract is an open-source product supported via the tech accelerator OS Labs/OS Labs-beta. We encourge all feedback and welcome all contributions. Note that this product does not have any full-time contributors. Expect that there will be a wait time before your pull request can be reviewed.
76 |
77 | #### Contributors
78 | - Jonnie Oak [@Github](https://github.com/oakj) [@Linkedin](https://www.linkedin.com/in/oakj28/)
79 | - Brian Cheng [@Github](https://github.com/chengbrian9) [@Linkedin](https://www.linkedin.com/in/brian-cheng24/)
80 | - Raymond Hu [@Github](https://github.com/rhu0) [@Linkedin](https://www.linkedin.com/in/raymond-hu-3b18231a2/)
81 | - Omar Brown [@Github](https://github.com/rashadhndrxx) [@Linkedin](https://www.linkedin.com/in/omar-b-76892521b/)
82 |
83 | #### How to contribute
84 | 1. Fork the repo and create a new feature branch from dev.
85 | 2. Any changes to the code base should include unit and/or integration tests.
86 | 3. Ensure your code follows proper formatting recommendations.
87 | 4. Create a pull request to the dev branch.
88 |
89 | #### New feature log
90 | 1. Drag and drop feature from the component menu onto the canvas
91 | 2. Refactor to TypeScript
92 | 3. Support TypeScript exports
93 | 4. Add support for additional component libraries (Material UI, Chakra UI, etc)
94 |
95 | ## License
96 | This project is licensed under the MIT License - see the LICENSE.md file for details.
--------------------------------------------------------------------------------
/server/controllers/accountController.js:
--------------------------------------------------------------------------------
1 | import db from '../db/db.js';
2 | import fetch from 'node-fetch';
3 | import { v4 as uuid } from 'uuid';
4 | import jwt from 'jsonwebtoken';
5 | import { Octokit } from "@octokit/core";
6 | import jwt_decode from 'jwt-decode';
7 | import { Buffer } from 'buffer';
8 | import bcrypt from 'bcrypt';
9 |
10 | const salt = 11; // bcrypt salt work factor
11 | const accountController = {};
12 |
13 | // OAuth
14 | accountController.handleOAuth = async (req, res, next) => {
15 | console.log('entered server.js/OAuth endpoint and handleOAuth controller');
16 | // deconstruct req.query to get code from first Github GET request
17 | const { code } = req.query;
18 | // handle edge case if code is not provided
19 | if (!code) {
20 | throw new Error("Missing code from Github!");
21 | }
22 |
23 | // once we get code back from Github, we need to make a POST to: https://github.com/login/oauth/access_token/
24 | let accessToken = await fetch(`https://github.com/login/oauth/access_token/?client_id=${process.env.GITHUB_OAUTH_CLIENT_ID}&client_secret=${process.env.GITHUB_OAUTH_CLIENT_SECRET}&code=${code}`, {
25 | method: 'POST',
26 | headers: {
27 | 'Accept': 'application/json',
28 | }
29 | })
30 | .then(response => response.json())
31 | .then(data => data)
32 | .catch(error => {
33 | return next({
34 | status: 500,
35 | message: error
36 | });
37 | });
38 |
39 | // use access token from POST request above to access user
40 | let userData = await fetch('https://api.github.com/user', {
41 | method: 'GET',
42 | // pass in access token into authorization header
43 | headers: {
44 | Authorization: `token ${accessToken.access_token}`
45 | }
46 | })
47 | .then(response => response.json())
48 | .then(data => data)
49 | .catch(error => {
50 | return next({
51 | status: 500,
52 | message: error
53 | })
54 | });
55 |
56 | // db query: store access token in database under unique user _id and username
57 | const id = uuid();
58 | const query = `
59 | INSERT INTO user_sessions ("_id", "session_id", "username" )
60 | VALUES ($1, $2, $3);`;
61 | const params = [id, accessToken, userData.login];
62 | const dbResponse = await db.query(query, params)
63 | .then(response => {
64 | return response;
65 | })
66 | .catch(error => {
67 | return next({
68 | status: 500,
69 | message: error
70 | })
71 | });
72 |
73 | // bcrypt access token before storing in db or in a jwt
74 | accessToken = await bcrypt.hash(accessToken.access_token, salt);
75 |
76 | const token = jwt.sign(JSON.stringify(accessToken), process.env.USER_JWT_SECRET)
77 |
78 | res.cookie("github-token-jwt", token, {
79 | httpOnly: true,
80 | secure: true
81 | })
82 |
83 | // store access token in a jwt cookie to send back to server on Github API request
84 |
85 | // store user data in a jwt cookie to send back to server on Github API request
86 | const user = jwt.sign(JSON.stringify(userData), process.env.USER_JWT_SECRET)
87 | res.cookie("github-user-jwt", user, {
88 | httpOnly: true,
89 | secure: true
90 | })
91 |
92 | // redirect to dashboard with the username as a query paramater (to modify Redux store)
93 | return res.redirect(`https://abstractreact.herokuapp.com/dashboard/username=${userData.login}`);
94 | }
95 |
96 | //create github repo
97 | accountController.createRepo = async (req, res, next) => {
98 | // console.log('got to create Repo')
99 |
100 | //get access token for octokit
101 | const cookie = req.cookies["github-token-jwt"];
102 | const decodedCookie = jwt_decode(cookie);
103 |
104 | // get user data cookie
105 | const userData = req.cookies["github-user-jwt"];
106 | const decodeduserData = jwt_decode(userData);
107 |
108 | // get access token for user from database
109 | const query = `SELECT session_id FROM user_sessions WHERE username = $1;`
110 | const db_result = await db.query(query, [decodeduserData.login])
111 | .then(response => response)
112 | .catch(err => next(err));
113 |
114 | // extract most recent access_token from db
115 | // compare decodedCookie with access token from db (hashed)
116 | const userToken = JSON.parse(db_result.rows[db_result.rows.length - 1].session_id).access_token;
117 | const isUserValid = await bcrypt.compare(userToken, decodedCookie);
118 |
119 |
120 | // start here after lunch
121 | // if access token is valid, do every thing below
122 | if (isUserValid) {
123 | const repo_name = req.body.repository_name;
124 | const octokit = new Octokit({ auth: `${userToken}` });
125 | const createResponse = await octokit.request(`POST /user/repos`, {
126 | name: `${repo_name}`,
127 | private: true,
128 | auto_init: true,
129 | })
130 | .then(response => response)
131 | .catch(error => next({
132 | status: 500,
133 | message: error
134 | }));
135 |
136 | // console.log('create Response', createResponse);
137 | return next();
138 | } else {
139 | // if access token is not valid, console log to server the error, redirect to login page
140 | return res.redirect('https://abstractreact.herokuapp.com');
141 | }
142 | }
143 |
144 | //update repo with files
145 | accountController.updateRepo = async (req, res, next) => {
146 | try {
147 | const username = req.body.username;
148 | const commit_msg = req.body.commit_message;
149 | const repo_name = req.body.repository_name.replace(' ', '-');
150 |
151 | // decoded token
152 | const cookie = req.cookies["github-token-jwt"];
153 | const decodedCookie = jwt_decode(cookie);
154 | const prototypeCode = Buffer.from(`${req.body.prototypeCode}`, 'binary').toString('base64');
155 |
156 | // decoded user data
157 | const userData = req.cookies["github-user-jwt"];
158 | const decodeduserData = jwt_decode(userData);
159 |
160 | // get access token for user from database
161 | const query = `SELECT session_id FROM user_sessions WHERE username = $1;`
162 | const db_result = await db.query(query, [decodeduserData.login])
163 | .then(response => response)
164 | .catch(err => next(err));
165 |
166 | // extract most recent access_token from db
167 | // compare decodedCookie with access token from db (hashed)
168 | const userToken = JSON.parse(db_result.rows[db_result.rows.length - 1].session_id).access_token;
169 | const isUserValid = await bcrypt.compare(userToken, decodedCookie);
170 |
171 | if (isUserValid) {
172 | const octokit = new Octokit({ auth: `${userToken}` });
173 | const updateResponse = await octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', {
174 | owner: `${username}`,
175 | repo: `${repo_name}`,
176 | message: `${commit_msg}`,
177 | content: `${prototypeCode}`,
178 | path: `App.jsx`,
179 | })
180 | return next();
181 | } else {
182 | return res.redirect('https://abstractreact.herokuapp.com/');
183 | }
184 |
185 | }
186 | catch (error) {
187 | return next({
188 | status: 500,
189 | message: error
190 | })
191 | }
192 | }
193 |
194 |
195 | export default accountController;
--------------------------------------------------------------------------------
/src/stylesheets/Dashboard.css:
--------------------------------------------------------------------------------
1 | /* Dashboard Container - only for Desktop */
2 | @media screen and (min-width: 1367px) {
3 | body {
4 | overflow: auto;
5 | }
6 |
7 | #dashboard_container {
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 |
13 | background-color: #ACC6E3;
14 | height: 100vh;
15 | }
16 |
17 | #dashboard {
18 | transform: scale(0.8);
19 | }
20 |
21 | /* Hide scrollbar for Chrome, Safari and Opera */
22 | #component_menu::-webkit-scrollbar {
23 | display: none;
24 | }
25 |
26 | /* Hide scrollbar for IE, Edge and Firefox */
27 | #component_menu {
28 | -ms-overflow-style: none; /* IE and Edge */
29 | scrollbar-width: none; /* Firefox */
30 | }
31 |
32 | .backdrop {
33 | z-index: 9001;
34 | }
35 |
36 | .abstract_modal {
37 | z-index: 9002;
38 | }
39 |
40 | }
41 |
42 | /* Export Modal Styling*/
43 | .backdrop {
44 | display: flex;
45 | align-items: center;
46 | justify-content: center;
47 | position: fixed;
48 | top: 0;
49 | left: 0;
50 | height: 100%;
51 | width: 100%;
52 | background: #000000e1;
53 | }
54 |
55 | #canvas_backdrop {
56 | background-color: white;
57 | /* border-style: solid; */
58 | display: flex;
59 | flex-direction: column;
60 | align-items: center;
61 | justify-content: center;
62 | background: linear-gradient(90deg, black 50%, transparent 50%),
63 | linear-gradient(90deg, black 50%, transparent 50%),
64 | linear-gradient(0deg, black 50%, transparent 50%),
65 | linear-gradient(0deg, black 50%, transparent 50%);
66 | background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
67 | background-size: 16px 4px, 16px 4px, 4px 16px, 4px 16px;
68 | background-position: 0% 0%, 100% 100%, 0% 100%, 100% 0px;
69 | border-radius: 5px;
70 | padding: 10px;
71 | animation: dash 15s linear infinite;
72 | }
73 | @keyframes dash {
74 | to {
75 | background-position: 100% 0%, 0% 100%, 0% 0%, 100% 100%;
76 | }
77 | }
78 | .abstract_modal {
79 | width: clamp(50%, 800px, 90%);
80 | /* width:50%; */
81 | height: min(70%, 700px);
82 | margin: auto;
83 | padding: 0 1rem 0 5rem;
84 | border-radius: 12px;
85 | display: flex;
86 | flex-direction: column;
87 | align-items: center;
88 | justify-content: flex-start;
89 | background: #f5fbff;
90 | }
91 |
92 | #export_form {
93 | width: 90%;
94 | display: flex;
95 | flex-direction: column;
96 | justify-content: flex-start;
97 | align-content: flex-start;
98 | align-items: flex-start;
99 | }
100 |
101 | #repository_name,
102 | #commit_message {
103 | height: fit-content;
104 | }
105 |
106 | #close_modal {
107 | display: flex;
108 | justify-content: right;
109 | align-self: flex-end;
110 | align-items: flex-end;
111 | padding: 15px;
112 | border: none;
113 | background: none;
114 | font-size: 2rem;
115 | }
116 |
117 | #export_form input {
118 | width: 100%;
119 | }
120 |
121 | .export_form_container {
122 | width: 100%;
123 | display: flex;
124 | flex-direction: column;
125 | justify-content: left;
126 | align-self: flex-start;
127 | }
128 |
129 | .export_form_container h2,
130 | .export_form_container h3 {
131 | margin: .5rem 0 .5rem 0;
132 | }
133 |
134 | #component_menu h3 {
135 | padding-top: 25px;
136 | }
137 | .export_form_container input {
138 | margin: .5rem 0 .5rem 0;
139 | height: 1.25rem;
140 | }
141 |
142 | #submit_export_button {
143 | display: flex;
144 | justify-content: center;
145 | align-self: center;
146 | /* padding: 10px; */
147 | margin: 30px;
148 | background-color: #f5fbff;
149 | border: 2px solid #093A7B;
150 |
151 | }
152 |
153 | /* Dashboard Styling */
154 | #dashboard {
155 | display: grid;
156 | grid-template-columns: 1fr 3fr;
157 | }
158 |
159 | /* Component Menu Styling*/
160 | #component_menu {
161 | display: flex;
162 | flex-direction: column;
163 | align-items: center;
164 | overflow: scroll;
165 | height: 1024px;
166 | width: 306px;
167 | border: 0.25px solid;
168 | background-color: #f5fbff
169 | }
170 | #component_menu_banner {
171 | width: 100%;
172 | display:flex;
173 | flex-direction: column;
174 | align-items: center;
175 | justify-content: center;
176 | background-color: #456DAA;
177 | font-size: 20px;
178 | font-weight: bold;
179 | color: whitesmoke;
180 | height: 60px;
181 | }
182 |
183 | /* Dashboard-Right Styling */
184 | #dashboard_right {
185 | display: grid;
186 | grid-template-columns: 60% auto 100px;
187 | grid-template-rows: 124px 420px auto;
188 | grid-template-areas:
189 | "profile_button_container profile_button_container profile_button_container"
190 | "body_container body_container size_button"
191 | "body_container body_container nav_button";
192 |
193 | height: 1024px;
194 | width: 1060px;
195 | background-color: white;
196 | }
197 |
198 | #profile_button_container {
199 | grid-area: profile_button_container;
200 | grid-column-end: four;
201 | width: 100%;
202 | display: flex;
203 | flex-direction: row;
204 | align-items: center;
205 | justify-content: space-between;
206 | /* border: 2px solid #093A7B; */
207 | /* margin-right: 20px; */
208 | height: 124px;
209 | }
210 |
211 | #profile_button {
212 | display: flex;
213 | flex-direction: row;
214 | align-items: center;
215 | justify-content: center;
216 |
217 | height: 85px;
218 | width: auto;
219 | font-size: 1.25rem;
220 | font-weight: lighter;
221 |
222 | /* background-color: #f5fbff; */
223 | border-top: 1px solid #093A7B;
224 | border-bottom: 1px solid #093A7B;
225 |
226 | padding-left: 20px;
227 | padding-right: 20px;
228 | margin-bottom: 0px;
229 | margin-right: 12px;
230 | }
231 |
232 | #export_modal_button {
233 | height: 85px;
234 | width: auto;
235 | padding-left: 10px;
236 | padding-right: 10px;
237 | background-color: #f5fbff;
238 | border: 1px solid #093A7B;
239 | font-size: 1.25rem;
240 | }
241 |
242 | #body_container {
243 | grid-area: body_container;
244 |
245 | display: flex;
246 | flex-direction: column;
247 | align-items: center;
248 | justify-content: flex-start;
249 | height: 878px;
250 | width: 100%;
251 | background-color: white;
252 | }
253 |
254 | #code_preview {
255 | background-color: #2E4053;
256 | height: 700px;
257 | width: 700px;
258 | overflow-y: scroll;
259 | padding: 0px 25px 0px 25px;
260 | }
261 |
262 | #size_button_container {
263 | grid-area: size_button;
264 | grid-column-end: one;
265 | display: flex;
266 | flex-direction: column;
267 | align-items: flex-end;
268 | justify-content: flex-start;
269 | margin-top: 48px;
270 | margin-right: 12px;
271 | }
272 |
273 | #size_button_container button {
274 | width: 85px;
275 | height: 85px;
276 |
277 | background-color: #f5fbff;
278 | border: 1px solid #093A7B;
279 |
280 | margin-bottom: 24px;
281 | }
282 |
283 | #size_button:active {
284 | transform: scale(1.1);
285 | }
286 |
287 | #size_button_container svg {
288 | width: 48px;
289 | height: 48px;
290 | }
291 |
292 | #nav_buttons {
293 | grid-area: nav_button;
294 | grid-column-end: one;
295 | display: flex;
296 | flex-direction: column;
297 | align-items: flex-end;
298 | justify-content: flex-start;
299 | margin-right: 12px;
300 | }
301 |
302 | #canvas_button,
303 | #preview_button {
304 | width: 85px;
305 | height: 85px;
306 |
307 | background-color: #f5fbff;
308 | border: 1px solid #093A7B;
309 |
310 | margin-bottom: 24px;
311 | }
312 |
313 | #canvas_button:active,
314 | #preview_button:active {
315 | transform: scale(1.1);
316 | }
317 |
318 | #nav_buttons svg {
319 | width: 48px;
320 | height: 48px;
321 | }
322 |
323 | #gitHubLogo {
324 | width: 40px;
325 | height: 40px;
326 | padding-right: 10px;
327 | }
328 |
329 | .compMenuBtn {
330 | width: 100%;
331 | margin-top: 15px;
332 | height: 40px;
333 | border-top: 2px solid #093A78;
334 | border-bottom: 2px solid #093A78;;
335 | border-left: none;
336 | border-right: none;
337 | font-size: 1.25rem;
338 | font-weight: lighter;
339 | }
340 |
341 | .compMenuBtn:hover {
342 | color: #3e3eff;
343 |
344 | }
345 |
346 | #library-div {
347 | margin-top: 25px;
348 | display: grid;
349 | grid-template-columns: repeat(2, 90%);
350 | grid-template-rows: repeat(3, auto);
351 | justify-items: center;
352 | justify-content: center;
353 | text-align: center;
354 | }
355 |
356 | #componentIcon {
357 | width: 50px;
358 | height: 50px;
359 | }
360 |
361 | #conditionalForm {
362 | padding-top: 25px;
363 | display: grid;
364 | justify-items: center
365 | }
366 |
367 | .componentButton:hover {
368 | color: #3e3eff;
369 | }
370 | .componentButton:focus {
371 | color: #3e3eff;
372 | border-color: #456DAA;
373 | }
374 | #addButton {
375 | padding: 7px 10px 7px 10px;
376 | margin-top: 40px;
377 | width: 150px;
378 | color: white;
379 | background-color: #2B2929;
380 | }
381 |
382 | #h2 {
383 | font-size: 1rem;
384 | }
385 |
386 | @media (min-width: 0px) {
387 | #dashboard_right {
388 | width: auto;
389 | height: auto
390 | }
391 |
392 | #body_container {
393 | width: auto;
394 | }
395 |
396 | }
--------------------------------------------------------------------------------
/readme-assets/Asset_2.svg:
--------------------------------------------------------------------------------
1 | A B S T R A C T
--------------------------------------------------------------------------------
/readme-assets/Asset_10.svg:
--------------------------------------------------------------------------------
1 | BET A A B S T R A C T
--------------------------------------------------------------------------------
/public/images/Asset_11.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ComponentMenu/ComSettings.jsx:
--------------------------------------------------------------------------------
1 | import AddButton from './AddButton.jsx';
2 | import { connect } from 'react-redux';
3 | import { Form, Dropdown, DropdownButton } from 'react-bootstrap';
4 | import { useState } from 'react';
5 | import 'bootstrap/dist/css/bootstrap.min.css'
6 |
7 | const mapStateToProps = (state) => ({
8 | selectedComponent: state.main.selectedComponent
9 | });
10 |
11 |
12 | function ComSettings(props) {
13 | const [ methods, setMethods ] = useState('')
14 |
15 | switch (props.selectedComponent) {
16 | case 'div':
17 | return (
18 |
35 | )
36 | case 'link':
37 | return (
38 |
54 | )
55 | case 'image':
56 | return (
57 |
77 | )
78 | case 'paragraph':
79 | return (
80 |
94 | )
95 | case 'button':
96 | return (
97 |
115 | )
116 | case 'form':
117 | function renderFormSettings() {
118 | // remove X in if statement methods when functionality works
119 | if (methods === 'XForm.Control') {
120 | return (
121 |
146 | )
147 | // remove X in if statement methods when functionality works
148 | } else if (methods === 'XForm.Group') {
149 | return (
150 |
166 | )
167 | // remove X in if statement methods when functionality works
168 | } else if (methods === 'XForm.Label') {
169 | return (
170 |
186 | );
187 | } else if (methods === 'Form') {
188 | return (
189 |
201 | )
202 | }
203 | } return (
204 |
205 |
206 | setMethods('Form')}>Form
207 | setMethods('Form.Control')}>Form.Control
208 | setMethods('Form.Group')}>Form.Group
209 | setMethods('Form.Label')}>Form.Label
210 |
211 | { renderFormSettings() }
212 |
213 | )
214 |
215 | case 'navbar':
216 | function renderNavbarSettings() {
217 | if (methods === 'Navbar') {
218 | return (
219 |
261 | )
262 | } else if (methods === 'Navbar.Brand') {
263 | return (
264 |
280 | )
281 | } else if (methods === 'Navbar.Toggle') {
282 | return (
283 |
300 | )
301 | } else if (methods === 'Navbar.Collapse') {
302 | return (
303 |
317 | )
318 | }
319 | }
320 | return (
321 |
322 |
323 | setMethods('Navbar')}>Navbar
324 | setMethods('Navbar.Brand')}>Navbar.Brand
325 | setMethods('Navbar.Toggle')}>Navbar.Toggle
326 | setMethods('Navbar.Collapse')}>Navbar.Collapse
327 |
328 | { renderNavbarSettings() }
329 |
330 | )
331 | case 'nav':
332 | function renderNavSettings() {
333 | if (methods === 'Nav') {
334 | return (
335 |
351 | )
352 | } else if (methods === 'Nav.Link') {
353 | return (
354 |
370 | )
371 | } else if (methods === 'Nav.DropDown') {
372 | return (
373 |
389 | )
390 | } else if (methods === 'Nav.DropDownItem') {
391 | return (
392 |
408 | )
409 | }
410 | }
411 | return (
412 |
413 |
414 | setMethods('Nav')}>Nav
415 | setMethods('Nav.Link')}>Nav.Link
416 | setMethods('Nav.DropDown')}>Navbar.DropDown
417 | setMethods('Nav.DropDownItem')}>Navbar.DropDownItem
418 |
419 | { renderNavSettings() }
420 |
421 | )
422 | case 'list':
423 | function renderListSettings() {
424 | if (methods === 'List') {
425 | return (
426 |
442 | )
443 | } else if (methods === 'List.Group') {
444 | return (
445 |
446 |
List.Group
447 |
449 | Parent Component
450 |
451 | 1
452 |
453 | Class Name
454 |
455 |
456 |
457 |
458 | )
459 | }
460 | }
461 | return (
462 |
463 |
464 | setMethods('List')}>List
465 | setMethods('List.Group')}>List.Group
466 |
467 |
468 | { renderListSettings() }
469 |
470 | )
471 | default:
472 | return (
473 |
474 |
475 | )
476 |
477 | }
478 | }
479 |
480 |
481 | export default connect(mapStateToProps, null)(ComSettings);
482 |
--------------------------------------------------------------------------------