├── .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 |
4 |
5 |
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 |
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 |
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 |
72 | );
73 | };
74 |
75 | export const ContentDialog = ({
76 | isOpen,
77 | onClose,
78 | title,
79 | content,
80 | onConfirm,
81 | }) => {
82 | return (
83 |
102 | );
103 | };
104 |
105 | export const WarningDialog = ({
106 | dialogOpen,
107 | onClose,
108 | onCancel,
109 | optionNumber,
110 | title,
111 | text,
112 | }) => {
113 | return (
114 |
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 |
82 |
83 | );
84 | };
85 |
86 | export default Dashboard;
87 |
--------------------------------------------------------------------------------
/client/src/pages/FriendPage.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import AuthenticatedNavbar from "../components/navbar/AuthenticatedNavbar";
3 |
4 | const FriendPage = () => {
5 | return (
6 |
7 |
8 | Feature of Friend Page coming in hot!
9 |
10 | );
11 | };
12 |
13 | export default FriendPage;
14 |
--------------------------------------------------------------------------------
/client/src/pages/NewAlgorithmPage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Box, Container } from "@mui/material";
3 | import { grey } from "@mui/material/colors";
4 | import AuthenticatedNavbar from "../components/navbar/AuthenticatedNavbar";
5 | import Footer from "../components/Footer";
6 | import AlgorithmForm from "../components/algorithm_page_components/AlgorithmForm";
7 | import AlgorithmHeader from "../components/algorithm_page_components/AlgorithmHeader";
8 | import { useLocation } from "react-router-dom";
9 | import UpdateAlgorithmForm from "../components/algorithm_page_components/UpdateAlgorithmForm";
10 |
11 | const NewAlgorithmPage = () => {
12 | const [isDataEntered, setDataEntered] = useState(false);
13 |
14 | const location = useLocation();
15 | const algorithm = location.state?.algorithm || null;
16 |
17 | return (
18 |
26 |
27 |
38 | {algorithm === null ? (
39 | <>
40 |
41 |
42 | >
43 | ) : (
44 | <>
45 |
49 |
53 | >
54 | )}
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default NewAlgorithmPage;
62 |
--------------------------------------------------------------------------------
/client/src/pages/NewQuestion.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import AuthenticatedNavbar from "../components/navbar/AuthenticatedNavbar";
3 | import NewQuestionForm from "../components/new_question/NewQuestionForm";
4 | import { Box, Typography } from "@mui/material";
5 | import Footer from "../components/Footer";
6 | import { useLocation, useNavigate } from "react-router-dom";
7 | import { WhiteBackgroundButton } from "../components/generic/GenericButton";
8 | import Stopwatch from "../components/Stopwatch";
9 | import { GenericDialog } from "../components/generic/GenericDialog";
10 | import { ArrowBack } from "@mui/icons-material";
11 | import UpdateQuestionForm from "../components/new_question/UpdateQuestionForm";
12 |
13 | const NewQuestion = () => {
14 | const [timeOfCompletion, setTimeOfCompletion] = useState("");
15 | const [dialogOpen, setDialogOpen] = useState(false);
16 |
17 | const location = useLocation();
18 | const withTimer = location.state?.withTimer || false;
19 | const question = location.state?.question || null;
20 | const navigate = useNavigate();
21 |
22 | const handleTimeSubmit = (time) => {
23 | console.log(time);
24 | setTimeOfCompletion(time);
25 | };
26 |
27 | return (
28 |
29 |
30 |
41 |
42 | }
44 | onClick={() => setDialogOpen(true)}
45 | buttonText="Back"
46 | />
47 |
48 |
49 |
54 | {question === null ? "Upload New Question" : "Modify Question"}
55 |
56 |
57 | {withTimer && (
58 |
59 |
60 |
61 | )}
62 |
63 |
64 | {question === null ? (
65 |
66 | ) : (
67 |
68 | )}
69 |
70 |
71 | setDialogOpen(false)}
74 | onConfirm={() => navigate(-1)}
75 | title={question === null ? "Return to Dashboard" : "Return to Question"}
76 | content="Are you sure? All unsaved data will be lost."
77 | />
78 |
79 | );
80 | };
81 |
82 | export default NewQuestion;
83 |
--------------------------------------------------------------------------------
/client/src/pages/NotFoundPage.jsx:
--------------------------------------------------------------------------------
1 | import { Box, Typography } from "@mui/material";
2 | import React from "react";
3 | import AuthenticatedNavbar from "../components/navbar/AuthenticatedNavbar";
4 | import Footer from "../components/Footer";
5 | import { BlackBackgroundButton } from "../components/generic/GenericButton";
6 | import { Link } from "react-router-dom";
7 |
8 | const NotFoundPage = () => {
9 | return (
10 |
17 |
18 |
29 |
30 | Page Not Found!
31 |
32 |
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default NotFoundPage;
40 |
--------------------------------------------------------------------------------
/client/src/pages/ProfilePage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import AuthenticatedNavbar from "../components/navbar/AuthenticatedNavbar";
3 | import { Box, Card, CardContent, Container } from "@mui/material";
4 | import { grey } from "@mui/material/colors";
5 | import { ProfileEdit } from "../components/Profile";
6 | import { ProfileView } from "../components/Profile";
7 | import { GreyBackgroundButton } from "../components/generic/GenericButton";
8 | import { toast } from "react-toastify";
9 | import { useUser } from "../context/userContext";
10 | import { UserHooks } from "../hooks/userHooks/UserHooks";
11 | import Footer from "../components/Footer";
12 |
13 | const ProfilePage = () => {
14 | const { state } = useUser();
15 | const { user } = state;
16 | const [editMode, setEditMode] = useState(false);
17 | const [editedUser, setEditedUser] = useState(null);
18 | const { updateUser } = UserHooks();
19 |
20 | useEffect(() => {
21 | setEditedUser(user);
22 | }, [user]);
23 |
24 | const handleEdit = () => {
25 | setEditMode(true);
26 | };
27 |
28 | const handleCancel = () => {
29 | setEditedUser(user);
30 | setEditMode(false);
31 | };
32 |
33 | const handleSave = async (e) => {
34 | e.preventDefault();
35 | const userId = editedUser.id;
36 | updateUser(userId, editedUser);
37 | toast.success("Update successfully!");
38 | setEditMode(false);
39 | };
40 |
41 | const handleChange = (e) => {
42 | const { name, value } = e.target;
43 | setEditedUser((prevState) => ({
44 | ...prevState,
45 | [name]: value,
46 | }));
47 | };
48 |
49 | return (
50 |
57 |
58 |
67 |
68 |
69 |
70 |
71 | {!editMode ? (
72 |
76 | ) : (
77 | <>
78 |
82 |
83 |
87 | >
88 | )}
89 |
90 | {!editMode ? (
91 |
92 | ) : (
93 |
97 | )}
98 |
99 |
100 |
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default ProfilePage;
108 |
--------------------------------------------------------------------------------
/client/src/reducer/algoirthmActions.js:
--------------------------------------------------------------------------------
1 | export const algorithmActionTypes = {
2 | PROCESS_START: "PROCESS_START",
3 | PROCESS_FAILURE: "PROCESS_FAILURE",
4 | FETCH_ALGORITHMS_SUCCESS: "FETCH_ALGORITHMS_SUCCESS",
5 | ADD_ALGORITHM_SUCCESS: "ADD_ALGORITHM_SUCCESS",
6 | ADD_ALGORITHM: "ADD_ALGORITHM",
7 | UPDATE_ALGORITHM: "UPDATE_ALGORITHM_NAME_SUCCESS",
8 | DELETE_ALGORITHM: "DELETE_ALGORITHM",
9 | };
10 |
--------------------------------------------------------------------------------
/client/src/reducer/algorithmReducer.js:
--------------------------------------------------------------------------------
1 | import { algorithmActionTypes } from "./algoirthmActions";
2 |
3 | export const initialState = {
4 | algorithms: [],
5 | loading: false,
6 | error: null,
7 | };
8 |
9 | export function algorithmReducer(state, action) {
10 | switch (action.type) {
11 | case algorithmActionTypes.PROCESS_START:
12 | return { ...state, loading: true, error: null };
13 | case algorithmActionTypes.PROCESS_FAILURE:
14 | return { ...state, loading: false, error: action.error };
15 | case algorithmActionTypes.FETCH_ALGORITHMS_SUCCESS:
16 | return { ...state, algorithms: action.payload, loading: false };
17 | case algorithmActionTypes.ADD_ALGORITHM:
18 | return {
19 | ...state,
20 | algorithms: [...state.algorithms, action.payload],
21 | loading: false,
22 | error: null,
23 | };
24 | case algorithmActionTypes.UPDATE_ALGORITHM:
25 | return {
26 | ...state,
27 | algorithms: [...state.algorithms, action.payload],
28 | loading: false,
29 | error: null,
30 | };
31 | case algorithmActionTypes.DELETE_ALGORITHM:
32 | return {
33 | ...state,
34 | algorithms: state.algorithms.filter((a) => a.id !== action.payload),
35 | loading: false,
36 | error: null,
37 | };
38 | default:
39 | return state;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/reducer/dataStructureActions.js:
--------------------------------------------------------------------------------
1 | export const actionTypes = {
2 | PROCESS_START: "PROCESS_START",
3 | PROCESS_FAILURE: "PROCESS_FAILURE",
4 | FETCH_DATA_STRUCTURES_SUCCESS: "FETCH_DATA_STRUCTURES_SUCCESS",
5 | ADD_DATA_STRUCTURE_SUCCESS: "ADD_DATA_STRUCTURE_SUCCESS",
6 | ADD_DATA_STRUCTURE: "ADD_DATA_STRUCTURE",
7 | UPDATE_DATA_STRUCTURE_NAME_SUCCESS: "UPDATE_DATA_STRUCTURE_NAME_SUCCESS",
8 | DELETE_DATA_STRUCTURE: "DELETE_DATA_STRUCTURE",
9 | ADD_NODE: "ADD_NODE",
10 | RENAME_NODE: "RENAME_NODE",
11 | DELETE_NODE: "DELETE_NODE",
12 | ADD_CONTENT: "ADD_CONTENT",
13 | DELETE_CONTENT: "DELETE_CONTENT",
14 | UPDATE_CONTENT: "UPDATE_CONTENT",
15 | };
16 |
--------------------------------------------------------------------------------
/client/src/reducer/dataStructureReducer.js:
--------------------------------------------------------------------------------
1 | import { actionTypes } from "./dataStructureActions";
2 |
3 | export const initialState = {
4 | dataStructures: [],
5 | loading: false,
6 | error: null,
7 | };
8 |
9 | export function dataStructureReducer(state, action) {
10 | switch (action.type) {
11 | case actionTypes.PROCESS_START:
12 | return { ...state, loading: true, error: null };
13 | case actionTypes.PROCESS_FAILURE:
14 | return { ...state, loading: false, error: action.error };
15 | case actionTypes.FETCH_DATA_STRUCTURES_SUCCESS:
16 | return { ...state, dataStructures: action.payload, loading: false };
17 | case actionTypes.ADD_DATA_STRUCTURE_SUCCESS:
18 | return {
19 | ...state,
20 | dataStructures: [...state.dataStructures, action.payload],
21 | loading: false,
22 | error: null,
23 | };
24 | case actionTypes.UPDATE_DATA_STRUCTURE_NAME_SUCCESS:
25 | const updatedDataStructures = state.dataStructures.map((ds) => {
26 | if (ds.id === action.payload.id) {
27 | return { ...ds, name: action.payload.name };
28 | }
29 | return ds;
30 | });
31 | return {
32 | ...state,
33 | dataStructures: updatedDataStructures,
34 | loading: false,
35 | error: null,
36 | };
37 | case actionTypes.DELETE_DATA_STRUCTURE:
38 | return {
39 | ...state,
40 | dataStructures: state.dataStructures.filter(
41 | (ds) => ds.id !== action.payload.id
42 | ),
43 | loading: false,
44 | error: null,
45 | };
46 | case actionTypes.ADD_NODE:
47 | return {
48 | ...state,
49 | dataStructures: state.dataStructures.map((dataStructure) => {
50 | if (dataStructure.id === action.payload.dataStructureId) {
51 | return {
52 | ...dataStructure,
53 | nodes: [...dataStructure.nodes, action.payload.node],
54 | };
55 | }
56 | return dataStructure;
57 | }),
58 | loading: false,
59 | error: null,
60 | };
61 | case actionTypes.RENAME_NODE:
62 | return {
63 | ...state,
64 | dataStructures: state.dataStructures.map((dataStructure) => {
65 | if (dataStructure.id === action.payload.dataStructureId) {
66 | // Found the parent dataStructure, now find and update the node
67 | const updatedNodes = dataStructure.nodes.map((node) => {
68 | if (node.id === action.payload.node.id) {
69 | return {
70 | ...node,
71 | name: action.payload.node.name,
72 | };
73 | }
74 | return node;
75 | });
76 | return { ...dataStructure, nodes: updatedNodes };
77 | }
78 | return dataStructure;
79 | }),
80 | loading: false,
81 | error: null,
82 | };
83 | case actionTypes.DELETE_NODE:
84 | return {
85 | ...state,
86 | dataStructures: state.dataStructures.map((dataStructure) => {
87 | if (dataStructure.id === action.payload.dataStructureId) {
88 | const updatedNodes = dataStructure.nodes.filter(
89 | (node) => node.id !== action.payload.node.id
90 | );
91 | return {
92 | ...dataStructure,
93 | nodes: updatedNodes,
94 | };
95 | }
96 | return dataStructure;
97 | }),
98 | loading: false,
99 | error: null,
100 | };
101 | case actionTypes.UPDATE_CONTENT:
102 | return {
103 | ...state,
104 | dataStructures: state.dataStructures.map((dataStructure) => {
105 | if (dataStructure.id === action.payload.dataStructureId) {
106 | // Found the parent dataStructure, now find and update the node
107 | const updatedNodes = dataStructure.nodes.map((node) => {
108 | if (node.id === action.payload.node.id) {
109 | return {
110 | ...node,
111 | content: action.payload.node.content,
112 | };
113 | }
114 | return node;
115 | });
116 | return { ...dataStructure, nodes: updatedNodes };
117 | }
118 | return dataStructure;
119 | }),
120 | loading: false,
121 | error: null,
122 | };
123 | default:
124 | return state;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/client/src/reducer/userActions.js:
--------------------------------------------------------------------------------
1 | export const userActionTypes = {
2 | PROCESS_START: "PROCESS_START",
3 | PROCESS_FAILURE: "PROCESS_FAILURE",
4 | SIGN_IN: "SIGN_IN",
5 | REGISTER: "REGISTER",
6 | LOGOUT: "LOGOUT",
7 | GET_CURRENT_USER: "GET_CURRENT_USER",
8 | UPDATE_USER: "UPDATE_USER",
9 | CLEAR_ERROR: "CLEAR_ERROR",
10 | };
11 |
--------------------------------------------------------------------------------
/client/src/reducer/userReducer.js:
--------------------------------------------------------------------------------
1 | import { userActionTypes } from "./userActions";
2 |
3 | const storedData = JSON.parse(localStorage.getItem("user"));
4 |
5 | export const initialState = {
6 | isAuthenticated: !!storedData,
7 | token: storedData ? storedData : null,
8 | user: {},
9 | loading: false,
10 | error: null,
11 | };
12 |
13 | export function userReducer(state, action) {
14 | switch (action.type) {
15 | case userActionTypes.PROCESS_START:
16 | return { ...state, loading: true, error: null };
17 | case userActionTypes.CLEAR_ERROR:
18 | return { ...state, loading: false, error: null };
19 | case userActionTypes.PROCESS_FAILURE:
20 | return { ...state, loading: false, error: action.error };
21 | case userActionTypes.SIGN_IN:
22 | return {
23 | ...state,
24 | isAuthenticated: true,
25 | token: action.payload.token,
26 | user: action.payload.user,
27 | loading: false,
28 | error: null,
29 | };
30 | case userActionTypes.REGISTER:
31 | return {
32 | ...state,
33 | isAuthenticated: true,
34 | token: action.payload.token,
35 | user: action.payload.user,
36 | loading: false,
37 | error: null,
38 | };
39 | case userActionTypes.LOGOUT:
40 | return {
41 | ...state,
42 | isAuthenticated: false,
43 | token: null,
44 | user: null,
45 | loading: false,
46 | error: null,
47 | };
48 | case userActionTypes.GET_CURRENT_USER:
49 | return {
50 | ...state,
51 | user: action.payload.user,
52 | loading: false,
53 | error: null,
54 | };
55 | case userActionTypes.UPDATE_USER:
56 | return {
57 | ...state,
58 | user: action.payload.updatedUser,
59 | loading: false,
60 | };
61 | default:
62 | return state;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/ylslc/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
--------------------------------------------------------------------------------
/ylslc/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/edisonyls/LeetCodeRecorder/6b82272a9f80e3ec52760b7d1bfdd226ebbb67b6/ylslc/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/ylslc/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar
3 |
--------------------------------------------------------------------------------
/ylslc/Dockerfile:
--------------------------------------------------------------------------------
1 | # Stage 1: Build the application
2 | FROM maven:3.8.4-openjdk-17 AS build
3 | WORKDIR /app
4 | COPY pom.xml .
5 | COPY src ./src
6 | # Build the package without running tests
7 | RUN mvn clean package -DskipTests
8 |
9 | # Stage 2: Run the application
10 | FROM openjdk:17-alpine
11 | WORKDIR /app
12 | # Copy the built jar file from the build stage
13 | COPY --from=build /app/target/ylslc-0.0.1-SNAPSHOT.jar ./ylslc.jar
14 | EXPOSE 8080
15 |
16 | # Define environment variables (placeholders for default values or documentation)
17 | ENV DATABASE_USERNAME=placeholder_username
18 | ENV DATABASE_PASSWORD=placeholder_password
19 | ENV JWT_KEY=placeholder
20 |
21 | # Use an entrypoint shell script to handle environment variables
22 | CMD ["java", "-jar", "ylslc.jar", \
23 | "--spring.datasource.username=${DATABASE_USERNAME}", \
24 | "--spring.datasource.password=${DATABASE_PASSWORD}", \
25 | "--jwt.secret=${JWT_KEY}"]
26 |
--------------------------------------------------------------------------------
/ylslc/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 3.2.2
9 |
10 |
11 | com.yls
12 | ylslc
13 | 0.0.1-SNAPSHOT
14 | ylslc
15 | Leet Code Recorder by Spring Boot
16 |
17 | 17
18 |
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-data-jpa
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-web
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-devtools
31 | runtime
32 | true
33 |
34 |
35 | commons-io
36 | commons-io
37 | 2.16.1
38 |
39 |
40 | com.mysql
41 | mysql-connector-j
42 | runtime
43 |
44 |
45 | org.projectlombok
46 | lombok
47 | true
48 |
49 |
50 | org.modelmapper
51 | modelmapper
52 | 3.0.0
53 |
54 |
55 | org.springframework.boot
56 | spring-boot-starter-test
57 | test
58 |
59 |
60 | org.springframework.boot
61 | spring-boot-starter-security
62 |
63 |
64 | io.jsonwebtoken
65 | jjwt-api
66 | 0.12.3
67 |
68 |
69 |
70 | com.fasterxml.jackson.datatype
71 | jackson-datatype-jsr310
72 | 2.16.1
73 |
74 |
75 |
80 |
81 |
82 | io.jsonwebtoken
83 | jjwt-impl
84 | 0.12.3
85 | runtime
86 |
87 |
88 |
89 | io.jsonwebtoken
90 | jjwt-jackson
91 | 0.12.3
92 | runtime
93 |
94 |
95 |
96 |
97 |
98 |
99 | src/main/resources
100 |
101 |
102 | static
103 | static
104 |
105 |
106 |
107 |
108 | org.springframework.boot
109 | spring-boot-maven-plugin
110 |
111 |
112 |
113 | org.projectlombok
114 | lombok
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/YlslcApplication.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class YlslcApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(YlslcApplication.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/AlgorithmController.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm;
2 |
3 | import com.yls.ylslc.config.response.Response;
4 | import com.yls.ylslc.mappers.Mapper;
5 | import org.springframework.http.HttpHeaders;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.http.ResponseEntity;
9 | import org.springframework.web.bind.annotation.*;
10 | import org.springframework.web.multipart.MultipartFile;
11 |
12 | import java.util.List;
13 | import java.util.UUID;
14 |
15 | @RestController
16 | @RequestMapping(path = "api/algorithm")
17 | @CrossOrigin
18 | public class AlgorithmController {
19 | private final AlgorithmService algorithmService;
20 | private final Mapper algorithmMapper;
21 |
22 | @GetMapping
23 | public Response getAlgorithms() {
24 | List algorithmEntities = algorithmService.getAlgorithms();
25 | List algorithmDtos = algorithmEntities.stream().map(algorithmMapper::mapTo).toList();
26 | return Response.ok(algorithmDtos, "Algorithm retrieved successfully!");
27 | }
28 |
29 | @PostMapping
30 | public Response createAlgorithm(@RequestBody AlgorithmDto algorithmDto) {
31 | AlgorithmEntity algorithmEntity = algorithmMapper.mapFrom(algorithmDto);
32 | AlgorithmEntity savedAlgorithmEntity = algorithmService.createAlgorithm(algorithmEntity);
33 | AlgorithmDto savedAlgorithm = algorithmMapper.mapTo(savedAlgorithmEntity);
34 | return Response.ok(savedAlgorithm, "Algorithm created successfully!");
35 | }
36 |
37 | @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, path = "upload-image")
38 | public Response uploadImages(@RequestPart("image") MultipartFile image) {
39 | String imageId = algorithmService.uploadImages(image);
40 | return Response.ok(imageId, "Image saved successfully!");
41 | }
42 |
43 | @DeleteMapping(path = "/{id}")
44 | public Response deleteAlgorithm(@PathVariable("id") UUID id) {
45 | algorithmService.delete(id);
46 | return Response.ok("Algorithm deleted!");
47 | }
48 |
49 | @PutMapping("/{id}")
50 | public Response updateAlgorithm(@PathVariable UUID id, @RequestBody AlgorithmDto algorithmDto) {
51 | try {
52 | AlgorithmEntity algorithmEntity = algorithmMapper.mapFrom(algorithmDto);
53 | AlgorithmEntity updatedAlgorithmEntity = algorithmService.updateAlgorithm(id, algorithmEntity);
54 | AlgorithmDto updatedAlgorithm = algorithmMapper.mapTo(updatedAlgorithmEntity);
55 | return Response.ok(updatedAlgorithm, "Algorithm updated successfully!");
56 | } catch (RuntimeException e) {
57 | return Response.failed(HttpStatus.BAD_REQUEST, "Failed to update algorithm.", e.toString());
58 | }
59 | }
60 |
61 | @DeleteMapping("image/{algorithmId}/{imageId}")
62 | public void deleteQuestionImage(@PathVariable String algorithmId, @PathVariable String imageId) {
63 | algorithmService.deleteImage(algorithmId, imageId);
64 | }
65 |
66 | @GetMapping("image/{imageId}")
67 | public ResponseEntity getQuestionImage(@PathVariable String imageId) {
68 | byte[] imageData = algorithmService.getImage(imageId);
69 |
70 | HttpHeaders headers = new HttpHeaders();
71 | headers.setContentType(getMediaTypeForImageId(imageId));
72 |
73 | return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
74 | }
75 |
76 | @GetMapping(path = "/count-algorithm/{id}")
77 | public Response countAlgorithm(@PathVariable("id") UUID id) {
78 | return Response.ok(algorithmService.countAlgorithm(id), "Count retrieved successfully!");
79 | }
80 |
81 | private MediaType getMediaTypeForImageId(String imageId) {
82 | if (imageId.endsWith(".png")) {
83 | return MediaType.IMAGE_PNG;
84 | } else if (imageId.endsWith(".jpg") || imageId.endsWith(".jpeg")) {
85 | return MediaType.IMAGE_JPEG;
86 | } else {
87 | // Default or fallback content type
88 | return MediaType.APPLICATION_OCTET_STREAM;
89 | }
90 | }
91 |
92 | public AlgorithmController(AlgorithmService algorithmService,
93 | Mapper algorithmMapper) {
94 | this.algorithmService = algorithmService;
95 | this.algorithmMapper = algorithmMapper;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/AlgorithmDto.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm;
2 |
3 | import com.yls.ylslc.algorithm.section.SectionDto;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Builder;
6 | import lombok.Data;
7 | import lombok.NoArgsConstructor;
8 |
9 | import java.time.LocalDateTime;
10 | import java.util.List;
11 | import java.util.UUID;
12 |
13 | @Data
14 | @AllArgsConstructor
15 | @NoArgsConstructor
16 | @Builder
17 | public class AlgorithmDto {
18 | private UUID id;
19 | private String title;
20 | private String tag;
21 | private String summary;
22 | private String imageId;
23 | private List sections;
24 | private LocalDateTime createdAt;
25 | }
26 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/AlgorithmEntity.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm;
2 |
3 | import com.yls.ylslc.algorithm.section.SectionEntity;
4 | import com.yls.ylslc.user.UserEntity;
5 | import jakarta.persistence.*;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 |
9 | import java.time.LocalDateTime;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import java.util.UUID;
13 |
14 | @Getter
15 | @Setter
16 | @Entity
17 | @Table(name = "algorithm")
18 | public class AlgorithmEntity {
19 | @Id
20 | @GeneratedValue(strategy = GenerationType.UUID)
21 | private UUID id;
22 |
23 | @ManyToOne
24 | @JoinColumn(name = "user_id")
25 | private UserEntity user;
26 |
27 | private String title;
28 | private String tag;
29 | private String summary;
30 | private String imageId;
31 |
32 | @OneToMany(mappedBy = "algorithm", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
33 | private List sections = new ArrayList<>();
34 |
35 | private LocalDateTime createdAt;
36 |
37 | public void addSection(SectionEntity section) {
38 | sections.add(section);
39 | section.setAlgorithm(this);
40 | }
41 |
42 | @PrePersist
43 | protected void onCreate() {
44 | createdAt = LocalDateTime.now();
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/AlgorithmRepository.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm;
2 |
3 | import com.yls.ylslc.user.UserEntity;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.Query;
6 | import org.springframework.data.repository.query.Param;
7 | import org.springframework.stereotype.Repository;
8 |
9 | import java.util.List;
10 | import java.util.UUID;
11 |
12 | @Repository
13 | public interface AlgorithmRepository extends JpaRepository {
14 | List findByUser(UserEntity currentUser);
15 | @Query("SELECT COUNT(algo) FROM AlgorithmEntity algo WHERE algo.user.id = :userId")
16 | long countAlgorithmByUserId(@Param("userId") UUID userId);
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/AlgorithmService.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm;
2 |
3 | import org.springframework.web.multipart.MultipartFile;
4 |
5 | import java.util.List;
6 | import java.util.UUID;
7 |
8 | public interface AlgorithmService {
9 | List getAlgorithms();
10 |
11 | AlgorithmEntity getAlgorithmById(UUID id);
12 |
13 | AlgorithmEntity createAlgorithm(AlgorithmEntity algorithm);
14 |
15 | String uploadImages(MultipartFile image);
16 |
17 | void delete(UUID id);
18 |
19 | AlgorithmEntity updateAlgorithm(UUID id, AlgorithmEntity algorithmEntity);
20 |
21 | void deleteImage(String algorithmId, String imageId);
22 |
23 | Long countAlgorithm(UUID userId);
24 |
25 | byte[] getImage( String imageId);
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/section/SectionDto.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm.section;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Builder;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | @Data
9 | @AllArgsConstructor
10 | @NoArgsConstructor
11 | @Builder
12 | public class SectionDto {
13 | private Long id;
14 | private String name;
15 | private String content;
16 | }
17 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/section/SectionEntity.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm.section;
2 |
3 | import com.yls.ylslc.algorithm.AlgorithmEntity;
4 | import jakarta.persistence.*;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 |
8 | @Getter
9 | @Setter
10 | @Entity
11 | @Table(name="section")
12 | public class SectionEntity {
13 | @Id
14 | @GeneratedValue(strategy = GenerationType.IDENTITY)
15 | private Long id;
16 |
17 | @ManyToOne
18 | @JoinColumn(name="algorithm_id")
19 | private AlgorithmEntity algorithm;
20 |
21 | private String name;
22 |
23 | @Column(columnDefinition = "TEXT")
24 | private String content;
25 | }
26 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/section/SectionRepository.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm.section;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 | import org.springframework.stereotype.Repository;
5 |
6 | @Repository
7 | public interface SectionRepository extends JpaRepository {
8 | }
9 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/section/SectionService.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm.section;
2 |
3 | import com.yls.ylslc.algorithm.AlgorithmEntity;
4 |
5 | import java.util.List;
6 |
7 | public interface SectionService {
8 | void updateSections(AlgorithmEntity existingAlgorithm, List sections);
9 | }
10 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/algorithm/section/SectionServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.algorithm.section;
2 |
3 | import com.yls.ylslc.algorithm.AlgorithmEntity;
4 | import org.springframework.stereotype.Service;
5 |
6 | import java.util.List;
7 |
8 | @Service
9 | public class SectionServiceImpl implements SectionService{
10 | @Override
11 | public void updateSections(AlgorithmEntity existingAlgorithm, List sections) {
12 | existingAlgorithm.getSections().removeIf(section ->
13 | sections.stream().noneMatch(newSec -> newSec.getId() != null && newSec.getId().equals(section.getId()))
14 | );
15 | for (SectionEntity newSection : sections){
16 | if (newSection.getId() == null){
17 | existingAlgorithm.addSection(newSection);
18 | } else {
19 | existingAlgorithm.getSections().stream()
20 | .filter(s -> s.getId().equals(newSection.getId()))
21 | .findFirst()
22 | .ifPresent(existingSection -> {
23 | existingSection.setName(newSection.getName());
24 | existingSection.setContent(newSection.getContent());
25 | });
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/MapperConfig.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config;
2 |
3 | import org.modelmapper.ModelMapper;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 |
7 | @Configuration
8 | public class MapperConfig {
9 | @Bean
10 | public ModelMapper modelMapper(){
11 | return new ModelMapper();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/exception/GlobalExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config.exception;
2 | import com.yls.ylslc.config.response.Response;
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.http.ResponseEntity;
5 | import org.springframework.web.bind.annotation.ControllerAdvice;
6 | import org.springframework.web.bind.annotation.ExceptionHandler;
7 |
8 | @ControllerAdvice
9 | public class GlobalExceptionHandler {
10 | @ExceptionHandler
11 | public ResponseEntity handleApiNotFoundException(Exception exc){
12 | Response error = new Response(
13 | HttpStatus.NOT_FOUND.value(),
14 | "Server Error!",
15 | exc.getMessage());
16 | return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
17 | }
18 |
19 | @ExceptionHandler
20 | public ResponseEntity handleQuestionException(QuestionException exc){
21 | Response error = new Response(HttpStatus.NOT_FOUND.value(),
22 | "Question Exception Thrown!",
23 | exc.getMessage());
24 | return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
25 | }
26 | }
27 |
28 |
29 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/exception/QuestionException.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config.exception;
2 |
3 | public class QuestionException extends RuntimeException{
4 | public QuestionException(String message){
5 | super(message);
6 | }
7 |
8 | public QuestionException(String message, Throwable cause){
9 | super(message, cause);
10 | }
11 |
12 | public QuestionException(Throwable cause){
13 | super(cause);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/jwt/CustomAuthenticationEntryPoint.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config.jwt;
2 |
3 | import jakarta.servlet.http.HttpServletRequest;
4 | import jakarta.servlet.http.HttpServletResponse;
5 | import org.springframework.security.core.AuthenticationException;
6 | import org.springframework.stereotype.Component;
7 |
8 | import java.io.IOException;
9 |
10 | @Component
11 | public class CustomAuthenticationEntryPoint implements org.springframework.security.web.AuthenticationEntryPoint {
12 | @Override
13 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
14 | response.setContentType("application/json");
15 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
16 | response.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" + authException.getMessage() + "\"}");
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/jwt/JwtAuthenticationFilter.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config.jwt;
2 |
3 | import com.yls.ylslc.user.UserDetailsServiceImpl;
4 | import jakarta.servlet.FilterChain;
5 | import jakarta.servlet.ServletException;
6 | import jakarta.servlet.http.HttpServletRequest;
7 | import jakarta.servlet.http.HttpServletResponse;
8 | import org.springframework.lang.NonNull;
9 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
10 | import org.springframework.security.core.context.SecurityContextHolder;
11 | import org.springframework.security.core.userdetails.UserDetails;
12 | import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
13 | import org.springframework.stereotype.Component;
14 | import org.springframework.web.filter.OncePerRequestFilter;
15 |
16 | import java.io.IOException;
17 |
18 | @Component
19 | public class JwtAuthenticationFilter extends OncePerRequestFilter {
20 | private final JwtService jwtService;
21 | private final UserDetailsServiceImpl userDetailsService;
22 |
23 | public JwtAuthenticationFilter(JwtService jwtService, UserDetailsServiceImpl userDetailsService) {
24 | this.jwtService = jwtService;
25 | this.userDetailsService = userDetailsService;
26 | }
27 |
28 | @Override
29 | protected void doFilterInternal(
30 | @NonNull HttpServletRequest request,
31 | @NonNull HttpServletResponse response,
32 | @NonNull FilterChain filterChain)
33 | throws ServletException, IOException {
34 | String authHeader = request.getHeader("Authorization");
35 | if (authHeader == null || !authHeader.startsWith("Bearer ")){
36 | filterChain.doFilter(request, response);
37 | return;
38 | }
39 |
40 | String token = authHeader.substring(7);
41 | String username = jwtService.extractUsername(token);
42 |
43 | if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
44 | UserDetails userDetails = userDetailsService.loadUserByUsername(username);
45 | if (jwtService.isValid(token, userDetails)) {
46 | UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
47 | userDetails, null, userDetails.getAuthorities()
48 | );
49 | authenticationToken.setDetails(
50 | new WebAuthenticationDetailsSource()
51 | .buildDetails(request)
52 | );
53 | SecurityContextHolder.getContext().setAuthentication(authenticationToken);
54 | }
55 | }
56 | filterChain.doFilter(request, response);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/jwt/JwtService.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config.jwt;
2 |
3 | import com.yls.ylslc.user.UserEntity;
4 | import io.jsonwebtoken.Claims;
5 | import io.jsonwebtoken.Jwts;
6 | import io.jsonwebtoken.io.Decoders;
7 | import io.jsonwebtoken.security.Keys;
8 | import org.springframework.beans.factory.annotation.Value;
9 | import org.springframework.security.core.userdetails.UserDetails;
10 | import org.springframework.stereotype.Service;
11 |
12 | import javax.crypto.SecretKey;
13 | import java.util.Date;
14 | import java.util.function.Function;
15 |
16 | @Service
17 | public class JwtService {
18 | @Value("${jwt.secret}")
19 | private String secretKey;
20 |
21 | public String extractUsername(String token) {
22 | return extractClaim(token, Claims::getSubject);
23 | }
24 |
25 | public boolean isValid(String token, UserDetails user){
26 | String username = extractUsername(token);
27 | return (username.equals(user.getUsername())) && !isTokenExpired(token);
28 | }
29 |
30 | private boolean isTokenExpired(String token) {
31 | return extractExpiration(token).before(new Date());
32 | }
33 |
34 | private Date extractExpiration(String token) {
35 | return extractClaim(token, Claims::getExpiration);
36 | }
37 |
38 | public T extractClaim(String token, Function resolver) {
39 | Claims claims = extractAllClaims(token);
40 | return resolver.apply(claims);
41 | }
42 |
43 | private Claims extractAllClaims(String token){
44 | return Jwts
45 | .parser()
46 | .verifyWith(getSignInKey())
47 | .build()
48 | .parseSignedClaims(token)
49 | .getPayload();
50 | }
51 | public String generateToken(UserEntity user){
52 | String token = Jwts
53 | .builder()
54 | .subject(user.getUsername())
55 | .issuedAt(new Date(System.currentTimeMillis()))
56 | .expiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000))
57 | .signWith(getSignInKey())
58 | .compact();
59 | return token;
60 | }
61 |
62 |
63 | private SecretKey getSignInKey(){
64 | byte[] keyBytes = Decoders.BASE64URL.decode(secretKey);
65 | return Keys.hmacShaKeyFor(keyBytes);
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/jwt/SecurityConfig.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config.jwt;
2 |
3 | import com.yls.ylslc.user.UserDetailsServiceImpl;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.security.authentication.AuthenticationManager;
7 | import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
8 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
9 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
10 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
11 | import org.springframework.security.config.http.SessionCreationPolicy;
12 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
13 | import org.springframework.security.crypto.password.PasswordEncoder;
14 | import org.springframework.security.web.SecurityFilterChain;
15 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
16 | import org.springframework.web.cors.CorsConfiguration;
17 | import org.springframework.web.cors.CorsConfigurationSource;
18 | import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
19 |
20 | @Configuration
21 | @EnableWebSecurity
22 | public class SecurityConfig {
23 |
24 | private final UserDetailsServiceImpl userDetailsService;
25 | private final JwtAuthenticationFilter jwtAuthenticationFilter;
26 | private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
27 |
28 | public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthenticationFilter jwtAuthenticationFilter,
29 | CustomAuthenticationEntryPoint customAuthenticationEntryPoint) {
30 | this.userDetailsService = userDetailsService;
31 | this.jwtAuthenticationFilter = jwtAuthenticationFilter;
32 | this.customAuthenticationEntryPoint = customAuthenticationEntryPoint;
33 | }
34 |
35 | @Bean
36 | public CorsConfigurationSource corsConfigurationSource() {
37 | CorsConfiguration configuration = new CorsConfiguration();
38 | configuration.addAllowedOrigin("https://ylslc.edisonyls.com");
39 | configuration.addAllowedOrigin("http://localhost:3000");
40 | configuration.addAllowedMethod("*");
41 | configuration.addAllowedHeader("*");
42 | configuration.setAllowCredentials(true);
43 | UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
44 | source.registerCorsConfiguration("/**", configuration);
45 | return source;
46 | }
47 |
48 | @Bean
49 | public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
50 | http
51 | .csrf(AbstractHttpConfigurer::disable)
52 | .cors().configurationSource(corsConfigurationSource())
53 | .and()
54 | .csrf(AbstractHttpConfigurer::disable)
55 | .authorizeHttpRequests(req -> req
56 | .requestMatchers("/api/auth/authenticate/**", "/api/auth/register/**").permitAll()
57 | .requestMatchers("/admin_only/**").hasAuthority("ADMIN")
58 | .anyRequest().authenticated())
59 | .userDetailsService(userDetailsService)
60 | .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
61 | .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
62 | .exceptionHandling(exception -> exception.authenticationEntryPoint(customAuthenticationEntryPoint));
63 | return http.build();
64 | }
65 |
66 | @Bean
67 | public PasswordEncoder passwordEncoder() {
68 | return new BCryptPasswordEncoder();
69 | }
70 |
71 | @Bean
72 | public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration)
73 | throws Exception {
74 | return authenticationConfiguration.getAuthenticationManager();
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/config/response/Response.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.config.response;
2 |
3 | import lombok.Data;
4 | import org.springframework.http.HttpStatus;
5 |
6 | import java.text.DateFormat;
7 | import java.text.SimpleDateFormat;
8 | import java.util.Date;
9 | import java.util.TimeZone;
10 |
11 | @Data
12 | public class Response {
13 | private Integer status;
14 | private String message;
15 | private String serverMessage;
16 | private Object data;
17 | private String timeStamp;
18 | private Long dataLength;
19 |
20 | public static Response ok(Object data, String message){
21 | return new Response(data, message);
22 | }
23 | public static Response ok(String message){
24 | return new Response(message);
25 | }
26 |
27 |
28 | public static Response failed(HttpStatus status, String message){
29 | return new Response(status.value(), message);
30 | }
31 |
32 | public static Response failed(HttpStatus status, String message, String serverMessage){
33 | return new Response(status.value(), message, serverMessage);
34 | }
35 |
36 | public Response(String message){
37 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
38 | df.setTimeZone(TimeZone.getTimeZone("UTC"));
39 | this.status = HttpStatus.OK.value();
40 | this.message = message;
41 | this.data = null;
42 | this.timeStamp = df.format(new Date());
43 | this.serverMessage = "SUCCESS";
44 | }
45 |
46 | public Response(Object data, String message){
47 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
48 | df.setTimeZone(TimeZone.getTimeZone("UTC"));
49 | this.status = HttpStatus.OK.value();
50 | this.message = message;
51 | this.data = data;
52 | this.timeStamp = df.format(new Date());
53 | this.serverMessage = "SUCCESS";
54 | }
55 |
56 | public Response(Object data, Long dataLength, String message){
57 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
58 | df.setTimeZone(TimeZone.getTimeZone("UTC"));
59 | this.status = HttpStatus.OK.value();
60 | this.message = message;
61 | this.data = data;
62 | this.timeStamp = df.format(new Date());
63 | this.serverMessage = "SUCCESS";
64 | this.dataLength = dataLength;
65 | }
66 |
67 | public static Response ok(Object data, Long dataLength, String message){
68 | return new Response(data, dataLength, message);
69 | }
70 |
71 | public Response(Integer status, String message) {
72 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
73 | df.setTimeZone(TimeZone.getTimeZone("UTC"));
74 | this.status = status;
75 | this.message = message;
76 | this.timeStamp = df.format(new Date());
77 | this.serverMessage = "FAILED";
78 | }
79 |
80 | public Response(Integer status, String message, String serverMessage) {
81 | DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
82 | df.setTimeZone(TimeZone.getTimeZone("UTC"));
83 |
84 | this.status = status;
85 | this.message = message;
86 | this.serverMessage = serverMessage;
87 | this.timeStamp = df.format(new Date());
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/controllers/PaymentController.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.controllers;
2 |
3 | import com.yls.ylslc.config.response.Response;
4 | import com.yls.ylslc.user.UserEntity;
5 | import com.yls.ylslc.user.UserService;
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.beans.factory.annotation.Value;
8 | import org.springframework.http.HttpStatus;
9 | import org.springframework.web.bind.annotation.*;
10 |
11 | import java.util.Arrays;
12 | import java.util.List;
13 | import java.util.UUID;
14 |
15 | @RestController
16 | @RequestMapping(path = "api/payment")
17 | @CrossOrigin(origins = { "https://ylslc.org", "http://localhost:3000" })
18 | public class PaymentController {
19 | private final UserService userService;
20 | private final List validRoles = Arrays.asList("REGULAR", "PREMIUM", "PREPLUS");
21 | private final List upgradeRoles = Arrays.asList("PREMIUM", "PREPLUS");
22 |
23 | @Value("${payment.secret-key}")
24 | private String paymentSecretKey;
25 |
26 | @PostMapping
27 | public Response processPayment(@RequestParam String currentRole,
28 | @RequestParam String upgradeRole) {
29 | UserEntity user = userService.getCurrentUser();
30 | if (!user.getRole().toString().equals(currentRole)) {
31 | return Response.failed(HttpStatus.BAD_REQUEST, "Provided currentRole does not match!");
32 | }
33 | if (!validRoles.contains(currentRole)) {
34 | return Response.failed(HttpStatus.BAD_REQUEST, "Invalid current role provided.");
35 | }
36 |
37 | // Validate upgrade role
38 | if (!upgradeRoles.contains(upgradeRole)) {
39 | return Response.failed(HttpStatus.BAD_REQUEST, "Invalid upgrade role provided.");
40 | }
41 |
42 | // Check if the upgrade is valid
43 | if (currentRole.equals(upgradeRole)) {
44 | return Response.failed(HttpStatus.BAD_REQUEST, "User is already in the requested role.");
45 | } else if (currentRole.equals("PREMIUM") && upgradeRole.equals("REGULAR")) {
46 | return Response.failed(HttpStatus.BAD_REQUEST, "Downgrade is not allowed.");
47 | } else if (currentRole.equals("PREPLUS") && (upgradeRole.equals("REGULAR") || upgradeRole.equals("PREMIUM"))) {
48 | return Response.failed(HttpStatus.BAD_REQUEST, "Downgrade is not allowed.");
49 | }
50 |
51 | // Simulate role upgrade
52 | // In a real-world scenario, you would update the user's role in the database
53 | userService.updateUserRole(user.getId(), upgradeRole);
54 |
55 | return Response.ok("Payment successful! User role updated to " + upgradeRole);
56 | }
57 |
58 | @PostMapping("upgrade-admin")
59 | public Response upgradeToAdmin(UUID userId) {
60 | UserEntity user = userService.getCurrentUser();
61 | if (!user.getRole().toString().equals("ADMIN"))
62 | return Response.failed(HttpStatus.BAD_REQUEST, "This action can only be performed by admin.");
63 | userService.updateUserRole(userId, "ADMIN");
64 | return Response.ok("Upgrade user <" + userId + "> to admin!");
65 | }
66 |
67 | @PostMapping("verify")
68 | public Response verifyUpgradeSecret(@RequestParam String secretKey) {
69 | if (secretKey.equals(paymentSecretKey)) {
70 | return Response.ok("Secret key verified successfully!");
71 | } else {
72 | return Response.failed(HttpStatus.UNAUTHORIZED, "Invalid secret key provided.");
73 | }
74 | }
75 |
76 | @Autowired
77 | public PaymentController(UserService userService) {
78 | this.userService = userService;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/controllers/TestController.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.controllers;
2 |
3 | import com.yls.ylslc.config.response.Response;
4 | import org.springframework.web.bind.annotation.*;
5 |
6 | @RestController
7 | @RequestMapping(path = "api/test")
8 | @CrossOrigin(origins = { "https://my-website.org", "http://localhost:3000" })
9 | public class TestController {
10 | @GetMapping
11 | public Response test(){
12 | return Response.ok("Test successful");
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/data_structure/DataStructureController.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.data_structure;
2 |
3 | import com.yls.ylslc.config.response.Response;
4 | import com.yls.ylslc.mappers.Mapper;
5 | import org.springframework.http.HttpStatus;
6 | import org.springframework.web.bind.annotation.*;
7 |
8 | import java.util.List;
9 | import java.util.Map;
10 | import java.util.UUID;
11 |
12 | @RestController
13 | @RequestMapping(path = "api/data-structure")
14 | @CrossOrigin(origins = { "https://ylslc.org", "http://localhost:3000" })
15 | public class DataStructureController {
16 | private final DataStructureService dataStructureService;
17 | private final Mapper dataStructureMapper;
18 |
19 | @GetMapping
20 | public Response getDataStructures() {
21 | List dataStructureEntities = dataStructureService.getDataStructures();
22 | List dataStructureDtos = dataStructureEntities.stream().map(dataStructureMapper::mapTo)
23 | .toList();
24 | return Response.ok(dataStructureDtos, "Data structure retrieved successfully!");
25 | }
26 |
27 | @PostMapping
28 | public Response createDataStructure(@RequestBody DataStructureDto dataStructureDto) {
29 | DataStructureEntity dataStructureEntity = dataStructureMapper.mapFrom(dataStructureDto);
30 | DataStructureEntity savedDataStructureEntity = dataStructureService.createDataStructure(dataStructureEntity);
31 | DataStructureDto savedDataStructureDto = dataStructureMapper.mapTo(savedDataStructureEntity);
32 | return Response.ok(savedDataStructureDto, "Data structure saved successfully!");
33 | }
34 |
35 | @PatchMapping(path = "/{id}")
36 | public Response updateDataStructureByName(@PathVariable("id") UUID id,
37 | @RequestBody Map updateRequest) {
38 | String name = updateRequest.get("name");
39 | if (!dataStructureService.isExist(id)) {
40 | return Response.failed(HttpStatus.NOT_FOUND, "Data structure not found!");
41 | }
42 | DataStructureEntity updatedDataStructure = dataStructureService.updateName(id, name);
43 | DataStructureDto updatedDataStructureDto = dataStructureMapper.mapTo(updatedDataStructure);
44 | return Response.ok(updatedDataStructureDto, "Name for the data structure is updated successfully!");
45 | }
46 |
47 | @DeleteMapping(path = "/{id}")
48 | public Response deleteDataStructure(@PathVariable("id") UUID id) {
49 | DataStructureEntity dataStructureEntity = dataStructureService.delete(id);
50 | DataStructureDto deletedDataStructure = dataStructureMapper.mapTo(dataStructureEntity);
51 | return Response.ok(deletedDataStructure, deletedDataStructure.getName() + " deleted!");
52 | }
53 |
54 | @GetMapping(path = "/count-data-structure/{id}")
55 | public Response countDataStructure(@PathVariable("id") UUID id) {
56 | return Response.ok(dataStructureService.countDataStructure(id), "Count retrieved successfully!");
57 | }
58 |
59 | public DataStructureController(DataStructureService dataStructureService,
60 | Mapper dataStructureMapper) {
61 | this.dataStructureService = dataStructureService;
62 | this.dataStructureMapper = dataStructureMapper;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/data_structure/DataStructureDto.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.data_structure;
2 |
3 | import com.yls.ylslc.node.NodeDto;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Builder;
6 | import lombok.Data;
7 | import lombok.NoArgsConstructor;
8 |
9 | import java.time.LocalDateTime;
10 | import java.util.List;
11 | import java.util.UUID;
12 |
13 | @Data
14 | @AllArgsConstructor
15 | @NoArgsConstructor
16 | @Builder
17 | public class DataStructureDto {
18 | private UUID id;
19 | private String name;
20 | private List nodes;
21 | private LocalDateTime createdAt;
22 | }
23 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/data_structure/DataStructureEntity.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.data_structure;
2 |
3 | import com.yls.ylslc.node.NodeEntity;
4 | import com.yls.ylslc.user.UserEntity;
5 | import jakarta.persistence.*;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 |
9 | import java.time.LocalDateTime;
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import java.util.UUID;
13 |
14 | @Getter
15 | @Setter
16 | @Entity
17 | @Table(name="data_structure")
18 | public class DataStructureEntity {
19 | @Id
20 | @GeneratedValue(strategy = GenerationType.AUTO)
21 | private UUID id;
22 |
23 | @ManyToOne
24 | @JoinColumn(name="user_id")
25 | private UserEntity user;
26 |
27 | private String name;
28 |
29 | @OneToMany(mappedBy = "dataStructure", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
30 | private List nodes = new ArrayList<>();
31 |
32 | private LocalDateTime createdAt;
33 |
34 | @PrePersist
35 | protected void onCreate() {
36 | createdAt = LocalDateTime.now();
37 | }
38 |
39 |
40 | public void addNode(NodeEntity node){
41 | node.setDataStructure(this);
42 | nodes.add(node);
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/data_structure/DataStructureRepository.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.data_structure;
2 |
3 | import com.yls.ylslc.user.UserEntity;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.data.jpa.repository.Query;
6 | import org.springframework.data.repository.query.Param;
7 | import org.springframework.stereotype.Repository;
8 |
9 | import java.util.List;
10 | import java.util.UUID;
11 |
12 | @Repository
13 | public interface DataStructureRepository extends JpaRepository {
14 | List findByUser(UserEntity currentUser);
15 |
16 | @Query("SELECT COUNT(ds) FROM DataStructureEntity ds WHERE ds.user.id = :userId")
17 | long countDataStructuresByUserId(@Param("userId") UUID userId);
18 | }
19 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/data_structure/DataStructureService.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.data_structure;
2 |
3 | import java.util.List;
4 | import java.util.UUID;
5 |
6 | public interface DataStructureService {
7 | DataStructureEntity createDataStructure(DataStructureEntity dataStructureEntity);
8 | List getDataStructures();
9 |
10 | boolean isExist(UUID id);
11 |
12 | DataStructureEntity updateName(UUID id, String name);
13 |
14 | DataStructureEntity delete(UUID id);
15 | Long countDataStructure(UUID userId);
16 | }
17 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/data_structure/DataStructureServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.data_structure;
2 |
3 | import com.yls.ylslc.user.UserEntity;
4 | import com.yls.ylslc.user.UserService;
5 | import jakarta.transaction.Transactional;
6 | import org.springframework.security.core.context.SecurityContextHolder;
7 | import org.springframework.stereotype.Service;
8 |
9 | import java.util.*;
10 |
11 | @Service
12 | public class DataStructureServiceImpl implements DataStructureService{
13 |
14 | private final DataStructureRepository dataStructureRepository;
15 | private final UserService userService;
16 |
17 | @Override
18 | public DataStructureEntity createDataStructure(DataStructureEntity dataStructureEntity) {
19 | UserEntity userEntity = userService.getCurrentUser();
20 | dataStructureEntity.setUser(userEntity);
21 | return dataStructureRepository.save(dataStructureEntity);
22 |
23 | }
24 |
25 | @Override
26 | public List getDataStructures() {
27 | String username = SecurityContextHolder.getContext().getAuthentication().getName();
28 | Optional currentUser = userService.findOneByUsername(username);
29 | List dataStructures = currentUser
30 | .map(dataStructureRepository::findByUser)
31 | .orElse(Collections.emptyList());
32 | dataStructures.sort(Comparator.comparing(DataStructureEntity::getCreatedAt, Comparator.nullsLast(Comparator.naturalOrder())));
33 | return dataStructures;
34 |
35 | }
36 |
37 | @Override
38 | public boolean isExist(UUID id) {
39 | return dataStructureRepository.existsById(id);
40 | }
41 |
42 | @Override
43 | public DataStructureEntity updateName(UUID id, String name) {
44 | DataStructureEntity dataStructureEntity = dataStructureRepository.findById(id)
45 | .orElseThrow(() -> new IllegalStateException("Data structure not found!"));
46 | dataStructureEntity.setName(name);
47 | return dataStructureRepository.save(dataStructureEntity);
48 | }
49 |
50 | @Override
51 | @Transactional
52 | public DataStructureEntity delete(UUID id) {
53 | DataStructureEntity dataStructureEntity = dataStructureRepository.findById(id)
54 | .orElseThrow(() -> new IllegalStateException("Data structure not found!"));
55 | dataStructureRepository.deleteById(id);
56 | return dataStructureEntity;
57 | }
58 |
59 | @Override
60 | public Long countDataStructure(UUID userId) {
61 | return dataStructureRepository.countDataStructuresByUserId(userId);
62 | }
63 |
64 | public DataStructureServiceImpl(DataStructureRepository dataStructureRepository, UserService userService) {
65 | this.dataStructureRepository = dataStructureRepository;
66 | this.userService = userService;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/mappers/AlgorithmMapperImpl.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.mappers;
2 |
3 | import com.yls.ylslc.algorithm.AlgorithmDto;
4 | import com.yls.ylslc.algorithm.AlgorithmEntity;
5 | import com.yls.ylslc.algorithm.section.SectionDto;
6 | import com.yls.ylslc.algorithm.section.SectionEntity;
7 | import org.modelmapper.ModelMapper;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.util.List;
11 | import java.util.stream.Collectors;
12 |
13 | @Component
14 | public class AlgorithmMapperImpl implements Mapper {
15 | private final ModelMapper modelMapper;
16 |
17 | public AlgorithmMapperImpl(ModelMapper modelMapper) {
18 | this.modelMapper = modelMapper;
19 | }
20 |
21 | @Override
22 | public AlgorithmDto mapTo(AlgorithmEntity algorithmEntity) {
23 | AlgorithmDto algorithmDto = modelMapper.map(algorithmEntity, AlgorithmDto.class);
24 | if (algorithmDto.getSections() != null && !algorithmEntity.getSections().isEmpty()) {
25 | List sectionDtos = algorithmEntity.getSections().stream()
26 | .map(sectionEntity -> modelMapper.map(sectionEntity, SectionDto.class))
27 | .collect(Collectors.toList());
28 | algorithmDto.setSections(sectionDtos);
29 | }
30 | return algorithmDto;
31 | }
32 |
33 | @Override
34 | public AlgorithmEntity mapFrom(AlgorithmDto algorithmDto) {
35 | if (algorithmDto == null) {
36 | return null;
37 | }
38 | AlgorithmEntity algorithmEntity = modelMapper.map(algorithmDto, AlgorithmEntity.class);
39 | List sectionEntities = algorithmDto.getSections().stream()
40 | .map(sectionDto -> {
41 | SectionEntity section = modelMapper.map(sectionDto, SectionEntity.class);
42 | section.setAlgorithm(algorithmEntity);
43 | return section;
44 | })
45 | .collect(Collectors.toList());
46 | algorithmEntity.setSections(sectionEntities);
47 | return algorithmEntity;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/mappers/DataStructureMapperImpl.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.mappers;
2 |
3 | import com.yls.ylslc.data_structure.DataStructureDto;
4 | import com.yls.ylslc.data_structure.DataStructureEntity;
5 | import com.yls.ylslc.node.NodeDto;
6 | import com.yls.ylslc.node.NodeEntity;
7 | import org.modelmapper.ModelMapper;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.util.ArrayList;
11 | import java.util.List;
12 | import java.util.stream.Collectors;
13 |
14 | @Component
15 | public class DataStructureMapperImpl implements Mapper{
16 |
17 | private final ModelMapper modelMapper;
18 |
19 | public DataStructureMapperImpl(ModelMapper modelMapper) {
20 | this.modelMapper = modelMapper;
21 | }
22 |
23 | @Override
24 | public DataStructureDto mapTo(DataStructureEntity dataStructureEntity) {
25 | DataStructureDto dataStructureDto = modelMapper.map(dataStructureEntity, DataStructureDto.class);
26 |
27 | if (dataStructureEntity.getNodes() != null && !dataStructureEntity.getNodes().isEmpty()){
28 | List nodeDtos = dataStructureEntity.getNodes().stream()
29 | .map(node -> modelMapper.map(node, NodeDto.class))
30 | .collect(Collectors.toList());
31 | dataStructureDto.setNodes(nodeDtos);
32 | }
33 | return dataStructureDto;
34 | }
35 |
36 | @Override
37 | public DataStructureEntity mapFrom(DataStructureDto dataStructureDto) {
38 | DataStructureEntity dataStructureEntity = modelMapper.map(dataStructureDto, DataStructureEntity.class);
39 | if (dataStructureEntity.getNodes() == null) {
40 | dataStructureEntity.setNodes(new ArrayList<>());
41 | }
42 | dataStructureEntity.getNodes().clear();
43 | if (dataStructureDto.getNodes() != null) {
44 | for (NodeDto nodeDto : dataStructureDto.getNodes()) {
45 | NodeEntity node = modelMapper.map(nodeDto, NodeEntity.class);
46 | node.setId(null);
47 | dataStructureEntity.addNode(node);
48 | }
49 | }
50 | return dataStructureEntity;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/mappers/Mapper.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.mappers;
2 |
3 | public interface Mapper {
4 | B mapTo(A a);
5 | A mapFrom(B b);
6 | }
7 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/mappers/NodeMapperImpl.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.mappers;
2 |
3 | import com.yls.ylslc.node.NodeDto;
4 | import com.yls.ylslc.node.NodeEntity;
5 | import org.modelmapper.ModelMapper;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | public class NodeMapperImpl implements Mapper {
10 | private final ModelMapper modelMapper;
11 |
12 | public NodeMapperImpl(ModelMapper modelMapper) {
13 | this.modelMapper = modelMapper;
14 | }
15 |
16 | @Override
17 | public NodeDto mapTo(NodeEntity nodeEntity) {
18 | return modelMapper.map(nodeEntity, NodeDto.class);
19 | }
20 |
21 | @Override
22 | public NodeEntity mapFrom(NodeDto nodeDto) {
23 | return modelMapper.map(nodeDto, NodeEntity.class);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/mappers/QuestionMapperImpl.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.mappers;
2 |
3 | import com.yls.ylslc.question.QuestionDto;
4 | import com.yls.ylslc.question.QuestionEntity;
5 | import com.yls.ylslc.question.solution.SolutionDto;
6 | import com.yls.ylslc.question.solution.SolutionEntity;
7 | import org.modelmapper.ModelMapper;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.util.List;
11 | import java.util.stream.Collectors;
12 |
13 | @Component
14 | public class QuestionMapperImpl implements Mapper{
15 |
16 | private final ModelMapper modelMapper;
17 |
18 | public QuestionMapperImpl(ModelMapper modelMapper){
19 | this.modelMapper = modelMapper;
20 | }
21 |
22 |
23 | @Override
24 | public QuestionEntity mapFrom(QuestionDto questionDto) {
25 | QuestionEntity questionEntity = modelMapper.map(questionDto, QuestionEntity.class);
26 | questionEntity.getSolutions().clear();
27 | if (questionDto.getSolutions() != null) {
28 | for (SolutionDto solutionDto : questionDto.getSolutions()) {
29 | SolutionEntity solution = modelMapper.map(solutionDto, SolutionEntity.class);
30 | solution.setId(null);
31 | questionEntity.addSolution(solution);
32 | }
33 | }
34 | return questionEntity;
35 | }
36 | @Override
37 | public QuestionDto mapTo(QuestionEntity questionEntity) {
38 | QuestionDto questionDto = modelMapper.map(questionEntity, QuestionDto.class);
39 | if (questionEntity.getSolutions() != null && !questionEntity.getSolutions().isEmpty()) {
40 | List solutionDtos = questionEntity.getSolutions().stream()
41 | .map(solution -> modelMapper.map(solution, SolutionDto.class))
42 | .collect(Collectors.toList());
43 | questionDto.setSolutions(solutionDtos);
44 | }
45 | return questionDto;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/mappers/UserMapperImpl.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.mappers;
2 |
3 | import com.yls.ylslc.user.UserDto;
4 | import com.yls.ylslc.user.UserEntity;
5 | import org.modelmapper.ModelMapper;
6 | import org.springframework.stereotype.Component;
7 |
8 | @Component
9 | public class UserMapperImpl implements Mapper {
10 | private ModelMapper modelMapper;
11 |
12 | public UserMapperImpl(ModelMapper modelMapper) {
13 | this.modelMapper = modelMapper;
14 | }
15 |
16 | @Override
17 | public UserDto mapTo(UserEntity userEntity) {
18 | return modelMapper.map(userEntity, UserDto.class);
19 | }
20 |
21 | @Override
22 | public UserEntity mapFrom(UserDto userDto) {
23 | return modelMapper.map(userDto, UserEntity.class);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/node/NodeController.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.node;
2 |
3 | import com.yls.ylslc.config.response.Response;
4 | import com.yls.ylslc.mappers.Mapper;
5 | import org.springframework.http.HttpHeaders;
6 | import org.springframework.http.HttpStatus;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.http.ResponseEntity;
9 | import org.springframework.web.bind.annotation.*;
10 | import org.springframework.web.multipart.MultipartFile;
11 |
12 | import java.util.Map;
13 | import java.util.UUID;
14 |
15 | @RestController
16 | @RequestMapping("api/node")
17 | @CrossOrigin(origins = { "https://ylslc.org", "http://localhost:3000" })
18 | public class NodeController {
19 | private final NodeService nodeService;
20 | private final Mapper nodeMapper;
21 |
22 | @PostMapping(path = "/{id}")
23 | public Response createNode(@PathVariable("id") UUID dataStructureId, @RequestBody NodeDto nodeDto) {
24 | NodeEntity nodeEntity = nodeMapper.mapFrom(nodeDto);
25 | NodeEntity savedNodeEntity = nodeService.createNode(dataStructureId, nodeEntity);
26 | NodeDto savedNodeDto = nodeMapper.mapTo(savedNodeEntity);
27 | return Response.ok(savedNodeDto, "Node created successfully!");
28 | }
29 |
30 | @PatchMapping(path = "/{id}")
31 | public Response updateNodeByName(@PathVariable("id") Long id, @RequestBody Map updateRequest) {
32 | String name = updateRequest.get("name");
33 | if (!nodeService.isExist(id)) {
34 | return Response.failed(HttpStatus.NOT_FOUND, "Node not found!");
35 | }
36 | NodeEntity nodeEntity = nodeService.updateName(id, name);
37 | NodeDto updatedNode = nodeMapper.mapTo(nodeEntity);
38 | return Response.ok(updatedNode, "Node name is updated successfully");
39 | }
40 |
41 | @PatchMapping(path = "content/{id}")
42 | public Response updateNodeByContent(@PathVariable("id") Long id, @RequestBody Map updateRequest) {
43 | String content = updateRequest.get("content");
44 | if (!nodeService.isExist(id)) {
45 | return Response.failed(HttpStatus.NOT_FOUND, "Node not found!");
46 | }
47 | NodeEntity nodeEntity = nodeService.updateContent(id, content);
48 | NodeDto updatedNode = nodeMapper.mapTo(nodeEntity);
49 | return Response.ok(updatedNode, "Node content is updated successfully!");
50 | }
51 |
52 | @DeleteMapping(path = "/{id}")
53 | public Response deleteNode(@PathVariable("id") Long id) {
54 | NodeEntity nodeEntity = nodeService.delete(id);
55 | NodeDto deletedNode = nodeMapper.mapTo(nodeEntity);
56 | return Response.ok(deletedNode, deletedNode.getName() + (" deleted successfully!"));
57 | }
58 |
59 | @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE, path = "upload-image")
60 | public Response uploadContentImages(@RequestPart("image") MultipartFile image,
61 | @RequestPart("nodeId") String nodeId) {
62 | String imageId = nodeService.uploadImages(image, nodeId);
63 | return Response.ok(imageId, "Image saved successfully!");
64 | }
65 |
66 | @GetMapping("image/{nodeName}/{imageId}")
67 | public ResponseEntity getContentImage(@PathVariable String nodeName, @PathVariable String imageId) {
68 | byte[] imageData = nodeService.getImage(nodeName, imageId);
69 |
70 | HttpHeaders headers = new HttpHeaders();
71 | headers.setContentType(getMediaTypeForImageId(imageId));
72 |
73 | return new ResponseEntity<>(imageData, headers, HttpStatus.OK);
74 | }
75 |
76 | @DeleteMapping("image/{nodeId}/{imageId}")
77 | public Response deleteImage(@PathVariable String nodeId, @PathVariable String imageId) {
78 | Boolean deleted = nodeService.deleteImage(nodeId, imageId);
79 | if (deleted) {
80 | return Response.ok(true, "Image deleted successfully!");
81 | } else {
82 | return Response.failed(HttpStatus.BAD_REQUEST, "Image deleted unsuccessfully!");
83 | }
84 | }
85 |
86 | private MediaType getMediaTypeForImageId(String imageId) {
87 | if (imageId.endsWith(".png")) {
88 | return MediaType.IMAGE_PNG;
89 | } else if (imageId.endsWith(".jpg") || imageId.endsWith(".jpeg")) {
90 | return MediaType.IMAGE_JPEG;
91 | } else {
92 | return MediaType.APPLICATION_OCTET_STREAM;
93 | }
94 | }
95 |
96 | public NodeController(NodeService nodeService, Mapper nodeMapper) {
97 | this.nodeService = nodeService;
98 | this.nodeMapper = nodeMapper;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/node/NodeDto.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.node;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Builder;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | import java.time.LocalDateTime;
9 |
10 | @Data
11 | @AllArgsConstructor
12 | @NoArgsConstructor
13 | @Builder
14 | public class NodeDto {
15 | private Long id;
16 | private String name;
17 | private String content;
18 | private LocalDateTime createdAt;
19 | }
20 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/node/NodeEntity.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.node;
2 |
3 | import com.yls.ylslc.data_structure.DataStructureEntity;
4 | import jakarta.persistence.*;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 |
8 | import java.time.LocalDateTime;
9 |
10 | @Getter
11 | @Setter
12 | @Entity
13 | @Table(name = "node")
14 | public class NodeEntity {
15 | @Id
16 | @GeneratedValue(strategy = GenerationType.IDENTITY)
17 | private Long id;
18 |
19 | @ManyToOne
20 | @JoinColumn(name = "data_structure_id")
21 | private DataStructureEntity dataStructure;
22 |
23 | private String name;
24 |
25 | @Lob
26 | @Column(columnDefinition = "TEXT")
27 | private String content;
28 |
29 | private LocalDateTime createdAt;
30 |
31 | @PrePersist
32 | protected void onCreate() {
33 | createdAt = LocalDateTime.now();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/node/NodeRepository.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.node;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 | import org.springframework.stereotype.Repository;
5 |
6 | @Repository
7 | public interface NodeRepository extends JpaRepository {
8 | }
9 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/node/NodeService.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.node;
2 |
3 | import org.springframework.web.multipart.MultipartFile;
4 |
5 | import java.util.UUID;
6 |
7 | public interface NodeService {
8 | NodeEntity createNode(UUID dataStructureEntityId, NodeEntity nodeEntity);
9 |
10 | boolean isExist(Long id);
11 |
12 | NodeEntity updateName(Long id, String name);
13 |
14 | NodeEntity delete(Long id);
15 |
16 | byte[] getImage(String nodeId, String imageId);
17 |
18 | String uploadImages(MultipartFile image, String nodeId);
19 |
20 |
21 | NodeEntity updateContent(Long id, String content);
22 |
23 | Boolean deleteImage(String nodeId, String imageId);
24 | }
25 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/question/QuestionDto.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.question;
2 |
3 |
4 | import com.yls.ylslc.question.solution.SolutionDto;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Builder;
7 | import lombok.Data;
8 | import lombok.NoArgsConstructor;
9 |
10 | import java.time.LocalDate;
11 | import java.time.LocalDateTime;
12 | import java.util.List;
13 | import java.util.UUID;
14 |
15 | @Data
16 | @AllArgsConstructor
17 | @NoArgsConstructor
18 | @Builder
19 | public class QuestionDto {
20 | private UUID id;
21 | private Integer number;
22 | private String title;
23 | private String difficulty;
24 | private LocalDate dateOfCompletion;
25 | private Boolean success;
26 | private Integer attempts;
27 | private String timeOfCompletion;
28 | private List solutions;
29 | private Boolean star;
30 | private String reasonOfFail;
31 | private LocalDateTime createdAt;
32 | }
33 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/question/QuestionEntity.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.question;
2 |
3 | import com.yls.ylslc.question.solution.SolutionEntity;
4 | import com.yls.ylslc.user.UserEntity;
5 | import jakarta.persistence.*;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 |
9 | import java.time.LocalDate;
10 | import java.time.LocalDateTime;
11 | import java.util.ArrayList;
12 | import java.util.List;
13 | import java.util.UUID;
14 |
15 | @Getter
16 | @Setter
17 | @Entity
18 | @Table(name = "question")
19 | public class QuestionEntity {
20 | @Id
21 | @GeneratedValue
22 | private UUID id;
23 |
24 | @ManyToOne
25 | @JoinColumn(name = "user_id")
26 | private UserEntity user;
27 |
28 | @OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
29 | private List solutions = new ArrayList<>();
30 |
31 | private Integer number;
32 | private String title;
33 | private String difficulty;
34 | private LocalDate dateOfCompletion;
35 | private Boolean success;
36 | private Integer attempts;
37 | private String timeOfCompletion;
38 | private Boolean star;
39 | @Column(columnDefinition = "TEXT")
40 | private String reasonOfFail;
41 | private LocalDateTime createdAt;
42 |
43 | @PrePersist
44 | protected void onCreate() {
45 | createdAt = LocalDateTime.now();
46 | }
47 |
48 | public void addSolution(SolutionEntity solution) {
49 | solutions.add(solution);
50 | solution.setQuestion(this);
51 | }
52 |
53 | public void removeSolution(SolutionEntity solution) {
54 | solutions.remove(solution);
55 | solution.setQuestion(null);
56 | }
57 |
58 | public QuestionEntity() {
59 | this.id = UUID.randomUUID();
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/ylslc/src/main/java/com/yls/ylslc/question/QuestionRepository.java:
--------------------------------------------------------------------------------
1 | package com.yls.ylslc.question;
2 |
3 | import com.yls.ylslc.user.UserEntity;
4 | import org.springframework.data.domain.Page;
5 | import org.springframework.data.domain.Pageable;
6 | import org.springframework.data.jpa.repository.JpaRepository;
7 | import org.springframework.data.jpa.repository.Query;
8 | import org.springframework.data.repository.query.Param;
9 | import org.springframework.stereotype.Repository;
10 |
11 | import java.util.List;
12 | import java.util.Map;
13 | import java.util.Optional;
14 | import java.util.UUID;
15 |
16 | @Repository
17 | public interface QuestionRepository extends JpaRepository {
18 | Page findByUser(UserEntity currentUser, Pageable pageable);
19 |
20 | @Query("SELECT q FROM QuestionEntity q WHERE q.user = :user AND (LOWER(q.title) LIKE LOWER(CONCAT('%', :searchQuery, '%')) OR CAST(q.number AS string) LIKE %:searchQuery%)")
21 | Page searchByTitleOrNumber(@Param("user") UserEntity user,
22 | @Param("searchQuery") String searchQuery,
23 | Pageable pageable);
24 |
25 | @Query("SELECT q FROM QuestionEntity q WHERE q.id = :id AND q.user.username = :username")
26 | Optional findByIdAndUsername(@Param("id") UUID id, @Param("username") String username);
27 |
28 | @Query("SELECT COUNT(q) FROM QuestionEntity q WHERE q.user.id = :userId")
29 | long countQuestionsByUserId(@Param("userId") UUID userId);
30 |
31 | @Query("SELECT q.difficulty AS difficulty, COUNT(q) AS count FROM QuestionEntity q WHERE q.user.id = :userId GROUP BY q.difficulty")
32 | List