├── .github └── workflows │ ├── prettier.yaml │ └── production.deployment.yaml ├── .gitignore ├── Makefile ├── backend ├── .dockerignore ├── .env.example ├── .prettierrc ├── Dockerfile.dev ├── Dockerfile.prod ├── package-lock.json ├── package.json ├── src │ ├── api │ │ ├── controller │ │ │ ├── admin.controller.ts │ │ │ ├── index.ts │ │ │ ├── student.controller.ts │ │ │ └── validator.ts │ │ ├── errors │ │ │ ├── badrequest.error.ts │ │ │ ├── custom.error.ts │ │ │ ├── database.connection.error.ts │ │ │ ├── index.ts │ │ │ ├── notfound.error.ts │ │ │ └── validation.errror.ts │ │ ├── middleware │ │ │ ├── auth.middleware.ts │ │ │ ├── error.handler.middleware.ts │ │ │ ├── index.ts │ │ │ └── validator.middleware.ts │ │ └── utils.ts │ ├── app.ts │ ├── config │ │ ├── db.config.ts │ │ ├── index.ts │ │ ├── logger.ts │ │ └── whatsapp.ts │ ├── database │ │ ├── model │ │ │ ├── admin.model.ts │ │ │ ├── index.ts │ │ │ ├── students.model.ts │ │ │ └── weeklymetrics.model.ts │ │ └── repository │ │ │ ├── admin.repository.ts │ │ │ ├── index.ts │ │ │ ├── student.repository.ts │ │ │ └── weeklymetrics.repository.ts │ ├── handler │ │ ├── cronjob.ts │ │ ├── leetcode-updater.ts │ │ ├── leetcode.ts │ │ └── utils.ts │ └── index.ts └── tsconfig.json ├── docker-compose-dev.yaml ├── docker-compose-prod.yaml ├── docs ├── .nojekyll ├── CNAME ├── README.md └── index.html ├── frontend ├── .dockerignore ├── .env.development ├── .env.production ├── .eslintrc.cjs ├── .prettierrc ├── .vite │ └── deps_temp_1c3d506d │ │ └── package.json ├── Dockerfile.dev ├── Dockerfile.prod ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── 81414-middle.png │ ├── android-chrome-192x192.png │ ├── istockphoto-1337144146-612x612.jpg │ ├── logo-black.png │ └── vite.svg ├── src │ ├── App.css │ ├── App.tsx │ ├── Features │ │ ├── Analytic.tsx │ │ ├── LeaderBorde.tsx │ │ └── StudentsDetails.tsx │ ├── components │ │ ├── AllStudentData.tsx │ │ ├── ErrorComponent.tsx │ │ ├── LeaderBoard.tsx │ │ ├── LeaderBoardStatic.tsx │ │ ├── LineChart.tsx │ │ ├── Login.tsx │ │ ├── Navbar.tsx │ │ ├── PieData.tsx │ │ ├── Sidebar.tsx │ │ ├── StudentLogin.tsx │ │ ├── StudentsDataUpdate.tsx │ │ ├── StudentsNotdone.tsx │ │ └── alert.tsx │ ├── index.css │ ├── main.tsx │ ├── pages │ │ └── Home.tsx │ ├── utils │ │ ├── api │ │ │ ├── api.Types │ │ │ │ ├── axios.Getapi.Typses.ts │ │ │ │ └── axios.Postapi.Types.ts │ │ │ └── config │ │ │ │ ├── axios.Config.ts │ │ │ │ ├── axios.DeleteApi.ts │ │ │ │ ├── axios.GetApi.ts │ │ │ │ └── axios.PostAPi.ts │ │ ├── routes │ │ │ └── ProtectedRoute.tsx │ │ └── validation │ │ │ └── formValidation.tsx │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── installer.sh └── proxy ├── Dockerfile └── nginx.conf /.github/workflows/prettier.yaml: -------------------------------------------------------------------------------- 1 | name: Prettier 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - dev 7 | paths: 8 | - "backend/**" 9 | jobs: 10 | prettier: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v2 15 | - name: run Prettier 16 | run: | 17 | cd backend 18 | npm install 19 | npm run prettier 20 | -------------------------------------------------------------------------------- /.github/workflows/production.deployment.yaml: -------------------------------------------------------------------------------- 1 | name: Deploying To Production 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: ssh and rolling out deployment 10 | uses: appleboy/ssh-action@v0.1.2 11 | with: 12 | host: ${{ secrets.HOST }} 13 | username: ${{ secrets.USERNAME }} 14 | password: ${{ secrets.PASSWORD }} 15 | script: | 16 | cd LeetCode_Tracker 17 | git fetch origin master 18 | git reset --hard origin/master 19 | make down-prod 20 | make prod 21 | exit 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | db 4 | .env.development 5 | .env.production 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | 15 | node_modules 16 | dist 17 | dist-ssr 18 | *.local 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | # Targets 4 | .PHONY: prod dev 5 | 6 | prod: 7 | docker compose -f docker-compose-prod.yaml up --build -d 8 | 9 | dev: 10 | docker compose -f docker-compose-dev.yaml up --build -d 11 | 12 | down-dev: 13 | docker compose -f docker-compose-dev.yaml down 14 | 15 | down-prod: 16 | docker compose -f docker-compose-prod.yaml down -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | node_modules/* 3 | 4 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | PORT=4000 2 | MONGO_URI=mongodb://mongodb:27017/leetcode-checker 3 | JWT_SECRET='YOURSECRET' 4 | JWT_EXPIRE = '7d' 5 | 6 | 7 | -------------------------------------------------------------------------------- /backend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 4, 4 | "useTabs": true, 5 | "printWidth": 140, 6 | "singleQuote": false, 7 | "trailingComma": "none", 8 | "jsxBracketSameLine": true, 9 | "bracketSameLine": true, 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /backend/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | EXPOSE 4000 12 | 13 | ENV NODE_ENV=dev 14 | 15 | CMD ["npm","run","dev"] 16 | -------------------------------------------------------------------------------- /backend/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 4000 14 | 15 | ENV NODE_ENV=production 16 | 17 | CMD ["npm","start"] 18 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leetcode_tracker", 3 | "version": "1.0.0", 4 | "description": "A leetcode daily challenge tracker to monitor student's perfomance in Brocamp", 5 | "author": "Packapeer Academy Pvt ltd", 6 | "main": "src/index.ts", 7 | "scripts": { 8 | "start": "node build/index.js", 9 | "build": "tsc", 10 | "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts", 11 | "prettier": "prettier --write ." 12 | }, 13 | "license": "MIT", 14 | "dependencies": { 15 | "@clack/prompts": "^0.7.0", 16 | "axios": "^1.5.0", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.3.1", 19 | "express": "^4.18.2", 20 | "express-async-errors": "^3.1.1", 21 | "express-mongo-sanitize": "^2.2.0", 22 | "express-rate-limit": "^7.1.1", 23 | "express-validator": "^7.0.1", 24 | "helmet": "^7.0.0", 25 | "jsonwebtoken": "^9.0.2", 26 | "leetcode-query": "^0.2.7", 27 | "mongoose": "^7.5.0", 28 | "node-cron": "^3.0.2", 29 | "p-limit": "^3.1.0", 30 | "picocolors": "^1.0.0", 31 | "qrcode-terminal": "^0.12.0", 32 | "winston": "^3.10.0", 33 | "xss-clean": "^0.1.4" 34 | }, 35 | "devDependencies": { 36 | "@types/cors": "^2.8.13", 37 | "@types/express": "^4.17.17", 38 | "@types/jsonwebtoken": "^9.0.2", 39 | "@types/morgan": "^1.9.5", 40 | "@types/node": "^20.5.4", 41 | "@types/node-cron": "^3.0.8", 42 | "@types/qrcode-terminal": "^0.12.0", 43 | "morgan": "^1.10.0", 44 | "nodemon": "^3.0.1", 45 | "prettier": "^3.0.2", 46 | "ts-node": "^10.9.1" 47 | }, 48 | "engines": { 49 | "node": ">=18.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /backend/src/api/controller/admin.controller.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import * as jwt from "jsonwebtoken"; 3 | import { IPayload, reqAuth, validateRequest } from "../middleware"; 4 | import { SendOTP } from "../utils"; 5 | import { messageValidator, otpValidator, signinValidator } from "./validator"; 6 | import { AdminRepository } from "../../database/repository"; 7 | import { BadRequestError } from "../errors"; 8 | 9 | const router = express.Router(); 10 | 11 | const repository = new AdminRepository(); 12 | 13 | router.post("/signin", signinValidator, validateRequest, async (req: Request, res: Response) => { 14 | const { phone, resend } = req.body as { phone: string; resend: boolean }; 15 | const Admin = await repository.findByPhone(phone); 16 | if (!Admin) throw new BadRequestError("admin dosent exist"); 17 | const otp = Math.floor(1000 + Math.random() * 9000); 18 | Admin.otp = otp; 19 | await Admin.save(); 20 | await SendOTP(Admin.otp as number, Admin.phone); 21 | res.status(200).json({ message: "otp sended to your mobile number" }); 22 | }); 23 | 24 | router.post("/verify-otp", otpValidator, validateRequest, async (req: Request, res: Response) => { 25 | const { otp, phone } = req.body; 26 | const admin = await repository.findByPhone(phone); 27 | if (!admin) throw new BadRequestError("no admin found"); 28 | const isOtpCorrect = admin.otp == otp; 29 | if (!isOtpCorrect) throw new BadRequestError("otp is not correct"); 30 | admin.otp = null; 31 | await admin.save(); 32 | const payload: IPayload = { 33 | userId: admin._id, 34 | phone: admin.phone, 35 | role: "admin" 36 | }; 37 | const token = jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: process.env.JWT_EXPIRE! }); 38 | res.status(200).json({ token }); 39 | }); 40 | 41 | router.get("/profile", reqAuth, async (req: Request, res: Response) => { 42 | const userId = req.user?.userId as string; 43 | const admin = await repository.findById(userId); 44 | if (!admin) throw new BadRequestError("no admin found"); 45 | res.json(admin); 46 | }); 47 | 48 | router.post("/message", reqAuth, messageValidator, validateRequest, async (req: Request, res: Response) => { 49 | // NEED TO IMPLEMENT 50 | res.status(200).json({ message: "okay" }); 51 | }); 52 | 53 | export { router as AdminRouter }; 54 | -------------------------------------------------------------------------------- /backend/src/api/controller/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./admin.controller"; 2 | export * from "./student.controller"; 3 | -------------------------------------------------------------------------------- /backend/src/api/controller/student.controller.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from "express"; 2 | import { reqAuth, validateRequest } from "../middleware"; 3 | import { StudentDTO } from "../../database/model"; 4 | import { studentValidator } from "./validator"; 5 | import { getProfile } from "../../handler/leetcode"; 6 | import { BadRequestError } from "../errors"; 7 | import { StudentRepository, WeeklymetricsRepository } from "../../database/repository"; 8 | 9 | const router = express.Router(); 10 | const studentRepository = new StudentRepository(); 11 | const weeklymetricsRepository = new WeeklymetricsRepository(); 12 | 13 | router.post("/add", studentValidator, validateRequest, async (req: Request, res: Response) => { 14 | const data = req.body as StudentDTO; 15 | const userId = await getProfile(data.leetcodeId); 16 | if (!userId?.matchedUser) throw new BadRequestError("LeetCodeId dosen't exist"); 17 | const phone = await studentRepository.findByPhone(data.phone); 18 | if (phone) throw new BadRequestError("Phone number already registerd"); 19 | let student = await studentRepository.findByLeetCodeId(data.leetcodeId); 20 | if (student) throw new BadRequestError("This profile already exist"); 21 | const result = await studentRepository.create(data); 22 | res.status(200).json({ message: "Successfully added to database", result }); 23 | }); 24 | 25 | router.get("/daily-metrics", reqAuth, async (req: Request, res: Response) => { 26 | const studentsSolvedCount = await weeklymetricsRepository.getLastDaySubmissionCount(); 27 | //get total count 28 | const totalCount = await studentRepository.countStudents(); 29 | const matrics = { 30 | totalStudents: totalCount, 31 | yesterdaySolvedStudentsCount: studentsSolvedCount[0]?.totalStudentsSolved || 0 32 | }; 33 | res.status(200).json(matrics); 34 | }); 35 | 36 | router.get("/leaderboard", reqAuth, async (req: Request, res: Response) => { 37 | const topLeetcodeSolvers = await studentRepository.leaderBoard(); 38 | const rank = { rank: topLeetcodeSolvers }; 39 | res.status(200).json(rank); 40 | }); 41 | 42 | router.get("/all", reqAuth, async (req: Request, res: Response) => { 43 | const page = Number(req.query.page) || 1; 44 | const limit = Number(req.query.limit) || 10; 45 | const result = await studentRepository.findAll(limit, page); 46 | res.json({ result }); 47 | }); 48 | 49 | router.get("/search", reqAuth, async (req: Request, res: Response) => { 50 | const query = req.query.query as string; 51 | const result = await studentRepository.search(query); 52 | res.json({ result }); 53 | }); 54 | 55 | router.get("/search/not", reqAuth, async (req: Request, res: Response) => { 56 | const query = req.query.query as string; 57 | const result = await studentRepository.searchNotDone(query); 58 | res.json({ result }); 59 | }); 60 | 61 | router.get("/not-doing", reqAuth, async (req: Request, res: Response) => { 62 | const result = await studentRepository.findStudentsNotDone(); 63 | res.json({ result }); 64 | }); 65 | 66 | router.get("/weekly-metrics", reqAuth, async (req: Request, res: Response) => { 67 | let lastWeekReport = await weeklymetricsRepository.weeklyMetrics(); 68 | lastWeekReport = lastWeekReport.reverse(); 69 | res.json({ lastWeekReport }); 70 | }); 71 | 72 | router.post("/edit/:id", reqAuth, async (req: Request, res: Response) => { 73 | const data = req.body as StudentDTO; 74 | const id = req.params.id as string; 75 | const student = await studentRepository.findById(id); 76 | if (!student) throw new BadRequestError("No student found"); 77 | if (data.leetcodeId !== '' || data.leetcodeId !== undefined) { 78 | const idExist = await getProfile(data.leetcodeId); 79 | if (idExist?.matchedUser === null) throw new BadRequestError("No leetcode id exist"); 80 | } 81 | const result = await studentRepository.editProfile(id, data); 82 | res.json(202).json(result); 83 | }); 84 | 85 | router.delete("/delete/:id", reqAuth, async (req: Request, res: Response) => { 86 | const id = req.params.id as string; 87 | const student = await studentRepository.findById(id); 88 | if (!student) throw new BadRequestError("student not exist"); 89 | await studentRepository.deleteStudent(id); 90 | res.status(200).json({ message: "Data deleted" }); 91 | }); 92 | 93 | export { router as StudentRouter }; 94 | -------------------------------------------------------------------------------- /backend/src/api/controller/validator.ts: -------------------------------------------------------------------------------- 1 | import { body } from "express-validator"; 2 | 3 | const Domains = [ 4 | "MEAN", 5 | "MERN", 6 | "PYTHON", 7 | "GO", 8 | "JAVA", 9 | "RUBY", 10 | "SWIFT", 11 | "FLUTTER", 12 | ".NET", 13 | "ML", 14 | "DATASCIENCE", 15 | "DATAENGINEERING", 16 | "CYBERSECURITY", 17 | "NODEJS", 18 | "DEVOPS", 19 | "LOWCODE", 20 | "GAMEDEVELOPEMENT", 21 | "KOTLIN" 22 | ]; 23 | 24 | export const studentValidator = [ 25 | // Name validation 26 | body("name") 27 | .notEmpty() 28 | .trim() 29 | .withMessage("Name is required") 30 | .bail() 31 | .isLength({ min: 2 }) 32 | .withMessage("Name should be at least 2 characters long") 33 | .isLength({ max: 50 }) 34 | .withMessage("Name should be less than 50 characters"), 35 | 36 | // Batch validation 37 | body("batch") 38 | .notEmpty() 39 | .trim() 40 | .withMessage("Batch is required") 41 | .bail() 42 | .isLength({ min: 2 }) 43 | .withMessage("Batch should be at least 2 characters long") 44 | .isLength({ max: 10 }) 45 | .withMessage("Batch should be less than 10 characters") 46 | .toUpperCase(), 47 | 48 | // Last name validation 49 | body("lastName") 50 | .notEmpty() 51 | .trim() 52 | .withMessage("LastName is required") 53 | .bail() 54 | .isLength({ min: 1 }) 55 | .withMessage("LastName should be at least 1 characters long") 56 | .isLength({ max: 50 }) 57 | .withMessage("LastName should be less than 50 characters"), 58 | 59 | // Domain validation 60 | body("domain") 61 | .notEmpty() 62 | .trim() 63 | .withMessage("Domain is required") 64 | .bail() 65 | .isIn(Domains) 66 | .withMessage(`Invalid Domain!Please select valid Domain name: ${Domains}`), 67 | 68 | // Phone validation 69 | body("phone") 70 | .notEmpty() 71 | .trim() 72 | .withMessage("Phone number is required") 73 | .bail() 74 | .isMobilePhone("en-IN", { strictMode: false }) 75 | .withMessage("Invalid phone number"), 76 | 77 | // Email validation 78 | body("email").notEmpty().trim().withMessage("Email is required").bail().isEmail().withMessage("Invalid email address").normalizeEmail(), 79 | 80 | // LeetCode ID validation 81 | body("leetcodeId") 82 | .notEmpty() 83 | .trim() 84 | .bail() 85 | .withMessage("LeetCode ID is required") 86 | .isLength({ min: 1 }) 87 | .withMessage("LeetCode ID should be at least 1 characters long") 88 | .isLength({ max: 40 }) 89 | .withMessage("LeetCode ID should be less than 40 characters") 90 | ]; 91 | 92 | export const signinValidator = [ 93 | body("phone") 94 | .notEmpty() 95 | .trim() 96 | .withMessage("Phone number is required") 97 | .bail() 98 | .isMobilePhone("en-IN", { strictMode: false }) 99 | .withMessage("Invalid phone number") 100 | ]; 101 | 102 | export const otpValidator = [ 103 | body("phone") 104 | .notEmpty() 105 | .trim() 106 | .withMessage("Phone number is required") 107 | .bail() 108 | .isMobilePhone("en-IN", { strictMode: false }) 109 | .withMessage("Invalid phone number"), 110 | 111 | body("otp") 112 | .notEmpty() 113 | .withMessage("OTP is required") 114 | .bail() 115 | .isLength({ min: 4, max: 4 }) 116 | .withMessage("OTP should be 4 digits") 117 | .bail() 118 | .matches(/^\d+$/) 119 | .withMessage("OTP should contain only digits") 120 | ]; 121 | 122 | export const messageValidator = [ 123 | body("message") 124 | .notEmpty() 125 | .trim() 126 | .withMessage("Message is required") 127 | .bail() 128 | .isLength({ min: 2 }) 129 | .withMessage("Message should be at least 2 characters long") 130 | ]; 131 | -------------------------------------------------------------------------------- /backend/src/api/errors/badrequest.error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom.error"; 2 | 3 | export class BadRequestError extends CustomError { 4 | statusCode = 400; 5 | constructor(public message: string) { 6 | super(message); 7 | Object.setPrototypeOf(this, BadRequestError.prototype); 8 | } 9 | serializeErrors() { 10 | return [{ message: this.message }]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/api/errors/custom.error.ts: -------------------------------------------------------------------------------- 1 | export abstract class CustomError extends Error { 2 | abstract statusCode: number; 3 | constructor(message: string) { 4 | super(message); 5 | Object.setPrototypeOf(this, CustomError.prototype); 6 | } 7 | abstract serializeErrors(): { message: string; field?: string }[]; 8 | } 9 | -------------------------------------------------------------------------------- /backend/src/api/errors/database.connection.error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom.error"; 2 | 3 | export class DatabaseConnectionError extends CustomError { 4 | statusCode = 500; 5 | reason = "Error connecting to database"; 6 | constructor() { 7 | super("Error connecting to db"); 8 | 9 | Object.setPrototypeOf(this, DatabaseConnectionError.prototype); 10 | } 11 | serializeErrors() { 12 | return [{ message: this.reason }]; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/api/errors/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./database.connection.error"; 2 | export * from "./badrequest.error"; 3 | export * from "./custom.error"; 4 | export * from "./notfound.error"; 5 | export * from "./validation.errror"; 6 | -------------------------------------------------------------------------------- /backend/src/api/errors/notfound.error.ts: -------------------------------------------------------------------------------- 1 | import { CustomError } from "./custom.error"; 2 | 3 | export class NotFoundError extends CustomError { 4 | statusCode = 404; 5 | constructor() { 6 | super("Route not found"); 7 | Object.setPrototypeOf(this, NotFoundError.prototype); 8 | } 9 | serializeErrors() { 10 | return [{ message: "Not Found" }]; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/api/errors/validation.errror.ts: -------------------------------------------------------------------------------- 1 | import { ValidationError } from "express-validator"; 2 | import { CustomError } from "./custom.error"; 3 | 4 | export class RequestValidationError extends CustomError { 5 | statusCode = 400; 6 | 7 | constructor(public errors: ValidationError[]) { 8 | super("Invalid request parameters"); 9 | 10 | Object.setPrototypeOf(this, RequestValidationError.prototype); 11 | } 12 | 13 | serializeErrors() { 14 | return this.errors.map((err) => { 15 | return { message: err.msg }; 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/api/middleware/auth.middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, Response } from "express"; 2 | import * as jwt from "jsonwebtoken"; 3 | import { BadRequestError } from "../errors"; 4 | 5 | export interface IPayload { 6 | userId: string; 7 | phone: string; 8 | role: "admin" | "student"; 9 | } 10 | 11 | declare global { 12 | namespace Express { 13 | interface Request { 14 | user?: IPayload; 15 | } 16 | } 17 | } 18 | 19 | export const reqAuth = async (req: Request, res: Response, next: NextFunction) => { 20 | const token = req.header("Authorization")?.replace("Bearer ", ""); 21 | if (!token) { 22 | throw new BadRequestError("UnAuthorized Request"); 23 | } 24 | try { 25 | const decoed = jwt.verify(token, process.env.JWT_SECRET!) as IPayload; 26 | req.user = decoed; 27 | next(); 28 | } catch (error) { 29 | throw new BadRequestError("UnAuthorized Request"); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /backend/src/api/middleware/error.handler.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { CustomError } from "../errors"; 3 | import { logger } from "../../config"; 4 | 5 | export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => { 6 | try { 7 | if (err instanceof CustomError) { 8 | return res.status(err.statusCode).send({ errors: err.serializeErrors() }); 9 | } 10 | logger.error(err.stack || err); 11 | res.status(400).send({ 12 | errors: [{ message: err.message }] 13 | }); 14 | } catch (error) { 15 | logger.error(error); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/api/middleware/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth.middleware"; 2 | export * from "./error.handler.middleware"; 3 | export * from "./validator.middleware"; 4 | -------------------------------------------------------------------------------- /backend/src/api/middleware/validator.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { validationResult } from "express-validator"; 3 | import { RequestValidationError } from "../errors"; 4 | 5 | export const validateRequest = (req: Request, res: Response, next: NextFunction) => { 6 | const errors = validationResult(req); 7 | if (!errors.isEmpty()) { 8 | throw new RequestValidationError(errors.array()); 9 | } 10 | 11 | next(); 12 | }; 13 | -------------------------------------------------------------------------------- /backend/src/api/utils.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | /** 4 | * The SendOTP function sends an OTP (One-Time Password) to a specified phone number using the Fast2SMS 5 | * API. 6 | * @param {number} otp - The `otp` parameter is the one-time password that you want to send to the 7 | * user's phone number for verification. 8 | * @param {string} phone - The `phone` parameter is a string that represents the phone number to which 9 | * the OTP (One-Time Password) will be sent. 10 | */ 11 | 12 | export const SendOTP = async (otp: number, phone: string) => { 13 | try { 14 | const apiUrl = "https://www.fast2sms.com/dev/bulkV2"; 15 | const apiKey = process.env.NODE_ENV == "dev" ? "" : process.env.OTP_API_KEY; 16 | const requestData = { 17 | variables_values: String(otp), 18 | route: "otp", 19 | numbers: phone 20 | }; 21 | 22 | const headers = { 23 | authorization: apiKey 24 | }; 25 | 26 | await axios.post(apiUrl, requestData, { headers }); 27 | } catch (error) { 28 | console.log(error); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /backend/src/app.ts: -------------------------------------------------------------------------------- 1 | import "express-async-errors"; 2 | import express, { Request, Response } from "express"; 3 | import cors from "cors"; 4 | import helmet from "helmet"; 5 | import rateLimiter from "express-rate-limit"; 6 | import mongoSanitize from "express-mongo-sanitize"; 7 | 8 | import { NotFoundError } from "./api/errors"; 9 | import { AdminRouter, StudentRouter } from "./api/controller"; 10 | import { errorHandler } from "./api/middleware"; 11 | import { morganMiddleware } from "./config"; 12 | 13 | const app = express(); 14 | 15 | app.use(express.json()); 16 | 17 | app.set("trust proxy", 1); 18 | app.use( 19 | rateLimiter({ 20 | windowMs: 15 * 60 * 1000, 21 | max: 60 22 | }) 23 | ); 24 | 25 | app.use(helmet()); 26 | app.use(cors()); 27 | app.use(mongoSanitize()); 28 | 29 | app.use(cors({ origin: "*" })); 30 | 31 | app.set("trust proxy", 1); 32 | 33 | app.use(morganMiddleware); 34 | 35 | app.use("/api/admin", AdminRouter); 36 | 37 | app.use("/api/student", StudentRouter); 38 | 39 | app.all("*", (req: Request, res: Response) => { 40 | throw new NotFoundError(); 41 | }); 42 | 43 | app.use(errorHandler); 44 | 45 | export default app; 46 | -------------------------------------------------------------------------------- /backend/src/config/db.config.ts: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { DatabaseConnectionError } from "../api/errors"; 3 | 4 | export const connectMongoDb = async (connectionUri: string) => { 5 | try { 6 | await mongoose.connect(connectionUri); 7 | console.log("Database Connected Successfully"); 8 | } catch (error) { 9 | throw new DatabaseConnectionError(); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./db.config"; 2 | export * from "./logger"; 3 | -------------------------------------------------------------------------------- /backend/src/config/logger.ts: -------------------------------------------------------------------------------- 1 | import morgan from "morgan"; 2 | import winston from "winston"; 3 | const { combine, timestamp, json, errors } = winston.format; 4 | 5 | /* The code block is creating a logger object using the 6 | Winston library. */ 7 | const logger = winston.createLogger({ 8 | level: "error", 9 | format: combine(timestamp(), json()), 10 | transports: [ 11 | new winston.transports.File({ 12 | filename: "error.log", 13 | level: "error", 14 | format: combine(errors({ stack: true }), timestamp(), json()) 15 | }) 16 | ] 17 | }); 18 | 19 | /* The code block is creating a logger object called `reqLogger` using the Winston library. This logger 20 | is specifically configured to handle HTTP request logs. */ 21 | const reqLogger = winston.createLogger({ 22 | level: "http", 23 | format: combine( 24 | timestamp({ 25 | format: "YYYY-MM-DD hh:mm:ss.SSS A" 26 | }), 27 | json() 28 | ), 29 | transports: [ 30 | new winston.transports.Console(), 31 | new winston.transports.File({ 32 | filename: "app-info.log", 33 | level: "http" 34 | }) 35 | ] 36 | }); 37 | 38 | /* The `morganMiddleware` constant is creating a middleware function using the `morgan` library. This 39 | middleware function is used to log HTTP request information. */ 40 | const morganMiddleware = morgan( 41 | function (tokens, req, res) { 42 | return JSON.stringify({ 43 | method: tokens.method(req, res), 44 | url: tokens.url(req, res), 45 | status: Number.parseFloat(tokens.status(req, res)!), 46 | content_length: tokens.res(req, res, "content-length"), 47 | response_time: Number.parseFloat(tokens["response-time"](req, res)!) 48 | }); 49 | }, 50 | { 51 | stream: { 52 | write: (message) => { 53 | const data = JSON.parse(message); 54 | reqLogger.http(`incoming-request`, data); 55 | } 56 | } 57 | } 58 | ); 59 | 60 | export { logger, morganMiddleware }; 61 | -------------------------------------------------------------------------------- /backend/src/config/whatsapp.ts: -------------------------------------------------------------------------------- 1 | // will implement in future 2 | -------------------------------------------------------------------------------- /backend/src/database/model/admin.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document, Schema } from "mongoose"; 2 | 3 | export interface IAdmin extends Document { 4 | name: string; 5 | phone: string; 6 | otp: number | null; 7 | isVerified: boolean; 8 | } 9 | 10 | const adminSchema = new Schema({ 11 | name: { 12 | type: String, 13 | required: true 14 | }, 15 | phone: { 16 | type: String, 17 | required: true 18 | }, 19 | otp: { 20 | type: Number 21 | }, 22 | isVerified: { 23 | type: Boolean, 24 | required: true 25 | } 26 | }); 27 | 28 | export const Admin = mongoose.model("Admin", adminSchema); 29 | -------------------------------------------------------------------------------- /backend/src/database/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./admin.model"; 2 | export * from "./students.model"; 3 | export * from "./weeklymetrics.model"; 4 | -------------------------------------------------------------------------------- /backend/src/database/model/students.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document, Schema } from "mongoose"; 2 | 3 | export interface Solved { 4 | all: number; 5 | easy: number; 6 | medium: number; 7 | hard: number; 8 | } 9 | 10 | export interface StudentDTO { 11 | name: string; 12 | lastName: string; 13 | batch: string; 14 | domain: string; 15 | phone: string; 16 | email: string; 17 | leetcodeId: string; 18 | } 19 | 20 | export interface IStudent extends Document { 21 | name: string; 22 | lastName: string; 23 | batch: string; 24 | domain: string; 25 | phone: string; 26 | email: string; 27 | leetcodeId: string; 28 | solved: Solved; 29 | totalNotSubmissionCount: number; 30 | lastSubmissionDate: string; 31 | totalSolvedCountInThisWeek: number; 32 | solvedQuestionsInThisWeek: string[]; 33 | } 34 | 35 | const studentSchema = new Schema( 36 | { 37 | name: { 38 | type: String, 39 | required: true 40 | }, 41 | lastName: { 42 | type: String, 43 | required: true 44 | }, 45 | batch: { 46 | type: String, 47 | required: true 48 | }, 49 | domain: { 50 | type: String, 51 | required: true 52 | }, 53 | phone: { 54 | type: String, 55 | required: true 56 | }, 57 | email: { 58 | type: String, 59 | required: true 60 | }, 61 | leetcodeId: { 62 | type: String, 63 | unique: true, 64 | required: true 65 | }, 66 | solved: { 67 | all: { type: Number, default: 0 }, 68 | easy: { type: Number, default: 0 }, 69 | medium: { type: Number, default: 0 }, 70 | hard: { type: Number, default: 0 } 71 | }, 72 | totalNotSubmissionCount: { 73 | type: Number, 74 | default: 0 75 | }, 76 | lastSubmissionDate: { 77 | type: String 78 | }, 79 | totalSolvedCountInThisWeek: { 80 | type: Number, 81 | default: 0 82 | }, 83 | solvedQuestionsInThisWeek: { 84 | type: [String], 85 | default: [] 86 | } 87 | }, 88 | { 89 | toJSON: { 90 | transform(doc, ret) { 91 | delete ret.__v; 92 | } 93 | } 94 | } 95 | ); 96 | 97 | export const Students = mongoose.model("Student", studentSchema); 98 | -------------------------------------------------------------------------------- /backend/src/database/model/weeklymetrics.model.ts: -------------------------------------------------------------------------------- 1 | import mongoose, { Document, Schema } from "mongoose"; 2 | 3 | export interface IWeeklymetrics extends Document { 4 | totalStudentsSolved: number; 5 | day: string; 6 | } 7 | 8 | const weeklymetricsSchema = new Schema( 9 | { 10 | totalStudentsSolved: { 11 | type: Number, 12 | required: true 13 | }, 14 | day: { 15 | type: String, 16 | required: true 17 | } 18 | }, 19 | { 20 | timestamps: true 21 | } 22 | ); 23 | 24 | export const WeeklyMetrics = mongoose.model("WeeklyMetric", weeklymetricsSchema); 25 | -------------------------------------------------------------------------------- /backend/src/database/repository/admin.repository.ts: -------------------------------------------------------------------------------- 1 | import { IAdmin, Admin } from "../model"; 2 | 3 | export class AdminRepository { 4 | async create(adminData: Partial): Promise { 5 | const admin = await Admin.create(adminData); 6 | return admin; 7 | } 8 | 9 | async findById(id: string): Promise { 10 | const admin = await Admin.findById(id).select({ otp: 0, _id: 0 }).exec(); 11 | return admin; 12 | } 13 | 14 | async findByPhone(phone: string): Promise { 15 | const admin = await Admin.findOne({ phone }).exec(); 16 | return admin; 17 | } 18 | 19 | async update(id: string, adminData: Partial): Promise { 20 | const admin = await Admin.findByIdAndUpdate(id, adminData, { new: true }).exec(); 21 | return admin; 22 | } 23 | 24 | async delete(id: string): Promise { 25 | await Admin.findByIdAndDelete(id).exec(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/src/database/repository/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./admin.repository"; 2 | export * from "./student.repository"; 3 | export * from "./weeklymetrics.repository"; 4 | -------------------------------------------------------------------------------- /backend/src/database/repository/student.repository.ts: -------------------------------------------------------------------------------- 1 | import { IStudent, StudentDTO, Students } from "../model"; 2 | 3 | interface ILeaderBoard { 4 | name: string; 5 | leetcodeId: string; 6 | totalSolvedCountInThisWeek: number; 7 | } 8 | 9 | export class StudentRepository { 10 | async create(user: StudentDTO): Promise { 11 | return Students.create(user); 12 | } 13 | 14 | async findById(userId: string): Promise { 15 | return Students.findById(userId); 16 | } 17 | 18 | async find() { 19 | return await Students.find({}); 20 | } 21 | 22 | async findAll(limit: number, page: number) { 23 | const totalStudents = await Students.countDocuments(); 24 | const totalPages = Math.ceil(totalStudents / limit); 25 | const skip = (page - 1) * limit; 26 | const students = await Students.find().skip(skip).limit(limit).exec(); 27 | return { 28 | totalStudents, 29 | totalPages, 30 | currentPage: page, 31 | students 32 | }; 33 | } 34 | 35 | async update(userId: string, updates: Partial): Promise { 36 | return Students.findByIdAndUpdate(userId, updates, { new: true }); 37 | } 38 | 39 | async findByLeetCodeId(userId: string) { 40 | const student = await Students.findOne({ leetcodeId: userId }); 41 | return student; 42 | } 43 | 44 | async search(query: string) { 45 | const fuzzyQuery = new RegExp(this.escapeRegex(query), "gi"); 46 | 47 | const result = await Students.find({ 48 | $or: [ 49 | { 50 | name: { $regex: fuzzyQuery } 51 | }, 52 | { 53 | batch: { $regex: fuzzyQuery } 54 | }, 55 | { 56 | domain: { $regex: fuzzyQuery } 57 | }, 58 | { 59 | email: query 60 | }, 61 | { 62 | leetcodeId: query 63 | } 64 | ] 65 | }); 66 | 67 | return result; 68 | } 69 | 70 | async searchNotDone(query: string) { 71 | const fuzzyQuery = new RegExp(this.escapeRegex(query), "gi"); 72 | 73 | const result = await Students.find({ 74 | $or: [ 75 | { 76 | name: { $regex: fuzzyQuery }, 77 | totalNotSubmissionCount: { $gt: 3 } 78 | }, 79 | { 80 | batch: { $regex: fuzzyQuery }, 81 | totalNotSubmissionCount: { $gt: 3 } 82 | }, 83 | { 84 | domain: { $regex: fuzzyQuery }, 85 | totalNotSubmissionCount: { $gt: 3 } 86 | }, 87 | { 88 | email: query, 89 | totalNotSubmissionCount: { $gt: 3 } 90 | }, 91 | { 92 | leetcodeId: query, 93 | totalNotSubmissionCount: { $gt: 3 } 94 | } 95 | ] 96 | }); 97 | 98 | return result; 99 | } 100 | 101 | escapeRegex(text: string) { 102 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 103 | } 104 | 105 | async getMetrics(): Promise<{ submissionCount: number }[]> { 106 | // Get the current date 107 | const currentDate = new Date(); 108 | 109 | // Calculate the starting time of yesterday (12:00 AM) 110 | const startTime = new Date(currentDate); 111 | startTime.setHours(0, 0, 0, 0); // Set to 00:00:00:000 112 | 113 | // Get the Unix timestamp in milliseconds 114 | const unixTimestamp = startTime.getTime(); 115 | 116 | return Students.aggregate([ 117 | { 118 | $project: { 119 | _id: 0, 120 | submission: { $toInt: "$lastSubmissionDate" } 121 | } 122 | }, 123 | { 124 | $addFields: { 125 | submission: { 126 | $multiply: [1000, "$submission"] 127 | } 128 | } 129 | }, 130 | { 131 | $match: { 132 | submission: { 133 | $gte: unixTimestamp 134 | } 135 | } 136 | }, 137 | { 138 | $group: { 139 | _id: null, 140 | submissionCount: { $sum: 1 } 141 | } 142 | }, 143 | { 144 | $project: { 145 | _id: 0, 146 | submissionCount: 1 147 | } 148 | } 149 | ]); 150 | } 151 | 152 | async countStudents(): Promise { 153 | return Students.find().countDocuments(); 154 | } 155 | 156 | async leaderBoard(): Promise { 157 | return Students.aggregate([ 158 | { 159 | $match: { totalSolvedCountInThisWeek: { $ne: 0 } } 160 | }, 161 | { 162 | $sort: { totalSolvedCountInThisWeek: -1 } 163 | }, 164 | { 165 | $limit: 100 166 | }, 167 | { 168 | $project: { 169 | _id: 0, 170 | name: 1, 171 | leetcodeId: 1, 172 | batch: 1, 173 | totalSolvedCountInThisWeek: 1 174 | } 175 | } 176 | ]); 177 | } 178 | 179 | async findStudentsNotDone() { 180 | const results = await Students.find({ 181 | totalNotSubmissionCount: { 182 | $gte: 3 183 | } 184 | }); 185 | return results; 186 | } 187 | 188 | async findByPhone(phone: string) { 189 | const student = await Students.findOne({ phone }); 190 | return student; 191 | } 192 | 193 | async deleteStudent(id: string) { 194 | const student = await Students.findByIdAndDelete(id); 195 | return student; 196 | } 197 | 198 | async editProfile(id: string, data: StudentDTO) { 199 | return await Students.updateOne( 200 | { _id: id }, 201 | { 202 | $set: { 203 | name: data.name, 204 | lastName: data.lastName, 205 | batch: data.batch, 206 | domain: data.domain, 207 | phone: data.phone, 208 | email: data.email, 209 | leetcodeId: data.leetcodeId 210 | } 211 | } 212 | ); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /backend/src/database/repository/weeklymetrics.repository.ts: -------------------------------------------------------------------------------- 1 | import { WeeklyMetrics } from "../model"; 2 | 3 | export class WeeklymetricsRepository { 4 | async weeklyMetrics() { 5 | return WeeklyMetrics.aggregate([ 6 | { 7 | $sort: { createdAt: -1 } 8 | }, 9 | { 10 | $limit: 7 11 | }, 12 | { 13 | $project: { 14 | _id: 0, 15 | totalStudentsSolved: 1, 16 | day: 1 17 | } 18 | } 19 | ]); 20 | } 21 | 22 | async getLastDaySubmissionCount(): Promise<{ totalStudentsSolved: number }[]> { 23 | return WeeklyMetrics.aggregate([ 24 | { 25 | $sort: { createdAt: -1 } 26 | }, 27 | { 28 | $limit: 1 29 | }, 30 | { 31 | $project: { 32 | _id: 0, 33 | totalStudentsSolved: 1 34 | } 35 | } 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/handler/cronjob.ts: -------------------------------------------------------------------------------- 1 | import cron from "node-cron"; 2 | import { LeetStudentProfileUpdate, weeklyUpdate } from "./leetcode-updater"; 3 | 4 | /* The code is defining a task called `LeetcodeDailyUpdateTask` using the `cron.schedule` function from 5 | the `node-cron` library. This task is scheduled to run every day at 23:15 (11:15 PM) in the 6 | Asia/Kolkata timezone. */ 7 | 8 | const dailyUpdateTimeSchedule = process.env.NODE_ENV == "production" ? "30 23 * * *" : "*/5 * * * *"; 9 | 10 | export const LeetcodeDailyUpdateTask = cron.schedule( 11 | dailyUpdateTimeSchedule, 12 | async () => { 13 | console.log("Students LeetCode Data Updating"); 14 | await LeetStudentProfileUpdate(); 15 | }, 16 | { 17 | scheduled: true, 18 | timezone: "Asia/Kolkata" 19 | } 20 | ); 21 | 22 | /* The code is defining a task called `WeeklyDatabaseUpdateTask` using the `cron.schedule` function 23 | from the `node-cron` library. This task is scheduled to run every Sunday at 23:00 (11:00 PM) in the 24 | Asia/Kolkata timezone. */ 25 | export const WeeklyDatabaseUpdateTask = cron.schedule( 26 | "0 23 * * 0", 27 | async () => { 28 | console.log("weekly updating db"); 29 | await weeklyUpdate(); 30 | }, 31 | { 32 | scheduled: true, 33 | timezone: "Asia/Kolkata" 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /backend/src/handler/leetcode-updater.ts: -------------------------------------------------------------------------------- 1 | import { logger } from "../config"; 2 | import { Students, WeeklyMetrics } from "../database/model"; 3 | import { StudentRepository } from "../database/repository"; 4 | import { getProfile, getTotalSolved, getRecentSubmissionList } from "./leetcode"; 5 | import { isToday, isAlreadySolvedOrNot, IsAlreadyInDb } from "./utils"; 6 | import pLimit from "p-limit"; 7 | 8 | /* This queue allows a maximum of 2 concurrent executions of the code block passed to it. It ensures that only 2 9 | students' profiles are updated at a time, preventing excessive resource usage and potential 10 | performance issues. */ 11 | const queue = pLimit(2); 12 | 13 | /** 14 | * The LeetStudentProfileUpdate function updates the profiles of Leet students by retrieving their 15 | * LeetCode submissions, calculating their total solved count and recent submissions, and updating 16 | * their profile information in the database. 17 | */ 18 | export const LeetStudentProfileUpdate = async () => { 19 | let studentRepository = new StudentRepository(); 20 | 21 | const students = await studentRepository.find(); 22 | 23 | // Concurrency: Process students concurrently 24 | await Promise.allSettled( 25 | students.map(async (student) => { 26 | await queue(async () => { 27 | try { 28 | // Getting leetcode profile 29 | const leetcodeProfile = await getProfile(student.leetcodeId); 30 | // Getting total questions solved by difficulty 31 | const totalSubmissions = getTotalSolved(leetcodeProfile!)!; 32 | // Getting recent array of submission 33 | const recentSubmissions = getRecentSubmissionList(leetcodeProfile!); 34 | 35 | student.solved = { 36 | all: totalSubmissions.all, 37 | easy: totalSubmissions.easy, 38 | medium: totalSubmissions.medium, 39 | hard: totalSubmissions.hard 40 | }; 41 | // finding the recent submissionDate is today or not 42 | student.lastSubmissionDate = 43 | recentSubmissions?.find((problem) => { 44 | return ( 45 | isToday(problem.timestamp) && 46 | problem.statusDisplay === "Accepted" && 47 | isAlreadySolvedOrNot(student.solvedQuestionsInThisWeek, problem.titleSlug) 48 | ); 49 | })?.timestamp || student.lastSubmissionDate; 50 | 51 | // if the last submisson date is not today will updating the totalNotSubmissionCount 52 | if (isToday(student.lastSubmissionDate)) { 53 | student.totalNotSubmissionCount = 0; 54 | } else { 55 | student.totalNotSubmissionCount++; 56 | } 57 | 58 | // filtering out total questions solved today from the recentsubmission list and saving in the database 59 | const solvedToday = recentSubmissions?.filter((submission) => { 60 | return ( 61 | submission.statusDisplay === "Accepted" && 62 | isToday(submission.timestamp) && 63 | IsAlreadyInDb(student.solvedQuestionsInThisWeek, submission.titleSlug) 64 | ); 65 | }); 66 | 67 | student.totalSolvedCountInThisWeek += solvedToday!.length; 68 | 69 | await student.save(); 70 | } catch (error: any) { 71 | logger.error(error.stack); 72 | } 73 | }); 74 | }) 75 | ); 76 | 77 | /* This code block is responsible for updating the weekly metrics of student submissions. */ 78 | let submissionResult = await studentRepository.getMetrics(); 79 | 80 | const currentDate = new Date(); 81 | 82 | const currentDay = currentDate.toLocaleString("en-US", { weekday: "long" }); 83 | 84 | await WeeklyMetrics.create({ 85 | totalStudentsSolved: submissionResult[0]?.submissionCount || 0, 86 | day: currentDay 87 | }); 88 | }; 89 | 90 | export const weeklyUpdate = async () => { 91 | try { 92 | const students = await Students.find({}); 93 | 94 | await Promise.allSettled( 95 | students.map(async (student) => { 96 | await queue(async () => { 97 | student.totalSolvedCountInThisWeek = 0; 98 | await student.save(); 99 | }); 100 | }) 101 | ); 102 | } catch (error: any) { 103 | logger.error(error.stack || error); 104 | } 105 | }; 106 | -------------------------------------------------------------------------------- /backend/src/handler/leetcode.ts: -------------------------------------------------------------------------------- 1 | import { LeetCode, UserProfile } from "leetcode-query"; 2 | import { logger } from "../config"; 3 | 4 | const leetcode = new LeetCode(); 5 | 6 | /** 7 | * The function `getProfile` retrieves a user profile from the LeetCode API based on a given user ID. 8 | * @param {string} userId - A string representing the user ID of the user whose profile is being 9 | * retrieved. 10 | * @returns a Promise that resolves to either a UserProfile object or undefined. 11 | */ 12 | const getProfile = async (userId: string): Promise => { 13 | try { 14 | const user = await leetcode.user(userId); 15 | if (user) return user; 16 | else null; 17 | } catch (error) { 18 | logger.error(error); 19 | } 20 | }; 21 | 22 | /** 23 | * The function `getTotalSolved` returns the total number of solved problems for a given user, 24 | * categorized by difficulty level. 25 | * @param {UserProfile} user - The `user` parameter is of type `UserProfile`. It represents the user's 26 | * profile information, including their matched user and submission statistics. 27 | * @returns The function `getTotalSolved` returns an object with the following properties: 28 | */ 29 | const getTotalSolved = (user: UserProfile) => { 30 | try { 31 | const acSubmissionNum = user.matchedUser?.submitStats.acSubmissionNum!; 32 | return { 33 | all: acSubmissionNum[0].count, 34 | easy: acSubmissionNum[1].count, 35 | medium: acSubmissionNum[2].count, 36 | hard: acSubmissionNum[3].count 37 | }; 38 | } catch (error) { 39 | logger.error(error); 40 | } 41 | }; 42 | 43 | /** 44 | * The function `getRecentSubmissionList` returns the recent submission list of a user. 45 | * @param {UserProfile} user - The user parameter is of type UserProfile. 46 | * @returns The recentSubmissionList property of the user object. 47 | */ 48 | 49 | const getRecentSubmissionList = (user: UserProfile) => { 50 | return user.recentSubmissionList; 51 | }; 52 | 53 | export { getProfile, getTotalSolved, getRecentSubmissionList }; 54 | -------------------------------------------------------------------------------- /backend/src/handler/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This function checks if a given question titleslug 3 | * is already solved. If the title slug is not in the list of already solved questions, 4 | * it adds it and returns true. Otherwise, if the title slug is already in the list, it returns false. 5 | */ 6 | export function isAlreadySolvedOrNot(alreaySolvedQuestions: string[], titleSlug: string): boolean { 7 | if (!alreaySolvedQuestions.includes(titleSlug)) return true; 8 | else return false; 9 | } 10 | 11 | /** 12 | * The function `isToday` checks if a given timestamp corresponds to the current date. 13 | * @param {string} timestamp - The `timestamp` parameter is a string representing a Unix timestamp. 14 | * @returns a boolean value indicating whether the given timestamp represents the current date or not. 15 | */ 16 | export function isToday(timestamp: string): boolean { 17 | const dateFromTimestamp = new Date(+timestamp * 1000); 18 | const currentDate = new Date(); 19 | return dateFromTimestamp.toDateString() === currentDate.toDateString(); 20 | } 21 | 22 | export function IsAlreadyInDb(alreaySolvedQuestions: string[], titleSlug: string): boolean { 23 | if (!alreaySolvedQuestions.includes(titleSlug)) { 24 | alreaySolvedQuestions.push(titleSlug); 25 | return true; 26 | } else { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import app from "./app"; 3 | import { connectMongoDb, logger } from "./config"; 4 | import { LeetcodeDailyUpdateTask, WeeklyDatabaseUpdateTask } from "./handler/cronjob"; 5 | 6 | const start = async () => { 7 | await connectMongoDb(process.env.MONGO_URI!); 8 | 9 | LeetcodeDailyUpdateTask.start(); 10 | 11 | WeeklyDatabaseUpdateTask.start(); 12 | 13 | app.listen(process.env.PORT!, () => { 14 | console.log(`server is Running on port ${process.env.PORT} `); 15 | }); 16 | }; 17 | 18 | ["uncaughtException", "unhandledRejection"].forEach((event) => 19 | process.on(event, (err) => { 20 | logger.error(`something bad happened : ${event}, msg: ${err.stack || err}`); 21 | process.exit(1) 22 | }) 23 | ); 24 | 25 | start(); 26 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | "experimentalDecorators": true /* Enable experimental support for legacy experimental decorators. */, 18 | "emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */, 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | "module": "commonjs" /* Specify what module code is generated. */, 29 | "rootDir": "./src" /* Specify the root folder within your source files. */, 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | "resolveJsonModule": true /* Enable importing .json files. */, 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./build" /* Specify an output folder for all emitted files. */, 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 83 | 84 | /* Type Checking */ 85 | "strict": true /* Enable all strict type-checking options. */, 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /docker-compose-dev.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | mongodb: 4 | image: mongo 5 | ports: 6 | - 27017:27017 7 | container_name: mongodb 8 | volumes: 9 | - ./db/:/data/db 10 | backend: 11 | build: 12 | dockerfile: Dockerfile.dev 13 | context: ./backend 14 | container_name: backend 15 | ports: 16 | - 4000:4000 17 | restart: always 18 | volumes: 19 | - /app/node_modules 20 | - ./backend:/app 21 | env_file: 22 | - ./backend/.env 23 | depends_on: 24 | - mongodb 25 | frontend: 26 | build: 27 | dockerfile: Dockerfile.dev 28 | context: ./frontend 29 | container_name: frontend 30 | ports: 31 | - 8000:8000 32 | restart: always 33 | volumes: 34 | - /app/node_modules 35 | - ./frontend/src:/app/src 36 | env_file: 37 | - ./frontend/.env.development 38 | nginx: 39 | container_name: nginx_proxy 40 | build: 41 | dockerfile: Dockerfile 42 | context: ./proxy 43 | ports: 44 | - 80:80 45 | restart: always 46 | depends_on: 47 | - frontend 48 | - backend 49 | volumes: 50 | - ./proxy/nginx.conf:/etc/nginx/nginx.conf 51 | -------------------------------------------------------------------------------- /docker-compose-prod.yaml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | backend: 4 | build: 5 | dockerfile: Dockerfile.prod 6 | context: ./backend 7 | container_name: backend 8 | ports: 9 | - 4000:4000 10 | restart: on-failure 11 | env_file: 12 | - ./backend/.env 13 | frontend: 14 | build: 15 | dockerfile: Dockerfile.prod 16 | context: ./frontend 17 | container_name: frontend 18 | ports: 19 | - 8000:8000 20 | restart: on-failure 21 | env_file: 22 | - ./frontend/.env.production 23 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/dd46fe7b65fb0050a1ab7d7439d0f9224a1122ef/docs/.nojekyll -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | hello.pranavs.tech -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Headline 2 | 3 | > An awesome project. 4 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Document 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/* -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | VITE_APP_BASE_URL = 'http://localhost:80/' -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | VITE_APP_BASE_URL = 'https://leet.brototype.com/' -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es2020: true }, 3 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react-hooks/recommended"], 4 | parser: "@typescript-eslint/parser", 5 | parserOptions: { ecmaVersion: "latest", sourceType: "module" }, 6 | plugins: ["react-refresh"], 7 | rules: { 8 | "react-refresh/only-export-components": "warn", 9 | "no-unused-vars": "off", 10 | "no-console/no-console": "off" 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 4, 4 | "useTabs": true, 5 | "printWidth": 140, 6 | "singleQuote": false, 7 | "trailingComma": "none", 8 | "jsxBracketSameLine": true, 9 | "bracketSameLine": true, 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /frontend/.vite/deps_temp_1c3d506d/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "module" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | CMD ["npm","run","dev"] 12 | -------------------------------------------------------------------------------- /frontend/Dockerfile.prod: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package.json . 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | CMD [ "npm","run","preview" ] -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Brototype-LeetCodeTracker 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "leetcode_tracker", 3 | "version": "1.0.0", 4 | "description": "A leetcode daily challenge tracker to monitor student's perfomance in BroCamp", 5 | "author": "Packapeer Academy Pvt ltd", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "tsc && vite build", 10 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 11 | "preview": "vite preview", 12 | "prettier": "prettier --write ." 13 | }, 14 | "dependencies": { 15 | "@hookform/resolvers": "3.3.1", 16 | "@uiball/loaders": "1.3.0", 17 | "axios": "1.5.0", 18 | "chart.js": "4.4.0", 19 | "eslint-plugin-no-console-log": "2.0.0", 20 | "faker": "6.6.6", 21 | "preline": "1.9.0", 22 | "prettier": "3.0.2", 23 | "react": "18.2.0", 24 | "react-chartjs-2": "5.2.0", 25 | "react-dom": "18.2.0", 26 | "react-hook-form": "7.45.4", 27 | "react-hot-toast": "2.4.1", 28 | "react-router-dom": "6.15.0", 29 | "react-svg": "16.1.23", 30 | "react-toastify": "9.1.3", 31 | "zod": "3.22.2" 32 | }, 33 | "devDependencies": { 34 | "@types/react": "18.2.15", 35 | "@types/react-dom": "18.2.7", 36 | "@typescript-eslint/eslint-plugin": "6.0.0", 37 | "@typescript-eslint/parser": "6.0.0", 38 | "@vitejs/plugin-react": "4.0.3", 39 | "autoprefixer": "10.4.15", 40 | "eslint": "8.45.0", 41 | "eslint-plugin-react-hooks": "4.6.0", 42 | "eslint-plugin-react-refresh": "0.4.3", 43 | "postcss": "8.4.29", 44 | "tailwindcss": "3.3.3", 45 | "typescript": "5.0.2", 46 | "vite": "4.4.5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/81414-middle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/dd46fe7b65fb0050a1ab7d7439d0f9224a1122ef/frontend/public/81414-middle.png -------------------------------------------------------------------------------- /frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/dd46fe7b65fb0050a1ab7d7439d0f9224a1122ef/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/public/istockphoto-1337144146-612x612.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/dd46fe7b65fb0050a1ab7d7439d0f9224a1122ef/frontend/public/istockphoto-1337144146-612x612.jpg -------------------------------------------------------------------------------- /frontend/public/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/dd46fe7b65fb0050a1ab7d7439d0f9224a1122ef/frontend/public/logo-black.png -------------------------------------------------------------------------------- /frontend/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/dd46fe7b65fb0050a1ab7d7439d0f9224a1122ef/frontend/src/App.css -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | 2 | import "./App.css"; 3 | // import { privateRoutes,routes } from "./utils/routes/routes"; 4 | import { Routes, Route } from "react-router-dom"; 5 | 6 | import Login from "./components/Login"; 7 | import Home from "./pages/Home"; 8 | import ErrorComponent from "./components/ErrorComponent"; 9 | import ProtectedRoute from "./utils/routes/ProtectedRoute"; 10 | import Analytic from "./Features/Analytic"; 11 | import { BrowserRouter } from "react-router-dom"; 12 | 13 | import LeaderBoard from "./Features/LeaderBorde"; 14 | import AllStudentData from "./components/AllStudentData"; 15 | import StudentsNotdone from "./components/StudentsNotdone"; 16 | import StudentLogin from "./components/StudentLogin"; 17 | 18 | // App component 19 | const App = () => { 20 | return ( 21 | <> 22 |
23 | 24 | 25 | {/* Auth Route */} 26 | } /> 27 | } /> 28 | {/* */} 29 | {/* ProtectedRoute */} 30 | }> 31 | }> 32 | } /> 33 | } /> 34 | } /> 35 | } /> 36 | 37 | } /> 38 | 39 | 40 | 41 |
42 | 43 | ); 44 | }; 45 | 46 | export default App; 47 | 48 | -------------------------------------------------------------------------------- /frontend/src/Features/Analytic.tsx: -------------------------------------------------------------------------------- 1 | import LeaderBorderStatic from "../components/LeaderBoardStatic"; 2 | import { LineChart } from "../components/LineChart"; 3 | import PieData from "../components/PieData"; 4 | 5 | function Analytic() { 6 | return ( 7 | <> 8 |
9 |
10 |
11 |

Daily overall statistics

12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 | 25 | ); 26 | } 27 | 28 | export default Analytic; 29 | -------------------------------------------------------------------------------- /frontend/src/Features/LeaderBorde.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import LeaderBoard from "../components/LeaderBoard"; 3 | import { getLeaderboard } from "../utils/api/config/axios.GetApi"; 4 | import { Toaster, toast } from "react-hot-toast"; 5 | 6 | const LeaderBorder = () => { 7 | const [leaderBordRank, setLeaderBoardRank] = useState() as any; 8 | useEffect(() => { 9 | const handleLeaderBoard = async () => { 10 | const response: any = await getLeaderboard(); 11 | if (response?.status === 200) { 12 | setLeaderBoardRank(response.data.rank); 13 | } else if (response.response.status === 404) { 14 | toast.error("Ooops...! Couldn't find rank table"); 15 | } else { 16 | toast.error(`${response.response.data.errors[0].message}`); 17 | } 18 | }; 19 | handleLeaderBoard(); 20 | }, []); 21 | return ( 22 | <> 23 | 24 |
25 |
26 | 27 |
28 | 34 |
35 |
38 |
39 |
40 |
41 | 59 |
60 |
61 | {/* Icon */} 62 | 63 | 70 | 71 | 72 | 73 | {/* End Icon */} 74 |

Publish Learderbord

75 |

Are you sure you would like to publish leader board?

76 |
77 | 80 | Cancel 81 | 82 | 88 |
89 |
90 |
91 |
92 |
93 | 94 |
95 |
96 | {leaderBordRank?.map((object: any, index: number) => { 97 | 98 | return ; 99 | })} 100 |
101 |
102 | 103 | ); 104 | }; 105 | 106 | 107 | export default LeaderBorder; 108 | -------------------------------------------------------------------------------- /frontend/src/Features/StudentsDetails.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { Toaster } from "react-hot-toast"; 3 | import { Outlet } from "react-router-dom"; 4 | 5 | function StudentsDetails() { 6 | return ( 7 | <> 8 | 9 |
10 | 11 |
12 | 13 | ); 14 | } 15 | 16 | export default StudentsDetails; 17 | -------------------------------------------------------------------------------- /frontend/src/components/AllStudentData.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { getAllStudents, searchStudents } from "../utils/api/config/axios.GetApi"; 3 | import { deleteStudentData } from "../utils/api/config/axios.DeleteApi"; 4 | import { Toaster, toast } from "react-hot-toast"; 5 | import axios from "axios"; 6 | import { Ring } from "@uiball/loaders"; 7 | import StudentsDataUpdate from "./StudentsDataUpdate"; 8 | 9 | type UserData = [ 10 | { 11 | batch: string; 12 | domain: string; 13 | email: string; 14 | lastSubmissionDate: string; 15 | leetcodeId: string; 16 | name: string; 17 | phone: string; 18 | solved: { 19 | all: number; 20 | easy: number; 21 | medium: number; 22 | hard: number; 23 | }; 24 | solvedQuestionsInThisWeek: string[]; 25 | totalNotSubmissionCount: number; 26 | totalSolvedCountInThisWeek: number; 27 | _id: string; 28 | } 29 | ]; 30 | let timer: number | undefined; 31 | 32 | function AllStudentData() { 33 | const [allStudentsData, setAllStudentsData] = useState([]); 34 | const [svgData, setSvgData] = useState("") as any; 35 | const [totalpageNumber, setTotalPageNumber] = useState(1); 36 | const [currentPage, setCurrentPage] = useState(1); 37 | const [uiControle, setUiControll] = useState(false); 38 | const [editeUicontroll, setEditeUiControl] = useState(false); 39 | const [searchInput, setSearchInput] = useState("") as any; 40 | const [isInputEmpty, setIsInputEmpty] = useState(true); 41 | const [editeData, setediteData] = useState() as any; 42 | const [renderCount, setRenderCount] = useState(0); 43 | 44 | useEffect(() => { 45 | const handleAllStudents = async () => { 46 | if (isInputEmpty) { 47 | const response: any = await getAllStudents(currentPage); 48 | if (response?.status === 200) { 49 | setTotalPageNumber(response.data.result.totalPages); 50 | console.log(response.data.result.students, "log"); 51 | setAllStudentsData(response.data.result.students); 52 | } else if (response.response.status === 404) { 53 | toast.error("Ooops...! Couldn't find rank table"); 54 | } else { 55 | toast.error(`${response.response.data.errors[0].message}`); 56 | } 57 | } else { 58 | // caling search api 59 | const response: any = await searchStudents(searchInput); 60 | if (response?.status === 200) { 61 | setAllStudentsData(response.data.result); 62 | setTotalPageNumber(0); 63 | } else if (response.response.status === 404) { 64 | toast.error("Ooops...! Couldn't find rank table"); 65 | } else { 66 | toast.error(`${response.response.data.errors[0].message}`); 67 | } 68 | // setAllStudentsData([]) 69 | } 70 | }; 71 | handleAllStudents(); 72 | }, [currentPage, searchInput, renderCount, editeUicontroll]); 73 | 74 | const handlePageChange = (pageNumber: number) => { 75 | setCurrentPage(pageNumber); 76 | }; 77 | 78 | const handlePrev = () => { 79 | if (currentPage != 1) { 80 | setCurrentPage((prev) => prev - 1); 81 | } 82 | }; 83 | 84 | const handleNext = () => { 85 | if (totalpageNumber != currentPage) { 86 | setCurrentPage((prev) => prev + 1); 87 | } 88 | }; 89 | 90 | const handleShowStudent = (userName: string) => { 91 | axios.get(`https://leetcard.jacoblin.cool/${userName}?ext=heatmap&theme=forest`).then((response: any) => { 92 | setSvgData(response.data); 93 | setUiControll(true); 94 | }); 95 | }; 96 | const clearSvgData = () => { 97 | setSvgData(""); 98 | setUiControll(false); 99 | }; 100 | 101 | const handleInputChange = (event: any) => { 102 | const inputValue = event.target.value; 103 | if (timer) { 104 | clearTimeout(timer); 105 | } 106 | timer = setTimeout(() => { 107 | setSearchInput(event.target.value); 108 | }, 1000); 109 | if (inputValue === "") { 110 | setSearchInput(""); 111 | setIsInputEmpty(true); 112 | } else { 113 | setIsInputEmpty(false); 114 | } 115 | }; 116 | 117 | const handleEditeUi = (data: any) => { 118 | console.log(uiControle); 119 | setEditeUiControl(true); 120 | console.log(data, "edite data...."); 121 | setediteData(data); 122 | }; 123 | const handleUiBack = () => { 124 | setEditeUiControl(false); 125 | }; 126 | 127 | const handledeleteStudent = async (id: string) => { 128 | const response: any = await deleteStudentData(id); 129 | console.log(response, "response delete"); 130 | if (response.status === 200) { 131 | toast.success("Data succesfully deleted"); 132 | setRenderCount(renderCount + 1); 133 | } else if (response.status === 400) { 134 | toast.error("Student not exist or somenthing went wrong"); 135 | setRenderCount(renderCount + 1); 136 | } else { 137 | toast.error("Oops..! something went wrong"); 138 | setRenderCount(renderCount + 1); 139 | } 140 | }; 141 | 142 | return ( 143 | <> 144 | 145 | {editeUicontroll ? ( 146 | <> 147 | {" "} 148 | {/* */} 149 | 164 | 165 | 166 | ) : ( 167 |
168 |
169 |
170 | 173 |
174 |
175 | 189 |
190 | 197 |
198 |
199 |
200 |
201 |
202 | No 203 |
204 |
205 | Name 206 |
207 |
208 | Batch 209 |
210 |
211 | User Name 212 |
213 |
214 | Manage 215 |
216 |
217 | 218 |
219 | {allStudentsData?.map((dataObject: any, index: number) => { 220 | return ( 221 |
224 |
225 | <> 226 | 233 |
237 |
238 |
239 |
240 |
241 |

LeetCode HeatMap

242 |
243 | 262 |
263 |
264 | {uiControle ? ( 265 | 266 | 276 | 277 | ) : ( 278 |
279 | 280 |
281 | )} 282 |
283 |
284 |
285 |
286 | 287 | 288 | {currentPage === 1 ? currentPage * 0 + (index + 1) : currentPage * 10 + (index + 1)} 289 | 290 |
291 |
292 | {dataObject.name +" "+ dataObject.lastName} 293 |
294 |
295 | {dataObject.batch} 296 |
297 |
298 | {dataObject.leetcodeId} 299 |
300 |
301 | 309 | 315 |
319 |
320 |
321 |
322 | 340 |
341 |
342 | {/* Icon */} 343 | 344 | 351 | 352 | 353 | 354 | {/* End Icon */} 355 |

Are you sure

356 |

Do you really want to delet this record...?

357 |
358 | 364 | 371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 | ); 379 | })} 380 |
381 |
382 |
383 |
384 | 408 |
409 |
410 |
411 |
412 | )} 413 | 414 | ); 415 | } 416 | 417 | export default AllStudentData; 418 | -------------------------------------------------------------------------------- /frontend/src/components/ErrorComponent.tsx: -------------------------------------------------------------------------------- 1 | function ErrorComponent() { 2 | return ( 3 | <> 4 |
5 |
6 |
7 |
8 | 9 | 15 | 16 |
17 |
18 |

404 - Page not found

19 |

20 | The page you are looking for doesn't exist or
21 | has been removed. 22 |

23 |
24 |
25 | 26 | ); 27 | } 28 | 29 | export default ErrorComponent; 30 | -------------------------------------------------------------------------------- /frontend/src/components/LeaderBoard.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "react-router-dom"; 2 | 3 | const LeaderBoard = (prop: any) => { 4 | console.log(prop); 5 | 6 | return ( 7 | <> 8 |
11 |
12 |
13 |

Rank

14 |
15 |
16 |

Name

17 |
18 |
19 |

Batch

20 |
21 |
22 |

Solved/week

23 |
24 |
25 |

UserId

26 |
27 |
28 |

Profile

29 |
30 |
31 |
32 |
33 |
34 |

{prop.index + 1}

35 |
36 |
37 |
38 |

{prop.rank.name}

39 |
40 |
41 |

{prop.rank.batch}

42 |
43 |
44 |

{prop.rank.totalSolvedCountInThisWeek}

45 |
46 |
47 |

{prop.rank.leetcodeId}

48 |
49 |
50 | View 51 |
52 |
53 |
54 | 55 | ); 56 | }; 57 | 58 | export default LeaderBoard; 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/components/LeaderBoardStatic.tsx: -------------------------------------------------------------------------------- 1 | function LeaderBoardStatic() { 2 | return ( 3 | <> 4 |
5 |
6 |
7 | 8 |
9 |
Pranav
10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |
Ajay
18 |
19 |
20 |
21 |
22 |
23 | 24 |
25 |
Magesh
26 |
27 |
28 |
29 |
30 | 31 | ); 32 | } 33 | 34 | export default LeaderBoardStatic; 35 | -------------------------------------------------------------------------------- /frontend/src/components/LineChart.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Filler, Legend } from "chart.js"; 3 | import { Line } from "react-chartjs-2"; 4 | import { weeklyMetrics } from "../utils/api/config/axios.GetApi"; 5 | ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Filler, Legend); 6 | 7 | export function LineChart() { 8 | const [date, setDate] = useState([]); 9 | const [studentsCount, setStudentsCount] = useState([]); 10 | 11 | useEffect(() => { 12 | const getWeeklyMetrics = async () => { 13 | const response:any = await weeklyMetrics(); 14 | const newDate:any = []; 15 | const newStudentsCount:any = []; 16 | 17 | response.data.lastWeekReport.forEach((dayObject:any) => { 18 | newDate.push(dayObject.day); 19 | newStudentsCount.push(dayObject.totalStudentsSolved); 20 | }); 21 | 22 | setDate(newDate); 23 | setStudentsCount(newStudentsCount); 24 | }; 25 | 26 | getWeeklyMetrics(); 27 | }, []); 28 | 29 | const options = { 30 | responsive: true, 31 | plugins: { 32 | legend: { 33 | position: "top" as const, 34 | }, 35 | title: { 36 | display: true, 37 | text: "Weekly analytics chart", 38 | }, 39 | }, 40 | }; 41 | 42 | const labels = date; 43 | 44 | // Replace this array with your actual data 45 | const rawData = studentsCount; 46 | 47 | const data = { 48 | labels, 49 | datasets: [ 50 | { 51 | fill: true, 52 | label: "solved", 53 | data: rawData, 54 | borderColor: "rgb(53, 162, 235)", 55 | backgroundColor: "rgba(53, 162, 235, 0.5)", 56 | }, 57 | ], 58 | }; 59 | 60 | return ( 61 | <> 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { OtpData, PhoneNumberData, useOtpValidation, usePhoneNumberValidate } from "../utils/validation/formValidation"; 3 | import Navbar from "./Navbar"; 4 | import { adminAuth, adminVerify } from "../utils/api/config/axios.PostAPi"; 5 | import { verifyPayload } from "../utils/api/api.Types/axios.Postapi.Types"; 6 | import { useNavigate } from "react-router-dom"; 7 | import toast, { Toaster } from "react-hot-toast"; 8 | 9 | 10 | 11 | const Login = () => { 12 | const [otp, setOtp] = useState(true); 13 | const [number, setNumber] = useState(""); 14 | const { errors, handleSubmit, register } = usePhoneNumberValidate(); 15 | const data = useOtpValidation(); 16 | const navigate = useNavigate(); 17 | const handlePhoneNumber = async (data: PhoneNumberData) => { 18 | // Admin authenrication Api 19 | const response: any = await adminAuth(data.phone); 20 | console.log(response,'otp response'); 21 | if (response.status === 200) { 22 | setOtp(false); 23 | setNumber(data.phone); 24 | } else if (response.response.status === 404) { 25 | toast.error("Ooops..! Error occured"); 26 | } else { 27 | toast.error("Ooops...! Invalid mobile phone provide a valid phone"); 28 | } 29 | }; 30 | 31 | 32 | const hanldleFormOtp = async (data: OtpData) => { 33 | const verifyPayload: verifyPayload = { 34 | otp: Number(data.otp), 35 | phone: number 36 | }; 37 | // Admin OTP verify Api 38 | const response: any = await adminVerify(verifyPayload); 39 | if (response.status === 200) { 40 | var isLoggedIn = true; 41 | localStorage.setItem("adminToken", response.data.token); 42 | localStorage.setItem("adminAuth", JSON.stringify(isLoggedIn)); 43 | toast.success("SuccesFully logged in"); 44 | navigate("/"); 45 | } else if (response.response.status === 404) { 46 | toast.error("Ooops..! Error occured"); 47 | } else { 48 | 49 | toast.error("Ooops...! Invalied OTP"); 50 | } 51 | }; 52 | return ( 53 | <> 54 | 55 | 56 |
57 |
58 |
59 | {otp ? ( 60 |
64 |

Hey, Admin

65 | {errors.phone?.message ? ( 66 | {errors.phone?.message} 67 | ) : ( 68 |

Enter your Registred Number to get sign in

69 | )} 70 |
71 | 76 | 79 | 80 | 81 | 87 |
88 | 92 |
93 | ) : ( 94 |
98 |

Verify

99 | {data.errors.otp?.message ? ( 100 | {data.errors.otp?.message} 101 | ) : ( 102 |

Enter the OTP to verify

103 | )} 104 |
105 | 110 | 113 | 114 | 120 |
121 | 125 |
126 | )} 127 |
128 |
129 |
130 | 131 | ); 132 | }; 133 | 134 | export default Login; 135 | -------------------------------------------------------------------------------- /frontend/src/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | function Navbar() { 2 | return ( 3 | <> 4 |
5 |
6 |
7 | scds 8 |
9 |
10 |
11 | 12 | ); 13 | } 14 | 15 | export default Navbar; 16 | -------------------------------------------------------------------------------- /frontend/src/components/PieData.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { getDailyMetrics } from "../utils/api/config/axios.GetApi"; 3 | import toast, { Toaster } from "react-hot-toast"; 4 | 5 | 6 | 7 | function PieData() { 8 | const [isHovered, setIsHovered] = useState(false); 9 | const [completedStudents, setCompletedStudents] = useState(0); 10 | const [notCompletedStudents, setNotCompletedStudents] = useState(0); 11 | const [completePersantage, setCompletePersantage] = useState(0); 12 | const [totalStudents, setTotalStudents] = useState(0); 13 | const [notCompletePersantage, setNotCompletedPersantage] = useState(0); 14 | 15 | useEffect(() => { 16 | const dailyMetricsHandler = async () => { 17 | const response: any = await getDailyMetrics(); 18 | if (response.status === 200) { 19 | const notCompletedStudents = response.data.totalStudents - response.data.yesterdaySolvedStudentsCount; 20 | const completPersantage = (response.data.yesterdaySolvedStudentsCount / response.data.totalStudents) * 100; 21 | setNotCompletedStudents(notCompletedStudents); 22 | setCompletedStudents(response.data.yesterdaySolvedStudentsCount); 23 | setCompletePersantage(completPersantage); 24 | setTotalStudents(response.data.totalStudents); 25 | setNotCompletedPersantage(100 - completPersantage); 26 | } else if (response.response.status === 404) { 27 | toast.error("Ooops...! Couldn't find Daily metrics"); 28 | } else { 29 | toast.error(`${response.response.data.errors[0].message}`); 30 | } 31 | }; 32 | dailyMetricsHandler(); 33 | }, []); 34 | 35 | const handleMouseEnter = () => { 36 | setIsHovered(true); 37 | }; 38 | const handleMouseLeave = () => { 39 | setIsHovered(false); 40 | }; 41 | return ( 42 | <> 43 | 44 |
45 |
46 |
47 |
53 |
54 |

58 | Students 59 |

60 |

64 | {isHovered ? `${notCompletedStudents}/${totalStudents}` : `${completedStudents}/${totalStudents}`} 65 |

66 |

70 | 71 | {isHovered ? `${notCompletePersantage.toFixed(1)}%` : `${completePersantage.toFixed(1)}%`} 72 |

73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |

45%

82 |
83 |
84 |
85 |
86 |

85%

87 |
88 |
89 |
90 |
91 |

65%

92 |
93 |
94 |
95 |
96 |
97 | 98 | ); 99 | } 100 | 101 | export default PieData; 102 | -------------------------------------------------------------------------------- /frontend/src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { Link, useLocation, useNavigate } from "react-router-dom"; 2 | const Sidebar = () => { 3 | const navigate = useNavigate(); 4 | const handleSignOut = () => { 5 | localStorage.removeItem("adminToken"); 6 | localStorage.removeItem("adminAuth"); 7 | navigate("/auth"); 8 | }; 9 | const path = useLocation(); 10 | return ( 11 | <> 12 |
13 |
14 |
15 | 16 | 17 | LeetCode Tracker 18 | 19 |
20 |
    21 |
  • 22 | 29 | 30 | Statistics 31 | 32 |
  • 33 |
  • 34 | 35 | 42 | 43 | Leader Board 44 | 45 |
  • 46 |
  • 47 | 54 | Students 55 | 56 |
  • 57 |
  • 58 | 65 | Students Notdone 66 | 67 | 68 |
  • 69 |
70 |
71 | 91 |
92 |
93 |
94 |
95 |

Send new Question

96 |
97 | 101 | 106 |
107 | 108 | ); 109 | }; 110 | 111 | export default Sidebar; 112 | -------------------------------------------------------------------------------- /frontend/src/components/StudentLogin.tsx: -------------------------------------------------------------------------------- 1 | import { studentAuth, useStudentAuth } from "../utils/validation/formValidation"; 2 | import { studentsAuth } from "../utils/api/config/axios.PostAPi"; 3 | import { Toaster, toast } from "react-hot-toast"; 4 | import { Waveform } from "@uiball/loaders"; 5 | import { useState } from "react"; 6 | function StudentLogin() { 7 | const { errors, handleSubmit, reset, register } = useStudentAuth(); 8 | const [loader,setLoader] = useState(false) 9 | const handleStudentsAuth = async (data: studentAuth) => { 10 | setLoader(true); 11 | if(loader){ 12 | toast('Please waite request under process!', { 13 | icon: '⏳', 14 | duration:580, 15 | style:{background:"" , width:"30rem",borderColor:"#D2042D",borderWidth:".2rem", borderRadius:"3rem"} 16 | }) 17 | }else{ 18 | const response: any = await studentsAuth(data); 19 | if (response?.status === 200) { 20 | setLoader(false) 21 | toast.success("Successfully registered"); 22 | reset(); 23 | } else if (response?.response.status === 400) { 24 | setLoader(false) 25 | toast.error(`${response?.response.data.errors[0].message}`); 26 | } else { 27 | setLoader(false) 28 | toast.error("Somthing went wrong"); 29 | } 30 | } 31 | 32 | }; 33 | return ( 34 |
35 | 36 |
37 |
38 |
39 |
43 |
44 |

45 | Hey, Amigo...🚀
{" "} 46 |

47 |
48 | scds 49 |
50 |
51 |
52 | {errors.name ? ( 53 | {errors.name?.message} 54 | ) : ( 55 |

First Name

56 | )} 57 |
58 | 64 |
65 | {errors.lastName ? ( 66 | {errors.lastName?.message} 67 | ) : ( 68 |

Last Name

69 | )} 70 |
71 | 77 |
78 |
79 | {errors.domain?.message ? ( 80 | {errors.domain?.message} 81 | ) : ( 82 |

Domain

83 | )} 84 | 85 | { 86 | loader && (
87 | 93 |
) 94 | } 95 | 96 |
97 | 122 |
123 | {errors.batch?.message ? ( 124 | {errors.batch?.message} 125 | ) : ( 126 |

Batch

127 | )} 128 |
129 | 135 |
136 | {errors.phone?.message ? ( 137 | {errors.phone?.message} 138 | ) : ( 139 |

Whatsapp number

140 | )} 141 |
142 | 148 |
149 | 150 | {errors.email?.message ? ( 151 | {errors.email?.message} 152 | ) : ( 153 |

Mail

154 | )} 155 |
156 | 162 |
163 | 164 | {errors.leetcodeId?.message ? ( 165 | {errors.leetcodeId?.message} 166 | ) : ( 167 |

Leetcode user name

168 | )} 169 |
170 | 176 |
177 | 181 |
182 |
183 |
184 |
185 |
186 | ); 187 | } 188 | 189 | export default StudentLogin; 190 | 191 | -------------------------------------------------------------------------------- /frontend/src/components/StudentsDataUpdate.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { editeStudentData } from "../utils/api/config/axios.PostAPi"; 3 | import { toast } from "react-hot-toast"; 4 | 5 | function StudentsDataUpdate(data: any) { 6 | const [formdata,setFormData]=useState({ 7 | name:"", 8 | lastName:"", 9 | email:"", 10 | domain:"", 11 | batch:"", 12 | phone:"", 13 | leetcodeId:"" 14 | }); 15 | useEffect(()=>{ 16 | setFormData({ 17 | ...formdata, 18 | name:data.data.name, 19 | lastName:data.data.lastName, 20 | email:data.data.email, 21 | domain:data.data.domain, 22 | batch:data.data.batch, 23 | phone:data.data.phone, 24 | leetcodeId:data.data.leetcodeId 25 | }) 26 | },[]) 27 | 28 | const formSubmit=async(id:string)=>{ 29 | const response:any = await editeStudentData(id,formdata); 30 | if(response.status === 200){ 31 | toast.success("Data succesufully updated") 32 | }else if(response.status === 400){ 33 | toast.error("leetcode id is not exist or student not found") 34 | }else{ 35 | toast.error("Oops...! something went wrong") 36 | } 37 | } 38 | 39 | return ( 40 | <> 41 |
42 |

Update UserData

43 |
44 |
45 |
46 | 49 |
50 | 52 | setFormData({ 53 | ...formdata, 54 | name:e.target.value 55 | }) 56 | } 57 | value={formdata.name} 58 | className="pl-2 w-full cursor-pointer outline-none border-none" 59 | type="text" 60 | placeholder="First name" 61 | name="name" 62 | /> 63 |
64 |
65 |
66 | 69 |
70 | 72 | setFormData({ 73 | ...formdata, 74 | name:e.target.value 75 | }) 76 | } 77 | value={formdata.lastName} 78 | className="pl-2 w-full cursor-pointer outline-none border-none" 79 | type="text" 80 | name="lastName" 81 | /> 82 |
83 |
84 | 85 |
86 | 89 |
90 | 117 |
118 |
119 |
120 | 123 |
124 | 126 | setFormData({ 127 | ...formdata, 128 | batch:e.target.value 129 | }) 130 | } 131 | name="batch" 132 | value={formdata.batch} 133 | className="pl-2 w-full cursor-pointer outline-none border-none" 134 | type="text" 135 | placeholder="e.g:BCE55" 136 | /> 137 |
138 |
139 |
140 | 143 |
144 | 146 | setFormData({ 147 | ...formdata, 148 | phone:e.target.value 149 | }) 150 | } 151 | name="phone" 152 | value={formdata.phone} 153 | className="pl-2 w-full cursor-pointer outline-none border-none" 154 | type="text" 155 | placeholder="Whatsapp Number" 156 | /> 157 |
158 |
159 |
160 | 163 |
164 | 166 | setFormData({ 167 | ...formdata, 168 | email:e.target.value 169 | }) 170 | } 171 | name="email" 172 | value={formdata.email} 173 | className="pl-2 w-full cursor-pointer outline-none border-none" 174 | type="text" 175 | placeholder="Mail" 176 | /> 177 |
178 |
179 |
180 | 183 |
184 | 186 | setFormData({ 187 | ...formdata, 188 | leetcodeId:e.target.value 189 | }) 190 | } 191 | name="leetcodeId" 192 | value={formdata.leetcodeId} 193 | className="pl-2 w-full cursor-pointer outline-none border-none" 194 | type="text" 195 | placeholder="First name" 196 | /> 197 |
198 |
199 |
200 |
201 | 208 |
209 |
210 |
211 | 212 | ); 213 | } 214 | 215 | export default StudentsDataUpdate; 216 | -------------------------------------------------------------------------------- /frontend/src/components/StudentsNotdone.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Toaster, toast } from "react-hot-toast"; 3 | import { getNotDoneStudents, searchStudentsNotDone } from "../utils/api/config/axios.GetApi"; 4 | import axios from "axios"; 5 | import { Ring } from "@uiball/loaders"; 6 | type UserData = [ 7 | { 8 | batch: string; 9 | domain: string; 10 | email: string; 11 | lastSubmissionDate: string; 12 | leetcodeId: string; 13 | name: string; 14 | phone: string; 15 | solved: { 16 | all: number; 17 | easy: number; 18 | medium: number; 19 | hard: number; 20 | }; 21 | solvedQuestionsInThisWeek: string[]; 22 | totalNotSubmissionCount: number; 23 | totalSolvedCountInThisWeek: number; 24 | _id: string; 25 | } 26 | ]; 27 | const StudentsNotdone = () => { 28 | const [uiControle, setUiControll] = useState(false); 29 | const [svgData, setSvgData] = useState("") as any; 30 | const [searchInput, setSearchInput] = useState("") as any; 31 | const [isInputEmpty, setIsInputEmpty] = useState(true); 32 | const [allStudentsData, setAllStudentsData] = useState([]); 33 | 34 | let timer: number | undefined; 35 | useEffect(() => { 36 | const handleLeaderBoard = async () => { 37 | if (isInputEmpty) { 38 | const response: any = await getNotDoneStudents(); 39 | if (response?.status === 200) { 40 | setAllStudentsData(response.data.result); 41 | } else if (response.response.status === 404) { 42 | toast.error("Ooops...! Couldn't find rank table"); 43 | } else { 44 | toast.error(`${response.response.data.errors[0].message}`); 45 | } 46 | } else { 47 | const response:any = await searchStudentsNotDone(searchInput) 48 | console.log(response,"response coming frontend"); 49 | if (response?.status === 200) { 50 | setAllStudentsData(response.data.result); 51 | } else if (response.response.status === 404) { 52 | toast.error("Ooops...! Couldn't find rank table"); 53 | } 54 | } 55 | 56 | }; 57 | 58 | handleLeaderBoard(); 59 | }, [searchInput]); 60 | const handleShowStudent = (userName: string) => { 61 | axios.get(`https://leetcard.jacoblin.cool/${userName}?ext=heatmap&theme=forest`).then((response: any) => { 62 | setSvgData(response.data); 63 | setUiControll(true); 64 | }); 65 | }; 66 | const clearSvgData = () => { 67 | setSvgData(""); 68 | setUiControll(false); 69 | }; 70 | const handleInputChange = (event:any) => { 71 | const value = event.target.value 72 | if (timer) { 73 | clearTimeout(timer); 74 | } 75 | timer = setTimeout(() => { 76 | setSearchInput(event.target.value); 77 | }, 1000); 78 | if (value === "") { 79 | setSearchInput(""); 80 | setIsInputEmpty(true); 81 | } else { 82 | setIsInputEmpty(false); 83 | } 84 | } 85 | 86 | return ( 87 | <> 88 | 89 |
90 |
91 |
92 |
93 | 96 |
97 |
98 | 112 |
113 | 119 |
120 |
121 |
122 |
123 |
124 | 130 |
131 |
134 |
135 |
136 |
137 | 155 |
156 |
157 | {/* Icon */} 158 | 159 | 166 | 167 | 168 | 169 | {/* End Icon */} 170 |

Send Warning

171 |

Are you sure you would like to send warning....?

172 |
173 | 176 | Cancel 177 | 178 | 184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 | No 194 |
195 |
196 | Name 197 |
198 |
199 | Batch 200 |
201 |
202 | User Name 203 |
204 |
205 | 206 |
207 | {allStudentsData?.map((dataObject: any, index: number) => { 208 | return ( 209 |
212 |
213 | <> 214 | 221 |
225 |
226 |
227 |
228 |
229 |

LeetCode HeatMap

230 |
231 | 250 |
251 |
252 | {uiControle ? ( 253 | 254 | 264 | 265 | ) : ( 266 |
267 | 268 |
269 | )} 270 |
271 |
272 |
273 |
274 | 275 | {index + 1} 276 |
277 |
278 | {dataObject.name +" "+ dataObject.lastName} 279 |
280 |
281 | {dataObject.batch} 282 |
283 |
284 | {dataObject.leetcodeId} 285 |
286 |
287 | ); 288 | })} 289 |
290 | {/*
291 |
292 |
293 | 317 |
318 |
319 |
*/} 320 |
321 | 322 | ); 323 | }; 324 | 325 | export default StudentsNotdone; 326 | 327 | 328 | -------------------------------------------------------------------------------- /frontend/src/components/alert.tsx: -------------------------------------------------------------------------------- 1 | function alert() { 2 | return ( 3 | <> 4 |
5 | Info alert! You should check in on some of those fields below. 6 |
7 | 8 | ); 9 | } 10 | 11 | export default alert; 12 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | *::-webkit-scrollbar { 3 | display: none; 4 | } 5 | @tailwind components; 6 | @tailwind utilities; 7 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import App from "./App.tsx"; 3 | import "./index.css"; 4 | ReactDOM.createRoot(document.getElementById("root")!).render(); 5 | -------------------------------------------------------------------------------- /frontend/src/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "../components/Navbar"; 2 | import Sidebar from "../components/Sidebar"; 3 | import { Outlet } from "react-router-dom"; 4 | 5 | function Home() { 6 | return ( 7 | <> 8 | 9 |
10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 | ); 21 | } 22 | 23 | export default Home; 24 | -------------------------------------------------------------------------------- /frontend/src/utils/api/api.Types/axios.Getapi.Typses.ts: -------------------------------------------------------------------------------- 1 | export type GetUsersResponseData = { 2 | id: string; 3 | name: string; 4 | email: string; 5 | }[]; 6 | 7 | export type GetUserResponseData = { 8 | id: string; 9 | name: string; 10 | email: string; 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/utils/api/api.Types/axios.Postapi.Types.ts: -------------------------------------------------------------------------------- 1 | export type verifyPayload = { 2 | otp: number; 3 | phone: string; 4 | }; 5 | -------------------------------------------------------------------------------- /frontend/src/utils/api/config/axios.Config.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosRequestConfig } from "axios"; 2 | 3 | export const api = axios.create({ 4 | baseURL: import.meta.env.VITE_APP_BASE_URL, 5 | timeout: 5000 6 | }); 7 | 8 | export const apiRequest = async (config: AxiosRequestConfig) => { 9 | try { 10 | const response = await api(config); 11 | return response; 12 | } catch (error) { 13 | console.error(error, "errr"); 14 | return error; 15 | } 16 | }; 17 | 18 | export const headerConfg = () => { 19 | const token = localStorage.getItem("adminToken"); 20 | if (token) { 21 | return { 22 | Authorization: ` Bearer ${token}` 23 | }; 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /frontend/src/utils/api/config/axios.DeleteApi.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from "axios"; 2 | import { apiRequest, headerConfg } from "./axios.Config"; 3 | 4 | export const deleteStudentData = async (id:string) => { 5 | const config: AxiosRequestConfig = { 6 | method: "DELETE", 7 | url: `api/student/delete/`+id, 8 | headers: headerConfg() 9 | 10 | }; 11 | return await apiRequest(config); 12 | }; -------------------------------------------------------------------------------- /frontend/src/utils/api/config/axios.GetApi.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from "axios"; 2 | import { apiRequest, headerConfg } from "./axios.Config"; 3 | 4 | export const getDailyMetrics = async () => { 5 | const config: AxiosRequestConfig = { 6 | method: "GET", 7 | url: "api/student/daily-metrics", 8 | headers: headerConfg() 9 | }; 10 | return await apiRequest(config); 11 | }; 12 | export const getLeaderboard = async () => { 13 | const config: AxiosRequestConfig = { 14 | method: "GET", 15 | url: "api/student/leaderboard", 16 | headers: headerConfg() 17 | }; 18 | return await apiRequest(config); 19 | }; 20 | 21 | export const getAllStudents = async (pageNumber: number) => { 22 | const pageLimit = 100; 23 | const config: AxiosRequestConfig = { 24 | method: "GET", 25 | url: `api/student/all?page=${pageNumber}&limit=${pageLimit}`, 26 | headers: headerConfg() 27 | }; 28 | return await apiRequest(config); 29 | }; 30 | 31 | export const getNotDoneStudents = async () => { 32 | const config: AxiosRequestConfig = { 33 | method: "GET", 34 | url: "api/student/not-doing", 35 | headers: headerConfg() 36 | }; 37 | return await apiRequest(config); 38 | }; 39 | export const searchStudents = async (query: string) => { 40 | const config: AxiosRequestConfig = { 41 | method: "GET", 42 | url: `api/student/search?query=${query}`, 43 | headers: headerConfg() 44 | }; 45 | return await apiRequest(config); 46 | }; 47 | 48 | export const searchStudentsNotDone = async (query: string) => { 49 | const config: AxiosRequestConfig = { 50 | method: "GET", 51 | url: `api/student/search/not?query=${query}`, 52 | headers: headerConfg() 53 | }; 54 | return await apiRequest(config); 55 | }; 56 | 57 | export const weeklyMetrics = async () => { 58 | const config: AxiosRequestConfig = { 59 | method: "GET", 60 | url: "api/student/weekly-metrics", 61 | headers: headerConfg() 62 | }; 63 | return await apiRequest(config); 64 | }; 65 | -------------------------------------------------------------------------------- /frontend/src/utils/api/config/axios.PostAPi.ts: -------------------------------------------------------------------------------- 1 | import { AxiosRequestConfig } from "axios"; 2 | import { apiRequest, headerConfg } from "./axios.Config"; 3 | import { verifyPayload } from "../api.Types/axios.Postapi.Types"; 4 | import { studentAuth } from "../../validation/formValidation"; 5 | 6 | 7 | export const adminAuth = async (Phone: string) => { 8 | const config: AxiosRequestConfig = { 9 | method: "POST", 10 | url: `api/admin/signin`, 11 | data: { phone: Phone } 12 | }; 13 | return await apiRequest(config); 14 | }; 15 | 16 | export const adminVerify = async (verifyPayload: verifyPayload) => { 17 | const config: AxiosRequestConfig = { 18 | method: "POST", 19 | url: `api/admin/verify-otp`, 20 | data: verifyPayload 21 | }; 22 | return await apiRequest(config); 23 | }; 24 | export const studentsAuth = async (authPayload: studentAuth) => { 25 | const config: AxiosRequestConfig = { 26 | method: "POST", 27 | url: `api/student/add`, 28 | headers: headerConfg(), 29 | data: authPayload 30 | }; 31 | return await apiRequest(config); 32 | }; 33 | export const editeStudentData = async (id:string,payload:any) => { 34 | const config: AxiosRequestConfig = { 35 | method: "POST", 36 | url: `api/student/edit/`+id, 37 | headers: headerConfg(), 38 | data:payload 39 | }; 40 | return await apiRequest(config); 41 | }; -------------------------------------------------------------------------------- /frontend/src/utils/routes/ProtectedRoute.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, Outlet } from "react-router-dom"; 2 | 3 | function ProtectedRoute() { 4 | let storedValue: any = localStorage.getItem("adminAuth"); 5 | return JSON.parse(storedValue) ? : ; 6 | 7 | } 8 | 9 | export default ProtectedRoute; 10 | -------------------------------------------------------------------------------- /frontend/src/utils/validation/formValidation.tsx: -------------------------------------------------------------------------------- 1 | import { z, ZodType } from "zod"; 2 | import { useForm } from "react-hook-form"; 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | 5 | // Validation for Authentication 6 | export type PhoneNumberData = { 7 | phone: string; 8 | }; 9 | export const phoneNumberSchema: ZodType = z.object({ 10 | phone: z 11 | .string() 12 | .min(10, { message: "Phone number should be at least 10 digits" }) 13 | .max(10, { message: "Phone number should not exceed 10 digits" }) 14 | }); 15 | export const usePhoneNumberValidate = () => { 16 | const { 17 | reset, 18 | register, 19 | handleSubmit, 20 | formState: { errors } 21 | } = useForm({ resolver: zodResolver(phoneNumberSchema) }); 22 | return { 23 | register, 24 | handleSubmit, 25 | errors, 26 | reset 27 | }; 28 | }; 29 | 30 | // Validation for OTP 31 | 32 | export type OtpData = { 33 | otp: string; 34 | }; 35 | export const OtpDataSchema: ZodType = z.object({ 36 | otp: z 37 | .string() 38 | .min(4, { message: "Phone number should be at least 4 digits" }) 39 | .max(4, { message: "Phone number should not exceed 4 digits" }) 40 | }); 41 | export const useOtpValidation = () => { 42 | const { 43 | reset, 44 | register, 45 | handleSubmit, 46 | formState: { errors } 47 | } = useForm({ resolver: zodResolver(OtpDataSchema) }); 48 | return { 49 | register, 50 | handleSubmit, 51 | errors, 52 | reset 53 | }; 54 | }; 55 | 56 | // Validation for students login 57 | 58 | export type studentAuth = { 59 | name: string; 60 | lastName:string; 61 | phone: string; 62 | email: string; 63 | leetcodeId: string; 64 | domain: string; 65 | batch: string; 66 | }; 67 | export const studentAuthSchema: ZodType = z.object({ 68 | name: z 69 | .string() 70 | .refine((value) => value.trim() !== "", { 71 | message: "Name cannot be empty" 72 | }) 73 | .refine((value) => /^[a-zA-Z ]+$/.test(value), { 74 | message: "Name must contain only alphabetic characters" 75 | }), 76 | lastName:z.string() 77 | .refine((value) => value.trim() !== "", { 78 | message: "Name cannot be empty" 79 | }) 80 | .refine((value) => /^[a-zA-Z ]+$/.test(value), { 81 | message: "Last name must contain only alphabetic characters" 82 | }), 83 | phone: z 84 | .string() 85 | .min(10, { message: "Whatsapp number should be atleast 10 digits" }) 86 | .max(10, { message: "Whatsapp number should not exceed 10 digits" }) 87 | .refine((value) => /^\d+$/.test(value), { message: "Only numeric characters are allowed" }), 88 | email: z.string().email({ message: "Invalid email format" }), 89 | leetcodeId: z.string().refine((value) => value.trim() !== "", { 90 | message: "Name cannot be empty" 91 | }), 92 | batch: z 93 | .string() 94 | .refine((value) => value.length >= 5 && value.length <= 6, { 95 | message: "Please provide the batch based one the example" 96 | }) 97 | .refine((value) => /^[A-Z0-9 ]+$/.test(value), { 98 | message: "Special charatcters or small leters are not accepted " 99 | }) 100 | .refine((value) => value.trim() !== "", { 101 | message: "Name cannot be empty" 102 | }), 103 | domain: z 104 | .string() 105 | .refine((value) => value.trim() !== "", { 106 | message: "Domain cannot be empty" 107 | }) 108 | }); 109 | export const useStudentAuth = () => { 110 | const { 111 | reset, 112 | register, 113 | handleSubmit, 114 | formState: { errors } 115 | } = useForm({ resolver: zodResolver(studentAuthSchema) }); 116 | return { 117 | register, 118 | handleSubmit, 119 | errors, 120 | reset 121 | }; 122 | }; 123 | 124 | -------------------------------------------------------------------------------- /frontend/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [require("preline/plugin")] 8 | }; 9 | 10 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | host: true, 9 | port: 8000, // This is the port which we will use in docker 10 | watch: { 11 | usePolling: true 12 | } 13 | }, 14 | build: { 15 | chunkSizeWarningLimit: 1600 16 | }, 17 | preview: { 18 | port: 8000 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /installer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if user has root/sudo access 4 | if [[ $(id -u) -ne 0 ]]; then 5 | echo "This script must be run as root or with sudo." 6 | exit 1 7 | fi 8 | 9 | # Check user's operating system 10 | os=$(uname -s) 11 | case $os in 12 | Linux) 13 | # Install required packages using package manager 14 | if command -v apt-get &> /dev/null; then 15 | echo "Installing packages using apt-get..." 16 | apt-get update 17 | echo "Installing latest version of docker..." 18 | curl -fsSL https://get.docker.com -o get-docker.sh 19 | sh get-docker.sh 20 | apt install docker-compose 21 | echo "Packages installed successfully." 22 | sudo apt-get update 23 | sudo apt-get install -y ca-certificates curl gnupg 24 | sudo mkdir -p /etc/apt/keyrings 25 | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg 26 | NODE_MAJOR=20 27 | echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list 28 | sudo apt-get update 29 | sudo apt-get install nodejs -y 30 | sudo apt-get install make -y 31 | else 32 | echo "Unsupported package manager." 33 | exit 1 34 | fi 35 | ;; 36 | *) 37 | echo "Unsupported operating system." 38 | exit 1 39 | ;; 40 | esac 41 | 42 | Clone Git repo and run Docker Compose 43 | echo "Cloning Git repo..." 44 | git clone https://github.com/brocamp/LeetCode_Tracker -------------------------------------------------------------------------------- /proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | RUN rm /etc/nginx/nginx.conf 4 | 5 | COPY nginx.conf /etc/nginx/nginx.conf -------------------------------------------------------------------------------- /proxy/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 4; 2 | 3 | events { worker_connections 1024;} 4 | 5 | http { 6 | 7 | server { 8 | 9 | listen 80; 10 | charset utf-8; 11 | 12 | location / { 13 | proxy_pass http://frontend:8000; 14 | proxy_set_header Host $host; 15 | proxy_set_header X-Real-IP $remote_addr; 16 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 17 | } 18 | location /api { 19 | proxy_pass http://backend:4000; 20 | proxy_set_header Host $host; 21 | proxy_set_header X-Real-IP $remote_addr; 22 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 23 | } 24 | 25 | } 26 | 27 | 28 | } --------------------------------------------------------------------------------