├── .github └── workflows │ ├── react-deploy.yml │ └── server-deploy.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── assets ├── BackendDeployment.drawio ├── BackendDeployment.png ├── FrontendDeployment.jpeg ├── aws.drawio ├── aws.png └── logo.docx ├── client ├── .env.example ├── .gitignore ├── .idea │ ├── .gitignore │ ├── .name │ ├── client.iml │ ├── inspectionProfiles │ │ └── Project_Default.xml │ ├── modules.xml │ └── vcs.xml ├── README.md ├── package.json ├── public │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ ├── logo.ico │ ├── logo.png │ ├── logo512.png │ └── manifest.json └── src │ ├── App.js │ ├── components │ ├── CodeSnippet.jsx │ ├── Footer.jsx │ ├── MenuBar.jsx │ ├── OptionDrawer.jsx │ ├── PasswordValidator.jsx │ ├── PrivateRoute.jsx │ ├── Profile.jsx │ ├── QuestionsTable.jsx │ ├── RandomQuote.jsx │ ├── ServiceCard.jsx │ ├── Stopwatch.jsx │ ├── UserBadge.jsx │ ├── algorithm_page_components │ │ ├── AlgorithmCard.jsx │ │ ├── AlgorithmDialog.jsx │ │ ├── AlgorithmForm.jsx │ │ ├── AlgorithmHeader.jsx │ │ ├── AlgorithmTextField.jsx │ │ ├── NewAlgorithmDialog.jsx │ │ └── UpdateAlgorithmForm.jsx │ ├── dashboard_page │ │ ├── AlgorithmStats.jsx │ │ ├── DataStructureStats.jsx │ │ ├── LeetCodeStats.jsx │ │ └── Welcome.jsx │ ├── data_structure_page │ │ ├── ContentDisplay.jsx │ │ ├── DataStructureDialogs.jsx │ │ ├── DataStructureList.jsx │ │ ├── EditorWithMenuBar.css │ │ ├── EditorWithMenuBar.jsx │ │ └── NodeList.jsx │ ├── generic │ │ ├── GenericButton.jsx │ │ ├── GenericDialog.jsx │ │ ├── GenericFormControl.jsx │ │ ├── GenericSearchBox.jsx │ │ ├── GenericSpinner.jsx │ │ └── GenricTextField.jsx │ ├── navbar │ │ ├── AccountNavbar.jsx │ │ ├── AuthenticatedNavbar.jsx │ │ └── HomeNavbar.jsx │ ├── new_question │ │ ├── NewQuestionFooter.jsx │ │ ├── NewQuestionForm.jsx │ │ ├── Solution.jsx │ │ ├── SuccessToggle.jsx │ │ └── UpdateQuestionForm.jsx │ └── routes │ │ ├── PremiumPlusRoute.jsx │ │ └── PremiumRoute.jsx │ ├── config │ └── axiosConfig.js │ ├── context │ ├── AlgorithmContext.js │ ├── dataStructureContext.js │ └── userContext.js │ ├── general │ └── quote.js │ ├── hooks │ ├── AlgorithmHooks.js │ ├── ContentHooks.js │ ├── DataStructureHooks.js │ ├── NodeHooks.js │ ├── useQuestionHooks.js │ └── userHooks │ │ └── UserHooks.js │ ├── images │ ├── DataStructureImage.png │ ├── admin.gif │ ├── codeDetails.png │ ├── dashboard.png │ ├── future.jpeg │ ├── premium.gif │ ├── premiumplus.gif │ ├── regular.gif │ └── table.png │ ├── index.css │ ├── index.js │ ├── pages │ ├── AlgorithmPage.jsx │ ├── Dashboard.jsx │ ├── DataStructurePage.jsx │ ├── FriendPage.jsx │ ├── Home.jsx │ ├── NewAlgorithmPage.jsx │ ├── NewQuestion.jsx │ ├── NotFoundPage.jsx │ ├── ProfilePage.jsx │ ├── QuestionDetails.jsx │ ├── RegisterPage.jsx │ ├── SignInPage.jsx │ ├── TablePage.jsx │ └── UpgradePage.jsx │ └── reducer │ ├── algoirthmActions.js │ ├── algorithmReducer.js │ ├── dataStructureActions.js │ ├── dataStructureReducer.js │ ├── userActions.js │ └── userReducer.js └── ylslc ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── Dockerfile ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── yls │ │ └── ylslc │ │ ├── YlslcApplication.java │ │ ├── algorithm │ │ ├── AlgorithmController.java │ │ ├── AlgorithmDto.java │ │ ├── AlgorithmEntity.java │ │ ├── AlgorithmRepository.java │ │ ├── AlgorithmService.java │ │ ├── AlgorithmServiceImpl.java │ │ └── section │ │ │ ├── SectionDto.java │ │ │ ├── SectionEntity.java │ │ │ ├── SectionRepository.java │ │ │ ├── SectionService.java │ │ │ └── SectionServiceImpl.java │ │ ├── config │ │ ├── MapperConfig.java │ │ ├── exception │ │ │ ├── GlobalExceptionHandler.java │ │ │ └── QuestionException.java │ │ ├── jwt │ │ │ ├── CustomAuthenticationEntryPoint.java │ │ │ ├── JwtAuthenticationFilter.java │ │ │ ├── JwtService.java │ │ │ └── SecurityConfig.java │ │ └── response │ │ │ └── Response.java │ │ ├── controllers │ │ ├── PaymentController.java │ │ └── TestController.java │ │ ├── data_structure │ │ ├── DataStructureController.java │ │ ├── DataStructureDto.java │ │ ├── DataStructureEntity.java │ │ ├── DataStructureRepository.java │ │ ├── DataStructureService.java │ │ └── DataStructureServiceImpl.java │ │ ├── mappers │ │ ├── AlgorithmMapperImpl.java │ │ ├── DataStructureMapperImpl.java │ │ ├── Mapper.java │ │ ├── NodeMapperImpl.java │ │ ├── QuestionMapperImpl.java │ │ └── UserMapperImpl.java │ │ ├── node │ │ ├── NodeController.java │ │ ├── NodeDto.java │ │ ├── NodeEntity.java │ │ ├── NodeRepository.java │ │ ├── NodeService.java │ │ └── NodeServiceImpl.java │ │ ├── question │ │ ├── QuestionController.java │ │ ├── QuestionDto.java │ │ ├── QuestionEntity.java │ │ ├── QuestionRepository.java │ │ ├── QuestionService.java │ │ ├── QuestionServiceImpl.java │ │ └── solution │ │ │ ├── SolutionDto.java │ │ │ ├── SolutionEntity.java │ │ │ ├── SolutionRepository.java │ │ │ ├── SolutionService.java │ │ │ └── SolutionServiceImpl.java │ │ └── user │ │ ├── Role.java │ │ ├── UserController.java │ │ ├── UserDetailsServiceImpl.java │ │ ├── UserDto.java │ │ ├── UserEntity.java │ │ ├── UserRepository.java │ │ ├── UserService.java │ │ ├── UserServiceImpl.java │ │ └── auth │ │ ├── AuthService.java │ │ ├── AuthServiceImpl.java │ │ └── AuthenticationController.java └── resources │ └── application.yml └── test └── java └── com └── yls └── ylslc └── YlslcApplicationTests.java /.github/workflows/react-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy React Frontend 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | paths: 8 | - "client/**" 9 | - ".github/workflows/react-deploy.yml" 10 | 11 | jobs: 12 | deploy-frontend: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Deploy to Frontend VM 16 | uses: appleboy/ssh-action@v1 17 | with: 18 | host: ${{ secrets.VM_HOST }} 19 | username: ${{ secrets.VM_USER }} 20 | key: ${{ secrets.REACT_VM_SSH_KEY }} 21 | port: 2222 22 | script: | 23 | cd /home/edisonyls/LeetCodeRecorder/client 24 | git pull --no-rebase 25 | npm install 26 | npm run build 27 | sudo rm -rf /var/www/ylslc 28 | sudo mv build /var/www/ylslc 29 | -------------------------------------------------------------------------------- /.github/workflows/server-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Spring Boot App 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | paths: 8 | - "server/**" 9 | - ".github/workflows/server-deploy.yml" 10 | 11 | jobs: 12 | deploy: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | 19 | - name: Deploy to Remote VM via SSH 20 | uses: appleboy/ssh-action@v1 21 | with: 22 | host: ${{ secrets.VM_HOST }} 23 | username: ${{ secrets.VM_USER }} 24 | key: ${{ secrets.SERVER_VM_SSH_KEY }} 25 | port: 2223 26 | script: | 27 | export DATABASE_USERNAME="${{ secrets.DATABASE_USERNAME }}" 28 | export DATABASE_PASSWORD="${{ secrets.DATABASE_PASSWORD }}" 29 | export JWT_SECRET="${{ secrets.JWT_SECRET }}" 30 | export UPGRADE_SECRET_KEY="${{ secrets.UPGRADE_SECRET_KEY }}" 31 | 32 | cd LeetCodeRecorder 33 | git pull 34 | cd ylslc 35 | ./mvnw clean package 36 | cd target 37 | rm ylslc-0.0.1-SNAPSHOT.jar.original || true 38 | cd .. 39 | sudo cp target/ylslc-0.0.1-SNAPSHOT.jar /opt/ylslc/ylslc-0.0.1-SNAPSHOT.jar 40 | sudo systemctl restart ylslc.service 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .idea 4 | 5 | .fake 6 | .env 7 | .env.local 8 | .env.production -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "interactive" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "React Init", 6 | "type": "shell", 7 | "command": "npm install", 8 | "options": { 9 | "cwd": "${workspaceFolder}/client" 10 | }, 11 | "group": "build", 12 | "presentation": { 13 | "reveal": "always", 14 | "panel": "new" 15 | }, 16 | "problemMatcher": [] 17 | }, 18 | { 19 | "label": "Start Spring Boot", 20 | "type": "shell", 21 | "command": "./mvnw spring-boot:run", 22 | "options": { 23 | "cwd": "${workspaceFolder}/ylslc" // Path to your Spring Boot project folder 24 | }, 25 | "group": "build", 26 | "isBackground": true, 27 | "presentation": { 28 | "reveal": "always", 29 | "panel": "new" 30 | }, 31 | "problemMatcher": [] 32 | }, 33 | { 34 | "label": "Start React App", 35 | "type": "shell", 36 | "command": "npm start", 37 | "options": { 38 | "cwd": "${workspaceFolder}/client" // Path to your React project folder 39 | }, 40 | "group": "build", 41 | "isBackground": true, 42 | "presentation": { 43 | "reveal": "always", 44 | "panel": "new" 45 | }, 46 | "problemMatcher": [] 47 | }, 48 | { 49 | "label": "Start Full Stack Development", 50 | "dependsOn": ["Start Spring Boot", "Start React App"] 51 | }, 52 | { 53 | "label": "Stop All Tasks", 54 | "command": "echo ${input:terminate}", 55 | "type": "shell", 56 | "problemMatcher": [] 57 | } 58 | ], 59 | "inputs": [ 60 | { 61 | "id": "terminate", 62 | "type": "command", 63 | "command": "workbench.action.tasks.terminate", 64 | "args": "terminateAll" 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LeetCode Recorder 2 | 3 | > An online tool to keep track of every LeetCode problem you have done. 4 | 5 | While enhancing my programming skills through practicing algorithms on LeetCode, I realized the potential benefits of systematically tracking my progress for each problem I solved. 6 | This included recording how long it took me to solve the problem, my approach, and the code I used. Initially, I started documenting these details manually using pen and paper, but soon 7 | found this method too time-consuming. Recognizing the need for a more efficient solution, I decided to digitize the process. The idea was to store this information in a database to ensure it 8 | wouldn't be lost and to make it easily revisitable. Having recently graduated with a master's degree, developing this online platform also provided an excellent opportunity to solidify my 9 | knowledge and learn new skills. This led to the creation of a comprehensive tool that not only helps in preserving my learning progress but also enhances my efficiency in revisiting and learning 10 | from past challenges. 11 | 12 | ### Demo 13 | 14 | https://github.com/edisonyls/LeetCodeRecorder/assets/89026659/d7c8a35b-73f8-44a8-b79d-c74ae26d830d 15 | 16 | ### Table of Contents 17 | 18 | - [Build With](###build-with) 19 | - [Getting Started](###getting-started) 20 | - [Frontend Setup](####frontend-setup) 21 | - [Backend Setup](####backend-end-setup) 22 | 23 | ### Build With 24 | 25 | - React 26 | - Spring Boot, Java 27 | - MySql 28 | 29 | ### Getting Started 30 | 31 | #### Frontend Setup 32 | 33 | - Make sure you have Node.js and npm installed 34 | - In the client directory, run `npm install` to install all the necessary dependencies 35 | - Configure your server address in `.env` file which is for local environment and `.env.production` which is for production environment. 36 | 37 | #### Backend Setup 38 | 39 | - This Spring Boot application runs on Java 17 40 | - Configure your database connection on the `application.yml` file. 41 | -------------------------------------------------------------------------------- /assets/BackendDeployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/assets/BackendDeployment.png -------------------------------------------------------------------------------- /assets/FrontendDeployment.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/assets/FrontendDeployment.jpeg -------------------------------------------------------------------------------- /assets/aws.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/assets/aws.png -------------------------------------------------------------------------------- /assets/logo.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/assets/logo.docx -------------------------------------------------------------------------------- /client/.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_API_ENDPOINT="" 2 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | package-lock.json -------------------------------------------------------------------------------- /client/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /client/.idea/.name: -------------------------------------------------------------------------------- 1 | index.js -------------------------------------------------------------------------------- /client/.idea/client.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /client/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /client/.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.11.4", 7 | "@emotion/styled": "^11.11.0", 8 | "@mui/icons-material": "^5.15.8", 9 | "@mui/material": "^5.15.11", 10 | "@mui/x-date-pickers": "^6.19.5", 11 | "@reduxjs/toolkit": "^2.2.7", 12 | "@tiptap/extension-color": "^2.2.4", 13 | "@tiptap/extension-image": "^2.2.4", 14 | "@tiptap/extension-list-item": "^2.2.4", 15 | "@tiptap/extension-text-style": "^2.2.4", 16 | "@tiptap/pm": "^2.2.4", 17 | "@tiptap/react": "^2.2.4", 18 | "@tiptap/starter-kit": "^2.2.4", 19 | "apexcharts": "^4.5.0", 20 | "axios": "^1.6.7", 21 | "canvas-confetti": "^1.9.2", 22 | "dayjs": "^1.11.10", 23 | "jwt-decode": "^4.0.0", 24 | "moment": "^2.30.1", 25 | "react": "^18.2.0", 26 | "react-apexcharts": "^1.4.1", 27 | "react-calendar-heatmap": "^1.10.0", 28 | "react-color": "^2.19.3", 29 | "react-dom": "^18.2.0", 30 | "react-redux": "^9.1.2", 31 | "react-router-dom": "^6.22.0", 32 | "react-scripts": "5.0.1", 33 | "react-spring": "^9.7.3", 34 | "react-toastify": "^10.0.4", 35 | "react-tooltip": "^4.5.1", 36 | "redux-persist": "^6.0.0" 37 | }, 38 | "scripts": { 39 | "start": "react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "eslintConfig": { 45 | "extends": [ 46 | "react-app", 47 | "react-app/jest" 48 | ] 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | }, 62 | "devDependencies": { 63 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /client/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/public/favicon-16x16.png -------------------------------------------------------------------------------- /client/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/public/favicon-32x32.png -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 30 | 34 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | LeetCode Recorder | Track and Analyze Your LeetCode Challenges 52 | 53 | 54 | 55 | 59 | 60 | 61 | 65 | 69 |
70 | 71 | 72 | -------------------------------------------------------------------------------- /client/public/logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/public/logo.ico -------------------------------------------------------------------------------- /client/public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/public/logo.png -------------------------------------------------------------------------------- /client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/public/logo512.png -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "YLSLC", 3 | "name": "LeetCode Recorder", 4 | "icons": [ 5 | { 6 | "src": "logo.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; 2 | import { ToastContainer } from "react-toastify"; 3 | import "react-toastify/dist/ReactToastify.css"; 4 | import SignInPage from "./pages/SignInPage"; 5 | import RegisterPage from "./pages/RegisterPage"; 6 | import Home from "./pages/Home"; 7 | import NewQuestion from "./pages/NewQuestion"; 8 | import QuestionDetails from "./pages/QuestionDetails"; 9 | import ProfilePage from "./pages/ProfilePage"; 10 | import FriendPage from "./pages/FriendPage"; 11 | import DataStructurePage from "./pages/DataStructurePage"; 12 | import AlgorithmPage from "./pages/AlgorithmPage"; 13 | import { UserProvider } from "./context/userContext"; 14 | import { DataStructureProvider } from "./context/dataStructureContext"; 15 | import { AlgorithmProvider } from "./context/AlgorithmContext"; 16 | import TablePage from "./pages/TablePage"; 17 | import Dashboard from "./pages/Dashboard"; 18 | import NewAlgorithmPage from "./pages/NewAlgorithmPage"; 19 | import PrivateRoute from "./components/PrivateRoute"; 20 | import UpgradePage from "./pages/UpgradePage"; 21 | import NotFoundPage from "./pages/NotFoundPage"; 22 | import PremiumRoute from "./components/routes/PremiumRoute"; 23 | import PremiumPlusRoute from "./components/routes/PremiumPlusRoute"; 24 | 25 | function App() { 26 | return ( 27 | <> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 54 | 59 | 64 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | 91 | export default App; 92 | -------------------------------------------------------------------------------- /client/src/components/CodeSnippet.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Typography } from "@mui/material"; 3 | 4 | const CodeSnippet = ({ code }) => { 5 | return ( 6 | 21 | 29 | {code} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default CodeSnippet; 36 | -------------------------------------------------------------------------------- /client/src/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { styled } from "@mui/material/styles"; 3 | import { Box, Typography, IconButton } from "@mui/material"; 4 | import InstagramIcon from "@mui/icons-material/Instagram"; 5 | import LinkedInIcon from "@mui/icons-material/LinkedIn"; 6 | import GitHubIcon from "@mui/icons-material/GitHub"; 7 | 8 | const Footer = () => { 9 | const handleIconClick = (url) => { 10 | window.open(url, "_blank", "noopener,noreferrer"); 11 | }; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 27 | YLSLC © {new Date().getFullYear()} 28 | 29 | 30 | 33 | 34 | 42 | handleIconClick("https://www.instagram.com/leesianyong/") 43 | } 44 | > 45 | 46 | 47 | 55 | handleIconClick( 56 | "https://www.linkedin.com/in/lishun-yang-152aa01b8/" 57 | ) 58 | } 59 | > 60 | 61 | 62 | handleIconClick("https://github.com/edisonyls")} 70 | > 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | const FooterContainer = styled(Box)(({ theme }) => ({ 82 | backgroundColor: "black", 83 | paddingTop: "10px", 84 | paddingBottom: "10px", 85 | display: "flex", 86 | flexDirection: "column", 87 | justifyContent: "center", 88 | alignItems: "center", 89 | height: "3vh", 90 | })); 91 | 92 | const SocialMediaBox = styled(Box)(({ theme }) => ({ 93 | maxWidth: "1000px", 94 | width: "100%", 95 | })); 96 | 97 | const SocialMediaWrap = styled(Box)(({ theme }) => ({ 98 | display: "flex", 99 | alignItems: "center", 100 | justifyContent: "center", 101 | width: "100%", 102 | margin: "4px auto 0 auto", 103 | [theme.breakpoints.down("md")]: { 104 | flexDirection: "column", 105 | alignItems: "center", 106 | }, 107 | })); 108 | 109 | const SocialIcon = styled(Box)(({ theme }) => ({ 110 | display: "flex", 111 | justifyContent: "space-between", 112 | alignItems: "center", 113 | width: "200px", 114 | })); 115 | 116 | export default Footer; 117 | -------------------------------------------------------------------------------- /client/src/components/OptionDrawer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Drawer, 4 | List, 5 | ListItem, 6 | ListItemIcon, 7 | ListItemText, 8 | Avatar, 9 | Box, 10 | Divider, 11 | Typography, 12 | } from "@mui/material"; 13 | import { useNavigate } from "react-router-dom"; 14 | import AccountCircleIcon from "@mui/icons-material/AccountCircle"; 15 | import Face from "@mui/icons-material/Face"; 16 | import LogoutIcon from "@mui/icons-material/ExitToApp"; 17 | import { BlackBackgroundButton } from "./generic/GenericButton"; 18 | import DashboardIcon from "@mui/icons-material/Dashboard"; 19 | import DataStructureIcon from "@mui/icons-material/Storage"; 20 | import CodeIcon from "@mui/icons-material/Code"; 21 | import PolylineIcon from "@mui/icons-material/Polyline"; 22 | 23 | const OptionDrawer = ({ 24 | isOpen, 25 | toggleDrawer, 26 | handleLogout, 27 | currentPath, 28 | user, 29 | }) => { 30 | const navigate = useNavigate(); 31 | 32 | const drawerOptions = [ 33 | { 34 | text: "Dashboard", 35 | icon: , 36 | path: "/dashboard", 37 | onClick: () => navigate("/dashboard"), 38 | roles: ["PREMIUM", "PREPLUS", "ADMIN"], 39 | }, 40 | { 41 | text: "LeetCode", 42 | icon: , 43 | path: "/table", 44 | onClick: () => navigate("/table"), 45 | roles: ["REGULAR", "PREMIUM", "PREPLUS", "ADMIN"], 46 | }, 47 | { 48 | text: "Data Structure", 49 | icon: , 50 | path: "/data-structure", 51 | onClick: () => navigate("/data-structure"), 52 | roles: ["PREPLUS", "ADMIN"], 53 | }, 54 | { 55 | text: "Algorithm", 56 | icon: , 57 | path: "/algorithm", 58 | onClick: () => navigate("/algorithm"), 59 | roles: ["PREPLUS", "ADMIN"], 60 | }, 61 | { 62 | text: "Profile", 63 | icon: , 64 | path: "/profile", 65 | onClick: () => navigate("/profile"), 66 | roles: ["REGULAR", "PREMIUM", "PREPLUS", "ADMIN"], 67 | }, 68 | ]; 69 | 70 | // Filter options based on user role 71 | const filteredOptions = drawerOptions.filter((option) => 72 | option.roles.includes(user?.role) 73 | ); 74 | 75 | return ( 76 | 90 | 99 | 100 | 101 | 102 | 103 | {user?.firstName} {user?.lastName} 104 | 105 | 106 | 107 | 108 | {filteredOptions.map((option, index) => ( 109 | 125 | {option.icon} 126 | 127 | 128 | ))} 129 | 130 | 131 | 132 | 145 | } 147 | onClick={handleLogout} 148 | buttonText="Log Out" 149 | /> 150 | 151 | 152 | ); 153 | }; 154 | 155 | export default OptionDrawer; 156 | -------------------------------------------------------------------------------- /client/src/components/PasswordValidator.jsx: -------------------------------------------------------------------------------- 1 | import { List, ListItem, ListItemIcon, ListItemText } from "@mui/material"; 2 | import React from "react"; 3 | import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; 4 | import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; 5 | 6 | const PasswordValidator = ({ password }) => { 7 | const isLengthValid = password.length >= 8 && password.length <= 16; 8 | const containsDigit = /\d/.test(password); 9 | const containsLetter = /[A-Za-z]/.test(password); 10 | return ( 11 | <> 12 | 13 | 14 | 15 | {isLengthValid ? ( 16 | 17 | ) : ( 18 | 19 | )} 20 | 21 | 30 | 31 | 32 | 33 | {containsDigit ? ( 34 | 35 | ) : ( 36 | 37 | )} 38 | 39 | 48 | 49 | 50 | 51 | {containsLetter ? ( 52 | 53 | ) : ( 54 | 55 | )} 56 | 57 | 66 | 67 | 68 | 69 | ); 70 | }; 71 | 72 | export default PasswordValidator; 73 | -------------------------------------------------------------------------------- /client/src/components/PrivateRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Outlet, Navigate } from "react-router-dom"; 3 | import { jwtDecode } from "jwt-decode"; 4 | import { toast } from "react-toastify"; 5 | import { useUser } from "../context/userContext"; 6 | 7 | const PrivateRoute = () => { 8 | const { state } = useUser(); 9 | const { token } = state; 10 | 11 | const isTokenValid = (token) => { 12 | try { 13 | if (!token) { 14 | toast.error("Not authenticated. Please login again."); 15 | return false; 16 | } 17 | 18 | const { exp } = jwtDecode(token); 19 | 20 | if (!exp) { 21 | toast.error("Invalid authentication. Please login again."); 22 | return false; 23 | } 24 | 25 | if (Date.now() > exp * 1000) { 26 | toast.error("Your authentication is expired. Please login again."); 27 | return false; 28 | } 29 | 30 | return true; 31 | } catch (error) { 32 | toast.error("Token is invalid. Please login again."); 33 | return false; 34 | } 35 | }; 36 | 37 | return isTokenValid(token) ? : ; 38 | }; 39 | 40 | export default PrivateRoute; 41 | -------------------------------------------------------------------------------- /client/src/components/RandomQuote.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import quotes from "../general/quote"; 3 | import { Typography, Box } from "@mui/material"; 4 | 5 | const RandomQuote = () => { 6 | const [selectedQuote, setSelectedQuote] = useState(null); 7 | 8 | useEffect(() => { 9 | const randomQuote = quotes[Math.floor(Math.random() * quotes.length)]; 10 | setSelectedQuote(randomQuote); 11 | }, []); 12 | 13 | if (!selectedQuote) return null; 14 | 15 | return ( 16 | 25 | 26 | "{selectedQuote.text}" 27 | 28 | 29 | ― {selectedQuote.author} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default RandomQuote; 36 | -------------------------------------------------------------------------------- /client/src/components/ServiceCard.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "@emotion/styled"; 3 | import { Box, Typography, Button } from "@mui/material"; 4 | import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; 5 | import { Link } from "react-router-dom"; 6 | 7 | const ServiceCard = ({ title, text, imageSrc }) => { 8 | const ServicesCard = styled(Box)(({ theme }) => ({ 9 | margin: "1rem", 10 | height: "525px", 11 | width: "400px", 12 | borderRadius: "4px", 13 | backgroundImage: `linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(17,17,17,0.6) 100%), url(${imageSrc})`, 14 | backgroundSize: "cover", 15 | position: "relative", 16 | color: "#fff", 17 | "&:hover": { 18 | transform: "scale(1.075)", 19 | transition: "0.2s ease-in", 20 | }, 21 | [theme.breakpoints.down("sm")]: { width: "300px" }, 22 | })); 23 | 24 | const ServiceButton = styled(Button)(({ theme }) => ({ 25 | color: "#fff", 26 | padding: "10px 20px", 27 | border: "none", 28 | outline: "none", 29 | borderRadius: "4px", 30 | background: "#f77062", 31 | position: "absolute", 32 | top: "440px", 33 | left: "30px", 34 | fontSize: "1rem", 35 | cursor: "pointer", 36 | "&:hover": { 37 | background: "linear-gradient(to top, #8e2de2 0%, #4a00e0 100%)", 38 | }, 39 | })); 40 | 41 | return ( 42 | 43 | 52 | {title} 53 | 54 | 66 | {text} 67 | 68 | } 72 | > 73 | Get Started 74 | 75 | 76 | ); 77 | }; 78 | 79 | export default ServiceCard; 80 | -------------------------------------------------------------------------------- /client/src/components/Stopwatch.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { IconButton, Box, Typography, Tooltip } from "@mui/material"; 3 | import PauseIcon from "@mui/icons-material/Pause"; 4 | import PlayArrowIcon from "@mui/icons-material/PlayArrow"; 5 | import ReplayIcon from "@mui/icons-material/Replay"; 6 | import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline"; 7 | import { GenericDialog } from "./generic/GenericDialog"; 8 | 9 | const MAX_TIME = 59999; 10 | 11 | const Stopwatch = ({ onTimeSubmit }) => { 12 | const [time, setTime] = useState(0); 13 | const [isRunning, setIsRunning] = useState(true); 14 | const [openFinishDialog, setOpenFinishDialog] = useState(false); 15 | const [openResetDialog, setOpenResetDialog] = useState(false); 16 | const [startTime, setStartTime] = useState(Date.now()); 17 | 18 | const [pausedTime, setPausedTime] = useState(0); 19 | 20 | useEffect(() => { 21 | let interval = null; 22 | let lastSecond = 0; 23 | 24 | if (isRunning) { 25 | interval = setInterval(() => { 26 | const now = Date.now(); 27 | const deltaTime = now - startTime; 28 | const newTime = Math.floor(deltaTime / 1000); 29 | 30 | if (newTime > lastSecond) { 31 | lastSecond = newTime; 32 | if (newTime >= MAX_TIME) { 33 | setTime(MAX_TIME); 34 | setIsRunning(false); 35 | clearInterval(interval); 36 | } else { 37 | setTime(newTime); 38 | } 39 | } 40 | }, 100); 41 | } else { 42 | clearInterval(interval); 43 | } 44 | 45 | return () => clearInterval(interval); 46 | }, [isRunning, startTime]); 47 | 48 | const formatTime = () => { 49 | const minutes = Math.floor(time / 60); 50 | const seconds = time % 60; 51 | return `${minutes < 10 ? "0" : ""}${minutes}:${ 52 | seconds < 10 ? "0" : "" 53 | }${seconds}`; 54 | }; 55 | 56 | const handlePauseResume = () => { 57 | if (isRunning) { 58 | setIsRunning(false); 59 | setPausedTime(Date.now() - startTime); 60 | } else { 61 | setStartTime(Date.now() - pausedTime); 62 | setIsRunning(true); 63 | setPausedTime(0); 64 | } 65 | }; 66 | 67 | const handleFinish = () => { 68 | setIsRunning(false); 69 | setPausedTime(Date.now() - startTime); 70 | setOpenFinishDialog(true); 71 | }; 72 | 73 | const handleConfirmTime = () => { 74 | onTimeSubmit(formatTime()); 75 | setOpenFinishDialog(false); 76 | }; 77 | 78 | const handleReset = () => { 79 | setTime(0); 80 | setStartTime(Date.now()); 81 | setIsRunning(true); 82 | setOpenResetDialog(false); 83 | }; 84 | 85 | return ( 86 | <> 87 | 99 | 106 | 110 | Time Spent: {formatTime()} 111 | 112 | 113 | 114 | {isRunning ? : } 115 | 116 | 117 | 118 | setOpenResetDialog(true)} 121 | > 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | setOpenFinishDialog(false)} 135 | onConfirm={handleConfirmTime} 136 | title="Time of Completion" 137 | content="Do you want to pass this time to the Time of Completion field?" 138 | /> 139 | setOpenResetDialog(false)} 142 | onConfirm={handleReset} 143 | title="Reset Recording" 144 | content="Do you want to reset time recording?" 145 | /> 146 | 147 | ); 148 | }; 149 | 150 | export default Stopwatch; 151 | -------------------------------------------------------------------------------- /client/src/components/UserBadge.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "@mui/material"; 3 | 4 | import regularBadgeGif from "../images/regular.gif"; 5 | import premiumBadgeGif from "../images/premium.gif"; 6 | import premiumPlusBadgeGif from "../images/premiumplus.gif"; 7 | import adminBadgeGif from "../images/admin.gif"; 8 | 9 | const badgeStyle = { 10 | width: 30, 11 | height: 30, 12 | display: "flex", 13 | alignItems: "center", 14 | justifyContent: "center", 15 | }; 16 | 17 | const badgeImages = { 18 | REGULAR: regularBadgeGif, 19 | PREMIUM: premiumBadgeGif, 20 | PREPLUS: premiumPlusBadgeGif, 21 | ADMIN: adminBadgeGif, 22 | }; 23 | 24 | const UserBadge = ({ tier }) => { 25 | const iconSrc = badgeImages[tier] || regularBadgeGif; 26 | 27 | return ( 28 | 29 | {`${tier 34 | 35 | ); 36 | }; 37 | 38 | export default UserBadge; 39 | -------------------------------------------------------------------------------- /client/src/components/algorithm_page_components/AlgorithmHeader.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Box, IconButton, Typography } from "@mui/material"; 3 | import { useNavigate } from "react-router-dom"; 4 | import ArrowBackIcon from "@mui/icons-material/ArrowBack"; 5 | import { GreyBackgroundDialog } from "../generic/GenericDialog"; 6 | 7 | const AlgorithmHeader = ({ isDataEntered, algorithm }) => { 8 | const [dialogOpen, setDialogOpen] = useState(false); 9 | const navigate = useNavigate(); 10 | 11 | const closeDialog = () => { 12 | setDialogOpen(false); 13 | navigate(-1); 14 | }; 15 | 16 | const handleBack = () => { 17 | if (isDataEntered) { 18 | setDialogOpen(true); 19 | } else { 20 | navigate(-1); 21 | } 22 | }; 23 | 24 | return ( 25 | 33 | 34 | 35 | 36 | {algorithm === undefined ? ( 37 | 38 | New Algorithm 39 | 40 | ) : ( 41 | 42 | {algorithm.title} 43 | 44 | )} 45 | 46 | 47 | setDialogOpen(false)} 50 | onConfirm={closeDialog} 51 | title="Are you sure?" 52 | content="All of the data will be lost." 53 | /> 54 | 55 | ); 56 | }; 57 | 58 | export default AlgorithmHeader; 59 | -------------------------------------------------------------------------------- /client/src/components/algorithm_page_components/AlgorithmTextField.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TextField } from "@mui/material"; 3 | 4 | const AlgorithmTextField = ({ 5 | label, 6 | value, 7 | onChange, 8 | multiline, 9 | required, 10 | }) => { 11 | return ( 12 | 43 | ); 44 | }; 45 | 46 | export default AlgorithmTextField; 47 | -------------------------------------------------------------------------------- /client/src/components/algorithm_page_components/NewAlgorithmDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | FormControl, 8 | InputLabel, 9 | MenuItem, 10 | Select, 11 | Typography, 12 | } from "@mui/material"; 13 | import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; 14 | import { grey } from "@mui/material/colors"; 15 | import AlgorithmTextField from "./AlgorithmTextField"; 16 | import { GreyBackgroundButton } from "../generic/GenericButton"; 17 | 18 | const NewAlgorithmDialog = ({ 19 | open, 20 | onClose, 21 | predefinedSections, 22 | sections, 23 | selectedSection, 24 | setSelectedSection, 25 | customSectionName, 26 | setCustomSectionName, 27 | customSectionContent, 28 | setCustomSectionContent, 29 | insertAfterSection, 30 | setInsertAfterSection, 31 | handleSectionAdd, 32 | formControlStyles, 33 | }) => { 34 | return ( 35 | 36 | Select a Section to Add 37 | 38 | 44 | New Section 45 | 65 | 66 | 67 | {selectedSection === "custom" && ( 68 | <> 69 | setCustomSectionName(e.target.value)} 74 | /> 75 | setCustomSectionContent(e.target.value)} 81 | /> 82 | 83 | )} 84 | 85 | Insert after 86 | 87 | 93 | Existing Section 94 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ); 118 | }; 119 | 120 | const dialogStyles = { 121 | "& .MuiDialog-paper": { 122 | backgroundColor: grey[800], 123 | color: "#fff", 124 | width: "60%", 125 | maxWidth: "60%", 126 | }, 127 | "& .MuiDialogContentText-root, & .MuiDialogTitle-root": { 128 | color: "#fff", 129 | }, 130 | }; 131 | 132 | export default NewAlgorithmDialog; 133 | -------------------------------------------------------------------------------- /client/src/components/dashboard_page/AlgorithmStats.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, IconButton, Typography } from "@mui/material"; 3 | import { useSpring, animated } from "react-spring"; 4 | import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; 5 | import { useNavigate } from "react-router-dom"; 6 | 7 | const AlgorithmStats = ({ userId }) => { 8 | const navigate = useNavigate(); 9 | 10 | const props = useSpring({ 11 | opacity: 1, 12 | from: { opacity: 0 }, 13 | border: "1px solid #ccc", 14 | borderRadius: "8px", 15 | padding: "16px", 16 | textAlign: "center", 17 | boxSizing: "border-box", 18 | margin: "10px", 19 | delay: 1800, 20 | width: "48%", 21 | }); 22 | 23 | return ( 24 | 25 | 33 | 34 | Algorithm Stats 35 | 36 | navigate("/algorithm")} 46 | > 47 | 48 | 49 | 50 | 51 | 52 | Total Algorithm You Have Recorded:{" "} 53 | 62 | N/A 63 | 64 | 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default AlgorithmStats; 71 | -------------------------------------------------------------------------------- /client/src/components/dashboard_page/DataStructureStats.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Box, IconButton, Typography } from "@mui/material"; 3 | import { useSpring, animated } from "react-spring"; 4 | import { axiosInstance } from "../../config/axiosConfig"; 5 | import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | const DataStructureStats = ({ userId }) => { 9 | const [dataStructureNumber, setDataStructureNumber] = useState(0); 10 | const navigate = useNavigate(); 11 | 12 | useEffect(() => { 13 | const fetchData = async () => { 14 | try { 15 | const response = await axiosInstance.get( 16 | `data-structure/count-data-structure/${userId}` 17 | ); 18 | setDataStructureNumber(response.data.data); 19 | } catch (err) { 20 | console.log("Error while fetching the question stats: " + err); 21 | } 22 | }; 23 | fetchData(); 24 | }, [userId]); 25 | 26 | const props = useSpring({ 27 | opacity: 1, 28 | from: { opacity: 0 }, 29 | border: "1px solid #ccc", 30 | borderRadius: "8px", 31 | padding: "16px", 32 | textAlign: "center", 33 | boxSizing: "border-box", 34 | margin: "10px", 35 | delay: 1500, 36 | width: "48%", 37 | }); 38 | 39 | return ( 40 | 41 | 49 | 50 | Data Structure Stats 51 | 52 | navigate("/data-structure")} 62 | > 63 | 64 | 65 | 66 | 67 | 68 | Total Data Structure You Have Recorded:{" "} 69 | 78 | {dataStructureNumber} 79 | 80 | 81 | 82 | 83 | ); 84 | }; 85 | 86 | export default DataStructureStats; 87 | -------------------------------------------------------------------------------- /client/src/components/dashboard_page/Welcome.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box, Typography } from "@mui/material"; 3 | import { useSpring, animated } from "react-spring"; 4 | import dayjs from "dayjs"; 5 | 6 | const Welcome = ({ user }) => { 7 | const props = useSpring({ 8 | opacity: 1, 9 | from: { opacity: 0 }, 10 | marginTop: "5rem", 11 | delay: 500, 12 | }); 13 | 14 | // Calculate the number of days since the user joined 15 | const today = dayjs(); 16 | const userCreatedAt = user?.createdAt ? dayjs(user.createdAt) : null; 17 | const daysWithUs = userCreatedAt ? today.diff(userCreatedAt, "day") : 0; 18 | 19 | // Animated number for daysWithUs 20 | const daysWithUsProps = useSpring({ 21 | number: daysWithUs, 22 | from: { number: 0 }, 23 | }); 24 | 25 | return ( 26 | 37 | 38 | 46 | Welcome, {user?.firstName} {user?.lastName}! 47 | 48 | {userCreatedAt && ( 49 | 50 | You have been with us for{" "} 51 | 60 | {daysWithUsProps.number.to((n) => Math.floor(n))} 61 | {" "} 62 | days. 63 | 64 | )} 65 | 66 | 67 | ); 68 | }; 69 | 70 | export default Welcome; 71 | -------------------------------------------------------------------------------- /client/src/components/data_structure_page/DataStructureDialogs.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Dialog, 4 | DialogContent, 5 | DialogTitle, 6 | TextField, 7 | Typography, 8 | DialogActions, 9 | DialogContentText, 10 | } from "@mui/material"; 11 | import { grey } from "@mui/material/colors"; 12 | import { LightGreyBackgroundButton } from "../generic/GenericButton"; 13 | 14 | export const ActionDialog = ({ 15 | open, 16 | onClose, 17 | actionType, 18 | structureName, 19 | newName, 20 | setNewName, 21 | onSubmit, 22 | }) => { 23 | const dialogTitle = `${actionType} ${structureName}`; 24 | const inputLabel = `Name of ${structureName}`; 25 | 26 | return ( 27 | 39 | {dialogTitle} 40 | 41 | {(actionType === "Add" || actionType === "Rename") && ( 42 | setNewName(e.target.value)} 52 | InputLabelProps={{ 53 | style: { color: grey[900] }, 54 | }} 55 | inputProps={{ 56 | style: { color: grey[900] }, 57 | }} 58 | /> 59 | )} 60 | {actionType === "Delete" && ( 61 | Caution! All the data will be deleted. 62 | )} 63 | 64 | 65 | 66 | 70 | 71 | 72 | ); 73 | }; 74 | 75 | export const ContentDialog = ({ 76 | isOpen, 77 | onClose, 78 | title, 79 | content, 80 | onConfirm, 81 | }) => { 82 | return ( 83 | 93 | {title} 94 | 95 | {content} 96 | 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | }; 104 | 105 | export const WarningDialog = ({ 106 | dialogOpen, 107 | onClose, 108 | onCancel, 109 | optionNumber, 110 | title, 111 | text, 112 | }) => { 113 | return ( 114 | 126 | {title} 127 | 128 | {text} 129 | 130 | {optionNumber === 2 ? ( 131 | 132 | 133 | 134 | 135 | ) : ( 136 | 137 | 138 | 139 | )} 140 | 141 | ); 142 | }; 143 | -------------------------------------------------------------------------------- /client/src/components/data_structure_page/EditorWithMenuBar.css: -------------------------------------------------------------------------------- 1 | .tiptap { 2 | background-color: #212121; /* grey[900] */ 3 | color: #fafafa; /* This sets the text color to grey[50] */ 4 | height: 60vh; 5 | overflow-y: auto; /* Allows scrolling within the editor when the content exceeds max-height */ 6 | padding: 8px; /* Optional: To match MUI TextField padding */ 7 | border: 1px solid #ced4da; /* Optional: To match MUI TextField border */ 8 | border-radius: 4px; /* Optional: To match MUI TextField border radius */ 9 | } 10 | 11 | .tiptap:focus { 12 | outline: 2px solid #757575; /* A darker shade for better visibility on grey[800] */ 13 | box-shadow: none; 14 | } 15 | 16 | .tiptap ul, 17 | .tiptap ol { 18 | padding: 0 1rem; 19 | } 20 | 21 | .tiptap h1, 22 | .tiptap h2, 23 | .tiptap h3, 24 | .tiptap h4, 25 | .tiptap h5, 26 | .tiptap h6 { 27 | line-height: 1.1; 28 | } 29 | 30 | .tiptap code { 31 | background-color: #fff; 32 | color: black; 33 | padding: 2px 4px; /* Add padding to the code element */ 34 | } 35 | 36 | .tiptap pre { 37 | background: #0d0d0d; 38 | color: #fff; 39 | font-family: "JetBrainsMono", monospace; 40 | padding: 0.75rem 1rem; 41 | border-radius: 0.5rem; 42 | } 43 | 44 | .tiptap pre code { 45 | color: inherit; 46 | padding: 0; 47 | background: none; 48 | font-size: 0.8rem; 49 | } 50 | 51 | .tiptap img { 52 | width: 100%; 53 | max-height: 250px; 54 | object-fit: contain; 55 | } 56 | 57 | .tiptap img.ProseMirror-selectednode { 58 | outline: 1px solid #ced4da; 59 | } 60 | 61 | .tiptap blockquote { 62 | padding-left: 1rem; 63 | border-left: 2px solid #eeeeee; 64 | } 65 | 66 | .tiptap hr { 67 | border: none; 68 | border-top: 2px solid #ced4da; 69 | margin: 2rem 0; 70 | } 71 | -------------------------------------------------------------------------------- /client/src/components/data_structure_page/EditorWithMenuBar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./EditorWithMenuBar.css"; 3 | import { Color } from "@tiptap/extension-color"; 4 | import ListItem from "@tiptap/extension-list-item"; 5 | import TextStyle from "@tiptap/extension-text-style"; 6 | import { EditorProvider } from "@tiptap/react"; 7 | import StarterKit from "@tiptap/starter-kit"; 8 | import { Image } from "@tiptap/extension-image"; 9 | import MenuBar from "../MenuBar"; 10 | 11 | const extensions = [ 12 | Color.configure({ types: [TextStyle.name, ListItem.name] }), 13 | TextStyle.configure({ types: [ListItem.name] }), 14 | StarterKit, 15 | Image, 16 | ]; 17 | 18 | const EditorWithMenuBar = ({ 19 | onClose, 20 | selectedNode, 21 | setAddClicked, 22 | selectedStructureId, 23 | safeHtml, 24 | content, 25 | }) => { 26 | return ( 27 | 36 | } 37 | extensions={extensions} 38 | content={safeHtml} 39 | > 40 | ); 41 | }; 42 | 43 | export default EditorWithMenuBar; 44 | -------------------------------------------------------------------------------- /client/src/components/generic/GenericFormControl.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import FormControl from "@mui/material/FormControl"; 3 | import InputLabel from "@mui/material/InputLabel"; 4 | import MenuItem from "@mui/material/MenuItem"; 5 | import Select from "@mui/material/Select"; 6 | 7 | function GenericFormControl({ label, value, onChange, options, ...props }) { 8 | return ( 9 | 27 | 28 | {label} 29 | 30 | 44 | 45 | ); 46 | } 47 | 48 | export default GenericFormControl; 49 | -------------------------------------------------------------------------------- /client/src/components/generic/GenericSearchBox.jsx: -------------------------------------------------------------------------------- 1 | import { Box, TextField, IconButton, InputAdornment } from "@mui/material"; 2 | import SearchIcon from "@mui/icons-material/Search"; 3 | import { useState } from "react"; 4 | 5 | const GenericSearchBox = ({ label, onSearch }) => { 6 | const [inputValue, setInputValue] = useState(""); 7 | 8 | const handleInputChange = (event) => { 9 | setInputValue(event.target.value); 10 | }; 11 | 12 | const handleKeyPress = (event) => { 13 | if (event.key === "Enter") { 14 | onSearch(inputValue); 15 | } 16 | }; 17 | 18 | const handleSearchClick = () => { 19 | onSearch(inputValue); 20 | }; 21 | 22 | return ( 23 | 24 | 34 | 35 | 36 | 37 | 38 | ), 39 | }} 40 | /> 41 | 42 | ); 43 | }; 44 | 45 | export default GenericSearchBox; 46 | -------------------------------------------------------------------------------- /client/src/components/generic/GenericSpinner.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CircularProgress from "@mui/material/CircularProgress"; 3 | import Box from "@mui/material/Box"; 4 | 5 | const GenericSpinner = ({ color }) => { 6 | return ( 7 | 26 | 33 | 34 | ); 35 | }; 36 | 37 | export default GenericSpinner; 38 | -------------------------------------------------------------------------------- /client/src/components/generic/GenricTextField.jsx: -------------------------------------------------------------------------------- 1 | import { TextField } from "@mui/material"; 2 | 3 | const GenericTextField = ({ 4 | label, 5 | name, 6 | value, 7 | onChange, 8 | inputLabelColor, 9 | inputColor, 10 | }) => { 11 | return ( 12 | 29 | ); 30 | }; 31 | 32 | export default GenericTextField; 33 | -------------------------------------------------------------------------------- /client/src/components/navbar/AccountNavbar.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import AppBar from "@mui/material/AppBar"; 4 | import Toolbar from "@mui/material/Toolbar"; 5 | import Typography from "@mui/material/Typography"; 6 | import FlutterDashIcon from "@mui/icons-material/FlutterDash"; 7 | 8 | const AccountNavbar = () => { 9 | return ( 10 | 11 | 12 | 13 | 24 | YLSLC 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default AccountNavbar; 32 | -------------------------------------------------------------------------------- /client/src/components/navbar/AuthenticatedNavbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Link, useNavigate, useLocation } from "react-router-dom"; 3 | import { AppBar, Toolbar, Typography, Box, IconButton } from "@mui/material"; 4 | import FlutterDashIcon from "@mui/icons-material/FlutterDash"; 5 | import ListIcon from "@mui/icons-material/List"; 6 | import OptionDrawer from "../OptionDrawer"; 7 | import { useUser } from "../../context/userContext"; 8 | import { UserHooks } from "../../hooks/userHooks/UserHooks"; 9 | 10 | const AuthenticatedNavbar = () => { 11 | const { state } = useUser(); 12 | const { user, token } = state; 13 | const { getCurrentUser } = UserHooks(); 14 | const [isDrawerOpen, setIsDrawerOpen] = useState(false); 15 | const { logout } = UserHooks(); 16 | const location = useLocation(); 17 | 18 | const navigate = useNavigate(); 19 | 20 | useEffect(() => { 21 | getCurrentUser(token).catch((error) => { 22 | alert("User credential expired. Please login again."); 23 | logout(); 24 | navigate("/"); 25 | }); 26 | }, [token]); // eslint-disable-line react-hooks/exhaustive-deps 27 | 28 | const handleLogout = () => { 29 | console.log("Logging out..."); 30 | logout(); 31 | navigate("/"); 32 | }; 33 | 34 | const getTimeOfDayGreeting = () => { 35 | const hour = new Date().getHours(); 36 | if (hour < 6 || hour > 23) return "Please go to sleep, "; 37 | if (hour < 12) return "Good Morning, "; 38 | if (hour < 18) return "Good Afternoon, "; 39 | return "Good Evening, "; 40 | }; 41 | 42 | const toggleDrawer = (open) => (event) => { 43 | if ( 44 | event.type === "keydown" && 45 | (event.key === "Tab" || event.key === "Shift") 46 | ) { 47 | return; 48 | } 49 | setIsDrawerOpen(open); 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 64 | 65 | 75 | YLSLC 76 | 77 | 78 | 79 | 88 | 89 | {getTimeOfDayGreeting()} {user.firstName} {user.lastName} 90 | 91 | 100 | 101 | 102 | 103 | 110 | 111 | 112 | ); 113 | }; 114 | 115 | export default AuthenticatedNavbar; 116 | -------------------------------------------------------------------------------- /client/src/components/navbar/HomeNavbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Link } from "react-router-dom"; 3 | import AppBar from "@mui/material/AppBar"; 4 | import Toolbar from "@mui/material/Toolbar"; 5 | import Typography from "@mui/material/Typography"; 6 | import FlutterDashIcon from "@mui/icons-material/FlutterDash"; 7 | import { grey } from "@mui/material/colors"; 8 | import { 9 | Box, 10 | useMediaQuery, 11 | IconButton, 12 | Drawer, 13 | List, 14 | ListItem, 15 | ListItemText, 16 | useTheme, 17 | } from "@mui/material"; 18 | import MenuIcon from "@mui/icons-material/Menu"; 19 | import { BlackBackgroundButton } from "../generic/GenericButton"; 20 | 21 | const HomeNavbar = () => { 22 | const theme = useTheme(); 23 | const isMobile = useMediaQuery(theme.breakpoints.down("sm")); 24 | const [drawerOpen, setDrawerOpen] = useState(false); 25 | 26 | const toggleDrawer = (newOpen) => () => { 27 | setDrawerOpen(newOpen); 28 | }; 29 | 30 | const list = () => ( 31 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | 48 | return ( 49 | 50 | 51 | 52 | 63 | YLSLC 64 | 65 | 66 | {isMobile ? ( 67 | <> 68 | 73 | 74 | 75 | 85 | {list()} 86 | 87 | 88 | ) : ( 89 | 90 | 95 | 100 | 101 | )} 102 | 103 | 104 | ); 105 | }; 106 | 107 | export default HomeNavbar; 108 | -------------------------------------------------------------------------------- /client/src/components/new_question/NewQuestionFooter.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Box } from "@mui/material"; 3 | import { WhiteBackgroundButton } from "../generic/GenericButton"; 4 | import AddIcon from "@mui/icons-material/Add"; 5 | 6 | const NewQuestionFooter = ({ onClick }) => { 7 | return ( 8 | 16 | } 20 | /> 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default NewQuestionFooter; 27 | -------------------------------------------------------------------------------- /client/src/components/new_question/SuccessToggle.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import confetti from "canvas-confetti"; 3 | import { useSpring, animated } from "react-spring"; 4 | import { WhiteBackgroundButton } from "../generic/GenericButton"; 5 | 6 | const SuccessAnimationToggle = ({ onChange, success }) => { 7 | const [visibility, setVisibility] = useState("hidden"); // 'hidden', 'fadeIn', or 'fadeOut' 8 | const [selected, setSelected] = useState(success); 9 | 10 | const messageAnimation = useSpring({ 11 | opacity: visibility === "fadeIn" ? 1 : 0, 12 | config: { duration: 1000 }, 13 | onRest: () => { 14 | if (visibility === "fadeIn") { 15 | setTimeout(() => setVisibility("fadeOut"), 1000); 16 | } 17 | if (visibility === "fadeOut") { 18 | setVisibility("hidden"); 19 | } 20 | }, 21 | }); 22 | 23 | const handleSuccessChange = (newSuccess) => { 24 | onChange(newSuccess); 25 | setSelected(newSuccess ? true : false); 26 | if (newSuccess) { 27 | makeCelebration(); 28 | setVisibility("hidden"); 29 | } else { 30 | setVisibility("fadeIn"); 31 | } 32 | }; 33 | 34 | const makeCelebration = () => { 35 | confetti({ 36 | particleCount: 100, 37 | spread: 100, 38 | origin: { y: 0.8 }, 39 | }); 40 | }; 41 | 42 | return ( 43 |
44 |
45 | handleSuccessChange(true)} 47 | buttonText="Yes" 48 | selected={selected === true} 49 | /> 50 |
51 |
52 | handleSuccessChange(false)} 54 | buttonText="No" 55 | selected={selected === false} 56 | /> 57 |
58 | {visibility !== "hidden" && ( 59 | 79 |
😢
80 |
Try Better !
81 |
82 | )} 83 |
84 | ); 85 | }; 86 | 87 | export default SuccessAnimationToggle; 88 | -------------------------------------------------------------------------------- /client/src/components/routes/PremiumPlusRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useUser } from "../../context/userContext"; 3 | import { Outlet } from "react-router-dom"; 4 | import NotFoundPage from "../../pages/NotFoundPage"; 5 | 6 | const PremiumPlusRoute = () => { 7 | const { state } = useUser(); 8 | const { user } = state; 9 | const role = user?.role; 10 | 11 | if (role === "PREPLUS" || role === "ADMIN") { 12 | return ; 13 | } else { 14 | return ; 15 | } 16 | }; 17 | 18 | export default PremiumPlusRoute; 19 | -------------------------------------------------------------------------------- /client/src/components/routes/PremiumRoute.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useUser } from "../../context/userContext"; 3 | import NotFoundPage from "../../pages/NotFoundPage"; 4 | import { Outlet } from "react-router-dom"; 5 | 6 | const PremiumRoute = () => { 7 | const { state } = useUser(); 8 | const { user } = state; 9 | const role = user?.role; 10 | 11 | if (role === "PREMIUM" || role === "PREPLUS" || role === "ADMIN") { 12 | return ; 13 | } else { 14 | return ; 15 | } 16 | }; 17 | 18 | export default PremiumRoute; 19 | -------------------------------------------------------------------------------- /client/src/config/axiosConfig.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const token = JSON.parse(localStorage.getItem("user")); 4 | const API_ENDPOINT = process.env.REACT_APP_API_ENDPOINT; 5 | 6 | const axiosInstanceNoAuth = axios.create({ 7 | baseURL: API_ENDPOINT, 8 | }); 9 | 10 | const axiosInstance = axios.create({ 11 | baseURL: API_ENDPOINT, 12 | headers: { 13 | Authorization: `Bearer ${token}`, 14 | }, 15 | }); 16 | 17 | axiosInstance.interceptors.request.use( 18 | (config) => { 19 | const token = JSON.parse(localStorage.getItem("user")); 20 | if (token) { 21 | config.headers.Authorization = `Bearer ${token}`; 22 | } 23 | return config; 24 | }, 25 | (error) => { 26 | return Promise.reject(error); 27 | } 28 | ); 29 | 30 | export { axiosInstance, axiosInstanceNoAuth }; 31 | -------------------------------------------------------------------------------- /client/src/context/AlgorithmContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useReducer } from "react"; 2 | import { algorithmReducer, initialState } from "../reducer/algorithmReducer"; 3 | 4 | const AlgorithmContext = createContext(); 5 | 6 | export const useAlgorithm = () => useContext(AlgorithmContext); 7 | 8 | export const AlgorithmProvider = ({ children }) => { 9 | const [state, dispatch] = useReducer(algorithmReducer, initialState); 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /client/src/context/dataStructureContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useReducer } from "react"; 2 | import { 3 | dataStructureReducer, 4 | initialState, 5 | } from "../reducer/dataStructureReducer"; 6 | 7 | const DataStructureContext = createContext(); 8 | 9 | export const useDataStructure = () => useContext(DataStructureContext); 10 | 11 | export const DataStructureProvider = ({ children }) => { 12 | const [state, dispatch] = useReducer(dataStructureReducer, initialState); 13 | 14 | return ( 15 | 16 | {children} 17 | 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /client/src/context/userContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useReducer } from "react"; 2 | import { userReducer, initialState } from "../reducer/userReducer"; 3 | 4 | const UserContext = createContext(); 5 | 6 | export const useUser = () => useContext(UserContext); 7 | 8 | export const UserProvider = ({ children }) => { 9 | const [state, dispatch] = useReducer(userReducer, initialState); 10 | 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /client/src/hooks/ContentHooks.js: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "../config/axiosConfig"; 2 | import { useDataStructure } from "../context/dataStructureContext"; 3 | import { actionTypes } from "../reducer/dataStructureActions"; 4 | 5 | export const ContentHooks = () => { 6 | const { dispatch } = useDataStructure(); 7 | 8 | async function convertBlobUrlToFile(blobUrl) { 9 | const response = await fetch(blobUrl); 10 | const blob = await response.blob(); 11 | return new File([blob], "image.jpg", { type: "image/jpeg" }); 12 | } 13 | 14 | const uploadImageToBackend = async (imageFile, nodeId) => { 15 | const fileData = new FormData(); 16 | fileData.append("image", imageFile); 17 | fileData.append("nodeId", nodeId); 18 | try { 19 | const response = await axiosInstance.post("node/upload-image", fileData, { 20 | headers: { 21 | "Content-Type": "multipart/form-data", 22 | }, 23 | }); 24 | if (response.data.serverMessage === "SUCCESS") { 25 | return response.data.data; 26 | } else { 27 | console.log("Image upload failed for content"); 28 | } 29 | } catch (error) { 30 | console.log("Error while uploading content image: " + error); 31 | } 32 | }; 33 | 34 | const deleteImage = async (nodeId, imageId) => { 35 | await axiosInstance.delete(`node/image/${nodeId}/${imageId}`); 36 | }; 37 | 38 | const handleSave = async ( 39 | nodeId, 40 | stringContent, 41 | dataStructureId, 42 | imageSrcs 43 | ) => { 44 | dispatch({ type: actionTypes.PROCESS_START }); 45 | try { 46 | const response = await axiosInstance.patch(`node/content/${nodeId}`, { 47 | content: stringContent, 48 | }); 49 | 50 | // Efficiently delete images and handle potential errors 51 | await Promise.all( 52 | imageSrcs.map((imageId) => 53 | deleteImage(nodeId, imageId).catch((error) => { 54 | console.error(`Failed to delete image ${imageId}:`, error); 55 | // Optionally handle this error in a more user-friendly way 56 | }) 57 | ) 58 | ); 59 | 60 | dispatch({ 61 | type: actionTypes.UPDATE_CONTENT, 62 | payload: { 63 | dataStructureId, 64 | node: response.data.data, 65 | }, 66 | }); 67 | } catch (error) { 68 | dispatch({ type: actionTypes.PROCESS_FAILURE, error: error }); 69 | console.error("Failed to save content: ", error); 70 | // Optionally, update the UI to inform the user of the failure 71 | } 72 | }; 73 | 74 | return { 75 | handleSave, 76 | convertBlobUrlToFile, 77 | uploadImageToBackend, 78 | }; 79 | }; 80 | -------------------------------------------------------------------------------- /client/src/hooks/DataStructureHooks.js: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "../config/axiosConfig"; 2 | import { useDataStructure } from "../context/dataStructureContext"; 3 | import { actionTypes } from "../reducer/dataStructureActions"; 4 | 5 | export const DataStructureHooks = () => { 6 | const { dispatch } = useDataStructure(); 7 | 8 | const fetchDataStructures = async () => { 9 | dispatch({ type: actionTypes.PROCESS_START }); 10 | try { 11 | const response = await axiosInstance.get("data-structure"); 12 | dispatch({ 13 | type: actionTypes.FETCH_DATA_STRUCTURES_SUCCESS, 14 | payload: response.data.data, 15 | }); 16 | } catch (error) { 17 | dispatch({ 18 | type: actionTypes.PROCESS_FAILURE, 19 | error: error, 20 | }); 21 | console.error("Failed to fetch data structures: ", error); 22 | } 23 | }; 24 | 25 | const addDataStructure = async (name) => { 26 | dispatch({ type: actionTypes.PROCESS_START }); 27 | try { 28 | const response = await axiosInstance.post("data-structure", { 29 | name, 30 | nodes: [], 31 | }); 32 | dispatch({ 33 | type: actionTypes.ADD_DATA_STRUCTURE_SUCCESS, 34 | payload: response.data.data, 35 | }); 36 | } catch (error) { 37 | dispatch({ 38 | type: actionTypes.PROCESS_FAILURE, 39 | error: error, 40 | }); 41 | console.log("Failed to add a new data structure: ", error); 42 | } 43 | }; 44 | 45 | const renameDataStructure = async (id, newName) => { 46 | dispatch({ type: actionTypes.PROCESS_START }); 47 | try { 48 | const response = await axiosInstance.patch(`data-structure/${id}`, { 49 | name: newName, 50 | }); 51 | dispatch({ 52 | type: actionTypes.UPDATE_DATA_STRUCTURE_NAME_SUCCESS, 53 | payload: response.data.data, 54 | }); 55 | } catch (error) { 56 | dispatch({ type: actionTypes.PROCESS_FAILURE, error: error }); 57 | console.log("Failed to rename: ", error); 58 | } 59 | }; 60 | 61 | const deleteDataStructure = async (id) => { 62 | dispatch({ type: actionTypes.PROCESS_START }); 63 | try { 64 | const response = await axiosInstance.delete(`data-structure/${id}`); 65 | dispatch({ 66 | type: actionTypes.DELETE_DATA_STRUCTURE, 67 | payload: response.data.data, 68 | }); 69 | } catch (error) { 70 | dispatch({ type: actionTypes.PROCESS_FAILURE, error: error }); 71 | console.log("Failed to delete the data structure: ", error); 72 | } 73 | }; 74 | 75 | return { 76 | fetchDataStructures, 77 | addDataStructure, 78 | renameDataStructure, 79 | deleteDataStructure, 80 | }; 81 | }; 82 | -------------------------------------------------------------------------------- /client/src/hooks/NodeHooks.js: -------------------------------------------------------------------------------- 1 | import { axiosInstance } from "../config/axiosConfig"; 2 | import { useDataStructure } from "../context/dataStructureContext"; 3 | import { actionTypes } from "../reducer/dataStructureActions"; 4 | 5 | export const NodeHooks = () => { 6 | const { dispatch } = useDataStructure(); 7 | 8 | const addNode = async (selectedStructureId, name) => { 9 | dispatch({ type: actionTypes.PROCESS_START }); 10 | try { 11 | const response = await axiosInstance.post(`node/${selectedStructureId}`, { 12 | name, 13 | contents: [], 14 | }); 15 | dispatch({ 16 | type: actionTypes.ADD_NODE, 17 | payload: { 18 | dataStructureId: selectedStructureId, 19 | node: response.data.data, 20 | }, 21 | }); 22 | } catch (error) { 23 | dispatch({ 24 | type: actionTypes.PROCESS_FAILURE, 25 | error: error, 26 | }); 27 | console.error("Failed to add a new node: ", error); 28 | } 29 | }; 30 | 31 | const renameNode = async (dataStructureId, nodeId, newName) => { 32 | dispatch({ type: actionTypes.PROCESS_START }); 33 | try { 34 | const response = await axiosInstance.patch(`node/${nodeId}`, { 35 | name: newName, 36 | }); 37 | dispatch({ 38 | type: actionTypes.RENAME_NODE, 39 | payload: { 40 | dataStructureId, 41 | node: response.data.data, 42 | }, 43 | }); 44 | } catch (error) { 45 | dispatch({ 46 | type: actionTypes.PROCESS_FAILURE, 47 | error: error, 48 | }); 49 | console.error("Failed to rename the node: ", error); 50 | } 51 | }; 52 | 53 | const deleteNode = async (dataStructureId, nodeId) => { 54 | dispatch({ type: actionTypes.PROCESS_START }); 55 | try { 56 | const response = await axiosInstance.delete(`node/${nodeId}`); 57 | dispatch({ 58 | type: actionTypes.DELETE_NODE, 59 | payload: { 60 | dataStructureId, 61 | node: response.data.data, 62 | }, 63 | }); 64 | } catch (error) { 65 | dispatch({ 66 | type: actionTypes.PROCESS_FAILURE, 67 | error: error, 68 | }); 69 | console.error("Failed to delete the node: ", error); 70 | } 71 | }; 72 | 73 | return { addNode, renameNode, deleteNode }; 74 | }; 75 | -------------------------------------------------------------------------------- /client/src/hooks/userHooks/UserHooks.js: -------------------------------------------------------------------------------- 1 | import { useNavigate } from "react-router-dom"; 2 | import { axiosInstance, axiosInstanceNoAuth } from "../../config/axiosConfig"; 3 | import { useUser } from "../../context/userContext"; 4 | import { userActionTypes } from "../../reducer/userActions"; 5 | 6 | export const UserHooks = () => { 7 | const { dispatch } = useUser(); 8 | 9 | const navigate = useNavigate(); 10 | 11 | const getCurrentUser = async () => { 12 | dispatch({ type: userActionTypes.PROCESS_START }); 13 | try { 14 | const res = await axiosInstance.get("user"); 15 | const user = res.data.data; 16 | dispatch({ 17 | type: userActionTypes.GET_CURRENT_USER, 18 | payload: { user }, 19 | }); 20 | } catch (error) { 21 | dispatch({ 22 | type: userActionTypes.PROCESS_FAILURE, 23 | error: error.message || "Failed to fetch current user", 24 | }); 25 | console.log(error); 26 | } 27 | }; 28 | 29 | const reset = () => { 30 | dispatch({ type: userActionTypes.CLEAR_ERROR }); 31 | }; 32 | 33 | const login = async (formData) => { 34 | dispatch({ type: userActionTypes.PROCESS_START }); 35 | try { 36 | const response = await axiosInstanceNoAuth.post( 37 | "auth/authenticate", 38 | formData 39 | ); 40 | const data = response.data; 41 | if (data.status !== 200) { 42 | dispatch({ 43 | type: userActionTypes.PROCESS_FAILURE, 44 | error: data.message, 45 | }); 46 | return; 47 | } 48 | const token = data.data; 49 | localStorage.setItem("user", JSON.stringify(token)); 50 | const userDetailsResponse = await axiosInstance.get("user"); 51 | const user = userDetailsResponse.data.data; 52 | dispatch({ 53 | type: userActionTypes.SIGN_IN, 54 | payload: { token, user }, 55 | }); 56 | console.log(user); 57 | if (user.role === "REGULAR") { 58 | navigate("/table"); 59 | } else { 60 | navigate("/dashboard"); 61 | } 62 | } catch (error) { 63 | dispatch({ 64 | type: userActionTypes.PROCESS_FAILURE, 65 | error: error.message || "Failed to login", 66 | }); 67 | console.log("Failed to login: " + error.message); 68 | } 69 | }; 70 | 71 | const register = async (formData) => { 72 | dispatch({ type: userActionTypes.PROCESS_START }); 73 | try { 74 | const response = await axiosInstanceNoAuth.post( 75 | "auth/register", 76 | formData 77 | ); 78 | const data = response.data; 79 | 80 | if (data.status !== 200) { 81 | dispatch({ 82 | type: userActionTypes.PROCESS_FAILURE, 83 | error: data.message, 84 | }); 85 | return; 86 | } 87 | const token = data.data; 88 | localStorage.setItem("user", JSON.stringify(token)); 89 | const userDetailsResponse = await axiosInstance.get("user"); 90 | const user = userDetailsResponse.data.data; 91 | dispatch({ 92 | type: userActionTypes.REGISTER, 93 | payload: { token, user }, 94 | }); 95 | navigate("/table"); 96 | } catch (error) { 97 | dispatch({ 98 | type: userActionTypes.PROCESS_FAILURE, 99 | error: error.message || "Failed to register", 100 | }); 101 | console.log("Failed to register: " + error.message); 102 | } 103 | }; 104 | 105 | const logout = () => { 106 | localStorage.removeItem("user"); 107 | dispatch({ type: userActionTypes.LOGOUT }); 108 | }; 109 | 110 | const updateUser = async (id, user) => { 111 | dispatch({ type: userActionTypes.PROCESS_START }); 112 | try { 113 | const res = await axiosInstance.put(`user/${id}`, user); 114 | const updatedUser = res.data.data; 115 | dispatch({ 116 | type: userActionTypes.UPDATE_USER, 117 | payload: { updatedUser }, 118 | }); 119 | } catch (error) { 120 | dispatch({ 121 | type: userActionTypes.PROCESS_FAILURE, 122 | error: error.message || "Failed to update user details", 123 | }); 124 | console.log("Failed to update user detail: " + error.message); 125 | } 126 | }; 127 | 128 | return { 129 | login, 130 | register, 131 | logout, 132 | getCurrentUser, 133 | updateUser, 134 | reset, 135 | }; 136 | }; 137 | -------------------------------------------------------------------------------- /client/src/images/DataStructureImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/DataStructureImage.png -------------------------------------------------------------------------------- /client/src/images/admin.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/admin.gif -------------------------------------------------------------------------------- /client/src/images/codeDetails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/codeDetails.png -------------------------------------------------------------------------------- /client/src/images/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/dashboard.png -------------------------------------------------------------------------------- /client/src/images/future.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/future.jpeg -------------------------------------------------------------------------------- /client/src/images/premium.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/premium.gif -------------------------------------------------------------------------------- /client/src/images/premiumplus.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/premiumplus.gif -------------------------------------------------------------------------------- /client/src/images/regular.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/regular.gif -------------------------------------------------------------------------------- /client/src/images/table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/client/src/images/table.png -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #root { 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | } 9 | * { 10 | font-variant-ligatures: none; 11 | text-rendering: optimizeLegibility; 12 | } 13 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import { createTheme, ThemeProvider } from "@mui/material/styles"; 6 | 7 | const theme = createTheme({ 8 | typography: { 9 | fontFamily: '"Saira Semi Condensed","JetBrains Mono", sans-serif', 10 | fontWeightBold: 600, 11 | allVariants: { 12 | fontWeight: 600, 13 | }, 14 | }, 15 | }); 16 | 17 | const root = ReactDOM.createRoot(document.getElementById("root")); 18 | root.render( 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /client/src/pages/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import { Box, CircularProgress } from "@mui/material"; 2 | import React, { useEffect } from "react"; 3 | import AuthenticatedNavbar from "../components/navbar/AuthenticatedNavbar"; 4 | import Footer from "../components/Footer"; 5 | import { useUser } from "../context/userContext"; 6 | import { UserHooks } from "../hooks/userHooks/UserHooks"; 7 | import Welcome from "../components/dashboard_page/Welcome"; 8 | import LeetCodeStats from "../components/dashboard_page/LeetCodeStats"; 9 | import DataStructureStats from "../components/dashboard_page/DataStructureStats"; 10 | import AlgorithmStats from "../components/dashboard_page/AlgorithmStats"; 11 | 12 | const Dashboard = () => { 13 | const { state } = useUser(); 14 | const { user, token } = state; 15 | const { getCurrentUser } = UserHooks(); 16 | 17 | useEffect(() => { 18 | getCurrentUser(token); 19 | }, []); // eslint-disable-line 20 | 21 | if (Object.keys(user).length === 0) { 22 | return ( 23 | 32 | 33 | 34 | ); 35 | } 36 | return ( 37 | 44 | 45 | 54 | 55 | 56 | 66 | 67 | 75 | 76 | 77 | 78 | 79 | 80 | 81 |