├── docs
├── .nojekyll
├── CNAME
├── README.md
└── index.html
├── frontend
├── src
│ ├── App.css
│ ├── vite-env.d.ts
│ ├── utils
│ │ ├── api
│ │ │ ├── api.Types
│ │ │ │ ├── axios.Postapi.Types.ts
│ │ │ │ └── axios.Getapi.Typses.ts
│ │ │ └── config
│ │ │ │ ├── axios.DeleteApi.ts
│ │ │ │ ├── axios.Config.ts
│ │ │ │ ├── axios.PostAPi.ts
│ │ │ │ └── axios.GetApi.ts
│ │ ├── routes
│ │ │ └── ProtectedRoute.tsx
│ │ └── validation
│ │ │ └── formValidation.tsx
│ ├── index.css
│ ├── main.tsx
│ ├── components
│ │ ├── alert.tsx
│ │ ├── Navbar.tsx
│ │ ├── ErrorComponent.tsx
│ │ ├── LeaderBoardStatic.tsx
│ │ ├── LineChart.tsx
│ │ ├── LeaderBoard.tsx
│ │ ├── Sidebar.tsx
│ │ ├── PieData.tsx
│ │ ├── Login.tsx
│ │ ├── StudentsDataUpdate.tsx
│ │ ├── StudentLogin.tsx
│ │ ├── StudentsNotdone.tsx
│ │ └── AllStudentData.tsx
│ ├── Features
│ │ ├── StudentsDetails.tsx
│ │ ├── Analytic.tsx
│ │ └── LeaderBorde.tsx
│ ├── pages
│ │ └── Home.tsx
│ └── App.tsx
├── .dockerignore
├── .env.development
├── .env.production
├── .vite
│ └── deps_temp_1c3d506d
│ │ └── package.json
├── postcss.config.js
├── public
│ ├── logo-black.png
│ ├── 81414-middle.png
│ ├── android-chrome-192x192.png
│ ├── istockphoto-1337144146-612x612.jpg
│ └── vite.svg
├── Dockerfile.dev
├── Dockerfile.prod
├── tailwind.config.js
├── .prettierrc
├── tsconfig.node.json
├── vite.config.ts
├── .eslintrc.cjs
├── index.html
├── tsconfig.json
└── package.json
├── backend
├── .dockerignore
├── src
│ ├── config
│ │ ├── whatsapp.ts
│ │ ├── index.ts
│ │ ├── db.config.ts
│ │ └── logger.ts
│ ├── api
│ │ ├── controller
│ │ │ ├── index.ts
│ │ │ ├── admin.controller.ts
│ │ │ ├── validator.ts
│ │ │ └── student.controller.ts
│ │ ├── middleware
│ │ │ ├── index.ts
│ │ │ ├── validator.middleware.ts
│ │ │ ├── error.handler.middleware.ts
│ │ │ └── auth.middleware.ts
│ │ ├── errors
│ │ │ ├── index.ts
│ │ │ ├── custom.error.ts
│ │ │ ├── notfound.error.ts
│ │ │ ├── badrequest.error.ts
│ │ │ ├── database.connection.error.ts
│ │ │ └── validation.errror.ts
│ │ └── utils.ts
│ ├── database
│ │ ├── model
│ │ │ ├── index.ts
│ │ │ ├── weeklymetrics.model.ts
│ │ │ ├── admin.model.ts
│ │ │ └── students.model.ts
│ │ └── repository
│ │ │ ├── index.ts
│ │ │ ├── weeklymetrics.repository.ts
│ │ │ ├── admin.repository.ts
│ │ │ └── student.repository.ts
│ ├── index.ts
│ ├── app.ts
│ └── handler
│ │ ├── cronjob.ts
│ │ ├── utils.ts
│ │ ├── leetcode.ts
│ │ └── leetcode-updater.ts
├── .env.example
├── Dockerfile.dev
├── .prettierrc
├── Dockerfile.prod
├── package.json
└── tsconfig.json
├── proxy
├── Dockerfile
└── nginx.conf
├── Makefile
├── .github
└── workflows
│ ├── prettier.yaml
│ └── production.deployment.yaml
├── .gitignore
├── docker-compose-prod.yaml
├── docker-compose-dev.yaml
└── installer.sh
/docs/.nojekyll:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/App.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | hello.pranavs.tech
--------------------------------------------------------------------------------
/frontend/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/*
--------------------------------------------------------------------------------
/backend/.dockerignore:
--------------------------------------------------------------------------------
1 |
2 | node_modules/*
3 |
4 |
--------------------------------------------------------------------------------
/backend/src/config/whatsapp.ts:
--------------------------------------------------------------------------------
1 | // will implement in future
2 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Headline
2 |
3 | > An awesome project.
4 |
--------------------------------------------------------------------------------
/frontend/.env.development:
--------------------------------------------------------------------------------
1 | VITE_APP_BASE_URL = 'http://localhost:80/'
--------------------------------------------------------------------------------
/frontend/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/.env.production:
--------------------------------------------------------------------------------
1 | VITE_APP_BASE_URL = 'https://leet.brototype.com/'
--------------------------------------------------------------------------------
/frontend/.vite/deps_temp_1c3d506d/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "module"
3 | }
4 |
--------------------------------------------------------------------------------
/backend/src/config/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./db.config";
2 | export * from "./logger";
3 |
--------------------------------------------------------------------------------
/backend/src/api/controller/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./admin.controller";
2 | export * from "./student.controller";
3 |
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/frontend/public/logo-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/HEAD/frontend/public/logo-black.png
--------------------------------------------------------------------------------
/proxy/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:latest
2 |
3 | RUN rm /etc/nginx/nginx.conf
4 |
5 | COPY nginx.conf /etc/nginx/nginx.conf
--------------------------------------------------------------------------------
/frontend/public/81414-middle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/HEAD/frontend/public/81414-middle.png
--------------------------------------------------------------------------------
/frontend/src/utils/api/api.Types/axios.Postapi.Types.ts:
--------------------------------------------------------------------------------
1 | export type verifyPayload = {
2 | otp: number;
3 | phone: string;
4 | };
5 |
--------------------------------------------------------------------------------
/frontend/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/HEAD/frontend/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | *::-webkit-scrollbar {
3 | display: none;
4 | }
5 | @tailwind components;
6 | @tailwind utilities;
7 |
--------------------------------------------------------------------------------
/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/src/database/model/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./admin.model";
2 | export * from "./students.model";
3 | export * from "./weeklymetrics.model";
4 |
--------------------------------------------------------------------------------
/frontend/public/istockphoto-1337144146-612x612.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/brocamp/LeetCode_Tracker/HEAD/frontend/public/istockphoto-1337144146-612x612.jpg
--------------------------------------------------------------------------------
/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/database/repository/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./admin.repository";
2 | export * from "./student.repository";
3 | export * from "./weeklymetrics.repository";
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/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/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" ]
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 | };
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Brototype-LeetCodeTracker
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/frontend/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | function Navbar() {
2 | return (
3 | <>
4 |
5 |
6 |
7 |

8 |
9 |
10 |
11 | >
12 | );
13 | }
14 |
15 | export default Navbar;
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
19 | >
20 | );
21 | }
22 |
23 | export default Home;
24 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
19 |
20 |
21 |
22 |
23 |
24 | >
25 | );
26 | }
27 |
28 | export default Analytic;
29 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/frontend/src/components/ErrorComponent.tsx:
--------------------------------------------------------------------------------
1 | function ErrorComponent() {
2 | return (
3 | <>
4 |
5 |
6 |
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/src/components/LeaderBoardStatic.tsx:
--------------------------------------------------------------------------------
1 | function LeaderBoardStatic() {
2 | return (
3 | <>
4 |
30 | >
31 | );
32 | }
33 |
34 | export default LeaderBoardStatic;
35 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
96 |
97 | >
98 | );
99 | }
100 |
101 | export default PieData;
102 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
93 | ) : (
94 |
126 | )}
127 |
128 |
129 |
130 | >
131 | );
132 | };
133 |
134 | export default Login;
135 |
--------------------------------------------------------------------------------
/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 |
72 |
73 | {/* End Icon */}
74 |
Publish Learderbord
75 |
Are you sure you would like to publish leader board?
76 |
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/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 |
210 |
211 | >
212 | );
213 | }
214 |
215 | export default StudentsDataUpdate;
216 |
--------------------------------------------------------------------------------
/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 |
186 | );
187 | }
188 |
189 | export default StudentLogin;
190 |
191 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
120 |
121 |
122 |
123 |
124 |
130 |
131 |
134 |
135 |
136 |
137 |
155 |
156 |
157 | {/* Icon */}
158 |
159 |
168 |
169 | {/* End Icon */}
170 |
Send Warning
171 |
Are you sure you would like to send warning....?
172 |
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/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 |
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 |
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 |
--------------------------------------------------------------------------------