├── .github └── workflows │ ├── deploy.yml │ └── testing.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── __tests__ ├── Jest.test.tsx ├── client │ └── App.test.tsx ├── server │ └── server.txt └── types │ └── types.txt ├── assetsTransformer.js ├── babel.config.js ├── contributing.md ├── cypress.config.ts ├── cypress ├── component │ ├── BankEl.cy.tsx │ └── Canvas.cy.tsx ├── e2e │ ├── canvas.cy.ts │ ├── codepreview.cy.ts │ ├── fileDir.cy.ts │ ├── flowtree.cy.ts │ └── login.cy.ts ├── fixtures │ └── example.json └── support │ ├── commands.ts │ ├── component-index.html │ ├── component.ts │ └── e2e.ts ├── docker-compose-test.yml ├── index.html ├── jest-setup.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── protract-icon-white.png ├── protract-icon.png └── vite.svg ├── src ├── client │ ├── App.css │ ├── App.tsx │ ├── assets │ │ ├── gifs │ │ │ ├── Create-demo.gif │ │ │ ├── Export-demo.gif │ │ │ ├── Projects-demo.gif │ │ │ ├── Protract-demo.gif │ │ │ └── Tree-demo.gif │ │ ├── github-dark.png │ │ ├── github-light.png │ │ ├── logo-bg-white.png │ │ ├── logo.png │ │ ├── logo2.png │ │ ├── logo3.png │ │ ├── protract-favicon-color.png │ │ ├── protract-readme-logo.png │ │ └── react.svg │ ├── components │ │ ├── BankEl.tsx │ │ ├── Canvas.tsx │ │ ├── CodePreview.tsx │ │ ├── ComponentBank.tsx │ │ ├── Containers │ │ │ ├── LeftColumn.tsx │ │ │ ├── MainContainer.tsx │ │ │ └── Preview.tsx │ │ ├── CustomComponentCreator.tsx │ │ ├── FileDirectory.tsx │ │ ├── FlowTree.tsx │ │ ├── Modals │ │ │ ├── DeleteModal.tsx │ │ │ ├── LoadModal.tsx │ │ │ ├── LoginModal.tsx │ │ │ ├── SaveModal.tsx │ │ │ ├── SignUpModal.tsx │ │ │ └── WarningModal.tsx │ │ ├── Navbar.tsx │ │ ├── Playground.tsx │ │ └── SortableBankEl.tsx │ ├── helperFunctions │ │ ├── capitalizeFirstLetter.ts │ │ ├── dataToD3Elems.ts │ │ ├── generateAppModule.ts │ │ ├── generateComponentContents.ts │ │ ├── generateImportStatements.ts │ │ ├── generateTestContents.ts │ │ ├── insertAppPrefix.ts │ │ ├── traverseAndWrite.ts │ │ └── zipFiles.ts │ ├── index.css │ ├── main.tsx │ ├── tsconfig.json │ └── vite-env.d.ts ├── server │ ├── controllers │ │ ├── authControllers │ │ │ └── userController.ts │ │ ├── cookieControllers │ │ │ ├── cookieController.ts │ │ │ └── sessionController.ts │ │ └── projControllers │ │ │ └── projController.ts │ ├── main.ts │ ├── models │ │ ├── projectModel.ts │ │ ├── sessionModel.ts │ │ └── userModel.ts │ └── routers │ │ ├── authRouter.ts │ │ └── projRouter.ts └── types.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.prod.json └── vite.config.ts /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | jobs: 7 | e2e-testing: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: npm install 12 | - uses: cypress-io/github-action@v5 13 | with: 14 | build: npm run build 15 | start: npm start 16 | wait-on: 'http://localhost:3000' 17 | env: 18 | MONGO_URI: ${{ secrets.MONGO_URI }} 19 | deploy: 20 | needs: e2e-testing 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Check out code 24 | uses: actions/checkout@v3 25 | 26 | - name: Create .env file 27 | uses: SpicyPizza/create-envfile@v2.0.2 28 | with: 29 | envkey_MONGO_URI: ${{ secrets.MONGO_URI }} 30 | envkey_MODE: ${{ secrets.MODE }} 31 | 32 | - name: Configure AWS credentials 33 | uses: aws-actions/configure-aws-credentials@v1 34 | with: 35 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 36 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 37 | aws-region: us-west-2 38 | 39 | - name: Login to Amazon ECR 40 | id: login-ecr 41 | uses: aws-actions/amazon-ecr-login@v1 42 | 43 | - name: Build, tag, and push image to Amazon ECR 44 | env: 45 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 46 | ECR_REPOSITORY: docker-protract 47 | IMAGE_TAG: latest 48 | 49 | run: | 50 | npm install 51 | docker build -t testing111 . 52 | docker tag testing111 $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 53 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 54 | -------------------------------------------------------------------------------- /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | name: testing 2 | on: 3 | pull_request: 4 | branches: 5 | - dev 6 | jobs: 7 | e2e-testing: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - run: npm install 12 | - uses: cypress-io/github-action@v5 13 | with: 14 | build: npm run build 15 | start: npm start 16 | wait-on: 'http://localhost:3000' 17 | env: 18 | MONGO_URI: ${{ secrets.MONGO_URI }} 19 | MODE: ${{ secrets.MODE }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | coverage 14 | *.local 15 | coverage-ts 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | 28 | # env variables 29 | .env 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY . . 6 | 7 | RUN npm i -g ts-node 8 | 9 | RUN npm i 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 3000 14 | 15 | CMD [ "ts-node", "src/server/main.ts" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Protract 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | Logo 5 | 6 | 7 |

Protract

8 | 9 |

10 | Supercharge your Angular development process 11 |
12 | Build Your Angular App » 13 |
14 | Report Bug 15 | · 16 | Request Feature 17 |

18 |
19 | 20 |
21 | 22 | ![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) 23 | ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) 24 | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) 25 | ![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) 26 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 27 | ![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) 28 | ![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) 29 | ![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) 30 | ![Angular](https://img.shields.io/badge/Angular-DD0031?style=for-the-badge&logo=angular&logoColor=white) 31 | ![cypress](https://img.shields.io/badge/-cypress-%23E5E5E5?style=for-the-badge&logo=cypress&logoColor=058a5e) 32 | ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) 33 | ![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) 34 | ![TailwindCSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) 35 | ![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white) 36 | ![Vite](https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white) 37 | ![Babel](https://img.shields.io/badge/Babel-F9DC3e?style=for-the-badge&logo=babel&logoColor=black) 38 | 39 |
40 | 41 |
42 | Table of Contents 43 |
    44 |
  1. About Protract
  2. 45 |
  3. Features
  4. 46 |
  5. 47 | Getting Started 48 | 52 |
  6. 53 |
  7. Run Exported Project
  8. 54 |
  9. Contributions
  10. 55 |
  11. Contributors
  12. 56 |
  13. License
  14. 57 |
58 |
59 | 60 | ##

About Protract

61 | 62 | Protract is an accessible developer tool built from the ground up to assist with the blueprinting of Angular applications, simplifying the process of creating a hierarchy of components and allowing developers to quickly create the structure of their app so they can start coding faster. 63 | 64 | [Here](https://medium.com/@protract-app/protract-a-visual-guide-for-angular-fd80cbcc32ba) is a medium article describing the philosophy behind Protract. 65 | 66 | Visit our website Protract.dev! 67 | 68 |

69 | 70 |

71 | 72 | ##

Features

73 | 74 | ### Component Manipulation 75 | 76 | Drag and drop functionality to create, reorder, and delete HTML tags. Create custom components declared by the user that can be nested. 77 | 78 |

79 | 80 |

81 |
82 | 83 | ### Live Updates 84 | 85 | Real-time visualization of code for each custom component, file structure of directory, and hierarchy of components. 86 | 87 |

88 | 89 |

90 |
91 | 92 | ### Context Switch 93 | 94 | Change current component canvas by clicking on the component in file directory or component tree. 95 |
96 |
97 | 98 | ### Cloud Storage 99 | 100 | Create, save, load, and delete projects. 101 | 102 |

103 | 104 |

105 |
106 | 107 | ### Access Anywhere 108 | 109 | Fully online in-browser functionality. 110 |
111 |
112 | 113 | ### Easy Export 114 | 115 | Export projects to use in a newly created Angular project. 116 | 117 |

118 | 119 |

120 |
121 |
122 | 123 | ##

Getting Started

124 | 125 | ###

Running Online

126 | 127 | You can start using Protract by visiting the website here. 128 | To save and load projects you will need to make an account and login. Once your blueprint is completed you can hit the export button on the canvas. 129 | 130 | ###

Running Locally

131 | 132 | If you would like to run with Docker, 133 | 134 | ``` 135 | docker pull protractors/protract-prod:latest 136 | ``` 137 | 138 | ``` 139 | docker run -p :3000 protractors/protract-prod 140 | ``` 141 | 142 | If you would like to use the app by forking and cloning: 143 | 144 | Fork this repository to your own GitHub account. 145 | Clone the forked repository to your machine 146 | 147 | ``` 148 | git clone https://github.com//protract.git 149 | ``` 150 | 151 | Create a .env in the root directory that contains 2 variables, 152 | 153 | ``` 154 | MONGO_URI= 155 | mode=production 156 | ``` 157 | 158 | Navigate to the root project directory and install dependencies. 159 | 160 | ``` 161 | cd protract 162 | npm install 163 | ``` 164 | 165 | If you would like to run in development mode, `npm run dev` and visit localhost:3000. 166 | 167 | If you would like to run in production mode, `npm run build` and then `npm start` and visit localhost:3000. 168 | 169 | ##

Run Exported Project

170 | 171 | In your terminal, 172 | 173 | ``` 174 | npm install -g @angular/cli 175 | ``` 176 | 177 | To install the Angular CLI if it has not already been installed. 178 | 179 | ``` 180 | ng new 181 | ``` 182 | 183 | To start your new project. 184 | 185 | In your file explorer, extract the zip file and replace the directory’s app folder with the one contained in the zip file. 186 | 187 | ##

Contributions

188 | 189 | We welcome contributions from the community. If you are interested in contributing to this project, please refer to our Contributing Guidelines for more information. 190 | 191 | ##

Contributors

192 | 193 | | Developed By | | | 194 | | :-----------: | :--------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------: | 195 | | Don Do | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Donlebon) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](www.linkedin.com/in/don-do-26822a281) | 196 | | Vander Harris | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/vdharris/) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/vanderharris/) | 197 | | Peter Tran | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/tranpeter95) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/peter-tran-6574b81b9/) | 198 | | Steven Vaughn | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Svaughn4418) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](www.linkedin.com/in/steven-vaughn-126a69280) | 199 | | Douglas Yao | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/douglas-yao) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/douglas-yao/) | 200 | 201 | ##

License

202 | 203 |

Protract is licensed under the terms of the MIT license.

204 | -------------------------------------------------------------------------------- /__tests__/Jest.test.tsx: -------------------------------------------------------------------------------- 1 | test('jest working', () => { 2 | expect(true).toBe(true); 3 | }); 4 | 5 | export {}; 6 | -------------------------------------------------------------------------------- /__tests__/client/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import App from '../../src/client/App'; 5 | 6 | describe('app', () => { 7 | test('renders without errors', () => { 8 | render(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/server/server.txt: -------------------------------------------------------------------------------- 1 | for server -------------------------------------------------------------------------------- /__tests__/types/types.txt: -------------------------------------------------------------------------------- 1 | for types -------------------------------------------------------------------------------- /assetsTransformer.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | process(src, filename, config, options) { 5 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-react', 12 | '@babel/preset-typescript', 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 |

Contributing to Protract

2 | 3 | The Protract Team would like to thank you for your interest in helping to maintain and improve Protract! 4 |
5 | 6 | Reporting Bugs:
7 | All code changes happen through Github Pull Requests and we actively welcome them. To submit your pull request, follow the steps below: 8 |
9 | 10 | Pull Requests: 11 |
    12 |
  1. Fork the repo and create your branch from main.
  2. 13 |
  3. If you've added code that should be tested, add tests.
  4. 14 |
  5. If you've changed APIs, update the documentation.
  6. 15 |
  7. Ensure the test suite passes.
  8. 16 |
  9. Make sure your code lints.
  10. 17 |
  11. Issue that pull request!
  12. 18 |
19 |
20 | 21 | Note:
22 | Any contributions you make will be under the MIT Software License and your submissions are understood to be under the same that covers the project. Please reach out to the team if you have any questions. 23 |
24 | 25 | Issues:
26 | We use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 27 |
28 | 29 | License:
30 | By contributing, you agree that your contributions will be licensed under Protract’s MIT License. 31 | 32 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | component: { 5 | devServer: { 6 | framework: "react", 7 | bundler: "vite", 8 | }, 9 | }, 10 | 11 | e2e: { 12 | setupNodeEvents(on, config) { 13 | // implement node event listeners here 14 | }, 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/component/BankEl.cy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BankEl from '../../src/client/components/BankEl'; 3 | 4 | describe('', () => { 5 | it('renders', () => { 6 | cy.mount(); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /cypress/component/Canvas.cy.tsx: -------------------------------------------------------------------------------- 1 | describe('Canvas.cy.tsx', () => { 2 | it('playground', () => { 3 | // cy.mount() 4 | }); 5 | }); 6 | 7 | export {}; 8 | -------------------------------------------------------------------------------- /cypress/e2e/canvas.cy.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | describe('canvas functionality', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/') 6 | }) 7 | 8 | it('can have items dragged in and deleted', () => { 9 | cy.contains('div').trigger('mousedown', {button: 0}).wait(200).trigger('mousemove', {clientX: 500, clientY: 300}).wait(200).trigger('mouseup') 10 | cy.get('[aria-label="elements"]').within(() => { 11 | cy.contains('div').within(() => { 12 | cy.contains('X').click() 13 | }) 14 | }) 15 | cy.contains('Yes').click() 16 | cy.get('[aria-label="elements"]').should('not.have.descendants', 'button') 17 | }) 18 | 19 | it('can add custom components', () => { 20 | cy.get('[placeholder="Component Name"]').click().type('Cypress is so amazing{enter}') 21 | cy.get('[aria-label="elements"]').should('have.descendants', 'button').contains('Cypress is so amazing') 22 | }) 23 | 24 | //TODO : make sorting order test work. some issue at line 28-30 25 | // it('can handle components changing order', () => { 26 | // cy.contains('div').trigger('mousedown', {button: 0}).trigger('mousemove', {clientX: 500, clientY: 300}).trigger('mouseup') 27 | // cy.wait(500) 28 | // cy.contains('form').trigger('mousedown', {button: 0}).trigger('mousemove', {clientX: 500, clientY: 300}).trigger('mouseup') 29 | // cy.wait(500) 30 | // cy.get('[aria-label="elements"]').within(() => { 31 | // cy.contains('form').trigger('mousedown', {button: 0}).trigger('mousemove', {clientX: 550, clientY: 300}).wait(1000).trigger('mouseup'); 32 | // }) 33 | // }) 34 | }) -------------------------------------------------------------------------------- /cypress/e2e/codepreview.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Code preview functionality', () => { 2 | 3 | beforeEach(() => { 4 | cy.visit('http://localhost:3000/') 5 | }) 6 | 7 | it('canvas item populates code preview', () => { 8 | cy.contains('img').trigger('mousedown', { button: 0 }).wait(200).trigger('mousemove', { clientX: 500, clientY: 300 }).wait(200).trigger('mouseup') 9 | cy.get('#codePreview').within(() => { 10 | cy.contains('').should('be.visible') 11 | }) 12 | }) 13 | it('deleting item from canvas should remove from code preview', () => { 14 | cy.contains('form').trigger('mousedown', { button: 0 }).wait(200).trigger('mousemove', { clientX: 500, clientY: 300 }).wait(200).trigger('mouseup') 15 | cy.get('#codePreview').within(() => { 16 | cy.contains('
').should('be.visible') 17 | }) 18 | cy.contains('X').click() 19 | cy.contains('Yes').click() 20 | cy.get('#codePreview').within(() => { 21 | cy.contains('
').should('not.exist') 22 | }) 23 | }) 24 | }) 25 | 26 | export {} -------------------------------------------------------------------------------- /cypress/e2e/fileDir.cy.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | describe('fileDir func', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/') 6 | }) 7 | 8 | it('populates based on custom components made and not on draggables', () => { 9 | cy.get('[placeholder="Component Name"]').click().type('Cypress is so amazing{enter}') 10 | cy.contains('ol').trigger('mousedown', {button: 0}).trigger('mousemove', {clientX: 500, clientY: 300}).trigger('mouseup') 11 | cy.get('#fileDir').should('contain', 'Cypress is so amazing'); 12 | cy.get('#fileDir').should('not.contain', 'ol'); 13 | }) 14 | 15 | it ('can enter the instance of a custom component', () => { 16 | cy.get('[placeholder="Component Name"]').click().type('I LOVE TESTING{enter}') 17 | cy.get('[placeholder="Component Name"]').click().type('I LOVE TESTING2{enter}') 18 | cy.get('[placeholder="Component Name"]').click().type('I LOVE TESTING3{enter}') 19 | cy.get('[placeholder="Component Name"]').click().type('I LOVE TESTING4{enter}') 20 | cy.get('#leftCol').within(() => { 21 | cy.contains('I LOVE TESTING4').click(); 22 | }) 23 | cy.get('h2').should('contain', 'I LOVE TESTING4'); 24 | }) 25 | }) -------------------------------------------------------------------------------- /cypress/e2e/flowtree.cy.ts: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | describe('flowtree functionality', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:3000/') 6 | }) 7 | 8 | it('contains only one node upon app start', () => { 9 | // click component tree tab 10 | cy.contains('Component Tree').click() 11 | // look inside component tree div / container, check for one single node called app 12 | cy.get('#flowTree').contains('app').should('be.visible') 13 | }) 14 | 15 | it('adds a custom component that populates in the tree, clicking on the new node populates it in canvas', () => { 16 | // click on component tree tab 17 | cy.contains('Component Tree').click() 18 | // select custom component input field 19 | // enter custom component, either click add or press enter 20 | cy.get('[placeholder="Component Name"]').click().type('Comp1{enter}') 21 | // check if new node populated 22 | cy.get('#flowTree').within(() => { 23 | cy.contains('Comp1').should('be.visible') 24 | // click on new node 25 | cy.contains('Comp1').click() 26 | }) 27 | // check if canvas updates to new node 28 | cy.get('#currCompTitle').contains('Comp1').should('be.visible') 29 | }) 30 | 31 | it('deletes appropriate tree node when corresponding node is deleted on canvas', () => { 32 | cy.contains('Component Tree').click(); 33 | cy.get('[placeholder="Component Name"]').click().type('Comp1{enter}') 34 | 35 | cy.get('#canvas').within(() => { 36 | cy.contains('Comp1').within(() => { 37 | cy.contains('X').click(); 38 | }) 39 | }) 40 | cy.get('#deleteModal').contains('Yes').click(); 41 | cy.get('#flowTree').should('not.contain', 'Comp1'); 42 | 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /cypress/e2e/login.cy.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | describe('login functionality', () => { 3 | it('allows a user to login and logout', () => { 4 | cy.visit('http://localhost:3000/') 5 | cy.contains('Login').should('be.visible').click(); 6 | cy.get('#loginModal').within(() => { 7 | cy.get('input[name="username"]').type('hulk'); 8 | cy.get('input[name="password"]').click().type('1234'); 9 | cy.get('button').first().click(); 10 | }) 11 | cy.contains('hulk').should('be.visible'); 12 | 13 | cy.contains('Projects').click(); 14 | cy.get('#loadModal').within(() => { 15 | cy.contains('Protract Blueprint').click(); 16 | cy.contains('Load').click(); 17 | }) 18 | cy.contains('div').trigger('mousedown', {button: 0}).wait(200).trigger('mousemove', {clientX: 400, clientY: 300}).wait(200).trigger('mouseup') 19 | cy.contains('ol').trigger('mousedown', {button: 0}).wait(200).trigger('mousemove', {clientX: 400, clientY: 300}).wait(200).trigger('mouseup') 20 | cy.contains('img').trigger('mousedown', {button: 0}).wait(200).trigger('mousemove', {clientX: 400, clientY: 300}).wait(200).trigger('mouseup') 21 | cy.get('[placeholder="Component Name"]').click().type('Cypress is so amazing{enter}') 22 | 23 | cy.get('[aria-label="elements"]').within(() => { 24 | cy.contains('amazing').within(() => { 25 | cy.contains('X').click() 26 | }) 27 | }) 28 | cy.contains('Yes').click() 29 | cy.get('[aria-label="elements"]').within(() => { 30 | cy.contains('div').within(() => { 31 | cy.contains('X').click() 32 | }) 33 | }) 34 | cy.contains('Yes').click() 35 | cy.get('[aria-label="elements"]').within(() => { 36 | cy.contains('img').within(() => { 37 | cy.contains('X').click() 38 | }) 39 | }) 40 | cy.contains('Yes').click() 41 | cy.get('[aria-label="elements"]').within(() => { 42 | cy.contains('ol').within(() => { 43 | cy.contains('X').click() 44 | }) 45 | }) 46 | cy.contains('Yes').click() 47 | 48 | cy.contains('Save').click(); 49 | cy.contains('New').click(); 50 | cy.contains('Yes').click(); 51 | cy.contains('Save').click(); 52 | cy.get('#saveModal').within(() => { 53 | cy.get('input').click().type('Super Cool Awesome Project also my WPM is so crazy fast holy moly'); 54 | cy.contains('Save').click(); 55 | }) 56 | cy.contains('Projects').click(); 57 | cy.get('#loadModal').within(() => { 58 | cy.contains('also').click(); 59 | cy.contains('Delete').click(); 60 | }) 61 | 62 | cy.contains('Logout').click(); 63 | 64 | cy.contains('Login').should('be.visible'); 65 | }) 66 | }) -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | 39 | export {} -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/react18' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | 38 | // Example use: 39 | // cy.mount() -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /docker-compose-test.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/docker-compose-test.yml -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Protract 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jest-setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-react-typescript-starter", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "mode=development nodemon src/server/main.ts -w src/server", 7 | "start": "ts-node src/server/main.ts", 8 | "build": "tsc --project tsconfig.json && vite build", 9 | "preview": "vite preview", 10 | "ts-coverage": "typescript-coverage-report", 11 | "test": "jest" 12 | }, 13 | "dependencies": { 14 | "@dnd-kit/core": "^6.0.8", 15 | "@dnd-kit/sortable": "^7.0.2", 16 | "@monaco-editor/react": "^4.5.1", 17 | "bcrypt": "^5.1.0", 18 | "bcryptjs": "^2.4.3", 19 | "cookie-parser": "^1.4.6", 20 | "d3": "^7.8.5", 21 | "dotenv": "^16.1.4", 22 | "express": "^4.18.2", 23 | "file-saver": "^2.0.5", 24 | "jszip": "^3.10.1", 25 | "monaco-editor": "^0.39.0", 26 | "mongodb": "^5.6.0", 27 | "mongoose": "^7.2.4", 28 | "react": "^18.2.0", 29 | "react-d3-tree": "^3.6.1", 30 | "react-dom": "^18.2.0", 31 | "ts-node": "^10.9.1", 32 | "typescript": "^4.9.3", 33 | "vite-express": "*" 34 | }, 35 | "devDependencies": { 36 | "@babel/preset-env": "^7.22.5", 37 | "@babel/preset-react": "^7.22.5", 38 | "@babel/preset-typescript": "^7.22.5", 39 | "@cypress/code-coverage": "^3.10.7", 40 | "@testing-library/jest-dom": "^5.16.5", 41 | "@testing-library/react": "^14.0.0", 42 | "@testing-library/user-event": "^14.4.3", 43 | "@types/d3": "^7.4.0", 44 | "@types/express": "^4.17.15", 45 | "@types/file-saver": "^2.0.5", 46 | "@types/node": "^18.11.18", 47 | "@types/react": "^18.0.26", 48 | "@types/react-dom": "^18.0.10", 49 | "@vitejs/plugin-react": "^3.0.1", 50 | "autoprefixer": "^10.4.14", 51 | "cypress": "^12.15.0", 52 | "esbuild": "^0.18.9", 53 | "identity-obj-proxy": "^3.0.0", 54 | "jest": "^29.5.0", 55 | "jest-environment-jsdom": "^29.5.0", 56 | "nodemon": "^2.0.20", 57 | "path": "^0.12.7", 58 | "postcss": "^8.4.24", 59 | "prettier": "^2.8.8", 60 | "prettier-plugin-tailwindcss": "^0.3.0", 61 | "tailwind-scrollbar": "^3.0.4", 62 | "tailwindcss": "^3.3.2", 63 | "typescript-coverage-report": "^0.7.0", 64 | "vite": "^4.3.9" 65 | }, 66 | "jest": { 67 | "testEnvironment": "jsdom", 68 | "setupFilesAfterEnv": [ 69 | "/jest-setup.ts" 70 | ], 71 | "collectCoverageFrom": [ 72 | "src/**/*.{ts,tsx,js.jsx}", 73 | "!src/**/*.d.ts" 74 | ], 75 | "moduleNameMapper": { 76 | "\\.(css|less)$": "identity-obj-proxy", 77 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/assetsTransformer.js" 78 | }, 79 | "transformIgnorePatterns": [ 80 | "/node_modules/(?!d3)/" 81 | ] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/protract-icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/public/protract-icon-white.png -------------------------------------------------------------------------------- /public/protract-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/public/protract-icon.png -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/App.css -------------------------------------------------------------------------------- /src/client/App.tsx: -------------------------------------------------------------------------------- 1 | import MainContainer from './components/Containers/MainContainer'; 2 | import React from 'react'; 3 | 4 | export default function App() { 5 | return ( 6 | <> 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/client/assets/gifs/Create-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/gifs/Create-demo.gif -------------------------------------------------------------------------------- /src/client/assets/gifs/Export-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/gifs/Export-demo.gif -------------------------------------------------------------------------------- /src/client/assets/gifs/Projects-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/gifs/Projects-demo.gif -------------------------------------------------------------------------------- /src/client/assets/gifs/Protract-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/gifs/Protract-demo.gif -------------------------------------------------------------------------------- /src/client/assets/gifs/Tree-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/gifs/Tree-demo.gif -------------------------------------------------------------------------------- /src/client/assets/github-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/github-dark.png -------------------------------------------------------------------------------- /src/client/assets/github-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/github-light.png -------------------------------------------------------------------------------- /src/client/assets/logo-bg-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/logo-bg-white.png -------------------------------------------------------------------------------- /src/client/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/logo.png -------------------------------------------------------------------------------- /src/client/assets/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/logo2.png -------------------------------------------------------------------------------- /src/client/assets/logo3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/logo3.png -------------------------------------------------------------------------------- /src/client/assets/protract-favicon-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/protract-favicon-color.png -------------------------------------------------------------------------------- /src/client/assets/protract-readme-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Protract/a4fe7dbfbe5d13bd237d62186c7d68a164c5cf1a/src/client/assets/protract-readme-logo.png -------------------------------------------------------------------------------- /src/client/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/BankEl.tsx: -------------------------------------------------------------------------------- 1 | import { UniqueIdentifier, useDraggable } from '@dnd-kit/core'; 2 | import React from 'react'; 3 | 4 | export default function BankEl(props: { 5 | key: UniqueIdentifier; 6 | id: UniqueIdentifier; 7 | }) { 8 | const { id } = props; 9 | 10 | const { attributes, listeners, setNodeRef } = useDraggable({ id }); 11 | 12 | return ( 13 |
  • 19 | {id} 20 |
  • 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/client/components/Canvas.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SortableContext, 3 | arrayMove, 4 | verticalListSortingStrategy, 5 | } from '@dnd-kit/sortable'; 6 | import SortableBankEl from './SortableBankEl'; 7 | import { DndContext, DragEndEvent } from '@dnd-kit/core'; 8 | import React, { useEffect, useState, useContext } from 'react'; 9 | import { useDroppable } from '@dnd-kit/core'; 10 | import { Item, Project } from '../../types'; 11 | import { PlaygroundContext } from './Playground'; 12 | import WarningModal from './Modals/WarningModal'; 13 | import SaveModal from './Modals/SaveModal'; 14 | import LoadModal from './Modals/LoadModal'; 15 | import zipFiles from '../helperFunctions/zipFiles'; 16 | 17 | export default function Canvas(props: { 18 | user: string; 19 | currComp: Item; 20 | handleCanvasUpdate: (arr: Item[]) => void; 21 | setChildren: React.Dispatch>; 22 | }) { 23 | const { user, currComp, setChildren, handleCanvasUpdate } = props; 24 | const { setComps, comps, setCurrComp } = useContext(PlaygroundContext); 25 | 26 | // used for dndkit, notifies this component as a droppable area 27 | const { setNodeRef } = useDroppable({ 28 | id: 'canvas', 29 | }); 30 | 31 | // list is the display of the canvas, originally set to currComp.children 32 | const [list, setList] = useState(currComp.children); 33 | 34 | // different modals show up depending on which buttons were clicked 35 | const [modal, setModal] = useState(''); 36 | 37 | // project title set if user decides to save a project 38 | const [project, setProject] = useState(''); 39 | 40 | // array of projects available to user when they choose to load projects on an account 41 | const [projects, setProjects] = useState([]); 42 | 43 | // updates order of children for the current component in the object that contains all components 44 | function handleAppReorder( 45 | comps: Item[], 46 | currComp: Item, 47 | list: Item[] 48 | ): Item[] { 49 | return comps.map((comp) => { 50 | if (comp.id === currComp.id && comp.children) { 51 | comp.children = list; 52 | } else { 53 | comp.children = handleAppReorder(comp.children, currComp, list); 54 | } 55 | return comp; 56 | }); 57 | } 58 | 59 | // switches list when new currComp selected 60 | useEffect(() => { 61 | setList(currComp.children); 62 | }, [currComp]); 63 | 64 | // whenever list updates, tell parent what the order is now, and also call handleAppReorder 65 | useEffect(() => { 66 | handleCanvasUpdate(list); 67 | const updateApp = handleAppReorder(comps, currComp, list); 68 | setComps(updateApp); 69 | }, [list]); 70 | 71 | // function to reorder list items in the canvas 72 | function handleDragEnd(e: DragEndEvent) { 73 | const { active, over } = e; 74 | if (over === null) { 75 | return; 76 | } 77 | if (active.id !== over.id) { 78 | setList((list) => { 79 | const oldIndex = list.findIndex((item) => active.id === item.id); 80 | const newIndex = list.findIndex((item) => over.id === item.id); 81 | const updated = arrayMove(list, oldIndex, newIndex); 82 | // whenever order changes in an instance, update the children array 83 | // useeffect hooks for updating currComp and comps will run everytime children arr is updated as well 84 | setChildren(updated); 85 | return updated; 86 | }); 87 | } 88 | } 89 | 90 | function handleCancel() { 91 | setModal(''); 92 | } 93 | 94 | // returns state of app back to default when user first visits site 95 | function handleReset() { 96 | setComps([ 97 | { 98 | value: 'app', 99 | id: 'app', 100 | codeStart: '', 101 | codeEnd: '', 102 | canEnter: true, 103 | children: [], 104 | }, 105 | ]); 106 | setCurrComp({ 107 | value: 'app', 108 | id: 'app', 109 | codeStart: '', 110 | codeEnd: '', 111 | canEnter: true, 112 | children: [], 113 | }); 114 | setChildren([]); 115 | setModal(''); 116 | setProject(''); 117 | } 118 | 119 | async function saveProj(project: string) { 120 | try { 121 | await fetch('/proj', { 122 | method: 'PATCH', 123 | headers: { 124 | 'Content-Type': 'application/json', 125 | }, 126 | body: JSON.stringify({ 127 | username: user, 128 | project, 129 | root: comps, 130 | }), 131 | }); 132 | } catch (err) { 133 | console.log(err); 134 | } 135 | } 136 | 137 | // asks for a project name if the project doesnt have one yet, otherwise patches the project 138 | function checkIfNewProj() { 139 | if (project === '') { 140 | showModal('save'); 141 | } else { 142 | saveProj(project); 143 | } 144 | } 145 | 146 | function showModal(string: string) { 147 | setModal(string); 148 | } 149 | 150 | // loads the projects associated with the user 151 | async function handleLoad() { 152 | try { 153 | const response = await fetch(`/proj/${user}`, { 154 | method: 'GET', 155 | headers: { 156 | 'Content-Type': 'application/json', 157 | }, 158 | }); 159 | if (response) { 160 | const data = await response.json(); 161 | setProjects(data); 162 | } else { 163 | throw new Error('Request failed'); 164 | } 165 | } catch (error) { 166 | console.error('Failed to load projects'); 167 | } 168 | setModal('load'); 169 | } 170 | 171 | function handleExport() { 172 | zipFiles(comps[0]); 173 | } 174 | 175 | return ( 176 |
    177 |
    178 |

    {project}

    179 |
    180 | 181 | 182 | 183 | 184 |
    185 |
    186 |
    190 |

    194 | {currComp.value} 195 |

    196 | 197 | item.id)} 199 | strategy={verticalListSortingStrategy} 200 | > 201 |
      206 | {list.map((item, index) => ( 207 | 212 | ))} 213 |
    214 |
    215 |
    216 |
    217 | {modal === 'reset' && ( 218 | 219 | )} 220 | {modal === 'save' && ( 221 | 226 | )} 227 | {modal === 'load' && ( 228 | 238 | )} 239 |
    240 | ); 241 | } 242 | -------------------------------------------------------------------------------- /src/client/components/CodePreview.tsx: -------------------------------------------------------------------------------- 1 | import Editor from '@monaco-editor/react'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { Item } from './../../types'; 4 | 5 | export default function CodePreview(props: { tags: Item[]; currComp: Item }) { 6 | const { tags, currComp } = props; 7 | 8 | const [preview, setPreview] = useState(''); 9 | 10 | //Convert the user input component into the Angular mandated syntax 11 | let compName; 12 | if (typeof currComp.value === 'string') { 13 | compName = currComp.value.toLowerCase().replace(' ', '-'); 14 | } else { 15 | compName = currComp.value; 16 | } 17 | 18 | //Boilerplate Angular HTML code added to code preview. Prefix are the items that occur before the HTML tags and components. 19 | const prefix = [ 20 | "import { Component } from '@angular/core';\n", 21 | "import { CommonModule } from '@angular/common';\n", 22 | '@Component({\n', 23 | ` selector: \'${compName}\',\n`, 24 | ' template: `\n', 25 | ]; 26 | 27 | //Boilerplate Angular HTML code added to code preview. Suffix are the items that occur after the HTML tags and components 28 | const suffix = [ 29 | ` \`\n`, 30 | ` styleUrls: [\'${compName}.component.css\']\n`, 31 | '})\n', 32 | `export class ${currComp.value}Component {};`, 33 | ]; 34 | 35 | //Updates the code preview whenever the Canvas items are updated including additions and reordering. 36 | useEffect(() => { 37 | const canvasCodeArr = tags.map((ele) => ` ${ele.code}`); 38 | const finArr = prefix.concat(canvasCodeArr).concat(suffix); 39 | setPreview(finArr.join('')); 40 | }, [tags]); 41 | 42 | return ( 43 |
    44 | 67 |
    68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/client/components/ComponentBank.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import BankEl from './BankEl'; 3 | import CustomComponentCreator from './CustomComponentCreator'; 4 | 5 | export default function ComponentBank() { 6 | const els: string[] = [ 7 | 'div', 8 | 'a', 9 | 'img', 10 | 'ul', 11 | 'ol', 12 | 'form', 13 | 'input', 14 | 'button', 15 | 'li', 16 | 'span', 17 | 'h1', 18 | 'h2', 19 | 'h3', 20 | ]; 21 | 22 | const elList: React.ReactElement[] = []; 23 | 24 | els.forEach((el, index) => { 25 | elList.push(); 26 | }); 27 | 28 | return ( 29 |
    30 | 31 |
      32 | {elList} 33 |
    34 |
    35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/client/components/Containers/LeftColumn.tsx: -------------------------------------------------------------------------------- 1 | import ComponentBank from '../ComponentBank'; 2 | import FileDirectory from '../FileDirectory'; 3 | import { PlaygroundContext } from '../Playground'; 4 | import React, { useContext } from 'react'; 5 | 6 | export default function LeftColumn() { 7 | const { comps } = useContext(PlaygroundContext); 8 | 9 | return ( 10 |
    14 | 15 |
    16 |

    File Directory

    17 |
    18 | 19 |
    20 |
    21 |
    22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/client/components/Containers/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import Playground from '../Playground'; 2 | import React from 'react'; 3 | 4 | export default function MainContainer() { 5 | return ( 6 |
    7 | 8 |
    9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/client/components/Containers/Preview.tsx: -------------------------------------------------------------------------------- 1 | import CodePreview from '../CodePreview'; 2 | import FlowTree from '../FlowTree'; 3 | import { Item } from '../../../types'; 4 | import React, { useEffect, useState, useContext } from 'react'; 5 | import { PlaygroundContext } from '../Playground'; 6 | 7 | export default function Preview(props: { tags: Item[]; currComp: Item }) { 8 | const { comps } = useContext(PlaygroundContext); 9 | 10 | const { tags, currComp } = props; 11 | 12 | const [display, setDisplay] = useState( 13 | 14 | ); 15 | const [tab, setTab] = useState('code'); 16 | 17 | const handlePreviewClick = () => { 18 | if (tab !== 'code') { 19 | setTab('code'); 20 | setDisplay(); 21 | } 22 | }; 23 | 24 | const handleTreeClick = () => { 25 | if (tab !== 'tree') { 26 | setTab('tree'); 27 | setDisplay(); 28 | } 29 | }; 30 | 31 | useEffect(() => { 32 | if (tab === 'code') { 33 | setDisplay(); 34 | } else if (tab === 'tree') { 35 | setDisplay(); 36 | } 37 | }, [tags]); 38 | 39 | return ( 40 |
    41 |
    42 | 48 | 54 |
    55 | {display} 56 |
    57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/client/components/CustomComponentCreator.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext } from 'react'; 2 | import { PlaygroundContext } from './Playground'; 3 | 4 | export default function CustomComponentCreator() { 5 | const [input, setInput] = useState(''); 6 | 7 | const { setComps, comps, children, currComp, setChildren, handleUpdateApp } = 8 | useContext(PlaygroundContext); 9 | 10 | function handleChange(e: string) { 11 | setInput(e); 12 | } 13 | 14 | // create a newComp based off input. update children of currComp and handleUpdateApp to update the overall stateful object to include the newComp 15 | function handleSubmit(e: React.FormEvent) { 16 | e.preventDefault(); 17 | if (input.trim().length) { 18 | const angularVer = input.toLowerCase().replace(' ', '-'); 19 | const newComp = { 20 | value: input, 21 | id: `${input}-${children.length}`, 22 | code: `<${angularVer}>\n`, 23 | canEnter: true, 24 | children: [], 25 | }; 26 | 27 | setChildren((prev) => [...prev, newComp]); 28 | const updatedComp = handleUpdateApp(comps, currComp, newComp); 29 | setComps(updatedComp); 30 | setInput(''); 31 | } 32 | } 33 | 34 | return ( 35 |
    36 |
    handleSubmit(e)} 39 | > 40 | handleChange(e.target.value)} 46 | maxLength={30} 47 | style={{ textAlign: 'left' }} 48 | /> 49 | 55 |
    56 |
    57 | ); 58 | } 59 | -------------------------------------------------------------------------------- /src/client/components/FileDirectory.tsx: -------------------------------------------------------------------------------- 1 | import { Item } from '../../types'; 2 | import { PlaygroundContext } from './Playground'; 3 | import React, { useContext } from 'react'; 4 | 5 | export default function FileDirectory(props: { comps: Item; depth: number }) { 6 | const { setCurrComp, setChildren } = useContext(PlaygroundContext); 7 | const { comps, depth } = props; 8 | 9 | function handleClick(comp: Item) { 10 | setCurrComp(comp); 11 | setChildren(comp.children); 12 | } 13 | 14 | const renderTree = (currentComponent: Item) => { 15 | const { value, children } = currentComponent; 16 | 17 | return ( 18 |
    0 ? 'ml-4 pl-1' : ''} mx-4`}> 19 | handleClick(currentComponent)} 22 | > 23 | {value} 24 | 25 | {children && children.length > 0 && ( 26 |
    27 | {children.map( 28 | (child, index) => 29 | child.canEnter && ( 30 |
    31 |
    32 | 33 |
    34 |
    35 | ) 36 | )} 37 |
    38 | )} 39 |
    40 | ); 41 | }; 42 | 43 | return ( 44 |
    45 | {renderTree(comps)} 46 |
    47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/client/components/FlowTree.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PlaygroundContext } from './Playground'; 3 | import { useContext } from 'react'; 4 | import { RawNodeDatum, TreeNodeDatum, Tree } from 'react-d3-tree'; 5 | import { Item } from '../../types'; 6 | import { convertDataToElements } from '../helperFunctions/dataToD3Elems'; 7 | 8 | interface TreeProps { 9 | root: Item; 10 | } 11 | 12 | const FlowTree: React.FC = ({ root }) => { 13 | 14 | const { setCurrComp, setChildren, comps, currComp } = useContext(PlaygroundContext); 15 | 16 | // nodeData is basically the Item type, except with additional d3-tree specific properties (name and __rd3t) 17 | function handleNodeClick(nodeData: any) { 18 | // destructure from nodeData 19 | const { value } = nodeData; 20 | 21 | function updateCanvas(children: Item[], value: string) { 22 | children.map((comp) => { 23 | if (comp.value === value) { 24 | setCurrComp(comp); 25 | setChildren(comp.children) 26 | return; 27 | } else { 28 | updateCanvas(comp.children, value); 29 | } 30 | } 31 | )}; 32 | 33 | updateCanvas(comps, value); 34 | } 35 | 36 | // convert root object to a d3 compliant data structure 37 | const elements = convertDataToElements(root); 38 | 39 | //Prevents the node names from overlapping and being too long 40 | const abbrev = (nodeName: string): string => { 41 | if(nodeName && nodeName.length > 10){ return nodeName.slice(0,7) + '...'} 42 | if(nodeName === 'App'){ return nodeName.toLowerCase()} 43 | return nodeName; 44 | } 45 | 46 | //Changes the SVG associated with the node and where the text shows in relation to node 47 | const renderSvgNode = ({ nodeDatum }: { nodeDatum: TreeNodeDatum }) => ( 48 | 49 | handleNodeClick(nodeDatum) } /> 50 | handleNodeClick(nodeDatum)}> 51 | {abbrev(nodeDatum.name)} 52 | 53 | 54 | ); 55 | 56 | 57 | return ( 58 |
    59 | 74 |
    75 | ); 76 | }; 77 | 78 | export default FlowTree; 79 | -------------------------------------------------------------------------------- /src/client/components/Modals/DeleteModal.tsx: -------------------------------------------------------------------------------- 1 | import { UniqueIdentifier } from '@dnd-kit/core'; 2 | import React, { useRef } from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | 5 | export default function DeleteModal(props: { 6 | value: UniqueIdentifier; 7 | handleDelete: () => void; 8 | handleCancel: () => void; 9 | }) { 10 | const { value, handleDelete, handleCancel } = props; 11 | 12 | const modalRef = useRef(null); 13 | const closeModal = (e: React.MouseEvent) => { 14 | if (e.target === modalRef.current) { 15 | handleCancel(); 16 | } 17 | }; 18 | 19 | return ReactDOM.createPortal( 20 |
    closeModal(e)} 24 | > 25 |
    29 | Delete {value}? 30 |
    31 | 37 | 43 |
    44 |
    45 |
    , 46 | document.getElementById('portal') as Element 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/client/components/Modals/LoadModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { useState } from 'react'; 4 | import { PlaygroundContext } from '../Playground'; 5 | import { Project } from '../../../types'; 6 | 7 | export default function LoadModal(props: { 8 | user: string; 9 | project: string; 10 | projects: Project[]; 11 | handleReset: () => void; 12 | handleCancel: () => void; 13 | showModal: (string: string) => void; 14 | setProject: React.Dispatch>; 15 | setProjects: React.Dispatch>; 16 | }) { 17 | const [active, setActive] = useState(''); 18 | const { 19 | user, 20 | project, 21 | projects, 22 | handleCancel, 23 | handleReset, 24 | showModal, 25 | setProject, 26 | setProjects, 27 | } = props; 28 | const { setComps, setChildren, setCurrComp } = useContext(PlaygroundContext); 29 | 30 | const modalRef = useRef(null); 31 | const closeModal = (e: React.MouseEvent) => { 32 | if (e.target === modalRef.current) { 33 | handleCancel(); 34 | } 35 | }; 36 | 37 | function handleActive(string: string) { 38 | setActive(string); 39 | } 40 | 41 | function handleLoad() { 42 | if (active) { 43 | const project = projects.filter((project) => project.title === active)[0]; 44 | setComps(project.root); 45 | setCurrComp(project.root[0]); 46 | setChildren(project.root[0].children); 47 | showModal(''); 48 | setProject(project.title); 49 | } 50 | } 51 | 52 | async function handleDelete() { 53 | if (active) { 54 | try { 55 | await fetch('/proj', { 56 | method: 'DELETE', 57 | headers: { 58 | 'Content-Type': 'application/json', 59 | }, 60 | body: JSON.stringify({ 61 | username: user, 62 | title: active, 63 | }), 64 | }); 65 | setProjects((prev) => 66 | prev.filter((project) => project.title !== active) 67 | ); 68 | if (project === active) { 69 | handleReset(); 70 | } 71 | } catch (err) { 72 | console.log('Failed to delete'); 73 | } 74 | } 75 | } 76 | 77 | return ReactDOM.createPortal( 78 |
    closeModal(e)} 82 | > 83 |
    87 |
    88 | Projects 89 |
    90 |
      91 | {projects.map((project, i) => ( 92 |
    • handleActive(project.title)} 95 | className={ 96 | active === project.title ? 'rounded-md bg-gray-200' : '' 97 | } 98 | > 99 | {project.title} 100 |
    • 101 | ))} 102 |
    103 |
    104 |
    105 |
    106 | 112 | 118 | 124 |
    125 |
    126 |
    , 127 | document.getElementById('portal') as Element 128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /src/client/components/Modals/LoginModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import React from 'react'; 3 | 4 | export default function LoginModal(props: { loginChange: () => void }) { 5 | const [user, setUser] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | 8 | const handleUserChange = (e: React.FormEvent) => { 9 | setUser(e.currentTarget.value); 10 | }; 11 | const handlePasswordChange = (e: React.FormEvent) => { 12 | setPassword(e.currentTarget.value); 13 | }; 14 | 15 | 16 | const handleLogin = async ( 17 | e: React.MouseEvent 18 | ) => { 19 | e.preventDefault(); 20 | try { 21 | const response = await fetch('/login', { 22 | method: 'POST', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | body: JSON.stringify({ 27 | username: user, 28 | password: password, 29 | }), 30 | }); 31 | if (response.ok) { 32 | const data = await response.json(); 33 | setUser(''); 34 | setPassword(''); 35 | props.loginChange(); 36 | (document.querySelector('#loginModal') as HTMLDialogElement).close(); 37 | } else { 38 | const data = await response.json(); 39 | throw new Error(`${data}`); 40 | } 41 | } catch (err) { 42 | console.error('Failed to login'); 43 | } 44 | }; 45 | 46 | function handleCancel() { 47 | setUser(''); 48 | setPassword(''); 49 | (document.querySelector('#loginModal') as HTMLDialogElement).close(); 50 | } 51 | 52 | return ( 53 | 54 |

    Protract Login

    55 |
    61 |
    62 | Username 63 | handleUserChange(e)} 69 | value={user} 70 | > 71 |
    72 |
    73 | Password 74 | handlePasswordChange(e)} 79 | value={password} 80 | > 81 |
    82 |
    83 | 90 | 97 |
    98 |
    99 |
    100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/client/components/Modals/SaveModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useRef, useState } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { PlaygroundContext } from '../Playground'; 4 | 5 | export default function SaveModal(props: { 6 | setProject: React.Dispatch>; 7 | showModal: (string: string) => void; 8 | handleCancel: () => void; 9 | }) { 10 | const { setProject, showModal, handleCancel } = props; 11 | 12 | const { user, comps } = useContext(PlaygroundContext); 13 | 14 | const [input, setInput] = useState(''); 15 | 16 | const modalRef = useRef(null); 17 | const closeModal = (e: React.MouseEvent) => { 18 | if (e.target === modalRef.current) { 19 | handleCancel(); 20 | } 21 | }; 22 | 23 | function handleChange(e: string) { 24 | setInput(e); 25 | } 26 | 27 | async function handleSave(e: React.FormEvent) { 28 | e.preventDefault(); 29 | if (input.trim().length) { 30 | try { 31 | const data = await fetch('/proj', { 32 | method: 'POST', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | }, 36 | body: JSON.stringify({ 37 | title: input, 38 | root: comps, 39 | users: user, 40 | }), 41 | }); 42 | } catch (err) { 43 | console.log('Failed to save'); 44 | } 45 | setProject(input); 46 | showModal(''); 47 | } 48 | } 49 | 50 | return ReactDOM.createPortal( 51 |
    closeModal(e)} 55 | > 56 |
    60 | Save this project as? 61 |
    62 |
    handleSave(e)}> 63 | handleChange(e.target.value)} 66 | value={input} 67 | > 68 |
    69 | 75 | 81 |
    82 |
    83 |
    84 |
    85 |
    , 86 | document.getElementById('portal') as Element 87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/client/components/Modals/SignUpModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import React from 'react'; 3 | 4 | export default function SignUpModal(props: { loginChange: () => void }) { 5 | const [user, setUser] = useState(''); 6 | const [password, setPassword] = useState(''); 7 | const [email, setEmail] = useState(''); 8 | 9 | function handleUserChange(e: React.FormEvent) { 10 | setUser(e.currentTarget.value); 11 | } 12 | 13 | function handleEmailChange(e: React.FormEvent) { 14 | setEmail(e.currentTarget.value); 15 | } 16 | 17 | function handlePasswordChange(e: React.FormEvent) { 18 | setPassword(e.currentTarget.value); 19 | } 20 | 21 | 22 | async function handleSignUp( 23 | e: React.MouseEvent 24 | ) { 25 | e.preventDefault(); 26 | try { 27 | const response = await fetch('/signup', { 28 | method: 'POST', 29 | headers: { 30 | 'Content-Type': 'application/json', 31 | }, 32 | body: JSON.stringify({ 33 | username: user, 34 | password: password, 35 | email: email, 36 | }), 37 | }); 38 | if (response.ok) { 39 | const data = await response.json(); 40 | setPassword(''); 41 | setEmail(''); 42 | setUser(''); 43 | props.loginChange(); 44 | (document.querySelector('#signUpModal') as HTMLDialogElement).close(); 45 | } else { 46 | const data = await response.json(); 47 | throw new Error(data); 48 | } 49 | } catch (err) { 50 | console.log('Failed to sign up'); 51 | } 52 | } 53 | 54 | function handleCancel() { 55 | setPassword(''); 56 | setEmail(''); 57 | setUser(''); 58 | (document.querySelector('#signUpModal') as HTMLDialogElement).close(); 59 | } 60 | 61 | return ( 62 | 63 |

    Protract Sign Up

    64 |
    69 |
    70 | Username 71 | { 77 | handleUserChange(e); 78 | }} 79 | value={user} 80 | > 81 |
    82 |
    83 | Email 84 | { 90 | handleEmailChange(e); 91 | }} 92 | value={email} 93 | > 94 |
    95 |
    96 | Password 97 | { 103 | handlePasswordChange(e); 104 | }} 105 | value={password} 106 | > 107 |
    108 |
    109 | 118 | 125 |
    126 |
    127 |
    128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /src/client/components/Modals/WarningModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | export default function WarningModal(props: { 5 | handleCancel: () => void; 6 | handleReset: () => void; 7 | }) { 8 | const { handleCancel, handleReset } = props; 9 | const modalRef = useRef(null); 10 | const closeModal = (e: React.MouseEvent) => { 11 | if (e.target === modalRef.current) { 12 | handleCancel(); 13 | } 14 | }; 15 | 16 | return ReactDOM.createPortal( 17 |
    closeModal(e)} 21 | > 22 |
    26 | Start a new project? 27 |
    28 | 34 | 40 |
    41 |
    42 |
    , 43 | document.getElementById('portal') as Element 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/client/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import logo from '../assets/logo3.png'; 3 | import LoginModal from './Modals/LoginModal'; 4 | import SignUpModal from './Modals/SignUpModal'; 5 | import React from 'react'; 6 | 7 | export default function Navbar(props: { 8 | user: string; 9 | setUser: React.Dispatch>; 10 | }) { 11 | const { user, setUser } = props; 12 | 13 | const [loginState, setLoginState] = useState(false); 14 | const [loginDisplay, setLoginDisplay] = useState('Login'); 15 | const [signUpDisplay, setSignUpDisplay] = useState(true); 16 | 17 | function loginChange() { 18 | //Case for if user is logged in but signs up for another account or logs directly into another account 19 | //setLoginState((prevloginState) => !prevloginState); 20 | const isLoggedIn = async () => { 21 | try { 22 | const response = await fetch('/loggedIn'); 23 | const data = await response.json(); 24 | setUser(data); 25 | } catch (err) { 26 | console.log('login error'); 27 | } 28 | }; 29 | isLoggedIn(); 30 | setLoginDisplay('Logout'); 31 | setSignUpDisplay(false); 32 | setLoginState(true); 33 | } 34 | 35 | useEffect(() => { 36 | const isLoggedIn = async () => { 37 | try { 38 | const response = await fetch('/loggedIn'); 39 | const data = await response.json(); 40 | //no user session being found which gives a long character string 41 | if (data.length < 20) { 42 | setUser(data); 43 | setLoginDisplay('Logout'); 44 | setSignUpDisplay(false); 45 | setLoginState(true); 46 | } 47 | } catch (err) { 48 | console.log('login error'); 49 | } 50 | }; 51 | isLoggedIn(); 52 | }, []); 53 | 54 | function logModal() { 55 | if (loginState === false) { 56 | (document.querySelector('#loginModal') as HTMLDialogElement).showModal(); 57 | } else { 58 | const logoutFunc = async () => { 59 | try { 60 | const response = await fetch('/logout', { 61 | method: 'PATCH', 62 | headers: { 63 | 'Content-Type': 'application/json', 64 | }, 65 | }); 66 | 67 | if (response) { 68 | const data = await response.json(); 69 | } else { 70 | throw new Error('Logout user has failed'); 71 | } 72 | } catch (error) { 73 | console.error('Error logging out'); 74 | } 75 | }; 76 | logoutFunc(); 77 | setLoginDisplay('Login'); 78 | setSignUpDisplay(true); 79 | setLoginState(false); 80 | } 81 | } 82 | 83 | return ( 84 | 131 | ); 132 | } 133 | -------------------------------------------------------------------------------- /src/client/components/Playground.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useEffect, useState, useMemo } from 'react'; 2 | import LeftColumn from './Containers/LeftColumn'; 3 | import Canvas from './Canvas'; 4 | import Preview from './Containers/Preview'; 5 | import { 6 | DndContext, 7 | DragEndEvent, 8 | DragOverlay, 9 | DragStartEvent, 10 | UniqueIdentifier, 11 | KeyboardSensor, 12 | MouseSensor, 13 | TouchSensor, 14 | useSensor, 15 | useSensors, 16 | PointerSensor, 17 | } from '@dnd-kit/core'; 18 | import BankEl from './BankEl'; 19 | import { Item } from '../../types'; 20 | import Navbar from './Navbar'; 21 | 22 | export const PlaygroundContext = createContext<{ 23 | comps: Item[]; 24 | currComp: Item; 25 | children: Item[]; 26 | setComps: React.Dispatch>; 27 | setCurrComp: React.Dispatch>; 28 | setChildren: React.Dispatch>; 29 | handleUpdateApp(comps: Item[], currComp: Item, newComp: Item): Item[]; 30 | user: string; 31 | }>({ 32 | comps: [], 33 | currComp: { 34 | value: 'app', 35 | id: 'app', 36 | codeStart: '', 37 | codeEnd: '', 38 | canEnter: true, 39 | children: [], 40 | }, 41 | children: [], 42 | setCurrComp: () => {}, 43 | setComps: () => {}, 44 | setChildren: () => {}, 45 | handleUpdateApp: () => [], 46 | user: '', 47 | }); 48 | 49 | export default function Playground() { 50 | //user session 51 | const [user, setUser] = useState(''); 52 | // used for drag overlay 53 | const [activeId, setActiveId] = useState(''); 54 | 55 | // updated order received from canvas when items are moved 56 | const [currOrder, setCurrOrder] = useState([]); 57 | 58 | const [children, setChildren] = useState([]); 59 | 60 | const app: Item = { 61 | value: 'app', 62 | id: 'app', 63 | codeStart: '', 64 | codeEnd: '', 65 | canEnter: true, 66 | children, 67 | }; 68 | 69 | // whenever children changes, update the state of the currComp to match the changes 70 | useEffect(() => { 71 | setCurrComp((prevComp) => ({ 72 | ...prevComp, 73 | children, 74 | })); 75 | }, [children]); 76 | 77 | // changes what component we are currently looking at 78 | const [currComp, setCurrComp] = useState(app); 79 | 80 | // comps is an array that holds app, the root object of a project 81 | const [comps, setComps] = useState([app]); 82 | 83 | // update the root app component anytime a change is made, and changes are desired to persist 84 | function handleUpdateApp( 85 | comps: Item[], 86 | currComp: Item, 87 | newComp: Item 88 | ): Item[] { 89 | return comps.map((comp) => { 90 | if (comp.id === currComp.id && comp.children) { 91 | comp.children.push(newComp); 92 | } else { 93 | comp.children = handleUpdateApp(comp.children, currComp, newComp); 94 | } 95 | return comp; 96 | }); 97 | } 98 | 99 | // function to update order of items in instance 100 | function handleCanvasUpdate(arr: Item[]) { 101 | setCurrOrder(arr); 102 | } 103 | 104 | // used by playgroundcontext provider 105 | const contextValue = { 106 | user, 107 | comps, 108 | currComp, 109 | children, 110 | setComps, 111 | setCurrComp, 112 | setChildren, 113 | handleUpdateApp, 114 | }; 115 | 116 | // drag and drop logic 117 | function handleDragStart(e: DragStartEvent) { 118 | setActiveId(e.active.id); 119 | } 120 | 121 | function handleDragEnd(e: DragEndEvent) { 122 | if (e.over === null) return; 123 | if (e.over.id === 'canvas') { 124 | const newItem = { 125 | value: e.active.id, 126 | id: `${e.active.id}-${children.length}-in-${currComp.value}`, 127 | code: `<${e.active.id}>\n`, 128 | children: [], 129 | }; 130 | setChildren((prev) => [...prev, newItem]); 131 | const updatedComp = handleUpdateApp(comps, currComp, newItem); 132 | setComps(updatedComp); 133 | } 134 | setActiveId(''); 135 | } 136 | 137 | const sensors = useSensors( 138 | useSensor(PointerSensor, { 139 | activationConstraint: { 140 | distance: 20, 141 | tolerance: 100, 142 | }, 143 | }), 144 | useSensor(MouseSensor), 145 | useSensor(TouchSensor), 146 | useSensor(KeyboardSensor) 147 | ); 148 | 149 | return ( 150 | <> 151 | 152 |
    153 | 154 | 159 | 160 | 161 | {activeId ? : null} 162 | 163 | 169 | 170 | 171 | 172 |
    173 | 174 | ); 175 | } 176 | -------------------------------------------------------------------------------- /src/client/components/SortableBankEl.tsx: -------------------------------------------------------------------------------- 1 | import { UniqueIdentifier } from '@dnd-kit/core'; 2 | import { useSortable } from '@dnd-kit/sortable'; 3 | import { CSS } from '@dnd-kit/utilities'; 4 | import { useState } from 'react'; 5 | import DeleteModal from './Modals/DeleteModal'; 6 | import React, { useContext } from 'react'; 7 | import { PlaygroundContext } from './Playground'; 8 | 9 | export default function SortableBankEl(props: { 10 | id: UniqueIdentifier; 11 | value: UniqueIdentifier; 12 | }) { 13 | const [deleteModal, setDeleteModal] = useState(false); 14 | 15 | const { id, value } = props; 16 | const { setComps, setChildren } = useContext(PlaygroundContext); 17 | 18 | const { attributes, listeners, setNodeRef, transform } = useSortable({ id }); 19 | 20 | const style = { 21 | transform: CSS.Transform.toString(transform), 22 | }; 23 | 24 | function handleClick() { 25 | setDeleteModal(true); 26 | } 27 | 28 | function handleDelete() { 29 | setDeleteModal(false); 30 | // remove the comp if it is a comp 31 | // TODO, update comps so that if the deleted comp had comps in its children, those comps are deleted as well 32 | setComps((prev) => prev.filter((comp) => comp.id !== id)); 33 | // remove the el from the children arr 34 | setChildren((prev) => prev.filter((item) => item.id !== id)); 35 | } 36 | 37 | function handleCancel() { 38 | setDeleteModal(false); 39 | return; 40 | } 41 | 42 | return ( 43 | <> 44 |
  • 51 | {value} 52 | 58 |
  • 59 | {deleteModal && ( 60 | 65 | )} 66 | 67 | ); 68 | } 69 | -------------------------------------------------------------------------------- /src/client/helperFunctions/capitalizeFirstLetter.ts: -------------------------------------------------------------------------------- 1 | // helper function that returns a string input with its first letter capitalized 2 | export function capitalizeFirstLetter(tag: string) { 3 | return tag.charAt(0).toUpperCase() + tag.slice(1); 4 | } 5 | -------------------------------------------------------------------------------- /src/client/helperFunctions/dataToD3Elems.ts: -------------------------------------------------------------------------------- 1 | import { RawNodeDatum } from "react-d3-tree"; 2 | import { Item } from "../../types"; 3 | 4 | // helper function to convert app to readable format for react-d3-trees 5 | export const convertDataToElements = (root: Item) => { 6 | // helper function to traverse nodes 7 | const traverse = (node: Item) => { 8 | const { value, children, canEnter } = node; 9 | 10 | // if current node is not a component, return out 11 | if (!canEnter) { 12 | return; 13 | } 14 | 15 | // create a new element, in the fashion of d3-tree syntax 16 | const element: RawNodeDatum|undefined = { ...node, name: value.toString(), children: [] }; 17 | 18 | // loop through children arrays if present 19 | if (children && children.length > 0) { 20 | children.forEach((child) => { 21 | // childElement will equal recursive calls to traverse each child, resulting in subsequent element arrays being populated 22 | const childElement: RawNodeDatum|undefined = traverse(child); 23 | // if childElement is not null, push to the element's children array 24 | if (childElement && element.children) { 25 | element.children.push(childElement); 26 | } 27 | }); 28 | } 29 | return element; 30 | }; 31 | 32 | // construct the desired d3-tree elements array using traverse() 33 | const elements = traverse(root); 34 | 35 | return elements; 36 | }; 37 | -------------------------------------------------------------------------------- /src/client/helperFunctions/generateAppModule.ts: -------------------------------------------------------------------------------- 1 | import { generateImportStatements } from "./generateImportStatements"; 2 | import { capitalizeFirstLetter } from "./capitalizeFirstLetter"; 3 | 4 | // helper function to generate app.module.ts contents 5 | // input: array of arrays, each containing modules, to import (these correspond to each component's selector string) 6 | // output: formatted string of desired app.module.ts contents 7 | export function generateAppModule(modules: string[]) { 8 | const importStatements = generateImportStatements(modules); 9 | const appComponentImport = `import { AppComponent } from './app.component';\n`; // Add the import statement for AppComponent 10 | const ngModule = ` 11 | @NgModule({ 12 | declarations: [ 13 | ${modules.map(module => `${capitalizeFirstLetter(module)}Component`).join(',\n ')}, 14 | AppComponent 15 | ], 16 | imports: [ 17 | BrowserModule 18 | ], 19 | providers: [], 20 | bootstrap: [AppComponent] 21 | }) 22 | `; 23 | return importStatements + appComponentImport + ngModule + '\nexport class AppModule { }'; 24 | } 25 | -------------------------------------------------------------------------------- /src/client/helperFunctions/generateComponentContents.ts: -------------------------------------------------------------------------------- 1 | import { UniqueIdentifier } from "@dnd-kit/core"; 2 | import { insertAppPrefix } from "./insertAppPrefix"; 3 | import { capitalizeFirstLetter } from "./capitalizeFirstLetter"; 4 | 5 | // helper function to generate app.component.ts contents 6 | // input: array of html tags as strings, and a componentName as a string (typed as UniqueIdentifier) 7 | // output: a large string containing the component's contents 8 | export function generateComponentContents(tags: string[], componentName: UniqueIdentifier) { 9 | let selector = componentName.toString().toLowerCase(); 10 | if (componentName === 'app') selector = 'app-root'; 11 | 12 | // const modifiedTags = insertAppPrefix(tags); 13 | 14 | let templateCode = ''; 15 | if (tags !== undefined) { 16 | templateCode = tags.map(tag => `\n ${tag}`).join(''); 17 | } 18 | 19 | const componentContents = ` 20 | import { Component } from '@angular/core'; 21 | import { CommonModule } from '@angular/common'; 22 | 23 | @Component({ 24 | selector: '${selector}', 25 | template: \`${templateCode} 26 | \`, 27 | styleUrls: ['${componentName}.component.css'] 28 | }) 29 | export class ${capitalizeFirstLetter(componentName.toString())}Component { 30 | } 31 | `; 32 | 33 | return componentContents; 34 | } 35 | -------------------------------------------------------------------------------- /src/client/helperFunctions/generateImportStatements.ts: -------------------------------------------------------------------------------- 1 | import { capitalizeFirstLetter } from "./capitalizeFirstLetter"; 2 | 3 | // helper function to generate import statements 4 | // input: array of modules as strings, path 5 | // output: import statement as a string 6 | export function generateImportStatements(modules: string[]) { 7 | let importStatements = `import { NgModule } from '@angular/core';\nimport { BrowserModule } from '@angular/platform-browser';\n`; 8 | for (const module of modules) { 9 | const path = `./components/${module}/${module}.component`; 10 | importStatements += `import { ${capitalizeFirstLetter(module)}Component } from '${path}';\n`; 11 | } 12 | return importStatements; 13 | } 14 | -------------------------------------------------------------------------------- /src/client/helperFunctions/generateTestContents.ts: -------------------------------------------------------------------------------- 1 | import { UniqueIdentifier } from "@dnd-kit/core"; 2 | import { capitalizeFirstLetter } from "./capitalizeFirstLetter"; 3 | 4 | // helper function to generate app.component.ts contents 5 | // input: a componentName string, typed as UniqueIdentifier 6 | // output: a large string containing the testing file code 7 | export function generateTestContents(componentName: UniqueIdentifier) { 8 | const pascalCompName = capitalizeFirstLetter(componentName.toString()); 9 | const importStatement = `import { TestBed } from '@angular/core/testing'; 10 | import { ${capitalizeFirstLetter(pascalCompName.toString())}Component } from './${componentName}.component';`; 11 | 12 | const testContents = ` 13 | describe('${pascalCompName}', () => { 14 | beforeEach(() => TestBed.configureTestingModule({ 15 | declarations: [${pascalCompName}Component] 16 | })); 17 | 18 | it('should create the app', () => { 19 | const fixture = TestBed.createComponent(${pascalCompName}Component); 20 | const app = fixture.componentInstance; 21 | expect(app).toBeTruthy(); 22 | }); 23 | }); 24 | `; 25 | 26 | return importStatement + testContents; 27 | } 28 | -------------------------------------------------------------------------------- /src/client/helperFunctions/insertAppPrefix.ts: -------------------------------------------------------------------------------- 1 | // helper function to reformat an array of html tags so that the string 'app-' is appended to the beginning of each tag 2 | // returns array of tags, with 'app-' inserted before each string's first character, and after each string's first occurence of '/' 3 | export function insertAppPrefix(tags:string[]) { 4 | const result = []; 5 | for (const element of tags) { 6 | let modifiedElement = element.replace(/(\/)/g, '$1app-').replace(/([a-zA-Z])/,'app-$1'); 7 | result.push(modifiedElement); 8 | } 9 | return result; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/helperFunctions/traverseAndWrite.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "../../types"; 2 | import { UniqueIdentifier } from "@dnd-kit/core"; 3 | import JSZip from "jszip"; 4 | import { generateComponentContents } from "./generateComponentContents"; 5 | import { generateTestContents } from "./generateTestContents"; 6 | 7 | // function to iterate through comps[], for each 'canEnter' node, create a folder named (property of) 'value', componentFiles.map() 8 | // input: comps 9 | // output: none, but componentsFolder will populate 10 | export function traverseAndWrite(node: Item, componentsFolder: JSZip|null|undefined, components:UniqueIdentifier[]): void { 11 | const { value, code, canEnter, children } = node; 12 | 13 | // if the current node is a component within app, then create corresponding angular component folder contents 14 | if (canEnter && value !== 'app' ) { 15 | 16 | // push component's name (also acts as the component selector string) into the global components array 17 | components.push(value); 18 | // create an individual component folder inside of the components folder 19 | const componentFolder = componentsFolder?.folder(`${value}`); 20 | 21 | // populate this folder with angular component files 22 | const staticComponentFiles = ['.component.html', '.component.css']; 23 | // CURRENT MAP FUNCTION IS ONLY FOR MVP, FIGURE OUT LOGIC LATER 24 | // go into .ts file, write boilerplate with corresponding 'code' property contents into the file 25 | staticComponentFiles.map(fileName => componentFolder?.file(`${value}${fileName}`, '')); 26 | 27 | // array to hold the current component's corresponding children html tags 28 | const componentTags:string[] = []; 29 | 30 | // if there are children, then write the corresponding children code into the current component's component.ts file, then recurse and complete the rest of the file directory 31 | if (children) { 32 | children.map(child => { 33 | if (child.code) componentTags.push(child.code); 34 | traverseAndWrite(child, componentsFolder, components); 35 | }) 36 | const componentContents = generateComponentContents(componentTags, value); 37 | const testContents = generateTestContents(value); 38 | componentFolder?.file(`${value}.component.ts`, componentContents); 39 | componentFolder?.file(`${value}.component.spec.ts`, testContents); 40 | } 41 | } else { 42 | if (children.length) { 43 | children.map(child => traverseAndWrite(child, componentsFolder, components)); 44 | } 45 | } 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /src/client/helperFunctions/zipFiles.ts: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver'; 2 | import { Item } from '../../types'; 3 | import JSZip from 'jszip'; 4 | import { traverseAndWrite } from './traverseAndWrite'; 5 | import { generateImportStatements } from './generateImportStatements'; 6 | import { generateAppModule } from './generateAppModule'; 7 | import { generateComponentContents } from './generateComponentContents'; 8 | import { generateTestContents } from './generateTestContents'; 9 | 10 | //function to create a zip file for export in web app 11 | const zipFiles = (app: Item) => { 12 | //initializes zip 13 | const zip = new JSZip(); 14 | // change project name to template literal of current project 15 | const angularProject = zip.folder('angularProject'); 16 | //creates app folder inside of zip folder 17 | const appFolder = angularProject?.folder('app'); 18 | 19 | // create app.component.html, .css, .spec.ts, .ts 20 | 21 | appFolder?.file('app.component.html', ''); 22 | appFolder?.file('app.component.css', ''); 23 | const appCode:any = []; 24 | if (app.children.length) { 25 | app.children.map(child => appCode.push(child.code)); 26 | } 27 | 28 | appFolder?.file('app.component.ts', generateComponentContents(appCode, app.value)) 29 | appFolder?.file('app.component.spec.ts', generateTestContents('app')); 30 | 31 | // create components folder 32 | const componentsFolder = appFolder?.folder('components'); 33 | 34 | // iterate through app, and create file structure with corresponding code for each component 35 | // also populates global components array, for future parsing to populate app.module.ts file 36 | const components:string[] = []; 37 | 38 | traverseAndWrite(app, componentsFolder, components); 39 | 40 | const importStatements = generateImportStatements(components); 41 | 42 | const appModuleContents = generateAppModule(components); 43 | appFolder?.file('app.module.ts', appModuleContents); 44 | 45 | // export zipped folder 46 | zip.generateAsync({type:"blob"}) 47 | .then(function(content) { 48 | // see FileSaver.js 49 | saveAs(content, "angularProject.zip"); 50 | }); 51 | 52 | }; 53 | 54 | export default zipFiles; 55 | -------------------------------------------------------------------------------- /src/client/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | * { 6 | font-family: sans-serif; 7 | } 8 | 9 | @layer components { 10 | .previewBtn { 11 | flex-basis: 50%; 12 | border-style: solid; 13 | padding-top: 10px; 14 | padding-bottom: 10px; 15 | border-bottom-width: 1px; 16 | color: gray; 17 | border-color: gray; 18 | } 19 | 20 | .previewBtnSelected { 21 | flex-basis: 50%; 22 | border-style: solid; 23 | padding-top: 10px; 24 | padding-bottom: 10px; 25 | border-bottom-width: 1px; 26 | color: #dc2626; 27 | border-color: #dc2626; 28 | } 29 | 30 | .modal { 31 | background-color: lighten(slategrey, 50%); 32 | max-width:fit-content; 33 | min-width:fit-content; 34 | color: black; 35 | border-radius: 12px; 36 | opacity: 1; 37 | } 38 | .modal::backdrop{ 39 | background: black; 40 | opacity:.4; 41 | } 42 | .nodeRoot > circle{ 43 | fill: #dc2626; 44 | } 45 | .nodeBranch > circle{ 46 | fill: #dc2626; 47 | } 48 | .nodeLeaf > circle{ 49 | fill: #dc2626; 50 | r: 40; 51 | } 52 | } -------------------------------------------------------------------------------- /src/client/main.tsx: -------------------------------------------------------------------------------- 1 | import "./index.css"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom/client"; 5 | 6 | import App from "./App"; 7 | 8 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 9 | 10 | 11 | 12 | ); 13 | -------------------------------------------------------------------------------- /src/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/client/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/server/controllers/authControllers/userController.ts: -------------------------------------------------------------------------------- 1 | // MongoDB Import 2 | import mongoose from 'mongoose'; 3 | import Session from '../../models/sessionModel' 4 | const models = require('../../models/userModel'); 5 | const bcrypt = require('bcryptjs'); 6 | const SALT_WORK_FACTOR = 10; 7 | import express, { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'; 8 | 9 | interface UserController { 10 | isLoggedIn: (req: Request, res: Response, next: NextFunction) => Promise; 11 | createUser: (req: Request, res: Response, next: NextFunction) => Promise; 12 | verifyUser: (req: Request, res: Response, next: NextFunction) => Promise; 13 | logOutUser: (req: Request, res: Response, next: NextFunction) => Promise; 14 | } 15 | 16 | const userController: UserController = { 17 | 18 | // Checks if a user is logged in 19 | 20 | isLoggedIn: async (req, res, next) => { 21 | const { SSID } = req.cookies 22 | try{ 23 | const sessionDoc = await Session.findOne({ cookieId: SSID }) 24 | res.locals.session = sessionDoc 25 | return next(); 26 | } catch(err){ 27 | return next({ 28 | log: 'userController', 29 | status: 400, 30 | message: `Login unsucessful`, 31 | }); 32 | } 33 | }, 34 | 35 | // Signs up a new user 36 | 37 | createUser: async (req, res, next) => { 38 | 39 | let {username, password, email} = req.body; 40 | username = username.toLowerCase() 41 | 42 | if(!username || !password || !email){ 43 | return next({ 44 | log: 'userController createUser error', 45 | status: 400, 46 | message: `Username, password, and email is required`, 47 | }); 48 | } 49 | try{ 50 | const userDoc = await models.User.create({username, password, email}) 51 | res.locals.user = userDoc 52 | return next() 53 | } catch (err) { 54 | return next({ 55 | log: 'userController', 56 | status: 400, 57 | message: `User cannot be created at this time. Please try again.`, 58 | }); 59 | } 60 | }, 61 | 62 | // User Login, verifying credentials 63 | 64 | verifyUser: async (req, res, next) => { 65 | const {username, password} = req.body 66 | 67 | if(!username || !password){ 68 | return next({ 69 | log: 'userController verifyUser error', 70 | status: 400, 71 | message: `Username and password is required`, 72 | }); 73 | } 74 | 75 | try{ 76 | const userExist = await models.User.findOne({username}) 77 | 78 | if(userExist){ 79 | const hashedPW = userExist.password 80 | 81 | bcrypt.compare(password, hashedPW, (err: any, valid: any) => { 82 | if (err) { 83 | return next(err); 84 | } 85 | 86 | if (valid) { 87 | res.locals.user = userExist 88 | return next(); 89 | } 90 | else { 91 | return next({ 92 | log: 'userController', 93 | status: 400, 94 | message: `Invalid username or password. Please try again.`, 95 | }) 96 | } 97 | }); 98 | } else{ 99 | return next({ 100 | log: 'userController', 101 | status: 400, 102 | message: `Invalid username or password. Please try again.`, 103 | }) 104 | } 105 | } catch (err) { 106 | return next({ 107 | log: 'userController', 108 | status: 400, 109 | message: `Invalid username or password. Please try again.`, 110 | }); 111 | }; 112 | }, 113 | 114 | // Logout User 115 | 116 | logOutUser: async (req, res, next) => { 117 | try{ 118 | // Grab User's current web SSID Cookies 119 | const {SSID} = req.cookies 120 | // Search and Delete MongoDB Session Database for cookieId that matches the user's current web SSID cookie 121 | const cookieExist = await Session.findOneAndDelete({cookieId: SSID}) 122 | return next() 123 | } catch (err) { 124 | return next({ 125 | log: 'userController', 126 | status: 400, 127 | message: `Unable to logout. Please try again.`, 128 | }); 129 | }; 130 | } 131 | } 132 | 133 | export default userController; 134 | -------------------------------------------------------------------------------- /src/server/controllers/cookieControllers/cookieController.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | 3 | interface CookieController { 4 | setSSIDCookie: (req: Request, res: Response, next: NextFunction) => Promise; 5 | } 6 | const cookieController: CookieController = { 7 | 8 | // Cookie is set after user successfully signs up or logs in 9 | // setSSIDCookie - store the user id in a cookie 10 | 11 | setSSIDCookie: async (req, res, next) => { 12 | const options = { 13 | httpOnly: true 14 | } 15 | const { id } = res.locals.user 16 | try{ 17 | res.cookie('SSID', id, options); 18 | return next(); 19 | } catch(err) { 20 | return next({ 21 | log: 'cookieController', 22 | status: 400, 23 | message: `Cookie error, please try again.`, 24 | }); 25 | } 26 | } 27 | } 28 | 29 | export default cookieController; 30 | -------------------------------------------------------------------------------- /src/server/controllers/cookieControllers/sessionController.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'; 2 | import Session from '../../models/sessionModel' 3 | 4 | interface SessionController { 5 | startSession: (req: Request, res: Response, next: NextFunction) => Promise ; 6 | } 7 | 8 | const sessionController: SessionController = { 9 | startSession: async (req, res, next) => { 10 | const { id } = res.locals.user 11 | try{ 12 | const sessionDoc = await Session.create({cookieId: id}); 13 | return next() 14 | } catch (err) { 15 | return next({ 16 | log: 'startSession Controller', 17 | status: 400, 18 | message: `Session error, please try again.`, 19 | }); 20 | } 21 | } 22 | } 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | export default sessionController 33 | -------------------------------------------------------------------------------- /src/server/controllers/projControllers/projController.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | import Project from '../../models/projectModel' 3 | 4 | interface ProjController { 5 | saveProj: (req: Request, res: Response, next: NextFunction) => Promise; 6 | updateProj: (req: Request, res: Response, next: NextFunction) => Promise; 7 | loadProj: (req: Request, res: Response, next: NextFunction) => Promise; 8 | deleteProj: (req: Request, res: Response, next: NextFunction) => Promise; 9 | } 10 | 11 | // Project Controllers for New/Save/Load Projects 12 | 13 | const projController: ProjController = { 14 | saveProj: async (req, res, next) => { 15 | const {title, root, users} = req.body; 16 | const arrObj = [users] 17 | try { 18 | const projDoc = await Project.create({title, root, users: arrObj}) 19 | res.locals.newProj = 'Project has been saved'; 20 | return next(); 21 | } catch (err) { 22 | return next({ 23 | log: 'saveProject Controller', 24 | status: 400, 25 | message: `Unable to save project, please try again.`, 26 | }); 27 | } 28 | }, 29 | 30 | updateProj: async (req, res, next) => { 31 | const {username, project, root} = req.body; 32 | try { 33 | const userInfo = {title: project, users: username}; 34 | const projDoc = await Project.findOneAndUpdate(userInfo, { $set: {root}}, {new: true}); 35 | res.locals.newProj = 'Project has been updated'; 36 | return next() 37 | } catch (err) { 38 | return next({ 39 | log: 'updateProj Controller', 40 | status: 400, 41 | message: `Unable to update project, please try again.`, 42 | }); 43 | } 44 | }, 45 | 46 | loadProj: async (req, res, next) => { 47 | const {id} = req.params; 48 | try { 49 | const projects = await Project.find({users: id}); 50 | res.locals.projects = projects 51 | return next(); 52 | } catch (err) { 53 | return next({ 54 | log: 'loadProj Controller', 55 | status: 400, 56 | message: `Unable to load project, please try again`, 57 | }); 58 | } 59 | }, 60 | 61 | deleteProj: async (req, res, next) => { 62 | const {username, title} = req.body; 63 | try { 64 | const userInfo = {title, users: username} 65 | const deleted = await Project.findOneAndDelete(userInfo) 66 | res.locals.deleted = deleted 67 | return next() 68 | } catch (err) { 69 | return next({ 70 | log: 'deleteProj Project Controller', 71 | status: 400, 72 | message: `Unable to delete project, please try again`, 73 | }); 74 | } 75 | } 76 | }; 77 | 78 | export default projController; 79 | -------------------------------------------------------------------------------- /src/server/main.ts: -------------------------------------------------------------------------------- 1 | // NPM Packages 2 | require('dotenv').config() 3 | import express, { Request, Response, NextFunction, RequestHandler, ErrorRequestHandler } from 'express'; 4 | import ViteExpress from "vite-express"; 5 | 6 | // comment in if prodmode 7 | if (process.env.mode === 'production') 8 | ViteExpress.config({ mode: "production" }) 9 | 10 | console.log('current mode: ', process.env.mode); 11 | 12 | const path = require('path'); 13 | const app = express(); 14 | const mongoose = require('mongoose'); 15 | const cookieParser = require('cookie-parser'); 16 | 17 | // Router Imports 18 | 19 | const authRouter = require('../server/routers/authRouter'); 20 | const projRouter = require('../server/routers/projRouter'); 21 | 22 | 23 | // Controller Imports 24 | 25 | // Connect to MongoDB Database 26 | 27 | const connectToMongoDB = async () => { 28 | try { 29 | await mongoose.connect(process.env.MONGO_URI); 30 | console.log('Connected to MongoDB'); 31 | } catch (error) { 32 | console.log('Failed to connect to MongoDB'); 33 | console.log(error); 34 | } 35 | }; 36 | 37 | connectToMongoDB(); 38 | 39 | //Parse incoming request 40 | 41 | app.use(express.json()); 42 | app.use(express.urlencoded({ extended: true })); 43 | app.use(cookieParser()); 44 | 45 | // Vite Testing Route 46 | 47 | // Render HTML Pages with Express - Vite already serves static files on dev mode 48 | 49 | // app.use(express.static(path.join(__dirname, 'dist'))); 50 | 51 | // Routers 52 | 53 | /*** Authentication Router ***/ 54 | 55 | app.use('/', authRouter) 56 | 57 | /*** New/Save/Load Project Router ***/ 58 | 59 | app.use('/proj', projRouter) 60 | 61 | // Global Error Handler 62 | 63 | app.use((err: ErrorRequestHandler, req: Request, res: Response, next: NextFunction) => { 64 | const defaultError = { 65 | log: 'Express error handler caught, unknown middleware error', 66 | status: 500, 67 | message: { err: 'There has been an error' }, 68 | }; 69 | const errorObj = Object.assign({}, defaultError, err); 70 | console.log(errorObj); 71 | return res.status(errorObj.status).json(errorObj.message); 72 | }); 73 | 74 | // Vite Port 75 | ViteExpress.listen(app, 3000, () => 76 | console.log("Server is listening on port 3000...") 77 | ); 78 | -------------------------------------------------------------------------------- /src/server/models/projectModel.ts: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | 5 | const projectSchema = new Schema({ 6 | title: {type: String, required: true}, 7 | root: {type: Array, required: true}, 8 | users: {type: Array, required: true} 9 | }) 10 | 11 | const Project = mongoose.model('Project', projectSchema); 12 | 13 | export default Project; 14 | -------------------------------------------------------------------------------- /src/server/models/sessionModel.ts: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const Schema = mongoose.Schema; 3 | 4 | 5 | 6 | const sessionSchema = new Schema({ 7 | cookieId: { type: String, required: true, unique: true }, 8 | createdAt: { type: Date, expires: 3000, default: Date.now } 9 | }); 10 | 11 | // Adjust MongoDB cleanup session for specific Time To Live Index (TTL) in seconds 12 | 13 | sessionSchema.index({ createdAt: 1 }, { expireAfterSeconds: 3000}); 14 | 15 | const Session = mongoose.model('Session', sessionSchema) 16 | 17 | export default Session; 18 | -------------------------------------------------------------------------------- /src/server/models/userModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | const Schema = mongoose.Schema; 3 | 4 | const SALT_WORK_FACTOR = 10; 5 | const bcrypt = require('bcryptjs'); 6 | 7 | interface userSchema { 8 | username: string; 9 | password: string; 10 | email: string; 11 | } 12 | 13 | const userSchema = new Schema({ 14 | username: {type: String, required: true, unique: true}, 15 | password: {type: String, required: true}, 16 | email: {type: String, required: true, unique: true} 17 | }) 18 | 19 | userSchema.pre('save', async function (next){ 20 | try { 21 | console.log('this is the user password', this.password) 22 | this.password = await bcrypt.hash(this.password, SALT_WORK_FACTOR) 23 | console.log('password has been hashed', this.password) 24 | return next() 25 | } catch (err){ 26 | console.log('error in hashing password', err) 27 | } 28 | }); 29 | 30 | const User = mongoose.model('User', userSchema); 31 | 32 | module.exports = { 33 | User, 34 | }; 35 | -------------------------------------------------------------------------------- /src/server/routers/authRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | const router = express.Router(); 3 | 4 | // MongoDB Imports 5 | const models = require('../models/userModel'); 6 | import userController from '../controllers/authControllers/userController' 7 | import cookieController from '../controllers/cookieControllers/cookieController' 8 | import sessionController from '../controllers/cookieControllers/sessionController' 9 | 10 | // Check if User is already Logged in and a Session exists - fetch to / 11 | 12 | router.get('/loggedIn', userController.isLoggedIn, async (req: Request, res: Response) => { 13 | if(res.locals.session === null){ 14 | res.status(200).json('User is currently not logged in') 15 | } else { 16 | const { cookieId } = res.locals.session 17 | const userDoc = await models.User.findOne({_id: cookieId}) 18 | res.status(200).json(userDoc.username) 19 | } 20 | }) 21 | 22 | // Signup User- fetch to /signup 23 | 24 | router.post('/signup', userController.createUser, cookieController.setSSIDCookie, sessionController.startSession, (req: Request, res: Response) => { 25 | return res.status(200).json('Sign Up succesful'); 26 | }); 27 | 28 | // Login User - fetch to /login 29 | 30 | router.post('/login', userController.verifyUser, cookieController.setSSIDCookie, sessionController.startSession, (req: Request, res: Response) => { 31 | return res.status(200).json('Logged in successfully'); 32 | }); 33 | 34 | // Logout User - fetch to /logout 35 | 36 | router.patch('/logout', userController.logOutUser, (req: Request, res: Response) => { 37 | return res.status(200).json('Logged out successfully'); 38 | }); 39 | 40 | 41 | module.exports = router; 42 | -------------------------------------------------------------------------------- /src/server/routers/projRouter.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response, NextFunction, RequestHandler } from 'express'; 2 | import projController from '../controllers/projControllers/projController'; 3 | const router = express.Router(); 4 | 5 | // Save Project fetch to '/proj/save' 6 | 7 | router.post('/', projController.saveProj, (req: Request, res: Response) => { 8 | return res.status(200).json('Project has been Saved'); 9 | }); 10 | 11 | // update a project '/proj' 12 | router.patch('/', projController.updateProj, (req: Request, res: Response) => { 13 | return res.status(200).json('Project Updated'); 14 | }) 15 | 16 | // Load Project - fetch to '/proj/load' 17 | 18 | router.get('/:id', projController.loadProj, (req: Request, res: Response) => { 19 | return res.status(200).json(res.locals.projects); 20 | }); 21 | 22 | router.delete('/', projController.deleteProj, (req: Request, res: Response) => { 23 | return res.status(200).json(res.locals.deleted) 24 | }) 25 | 26 | module.exports = router; 27 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { UniqueIdentifier } from "@dnd-kit/core" 2 | 3 | export type Item = { 4 | value: UniqueIdentifier, 5 | id: string, 6 | children: Array, 7 | code?: string, 8 | codeStart?: string, 9 | codeEnd?: string, 10 | canEnter?: boolean 11 | } 12 | 13 | export type Project = { 14 | title: string; 15 | root: Array; 16 | users: Array; 17 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './index.html', 5 | './src/client/*.{js,ts,jsx,tsx}', 6 | './src/client/components/*.{js,ts,jsx,tsx}', 7 | './src/client/components/Containers/*.{js,ts,jsx,tsx}', 8 | './src/client/components/Modals/*.{js,ts,jsx,tsx}', 9 | ], 10 | theme: { 11 | extend: {}, 12 | }, 13 | plugins: [require('tailwind-scrollbar')], 14 | }; 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "jsx": "preserve", 5 | "useDefineForClassFields": true, 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "allowJs": false, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "module": "CommonJS", 14 | "moduleResolution": "Node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true 18 | }, 19 | "include": ["src", "cypress"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": [ 4 | "./__tests__/**", 5 | ] 6 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------