├── .babelrc
├── .editorconfig
├── .eslintrc.js
├── .github
└── ISSUE_TEMPLATE
│ └── issue_template-md.md
├── .gitignore
├── .npmignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── __tests__
├── Header.test.js
├── LeftPanel.test.js
├── Navigation.test.js
└── redux.test.js
├── main.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── assets
│ ├── GitHub_README_logo.png
│ ├── clearCanvas.gif
│ ├── cssEditor.gif
│ ├── customComps.gif
│ ├── darkMode.gif
│ ├── dnd.gif
│ ├── export.gif
│ ├── splash.html
│ ├── splashDemo.gif
│ └── splashscreen.gif
├── components
│ ├── App.jsx
│ ├── Body.jsx
│ ├── CSSCodeEditor.jsx
│ ├── Canvas.jsx
│ ├── CanvasItem.jsx
│ ├── CodePreview.jsx
│ ├── CompCreator.jsx
│ ├── CustomComponents.jsx
│ ├── DeleteCanvasItem.jsx
│ ├── DnD.jsx
│ ├── DragList.jsx
│ ├── ExportApp.jsx
│ ├── ExportFiles.jsx
│ ├── ExportModal.jsx
│ ├── Header.jsx
│ ├── JSCodeEditor.jsx
│ ├── Login.jsx
│ ├── Navigation.jsx
│ ├── TabContainer.jsx
│ ├── TagCreator.jsx
│ ├── TerminalView.jsx
│ ├── Tree.jsx
│ ├── TreeFile.jsx
│ ├── TreeFolder.jsx
│ └── TreeRecursive.jsx
├── electron
│ ├── menu.js
│ └── preload.js
├── index.js
├── localForage.js
├── redux
│ ├── canvasSlice.js
│ ├── componentCodeSlice.js
│ ├── fileTreeSlice.js
│ ├── navigationSlice.js
│ ├── store.js
│ ├── tagsSlice.js
│ └── themeSlice.js
├── server
│ ├── server.js
│ ├── userController.js
│ └── userModel.js
├── styleMock.js
├── stylesheets
│ ├── App.css
│ ├── BodyContainer.css
│ ├── Canvas.css
│ ├── CodePreview.css
│ ├── CompCreator.css
│ ├── CustomComponents.css
│ ├── DnD.css
│ ├── DragList.css
│ ├── ExportModal.css
│ ├── Header.css
│ ├── Login.css
│ ├── Navigation.css
│ ├── TagCreator.css
│ ├── Terminal.css
│ └── index.css
└── test-utils.js
├── styleMock.js
├── tailwind.config.js
├── webpack.build.config.js
├── webpack.dev.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-react",
4 | "@babel/preset-env"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = crlf
8 | indent_size = 2
9 | indent_style = space
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['airbnb', 'prettier', 'prettier/react'],
3 | parser: '@babel/eslint-parser',
4 | root: true,
5 | env: {
6 | browser: true,
7 | node: true,
8 | jest: true,
9 | },
10 | rules: {
11 | 'indent': ['warn', 2],
12 | 'quotes': ['warn', 'single'],
13 | 'arrow-parens': 'off',
14 | 'consistent-return': 'off',
15 | 'func-names': 'off',
16 | 'no-console': 'off',
17 | 'prefer-const': 'warn',
18 | 'radix': 'off',
19 | 'no-unused-vars': ["off", { "vars": "local" }],
20 | 'react/button-has-type': 'off',
21 | 'react/destructuring-assignment': 'off',
22 | 'react/jsx-filename-extension': 'off',
23 | 'react/prop-types': 'off',
24 | },
25 | plugins: [
26 | 'react-hooks'
27 | ],
28 | ignorePatterns: ["**/test", "**/__tests__"],
29 | };
30 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/issue_template-md.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: issue_template.md
3 | about: Create an issue for contributors to work on
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Context (current result)
11 | [provide more detailed introduction to the issue itself and why it is relevant]
12 |
13 | ## Process to reproduce issue
14 | [ordered list the process to finding and recreating the issue, example below]
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | ## Expected Result
21 | [A clear and concise description of what you expected to happen.]
22 |
23 | ## Screenshots
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | ### Desktop (please include the following information if relevant to the issue):
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | Smartphone (please complete the following information):
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | ## Possible Fix
38 | [not obligatory, but suggest fixes or reasons for the bug]
39 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build folder and files #
2 | builds/
3 |
4 | # Development folders and files #
5 | .tmp/
6 | dist/
7 | node_modules/
8 | *.compiled.*
9 |
10 | # Folder config file #
11 | Desktop.ini
12 |
13 | # Log files & folders #
14 | logs/
15 | *.log
16 | npm-debug.log*
17 | .npm
18 |
19 | # New Features under Development #
20 | **Login
21 |
22 |
23 | # Packages #
24 | # it's better to unpack these files and commit the raw source
25 | # git has its own built in compression methods
26 | *.7z
27 | *.dmg
28 | *.gz
29 | *.iso
30 | *.jar
31 | *.rar
32 | *.tar
33 | *.zip
34 |
35 | # Windows & Mac file caches #
36 | .DS_Store
37 | Thumbs.db
38 | ehthumbs.db
39 |
40 | # Windows shortcuts #
41 | *.lnk
42 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | _ignore/
2 | docs/
3 | builds/
4 | dist/
5 | .editorconfig
6 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "printWidth": 160,
7 | "jsxSingleQuote": true,
8 | "useTabs": false,
9 | "useEditorConfig": true,
10 | "htmlWhitespaceSensitivity": "css",
11 | "bracketSpacing": true,
12 | "arrowParens": "always"
13 | }
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) fflow.
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 copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Give a ⭐️ if our project helped or interests you!
31 |
32 |
33 |
34 |
35 |
36 | Table of Contents
37 |
38 |
39 | About fflow
40 |
44 |
45 |
46 | Getting Started
47 |
51 |
52 | Run Exported Project
53 | Contributors
54 | Roadmap
55 | Contributing Guide
56 | License
57 |
58 |
59 |
60 | ## About fflow
61 |
62 | React is the most popular library used by frontend developers today. Yet, getting a React application started requires a too much boilerplate code and unnecessary time.
63 |
64 | fflow is a free, open-source developer tool to create React applications rapidly and with minimal effort using the simple drag and drop UI. It combines the most compelling features of Create React App, React ES6 snippets, and a beautiful user experience. We are really excited to launch this alpha version and hope you will download, play around with it, and provide us with feedback.
65 |
66 | [Here](https://jpino831.medium.com/ease-into-your-next-react-app-with-fflow-f60c5a899817) is a medium article describing the philosophy behind fflow and you can download it [here](https://github.com/oslabs-beta/fflow/releases).
67 |
68 | Visit our website here 👉 https://fflow.dev
69 |
70 |
71 |
72 | ### Features
73 |
74 | - Drag, Drop, Reorder and Delete HTML Tags
75 | - Create Custom React Components
76 | - Light and Dark Theme based on OS Preferences and Manual Toggle
77 | - Delete Project and Clear Canvas to restart build process
78 | - Live preview in Code editor
79 | - Interactable CSS Code Editor
80 | - Easily switch between files with file tree or on the canvas
81 | - Easy redirect to App.jsx by clicking on the fflow icon
82 | - Export app with preconfigured and versatile webpack
83 | - In-built terminal initializes an instance of the exported app without leaving fflow
84 | - Accessibility features including canvas item highlighting when the HTML tag or Component is picked up
85 |
86 | ### Built with
87 |
88 | - [React.js](https://reactjs.org/)
89 | - [Electron](https://www.electronjs.org/)
90 | - [React-Monaco/Editor](https://github.com/react-monaco-editor/react-monaco-editor)
91 | - [Webpack 5](https://webpack.js.org)
92 | - [TailwindUI](https://tailwindui.com)
93 | - [React–Redux](https://react-redux.js.org)
94 | - [Redux Toolkit](https://redux-toolkit.js.org)
95 | - [React Beautiful DnD](https://github.com/atlassian/react-beautiful-dnd)
96 | - [React Icons](https://react-icons.github.io/react-icons/)
97 | - [Xterm.js](https://xtermjs.org/)
98 | - [node-pty](https://www.npmjs.com/package/node-pty)
99 | - [Jest](https://jestjs.io)
100 | - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/)
101 |
102 | (back to top )
103 |
104 | ### Loading Screen
105 |
106 |
107 |
108 | ### Light and Dark Mode
109 |
110 |
111 |
112 | ### Create Custom Components and Nest HTML elements
113 |
114 |
115 |
116 | ### Edit CSS directly in the App (Currently only on Mac version)
117 |
118 | 💫💫 Coming soon to Windows 💫💫
119 |
120 |
121 | ### Export Working Project
122 |
123 |
124 |
125 | ### Clear canvas to start from scratch
126 |
127 |
128 |
129 | ## Getting Started
130 |
131 | The following instructions are split into two sections for:
132 |
133 | - [Production Mode](#production-mode)
134 | - [Development Mode](#development-mode)
135 |
136 | ## Production Mode
137 |
138 | Please download the latest release of fflow for [MacOS](https://github.com/oslabs-beta/fflow/releases) or [Windows](https://github.com/oslabs-beta/fflow/releases).
139 |
140 |
141 |
142 | 💫 Linux version coming soon. 💫
143 |
144 | - **Mac users**: After opening the dmg and dragging fflow into your Applications folder, `CTL + Click` the icon and 'Open' from the context menu to open the app. This extra step is necessary since we don't have an Apple developer license yet.
145 |
146 | - **Windows users**: Install the application by running fflow setup 1.0.0.exe.
147 |
148 |
149 |
150 | #### Clone this repo
151 |
152 | 1. Clone this repo `git clone https://github.com/oslabs-beta/fflow`
153 | 2. Install the dependencies `npm install`
154 | 3. On a Mac, run `./node_modules/.bin/electron-rebuild` in your terminal or `.\node_modules\.bin\electron-rebuild.cmd` on Windows
155 | 4. Run script for development mode `npm run start`
156 | 5. Build the app (automatic) `npm run package`
157 | 6. Test the app (after `npm run build` || `yarn run build`) `npm run prod`
158 |
159 | #### Current issues
160 |
161 | Note there is currently an issue where the Monaco Code Editor keeps showing 'loading...' in development mode. We are working to solve this. In the meantime, when you drag HTML tags onto canvas you can still see the code editor's contents in the Developer Tools.
162 |
163 | #### Running tests
164 |
165 | Electron, React and Redux tests can be run using `npm run test`
166 |
167 | (back to top )
168 |
169 | ## Run Exported Project
170 |
171 | Below is the generated directory structure of the React and Webpack application that is created when you export your application.
172 |
173 | ```
174 | ├── dist # Compiled files
175 | │ └── index.html
176 | ├── src # Source files
177 | │ └── App.js
178 | │ └── index.js
179 | │ └── styles.css
180 | ├── webpack.config.js # Webpack configuration
181 | ├── .babelrc # Babel configuration
182 | ├── .gitignore # Git ignore file
183 | ├── package.json # Dependencies file
184 | └── README.md # Boilerplate README file
185 |
186 | ```
187 |
188 | 1. Using the inbuilt terminal, `cd` into the Exported Project in your `Downloads` folder
189 | 2. In the root of the Exported Project folder, install dependencies `npm install`
190 | 3. Start an instance `npm run start`
191 | 4. If `Localhost:8080` does not open automatically in your default browser, navigate to the specified port to see your running app with Hot Module Reloading and a pre-configured webpack
192 |
193 |
194 |
195 | ### Add Sass
196 |
197 | Adding Sass to your exported project requires updating webpack configs and adding necessary loaders.
198 |
199 | 1. Install loaders for sass, `sass-loader` and `node-sass`
200 | 2. Add another object to the rules in `webpack.config` with the following:
201 |
202 | ```javascript
203 | {
204 | test: /\.scss$/,
205 | use: [
206 | 'style-loader',
207 | 'css-loader',
208 | 'sass-loader'
209 | ]
210 | }
211 | ```
212 |
213 | 3. Rename `styles.css` to `styles.scss`
214 |
215 | ### Add Tailwind
216 |
217 | Adding Tailwind to your exported project requires updating webpack configs, adding necessary loaders and tailwind config.
218 |
219 | 1. Install tailwind as a dependency
220 |
221 | ```
222 | npm install tailwindcss
223 | ```
224 |
225 | 2. Install postcss-loader and autoprefixer as development dependencies
226 |
227 | ```
228 | npm install -D postcss-loader autoprefixer
229 | ```
230 |
231 | 3. Copy the following copy into `src/styles.css`
232 |
233 | ```css
234 | @tailwind base;
235 |
236 | @tailwind components;
237 |
238 | @tailwind utilities;
239 | ```
240 |
241 | 4. Create a `postcss.config.js` file and copy the following code in
242 |
243 | ```javascript
244 | module.exports = {
245 | plugins: [require('tailwindcss'), require('autoprefixer')],
246 | };
247 | ```
248 |
249 | 5. Add `'postcss-loader'` to the css rules in `webpack.config`
250 |
251 | ### Change App Title
252 |
253 | This boilerplate names your project `Exported Project`. You can change the app title by inserting the app title within the ` ` tags in `index.html`.
254 |
255 | (back to top )
256 |
257 | ## Contributors
258 |
259 |
260 |
261 |
262 |
263 | - Rain Hsu [@crumblepie](https://github.com/crumblepie) | [Linkedin](https://www.linkedin.com/in/rainhsu/)
264 | - Jake Pino [@jakepino](https://github.com/jakepino) | [Linkedin](https://www.linkedin.com/in/jakepino/)
265 | - Bryanna DeJesus [@BryannaDeJesus](https://github.com/BryannaDeJesus) | [Linkedin](https://www.linkedin.com/in/bryannadejesus/)
266 | - Ronak Hirpara [@ronakh130](https://github.com/ronakh130) | [Linkedin](https://www.linkedin.com/in/ronak-hirpara/)
267 |
268 | Project Links: [Github](https://github.com/oslabs-beta/fflow) | [Linkedin](https://www.linkedin.com/company/fflowdev) | [Media](https://jpino831.medium.com/ease-into-your-next-react-app-with-fflow-f60c5a899817)
269 |
270 | (back to top )
271 |
272 | ## Roadmap
273 |
274 | - Editable JS Code Preview
275 | - Nesting HTML elements
276 | - GitHub OAuth Integration and online project save
277 | - Keyboard shortcuts for Mac and Windows
278 | - Export in TypeScript
279 | - Save Multiple Projects
280 | - Create Custom HTML Elements
281 | - Open and compose multiple projects in different windows simultaneously
282 | - BrowserView to preview project
283 | - Live Editing with other collaborators
284 | - AWS hosted version
285 |
286 | (back to top )
287 |
288 | ## Contributing Guide
289 |
290 | Contributions are what make the open source community an amazing place to learn, inspire, and create. Any contributions are greatly appreciated.
291 |
292 | If you have a suggestion of how to make fflow better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement".
293 |
294 | 1. Fork this repo
295 | 2. Create your Feature Branch (`git checkout -b` yourgithubhandle/AmazingFeature)
296 | 3. Commit your Changes (`git commit -m` 'Add some AmazingFeature')
297 | 4. Create and push to your remote branch (`git push origin` yourgithubhandle/AmazingFeature)
298 | 5. Open a Pull Request
299 |
300 | (back to top )
301 |
302 |
303 |
304 | ## License
305 |
306 | Licensed under MIT License © [fflow](fflow.dev).
307 |
--------------------------------------------------------------------------------
/__tests__/Header.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent } from '../src/test-utils.js';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import Header from '../src/components/Header';
5 |
6 | // issue: unable to implement window.confirm function for test
7 | xdescribe('Header', () => {
8 | test('renders Header component with Clear Project button with on click functionality', () => {
9 | const { getByTestId } = render();
10 | const mockFunction = jest.fn(() => true);
11 | const clearProjectButton = getByTestId('clear-project-button');
12 | expect(clearProjectButton).toBeInTheDocument();
13 | fireEvent.click(clearProjectButton);
14 | expect(mockFunction()).toBe(true);
15 | });
16 |
17 | test('renders Header component with light/dark mode Toggle with on click functionality', () => {
18 | const { getByTestId } = render();
19 | const mockFunction = jest.fn(() => true);
20 | const themeToggle = getByTestId('toggle-theme-btn');
21 | expect(themeToggle).toBeInTheDocument();
22 | fireEvent.click(themeToggle);
23 | expect(mockFunction()).toBe(true);
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/__tests__/LeftPanel.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent } from '../src/test-utils.js';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import CompCreator from '../src/components/CompCreator';
5 | import Tree from '../src/components/Tree';
6 |
7 | describe('Left Panel', () => {
8 | test('value of comp creator input field changes', () => {
9 | const { queryByPlaceholderText } = render( );
10 | const input = queryByPlaceholderText('Component Name');
11 | fireEvent.change(input, { target: { value: 'test' } });
12 | expect(input.value).toBe('test');
13 | });
14 |
15 | test('renders the tree component which displays file tree', () => {
16 | const { getByTestId } = render( );
17 | expect(getByTestId('filetree')).toBeInTheDocument();
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/__tests__/Navigation.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent } from '../src/test-utils.js';
3 | import '@testing-library/jest-dom/extend-expect';
4 | import Navigation from '../src/components/Navigation';
5 |
6 | describe('Navigation', () => {
7 | test('renders Navigation component with DnD icon button with on click functionality', () => {
8 | const { getByTestId } = render( );
9 | const dndButton = getByTestId('dnd-button');
10 | expect(dndButton).toBeInTheDocument();
11 | const mockFunction = jest.fn(() => true);
12 | fireEvent.click(dndButton);
13 | expect(mockFunction()).toBe(true);
14 | });
15 |
16 | test('renders Navigation component with file tree icon button with on click functionality', () => {
17 | const { getByTestId } = render( );
18 | const fileTreeButton = getByTestId('filetree-button');
19 | expect(fileTreeButton).toBeInTheDocument();
20 | const mockFunction = jest.fn(() => true);
21 | fireEvent.click(fileTreeButton);
22 | expect(mockFunction()).toBe(true);
23 | });
24 |
25 | test('renders Navigation component with export icon button with on click functionality', () => {
26 | const { getByTestId } = render( );
27 | const exportButton = getByTestId('export-button');
28 | expect(exportButton).toBeInTheDocument();
29 | const mockFunction = jest.fn(() => true);
30 | fireEvent.click(exportButton);
31 | expect(mockFunction()).toBe(true);
32 | });
33 |
34 | test('renders Navigation component with trash icon button with on click functionality', () => {
35 | const { getByTestId } = render( );
36 | const trashButton = getByTestId('trash-button');
37 | expect(trashButton).toBeInTheDocument();
38 | const mockFunction = jest.fn(() => true);
39 | fireEvent.click(trashButton);
40 | expect(mockFunction()).toBe(true);
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/__tests__/redux.test.js:
--------------------------------------------------------------------------------
1 | import reducer, { addComponent, clearProject, createComponent, reorderComponent, setCurrentFile } from '../src/redux/canvasSlice';
2 |
3 | describe('canvasSlice reducers', () => {
4 | let initialState;
5 |
6 | function addCompTest(state, action) {
7 | state.components.splice(action.destination.index, 0, action.draggableId);
8 | state.tags.splice(action.destination.index, 0, '\n\t\t\t' + state.codeList[action.draggableId]);
9 | return state;
10 | }
11 |
12 | function createAction(end, id, start) {
13 | return {
14 | destination: {
15 | index: end,
16 | },
17 | source: {
18 | index: start,
19 | },
20 | draggableId: id,
21 | name: id,
22 | };
23 | }
24 |
25 | beforeEach(() => {
26 | initialState = {
27 | components: [],
28 | code: '',
29 | cssCode: `html {
30 | box-sizing: border-box;
31 | height: 100%;
32 | }
33 | body {
34 | margin: 0;
35 | padding-top: 20%;
36 | overflow: hidden;
37 | background-color: #272727;
38 | font-family: "Helvetica Neue";
39 | display: flex;
40 | justify-content: center;
41 | text-align: center;
42 | height: 100%;
43 | }
44 | h1 {
45 | color: white;
46 | font-size: 3rem;
47 | }
48 | p {
49 | color: white;
50 | font-size: 1.5rem;
51 | }
52 | .default-spans {
53 | color: #4338ca;
54 | }`,
55 | tags: [],
56 | customComponents: [],
57 | imports: ["import React from 'react';\n"],
58 | codeList: {
59 | Div: `
`,
60 | Paragraph: `
`,
61 | Anchor: ` `,
62 | Image: ` `,
63 | 'Unordered List': ``,
64 | Form: ``,
65 | Input: ` `,
66 | 'Ordered List': ` `,
67 | Button: ` `,
68 | 'List Item': ` `,
69 | Span: ` `,
70 | 'Header 1': ` `,
71 | 'Header 2': ` `,
72 | 'Header 3': ` `,
73 | 'Line Break': ` `,
74 | Table: ``,
75 | THead: ` `,
76 | },
77 | files: [
78 | {
79 | type: 'file',
80 | name: 'App.js',
81 | fileCode: '',
82 | fileTags: [],
83 | fileImports: [],
84 | fileComponents: [],
85 | },
86 | {
87 | type: 'file',
88 | name: 'styles.css',
89 | fileCode: '',
90 | fileTags: [],
91 | fileImports: [],
92 | fileComponents: [],
93 | },
94 | ],
95 | currentFile: 'App.js',
96 | };
97 | });
98 |
99 | // issue: test skipped until fixed
100 | xdescribe('addComponent:', () => {
101 | test('should return the initial state', () => {
102 | expect(reducer(undefined, {})).toEqual(initialState);
103 | });
104 |
105 | test('should add to initial state', () => {
106 | const action = createAction(0, 'Div');
107 | expect(reducer(initialState, addComponent(action))).toEqual(addCompTest(initialState, action));
108 | });
109 |
110 | test('should add to end of array', () => {
111 | const action = createAction(1, 'Button');
112 | expect(reducer(initialState, addComponent(action))).toEqual(addCompTest(initialState, action));
113 | });
114 |
115 | test('should add to middle of array', () => {
116 | const action = createAction(1, 'Anchor');
117 | expect(reducer(initialState, addComponent(action))).toEqual(addCompTest(initialState, action));
118 | });
119 | });
120 |
121 | describe('reorderComponent:', () => {
122 | test('should swap 2 items', () => {
123 | const action = createAction(1, 'Button', 2);
124 | const comps = ['Div', 'Button', 'Anchor'];
125 | const tags = comps.map((ele) => '\n\t\t\t' + initialState.codeList[ele]);
126 | addCompTest(initialState, createAction(0, 'Div'));
127 | addCompTest(initialState, createAction(1, 'Anchor'));
128 | addCompTest(initialState, createAction(2, 'Button'));
129 | expect(reducer(initialState, reorderComponent(action))).toEqual({ ...initialState, components: comps, tags: tags });
130 | });
131 | });
132 | // issue: test skipped until fixed
133 | xdescribe('clearProject:', () => {
134 | test('should reset initial state', () => {
135 | addCompTest(initialState, createAction(0, 'Div'));
136 | const newState = {
137 | ...initialState,
138 | components: [],
139 | tags: [],
140 | };
141 | expect(reducer(initialState, clearProject())).toEqual(newState);
142 | });
143 | });
144 |
145 | describe('createComponent:', () => {
146 | test('should create new custom component', () => {
147 | const state = {
148 | ...initialState,
149 | tags: ['\n\t\t\t '],
150 | customComponents: ['TestFile'],
151 | components: ['TestFile'],
152 | imports: ["import React from 'react';\n", "import TestFile from './TestFile.jsx';\n"],
153 | files: [
154 | {
155 | type: 'file',
156 | name: 'App.js',
157 | fileCode: '',
158 | fileTags: [],
159 | fileImports: [],
160 | fileComponents: [],
161 | },
162 | {
163 | type: 'file',
164 | name: 'styles.css',
165 | fileCode: '',
166 | fileTags: [],
167 | fileImports: [],
168 | fileComponents: [],
169 | },
170 | {
171 | type: 'file',
172 | name: 'TestFile.jsx',
173 | fileCode: `import React from 'react';\n\nconst TestFile = () => {\n\treturn (\n\t\t\n\t\t
\n\t)\n}\nexport default TestFile;`,
174 | fileTags: [],
175 | fileImports: ["import React from 'react';\n"],
176 | fileComponents: [],
177 | },
178 | ],
179 | };
180 | expect(reducer(initialState, createComponent({ text: 'TestFile' }))).toEqual(state);
181 | });
182 | });
183 |
184 | describe('setCurrentFile:', () => {
185 | test('should update state', () => {
186 | expect(reducer(initialState, setCurrentFile('TestFile.jsx'))).toEqual({ ...initialState, currentFile: 'TestFile.jsx' });
187 | });
188 | });
189 | });
190 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | // Import parts of electron to use
3 | const { app, BrowserWindow, ipcMain } = require('electron');
4 | const Store = require('electron-store');
5 | const os = require('os');
6 | const path = require('path');
7 | const url = require('url');
8 | const pty = require('node-pty');
9 |
10 | // Determines the type of shell needed for the terminal based on the user's platform
11 | const shell = os.platform === 'win32' ? 'powershell.exe' : 'zsh';
12 |
13 | // Keep a global reference of the window object, if you don't, the window will
14 | // be closed automatically when the JavaScript object is garbage collected.
15 | let mainWindow;
16 |
17 | // Keep a reference for dev mode
18 | let dev = false;
19 |
20 | if (process.env.NODE_ENV !== undefined && process.env.NODE_ENV === 'development') {
21 | dev = true;
22 | }
23 |
24 | const storage = new Store();
25 |
26 | const createWindow = () => {
27 |
28 | const getWinSettings = () => {
29 | //Gets and stores the previous window's size upon close and restores them
30 | const defaultBounds = [1280, 1024];
31 | const size = storage.get('win-size');
32 |
33 | if (size) return size;
34 | else {
35 | storage.set('win-size', defaultBounds);
36 | return defaultBounds;
37 | }
38 | };
39 |
40 | const bounds = getWinSettings();
41 |
42 | const saveBounds = (bounds) => {
43 | storage.set('win-size', bounds);
44 | };
45 |
46 | // Create the browser window.
47 | mainWindow = new BrowserWindow({
48 | // width: bounds[0],
49 | // height: bounds[1],
50 | width: 1440,
51 | height: 880,
52 | show: false,
53 | webPreferences: {
54 | nodeIntegration: true,
55 | contextIsolation: false,
56 | },
57 | backgroundColor: '#121212',
58 | minWidth: 850,
59 | minHeight: 600,
60 | maxWidth: 1440,
61 | maxHeight: 880,
62 | });
63 |
64 | // and load the index.html of the app.
65 | let indexPath;
66 |
67 | if (dev && process.argv.indexOf('--noDevServer') === -1) {
68 | indexPath = url.format({
69 | protocol: 'http:',
70 | host: 'localhost:8080',
71 | pathname: 'index.html',
72 | slashes: true,
73 | });
74 | } else {
75 | indexPath = url.format({
76 | protocol: 'file:',
77 | pathname: path.join(__dirname, 'dist', 'index.html'),
78 | slashes: true,
79 | });
80 | }
81 |
82 | mainWindow.loadURL(indexPath);
83 |
84 | // For the Terminal
85 | const ptyProcess = pty.spawn(shell, [], {
86 | name: 'xterm-color',
87 | rows: 25,
88 | cols: 70,
89 | cwd: process.env.HOME,
90 | env: process.env,
91 | });
92 |
93 | // Send incoming data to the Terminal
94 | ptyProcess.on('data', (data) => {
95 | mainWindow.webContents.send('terminal.sentData', data);
96 | });
97 | // in the main process, when data is received in the terminal,
98 | // main process writes and adds to ptyProcess
99 | ipcMain.on('terminal.toTerm', (event, data) => {
100 | ptyProcess.write(data);
101 | console.log(data, `being written in ptyMain: ${data}`);
102 | });
103 |
104 | // in main process, create new window for splash screen
105 | var splash = new BrowserWindow({
106 | width: 500,
107 | height: 300,
108 | transparent: true,
109 | frame: false,
110 | alwaysOnTop: true,
111 | });
112 | // load splashscreen content into created window
113 | splash.loadFile('./src/assets/splash.html');
114 |
115 | // Don't show main app window until we are ready and loaded
116 | mainWindow.once('ready-to-show', () => {
117 | // splashscreen to show for 3 seconds minimum
118 | setTimeout(() => {
119 | mainWindow.center();
120 | splash.destroy();
121 | // uncomment the below and comment out the maximum and minimum height and width
122 | // in main window to maximise the app
123 | // mainWindow.maximize();
124 | mainWindow.show();
125 | mainWindow.focus();
126 | }, 3000);
127 |
128 | // Open the DevTools automatically if developing
129 | if (dev) {
130 | const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer');
131 |
132 | installExtension(REACT_DEVELOPER_TOOLS).catch((err) => console.log('Error loading React DevTools: ', err));
133 | mainWindow.webContents.openDevTools();
134 | }
135 | });
136 |
137 | mainWindow.on('resize', () => saveBounds(mainWindow.getSize()));
138 | // Emitted when the window is closed.
139 | mainWindow.on('closed', () => {
140 | // Dereference the window object, usually you would store windows
141 | // in an array if your app supports multi windows, this is the time
142 | // when you should delete the corresponding element.
143 | // mainWindow.webContents.send('terminal.close', () => {
144 | // });
145 | ptyProcess.kill();
146 | mainWindow = null;
147 | });
148 | };
149 |
150 | // This method will be called when Electron has finished
151 | // initialization and is ready to create browser windows.
152 | // Some APIs can only be used after this event occurs.
153 | app.on('ready', createWindow);
154 |
155 | // call createWindow function after whenReady() resolves its promise
156 | // app.whenReady().then(() => {
157 | // if (process.platform === 'darwin') {
158 | // app.dock.setMenu(dockMenu)
159 | // }
160 | // }).then(createWindow)
161 |
162 | // Quit when all windows are closed.
163 | app.on('window-all-closed', () => {
164 | // On macOS it is common for applications and their menu bar
165 | // to stay active until the user quits explicitly with Cmd + Q
166 | if (process.platform !== 'darwin') {
167 | app.quit();
168 | }
169 | });
170 |
171 | app.on('activate', () => {
172 | // On macOS it's common to re-create a window in the app when the
173 | // dock icon is clicked and there are no other windows open.
174 | if (mainWindow === null) {
175 | createWindow();
176 | }
177 | });
178 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fflow.",
3 | "version": "1.0",
4 | "description": "fflow is an easy-to-use open-source tool for all developers to create their React application.",
5 | "license": "MIT",
6 | "author": {
7 | "name": "Rain Hsu, Bryanna DeJesus, Jake Pino and Ronak Hirpara"
8 | },
9 | "keywords": [
10 | "app",
11 | "boilerplate",
12 | "electron",
13 | "open",
14 | "open-source",
15 | "react",
16 | "reactjs",
17 | "webpack"
18 | ],
19 | "engines": {
20 | "node": ">=9.0.0",
21 | "npm": ">=5.0.0",
22 | "yarn": ">=1.0.0"
23 | },
24 | "browserslist": [
25 | "last 4 versions"
26 | ],
27 | "browser": {
28 | "fs": false,
29 | "path": false,
30 | "os": false
31 | },
32 | "jest": {
33 | "testPathIgnorePatterns": [
34 | "/builds"
35 | ],
36 | "moduleNameMapper": {
37 | ".(css|less|scss|sass)$": "/styleMock.js"
38 | }
39 | },
40 | "main": "main.js",
41 | "scripts": {
42 | "prod": "cross-env NODE_ENV=production webpack --mode production --config webpack.build.config.js && electron --noDevServer .",
43 | "start": "cross-env NODE_ENV=development webpack serve --hot --host 0.0.0.0 --config=./webpack.dev.config.js --mode development",
44 | "build": "cross-env NODE_ENV=production webpack --config webpack.build.config.js --mode production",
45 | "package": "npm run build",
46 | "postpackage": "electron-packager ./ --out=./builds",
47 | "electron-test": "npx playwright test",
48 | "test": "jest --verbose --env=jsdom"
49 | },
50 | "dependencies": {
51 | "@reduxjs/toolkit": "^1.7.1",
52 | "@themesberg/flowbite": "^1.3.0",
53 | "@uiw/react-monacoeditor": "^3.4.7",
54 | "downloads-folder": "^3.0.1",
55 | "electron-builder": "^22.14.13",
56 | "electron-rebuild": "^3.2.7",
57 | "electron-store": "^8.0.1",
58 | "fs-extra": "^10.0.0",
59 | "jscodeshift": "^0.13.1",
60 | "localforage": "^1.10.0",
61 | "monaco-editor-webpack-plugin": "^7.0.1",
62 | "node-loader": "^2.0.0",
63 | "node-pty": "^0.10.1",
64 | "postcss": "^8.4.5",
65 | "react": "^17.0.2",
66 | "react-beautiful-dnd": "^13.1.0",
67 | "react-dom": "^17.0.2",
68 | "react-icons": "^4.3.1",
69 | "react-redux": "^7.2.6",
70 | "regenerator-runtime": "^0.13.9",
71 | "styled-components": "^5.3.3"
72 | },
73 | "devDependencies": {
74 | "@babel/core": "^7.16.12",
75 | "@babel/plugin-transform-runtime": "^7.17.0",
76 | "@babel/preset-env": "^7.16.11",
77 | "@babel/preset-react": "^7.16.7",
78 | "@monaco-editor/react": "^4.3.1",
79 | "@testing-library/jest-dom": "^5.16.2",
80 | "@testing-library/react": "^12.1.2",
81 | "@types/react": "^17.0.38",
82 | "@types/react-dom": "^17.0.11",
83 | "autoprefixer": "^10.4.2",
84 | "babel-jest": "^27.5.1",
85 | "babel-loader": "^8.2.3",
86 | "cross-env": "^7.0.3",
87 | "css-loader": "^6.5.1",
88 | "electron": "^16.0.8",
89 | "electron-devtools-installer": "^3.2.0",
90 | "electron-packager": "^15.4.0",
91 | "eslint": "^8.8.0",
92 | "eslint-config-airbnb": "^19.0.4",
93 | "eslint-plugin-import": "^2.25.4",
94 | "eslint-plugin-jsx-a11y": "^6.5.1",
95 | "eslint-plugin-react": "^7.28.0",
96 | "file-loader": "^6.2.0",
97 | "html-webpack-plugin": "^5.5.0",
98 | "jest": "^27.5.1",
99 | "jest-dom": "^4.0.0",
100 | "jest-environment-jsdom": "^27.5.1",
101 | "mini-css-extract-plugin": "^2.5.3",
102 | "monaco-editor": "^0.31.1",
103 | "postcss-import": "^14.0.2",
104 | "postcss-loader": "^6.2.1",
105 | "postcss-nested": "^5.0.6",
106 | "postcss-preset-env": "^7.2.3",
107 | "postcss-pxtorem": "^6.0.0",
108 | "prettier": "2.5.1",
109 | "style-loader": "^3.3.1",
110 | "tailwindcss": "^3.0.18",
111 | "ts-loader": "^9.2.6",
112 | "url-loader": "^4.1.1",
113 | "webpack": "^5.67.0",
114 | "webpack-cli": "^4.9.1",
115 | "webpack-dev-server": "^3.11.3",
116 | "xterm": "^4.16.0",
117 | "xterm-addon-fit": "^0.5.0"
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'postcss-import': {},
4 | 'postcss-nested': {},
5 | 'postcss-preset-env': {},
6 | 'postcss-pxtorem': {
7 | rootValue: 16,
8 | unitPrecision: 5,
9 | propList: ['*'],
10 | selectorBlackList: ['html', 'body'],
11 | replace: true,
12 | mediaQuery: false,
13 | minPixelValue: 0
14 | },
15 | tailwindcss: {},
16 | autoprefixer: {},
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/assets/GitHub_README_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/GitHub_README_logo.png
--------------------------------------------------------------------------------
/src/assets/clearCanvas.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/clearCanvas.gif
--------------------------------------------------------------------------------
/src/assets/cssEditor.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/cssEditor.gif
--------------------------------------------------------------------------------
/src/assets/customComps.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/customComps.gif
--------------------------------------------------------------------------------
/src/assets/darkMode.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/darkMode.gif
--------------------------------------------------------------------------------
/src/assets/dnd.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/dnd.gif
--------------------------------------------------------------------------------
/src/assets/export.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/export.gif
--------------------------------------------------------------------------------
/src/assets/splash.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
51 |
52 | let's get into fflow...
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/src/assets/splashDemo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/splashDemo.gif
--------------------------------------------------------------------------------
/src/assets/splashscreen.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/fflow/beb6914b1592ba6fe1471c3cbea8636b4dd0fa8a/src/assets/splashscreen.gif
--------------------------------------------------------------------------------
/src/components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import Navigation from './Navigation';
3 | import Body from './Body';
4 | import '../stylesheets/App.css';
5 | import 'regenerator-runtime/runtime';
6 | import { useDispatch } from 'react-redux';
7 | import { loadPrevState, refreshCode } from '../redux/canvasSlice';
8 | import { loadState } from '../localforage';
9 | import localforage from 'localforage';
10 |
11 | const App = () => {
12 | const dispatch = useDispatch();
13 |
14 | const checkForPrevState = async () => {
15 | const isPrev = await loadState();
16 | return isPrev ? isPrev : null;
17 | };
18 |
19 | // following useEffect runs on first mount
20 | useEffect(() => {
21 | checkForPrevState().then((res) => {
22 | console.log('isPrev fired');
23 | console.log('prevousProject: ', res);
24 | // if saved project exists, use dispatch to load previous state
25 | // issue: thread of execution never enters this conditional statement
26 | if (res) {
27 | dispatch(loadPrevState(res));
28 | dispatch(refreshCode());
29 | console.log('save exists and LoadPrevState and refresh Code fired');
30 | } else {
31 | console.log('No saved project found in localforage, setting initial state blank');
32 | }
33 | });
34 | }, []);
35 |
36 | // useEffect(() => {
37 | // localforage.setItem('state', state);
38 | // console.log('localforage.setItem ran');
39 | // }, [state]);
40 |
41 | return (
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default App;
50 |
--------------------------------------------------------------------------------
/src/components/Body.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import DnD from './DnD';
3 | import Canvas from './Canvas';
4 | import Header from './Header';
5 | import CodePreview from './CodePreview';
6 | import '../stylesheets/BodyContainer.css';
7 | import { DragDropContext } from 'react-beautiful-dnd';
8 | import { useDispatch, useSelector } from 'react-redux';
9 | import { addComponent, refreshCode, reorderComponent, saveComponentCode } from '../redux/canvasSlice';
10 |
11 | const Body = () => {
12 | const dispatch = useDispatch();
13 | // to pull array of all components that were dragged on
14 | const components = useSelector((state) => state.canvas.components);
15 |
16 | const dragStart = (dragItem) => {
17 | if (dragItem.source.droppableId === 'canvas') document.getElementById(dragItem.draggableId).style.backgroundColor = '#d0f0fd';
18 | };
19 |
20 | const dragEnd = (dragItem) => {
21 | //update state with what's dragged onto canvas
22 | //if dragged from canvas
23 | if (dragItem.source.droppableId === 'canvas') document.getElementById(dragItem.draggableId).style.backgroundColor = 'inherit';
24 |
25 | if (!dragItem.destination) return;
26 | if (dragItem.source.droppableId === 'htmlTags' && dragItem.destination.droppableId === 'canvas') {
27 | // if dragged from tags to canvas
28 | // dispatch addComponent and refreshCode reducers
29 | dispatch(addComponent(dragItem));
30 | dispatch(refreshCode());
31 | } else if (dragItem.source.droppableId === 'canvas' && dragItem.destination.droppableId === 'canvas') {
32 | //if dragged to and from canvas (reordered)
33 | // dispatch reorderComponent and refreshCode reducers
34 | dispatch(reorderComponent(dragItem));
35 | dispatch(refreshCode());
36 | }
37 | // save code after any dragging
38 | dispatch(saveComponentCode());
39 | };
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | export default Body;
57 |
--------------------------------------------------------------------------------
/src/components/CSSCodeEditor.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Editor from '@monaco-editor/react';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { updateCss } from '../redux/canvasSlice';
5 |
6 | const CSSCodeEditor = () => {
7 | const theme = useSelector((state) => state.theme.currTheme);
8 | const cssCode = useSelector((state) => state.canvas.cssCode);
9 | const dispatch = useDispatch();
10 |
11 | const onChange = (newValue) => dispatch(updateCss(newValue));
12 |
13 | return (
14 |
15 |
30 |
31 | );
32 | };
33 |
34 | export default CSSCodeEditor;
35 |
--------------------------------------------------------------------------------
/src/components/Canvas.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Droppable, Draggable } from 'react-beautiful-dnd';
3 | import { useSelector } from 'react-redux';
4 | import '../stylesheets/Canvas.css';
5 | import CanvasItem from './CanvasItem';
6 |
7 | const Canvas = (props) => {
8 | const fileName = useSelector((state) => state.canvas.currentFile);
9 |
10 | return (
11 |
12 | {(provided) => (
13 |
14 |
15 | Add elements into
16 |
17 | {fileName}
18 |
19 |
20 | {props.components.map((ele, ind) => {
21 | return (
22 |
23 | {(provided, snapshot) => }
24 |
25 | );
26 | })}
27 | {provided.placeholder}
28 |
29 | )}
30 |
31 | );
32 | };
33 |
34 | export default Canvas;
35 |
--------------------------------------------------------------------------------
/src/components/CanvasItem.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import DeleteCanvasItem from './DeleteCanvasItem';
3 | import '../stylesheets/Canvas.css';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { renderComponentCode, saveComponentCode, setCurrentFile } from '../redux/canvasSlice';
6 |
7 | const CanvasItem = (props) => {
8 | const dispatch = useDispatch();
9 | const custom = useSelector((state) => state.canvas.customComponents);
10 |
11 | const onClick = e => {
12 | const name = e.target.innerText + '.jsx';
13 | // dispatch(saveComponentCode({ currentCode, currentFile }));
14 | if (custom.includes(e.target.innerText)) {
15 | dispatch(saveComponentCode());
16 | dispatch(setCurrentFile(name));
17 | dispatch(renderComponentCode({ name }));
18 | }
19 | }
20 |
21 | return (
22 |
23 |
32 |
33 |
34 | {props.name}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default CanvasItem;
47 |
--------------------------------------------------------------------------------
/src/components/CodePreview.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import TabContainer from './TabContainer';
3 | import '../stylesheets/CodePreview.css';
4 | import '../stylesheets/Terminal.css';
5 |
6 | const CodePreview = () => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default CodePreview;
15 |
--------------------------------------------------------------------------------
/src/components/CompCreator.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { createComponent, refreshCode, saveComponentCode } from '../redux/canvasSlice';
4 | import '../stylesheets/CompCreator.css';
5 |
6 | const CompCreator = () => {
7 | const dispatch = useDispatch();
8 | const custom = useSelector((state) => state.canvas.customComponents);
9 | const tags = useSelector((state) => state.canvas.tags);
10 |
11 | const onClick = (e) => {
12 | e.preventDefault();
13 | const input = document.getElementById('create-react-component-input-field');
14 | const text = input.value[0].toUpperCase() + input.value.slice(1);
15 | if (!custom.includes(text)) {
16 | dispatch(createComponent({ text }));
17 | dispatch(refreshCode());
18 | dispatch(saveComponentCode());
19 | } else {
20 | alert('Component with that name already exists');
21 | }
22 | input.value = '';
23 | };
24 |
25 | return (
26 |
48 | );
49 | };
50 |
51 | export default CompCreator;
52 |
--------------------------------------------------------------------------------
/src/components/CustomComponents.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../stylesheets/CustomComponents.css';
3 | import { useSelector } from 'react-redux';
4 | import { Droppable, Draggable } from 'react-beautiful-dnd';
5 |
6 | const CustomComponents = () => {
7 | const custom = useSelector((state) => state.canvas.customComponents);
8 | if (custom.length) {
9 | return (
10 | //
11 | // {custom.map((ele) => {
12 | // const id = ele.toLowerCase();
13 | // return (
14 | //
15 | // {ele}
16 | //
17 | // );
18 | // })}
19 |
20 | {(providedDrop) => (
21 |
22 | {custom.map((ele, ind) => {
23 | const id = ele.toLowerCase();
24 | return (
25 |
26 | {(providedDrag, snapshot) => (
27 |
35 | {ele}
36 |
37 | )}
38 |
39 | );
40 | })}
41 | {providedDrop.placeholder}
42 |
43 | )}
44 |
45 | //
46 | );
47 | } else {
48 | return null;
49 | }
50 | };
51 |
52 | export default CustomComponents;
53 |
--------------------------------------------------------------------------------
/src/components/DeleteCanvasItem.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch } from "react-redux";
3 | import { deleteComponent, refreshCode } from "../redux/canvasSlice";
4 |
5 | const DeleteCanvasItem = (props) => {
6 | const dispatch = useDispatch();
7 | return (
8 | {
13 | e.preventDefault();
14 | dispatch(deleteComponent(props));
15 | dispatch(refreshCode());
16 | }}
17 | >
18 |
19 |
20 | );
21 | };
22 |
23 | export default DeleteCanvasItem;
--------------------------------------------------------------------------------
/src/components/DnD.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import '../stylesheets/DnD.css';
3 | import CompCreator from './compCreator';
4 | import DragList from './DragList';
5 | import CustomComponents from './CustomComponents';
6 | import { useSelector, useDispatch } from 'react-redux';
7 | import Tree from './Tree';
8 | import { renderComponentCode, saveComponentCode } from '../redux/canvasSlice';
9 |
10 | const DnD = () => {
11 | const toggleState = useSelector((state) => state.nav.toggle);
12 | const fileState = useSelector((state) => state.canvas.files);
13 |
14 | const structure = [
15 | {
16 | type: 'folder',
17 | name: 'dist',
18 | childrens: [
19 | {
20 | type: 'file',
21 | name: 'index.html',
22 | },
23 | ],
24 | },
25 | {
26 | type: 'folder',
27 | name: 'src',
28 | childrens: fileState,
29 | },
30 | ];
31 |
32 | let currentFile = useSelector((state) => state.canvas.currentFile);
33 | const currentCode = useSelector((state) => state.canvas.code);
34 | const dispatch = useDispatch();
35 |
36 | const showAppCodeHandleClick = (e) => {
37 | const name = 'App.js';
38 | dispatch(saveComponentCode({ currentCode, currentFile }));
39 | dispatch(renderComponentCode({ name }));
40 | };
41 |
42 | return (
43 | <>
44 | {toggleState === 'DnD' ? (
45 |
46 |
52 | fflow
53 |
54 |
55 |
56 |
57 |
58 |
FOLDERS
59 |
60 |
61 |
62 | ) : (
63 |
64 |
69 | fflow
70 |
71 |
FOLDERS
72 |
73 |
74 | )}
75 | >
76 | );
77 | };
78 |
79 | export default DnD;
80 |
--------------------------------------------------------------------------------
/src/components/DragList.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import '../stylesheets/DragList.css';
3 | import { Droppable, Draggable } from 'react-beautiful-dnd';
4 | import { useSelector } from 'react-redux';
5 |
6 | const DragList = () => {
7 | const items = useSelector((state) => state.tags.tagList);
8 | let count = 0;
9 | const tags = [];
10 |
11 | for (const id in items) {
12 | const label = items[id];
13 | tags.push(
14 |
15 | {(provided) => (
16 |
19 | )}
20 |
21 | );
22 | }
23 |
24 | return (
25 |
26 | {(provided) => (
27 |
28 | {tags}
29 | {provided.placeholder}
30 |
31 | )}
32 |
33 | );
34 | };
35 |
36 | export default DragList;
37 |
--------------------------------------------------------------------------------
/src/components/ExportApp.jsx:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const fse = require('fs-extra');
3 | const df = require('downloads-folder');
4 |
5 | export default function exportApp(snapshot) {
6 | const templateHTML = `
7 |
8 |
9 | Exported Project
10 |
11 |
12 |
13 |
14 |
15 |
16 | `;
17 |
18 | const templateIndexJS = `import React from "react";
19 | import ReactDOM from "react-dom";
20 | import App from "./App";
21 | import "./styles.css";
22 |
23 | var mountNode = document.getElementById("root");
24 | ReactDOM.render( , mountNode);`;
25 |
26 | const appFile = snapshot.files[0];
27 | const tags = appFile.fileTags.map((ele) => {
28 | return ele.slice(1);
29 | });
30 |
31 | const templateAppJS = `${appFile.fileImports.join('')}import { hot } from 'react-hot-loader/root';
32 |
33 | const App = () => {
34 | return (
35 |
36 |
Hello there!
37 |
Thanks for using fflow
\n${tags.join('\n')}
38 |
39 | )
40 | };
41 |
42 | export default hot(App);`;
43 |
44 | const templatePackage = `{
45 | "name": "exported-project",
46 | "version": "1.0.0",
47 | "description": "",
48 | "main": "index.js",
49 | "keywords": [],
50 | "author": "",
51 | "license": "ISC",
52 | "scripts": {
53 | "clean": "rm dist/bundle.js",
54 | "build-dev": "webpack --mode development",
55 | "build-prod": "webpack --mode production",
56 | "start": "webpack serve --open --hot --mode development"
57 | },
58 | "dependencies": {
59 | "react": "^17.0.2",
60 | "react-dom": "^17.0.2",
61 | "react-hot-loader": "^4.13.0"
62 | },
63 | "devDependencies": {
64 | "webpack": "^5.68.0",
65 | "webpack-cli": "^4.9.2",
66 | "babel-loader": "^8.2.3",
67 | "@babel/core": "^7.16.12",
68 | "@babel/preset-env": "^7.16.11",
69 | "@hot-loader/react-dom": "^17.0.2+4.13.0",
70 | "@babel/preset-react": "^7.16.7",
71 | "webpack-dev-server": "^4.7.3",
72 | "css-loader": "^6.5.1",
73 | "style-loader": "^3.3.1"
74 | }
75 | }`;
76 |
77 | const ticks = '```';
78 | const templateReadMe = `# exported-project
79 |
80 | ## Building and running on localhost
81 |
82 | First install dependencies:
83 |
84 | ${ticks}sh
85 | npm install
86 | ${ticks}
87 |
88 | To run in hot module reloading mode:
89 |
90 | ${ticks}sh
91 | npm start
92 | ${ticks}
93 |
94 | To create a production build:
95 |
96 | ${ticks}sh
97 | npm run build-prod
98 | ${ticks}
99 |
100 | To create a development build:
101 |
102 | ${ticks}sh
103 | npm run build-dev
104 | ${ticks}
105 |
106 | ## Running
107 |
108 | Open the file 'dist/index.html' in your browser
109 |
110 | ## Credits
111 |
112 | Made with [fflow.io](https://fflow.dev/)`;
113 |
114 | const templateGitIgnore = `.cache/
115 | coverage/
116 | dist/*
117 | !dist/index.html
118 | node_modules/
119 | *.log
120 |
121 | # OS generated files
122 | .DS_Store
123 | .DS_Store?
124 | ._*
125 | .Spotlight-V100
126 | .Trashes
127 | ehthumbs.db
128 | Thumbs.db`;
129 |
130 | const templateWebpack = `const webpack = require('webpack');
131 | const path = require('path');
132 |
133 | const config = {
134 | entry: [
135 | 'react-hot-loader/patch',
136 | './src/index.js'
137 | ],
138 | output: {
139 | path: path.resolve(__dirname, 'dist'),
140 | filename: 'bundle.js'
141 | },
142 | module: {
143 | rules: [
144 | {
145 | test: /\.(js|jsx)$/,
146 | use: 'babel-loader',
147 | exclude: /node_modules/
148 | },
149 | {
150 | test: /\.css$/,
151 | use: [
152 | 'style-loader',
153 | 'css-loader'
154 | ],
155 | exclude: /\.module\.css$/
156 | },
157 | {
158 | test: /\.css$/,
159 | use: [
160 | 'style-loader',
161 | {
162 | loader: 'css-loader',
163 | options: {
164 | importLoaders: 1,
165 | modules: true
166 | }
167 | }
168 | ],
169 | include: /\.module\.css$/
170 | }
171 | ]
172 | },
173 | devServer: {
174 | 'static': {
175 | directory: './dist'
176 | }
177 | }
178 | };
179 |
180 | module.exports = config;`;
181 |
182 | const templateBabel = `{
183 | presets: [
184 | [
185 | '@babel/preset-env',
186 | {
187 | modules: false
188 | }
189 | ],
190 | '@babel/preset-react'
191 | ],
192 | plugins: [
193 | 'react-hot-loader/babel'
194 | ]
195 | }`;
196 |
197 | const path = df(); //module to find download folder
198 | const name = 'Exported_Project_' + Math.floor(Math.random() * 100).toString();
199 |
200 | //iterate through files array, create file for each, fill with its code key
201 |
202 | const files = snapshot.files;
203 | for (let i = 1; i < files.length; i++) {
204 | //start at one to skip app file which has it's own template
205 | const curr = files[i];
206 | fse.outputFile(path + `/${name}/src/${curr.name}`, curr.fileCode);
207 | }
208 |
209 | const css = snapshot.cssCode;
210 |
211 | fse.outputFile(path + `/${name}/dist/index.html`, templateHTML);
212 | fse.outputFile(path + `/${name}/src/index.js`, templateIndexJS);
213 | fse.outputFile(path + `/${name}/src/App.js`, templateAppJS);
214 | fse.outputFile(path + `/${name}/src/styles.css`, css);
215 | fse.outputFile(path + `/${name}/.gitignore`, templateGitIgnore);
216 | fse.outputFile(path + `/${name}/package.json`, templatePackage);
217 | fse.outputFile(path + `/${name}/README.md`, templateReadMe);
218 | fse.outputFile(path + `/${name}/webpack.config.js`, templateWebpack);
219 | fse.outputFile(path + `/${name}/.babelrc`, templateBabel);
220 | alert('App downloaded to your downloads folder');
221 | }
222 |
--------------------------------------------------------------------------------
/src/components/ExportFiles.jsx:
--------------------------------------------------------------------------------
1 | // Creates all component files (but not the full application files) and places them in a "components" directory
2 |
3 | const fs = require('fs');
4 | const fse = require('fs-extra');
5 | const df = require('downloads-folder');
6 |
7 | export default function exportFiles(snapshot) {
8 | const templateHTML = `
9 |
10 |
11 | Exported Project
12 |
13 |
14 |
15 |
16 |
17 |
18 | `;
19 |
20 | const templateIndexJS = `import React from "react";
21 | import ReactDOM from "react-dom";
22 | import App from "./App";
23 | import "./styles.css";
24 |
25 | var mountNode = document.getElementById("root");
26 | ReactDOM.render( , mountNode);`;
27 |
28 | const tags = snapshot.tags.map((ele) => {
29 | return ele.slice(1);
30 | });
31 |
32 | const templateAppJS = `${snapshot.imports}import { hot } from 'react-hot-loader/root';
33 |
34 | const App = () => {
35 | return (
36 |
37 |
Hello there!
38 |
Thanks for using fflow
39 |
LOGO HERE
\n${tags.join('\n')}
40 |
41 | )
42 | };
43 |
44 | export default hot(App);`;
45 |
46 | const path = df();
47 | const name = 'Exported_Project';
48 |
49 | //iterate through files array, create file for each, fill with its code key
50 |
51 | const files = snapshot.files;
52 | for (let i = 1; i < files.length; i++) {
53 | //start at one to skip app file which has it's own template
54 | const curr = files[i];
55 | fse.outputFile(path + `/${name}/src/${curr.name}`, curr.fileCode);
56 | }
57 |
58 | fse.outputFile(path + `/${name}/dist/index.html`, templateHTML);
59 | fse.outputFile(path + `/${name}/src/index.js`, templateIndexJS);
60 | fse.outputFile(path + `/${name}/src/App.js`, templateAppJS);
61 | fse.outputFile(path + `/${name}/README.md`, templateReadMe);
62 | alert('Component files downloaded to your download folder');
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/ExportModal.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { modalToggle } from '../redux/navigationSlice';
4 | import '../stylesheets/ExportModal.css';
5 |
6 | import exportApp from './Export';
7 |
8 | const ExportModal = () => {
9 | const show = useSelector((state) => state.navigation.showModal);
10 | const snapshot = useSelector((state) => state.canvas);
11 | const dispatch = useDispatch();
12 |
13 | if (!show) return null;
14 |
15 | const closeClick = (e) => dispatch(modalToggle(false));
16 |
17 | return (
18 |
19 |
20 | Title
21 |
22 |
23 | Select which files you would like to export.
24 |
25 |
26 | exportApp()}>Export App
27 | Export Component Files
28 | {
30 | closeClick(e);
31 | }}
32 | >
33 | Close
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default ExportModal;
41 |
--------------------------------------------------------------------------------
/src/components/Header.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { clearProject } from '../redux/canvasSlice';
4 | import { changeTheme } from '../redux/themeSlice';
5 | import '../stylesheets/Header.css';
6 | import { IconContext } from 'react-icons';
7 | import { FaSun, FaMoon } from 'react-icons/fa';
8 |
9 | const Header = () => {
10 | const dispatch = useDispatch();
11 |
12 | const themeToggle = () => {
13 | document.body.classList.toggle('theme-light');
14 | dispatch(changeTheme());
15 | };
16 |
17 | const clear = () => {
18 | if (confirm('Are you sure you want to clear project?')) {
19 | dispatch(clearProject());
20 | }
21 | };
22 |
23 | return (
24 |
25 | {/* */}
26 |
34 | Clear Project
35 |
36 |
37 | {/* */}
38 |
themeToggle()} />
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default Header;
59 |
--------------------------------------------------------------------------------
/src/components/JSCodeEditor.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Editor, { monaco, loader } from '@monaco-editor/react';
3 | import { renderComponentCode, saveComponentCode, updateJs } from '../redux/canvasSlice';
4 | import { useSelector, useDispatch } from 'react-redux';
5 |
6 | const path = require('path');
7 |
8 | const ensureFirstBackSlash = (str) => {
9 | return str.length > 0 && str.charAt(0) !== '/' ? '/' + str : str;
10 | }
11 |
12 | function uriFromPath(_path) {
13 | const pathName = path.resolve(_path).replace(/\\/g, '/');
14 | return encodeURI('file://' + ensureFirstBackSlash(pathName));
15 | }
16 |
17 | loader.config({
18 | paths: {
19 | vs: uriFromPath(path.join(__dirname, '../node_modules/monaco-editor/min/vs')),
20 | },
21 | });
22 |
23 | const JSCodeEditor = () => {
24 | const theme = useSelector((state) => state.theme.currTheme);
25 | const code = useSelector((state) => state.canvas.code);
26 | const dispatch = useDispatch();
27 |
28 | const onChange = (newValue) => {
29 | dispatch(updateJs(newValue));
30 | dispatch(saveComponentCode());
31 | };
32 |
33 | return (
34 |
35 |
51 |
52 | );
53 | };
54 |
55 | export default JSCodeEditor;
56 |
--------------------------------------------------------------------------------
/src/components/Login.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../stylesheets/Login.css';
3 | import '../stylesheets/Navigation.css';
4 |
5 | const Login = (props) => {
6 | return (
7 |
8 |
9 | {/*
10 |
11 | */}
12 |
13 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default Login;
74 |
--------------------------------------------------------------------------------
/src/components/Navigation.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import '../stylesheets/Navigation.css';
3 | import { toggleLeftPanel } from '../redux/navigationSlice';
4 | import { clearProject, saveComponentCode } from '../redux/canvasSlice';
5 | import { useSelector, useDispatch } from 'react-redux';
6 | import exportApp from './ExportApp';
7 | import { FaPencilRuler, FaFolderOpen, FaSave, FaDownload, FaTrash } from 'react-icons/fa';
8 |
9 | const Navigation = () => {
10 | const dispatch = useDispatch();
11 |
12 | const snapshot = useSelector((state) => state.canvas);
13 |
14 | // functions to toggle between DnD and fileTree
15 | const openDnD = () => dispatch(toggleLeftPanel('DnD'));
16 | const openFileTree = () => dispatch(toggleLeftPanel('fileTree'));
17 |
18 | const clear = () => {
19 | if (confirm('Are you sure you want to clear project?')) dispatch(clearProject());
20 | };
21 |
22 | const handleSave = () => {
23 | // saveState is in localForage.js - should we be importing this or did you mean
24 | // to dispatch saveComponentCode reducer here instead?
25 | dispatch(saveComponentCode(snapshot));
26 | console.log('saveComponentCode fired');
27 | // saveState(snapshot);
28 | console.log('snapshot: ', snapshot);
29 | alert('Current project saved');
30 | };
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | {' '}
42 |
43 |
44 | {
47 | exportApp(snapshot);
48 | }}
49 | />
50 |
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default Navigation;
59 |
--------------------------------------------------------------------------------
/src/components/TabContainer.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import JSCodeEditor from './JSCodeEditor';
3 | import CSSCodeEditor from './CSSCodeEditor';
4 | import TerminalView from './TerminalView';
5 | import '../stylesheets/CodePreview.css';
6 |
7 | const TabContainer = () => {
8 | const [tabState, setTabState] = useState(1);
9 | const toggleTab = (tabNum) => setTabState(tabNum);
10 |
11 | return (
12 |
13 |
14 | toggleTab(1)}>
15 | Code Preview
16 |
17 | toggleTab(2)}>
18 | Terminal
19 |
20 | toggleTab(3)}>
21 | CSS Editor
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default TabContainer;
40 |
--------------------------------------------------------------------------------
/src/components/TagCreator.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import '../stylesheets/TagCreator.css';
3 |
4 | const TagCreator = () => {
5 | return (
6 |
7 |
8 |
17 |
18 | )
19 | }
20 |
21 | export default TagCreator;
22 |
23 |
--------------------------------------------------------------------------------
/src/components/TerminalView.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Terminal } from 'xterm';
3 | import { FitAddon } from 'xterm-addon-fit';
4 | import { ipcRenderer } from 'electron';
5 | // import '../stylesheets/Terminal.css';
6 | const ipc = ipcRenderer;
7 |
8 | const terminalArgs = {
9 | fontSize: 15,
10 | cols: 49,
11 | rows: 38,
12 | fontFamily: 'monospace',
13 | theme: {
14 | background: 'black',
15 | },
16 | cursorStyle: 'bar',
17 | cursorBlink: 'block',
18 | };
19 |
20 | const term = new Terminal(terminalArgs);
21 | const fitAddon = new FitAddon();
22 |
23 | const TerminalView = () => {
24 | useEffect(() => {
25 | term.loadAddon(fitAddon);
26 | term.open(document.getElementById('terminal'));
27 | fitAddon.fit();
28 |
29 | ipc.on('terminal.sentData', (event, data) => {
30 | term.write(data);
31 | });
32 |
33 | term.onData((data) => {
34 | ipc.send('terminal.toTerm', data);
35 | });
36 | }, []);
37 |
38 | return
;
39 | };
40 |
41 | export default TerminalView;
42 |
--------------------------------------------------------------------------------
/src/components/Tree.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import TreeRecursive from './TreeRecursive';
4 |
5 | const StyledTree = styled.div`
6 | line-height: 1.6;
7 | `;
8 |
9 | const Tree = ({ data, children }) => {
10 | const isImparative = data && !children;
11 |
12 | return {isImparative ? : children} ;
13 | };
14 |
15 | export default Tree;
16 |
--------------------------------------------------------------------------------
/src/components/TreeFile.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { DiJavascript1, DiCss3Full, DiHtml5, DiReact } from 'react-icons/di';
4 | import { useSelector, useDispatch } from 'react-redux';
5 | import { renderComponentCode, setCurrentFile, saveComponentCode, refreshCode } from '../redux/canvasSlice';
6 |
7 | const StyledFile = styled.div`
8 | padding-left: 20px;
9 | display: flex;
10 | align-items: center;
11 | width: 250px;
12 | ${'' /* font-size: 15px; */}
13 | `;
14 |
15 | const FILE_ICONS = {
16 | js: (
17 |
21 |
22 |
23 | ),
24 | css: (
25 |
26 |
27 |
28 | ),
29 | html: (
30 |
31 |
32 |
33 | ),
34 | jsx: (
35 |
39 |
40 |
41 | ),
42 | };
43 |
44 | const TreeFile = ({ name, code }) => {
45 | const ext = name.split('.')[1];
46 |
47 | const dispatch = useDispatch();
48 |
49 | let currentFile = useSelector((state) => state.canvas.currentFile);
50 |
51 | const handleClick = () => {
52 | if (ext != 'css') {
53 | dispatch(saveComponentCode());
54 |
55 | currentFile = name;
56 | dispatch(renderComponentCode({ name }));
57 | }
58 | };
59 |
60 | return (
61 |
62 | {/* render the extension or fallback to generic file icon */}
63 | {FILE_ICONS[ext]}
64 | handleClick()}>
65 | {name}
66 |
67 |
68 | );
69 | };
70 |
71 | export default TreeFile;
72 |
--------------------------------------------------------------------------------
/src/components/TreeFolder.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { FaFolder } from 'react-icons/fa';
3 | import styled from 'styled-components';
4 |
5 | const StyledFolder = styled.div`
6 | padding-left: 20px;
7 |
8 | .folder--label {
9 | display: flex;
10 | align-items: center;
11 | span {
12 | margin-left: 5px;
13 | }
14 | color: var(--textColor);
15 | width: 250px;
16 | font-size: 15px;
17 | }
18 |
19 | .folder--label:hover {
20 | background-color: var(--file-tree-hover-color);
21 | }
22 | `;
23 |
24 | const Collapsible = styled.div`
25 | height: ${(p) => (p.isOpen ? 'auto' : '0')};
26 | overflow: hidden;
27 | color: var(--textColor);
28 | `;
29 |
30 | const TreeFolder = ({ name, children }) => {
31 | // sets default view of filetree to be expanded
32 | const [isOpen, setIsOpen] = useState(true);
33 |
34 | const handleToggle = (e) => {
35 | e.preventDefault();
36 | setIsOpen(!isOpen);
37 | };
38 |
39 | return (
40 |
41 |
42 |
43 |
44 |
45 | {name}
46 |
47 | {children}
48 |
49 | );
50 | };
51 |
52 | export default TreeFolder;
53 |
--------------------------------------------------------------------------------
/src/components/TreeRecursive.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import File from './TreeFile';
3 | import Folder from './TreeFolder';
4 |
5 | const TreeRecursive = ({ data }) => {
6 | // loop through the data
7 | return data.map((item) => {
8 | // if its a file render
9 | if (item.type === 'file') {
10 | return ;
11 | }
12 | // if its a folder render
13 | if (item.type === 'folder') {
14 | return (
15 |
16 | {/* Call the component with the current item.childrens */}
17 |
18 |
19 | );
20 | }
21 | });
22 | };
23 |
24 | export default TreeRecursive;
25 |
--------------------------------------------------------------------------------
/src/electron/menu.js:
--------------------------------------------------------------------------------
1 | // Not currently used in alpha release
2 |
3 | const { app, Menu } = require('electron')
4 |
5 | const isMac = process.platform === 'darwin'
6 |
7 | const template = [
8 | // { role: 'appMenu' }
9 | ...(isMac ? [{
10 | label: app.name,
11 | submenu: [
12 | { role: 'about' },
13 | { type: 'separator' },
14 | { role: 'services' },
15 | { type: 'separator' },
16 | { role: 'hide' },
17 | { role: 'hideOthers' },
18 | { role: 'unhide' },
19 | { type: 'separator' },
20 | { role: 'quit' }
21 | ]
22 | }] : []),
23 | // { role: 'fileMenu' }
24 | {
25 | label: 'File',
26 | submenu: [
27 | isMac ? { role: 'close' } : { role: 'quit' }
28 | ]
29 | },
30 | // { role: 'editMenu' }
31 | {
32 | label: 'Edit',
33 | submenu: [
34 | { role: 'undo' },
35 | { role: 'redo' },
36 | { type: 'separator' },
37 | { role: 'cut' },
38 | { role: 'copy' },
39 | { role: 'paste' },
40 | ...(isMac ? [
41 | { role: 'pasteAndMatchStyle' },
42 | { role: 'delete' },
43 | { role: 'selectAll' },
44 | { type: 'separator' },
45 | {
46 | label: 'Speech',
47 | submenu: [
48 | { role: 'startSpeaking' },
49 | { role: 'stopSpeaking' }
50 | ]
51 | }
52 | ] : [
53 | { role: 'delete' },
54 | { type: 'separator' },
55 | { role: 'selectAll' }
56 | ])
57 | ]
58 | },
59 | // { role: 'viewMenu' }
60 | {
61 | label: 'View',
62 | submenu: [
63 | { role: 'reload' },
64 | { role: 'forceReload' },
65 | { role: 'toggleDevTools' },
66 | { type: 'separator' },
67 | { role: 'resetZoom' },
68 | { role: 'zoomIn' },
69 | { role: 'zoomOut' },
70 | { type: 'separator' },
71 | { role: 'togglefullscreen' }
72 | ]
73 | },
74 | // { role: 'windowMenu' }
75 | {
76 | label: 'Window',
77 | submenu: [
78 | { role: 'minimize' },
79 | { role: 'zoom' },
80 | ...(isMac ? [
81 | { type: 'separator' },
82 | { role: 'front' },
83 | { type: 'separator' },
84 | { role: 'window' }
85 | ] : [
86 | { role: 'close' }
87 | ])
88 | ]
89 | },
90 | {
91 | role: 'help',
92 | submenu: [
93 | {
94 | label: 'Learn More',
95 | click: async () => {
96 | const { shell } = require('electron')
97 | await shell.openExternal('https://electronjs.org')
98 | }
99 | }
100 | ]
101 | }
102 | ]
103 |
104 | const menu = Menu.buildFromTemplate(template)
105 | Menu.setApplicationMenu(menu)
--------------------------------------------------------------------------------
/src/electron/preload.js:
--------------------------------------------------------------------------------
1 | // Not currently used in alpha release
2 |
3 | // All of the Node.js APIs are available in the preload process.
4 | // preload script runs before the renderer process is loaded and
5 | // has access to both renderer globals (window and document) and a Node.js environment
6 |
7 | // window.addEventListener('DOMContentLoaded', () => {
8 | // const replaceText = (selector, text) => {
9 | // const element = document.getElementById(selector)
10 | // if (element) element.innerText = text
11 | // }
12 |
13 | // for (const dependency of ['chrome', 'node', 'electron']) {
14 | // replaceText(`${dependency}-version`, process.versions[dependency])
15 | // }
16 | // })
17 |
18 | // const { contextBridge } = require('electron');
19 |
20 | // contextBridge.exposeInMainWorld('myAPI', {
21 | // desktop: true,
22 | // });
23 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDOM from 'react-dom';
3 | import App from './components/App';
4 | import { store } from './redux/store';
5 | import { Provider } from 'react-redux';
6 | import './stylesheets/App.css';
7 | import './stylesheets/index.css';
8 | import '@themesberg/flowbite';
9 |
10 | // // Since we are using HtmlWebpackPlugin WITHOUT a template, we should create our own root node in the body element before rendering into it
11 | const root = document.createElement('div');
12 |
13 | root.id = 'root';
14 | document.body.appendChild(root);
15 |
16 | // Now we can render our application into it
17 | ReactDOM.render(
18 |
19 |
20 | ,
21 | document.getElementById('root')
22 | );
23 |
--------------------------------------------------------------------------------
/src/localForage.js:
--------------------------------------------------------------------------------
1 | // this could be added all into App.jsx
2 |
3 | import localforage from 'localforage';
4 |
5 | export const saveState = (state) => {
6 | localforage.setItem('state', state);
7 | };
8 |
9 | export const loadState = () => {
10 | return localforage.getItem('state');
11 | };
12 |
--------------------------------------------------------------------------------
/src/redux/canvasSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | components: [],
5 | code: '',
6 | cssCode: `html {
7 | box-sizing: border-box;
8 | height: 100%;
9 | }
10 | body {
11 | margin: 0;
12 | padding-top: 20%;
13 | overflow: hidden;
14 | background-color: #272727;
15 | font-family: "Helvetica Neue";
16 | display: flex;
17 | justify-content: center;
18 | text-align: center;
19 | height: 100%;
20 | }
21 | h1 {
22 | color: white;
23 | font-size: 3rem;
24 | }
25 | p {
26 | color: white;
27 | font-size: 1.5rem;
28 | }
29 | .default-spans {
30 | color: #4338ca;
31 | }`,
32 | tags: [],
33 | customComponents: [],
34 | imports: ["import React from 'react';\n"],
35 | codeList: {
36 | Div: `
`,
37 | Paragraph: `
`,
38 | Anchor: ` `,
39 | Image: ` `,
40 | 'Unordered List': ``,
41 | Form: ``,
42 | Input: ` `,
43 | 'Ordered List': ` `,
44 | Button: ` `,
45 | 'List Item': ` `,
46 | Span: ` `,
47 | 'Header 1': ` `,
48 | 'Header 2': ` `,
49 | 'Header 3': ` `,
50 | 'Line Break': ` `,
51 | Table: ``,
52 | THead: ` `,
53 | },
54 | files: [
55 | {
56 | type: 'file',
57 | name: 'App.js',
58 | fileCode: '',
59 | fileTags: [],
60 | fileImports: [],
61 | fileComponents: [],
62 | },
63 | {
64 | type: 'file',
65 | name: 'styles.css',
66 | fileCode: '',
67 | fileTags: [],
68 | fileImports: [],
69 | fileComponents: [],
70 | },
71 | ],
72 | currentFile: 'App.js',
73 | };
74 |
75 | export const canvasSlice = createSlice({
76 | name: 'canvas',
77 | initialState,
78 | reducers: {
79 | addComponent: (state, action) => {
80 | state.components.splice(action.payload.destination.index, 0, action.payload.draggableId);
81 | state.tags.splice(action.payload.destination.index, 0, '\n\t\t\t' + state.codeList[action.payload.draggableId]);
82 | },
83 | deleteComponent: (state, action) => {
84 | if (confirm(`Delete this component?\n${action.payload.name + ' in position ' + action.payload.index}`)) {
85 | state.components.splice(action.payload.index, 1);
86 | state.tags.splice(action.payload.index, 1);
87 | //if custom component, remove from customComp array, files array, and imports
88 | for (let i = 0; i < state.customComponents.length; i++) {
89 | const curr = state.customComponents[i];
90 | if (curr === action.payload.name) {
91 | state.customComponents.splice(i, 1);
92 | state.files.splice(i + 2, 1);
93 | state.imports.splice(i + 2, 1);
94 | }
95 | }
96 | }
97 | },
98 | reorderComponent: (state, action) => {
99 | const [item] = state.components.splice(action.payload.source.index, 1);
100 | state.components.splice(action.payload.destination.index, 0, item);
101 | const [tag] = state.tags.splice(action.payload.source.index, 1);
102 | state.tags.splice(action.payload.destination.index, 0, tag);
103 | },
104 | clearProject: (state) => {
105 | state.components = [];
106 | state.tags = [];
107 | state.code = '';
108 | state.imports = ["import React from 'react';\n"];
109 | state.customComponents = [];
110 | state.files = [
111 | {
112 | type: 'file',
113 | name: 'App.js',
114 | fileCode: '',
115 | fileTags: [],
116 | fileImports: [],
117 | fileComponents: [],
118 | },
119 | ];
120 | state.currentFile = 'App.js';
121 | },
122 | combineComponents: (state, action) => {
123 | const [item] = state.components.splice(action.payload.source.index, 1);
124 | const [tag] = state.tags.splice(action.payload.source.index, 1);
125 | const index = action.payload.combine.draggableId.split('-')[0];
126 |
127 | if (Array.isArray(state.components[index])) {
128 | state.components[index].push(item);
129 | } else {
130 | state.components.splice(index, 1, [state.components[index], item]);
131 | }
132 | },
133 | refreshCode: (state) => {
134 | const name = state.currentFile.split('.')[0];
135 | state.code = `${state.imports.join('')}\nconst ${name} = () => {\n\treturn (\n\t\t${state.tags}\n\t\t
\n\t)\n}\nexport default ${name};`;
136 | },
137 | createComponent: (state, action) => {
138 | const { text } = action.payload;
139 | const newTag = `\n\t\t\t<${text} />`;
140 | const fileName = `${text}.jsx`;
141 | state.tags.push(newTag); // add custom comp to code
142 | state.customComponents.push(text); // add to list of custom comps
143 | state.components.push(text); // add to canvas
144 | state.imports.push(`import ${text} from './${text}.jsx';\n`); // add as import
145 | state.files.push({
146 | // add to file system
147 | type: 'file',
148 | name: fileName,
149 | fileCode: `import React from 'react';\n\nconst ${text} = () => {\n\treturn (\n\t\t\n\t\t
\n\t)\n}\nexport default ${text};`,
150 | fileTags: [],
151 | fileImports: ["import React from 'react';\n"],
152 | fileComponents: [],
153 | });
154 | },
155 | renderComponentCode: (state, action) => {
156 | const { name } = action.payload;
157 | for (const file of state.files) {
158 | //iterate thru list of files to find match
159 | if (file.name === name) {
160 | // if match, pull all values and update outer state
161 | state.code = file.fileCode;
162 | state.tags = file.fileTags;
163 | state.imports = file.fileImports;
164 | state.components = file.fileComponents;
165 | state.currentFile = file.name;
166 | }
167 | }
168 | },
169 | setCurrentFile: (state, action) => {
170 | state.currentFile = action.payload;
171 | },
172 | saveComponentCode: (state) => {
173 | // const { currentCode, currentFile } = action.payload;
174 | state.files.forEach((file) => {
175 | if (file.name === state.currentFile) {
176 | // find file in list and take snapshot of code
177 | file.fileCode = state.code;
178 | file.fileTags = state.tags;
179 | file.fileImports = state.imports;
180 | file.fileComponents = state.components;
181 | }
182 | });
183 | },
184 | updateCss: (state, action) => {
185 | state.cssCode = action.payload;
186 | },
187 | updateJs: (state, action) => {
188 | state.code = action.payload;
189 | },
190 | loadPrevState: (state, action) => {
191 | const newState = action.payload;
192 | state.components = newState.components;
193 | state.code = newState.code;
194 | state.cssCode = newState.cssCode;
195 | state.tags = newState.tags;
196 | state.customComponents = newState.customComponents;
197 | state.imports = newState.imports;
198 | state.codeList = newState.codeList;
199 | state.files = newState.files;
200 | state.currentFile = newState.currentFile;
201 | },
202 | },
203 | });
204 |
205 | // Action creators are generated for each case reducer function
206 | export const {
207 | addComponent,
208 | deleteComponent,
209 | reorderComponent,
210 | clearProject,
211 | combineComponents,
212 | refreshCode,
213 | createComponent,
214 | renderComponentCode,
215 | saveComponentCode,
216 | setCurrentFile,
217 | updateCss,
218 | updateJs,
219 | loadPrevState,
220 | } = canvasSlice.actions;
221 |
222 | export default canvasSlice.reducer;
223 |
--------------------------------------------------------------------------------
/src/redux/componentCodeSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | componentCode: [],
5 | };
6 |
7 | export const componentCodeSlice = createSlice({
8 | name: 'componentCodePreview',
9 | initialState,
10 | reducers: {
11 | addComponentCode: (state) => {},
12 | },
13 | });
14 |
15 | // Action creators are generated for each case reducer function
16 | export const { addComponentCode } = componentCodeSlice.actions;
17 |
18 | export default componentCodeSlice.reducer;
19 |
--------------------------------------------------------------------------------
/src/redux/fileTreeSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | fileNames: [],
5 | };
6 |
7 | export const fileTreeSlice = createSlice({
8 | name: 'fileTree',
9 | initialState,
10 | reducers: {
11 | addFile: (state) => {},
12 | },
13 | });
14 |
15 | export const { addFile } = fileTreeSlice.actions;
16 |
17 | export default fileTreeSlice.reducer;
18 |
--------------------------------------------------------------------------------
/src/redux/navigationSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | toggle: 'DnD',
5 | };
6 |
7 | export const navigationSlice = createSlice({
8 | name: 'nav',
9 | initialState,
10 | reducers: {
11 | toggleLeftPanel: (state, action) => {
12 | state.toggle = action.payload;
13 | },
14 | },
15 | });
16 |
17 | export const { toggleLeftPanel } = navigationSlice.actions;
18 |
19 | export default navigationSlice.reducer;
20 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import canvasReducer from './canvasSlice';
3 | import tagReducer from './tagsSlice';
4 | import themeReducer from './themeSlice';
5 | import fileTreeReducer from './fileTreeSlice';
6 | import navigationReducer from './navigationSlice';
7 |
8 | export const store = configureStore({
9 | reducer: {
10 | canvas: canvasReducer,
11 | tags: tagReducer,
12 | theme: themeReducer,
13 | fileTree: fileTreeReducer,
14 | nav: navigationReducer,
15 | },
16 | });
17 |
18 |
--------------------------------------------------------------------------------
/src/redux/tagsSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | tagList: {
5 | divTag: 'Div',
6 | pTag: 'Paragraph',
7 | aTag: 'Anchor',
8 | imgTag: 'Image',
9 | ulTag: 'Unordered List',
10 | formTag: 'Form',
11 | olTag: 'Ordered List',
12 | buttonTag: 'Button',
13 | liTag: 'List Item',
14 | spanTag: 'Span',
15 | h1Tag: 'Header 1',
16 | h2Tag: 'Header 2',
17 | h3Tag: 'Header 3',
18 | tableTag: 'Table',
19 | tHeadTag: 'THead'
20 | },
21 | };
22 |
23 | export const tagsSlice = createSlice({
24 | name: 'tags',
25 | initialState,
26 | reducers: {},
27 | });
28 |
29 | // Action creators are generated for each case reducer function
30 | // export const { } = tagsSlice.actions;
31 |
32 | export default tagsSlice.reducer;
33 |
--------------------------------------------------------------------------------
/src/redux/themeSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | const initialState = {
4 | currTheme: 'vs-dark',
5 | };
6 |
7 | export const themeSlice = createSlice({
8 | name: 'theme',
9 | initialState,
10 | reducers: {
11 | changeTheme: (state) => {
12 | state.currTheme = state.currTheme === 'vs-dark' ? 'vs-light' : 'vs-dark';
13 | },
14 | },
15 | });
16 |
17 | // Action creators are generated for each case reducer function
18 | export const { changeTheme } = themeSlice.actions;
19 |
20 | export default themeSlice.reducer;
21 |
--------------------------------------------------------------------------------
/src/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const userController = require('./userController');
3 |
4 | const app = express();
5 |
6 | const PORT = 8080;
7 |
8 | app.use(express.json());
9 |
10 | //test
11 | app.get('/', (req, res) => {
12 | res.send('Hi');
13 | });
14 |
15 | //POST -> USER SIGNUP
16 | app.post('/signup', userController.bcrypt, userController.signup, (req, res) => {
17 | return res.status(200).send('Sign up successful');
18 | });
19 |
20 | //POST -> USER LOGIN
21 | app.post('/login', userController.userLogin, (req, res) => {
22 | res.status(200).send(res.locals.loggedIn);
23 | });
24 |
25 | app.use('*', (req, res) => {
26 | res.status(404).send('Page not Found');
27 | });
28 |
29 | app.use((err, req, res, next) => {
30 | return res.status(500).send(`Internal Server Error: ${err}`);
31 | });
32 |
33 | app.listen(PORT, () => console.log(`listening on port ${PORT}`));
34 |
35 | module.exports = app;
36 |
--------------------------------------------------------------------------------
/src/server/userController.js:
--------------------------------------------------------------------------------
1 | const User = require('./userModel.js');
2 | const bcrypt = require('bcrypt');
3 |
4 | const userController = {};
5 |
6 | userController.bcrypt = (req, res, next) => {
7 | const { password } = req.body;
8 |
9 | bcrypt.hash(password, 10, (err, hash) => {
10 | res.locals.hashedPassword = hash;
11 | return next();
12 | });
13 | };
14 |
15 | userController.signup = (req, res, next) => {
16 | User.create({ username: req.body.username, password: res.locals.hashedPassword }, (err, newUser) => {
17 | if (err) return next(err);
18 | res.locals.userId = newUser._id;
19 | return next();
20 | });
21 | };
22 |
23 | userController.userLogin = (req, res, next) => {
24 | try {
25 | const { username, password } = req.body;
26 |
27 | User.findOne({ username: username }).exec((error, user) => {
28 | if (error || !user) return next(error);
29 | bcrypt.compare(password, user.password, (error, result) => {
30 | if (error) return next(error);
31 | if (result === true) {
32 | res.locals.loggedIn = 'successfully logged in';
33 | return next();
34 | } else if (result === false) {
35 | alert('Incorrect password.');
36 | return next();
37 | }
38 | });
39 | });
40 | } catch (err) {
41 | return next({ err });
42 | }
43 | };
44 |
45 | module.exports = userController;
46 |
--------------------------------------------------------------------------------
/src/server/userModel.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const mongoURI = ``;
4 |
5 | mongoose
6 | .connect(mongoURI, {
7 | useNewUrlParser: true,
8 | useUnifiedTopology: true,
9 | })
10 | .then(() => console.log('Connected to Mongo DB.'))
11 | .catch((err) => console.log(err));
12 |
13 | const Schema = mongoose.Schema;
14 |
15 | const userSchema = new Schema({
16 | username: { type: String, required: true },
17 | password: { type: String, required: true },
18 | });
19 |
20 | const User = mongoose.model('User', userSchema);
21 |
22 | module.exports = User;
23 |
--------------------------------------------------------------------------------
/src/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
--------------------------------------------------------------------------------
/src/stylesheets/App.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html,
6 | *,
7 | *::before,
8 | *::after {
9 | box-sizing: border-box;
10 | }
11 |
12 | ::-webkit-scrollbar {
13 | display: none;
14 | }
15 |
16 | body {
17 | margin: 0;
18 | overflow: hidden;
19 | }
20 |
21 | .App {
22 | display: flex;
23 | height: 100vh;
24 | background-color: var(--backgroundColor);
25 | color: var(--textColor);
26 | transition: all 0.2s ease-in;
27 | box-sizing: border-box;
28 | }
29 |
30 | /** COMPLETE COLOR PALETTE OF FFLOW
31 | * NOT ALL COLORS ARE USED CURRENTLY
32 | **/
33 | :root {
34 | /* Shades of Green */
35 | --green-500: #03aa99;
36 | --green-400: #03b9a7;
37 | --green-300: #03c9b5;
38 | --green-200: #03dac6;
39 | --green-100: #04ecd5;
40 |
41 | /* Shades of purple */
42 | --purple-900: #390089;
43 | --purple-700: #4e00bc;
44 | --purple-600: #5d00df;
45 | --purple-500: #6200ee;
46 | --purple-400: #832bff;
47 | --purple-300: #a565ff;
48 | --purple-200: #bb8bff;
49 | --purple-100: #ddc5ff;
50 |
51 | /* Shades of orange */
52 | --orange-400: #ffd523;
53 | --orange-500: #ffc600;
54 |
55 | /* Neutrals Palette */
56 | --neutral-50: #f8f9fa;
57 | --neutral-100: #f6f7f9;
58 | --neutral-200: #e5e7ea;
59 | --neutral-300: #ced2d6;
60 | --neutral-400: #9ea5ad;
61 | --neutral-500: #7f8790;
62 | --neutral-600: #676e76;
63 | --neutral-700: #596066;
64 | --neutral-800: #454c52;
65 | --neutral-900: #383f45;
66 | --neutral-1000: #24292e;
67 | }
68 |
69 | /* DEFAULT THEME IS DARK */
70 | body {
71 | --backgroundColor: #272727;
72 | --textColor: #ffffff;
73 | --thirdBackgroundColor: #27272a;
74 | --errorColor: #cf6679;
75 | --navigation-panel-background-color: var(--purple-200);
76 | --navigation-panel-border-color: var(--neutral-600);
77 | --tab-bottom-borderColor: #5b5a5f;
78 | --file-tree-folder-color: #ffffff;
79 | --canvas-file-name-color: #61dbfb;
80 | --javascript-icon-color: #f7df1e;
81 | --javascript-icon-background-color: transparent;
82 | --css-icon-background-color: transparent;
83 | --css-icon-color: #0098ff;
84 | --file-tree-hover-color: var(--neutral-800);
85 | --tab-container-color: #818cf8;
86 | }
87 |
88 | .theme-light {
89 | --backgroundColor: #eeeeee;
90 | --textColor: #000000;
91 | --thirdBackgroundColor: var(--neutral-50);
92 | --errorColor: #b00020;
93 | --navigation-panel-background-color: #4338ca;
94 | --navigation-panel-border-color: none;
95 | --tab-bottom-borderColor: var(--neutral-200);
96 | --file-tree-folder-color: var(--orange-500);
97 | --canvas-file-name-color: #4338ca;
98 | --javascript-icon-color: black;
99 | --javascript-icon-background-color: #f7df1e;
100 | --css-icon-background-color: #0098ff;
101 | --css-icon-color: white;
102 | --file-tree-hover-color: var(--neutral-300);
103 | --react-icon-background-color: #27272a;
104 | --tab-container-color: #4f46e5;
105 | }
106 |
107 | /* THEME BASED ON USER'S OS PREFERENCE */
108 | /* @media (prefers-color-scheme: dark) {
109 | body {
110 | --backgroundColor: var(--secondaryBackgroundColor);
111 | --textColor: #ffffff;
112 | --thirdBackgroundColor: var(--secondaryBackgroundColor);
113 | --errorColor: #cf6679;
114 | --navigation-panel-background-color: var(--purple-200);
115 | --navigation-panel-border-color: var(--neutral-600);
116 | --navigation-panel-textColor: #ffffff;
117 | --tab-bottom-borderColor: #5b5a5f;
118 | }
119 | }
120 |
121 | @media (prefers-color-scheme: no-preference) {
122 | body {
123 | --backgroundColor: var(--secondaryBackgroundColor);
124 | --textColor: #ffffff;
125 | --thirdBackgroundColor: var(--secondaryBackgroundColor);
126 | --errorColor: #cf6679;
127 | --navigation-panel-background-color: var(--purple-200);
128 | --navigation-panel-border-color: var(--neutral-600);
129 | --navigation-panel-textColor: #ffffff;
130 | --tab-bottom-borderColor: #5b5a5f;
131 | }
132 | }
133 |
134 | @media (prefers-color-scheme: light) {
135 | body {
136 | --backgroundColor: var(--secondaryBackgroundColor);
137 | --textColor: #ffffff;
138 | --thirdBackgroundColor: var(--secondaryBackgroundColor);
139 | --errorColor: #cf6679;
140 | --navigation-panel-background-color: var(--purple-200);
141 | --navigation-panel-border-color: var(--neutral-600);
142 | --navigation-panel-textColor: #ffffff;
143 | --tab-bottom-borderColor: #5b5a5f;
144 | }
145 | } */
146 |
--------------------------------------------------------------------------------
/src/stylesheets/BodyContainer.css:
--------------------------------------------------------------------------------
1 | .bodyContainer {
2 | display: flex;
3 | flex: 1 1;
4 | gap: 3rem;
5 | height: 100%;
6 | }
7 |
8 | .bodyContainer > div {
9 | text-align: center;
10 | }
11 |
12 | #headerAndCodePreviewContainer {
13 | display: flex;
14 | flex-direction: column;
15 | flex: 1 1 20rem;
16 | max-width: 35%;
17 | height: 100%;
18 | border-left: 1px solid var(--file-tree-hover-color);
19 | }
20 |
21 |
--------------------------------------------------------------------------------
/src/stylesheets/Canvas.css:
--------------------------------------------------------------------------------
1 | .canvas {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: flex-start;
5 | flex: 1 1 30%;
6 | background-color: var(--thirdBackgroundColor);
7 | position: relative;
8 | margin: 3rem 0rem;
9 | border-radius: 0.5rem;
10 | padding: 0.5rem 1rem;
11 | overflow-y: overlay;
12 | transition: 200ms;
13 | border: 0.15rem dashed var(--neutral-600);
14 | box-shadow: rgba(0, 0, 0, 0.24) 0px 3px 8px;
15 | }
16 |
17 | #canvas-item:active {
18 | background-color: var(--canvas-file-name-color);
19 | }
20 |
21 | .canvas:hover {
22 | cursor: move;
23 | }
24 |
25 | #canvas-instruction {
26 | color: var(--neutral-600);
27 | font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
28 | font-size: 1.2rem;
29 | font-weight: 700;
30 | }
31 |
32 | #current-canvas-file-name {
33 | color: var(--canvas-file-name-color);
34 | }
35 |
36 | .flex-auto {
37 | border: 0.1rem solid var(--textColor);
38 | text-align: center;
39 | }
40 |
41 | .flex-auto:hover {
42 | border: 0.1rem solid var(--tab-container-color);
43 | cursor: move;
44 | transition: 200ms;
45 | }
46 |
47 | ::-webkit-scrollbar-track {
48 | background: rgba(0, 0, 0, 0.2);
49 | }
50 |
--------------------------------------------------------------------------------
/src/stylesheets/CodePreview.css:
--------------------------------------------------------------------------------
1 | .codePreviewContainer {
2 | position: relative;
3 | background-color: var(--thirdBackgroundColor);
4 | box-shadow: -0.2rem 0rem 0.5rem var(--shadow-color);
5 | margin-right: 2%;
6 | border-radius: 0.5rem;
7 | }
8 |
9 | .tabContainer {
10 | display: flex;
11 | box-shadow: -0.3rem 0rem 1rem var(--shadow-color);
12 | }
13 |
14 | .tabs {
15 | width: 50%;
16 | background: var(--thirdBackgroundColor);
17 | color: var(--neutral-600);
18 | cursor: pointer;
19 | border-bottom: 0.1rem solid var(--tab-bottom-borderColor);
20 | position: relative;
21 | outline: none;
22 | font-size: 1rem;
23 | padding: 0.8rem;
24 | text-align: center;
25 | transition: 200ms;
26 | }
27 |
28 | .tabs:hover {
29 | color: var(--tab-container-color);
30 | transition: 200ms;
31 | }
32 |
33 | .active-tab {
34 | width: 50%;
35 | cursor: pointer;
36 | position: relative;
37 | outline: none;
38 | font-size: 1rem;
39 | padding: 0.8rem;
40 | background: var(--thirdBackgroundColor);
41 | border-bottom: 1px solid var(--tab-container-color);
42 | color: var(--tab-container-color);
43 | font-weight: 600;
44 | transition: 200ms;
45 | }
46 |
47 | .contentContainer {
48 | flex-grow: 1;
49 | width: 100%;
50 | position: absolute;
51 | height: 100vh;
52 | text-align: left;
53 | box-shadow: -0.3rem 0rem 0.5rem var(--shadow-color);
54 | }
55 |
56 | .content {
57 | width: 100%;
58 | height: 100%;
59 | display: none;
60 | text-align: left;
61 | }
62 |
63 | .active-content {
64 | width: 100%;
65 | height: 100vh;
66 | text-align: left;
67 | }
68 |
69 | #cssContainer {
70 | height: 100%;
71 | }
72 |
--------------------------------------------------------------------------------
/src/stylesheets/CompCreator.css:
--------------------------------------------------------------------------------
1 | #comp-create-box {
2 | display: flex;
3 | flex-direction: column;
4 | margin-top: 0.5rem;
5 | margin-bottom: 1rem;
6 | }
7 | .create-component-form {
8 | display: flex;
9 | align-items: center;
10 | width: 100%;
11 | }
12 | #create-react-component-inputs {
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | margin: 0.5rem;
17 | gap: 0.5rem;
18 | }
19 | ::placeholder {
20 | font-size: 0.8em;
21 | margin: 0;
22 | }
23 |
24 | #create-react-component-button {
25 | margin-top: 0;
26 | text-transform: uppercase;
27 | box-shadow: none;
28 | font-size: 0.8rem;
29 | background-color: var(--navigation-panel-background-color);
30 | color: white;
31 | padding: 0.3rem 0.8rem;
32 | border-radius: 0.3rem;
33 | font-weight: 500;
34 | border: 0px transparent none;
35 | background-position: center;
36 | transition: background 0.2s;
37 | }
38 |
39 | #create-react-component-button:hover {
40 | background-color: var(--purple-300);
41 | transition: 200ms;
42 | }
43 |
44 | #create-react-component-button:active {
45 | box-shadow: 0 0 2px #666;
46 | transform: translateY(0.5px);
47 | background-color: var(--purple-100);
48 | background-size: 100%;
49 | transition: background 0s;
50 | }
51 |
--------------------------------------------------------------------------------
/src/stylesheets/CustomComponents.css:
--------------------------------------------------------------------------------
1 | #custom-components-container {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | .custom-components {
7 | border-radius: 8px;
8 | padding: 4px 20px;
9 | margin-top: 5px;
10 | font-size: 12px;
11 | font-weight: 500;
12 | text-transform: uppercase;
13 | color: var(--navigation-panel-background-color);
14 | border: 1px solid var(--navigation-panel-background-color);
15 | }
--------------------------------------------------------------------------------
/src/stylesheets/DnD.css:
--------------------------------------------------------------------------------
1 | .dndContainer {
2 | display: flex;
3 | position: relative;
4 | height: 100%;
5 | flex-direction: column;
6 | justify-content: space-between;
7 | align-items: center;
8 | background-color: var(--thirdBackgroundColor);
9 | overflow: hidden;
10 | max-width: 16rem;
11 | min-width: 14rem;
12 | box-shadow: 0.3rem 0.3rem 0.5rem var(--shadow-color);
13 | border-right: 0.1rem solid var(--file-tree-hover-color);
14 | }
15 |
16 | .fileTreeContainer {
17 | display: flex;
18 | position: relative;
19 | height: 100%;
20 | flex-direction: column;
21 | justify-content: flex-start;
22 | align-items: center;
23 | background-color: var(--thirdBackgroundColor);
24 | overflow: hidden;
25 | max-width: 16rem;
26 | min-width: 14rem;
27 | padding: 3rem auto;
28 | border-right: 0.1rem solid var(--file-tree-hover-color);
29 | }
30 |
31 | h3 {
32 | padding-bottom: 1.5rem;
33 | }
34 |
35 | #app-name {
36 | font-size: 1.5rem;
37 | font-weight: 700;
38 | color: var(--navigation-panel-background-color);
39 | margin-top: 0.8rem;
40 | }
41 |
42 | .homePageFileTreeContainer {
43 | border-top: 0.1rem solid var(--file-tree-hover-color);
44 | margin-top: 1.2rem;
45 | padding-top: 0.8rem;
46 | padding: 0;
47 | padding-left: 0.2rem;
48 | display: flex;
49 | flex-direction: column;
50 | align-items: flex-start;
51 | width: 100%;
52 | height: 40%;
53 | }
54 |
55 | .homePageFileTreeContainer div:hover {
56 | cursor: pointer;
57 | }
58 |
59 | .file-tree-heading {
60 | margin-bottom: 0.8rem;
61 | color: var(--neutral-500);
62 | }
63 |
64 | .folder-icon-in-file-tree {
65 | color: var(--file-tree-folder-color);
66 | opacity: 0.8;
67 | }
68 |
69 | .folder-icon-in-file-tree:hover {
70 | background-color: var(--neutral-700);
71 | }
72 |
73 | .nested-files:hover {
74 | background-color: var(--file-tree-hover-color);
75 | }
76 |
77 | h2 {
78 | padding-left: 0.6rem;
79 | padding-top: 0.6rem;
80 | }
81 |
82 | #file-tree-heading-page2 {
83 | color: var(--neutral-500);
84 | margin-left: 0;
85 | align-self: flex-start;
86 | align-items: flex-start;
87 | justify-self: flex-start;
88 | }
89 |
--------------------------------------------------------------------------------
/src/stylesheets/DragList.css:
--------------------------------------------------------------------------------
1 | #draggable-elements-container {
2 | display: flex;
3 | flex-direction: column;
4 | height: 32rem;
5 | width: 100%;
6 | padding: 0rem 3.75rem;
7 | border-radius: 0.5rem;
8 | align-self: center;
9 | overflow-y: overlay;
10 | }
11 | .dragItems p {
12 | padding: 0.3rem 1rem;
13 | margin: 0;
14 | box-shadow: none;
15 | border-radius: 0.3rem;
16 | font-weight: 600;
17 | color: var(--tab-container-color);
18 | border: 0.1rem solid var(--tab-container-color);
19 | text-transform: uppercase;
20 | font-size: 0.7rem;
21 | overflow: none;
22 | background-color: var(--thirdBackgroundColor);
23 | margin-bottom: 0.2rem;
24 | user-select: none;
25 | }
26 | .dragItems p:hover {
27 | border: 0.1rem solid var(--tab-container-color);
28 | background-color: var(--tab-container-color);
29 | color: var(--thirdBackgroundColor);
30 | cursor: move;
31 | transition: 100ms;
32 | }
33 | .dragItems p:active {
34 | border-radius: 0.3rem;
35 | background-color: var(--tab-container-color);
36 | color: var(--thirdBackgroundColor);
37 | cursor: move;
38 | }
39 |
--------------------------------------------------------------------------------
/src/stylesheets/ExportModal.css:
--------------------------------------------------------------------------------
1 | .modal {
2 | display: flex;
3 | flex-direction: column;
4 | justify-content: center;
5 | align-items: center;
6 | position: relative;
7 | background-color: white;
8 | color: black;
9 | height: 20rem;
10 | width: 25rem;
11 | margin: auto;
12 | z-index: 5;
13 | }
14 |
15 |
--------------------------------------------------------------------------------
/src/stylesheets/Header.css:
--------------------------------------------------------------------------------
1 | .headerContainer {
2 | flex: 0 0 5%;
3 | display: flex;
4 | justify-content: space-between;
5 | align-items: center;
6 | padding-left: 1rem;
7 | margin: 1rem 1.8rem 1rem 0rem;
8 | }
9 |
10 | .headerContainer button {
11 | transition: 200ms;
12 | }
13 |
14 | .headerContainer button:active {
15 | transform: translateY(1px);
16 | background-color: var(--purple-500);
17 | background-size: 100%;
18 | transition: background 0s;
19 | }
20 |
21 | .checkbox {
22 | opacity: 0;
23 | position: absolute;
24 | }
25 |
26 | .label {
27 | width: 2.5rem;
28 | height: 1.25rem;
29 | background-color: #454c52;
30 | display: flex;
31 | border-radius: 1rem;
32 | align-items: center;
33 | justify-content: space-between;
34 | padding: 0.2rem;
35 | position: relative;
36 | transform: scale(1.5);
37 | cursor: pointer;
38 | }
39 |
40 | .ball {
41 | border-radius: 50%;
42 | transition: transform 0.2s linear;
43 | left: 2px;
44 | right: 1px;
45 | background-color: white;
46 | position: absolute;
47 | width: 12px;
48 | height: 12px;
49 | }
50 |
51 | /* target the element after the label*/
52 | .checkbox:checked + .label .ball {
53 | transform: translateX(23px);
54 | }
55 |
--------------------------------------------------------------------------------
/src/stylesheets/Login.css:
--------------------------------------------------------------------------------
1 | #modal-overlay {
2 | position: absolute;
3 | z-index: 1000;
4 | top: 0;
5 | right: 0;
6 | bottom: 0;
7 | left: 0;
8 | background-color: rgba(0, 0, 0, 0.8);
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | }
13 |
14 | .form-modal {
15 | position: relative;
16 | background-color: #1c1c1e;
17 | color: white;
18 | flex: 0 1 300px;
19 | max-height: 450px;
20 | border-radius: 8px;
21 | padding-bottom: 20px;
22 | display: flex;
23 | flex-direction: column;
24 | justify-content: center;
25 | align-items: center;
26 | gap: 20px;
27 | }
28 |
29 | #login-email-label {
30 | size: 13px;
31 | text-align: left;
32 | color: white;
33 | }
34 |
35 | #login-password-label {
36 | size: 13px;
37 | text-align: left;
38 | color: white;
39 | }
40 |
41 | #login-email-input-field {
42 | height: 40px;
43 | border-radius: 5px;
44 | background-color: var(--neutral-700);
45 | }
46 |
47 | ::placeholder {
48 | color: white;
49 | font-size: 12px;
50 | }
51 |
52 | #login-password-input-field {
53 | height: 40px;
54 | border-radius: 5px;
55 | background-color: var(--neutral-700);
56 | }
57 | #login-submit-button {
58 | width: 70%;
59 | }
60 | #close-login-form-modal {
61 | display: flex;
62 | justify-content: flex-end;
63 | margin-left: 90%;
64 | }
65 |
--------------------------------------------------------------------------------
/src/stylesheets/Navigation.css:
--------------------------------------------------------------------------------
1 | .navigation-bar {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | width: 2.5rem;
6 | gap: 1rem;
7 | background-color: var(--navigation-panel-background-color);
8 | font-size: 1.6rem;
9 | padding-top: 1rem;
10 | }
11 |
12 | .nav-icons {
13 | padding: 0.8rem;
14 | color: #ffffff;
15 | opacity: 0.9;
16 | transition: 300ms;
17 | }
18 |
19 | .nav-icons:hover {
20 | color: black;
21 | cursor: pointer;
22 | transition: 300ms;
23 | }
24 |
--------------------------------------------------------------------------------
/src/stylesheets/TagCreator.css:
--------------------------------------------------------------------------------
1 | #tag-create-form {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | margin-bottom: 1rem;
6 | width: 250px;
7 | }
8 | #html-tag-creator-container-header {
9 | font-size: 16px;
10 | color: var(--textColor);
11 | margin-bottom: 10px;
12 | }
13 |
14 | .tag-name-input-field {
15 | display: flex;
16 | outline: none;
17 | margin: 0.3rem 0;
18 | background: var(--backgroundColor);
19 | border-radius: 5px;
20 | width: 10rem;
21 | font-size: 14px;
22 | outline: none;
23 | opacity: 0.5;
24 | border: 1px solid var(--navigation-panel-background-color);
25 | color: var(--textColor);
26 | }
27 |
28 | .tag-name-labels {
29 | font-size: 12px;
30 | display: in-line block;
31 | }
32 |
33 | ::placeholder {
34 | color: var(--navigation-panel-background-color);
35 | }
36 |
37 | #create-html-tag-submit-button {
38 | margin-top: 0;
39 | text-transform: uppercase;
40 | box-shadow: none;
41 | font-size: 12px;
42 | background-color: var(--navigation-panel-background-color);
43 | color: white;
44 | padding: 4px 20px;
45 | margin-top: 1rem;
46 | border-radius: 7px;
47 | font-weight: 500;
48 | border: 0px transparent none;
49 | }
50 |
--------------------------------------------------------------------------------
/src/stylesheets/Terminal.css:
--------------------------------------------------------------------------------
1 | .xterm {
2 | /* position: absolute; */
3 | position: relative;
4 | user-select: none;
5 | -ms-user-select: none;
6 | -webkit-user-select: none;
7 | }
8 |
9 | .xterm.focus,
10 | .xterm:focus {
11 | outline: none;
12 | }
13 |
14 | .xterm .xterm-helpers {
15 | position: absolute;
16 | top: 0;
17 | /**
18 | * The z-index of the helpers must be higher than the canvases in order for
19 | * IMEs to appear on top.
20 | */
21 | z-index: 5;
22 | }
23 |
24 | .xterm .xterm-helper-textarea {
25 | padding: 0;
26 | border: 0;
27 | margin: 0;
28 | /* Move textarea out of the screen to the far left, so that the cursor is not visible */
29 | position: absolute;
30 | opacity: 0;
31 | left: -9999em;
32 | top: 0;
33 | width: 0;
34 | height: 0;
35 | z-index: -5;
36 | /** Prevent wrapping so the IME appears against the textarea at the correct position */
37 | white-space: nowrap;
38 | overflow: hidden;
39 | resize: none;
40 | }
41 |
42 | .xterm .composition-view {
43 | /* TODO: Composition position got messed up somewhere */
44 | background: #000;
45 | color: #fff;
46 | display: none;
47 | position: absolute;
48 | white-space: nowrap;
49 | z-index: 1;
50 | }
51 |
52 | .xterm .composition-view.active {
53 | display: block;
54 | }
55 |
56 | .xterm .xterm-viewport {
57 | /* On OS X this is required in order for the scroll bar to appear fully opaque */
58 | background-color: #000;
59 | overflow-y: scroll;
60 | cursor: default;
61 | position: absolute;
62 | right: 0;
63 | left: 0;
64 | top: 0;
65 | bottom: 0;
66 | }
67 |
68 | .xterm .xterm-screen {
69 | position: relative;
70 | }
71 |
72 | .xterm .xterm-screen canvas {
73 | position: absolute;
74 | left: 0;
75 | top: 0;
76 | }
77 |
78 | .xterm .xterm-scroll-area {
79 | visibility: hidden;
80 | }
81 |
82 | .xterm-char-measure-element {
83 | display: inline-block;
84 | visibility: hidden;
85 | position: absolute;
86 | top: 0;
87 | left: -9999em;
88 | line-height: normal;
89 | }
90 |
91 | .xterm {
92 | cursor: text;
93 | }
94 |
95 | .xterm.enable-mouse-events {
96 | /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
97 | cursor: default;
98 | }
99 |
100 | .xterm.xterm-cursor-pointer,
101 | .xterm .xterm-cursor-pointer {
102 | cursor: pointer;
103 | }
104 |
105 | .xterm.column-select.focus {
106 | /* Column selection mode */
107 | cursor: crosshair;
108 | }
109 |
110 | .xterm .xterm-accessibility,
111 | .xterm .xterm-message {
112 | position: absolute;
113 | left: 0;
114 | top: 0;
115 | bottom: 0;
116 | right: 0;
117 | z-index: 10;
118 | color: transparent;
119 | }
120 |
121 | .xterm .live-region {
122 | position: absolute;
123 | left: -9999px;
124 | width: 1px;
125 | height: 1px;
126 | overflow: hidden;
127 | }
128 |
129 | .xterm-dim {
130 | opacity: 0.5;
131 | }
132 |
133 | .xterm-underline {
134 | text-decoration: underline;
135 | }
136 |
137 | .xterm-strikethrough {
138 | text-decoration: line-through;
139 | }
140 |
--------------------------------------------------------------------------------
/src/stylesheets/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
--------------------------------------------------------------------------------
/src/test-utils.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { configure, render as rtlRender } from '@testing-library/react';
3 | import { configureStore } from '@reduxjs/toolkit';
4 | import { Provider } from 'react-redux';
5 |
6 | import canvasReducer from './redux/canvasSlice';
7 | import tagReducer from './redux/tagsSlice';
8 | import themeReducer from './redux/themeSlice';
9 | import fileTreeReducer from './redux/fileTreeSlice';
10 | import navigationReducer from './redux/navigationSlice';
11 |
12 | function render(
13 | ui,
14 | {
15 | preloadedState,
16 | store = configureStore({
17 | reducer: { canvas: canvasReducer, tags: tagReducer, theme: themeReducer, fileTree: fileTreeReducer, nav: navigationReducer },
18 | preloadedState,
19 | }),
20 | ...renderOptions
21 | } = {}
22 | ) {
23 | function Wrapper({ children }) {
24 | return {children} ;
25 | }
26 | return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
27 | }
28 |
29 | export * from '@testing-library/react';
30 | export { render };
31 |
--------------------------------------------------------------------------------
/styleMock.js:
--------------------------------------------------------------------------------
1 | module.exports = {};
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme');
2 |
3 | module.exports = {
4 | content: [
5 | "./src/**/*.{js,jsx,ts,tsx}",
6 | ],
7 | theme: {
8 | extend: {
9 | fontFamily: {
10 | sans: ['Inter var', ...defaultTheme.fontFamily.sans],
11 | },
12 | },
13 | },
14 | plugins: [
15 | require('@themesberg/flowbite/plugin')
16 | ],
17 | }
--------------------------------------------------------------------------------
/webpack.build.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
6 | const APP_DIR = path.resolve(__dirname, './src');
7 | const MONACO_DIR = path.resolve(__dirname, './node_modules/monaco-editor');
8 |
9 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up
10 | const defaultInclude = path.resolve(__dirname, 'src');
11 |
12 | module.exports = {
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/,
17 | use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'],
18 | include: defaultInclude,
19 | },
20 | {
21 | test: /\.jsx?$/,
22 | use: [{ loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] } }],
23 | include: defaultInclude,
24 | },
25 | {
26 | test: /\.(jpe?g|png|gif)$/,
27 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }],
28 | include: defaultInclude,
29 | },
30 | {
31 | test: /\.(eot|svg|ttf|woff|woff2)$/,
32 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }],
33 | include: defaultInclude,
34 | },
35 | {
36 | test: /\.css$/,
37 | include: MONACO_DIR,
38 | use: ['style-loader', 'css-loader'],
39 | },
40 | {
41 | test: /\.node$/,
42 | use: ['node-loader'],
43 | },
44 | ],
45 | },
46 | target: 'electron-renderer',
47 | plugins: [
48 | new HtmlWebpackPlugin({ title: 'fflow' }),
49 | new MiniCssExtractPlugin({
50 | // Options similar to the same options in webpackOptions.output
51 | // both options are optional
52 | filename: 'bundle.css',
53 | chunkFilename: '[id].css',
54 | }),
55 | new webpack.DefinePlugin({
56 | 'process.env.NODE_ENV': JSON.stringify('production'),
57 | }),
58 | // new MinifyPlugin()
59 | ],
60 | resolve: {
61 | extensions: ['.js', '.jsx'],
62 | },
63 | stats: {
64 | colors: true,
65 | children: false,
66 | chunks: false,
67 | modules: false,
68 | },
69 | optimization: {
70 | minimize: true,
71 | },
72 | };
73 |
--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 | const HtmlWebpackPlugin = require('html-webpack-plugin');
4 | const { spawn } = require('child_process');
5 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
6 | const APP_DIR = path.resolve(__dirname, './src');
7 | const MONACO_DIR = path.resolve(__dirname, './node_modules/monaco-editor');
8 |
9 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up
10 | const defaultInclude = path.resolve(__dirname, 'src');
11 |
12 | module.exports = {
13 | module: {
14 | rules: [
15 | {
16 | test: /\.css$/,
17 | // include: APP_DIR,
18 | exclude: /node_modules/,
19 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' }],
20 | include: defaultInclude,
21 | },
22 | {
23 | test: /\.(jsx|js)$/,
24 | exclude: /node_modules/,
25 | use: [{ loader: 'babel-loader', options: { presets: ['@babel/preset-env', '@babel/preset-react'] } }],
26 | include: defaultInclude,
27 | },
28 | {
29 | test: /\.(jpe?g|png|gif)$/,
30 | exclude: /node_modules/,
31 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }],
32 | include: defaultInclude,
33 | },
34 | {
35 | test: /\.(eot|svg|ttf|woff|woff2)$/,
36 | exclude: /node_modules/,
37 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }],
38 | include: defaultInclude,
39 | },
40 | {
41 | test: /\.css$/,
42 | include: MONACO_DIR,
43 | use: ['style-loader', 'css-loader'],
44 | },
45 | {
46 | test: /\.node$/,
47 | use: ['node-loader'],
48 | },
49 | ],
50 | },
51 | target: 'electron-renderer',
52 | plugins: [
53 | new HtmlWebpackPlugin({
54 | title: 'fflow',
55 | // template: 'public/index.html'
56 | }),
57 | new webpack.DefinePlugin({
58 | 'process.env.NODE_ENV': JSON.stringify('development'),
59 | }),
60 | new MonacoWebpackPlugin(),
61 | ],
62 | resolve: {
63 | extensions: ['.js', '.jsx'],
64 | },
65 | devtool: 'cheap-source-map',
66 | devServer: {
67 | contentBase: path.resolve(__dirname, 'dist'),
68 | stats: {
69 | colors: true,
70 | chunks: false,
71 | children: false,
72 | },
73 | before() {
74 | spawn('electron', ['.'], { shell: true, env: process.env, stdio: 'inherit' })
75 | .on('close', (code) => process.exit(0))
76 | .on('error', (spawnError) => console.error(spawnError));
77 | },
78 | },
79 | };
80 |
--------------------------------------------------------------------------------