├── .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 |
3 | 4 | Logo 5 | 6 | 7 |

fflow

8 | 9 |

10 | Supercharge your React development process 11 |
12 | Explore the docs » 13 |
14 |
15 | View Demo 16 | · 17 | Report Bug 18 | · 19 | Request Feature 20 |

21 |
22 | 23 |

24 | GitHub Repo stars 25 | GitHub license 26 | GitHub issues 27 | Contributions Welcome 28 |
29 |
30 | Give a ⭐️ if our project helped or interests you! 31 |

32 | 33 |
34 | 35 |
36 | Table of Contents 37 |
    38 |
  1. 39 | About fflow 40 | 44 |
  2. 45 |
  3. 46 | Getting Started 47 | 51 |
  4. 52 |
  5. Run Exported Project
  6. 53 |
  7. Contributors
  8. 54 |
  9. Roadmap
  10. 55 |
  11. Contributing Guide
  12. 56 |
  13. License
  14. 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': `
    1. `, 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 |
      27 |
      28 |
      29 |
      30 | 36 | 44 |
      45 |
      46 |
      47 |
      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 |
      17 |

      {label}

      18 |
      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 | 27 | 28 | 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 | 36 | 37 | {/* */} 38 | themeToggle()} /> 39 | 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 | 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 | 17 | 20 | 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 |

      Custom HTML Elements

      8 |
      9 | {/* */} 10 | 11 | {/* */} 12 | 13 | {/* */} 14 | 15 | 16 |
      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': `
        1. `, 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 | --------------------------------------------------------------------------------