├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── jsconfig.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── api ├── config.js ├── exam.js ├── mock-data.js ├── user.js └── utils.js ├── app.jsx ├── assets └── icons │ ├── announcementBlack.png │ ├── announcementWhite.png │ ├── crown.png │ ├── diskette.png │ ├── tickGreen.png │ ├── tickRed.png │ └── user.png ├── common-style.js ├── components ├── carousel │ ├── index.jsx │ └── styles.js ├── date-time-picker │ ├── date-time.jsx │ ├── date.jsx │ └── time.jsx ├── dialog │ ├── confirmation.jsx │ └── index.jsx ├── dropdown-field │ └── index.jsx ├── exam-banner │ ├── index.jsx │ └── styles.js ├── exam-details-card │ └── index.jsx ├── exam-details-card2 │ ├── index.jsx │ └── styles.js ├── file-upload-input │ └── index.jsx ├── header │ └── index.jsx ├── multi-select-dropdown │ └── index.jsx ├── navbar │ └── index.jsx ├── speed-dial │ └── index.jsx ├── text-editor │ └── index.jsx └── text-input-field │ └── index.jsx ├── index.css ├── index.js ├── layouts ├── a.css ├── login.jsx ├── questionType-result │ ├── fill-blanks.jsx │ ├── mcq-multiple.jsx │ └── mcq-single.jsx ├── register.jsx └── request-full-screen.jsx ├── pages ├── exam-creation │ ├── index.jsx │ ├── page1.jsx │ ├── page2.jsx │ ├── page3.jsx │ └── question-types │ │ ├── fill-blanks.jsx │ │ └── mcq.jsx ├── exam-details │ ├── index.jsx │ ├── styles.js │ └── tab-content │ │ ├── about.jsx │ │ ├── result.jsx │ │ └── scores.jsx ├── give-exam │ ├── index.jsx │ ├── page1.jsx │ ├── page2.jsx │ └── question-options │ │ ├── fill-blanks.jsx │ │ ├── mcq-multiple.jsx │ │ └── mcq-single.jsx ├── home │ ├── arrows.jsx │ ├── exams-horizontal-scroll-section.jsx │ ├── index.jsx │ └── styles.js └── profile │ ├── User.jsx │ ├── crop-image.jsx │ ├── crop-img-utils.jsx │ ├── details-section.jsx │ ├── exams-list.jsx │ └── profile-section.jsx ├── redux ├── slices │ ├── auth.js │ ├── confirmation-dialog.js │ └── dialog-visibility.js └── store.js └── utilities ├── functions.js └── theme.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | yarn.lock 26 | package-lock.json 27 | *.env -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 150, 7 | "jsxSingleQuote": true, 8 | "bracketSameLine": true, 9 | "bracketSpacing": true 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "px-to-rem.px-per-rem": 10 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quizzio: Frontend 2 | 3 | Frontend for the Exam Simulation project. 4 | 5 | ## Installation 6 | 7 | Install my-project with npm 8 | 9 | ```bash 10 | npm install 11 | npm start 12 | ``` 13 | 14 | ## Demo 15 | 16 | https://quizzio.vercel.app 17 | 18 | ## Color Reference 19 | 20 | | Color | Hex | 21 | | ------- | ---------------------------------------------------------------- | 22 | | Black | ![#0a192f](https://via.placeholder.com/10/000000?text=+) #000000 | 23 | | White | ![#ffffff](https://via.placeholder.com/10/ffffff?text=+) #ffffff | 24 | | Primary | ![#1AB273](https://via.placeholder.com/10/1AB273?text=+) #1AB273 | 25 | 26 | ## Authors 27 | 28 | - [@Bihan001](https://www.github.com/Bihan001) 29 | - [@niharika2k00](https://github.com/niharika2k00) 30 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "src" 4 | }, 5 | "include": [ 6 | "src" 7 | ] 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.5.0", 7 | "@emotion/styled": "^11.3.0", 8 | "@mui/icons-material": "^5.0.5", 9 | "@mui/lab": "^5.0.0-alpha.53", 10 | "@mui/material": "^5.0.6", 11 | "@mui/styles": "^5.0.2", 12 | "@reduxjs/toolkit": "^1.7.1", 13 | "@tinymce/tinymce-react": "^3.13.0", 14 | "axios": "^0.24.0", 15 | "date-fns": "^2.25.0", 16 | "dompurify": "^2.3.4", 17 | "notistack": "^2.0.3", 18 | "react": "^17.0.2", 19 | "react-dom": "^17.0.2", 20 | "react-easy-crop": "^3.5.3", 21 | "react-horizontal-scrolling-menu": "^2.7.1", 22 | "react-redux": "^7.2.6", 23 | "react-router-dom": "^5.2.0", 24 | "react-scripts": "4.0.3", 25 | "react-swipeable-views": "^0.14.0" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts start", 29 | "build": "react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Exam Platform 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/api/config.js: -------------------------------------------------------------------------------- 1 | import Axios from 'axios'; 2 | 3 | export const getHeaders = () => { 4 | return { headers: { Authorization: `Bearer ${localStorage.getItem('AUTH_TOKEN')}` } }; 5 | }; 6 | 7 | export const axios = Axios.create({ 8 | baseURL: true || process.env.NODE_ENV === 'production' ? 'https://quizyfy.herokuapp.com' : 'http://localhost:5000', 9 | }); 10 | -------------------------------------------------------------------------------- /src/api/exam.js: -------------------------------------------------------------------------------- 1 | 2 | import { axios, getHeaders } from './config'; 3 | import { examScores } from './mock-data'; 4 | 5 | // ------------------------------------------------ 6 | // baseURL: 'http://localhost:5000' 7 | // ------------------------------------------------- 8 | 9 | export const getExams = (filters) => { 10 | return axios.post('/exam/all', filters); 11 | }; 12 | 13 | export const getExamDetails = (id) => { 14 | return axios.get(`/exam/${id}`); 15 | }; 16 | 17 | export const createExam = async (data) => { 18 | return axios.post('/exam/create', data, getHeaders()); 19 | }; 20 | 21 | export const registerInExam = async (data) => { 22 | return axios.post('/exam/register', data, getHeaders()); 23 | }; 24 | 25 | export const getUserExamRegisterStatus = async (examId) => { 26 | return axios.get(`/exam/exam-registered?examId=${examId}`, getHeaders()); 27 | }; 28 | 29 | export const startExam = (examId) => { 30 | return axios.get(`/exam/${examId}/start`, getHeaders()); 31 | }; 32 | 33 | export const submitExamAnswers = (data) => { 34 | return axios.post(`/exam/submit`, data, getHeaders()); 35 | }; 36 | 37 | export const getExamTags = () => { 38 | return axios.get('/exam/tags'); 39 | }; 40 | 41 | export const getExamTypes = () => { 42 | return new Promise((resolve, reject) => { 43 | setTimeout(() => { 44 | resolve({ 45 | data: [ 46 | { label: 'Public', value: 'public' }, 47 | { label: 'Private', value: 'private' }, 48 | ], 49 | }); 50 | }, 100); 51 | }); 52 | }; 53 | 54 | export const getQuestionTypes = () => { 55 | return axios.get('/exam/question-types'); 56 | }; 57 | 58 | 59 | export const getExamResult = (examId) => { 60 | return axios.get(`/exam/solution?examId=${examId}`, getHeaders()); 61 | }; 62 | 63 | 64 | export const getExamScores = (examId) => { 65 | return axios.get(`/exam/scores?examId=${examId}`, getHeaders()); 66 | }; 67 | 68 | 69 | 70 | 71 | // ----------------------- 72 | // DUMMY DATA FORMAT 73 | // ----------------------- 74 | /* export const getExamScores = (examId) => { 75 | return axios.get(`/exam/scores?examId=${examId}`, getHeaders()); 76 | return new Promise((resolve, reject) => { 77 | setTimeout(() => { 78 | resolve({ 79 | data: { data: examScores }, 80 | }); 81 | }, 1000); 82 | }); 83 | }; */ 84 | -------------------------------------------------------------------------------- /src/api/mock-data.js: -------------------------------------------------------------------------------- 1 | export const examFetchedData = { 2 | success: true, 3 | status: 'success', 4 | message: 'Exam Started !', 5 | data: { 6 | id: 'db5ed499-8898-4558-89ca-d8f35a11572f', 7 | name: 'Data Structures and Algorithms', 8 | description: 'This is an annual Exam of DSA held by TTT Community , Participate to win exciting prices !', 9 | image: 'https://image.freepik.com/free-vector/online-exam-isometric-web-banner_33099-2305.jpg', 10 | userId: '8f1576ca-80a5-4f96-a215-04caeb307e15', 11 | tags: ['Programming', 'Data Structures', 'Competitive Programming'], 12 | questions: [ 13 | { 14 | id: 1, 15 | question: 'Which alphabets are vowels ?', 16 | options: [ 17 | { 18 | id: 1, 19 | data: 'uUUUUUUUUUUU', 20 | }, 21 | { 22 | id: 0, 23 | data: 'VVVVVVVVVVVVVVVVVVVv', 24 | }, 25 | ], 26 | type: 'multipleOptions', 27 | correctOption: [0, 1], 28 | marks: 5, 29 | negMarks: -2, 30 | }, 31 | { 32 | id: 2, 33 | question: 'You are ____', 34 | type: 'fillInTheBlanks', 35 | correctOption: 'Gay', 36 | marks: 5, 37 | negMarks: -2, 38 | }, 39 | { 40 | id: 0, 41 | question: 'Number of english alphabets', 42 | options: [ 43 | { 44 | id: 1, 45 | data: '20', 46 | }, 47 | { 48 | id: 0, 49 | data: '26', 50 | }, 51 | ], 52 | type: 'mcq', 53 | correctOption: [0], 54 | marks: 5, 55 | negMarks: -2, 56 | }, 57 | ], 58 | startTime: '2021-11-20T05:08:30.000Z', 59 | duration: 33, 60 | ongoing: 1, 61 | finished: 0, 62 | isPrivate: 0, 63 | }, 64 | }; 65 | 66 | export const examResult = [ 67 | { 68 | question: 'What is the return type of printf in C?', 69 | type: 'mcq', 70 | options: [ 71 | { id: 1, data: 'char' }, 72 | { id: 2, data: 'float' }, 73 | { id: 3, data: 'int' }, 74 | { id: 4, data: 'void' }, 75 | ], 76 | marks: 2, 77 | negMarks: 2, 78 | givenOption: [3], 79 | correctOption: [3], 80 | }, 81 | { 82 | question: 'Select the languages that uses an compiler', 83 | type: 'multipleOptions', 84 | options: [ 85 | { id: 1, data: 'C++' }, 86 | { id: 2, data: 'C' }, 87 | { id: 3, data: 'Python' }, 88 | { id: 4, data: 'C#' }, 89 | ], 90 | marks: 2, 91 | negMarks: 2, 92 | givenOption: [1, 3], 93 | correctOption: [1, 2, 4], 94 | }, 95 | { 96 | question: 'Which function is used to print a string in Java?', 97 | type: 'fillInTheBlanks', 98 | marks: 2, 99 | negMarks: 2, 100 | givenOption: 'System.out.println("Hello World");', 101 | correctOption: 'System.out.println', 102 | }, 103 | ]; 104 | 105 | export const examScores = { 106 | participantId: "76fa9982-46e6-4bee-a9f8-0b0de03b3bbf", 107 | rank: 2, 108 | finishTime: 1641894484307, 109 | totalScore: 1, 110 | topPerformers: [ 111 | { 112 | participantId: "dc738946-5461-4e6a-bb4f-dc16d8a94e09", 113 | totalScore: 4, 114 | rank: 1, 115 | finishTime: 1641894515349, 116 | name: "Nero Devil Trigger" 117 | }, 118 | { 119 | participantId: "76fa9982-46e6-4bee-a9f8-0b0de03b3bbf", 120 | totalScore: 1, 121 | rank: 2, 122 | finishTime: 1641894484307, 123 | name: "Dante" 124 | }, 125 | { 126 | participantId: "dc738946-5461-4e6a-bb4f-dc16d8a94e09", 127 | totalScore: 4, 128 | rank: 1, 129 | finishTime: 1641894515349, 130 | name: "Nero Devil Trigger" 131 | }, 132 | { 133 | participantId: "dc738946-5461-4e6a-bb4f-dc16d8a94e09", 134 | totalScore: 4, 135 | rank: 1, 136 | finishTime: 1641894515349, 137 | name: "Nero Devil Trigger" 138 | }, 139 | { 140 | participantId: "dc738946-5461-4e6a-bb4f-dc16d8a94e09", 141 | totalScore: 4, 142 | rank: 1, 143 | finishTime: 1641894515349, 144 | name: "Nero Devil Trigger" 145 | }, 146 | ] 147 | }; 148 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | import { axios, getHeaders } from './config'; 2 | /** 3 | * 4 | * @param {string} email User's Email 5 | * @param {string} password User's Password 6 | * @returns Promise> 7 | * @example 8 | * import {loginWithEmailAndPassword} from 'api/user'; 9 | * loginWithEmailAndPassword('bihan', 'abcdefgh').then(res => console.log(res.data)).catch(err => console.log(err)); 10 | */ 11 | 12 | export const loginWithEmailAndPassword = (email, password) => { 13 | return axios.post('/user/login', { email, password }, getHeaders()); 14 | }; 15 | 16 | export const registerNewUser = (data) => { 17 | return axios.post('/user/register', data, getHeaders()); 18 | }; 19 | 20 | export const getCurrentUser = () => { 21 | return axios.get('/user/current', getHeaders()); 22 | }; 23 | 24 | export const getHostedExams = () => { 25 | return axios.get('/user/exams-hosted', getHeaders()); 26 | }; 27 | 28 | export const getGivenExams = () => { 29 | return axios.get('/user/exams-given', getHeaders()); 30 | }; 31 | -------------------------------------------------------------------------------- /src/api/utils.js: -------------------------------------------------------------------------------- 1 | import { axios, getHeaders } from './config'; 2 | 3 | export const uploadImages = (formData) => { 4 | const headers = getHeaders(); 5 | headers.headers['Content-Type'] = 'multipart/form-data'; 6 | return axios.post('/utils/upload/images', formData, headers); 7 | }; 8 | -------------------------------------------------------------------------------- /src/app.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useMemo, useEffect } from 'react'; 2 | import { Switch, Route, useLocation, Redirect } from 'react-router-dom'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { SnackbarProvider } from 'notistack'; 5 | import CreateExam from 'pages/exam-creation'; 6 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 7 | import { CssBaseline, Paper, Slide } from '@mui/material'; 8 | import Navbar from 'components/navbar'; 9 | import Register from 'layouts/register'; 10 | import Login from 'layouts/login'; 11 | import User from 'pages/profile/User'; 12 | import Home from 'pages/home'; 13 | import GiveExam from 'pages/give-exam'; 14 | import ExamDetails from 'pages/exam-details'; 15 | import getDesignTokens from 'utilities/theme'; 16 | import { getCurrentUser } from 'api/user'; 17 | import { setUserAndToken } from 'redux/slices/auth'; 18 | import ConfirmationDialog from 'components/dialog/confirmation'; 19 | 20 | const App = () => { 21 | const location = useLocation(); 22 | const dispatch = useDispatch(); 23 | const { user } = useSelector((state) => state.auth); 24 | const [themeMode, setThemeMode] = useState(localStorage.getItem('theme') || 'light'); 25 | 26 | useEffect(() => { 27 | fetchCurrentUser(); 28 | }, []); 29 | 30 | const fetchCurrentUser = async () => { 31 | try { 32 | const res = await getCurrentUser(); 33 | dispatch(setUserAndToken({ user: res.data.data })); 34 | } catch (err) { 35 | console.error(err); 36 | } 37 | }; 38 | 39 | // The dark mode switch would invoke this method 40 | const colorMode = useMemo( 41 | () => ({ 42 | toggleColorMode: () => { 43 | setThemeMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); 44 | }, 45 | }), 46 | [] 47 | ); 48 | 49 | const toggleTheme = () => { 50 | setThemeMode((t) => { 51 | const newMode = t === 'light' ? 'dark' : 'light'; 52 | localStorage.setItem('theme', newMode); 53 | return newMode; 54 | }); 55 | }; 56 | 57 | // Update the theme only if the mode changes 58 | const theme = useMemo(() => createTheme(getDesignTokens(themeMode)), [themeMode]); 59 | 60 | return ( 61 | 62 | 70 | 71 | 72 | {!location.pathname.includes('/give') && } 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {/* /exam/:id */} 81 | 82 | 83 | 84 | 85 | 86 | 87 | ); 88 | }; 89 | 90 | export default App; 91 | -------------------------------------------------------------------------------- /src/assets/icons/announcementBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/src/assets/icons/announcementBlack.png -------------------------------------------------------------------------------- /src/assets/icons/announcementWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/src/assets/icons/announcementWhite.png -------------------------------------------------------------------------------- /src/assets/icons/crown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/src/assets/icons/crown.png -------------------------------------------------------------------------------- /src/assets/icons/diskette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/src/assets/icons/diskette.png -------------------------------------------------------------------------------- /src/assets/icons/tickGreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/src/assets/icons/tickGreen.png -------------------------------------------------------------------------------- /src/assets/icons/tickRed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/src/assets/icons/tickRed.png -------------------------------------------------------------------------------- /src/assets/icons/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niharika2k00/quizzio-frontend/95f471b8c70ef70340fb72d6328d72fb7b55ae38/src/assets/icons/user.png -------------------------------------------------------------------------------- /src/common-style.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@mui/styles'; 2 | 3 | export default makeStyles((theme) => ({ 4 | heading: { 5 | fontSize: '2.6rem', 6 | textAlign: 'center', 7 | fontWeight: '600', 8 | color: theme.palette.primary.main, 9 | alignItems: 'center', 10 | justifyContent: 'center', 11 | }, 12 | })); 13 | -------------------------------------------------------------------------------- /src/components/carousel/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import MobileStepper from '@mui/material/MobileStepper'; 3 | import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'; 4 | import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; 5 | import SwipeableViews from 'react-swipeable-views'; 6 | import { autoPlay } from 'react-swipeable-views-utils'; 7 | import { Button } from '@mui/material'; 8 | import { useTheme } from '@emotion/react'; 9 | import useStyles from './styles'; 10 | import ExamBanner from 'components/exam-banner'; 11 | import { ClassNames } from '@emotion/react'; 12 | 13 | const AutoPlaySwipeableViews = autoPlay(SwipeableViews); 14 | console.log(AutoPlaySwipeableViews); 15 | 16 | const Carousel = ({ data, type }) => { 17 | const theme = useTheme(); 18 | const classes = useStyles(); 19 | //Carousel States=========== 20 | const [activeStep, setActiveStep] = useState(0); 21 | const [maxSteps, setMaxSteps] = useState(0); 22 | //========================== 23 | 24 | // Map and send each data Object 25 | const Cards = { 26 | 'exam-banner': data.map((examDetail, index) => ), 27 | }; 28 | 29 | useEffect(() => { 30 | setMaxSteps(data.length); 31 | }, [data]); 32 | 33 | //Carousel Config functions-============== 34 | const handleNext = () => { 35 | setActiveStep((prevActiveStep) => prevActiveStep + 1); 36 | }; 37 | const handleBack = () => { 38 | setActiveStep((prevActiveStep) => prevActiveStep - 1); 39 | }; 40 | const handleStepChange = (step) => { 41 | setActiveStep(step); 42 | }; 43 | //======================================== 44 | 45 | return ( 46 | <> 47 | 53 | {Cards[type]} 54 | 55 | 56 | 63 | Next 64 | {theme.direction === 'rtl' ? : } 65 | 66 | } 67 | backButton={ 68 | 72 | } 73 | /> 74 | 75 | ); 76 | }; 77 | export default Carousel; 78 | -------------------------------------------------------------------------------- /src/components/carousel/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@mui/styles'; 2 | 3 | export default makeStyles((theme) => ({ 4 | card: { 5 | width: '85vw', 6 | margin: '0 auto', 7 | borderRadius: '.5rem', 8 | }, 9 | stepper: { 10 | margin: '0 auto', 11 | width: '60vw', 12 | backgroundColor: 'transparent', 13 | }, 14 | })); 15 | -------------------------------------------------------------------------------- /src/components/date-time-picker/date-time.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@mui/styles'; 4 | import { TextField, Typography } from '@mui/material'; 5 | import DateTimePicker from '@mui/lab/DateTimePicker'; 6 | import AdapterDateFns from '@mui/lab/AdapterDateFns'; 7 | import LocalizationProvider from '@mui/lab/LocalizationProvider'; 8 | 9 | const styles = (theme) => ({ 10 | textField: { 11 | width: 192, 12 | }, 13 | cssOutlinedInput: { 14 | '&$cssFocused $notchedOutline': { 15 | borderColor: '#1AB273 !important', 16 | }, 17 | }, 18 | cssFocused: {}, 19 | notchedOutline: { 20 | borderWidth: '1px', 21 | borderColor: 'rgba(0, 0, 0, 0.15) !important', 22 | color: 'black', 23 | // backgroundColor:'white' 24 | }, 25 | disable: { 26 | color: '#D9D7D7', 27 | fontSize: 12, 28 | height: 1, 29 | }, 30 | }); 31 | 32 | const DateTimeField = (props) => { 33 | const { name, value, onChange, label, disabled, required, fullWidth = false, style } = props; 34 | 35 | return ( 36 | <> 37 | 38 | {label} 39 | {required ? * : null} 40 | 41 | 42 |
43 | 44 | ( 49 | 50 | )} 51 | /> 52 | 53 |
54 | 55 | ); 56 | }; 57 | 58 | export default withStyles(styles)(DateTimeField); 59 | -------------------------------------------------------------------------------- /src/components/date-time-picker/date.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@mui/styles'; 4 | import { TextField, Typography } from '@mui/material'; 5 | import DesktopDatePicker from '@mui/lab/DesktopDatePicker'; 6 | import AdapterDateFns from '@mui/lab/AdapterDateFns'; 7 | import LocalizationProvider from '@mui/lab/LocalizationProvider'; 8 | 9 | const styles = (theme) => ({ 10 | textField: { 11 | width: 192, 12 | }, 13 | cssOutlinedInput: { 14 | '&$cssFocused $notchedOutline': { 15 | borderColor: '#1AB273 !important', 16 | }, 17 | }, 18 | cssFocused: {}, 19 | notchedOutline: { 20 | borderWidth: '1px', 21 | borderColor: 'rgba(0, 0, 0, 0.15) !important', 22 | color: 'black', 23 | // backgroundColor:'white' 24 | }, 25 | disable: { 26 | color: '#D9D7D7', 27 | fontSize: 12, 28 | height: 1, 29 | }, 30 | }); 31 | 32 | const DateField = (props) => { 33 | const { name, value, onChange, label, disabled, required, fullWidth = false, style } = props; 34 | 35 | return ( 36 | <> 37 | 38 | {label} 39 | {required ? * : null} 40 | 41 | 42 |
43 | 44 | ( 50 | 51 | )} 52 | /> 53 | 54 |
55 | 56 | ); 57 | }; 58 | 59 | export default withStyles(styles)(DateField); 60 | -------------------------------------------------------------------------------- /src/components/date-time-picker/time.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { withStyles } from '@mui/styles'; 4 | import { TextField, Typography } from '@mui/material'; 5 | import TimePicker from '@mui/lab/TimePicker'; 6 | import AdapterDateFns from '@mui/lab/AdapterDateFns'; 7 | import LocalizationProvider from '@mui/lab/LocalizationProvider'; 8 | 9 | const styles = (theme) => ({ 10 | cssOutlinedInput: { 11 | '&$cssFocused $notchedOutline': { 12 | borderColor: '#1AB273 !important', 13 | }, 14 | }, 15 | cssFocused: {}, 16 | notchedOutline: { 17 | borderWidth: '1px', 18 | borderColor: 'rgba(0, 0, 0, 0.15) !important', 19 | color: 'black', 20 | // backgroundColor:'white' 21 | }, 22 | disable: { 23 | color: '#D9D7D7', 24 | fontSize: 12, 25 | height: 1, 26 | }, 27 | }); 28 | 29 | const DateField = (props) => { 30 | const { label, disabled, name, value, onChange, required, fullWidth = false, style } = props; 31 | 32 | return ( 33 | <> 34 | 35 | {label} 36 | {required ? * : null} 37 | 38 |
39 | 40 | ( 45 | 46 | )} 47 | /> 48 | 49 |
50 | 51 | ); 52 | }; 53 | 54 | export default withStyles(styles)(DateField); 55 | -------------------------------------------------------------------------------- /src/components/dialog/confirmation.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { useSelector, useDispatch } from 'react-redux'; 4 | import { makeStyles } from '@mui/styles'; 5 | import { TextField, Typography, Button } from '@mui/material'; 6 | import { Dialog, DialogContent, DialogActions, DialogTitle } from 'components/dialog'; 7 | import TickMarkGreen from 'assets/icons/tickGreen.png'; 8 | import TickMarkRed from 'assets/icons/tickRed.png'; 9 | import { hideConfirmation } from 'redux/slices/confirmation-dialog'; 10 | 11 | const useStyles = makeStyles((theme) => ({})); 12 | 13 | function ConfirmationDialog(props) { 14 | const { ...rest } = props; 15 | const { open, title, content, primaryBtnText, onPrimaryBtnClick, secondaryBtnText, onSecondaryBtnClick } = useSelector( 16 | (state) => state.confirmationDialog 17 | ); 18 | const classes = useStyles(); 19 | const history = useHistory(); 20 | const dispatch = useDispatch(); 21 | 22 | const handleClose = () => { 23 | dispatch(hideConfirmation()); 24 | }; 25 | 26 | const handleSecondaryClick = () => { 27 | handleClose(); 28 | onSecondaryBtnClick(); 29 | }; 30 | 31 | const handlePrimaryClick = () => { 32 | handleClose(); 33 | onPrimaryBtnClick(); 34 | }; 35 | 36 | return ( 37 | handleClose()}> 38 | {title} 39 | 40 | {content} 41 | 42 | 43 | 46 | 47 | 48 | 49 | ); 50 | } 51 | 52 | export default ConfirmationDialog; 53 | -------------------------------------------------------------------------------- /src/components/dialog/index.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog as MyDialog, 3 | DialogActions as MyDialogActions, 4 | DialogContent as MyDialogContent, 5 | DialogTitle as MyDialogTitle, 6 | } from '@mui/material'; 7 | 8 | export const DialogActions = (props) => { 9 | const { children, ...rest } = props; 10 | return {children}; 11 | }; 12 | 13 | export const DialogContent = (props) => { 14 | const { children, ...rest } = props; 15 | return {children}; 16 | }; 17 | 18 | export const DialogTitle = (props) => { 19 | const { children, ...rest } = props; 20 | return {children}; 21 | }; 22 | 23 | export const Dialog = (props) => { 24 | const { fullWidth, maxWidth, open, handleClose, children, ...rest } = props; 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /src/components/dropdown-field/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField, Typography, Select, MenuItem } from '@mui/material'; 3 | 4 | const Dropdown = ({ 5 | label, 6 | disabled, 7 | value, 8 | onChange, 9 | required = false, 10 | options, 11 | name, 12 | fullWidth, 13 | labelStyle = {}, 14 | labelClassName, 15 | style = {}, 16 | }) => { 17 | return ( 18 |
19 | 20 | {label} 21 | {required ? * : null} 22 | 23 | 24 |
25 | 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default Dropdown; 49 | -------------------------------------------------------------------------------- /src/components/exam-banner/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import useStyles from './styles'; 3 | import { Box } from '@mui/material'; 4 | 5 | const ExamBanner = ({ data }) => { 6 | const classes = useStyles(); 7 | const [cardHover, setCardHover] = useState(false); 8 | 9 | console.log('data is : ', data); 10 | 11 | return ( 12 | <> 13 | setCardHover(true)} 15 | onMouseLeave={() => setCardHover(false)} 16 | style={{ 17 | backgroundImage: `linear-gradient(rgba(28, 40, 51,.5), rgba(28, 40, 51,.5)),url(${data.image})`, 18 | color: 'white', 19 | }} 20 | className={classes.examBanner} 21 | > 22 |
34 |
{data.name}
35 |
36 | {data.ongoing ? 'ongoing' : 'upcoming'} 37 |
38 |
39 | 40 |
49 | Registered : {data.numberOfParticipants} 50 |
51 | 52 |
56 | {data.tags.map((tag) => ( 57 |
{tag}
58 | ))} 59 |
60 |
61 | 62 | ); 63 | }; 64 | export default ExamBanner; 65 | -------------------------------------------------------------------------------- /src/components/exam-banner/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@mui/styles'; 2 | 3 | export default makeStyles((theme) => ({ 4 | heading: { 5 | fontSize: '3.2rem', 6 | textAlign: 'center', 7 | marginBottom: '2rem', 8 | }, 9 | examBanner: { 10 | display: 'flex', 11 | flexDirection: 'column', 12 | justifyContent: 'space-between', 13 | width: '100%', 14 | height: '55rem', 15 | backgroundPosition: 'center', 16 | backgroundSize: 'cover', 17 | backgroundRepeat: 'no-repeat', 18 | cursor: 'pointer', 19 | transition: '.2s ease', 20 | position: 'relative', 21 | color: 'white', 22 | '&:hover': {}, 23 | }, 24 | show: { 25 | opacity: 1, 26 | }, 27 | hide: { 28 | opacity: 0, 29 | }, 30 | ongoing: { 31 | backgroundColor: 'green', 32 | fontSize: '2rem', 33 | height: 'fit-content', 34 | marginLeft: '3rem', 35 | padding: '.5rem', 36 | borderRadius: '.2rem', 37 | }, 38 | upcoming: { 39 | backgroundColor: 'blue', 40 | fontSize: '2rem', 41 | height: 'fit-content', 42 | marginLeft: '3rem', 43 | padding: '.5rem', 44 | borderRadius: '.2rem', 45 | }, 46 | })); 47 | -------------------------------------------------------------------------------- /src/components/exam-details-card/index.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { useState } from 'react'; 3 | import { Box, Card, CardContent, Button, Typography } from '@mui/material'; 4 | import { useHistory } from 'react-router-dom'; 5 | 6 | 7 | const ExamDetailsCard = (props) => { 8 | 9 | const history = useHistory(); 10 | const { fullWidth, width, data, cardDetails, ...rest } = props; 11 | 12 | 13 | // ----------------------------------------------------- 14 | // Time Manupulation 15 | // ----------------------------------------------------- 16 | let a = new Date(cardDetails.startTime); 17 | var StartTime = a.toDateString(); 18 | var start = a.toLocaleTimeString(); 19 | 20 | var addMins = new Date(a.getTime() + cardDetails.duration * 60000); 21 | var event = new Date(addMins); 22 | var end = event.toLocaleTimeString(); 23 | 24 | 25 | const redirectToExamDetailsPage = () => { 26 | history.push(`/exam/${cardDetails.id}`); 27 | } 28 | 29 | 30 | return ( 31 | 32 | 33 | {cardDetails.name !== null ? cardDetails.name : '-'} 34 | 35 | {cardDetails.startTime !== null ? StartTime : '-'} 36 | 37 | 38 | 39 | Duration 40 | 41 | {cardDetails.duration !== null ? `${cardDetails.duration} mins` : '-'} 42 | 43 | 44 | 45 | No. of Participants 46 | 47 | {cardDetails.numberOfParticipants !== null ? cardDetails.numberOfParticipants : '-'} 48 | 49 | 50 | 51 | Status 52 | 53 | SCHEDULED 54 | 55 | 56 | 57 | Exam Timing 58 | 59 | {cardDetails.timing !== null ? `${start} - ${end}` : '-'} 60 | 61 | 62 | 63 | Exam Visibility 64 | 65 | PUBLIC 66 | 67 | 68 | 69 | 70 | 73 | 74 | 75 | 76 | ); 77 | }; 78 | 79 | 80 | export default ExamDetailsCard; 81 | -------------------------------------------------------------------------------- /src/components/exam-details-card2/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Box, Card, CardContent, Button, Typography } from '@mui/material'; 3 | import useStyles from './styles'; 4 | import AccessTimeIcon from '@mui/icons-material/AccessTime'; 5 | import { useHistory } from 'react-router-dom'; 6 | 7 | const ExamDetailsCard2 = (props) => { 8 | const history = useHistory(); 9 | const classes = useStyles(); 10 | const { fullWidth, width, data, cardDetails, ...rest } = props; 11 | 12 | return ( 13 | history.push(`/exam/${cardDetails.id}`)} 17 | > 18 | {cardDetails && ( 19 | 28 | 34 | 35 | 36 | 37 | 38 | {cardDetails.name !== null ? cardDetails.name : '-'}{' '} 39 | 40 | 41 | 49 | {cardDetails.tags.slice(0, 3).map((tag) => ( 50 | 51 | ))} 52 | 53 | 65 | 71 | 72 | {new Date(cardDetails.startTime).toDateString()} 73 | 74 | 75 | 76 | )} 77 | 78 | ); 79 | }; 80 | export default ExamDetailsCard2; 81 | 82 | const Tag = ({ tag }) => { 83 | const classes = useStyles(); 84 | return ( 85 | 86 | #{tag} 87 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /src/components/exam-details-card2/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@mui/styles'; 2 | 3 | export default makeStyles((theme) => ({ 4 | img: { 5 | width: '100%', 6 | height: '20.7rem', 7 | borderRadius: '1rem', 8 | transition: '0.3s all ease', 9 | '&:hover': { 10 | transform: 'scale(1.08)', 11 | }, 12 | }, 13 | tag: { 14 | backgroundColor: 15 | theme.palette.mode === 'dark' 16 | ? theme.palette.grey[800] 17 | : theme.palette.grey[300], 18 | margin: '.5rem', 19 | borderRadius: '.5rem', 20 | padding: '.5rem', 21 | }, 22 | })); 23 | -------------------------------------------------------------------------------- /src/components/file-upload-input/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField, Typography, Button, InputAdornment, IconButton } from '@mui/material'; 3 | import PhotoCamera from '@mui/icons-material/PhotoCamera'; 4 | import { styled } from '@mui/system'; 5 | 6 | const Input = styled('input')({ 7 | display: 'none', 8 | }); 9 | 10 | const FileUploadField = ({ 11 | label, 12 | placeholder, 13 | disabled = false, 14 | required, 15 | name, 16 | type, 17 | onChange, 18 | value, 19 | fullWidth, 20 | row = '', 21 | rowMax = '', 22 | multiline, 23 | style = {}, 24 | labelStyle = {}, 25 | labelClassName, 26 | className, 27 | variant, 28 | showActionBtn = false, 29 | actionBtnText = '', 30 | accept = 'image/*', 31 | uploadIcon = , 32 | uploadDisabled = false, 33 | actionOnClick = () => {}, 34 | }) => { 35 | return ( 36 |
37 | 38 | {label} 39 | {required ? * : null} 40 | 41 | 42 |
43 | 61 | 67 | 68 | ), 69 | }} 70 | /> 71 | {showActionBtn && ( 72 | 75 | )} 76 |
77 |
78 | ); 79 | }; 80 | 81 | export default FileUploadField; 82 | -------------------------------------------------------------------------------- /src/components/header/index.jsx: -------------------------------------------------------------------------------- 1 | import { Paper, Typography } from '@mui/material'; 2 | import { makeStyles } from '@mui/styles'; 3 | 4 | const useStyles = makeStyles((theme) => ({ 5 | root: { 6 | padding: '3rem', 7 | }, 8 | })); 9 | 10 | const Header = (props) => { 11 | const classes = useStyles(); 12 | const { elevation = 2, title, description, children, className, style, ...rest } = props; 13 | return ( 14 | 15 | {title} 16 | {description} 17 | {children} 18 | 19 | ); 20 | }; 21 | 22 | export default Header; 23 | -------------------------------------------------------------------------------- /src/components/multi-select-dropdown/index.jsx: -------------------------------------------------------------------------------- 1 | import { useTheme } from '@mui/material/styles'; 2 | import Box from '@mui/material/Box'; 3 | import Typography from '@mui/material/Typography'; 4 | import OutlinedInput from '@mui/material/OutlinedInput'; 5 | import MenuItem from '@mui/material/MenuItem'; 6 | import Select from '@mui/material/Select'; 7 | import Chip from '@mui/material/Chip'; 8 | 9 | const ITEM_HEIGHT = 48; 10 | const ITEM_PADDING_TOP = 8; 11 | 12 | const getStyles = (opt, value, theme) => { 13 | return { 14 | fontWeight: value.indexOf(opt) === -1 ? theme.typography.fontWeightRegular : theme.typography.fontWeightMedium, 15 | }; 16 | }; 17 | 18 | const MultiSelect = (props) => { 19 | const { name, value, onChange, options = [], label, placeholder = '', required, style, disabled, labelStyle, labelClassName } = props; 20 | const theme = useTheme(); 21 | 22 | return ( 23 | <> 24 | 25 | {label} 26 | {required ? * : null} 27 | 28 |
29 | 52 | 53 | ); 54 | }; 55 | 56 | export default MultiSelect; 57 | -------------------------------------------------------------------------------- /src/components/navbar/index.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from 'react-router-dom'; 2 | import { styled, alpha } from '@mui/material/styles'; 3 | import { useTheme } from '@mui/styles'; 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import { clearUserAndToken } from 'redux/slices/auth'; 6 | import { enableVisibility, dialogNames } from 'redux/slices/dialog-visibility'; 7 | import AppBar from '@mui/material/AppBar'; 8 | import Box from '@mui/material/Box'; 9 | import Button from '@mui/material/Button'; 10 | import Container from '@mui/material/Container'; 11 | import Toolbar from '@mui/material/Toolbar'; 12 | import IconButton from '@mui/material/IconButton'; 13 | import Typography from '@mui/material/Typography'; 14 | import InputBase from '@mui/material/InputBase'; 15 | import SearchIcon from '@mui/icons-material/Search'; 16 | import DarkModeIcon from '@mui/icons-material/DarkMode'; 17 | import LightModeIcon from '@mui/icons-material/LightMode'; 18 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 19 | 20 | const Search = styled('div')(({ theme }) => ({ 21 | position: 'relative', 22 | borderRadius: theme.shape.borderRadius, 23 | backgroundColor: alpha(theme.palette.common.white, 0.15), 24 | '&:hover': { 25 | backgroundColor: alpha(theme.palette.common.white, 0.25), 26 | }, 27 | marginRight: theme.spacing(2), 28 | marginLeft: 0, 29 | width: '100%', 30 | [theme.breakpoints.up('sm')]: { 31 | marginLeft: theme.spacing(3), 32 | width: 'auto', 33 | }, 34 | })); 35 | 36 | const SearchIconWrapper = styled('div')(({ theme }) => ({ 37 | padding: theme.spacing(0, 2), 38 | height: '100%', 39 | position: 'absolute', 40 | pointerEvents: 'none', 41 | display: 'flex', 42 | alignItems: 'center', 43 | justifyContent: 'center', 44 | })); 45 | 46 | const StyledInputBase = styled(InputBase)(({ theme }) => ({ 47 | color: 'inherit', 48 | '& .MuiInputBase-input': { 49 | padding: theme.spacing(1, 1, 1, 0), 50 | // vertical padding + font size from searchIcon 51 | paddingLeft: `calc(1em + ${theme.spacing(4)})`, 52 | transition: theme.transitions.create('width'), 53 | width: '100%', 54 | [theme.breakpoints.up('md')]: { 55 | width: '20ch', 56 | }, 57 | }, 58 | })); 59 | 60 | export default function Navbar(props) { 61 | 62 | const { toggleTheme } = props; 63 | const theme = useTheme(); 64 | const dispatch = useDispatch(); 65 | const { user } = useSelector((state) => state.auth); 66 | 67 | const logout = () => { 68 | dispatch(clearUserAndToken()); 69 | }; 70 | 71 | return ( 72 | <> 73 | 74 | 75 | 76 | 83 | Quizzio 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | {!user && ( 93 | <> 94 | 102 | 110 | 111 | )} 112 | {!!user && ( 113 | <> 114 | 117 | 118 | 119 | 120 | 123 | 124 | )} 125 | toggleTheme()} sx={{ ml: 1 }}> 126 | {theme.palette.mode === 'dark' ? : } 127 | 128 | 129 | 130 | 131 | 132 | 133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /src/components/speed-dial/index.jsx: -------------------------------------------------------------------------------- 1 | import MUISpeedDial from '@mui/material/SpeedDial'; 2 | import SpeedDialIcon from '@mui/material/SpeedDialIcon'; 3 | import SpeedDialAction from '@mui/material/SpeedDialAction'; 4 | 5 | const SpeedDial = (props) => { 6 | const { 7 | style = { position: 'absolute', bottom: 16, right: 16 }, 8 | icon = , 9 | actions = [], 10 | onOpen = undefined, 11 | onClose = undefined, 12 | open = undefined, 13 | closeOnActionClick = false, 14 | } = props; 15 | return ( 16 | 24 | {actions.map((action, idx) => ( 25 | { 27 | action.onClick(); 28 | if (closeOnActionClick) onClose(); 29 | }} 30 | key={idx} 31 | icon={action.icon} 32 | tooltipTitle={action.name} 33 | /> 34 | ))} 35 | 36 | ); 37 | }; 38 | 39 | export default SpeedDial; 40 | -------------------------------------------------------------------------------- /src/components/text-editor/index.jsx: -------------------------------------------------------------------------------- 1 | import { Editor } from '@tinymce/tinymce-react'; 2 | import tinymce from 'tinymce/tinymce'; 3 | import { useTheme } from '@mui/material'; 4 | import { uploadImages } from 'api/utils'; 5 | import { useSnackbar } from 'notistack'; 6 | // Theme 7 | import 'tinymce/themes/silver'; 8 | import 'tinymce/themes/mobile'; 9 | import 'tinymce/icons/default'; 10 | // import 'tinymce/skins/ui/oxide-dark/skin.min.css'; 11 | import 'tinymce/skins/ui/oxide/skin.min.css'; 12 | // importing the plugin js. 13 | import 'tinymce/plugins/advlist'; 14 | import 'tinymce/plugins/autolink'; 15 | import 'tinymce/plugins/link'; 16 | import 'tinymce/plugins/image'; 17 | import 'tinymce/plugins/imagetools'; 18 | import 'tinymce/plugins/lists'; 19 | import 'tinymce/plugins/charmap'; 20 | import 'tinymce/plugins/hr'; 21 | import 'tinymce/plugins/anchor'; 22 | import 'tinymce/plugins/searchreplace'; 23 | import 'tinymce/plugins/wordcount'; 24 | import 'tinymce/plugins/code'; 25 | import 'tinymce/plugins/fullscreen'; 26 | import 'tinymce/plugins/insertdatetime'; 27 | import 'tinymce/plugins/media'; 28 | import 'tinymce/plugins/nonbreaking'; 29 | import 'tinymce/plugins/table'; 30 | import 'tinymce/plugins/help'; 31 | import 'tinymce/plugins/template'; 32 | import 'tinymce/plugins/print'; 33 | import 'tinymce/plugins/preview'; 34 | import 'tinymce/plugins/paste'; 35 | import 'tinymce/plugins/importcss'; 36 | import 'tinymce/plugins/autosave'; 37 | import 'tinymce/plugins/directionality'; 38 | import 'tinymce/plugins/visualblocks'; 39 | import 'tinymce/plugins/visualchars'; 40 | import 'tinymce/plugins/codesample'; 41 | import 'tinymce/plugins/pagebreak'; 42 | import 'tinymce/plugins/toc'; 43 | import 'tinymce/plugins/textpattern'; 44 | import 'tinymce/plugins/noneditable'; 45 | import 'tinymce/plugins/noneditable'; 46 | import 'tinymce/plugins/quickbars'; 47 | import 'tinymce/plugins/emoticons'; 48 | 49 | const plugins = 50 | 'print preview paste importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists wordcount imagetools textpattern noneditable help quickbars emoticons'; 51 | const menubar = 'file edit view insert format tools table help'; 52 | const toolbar = 53 | 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | pagebreak | charmap emoticons | fullscreen preview save print | insertfile image media template link anchor codesample | ltr rtl'; 54 | 55 | const TinyEditor = (props) => { 56 | const theme = useTheme(); 57 | const { enqueueSnackbar } = useSnackbar(); 58 | const { width = 700, height = 350, value, onChange } = props; 59 | const handleEditorChange = (e) => { 60 | console.log('Content was updated:', e.target.getContent()); 61 | }; 62 | 63 | const filePickerHandler = (cb, value, meta) => { 64 | console.log('File Picker!'); 65 | let input = document.createElement('input'); 66 | input.setAttribute('type', 'file'); 67 | // input.setAttribute('accept', 'image/*'); 68 | input.onchange = async function () { 69 | // let file = this.files[0]; 70 | // let reader = new FileReader(); 71 | // reader.onload = function () { 72 | // let id = 'blobid' + new Date().getTime(); 73 | // let blobCache = tinymce.activeEditor.editorUpload.blobCache; 74 | // let base64 = reader.result.split(',')[1]; 75 | // let blobInfo = blobCache.create(id, file, base64); 76 | // blobCache.add(blobInfo); 77 | // cb(blobInfo.blobUri(), { title: file.name }); 78 | // }; 79 | // reader.readAsDataURL(file); 80 | try { 81 | const file = this.files[0]; 82 | const formData = new FormData(); 83 | formData.append('image', file); 84 | const res = await uploadImages(formData); 85 | const { url, public_id } = res.data.data.urls[0]; 86 | cb(url, { title: public_id }); 87 | } catch (err) { 88 | enqueueSnackbar(err.message, { variant: 'error' }); 89 | } 90 | }; 91 | input.click(); 92 | }; 93 | 94 | const editorOptions = { 95 | selector: 'textarea', 96 | skin: theme.palette.mode === 'dark' ? 'oxide-dark' : 'oxide', 97 | content_css: theme.palette.mode === 'dark' ? 'dark' : 'default', 98 | // content_style: [contentCss, contentUiCss].join('\n'), 99 | width, 100 | height, 101 | menubar, 102 | plugins, 103 | toolbar, 104 | toolbar_sticky: true, 105 | autosave_ask_before_unload: true, 106 | autosave_interval: '30s', 107 | autosave_prefix: '{path}{query}-{id}-', 108 | autosave_restore_when_empty: false, 109 | autosave_retention: '2m', 110 | image_advtab: true, 111 | image_title: true, 112 | automatic_uploads: true, 113 | file_picker_types: 'file image media', 114 | file_picker_callback: filePickerHandler, 115 | link_list: [ 116 | { title: 'My page 1', value: 'https://www.tiny.cloud' }, 117 | { title: 'My page 2', value: 'http://www.moxiecode.com' }, 118 | ], 119 | image_list: [ 120 | { title: 'My page 1', value: 'https://www.tiny.cloud' }, 121 | { title: 'My page 2', value: 'http://www.moxiecode.com' }, 122 | ], 123 | image_class_list: [ 124 | { title: 'None', value: '' }, 125 | { title: 'Some class', value: 'class-name' }, 126 | ], 127 | importcss_append: true, 128 | templates: [ 129 | { 130 | title: 'New Table', 131 | description: 'creates a new table', 132 | content: 133 | '
', 134 | }, 135 | { 136 | title: 'Starting my story', 137 | description: 'A cure for writers block', 138 | content: 'Once upon a time...', 139 | }, 140 | { 141 | title: 'New list with dates', 142 | description: 'New List with dates', 143 | content: 144 | '
cdate
mdate

My List

', 145 | }, 146 | ], 147 | template_cdate_format: '[Date Created (CDATE): %m/%d/%Y : %H:%M:%S]', 148 | template_mdate_format: '[Date Modified (MDATE): %m/%d/%Y : %H:%M:%S]', 149 | image_caption: true, 150 | quickbars_selection_toolbar: 'bold italic | quicklink h2 h3 blockquote quickimage quicktable', 151 | noneditable_noneditable_class: 'mceNonEditable', 152 | toolbar_mode: 'sliding', 153 | contextmenu: 'link image imagetools table', 154 | }; 155 | 156 | return ( 157 | onChange(value, editorRef)} 163 | /> 164 | ); 165 | }; 166 | 167 | export default TinyEditor; 168 | -------------------------------------------------------------------------------- /src/components/text-input-field/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextField, Typography, Button, InputAdornment, IconButton } from '@mui/material'; 3 | import Visibility from '@mui/icons-material/Visibility'; 4 | 5 | const TextInputField = ({ 6 | label, 7 | placeholder, 8 | disabled = false, 9 | required, 10 | name, 11 | type, 12 | color, 13 | onChange, 14 | value, 15 | fullWidth, 16 | row = '', 17 | rowMax = '', 18 | multiline, 19 | style = {}, 20 | labelStyle = {}, 21 | labelClassName, 22 | className, 23 | variant, 24 | showActionBtn = false, 25 | actionBtnText = '', 26 | actionOnClick = () => {}, 27 | endIcon, 28 | startIcon, 29 | endIconOnClick = () => {}, 30 | startIconOnClick = () => {}, 31 | InputPropsStyle = {}, 32 | }) => { 33 | return ( 34 |
35 | 36 | {label} 37 | {required ? * : null} 38 | 39 | 40 |
41 | 62 | startIconOnClick(e)}> 63 | {startIcon} 64 | 65 | 66 | ), 67 | endAdornment: endIcon && ( 68 | 69 | endIconOnClick(e)}> 70 | {endIcon} 71 | 72 | 73 | ), 74 | }} 75 | /> 76 | 77 | {/* Password */} 78 | {showActionBtn && ( 79 | 82 | )} 83 |
84 |
85 | ); 86 | }; 87 | 88 | export default TextInputField; 89 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap'); 2 | 3 | html { 4 | font-size: 62.5%; 5 | /* We are setting the html font size to 10px i.e. 62.5% of 16px (default) */ 6 | /* So 1rem = 10px */ 7 | } 8 | 9 | *::-webkit-scrollbar { 10 | width: 5px; 11 | } 12 | 13 | *::-webkit-scrollbar-track { 14 | border-radius: 10px; 15 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1); 16 | } 17 | 18 | *::-webkit-scrollbar-thumb { 19 | border-radius: 10px; 20 | background-color: rgb(211, 211, 211); 21 | } 22 | 23 | 24 | .basicStyle { 25 | margin: 2rem; 26 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { BrowserRouter } from 'react-router-dom'; 4 | import store from 'redux/store'; 5 | import { Provider } from 'react-redux'; 6 | import './index.css'; 7 | import App from './app'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ); 17 | -------------------------------------------------------------------------------- /src/layouts/a.css: -------------------------------------------------------------------------------- 1 | .css-r97pbf-MuiPaper-root-MuiDialog-paper { 2 | max-width: 580px !important; 3 | } -------------------------------------------------------------------------------- /src/layouts/login.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { Button } from '@mui/material'; 4 | import { DialogActions, DialogContent, DialogTitle, Dialog } from 'components/dialog'; 5 | import { dialogNames, hideVisibility } from 'redux/slices/dialog-visibility'; 6 | import { setUserAndToken } from 'redux/slices/auth'; 7 | import TextInputField from 'components/text-input-field'; 8 | import { loginWithEmailAndPassword } from 'api/user'; 9 | import { useSnackbar } from 'notistack'; 10 | import './a.css'; 11 | 12 | const Login = () => { 13 | const dispatch = useDispatch(); 14 | const { enqueueSnackbar } = useSnackbar(); 15 | const { [dialogNames.login]: loginVisibility } = useSelector((state) => state.dialogVisibility); 16 | const [email, setEmail] = useState(''); 17 | const [password, setPassword] = useState(''); 18 | 19 | const handleClose = () => { 20 | dispatch(hideVisibility(dialogNames.login)); 21 | }; 22 | 23 | const handleLogin = async () => { 24 | try { 25 | const res = await loginWithEmailAndPassword(email, password); 26 | console.log(res); 27 | dispatch(setUserAndToken({ user: res.data.data.user, token: res.data.data.token })); 28 | handleClose(); 29 | } catch (err) { 30 | enqueueSnackbar(err.message, { variant: 'error' }); 31 | } 32 | }; 33 | 34 | return ( 35 | handleClose()}> 36 | Welcome Back! 37 | 38 | 39 | setEmail(e.target.value)} 45 | style={{ marginBottom: '1rem' }} 46 | /> 47 | 48 | setPassword(e.target.value)} 55 | /> 56 | 57 | 58 | 61 | 62 | 63 | 64 | ); 65 | }; 66 | 67 | export default Login; 68 | -------------------------------------------------------------------------------- /src/layouts/questionType-result/fill-blanks.jsx: -------------------------------------------------------------------------------- 1 | import TextInputField from 'components/text-input-field'; 2 | import { Typography, Alert, AlertTitle } from '@mui/material/'; 3 | import { useTheme, makeStyles } from '@mui/styles'; 4 | 5 | const useStyles = makeStyles((theme) => ({ 6 | incorrectText: { 7 | textFillColor: 'crimson !important', 8 | fontWeight: 500, 9 | color: 'crimson', 10 | }, 11 | })); 12 | 13 | const FillBlanks = (props) => { 14 | const { questionId, userAnswer = '', correctAnswer } = props; 15 | 16 | const theme = useTheme(); 17 | const classes = useStyles(); 18 | 19 | const getInputTextStyle = (isCorrect) => { 20 | const style = { fontWeight: 500 }; 21 | if (isCorrect) style.color = theme.palette.success.main; 22 | else style.color = theme.palette.error.main; 23 | return style; 24 | }; 25 | 26 | return ( 27 | <> 28 | 35 | {userAnswer ? 'Your Answer' : 'Answer'} 36 | 37 | 38 | {userAnswer && ( 39 | 46 | )} 47 | 48 | {correctAnswer && correctAnswer.toLowerCase() !== userAnswer.toLowerCase() && ( 49 | 50 | Correct Answer : {correctAnswer} 51 | 52 | )} 53 | 54 | ); 55 | }; 56 | // ffccd5 57 | // ffb3c1 58 | export default FillBlanks; 59 | -------------------------------------------------------------------------------- /src/layouts/questionType-result/mcq-multiple.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { FormGroup, FormControlLabel, Checkbox, Typography } from '@mui/material'; 3 | import { pink } from '@mui/material/colors'; 4 | import { useTheme } from '@mui/styles'; 5 | 6 | const MCQMultiple = (props) => { 7 | const { questionId, options, userAnswer = '', correctAnswer } = props; 8 | const theme = useTheme(); 9 | 10 | // ------------------------------------------------------- 11 | // LOGIC FOR THE COLOR OF THE CORRECT AND WRONG 12 | // -------------------------------------------------------- 13 | const getBackgroundColor = (optionId) => { 14 | if (correctAnswer.includes(optionId)) return 'rgb(237, 247, 237)'; 15 | 16 | if (userAnswer.includes(optionId)) return '#ffccd5'; 17 | 18 | return theme.palette.background.default; 19 | }; 20 | 21 | const getRedTick = (optionId) => { 22 | if (userAnswer.includes(optionId) && !correctAnswer.includes(optionId)) 23 | return { 24 | color: pink[800], 25 | '&.Mui-checked': { color: pink[600] }, // Red CheckBox Tick 26 | }; 27 | }; 28 | 29 | return ( 30 | <> 31 | 32 | {userAnswer ? 'Your Answer' : 'Answer'} 33 | 34 | 35 | 36 | {options.map((o) => ( 37 | } 39 | label={o.data} 40 | style={{ backgroundColor: getBackgroundColor(o.id), width: '50%', margin: '0.4rem 0', borderRadius: theme.shape.borderRadius }} 41 | /> 42 | ))} 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default MCQMultiple; 49 | -------------------------------------------------------------------------------- /src/layouts/questionType-result/mcq-single.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Radio, RadioGroup, FormControlLabel, FormControl, Typography } from '@mui/material'; 3 | import { useTheme } from '@mui/styles'; 4 | 5 | const MCQSingle = (props) => { 6 | const { questionId, options, userAnswer = '', correctAnswer } = props; 7 | const theme = useTheme(); 8 | 9 | const getBackgroundColor = (optionId) => { 10 | if (optionId === correctAnswer[0]) return 'rgb(237, 247, 237)'; 11 | if (optionId === userAnswer[0]) return '#ffccd5'; 12 | return theme.palette.background.default; 13 | }; 14 | 15 | return ( 16 | <> 17 | 18 | {userAnswer ? 'Your Answer' : 'Answer'} 19 | 20 | 21 | 22 | 23 | {options.map((o) => ( 24 | } 27 | label={o.data} 28 | style={{ backgroundColor: getBackgroundColor(o.id), margin: '0.4rem 0', borderRadius: theme.shape.borderRadius }} 29 | /> 30 | ))} 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export default MCQSingle; 38 | -------------------------------------------------------------------------------- /src/layouts/register.jsx: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import { useState } from 'react'; 3 | import { DialogActions, DialogContent, DialogTitle, Dialog } from 'components/dialog'; 4 | import { dialogNames, hideVisibility, enableVisibility } from 'redux/slices/dialog-visibility'; 5 | import { Grid, Button } from '@mui/material'; 6 | import { makeStyles, useTheme } from '@mui/styles'; 7 | import TextInputField from 'components/text-input-field'; 8 | import DatePicker from 'components/date-time-picker/date'; 9 | import DropdownField from 'components/dropdown-field'; 10 | import Visibility from '@mui/icons-material/Visibility'; 11 | import VisibilityOff from '@mui/icons-material/VisibilityOff'; 12 | import { useSnackbar } from 'notistack'; 13 | import './a.css'; 14 | 15 | import { registerNewUser } from 'api/user'; 16 | import { setUserAndToken } from 'redux/slices/auth'; 17 | 18 | const genders = [ 19 | { label: 'Male', value: 'male' }, 20 | { label: 'Female', value: 'female' }, 21 | { label: 'Other', value: 'other' }, 22 | ]; 23 | 24 | const useStyles = makeStyles((theme) => ({ 25 | MaxWidth: { 26 | maxWidth: '600px !important', 27 | }, 28 | })); 29 | 30 | const Register = () => { 31 | const classes = useStyles(); 32 | const { enqueueSnackbar } = useSnackbar(); 33 | const [showPassword, setShowPassword] = useState(false); 34 | const [newUserData, setNewUserData] = useState({ 35 | name: '', 36 | email: '', 37 | bio: '', 38 | dob: '', 39 | address: '', 40 | image: '', 41 | institution: '', 42 | password: '', 43 | gender: genders[0].value, 44 | phoneNumber: '', 45 | }); 46 | const dispatch = useDispatch(); 47 | 48 | const { [dialogNames.register]: registerVisibility } = useSelector((state) => state.dialogVisibility); 49 | 50 | const handleClose = () => { 51 | dispatch(hideVisibility(dialogNames.register)); 52 | }; 53 | 54 | const handleDataChange = (key, value) => { 55 | setNewUserData((u) => ({ ...u, [key]: value })); 56 | }; 57 | 58 | const handleSubmit = async () => { 59 | try { 60 | const body = JSON.parse(JSON.stringify(newUserData)); 61 | body.dob = '2018-03-29T13:34:00.000'; 62 | const res = await registerNewUser(body); 63 | console.log(res); 64 | dispatch(setUserAndToken({ user: res.data.data.user, token: res.data.data.token })); 65 | handleClose(); 66 | } catch (err) { 67 | enqueueSnackbar(err.message, { variant: 'error' }); 68 | } 69 | }; 70 | 71 | return ( 72 | 73 | Register 74 | 75 | 76 | 77 | 78 | handleDataChange(e.target.name, e.target.value)} 86 | /> 87 | 88 | 89 | handleDataChange(e.target.name, e.target.value)} 97 | /> 98 | 99 | 100 | : } 108 | endIconOnClick={() => setShowPassword(!showPassword)} 109 | value={newUserData.password} 110 | onChange={(e) => handleDataChange(e.target.name, e.target.value)} 111 | /> 112 | 113 | 114 | handleDataChange(e.target.name, e.target.value)} 122 | /> 123 | 124 | 125 | handleDataChange('dob', newDate)} 132 | /> 133 | 134 | 135 | handleDataChange(e.target.name, e.target.value)} 143 | /> 144 | 145 | 146 | handleDataChange(e.target.name, e.target.value)} 154 | /> 155 | 156 | 157 | handleDataChange(e.target.name, e.target.value)} 165 | /> 166 | 167 | 168 | handleDataChange(e.target.name, e.target.value)} 177 | /> 178 | 179 | 180 | 181 | 182 | 185 | 186 | 187 | 188 | ); 189 | }; 190 | 191 | export default Register; 192 | -------------------------------------------------------------------------------- /src/layouts/request-full-screen.jsx: -------------------------------------------------------------------------------- 1 | 2 | import { Typography, Button } from '@mui/material'; 3 | import { useTheme } from '@mui/styles'; 4 | 5 | const RequestFullScreen = (props) => { 6 | 7 | const theme = useTheme(); 8 | const { EnterFullScreen = () => { } } = props; 9 | 10 | return ( 11 |
14 | 15 | Enter Full Screen to continue 16 | 17 | 20 |
21 | ); 22 | }; 23 | 24 | export default RequestFullScreen; 25 | -------------------------------------------------------------------------------- /src/pages/exam-creation/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useHistory } from 'react-router-dom'; 3 | import { Container, Stepper, Step, StepButton } from '@mui/material'; 4 | import Page1 from './page1'; 5 | import Page2 from './page2'; 6 | import Page3 from './page3'; 7 | import { createExam } from 'api/exam'; 8 | import { uploadImages } from 'api/utils'; 9 | import { useSnackbar } from 'notistack'; 10 | // import { makeStyles, useTheme } from '@mui/styles'; 11 | 12 | const steps = ['Exam Details', 'Questions', 'Preview']; 13 | 14 | const combineDateAndTime = (date, time) => { 15 | date = new Date(date); 16 | time = new Date(time); 17 | let timeString = time.getHours() + ':' + time.getMinutes() + ':00'; 18 | let year = date.getFullYear(); 19 | let month = date.getMonth() + 1; // Jan is 0, dec is 11 20 | let day = date.getDate(); 21 | let dateString = '' + year + '-' + month + '-' + day; 22 | let combined = new Date(dateString + ' ' + timeString); 23 | return combined; 24 | }; 25 | 26 | const ExamCreation = () => { 27 | // const classes = useStyles(); 28 | const history = useHistory(); 29 | const { enqueueSnackbar } = useSnackbar(); 30 | const [activeStep, setActiveStep] = useState(0); 31 | const [examDetails, setExamDetails] = useState({ 32 | name: '', 33 | description: '', 34 | isPrivate: false, 35 | startTime: null, 36 | endTime: null, 37 | image: '', 38 | type: '', 39 | allowedUsers: [], 40 | tags: [], 41 | }); 42 | const [questions, setQuestions] = useState([]); 43 | 44 | const handleStep = (step) => () => { 45 | setActiveStep(step); 46 | }; 47 | 48 | const handleDetailsChange = (name, value) => { 49 | setExamDetails((d) => ({ ...d, [name]: value })); 50 | }; 51 | 52 | const handleQuestionsChange = (q) => { 53 | if (questions.find((qs) => qs.id === q.id)) { 54 | setQuestions((x) => x.map((qs) => (qs.id === q.id ? q : qs))); 55 | } else { 56 | setQuestions((qs) => [...qs, q]); 57 | } 58 | }; 59 | 60 | const handleSubmitExamData = async () => { 61 | try { 62 | const examData = JSON.parse(JSON.stringify(examDetails)); 63 | 64 | examData.questions = questions; 65 | examData.duration = +new Date(examData.endTime) - +new Date(examData.startTime); 66 | examData.startTime = +new Date(examData.startTime); 67 | delete examData.endTime; 68 | 69 | examData.image = await handleUploadBannerImage(); 70 | console.log(examData.image); 71 | 72 | // Final Creation of Question Set with 73 | const res = await createExam(examData); 74 | console.log('Exam Data : ', examData, ' Response : ', res); 75 | // history.push('/'); 76 | } catch (err) { 77 | enqueueSnackbar(err.message, { variant: 'error' }); 78 | } 79 | }; 80 | 81 | const handleUploadBannerImage = async () => { 82 | try { 83 | const formData = new FormData(); 84 | formData.append('image', examDetails.image); 85 | const res = await uploadImages(formData); 86 | console.log(res); 87 | return res.data.data.urls[0].url; 88 | } catch (err) { 89 | // enqueueSnackbar(err.message, { variant: 'error' }); 90 | console.error(err); 91 | return ' '; 92 | } 93 | }; 94 | 95 | return ( 96 | 97 | 98 | {steps.map((label, index) => ( 99 | 100 | 101 | {label} 102 | 103 | 104 | ))} 105 | 106 | 107 | {activeStep === 0 && } 108 | {activeStep === 1 && } 109 | {activeStep === 2 && } 110 | 111 | ); 112 | }; 113 | 114 | export default ExamCreation; 115 | -------------------------------------------------------------------------------- /src/pages/exam-creation/page1.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Grid, Typography } from '@mui/material'; 3 | import TextInputField from 'components/text-input-field'; 4 | import DateTimePicker from 'components/date-time-picker/date-time'; 5 | import MultiSelect from 'components/multi-select-dropdown'; 6 | import DropdownField from 'components/dropdown-field'; 7 | import FileUploadInput from 'components/file-upload-input'; 8 | import TextEditor from 'components/text-editor'; 9 | import UploadFileIcon from '@mui/icons-material/UploadFile'; 10 | import { getExamTags, getExamTypes } from 'api/exam'; 11 | import { csvToJSON } from 'utilities/functions'; 12 | 13 | const Page1 = (props) => { 14 | 15 | const { examDetails, handleDetailsChange } = props; 16 | const [selectedTags, setSelectedTags] = useState([]); 17 | const [tags, setTags] = useState([]); 18 | const [examTypes, setExamTypes] = useState([]); 19 | const [examType, setExamType] = useState('public'); 20 | 21 | useEffect(() => { 22 | fetchTags(); 23 | fetchExamTypes(); 24 | }, []); 25 | 26 | const fetchTags = async () => { 27 | const res = await getExamTags(); 28 | setTags(res.data.data); 29 | }; 30 | 31 | const fetchExamTypes = async () => { 32 | const res = await getExamTypes(); 33 | setExamTypes(res.data); 34 | if (res.data.length > 0) setExamType(res.data[0].value); 35 | }; 36 | 37 | const handleBannerUpload = (event) => { 38 | const file = event.target.files[0]; 39 | console.log(file); 40 | handleDetailsChange('image', file); 41 | }; 42 | 43 | const handleAllowedUsersUpload = (event) => { 44 | const file = event.target.files[0]; 45 | console.log(file); 46 | const reader = new FileReader(); 47 | reader.onload = (e) => { 48 | const str = reader.result; 49 | const json = csvToJSON(str); 50 | const emails = json.map((x) => x.email); 51 | console.log(emails); 52 | handleDetailsChange('allowedUsers', emails); 53 | reader.abort(); 54 | }; 55 | reader.readAsText(file); 56 | }; 57 | 58 | return ( 59 | <> 60 | 61 | 62 | 63 | 64 | handleDetailsChange(e.target.name, e.target.value)} 72 | /> 73 | 74 | 75 | handleBannerUpload(e)} 82 | /> 83 | 84 | 85 | handleDetailsChange('startTime', newTime)} 92 | /> 93 | 94 | 95 | handleDetailsChange('endTime', newTime)} 102 | /> 103 | 104 | 105 | setExamType(e.target.value)} 111 | name='isPrivate' 112 | value={examDetails.isPrivate ? 'private' : 'public'} 113 | onChange={(e) => handleDetailsChange(e.target.name, e.target.value === 'private')} 114 | /> 115 | 116 | 117 | } 124 | name='allowedUsers' 125 | value={examDetails.allowedUsers} 126 | onChange={(e) => handleAllowedUsersUpload(e)} 127 | /> 128 | 129 | 130 | handleDetailsChange(e.target.name, e.target.value)} 137 | /> 138 | 139 | 140 | 141 | 142 | 143 | 144 | Description 145 | 146 | handleDetailsChange('description', value)} 151 | /> 152 | 153 | 154 | 155 | ); 156 | }; 157 | 158 | export default Page1; 159 | -------------------------------------------------------------------------------- /src/pages/exam-creation/page2.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useHistory, useParams } from 'react-router-dom'; 3 | import { makeStyles, useTheme } from '@mui/styles'; 4 | import { Grid, Button } from '@mui/material'; 5 | import TextEditor from 'components/text-editor'; 6 | import TextInputField from 'components/text-input-field'; 7 | import DropdownField from 'components/dropdown-field'; 8 | import FileCopyIcon from '@mui/icons-material/FileCopyOutlined'; 9 | import SaveIcon from '@mui/icons-material/Save'; 10 | import PrintIcon from '@mui/icons-material/Print'; 11 | import ShareIcon from '@mui/icons-material/Share'; 12 | import AddIcon from '@mui/icons-material/Add'; 13 | import { getQuestionTypes } from 'api/exam'; 14 | import MCQ from './question-types/mcq'; 15 | import FillBlanks from './question-types/fill-blanks'; 16 | 17 | const actions = [ 18 | { 19 | icon: , 20 | name: 'Copy', 21 | onClick: () => { 22 | alert(1); 23 | }, 24 | }, 25 | { icon: , name: 'Save', onClick: () => {} }, 26 | { icon: , name: 'Print', onClick: () => {} }, 27 | { icon: , name: 'Share', onClick: () => {} }, 28 | ]; 29 | 30 | const useStyles = makeStyles((theme) => ({ 31 | heading: { 32 | fontSize: '3.2rem', 33 | textAlign: 'center', 34 | marginBottom: '2rem', 35 | }, 36 | bubble: { 37 | display: 'flex', 38 | justifyContent: 'center', 39 | alignItems: 'center', 40 | width: 50, 41 | minWidth: 0, 42 | height: 50, 43 | margin: '1rem', 44 | borderRadius: '50%', 45 | background: theme.palette.mode === 'dark' ? theme.palette.grey[600] : theme.palette.grey[400], 46 | color: theme.palette.common.white, 47 | }, 48 | })); 49 | 50 | const Page2 = (props) => { 51 | const { questions, handleQuestionsChange } = props; 52 | 53 | const classes = useStyles(); 54 | const theme = useTheme(); 55 | 56 | console.log(questions); 57 | const [questionTypes, setQuestionTypes] = useState([]); 58 | const [currentQuestion, setCurrentQuestion] = useState(null); 59 | 60 | useEffect(() => { 61 | fetchQuestionTypes(); 62 | if (questions.length === 0) { 63 | addNewQuestion(); 64 | } else { 65 | setCurrentQuestion(questions[0]); 66 | } 67 | }, []); 68 | 69 | useEffect(() => { 70 | if (currentQuestion && !isNaN(currentQuestion.id)) { 71 | handleQuestionsChange(currentQuestion); 72 | } 73 | }, [currentQuestion]); 74 | 75 | const fetchQuestionTypes = async () => { 76 | const res = await getQuestionTypes(); 77 | setQuestionTypes(res.data.data); 78 | }; 79 | 80 | const addNewQuestion = () => { 81 | setCurrentQuestion({ 82 | id: questions.length, 83 | type: 'mcq', 84 | question: '', 85 | options: [], 86 | correctOption: [], 87 | marks: 2, 88 | negMarks: 1, 89 | }); 90 | }; 91 | 92 | return ( 93 | 94 | {/* EDITOR SECTION */} 95 | {currentQuestion && ( 96 | 97 | 98 | 99 | setCurrentQuestion((q) => ({ ...q, type: e.target.value }))} 107 | /> 108 | 109 | 110 | 111 | 119 | setCurrentQuestion((q) => ({ 120 | ...q, 121 | marks: +e.target.value < 0 ? 0 : +e.target.value, 122 | })) 123 | } 124 | /> 125 | 126 | 127 | 128 | 136 | setCurrentQuestion((q) => ({ 137 | ...q, 138 | negMarks: +e.target.value < 0 ? 0 : +e.target.value, 139 | })) 140 | } 141 | /> 142 | 143 | 144 | 145 | setCurrentQuestion((q) => ({ ...q, question: value }))} /> 146 | 147 | )} 148 | 149 | {/* OPTION SECTION */} 150 | {currentQuestion && ( 151 | 152 | {(currentQuestion.type === 'mcq' || currentQuestion.type === 'multipleOptions') && ( 153 | 154 | )} 155 | 156 | {currentQuestion.type === 'fillInTheBlanks' && } 157 | 158 | )} 159 | 160 | {/* BUTTONS -- FOR QUESTION NUMBER */} 161 | 162 |
163 | {questions.map((ques, i) => ( 164 | 173 | ))} 174 | 175 | 178 |
179 |
180 |
181 | ); 182 | }; 183 | 184 | export default Page2; 185 | -------------------------------------------------------------------------------- /src/pages/exam-creation/page3.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useDispatch } from 'react-redux'; 2 | import { makeStyles, useTheme } from '@mui/styles'; 3 | import { Typography, Button, Paper, Grid } from '@mui/material'; 4 | import { showConfirmation } from 'redux/slices/confirmation-dialog'; 5 | import MCQSingle from 'layouts/questionType-result/mcq-single'; 6 | import MCQMultiple from 'layouts/questionType-result/mcq-multiple'; 7 | import FillBlanks from 'layouts/questionType-result/fill-blanks'; 8 | import DomPurify from 'dompurify'; 9 | import '../../index.css'; 10 | import commonStyle from '../../common-style.js'; 11 | 12 | const useStyles = makeStyles((theme) => ({})); 13 | 14 | const Page3 = (props) => { 15 | const { questions, handleSubmitExamData } = props; 16 | 17 | const dispatch = useDispatch(); 18 | const classes = useStyles(); 19 | const theme = useTheme(); 20 | const classes2 = commonStyle(); 21 | 22 | const handleSubmit = () => { 23 | dispatch( 24 | showConfirmation({ 25 | title: 'Create Exam', 26 | content: 'Are you sure you want to create this exam?', 27 | primaryBtnText: 'Yes', 28 | secondaryBtnText: 'No', 29 | onPrimaryBtnClick: handleSubmitExamData, 30 | // onSecondaryBtnClick: () => console.log('do nothing'), 31 | }) 32 | ); 33 | }; 34 | 35 | console.log('All_Questions ==> ', questions); 36 | 37 | return ( 38 |
39 | PREVIEW 40 | 41 | {questions && questions.length > 0 && ( 42 |
    43 | {questions.map((eachQues, idx) => { 44 | return ( 45 |
  1. 46 | {/* ==================== MAIN QUESTION BODY ==================== */} 47 |
    48 |
    54 | 55 | {`Marks: ${eachQues.marks}`} 56 | {`Negative Marks: ${eachQues.negMarks}`} 57 | 58 |
    59 | {eachQues.type === 'multipleOptions' && ( 60 | 61 | )} 62 | 63 | {eachQues.type === 'mcq' && ( 64 | 65 | )} 66 | 67 | {eachQues.type === 'fillInTheBlanks' && } 68 |
    69 |
    70 |
  2. 71 | ); 72 | })} 73 |
74 | )} 75 | 76 |
77 | 80 | 83 |
84 |
85 | ); 86 | }; 87 | 88 | export default Page3; 89 | -------------------------------------------------------------------------------- /src/pages/exam-creation/question-types/fill-blanks.jsx: -------------------------------------------------------------------------------- 1 | import TextInputField from 'components/text-input-field'; 2 | 3 | const FillBlanks = (props) => { 4 | const { currentQuestion, setCurrentQuestion } = props; 5 | 6 | return ( 7 | <> 8 | setCurrentQuestion((q) => ({ ...q, correctOption: e.target.value }))} 15 | /> 16 | 17 | ); 18 | }; 19 | 20 | export default FillBlanks; 21 | -------------------------------------------------------------------------------- /src/pages/exam-creation/question-types/mcq.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Grid, Typography, Box, Button } from '@mui/material'; 3 | import { makeStyles } from '@mui/styles'; 4 | import Bullet from '@mui/icons-material/FiberManualRecord'; 5 | import TextInputField from 'components/text-input-field'; 6 | 7 | const useStyles = makeStyles((theme) => ({ 8 | labelSmall: { 9 | fontSize: '1.4rem', 10 | }, 11 | })); 12 | 13 | const MCQ = (props) => { 14 | const classes = useStyles(); 15 | const { currentQuestion, setCurrentQuestion } = props; 16 | 17 | const [newOption, setNewOption] = useState(''); 18 | const [newCorrectOption, setNewCorrectOption] = useState(''); 19 | 20 | const addNewOption = () => { 21 | if (!newOption || !newOption.trim()) return; 22 | setCurrentQuestion((q) => ({ ...q, options: [...q.options, { id: q.options.length + 1 || 1, data: newOption }] })); 23 | setNewOption(''); 24 | }; 25 | 26 | const addCorrectOption = () => { 27 | if (!newCorrectOption || !newCorrectOption.trim()) return; 28 | if (currentQuestion.type === 'mcq' && currentQuestion.correctOption?.length >= 1) return; 29 | setCurrentQuestion((q) => ({ ...q, correctOption: [...q.correctOption, +newCorrectOption] })); 30 | setNewCorrectOption(''); 31 | }; 32 | 33 | return ( 34 | 35 | 36 | 37 | Available Options : 38 | 39 | setNewOption(e.target.value)} 45 | showActionBtn 46 | fullWidth 47 | actionBtnText='Add' 48 | actionOnClick={(e) => addNewOption()} 49 | /> 50 | 51 | {currentQuestion.options && 52 | currentQuestion.options.length != 0 && 53 | currentQuestion.options.map((item, index) => ( 54 | 55 | {index + 1}) {item.data} 56 | 57 | ))} 58 | 59 | 60 | 61 | 62 | 63 | {`Correct Option${currentQuestion.type === 'multipleOptions' ? 's' : ''}:`} 64 | 65 | setNewCorrectOption(e.target.value)} 73 | fullWidth 74 | showActionBtn 75 | actionBtnText='Add' 76 | actionOnClick={(e) => addCorrectOption()} 77 | /> 78 | 79 | {currentQuestion.correctOption && 80 | currentQuestion.correctOption.length != 0 && 81 | currentQuestion.correctOption.map((item, index) => ( 82 | 83 | {item} 84 | 85 | ))} 86 | 87 | 88 | 89 | ); 90 | }; 91 | 92 | export default MCQ; 93 | -------------------------------------------------------------------------------- /src/pages/exam-details/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { 3 | Container, 4 | Grid, 5 | Typography, 6 | Box, 7 | Tab, 8 | Tabs, 9 | Button, 10 | Paper, 11 | } from "@mui/material"; 12 | import { useTheme } from "@mui/styles"; 13 | import { useHistory, useLocation } from "react-router-dom"; 14 | import { useSelector, useDispatch } from "react-redux"; 15 | import useStyles from "./styles"; 16 | import { 17 | getExamDetails, 18 | registerInExam, 19 | getUserExamRegisterStatus, 20 | getExamResult, 21 | getExamScores, 22 | } from "api/exam"; 23 | import { showConfirmation } from "redux/slices/confirmation-dialog"; 24 | import DomPurify from "dompurify"; 25 | import { useSnackbar } from "notistack"; 26 | import { getUnitsFromDuration } from "utilities/functions"; 27 | import About from "./tab-content/about"; 28 | import Scores from "./tab-content/scores"; 29 | import Result from "./tab-content/result"; 30 | 31 | let timerInterval; 32 | 33 | const TabPanel = (props) => { 34 | const { children, value, index, ...other } = props; 35 | return ( 36 | 43 | ); 44 | }; 45 | 46 | const a11yProps = (index) => { 47 | return { 48 | id: `vertical-tab-${index}`, 49 | fontWeight: "bold", 50 | }; 51 | }; 52 | 53 | const Exam_Details = () => { 54 | const history = useHistory(); 55 | const location = useLocation(); 56 | const classes = useStyles(); 57 | const { enqueueSnackbar } = useSnackbar(); 58 | const dispatch = useDispatch(); 59 | const theme = useTheme(); 60 | const examId = location.pathname.split("/")[2]; 61 | 62 | const { user } = useSelector((state) => state.auth); 63 | const [value, setValue] = useState(0); 64 | const [examData, setExamData] = useState({}); 65 | const [registerStatus, setRegisterStatus] = useState(false); 66 | const [resultDetails, setResultDetails] = useState(null); 67 | const [examScores, setExamScores] = useState(null); 68 | const cleanDescription = DomPurify.sanitize(examData.description); 69 | 70 | // Timer states 71 | const [remainingTime, setRemainingTime] = useState({ 72 | days: null, 73 | hours: null, 74 | minutes: null, 75 | seconds: null, 76 | }); 77 | 78 | useEffect(() => { 79 | if (!examData?.startTime) return; 80 | const duration = new Date(examData.startTime) - Date.now(); 81 | if (duration < 0) { 82 | return setRemainingTime({ days: 0, hours: 0, minutes: 0, seconds: 0 }); 83 | } 84 | const dd = getUnitsFromDuration(duration); 85 | console.log(duration, dd); 86 | setRemainingTime({ 87 | days: dd.days, 88 | hours: dd.hours, 89 | minutes: dd.minutes, 90 | seconds: dd.seconds, 91 | }); 92 | }, [examData.startTime]); 93 | 94 | useEffect(() => { 95 | if ( 96 | remainingTime.days === null || 97 | remainingTime.hours === null || 98 | remainingTime.minutes === null || 99 | remainingTime.seconds === null 100 | ) { 101 | return; 102 | } 103 | 104 | clearInterval(timerInterval); 105 | timerInterval = setInterval(() => { 106 | if (remainingTime.seconds > 0) { 107 | setRemainingTime((t) => ({ ...t, seconds: t.seconds - 1 })); 108 | } 109 | if (remainingTime.seconds === 0) { 110 | if (remainingTime.minutes === 0) { 111 | if (remainingTime.hours === 0) { 112 | if (remainingTime.days === 0) { 113 | // Timer has finished 114 | clearInterval(timerInterval); 115 | } else { 116 | setRemainingTime((t) => ({ 117 | ...t, 118 | days: t.days - 1, 119 | hours: 23, 120 | minutes: 59, 121 | seconds: 59, 122 | })); 123 | } 124 | } else { 125 | setRemainingTime((t) => ({ 126 | ...t, 127 | hours: t.hours - 1, 128 | minutes: 59, 129 | seconds: 59, 130 | })); 131 | } 132 | } else { 133 | setRemainingTime((t) => ({ 134 | ...t, 135 | minutes: t.minutes - 1, 136 | seconds: 59, 137 | })); 138 | } 139 | } 140 | }, 1000); 141 | 142 | return () => { 143 | clearInterval(timerInterval); 144 | }; 145 | }, [remainingTime]); 146 | 147 | // ------------------------------------- 148 | // FETCH ALL EXAMS DATA 149 | // ------------------------------------- 150 | useEffect(async () => { 151 | fetchExamDetails(); 152 | checkRegisterStatus(); 153 | fetchResult(); 154 | fetchScores(); 155 | window.scrollTo(0, 0); 156 | }, []); 157 | 158 | const fetchResult = async () => { 159 | try { 160 | const res = await getExamResult(examId); 161 | console.log(res); 162 | if (res.data.data) setResultDetails(res.data.data); 163 | } catch (err) { 164 | // enqueueSnackbar(err.message, { variant: 'error' }); 165 | console.error(err); 166 | } 167 | }; 168 | 169 | const fetchScores = async () => { 170 | try { 171 | const res = await getExamScores(examId); 172 | setExamScores(res.data.data); 173 | } catch (err) { 174 | // enqueueSnackbar(err.message, { variant: 'error' }); 175 | console.error(err); 176 | } 177 | }; 178 | 179 | const checkRegisterStatus = async () => { 180 | try { 181 | const res = await getUserExamRegisterStatus(examId); 182 | setRegisterStatus(!!res.data.data.registered); 183 | } catch (err) { 184 | enqueueSnackbar(err.message, { variant: "error" }); 185 | } 186 | }; 187 | 188 | const fetchExamDetails = async () => { 189 | try { 190 | const res = await getExamDetails(examId); 191 | setExamData(res.data.data); 192 | } catch (err) { 193 | enqueueSnackbar(err.message, { variant: "error" }); 194 | } 195 | }; 196 | 197 | const handleChange = (event, newValue) => { 198 | setValue(newValue); 199 | }; 200 | 201 | const registerUserInExam = async () => { 202 | try { 203 | const res = await registerInExam({ examId }); 204 | if (res.status === 200) { 205 | setRegisterStatus(true); 206 | } 207 | } catch (err) { 208 | enqueueSnackbar(err.message, { variant: "error" }); 209 | } 210 | }; 211 | 212 | const redirectToGiveExam = () => { 213 | history.push(`/exam/${examId}/give`); 214 | }; 215 | 216 | const TextPair = ({ heading, value }) => { 217 | return ( 218 |
219 | 223 | {heading} 224 | 225 | {value} 226 |
227 | ); 228 | }; 229 | 230 | const JoinCardText = ({ heading, value }) => { 231 | return ( 232 |
233 | 234 | 237 | {heading}: 238 | 239 | 240 | 241 | 242 | {value} 243 | 244 | 245 |
246 | ); 247 | }; 248 | 249 | const getTypography = (text = "", style = {}) => { 250 | return ( 251 | 256 | {text} 257 | 258 | ); 259 | }; 260 | 261 | const getButton = (text = "", type = "") => { 262 | return ( 263 | 293 | ); 294 | }; 295 | 296 | const JoinRegisterSection = () => { 297 | if (!examData.startTime) return null; 298 | if ( 299 | remainingTime.days === null || 300 | remainingTime.hours === null || 301 | remainingTime.minutes === null || 302 | remainingTime.seconds === null 303 | ) 304 | return null; 305 | const startTime = new Date(examData.startTime); 306 | const endTime = new Date(+startTime + examData.duration); 307 | const remain = Math.abs( 308 | remainingTime.days * 86400000 - 309 | remainingTime.hours * 3600000 - 310 | remainingTime.minutes * 60000 - 311 | remainingTime.seconds * 1000 312 | ); 313 | // If days,hours,minutes,seconds are zeros, then remain = 0, and startTime - remain = 0, so currentTime should be Date.now() 314 | const currentTime = remain === 0 ? Date.now() : +startTime - remain; 315 | if (currentTime < startTime) { 316 | // before exam 317 | if (!user) { 318 | return ( 319 | <> 320 | {getTypography("Login to register for exam")} 321 | 322 | 323 | ); 324 | } else { 325 | if (!registerStatus) { 326 | return ( 327 | <> 328 | {getButton("Register", "register")} 329 | 330 | 331 | ); 332 | } else { 333 | return ( 334 | <> 335 | {getTypography("You have registered")} 336 | 337 | 338 | ); 339 | } 340 | } 341 | } else if (currentTime >= startTime && currentTime < endTime) { 342 | if (!user) { 343 | return getTypography("Login to enter exam"); 344 | } 345 | // user has not registered 346 | else if (!registerStatus) { 347 | return getTypography("You have not registered for this exam"); 348 | } else { 349 | return getButton("Enter Exam", "start"); 350 | } 351 | } else if (currentTime >= endTime) { 352 | // after exam 353 | return getTypography("Exam has ended", { 354 | color: "crimson", 355 | fontWeight: "900", 356 | }); 357 | } 358 | }; 359 | 360 | const CountdownTimer = () => { 361 | return ( 362 | 363 | {`${remainingTime.days} days ${remainingTime.hours} hours ${remainingTime.minutes} minutes ${remainingTime.seconds} seconds`} 364 | 365 | ); 366 | }; 367 | 368 | const getExamDurationFormatted = () => { 369 | if (!examData.duration) return null; 370 | const { days, hours, minutes, seconds } = getUnitsFromDuration( 371 | examData.duration 372 | ); 373 | let duration = ""; 374 | if (days > 0) { 375 | duration += days === 1 ? `${days} day ` : `${days} days `; 376 | } 377 | if (hours > 0) { 378 | duration += hours === 1 ? `${hours} hour ` : `${hours} hours `; 379 | } 380 | if (minutes > 0) { 381 | duration += minutes === 1 ? `${minutes} minute ` : `${minutes} minutes `; 382 | } 383 | if (seconds > 0) { 384 | duration += seconds === 1 ? `${seconds} second ` : `${seconds} seconds `; 385 | } 386 | return duration; 387 | }; 388 | 389 | return ( 390 | <> 391 | {/* ----------- Banner ----------- */} 392 |
401 | 402 | 403 |
404 | 405 | 406 |
407 |
408 | 409 | {examData.name !== null ? examData.name : "-"} 410 | 411 | 412 | {" "} 413 | Author : {examData.user?.name} 414 | 415 |
416 | 417 |
418 | {/*-------------------------------------------------- */} 419 | {/* INFORMATION */} 420 | {/*-------------------------------------------------- */} 421 |
422 | 426 | 430 | 434 | 442 |
443 | 444 | {/*-------------------------------------------------- */} 445 | {/* EXAM TIME DISPLAY CARD */} 446 | {/*-------------------------------------------------- */} 447 | 448 | 452 |
453 | 461 |
462 | 466 |
467 | 468 | {/* Timer Part */} 469 | 470 |
471 |
472 |
473 |
474 | 475 | {/* SECTION - 2 */} 476 | {/* Side Vertical Tabs */} 477 |
478 | 484 | 489 | 495 | 501 | 506 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | FAQ's 527 | 528 | 529 | 530 | Discussions 531 | 532 |
533 |
534 | 535 | ); 536 | }; 537 | 538 | export default Exam_Details; 539 | -------------------------------------------------------------------------------- /src/pages/exam-details/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@mui/styles'; 2 | 3 | export default makeStyles((theme) => ({ 4 | banner: { 5 | width: '100%', 6 | objectFit: 'cover', 7 | objectPosition: 'center', 8 | }, 9 | dp: { 10 | position: 'relative', 11 | transform: 'translateY(-40%)', 12 | borderRadius: '8px', 13 | width: 180, 14 | height: 180, 15 | objectFit: 'cover', 16 | }, 17 | root: { 18 | flexGrow: 1, 19 | backgroundColor: theme.palette.background.paper, 20 | display: 'flex', 21 | minHeight: '50rem', 22 | paddingBottom: '10rem', 23 | fontWeight: '600', 24 | }, 25 | tabVerticalLine: { 26 | borderRight: `2px solid ${theme.palette.divider}`, 27 | overflow: 'visible !important', 28 | }, 29 | sliderTitle: { 30 | fontWeight: 'bold', 31 | }, 32 | examInfo: { 33 | // padding: '1.6rem', 34 | width: '75%', 35 | display: 'grid', 36 | gridTemplateColumns: '1fr 1fr 1fr', 37 | }, 38 | joinCard: { 39 | position: 'relative', 40 | width: '25%', 41 | backgroundColor: theme.palette.background.default, 42 | padding: '1.5rem', 43 | transform: 'translateY(-20%)', // Shifts Vertically 44 | }, 45 | joinButton: { 46 | width: '100%', 47 | margin: '1rem auto', 48 | fontWeight: '600', 49 | textTransform: 'uppercase', 50 | }, 51 | joinText: { 52 | margin: '0 auto', 53 | fontWeight: '600', 54 | textTransform: 'uppercase', 55 | textAlign: 'center', 56 | }, 57 | })); 58 | -------------------------------------------------------------------------------- /src/pages/exam-details/tab-content/about.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Grid, Typography } from '@mui/material'; 3 | import { makeStyles, useTheme } from '@mui/styles'; 4 | import commonStyle from '../../../common-style'; 5 | 6 | const useStyles = makeStyles((theme) => ({})); 7 | 8 | const About = (props) => { 9 | const { content = '' } = props; 10 | const classes = useStyles(); 11 | const classes2 = commonStyle(); 12 | const theme = useTheme(); 13 | 14 | return ( 15 | <> 16 | DESCRIPTION 17 | 18 |
19 | 20 | ); 21 | }; 22 | 23 | export default About; 24 | -------------------------------------------------------------------------------- /src/pages/exam-details/tab-content/result.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Grid, Typography } from '@mui/material'; 3 | import { makeStyles, useTheme } from '@mui/styles'; 4 | import MCQSingle from 'layouts/questionType-result/mcq-single'; 5 | import MCQMultiple from 'layouts/questionType-result/mcq-multiple'; 6 | import FillBlanks from 'layouts/questionType-result/fill-blanks'; 7 | import DomPurify from 'dompurify'; 8 | import commonStyle from '../../../common-style'; 9 | 10 | const useStyles = makeStyles((theme) => ({})); 11 | 12 | const Result = (props) => { 13 | const { ['result']: resultDetails } = props; 14 | const classes = useStyles(); 15 | const classes2 = commonStyle(); 16 | const theme = useTheme(); 17 | 18 | if (!resultDetails) return null; 19 | 20 | return ( 21 | <> 22 | RESULT 23 | 24 | {resultDetails && resultDetails.length > 0 && ( 25 |
    26 | {resultDetails.map((ques, idx) => { 27 | return ( 28 |
  1. 29 | {/* ==================== MAIN QUESTION BODY ==================== */} 30 |
    31 |
    37 | 38 |
    39 | {ques.type === 'multipleOptions' && ( 40 | 41 | )} 42 | 43 | {ques.type === 'mcq' && ( 44 | 45 | )} 46 | 47 | {ques.type === 'fillInTheBlanks' && ( 48 | 49 | )} 50 |
    51 |
    52 |
  2. 53 | ); 54 | })} 55 |
56 | )} 57 | 58 | ); 59 | }; 60 | 61 | export default Result; 62 | -------------------------------------------------------------------------------- /src/pages/exam-details/tab-content/scores.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useDispatch } from 'react-redux'; 3 | import { showConfirmation } from 'redux/slices/confirmation-dialog'; 4 | import { useHistory } from 'react-router-dom'; 5 | import { useSnackbar } from 'notistack'; 6 | import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Card, CardContent, Box, Typography } from '@mui/material'; 7 | import { makeStyles, useTheme } from '@mui/styles'; 8 | import commonStyle from '../../../common-style'; 9 | import crown from '../../../assets/icons/crown.png'; 10 | 11 | const useStyles = makeStyles((theme) => ({ 12 | card: { 13 | marginBottom: '2rem', 14 | paddingBottom: '2rem', 15 | marginTop: '2.6rem', 16 | textAlign: 'center', 17 | }, 18 | })); 19 | 20 | const Scores = (props) => { 21 | const { examScoresDetails } = props; 22 | const dispatch = useDispatch(); 23 | const classes = useStyles(); 24 | const classes2 = commonStyle(); 25 | const history = useHistory(); 26 | const theme = useTheme(); 27 | const { enqueueSnackbar, closeSnackbar } = useSnackbar(); 28 | 29 | console.log(examScoresDetails); 30 | 31 | if (!examScoresDetails) return null; 32 | 33 | return ( 34 | <> 35 | SCORES 36 | 37 | {/*
38 | 54 | 55 | 56 | 57 |
*/} 58 | 59 |
60 | 61 | 62 | 63 | 64 | Your Score 65 | 66 | 67 | 68 | 69 | COMPLETED AT 70 | 71 | {new Date(examScoresDetails.finishTime).toDateString()} , {new Date(examScoresDetails.finishTime).toLocaleTimeString()} 72 | 73 | 74 | 75 | 76 | RANK 77 | 78 | {examScoresDetails.rank !== null ? ` Position ${examScoresDetails.rank}` : '-'} 79 | 80 | 81 | 82 | 83 | SCORE ACHIEVED 84 | 85 | {examScoresDetails.totalScore !== null ? examScoresDetails.totalScore : '-'} 86 | 87 | 88 | 89 | 90 | 91 | 92 |
93 | 94 | 95 | 96 | Top 5 Performers 97 | 98 | 99 | 100 | 101 | 102 | 103 | Rank 104 | Name 105 | Achieved Score 106 | Completed At  107 | 108 | 109 | 110 | 111 | {examScoresDetails.topPerformers && 112 | examScoresDetails.topPerformers.map((user) => ( 113 | 117 | {user.rank} 118 | {user.name} 119 | {user.totalScore} 120 | 121 | {' '} 122 | {new Date(user.finishTime).toDateString()} , {new Date(user.finishTime).toLocaleTimeString()}{' '} 123 | 124 | 125 | ))} 126 | 127 |
128 |
129 | 130 | ); 131 | }; 132 | 133 | export default Scores; 134 | -------------------------------------------------------------------------------- /src/pages/give-exam/index.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from 'react'; 2 | import { Container, Typography, Button, Paper, Grid } from '@mui/material'; 3 | import { useLocation, useHistory } from 'react-router-dom'; 4 | import { submitExamAnswers, startExam } from 'api/exam'; 5 | import Page1 from './page1'; 6 | import Page2 from './page2'; 7 | import RequestFullScreen from 'layouts/request-full-screen'; 8 | import { getUnitsFromDuration } from 'utilities/functions'; 9 | import { useSnackbar } from 'notistack'; 10 | 11 | let timerInterval; 12 | 13 | const GiveExam = () => { 14 | const location = useLocation(); 15 | const history = useHistory(); 16 | const { enqueueSnackbar } = useSnackbar(); 17 | const examId = location.pathname.split('/')[2]; 18 | const [examData, setExamData] = useState({}); 19 | const [answer, setAnswer] = useState({}); 20 | const [questionsStatus, setQuestionsStatus] = useState({}); 21 | const [page, setPage] = useState(1); 22 | const [isFullScreen, setIsFullScreen] = useState(false); 23 | const documentElement = document.documentElement; 24 | const [remainingTime, setRemainingTime] = useState({ 25 | days: null, 26 | hours: null, 27 | minutes: null, 28 | seconds: null, 29 | }); 30 | 31 | const questions = examData.questions || []; 32 | 33 | useEffect(() => { 34 | EnterFullScreen(); 35 | documentElement.addEventListener('fullscreenchange', () => { 36 | if (document.fullscreenElement) setIsFullScreen(true); 37 | else setIsFullScreen(false); 38 | }); 39 | getExamData(); 40 | }, []); 41 | 42 | useEffect(() => { 43 | if (remainingTime.days === null || remainingTime.hours === null || remainingTime.minutes === null || remainingTime.seconds === null) { 44 | return; 45 | } 46 | clearInterval(timerInterval); 47 | timerInterval = setInterval(() => { 48 | if (remainingTime.seconds > 0) { 49 | setRemainingTime((t) => ({ ...t, seconds: t.seconds - 1 })); 50 | } 51 | if (remainingTime.seconds === 0) { 52 | if (remainingTime.minutes === 0) { 53 | if (remainingTime.hours === 0) { 54 | if (remainingTime.days === 0) { 55 | // Timer has finished 56 | clearInterval(timerInterval); 57 | handleEndExam(); 58 | } else { 59 | setRemainingTime((t) => ({ ...t, days: t.days - 1, hours: 23, minutes: 59, seconds: 59 })); 60 | } 61 | } else { 62 | setRemainingTime((t) => ({ ...t, hours: t.hours - 1, minutes: 59, seconds: 59 })); 63 | } 64 | } else { 65 | setRemainingTime((t) => ({ ...t, minutes: t.minutes - 1, seconds: 59 })); 66 | } 67 | } 68 | }, 1000); 69 | 70 | return () => { 71 | clearInterval(timerInterval); 72 | }; 73 | }, [remainingTime]); 74 | 75 | const EnterFullScreen = () => { 76 | documentElement 77 | .requestFullscreen() 78 | .then(() => console.log('entered full screen')) 79 | .catch((err) => enqueueSnackbar(err.message, { variant: 'error' })); 80 | }; 81 | 82 | const ExitFullScreen = () => { 83 | document 84 | .exitFullscreen() 85 | .then(() => console.log('exited full screen')) 86 | .catch((err) => enqueueSnackbar(err.message, { variant: 'error' })); 87 | }; 88 | 89 | const getExamData = async () => { 90 | try { 91 | const res = await startExam(examId); 92 | if (res) setExamData(res.data.data); 93 | else console.log('Error occured !', res); 94 | } catch (err) { 95 | enqueueSnackbar(err.message, { variant: 'error' }); 96 | history.push(`/exam/${examId}`); 97 | } 98 | }; 99 | 100 | const getQuestionFromId = (id) => { 101 | return questions.find((q) => q.id === id); 102 | }; 103 | 104 | const getQuestionType = (q) => { 105 | if (q) return q.type; 106 | return ''; 107 | }; 108 | 109 | const handleQAnswer = (qId, ans) => { 110 | setAnswer((a) => ({ ...a, [qId]: { type: getQuestionType(getQuestionFromId(qId)), answer: ans } })); 111 | }; 112 | 113 | const handleQStatus = (qId, status) => { 114 | setQuestionsStatus((qs) => ({ ...qs, [qId]: status })); 115 | }; 116 | 117 | const handleEndExam = async () => { 118 | const body = { 119 | examId, 120 | answers: answer, 121 | finishTime: Date.now(), 122 | }; 123 | const res = await submitExamAnswers(body); 124 | console.log(res); 125 | ExitFullScreen(); 126 | history.push(`/exam/${examId}`); 127 | }; 128 | 129 | const handleTimerCountdown = () => { 130 | if (!examData) return; 131 | const endTime = new Date(+examData.startTime + examData.duration); 132 | const dd = getUnitsFromDuration(+endTime - Date.now()); 133 | setRemainingTime({ days: dd.days, hours: dd.hours, minutes: dd.minutes, seconds: dd.seconds }); 134 | }; 135 | 136 | const handleViewQuestions = () => { 137 | setPage((p) => p + 1); 138 | handleTimerCountdown(); 139 | }; 140 | 141 | return ( 142 |
143 | {false && !isFullScreen ? ( 144 | 145 | ) : ( 146 | 147 | {page === 1 && } 148 | {page === 2 && ( 149 | 158 | )} 159 | 160 | )} 161 |
162 | ); 163 | }; 164 | 165 | export default GiveExam; 166 | -------------------------------------------------------------------------------- /src/pages/give-exam/page1.jsx: -------------------------------------------------------------------------------- 1 | import { Typography, Button, Paper, Grid } from '@mui/material'; 2 | 3 | const Page1 = (props) => { 4 | const { handlePageNext = () => {} } = props; 5 | 6 | return ( 7 | <> 8 | 9 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Iure mollitia officia incidunt recusandae maxime facilis deleniti cum vel 10 | enim, quod sequi repellat porro quas molestiae quisquam dolores, voluptas in quos quo magnam et! Eaque corporis et aut quo officiis 11 | ullam eveniet, ipsa eius dolorum inventore velit beatae iusto fugit labore, excepturi similique deleniti ut quibusdam totam! Vitae 12 | quisquam numquam doloremque magni esse temporibus suscipit. Odit, odio quae consequuntur vero voluptas aperiam quaerat? Veniam natus 13 | molestias minima exercitationem, repellendus quam! Officia, nisi ad inventore a qui illum reprehenderit eligendi tempora fuga ea 14 | quasi quia quibusdam repudiandae neque dicta, voluptate dolorem ducimus? 15 | 16 | 19 | 20 | ); 21 | }; 22 | 23 | export default Page1; 24 | -------------------------------------------------------------------------------- /src/pages/give-exam/page2.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { Typography, Button, Paper, Grid } from '@mui/material'; 3 | import { makeStyles, useTheme } from '@mui/styles'; 4 | import MCQSingle from './question-options/mcq-single'; 5 | import MCQMultiple from './question-options/mcq-multiple'; 6 | import FillBlanks from './question-options/fill-blanks'; 7 | import DomPurify from 'dompurify'; 8 | import { useDispatch } from 'react-redux'; 9 | import { showConfirmation } from 'redux/slices/confirmation-dialog'; 10 | 11 | const qStatus = { 12 | notAttempted: 'not_attempted', 13 | marked: 'marked', 14 | review: 'review', 15 | }; 16 | 17 | const useStyles = makeStyles((theme) => ({ 18 | bubble: { 19 | display: 'flex', 20 | justifyContent: 'center', 21 | alignItems: 'center', 22 | width: 50, 23 | minWidth: 0, 24 | height: 50, 25 | margin: '1rem', 26 | borderRadius: '50%', 27 | background: theme.palette.mode === 'dark' ? theme.palette.grey[600] : theme.palette.grey[400], 28 | color: theme.palette.common.white, 29 | }, 30 | })); 31 | 32 | const Page2 = (props) => { 33 | const { 34 | questions = [], 35 | answerObj = {}, 36 | handleQAnswer = () => {}, 37 | questionsStatus = {}, 38 | handleQStatus = () => {}, 39 | handleEndExam = () => {}, 40 | remainingTime = {}, 41 | } = props; 42 | 43 | const classes = useStyles(); 44 | const theme = useTheme(); 45 | const dispatch = useDispatch(); 46 | const [currentQuestion, setCurrentQuestion] = useState({}); 47 | const paperHeight = 'calc(100vh - 20rem)'; 48 | const questionHeight = 'calc(100vh - 35rem)'; 49 | const QuestionStatement = DomPurify.sanitize(currentQuestion.question); 50 | 51 | useEffect(() => { 52 | if (questions.length > 0) { 53 | console.log('questions :: ', questions); 54 | setCurrentQuestion(questions[0]); 55 | } 56 | }, [questions]); 57 | 58 | // -------------------------------------------- 59 | // Function for the Question Number Bubble (returns the color) 60 | // -------------------------------------------- 61 | const getBubbleBackground = (qId) => { 62 | const status = questionsStatus[qId]; 63 | if (!status || status === qStatus.notAttempted) return theme.palette.mode === 'dark' ? theme.palette.grey[600] : theme.palette.grey[400]; 64 | if (status === qStatus.marked) return theme.palette.mode === 'dark' ? theme.palette.primary.dark : theme.palette.primary.main; 65 | if (status === qStatus.review) return theme.palette.mode === 'dark' ? theme.palette.secondary.dark : theme.palette.secondary.main; 66 | }; 67 | 68 | return ( 69 | <> 70 | {/* ==================== TIMER COMPONENT ==================== */} 71 | 79 | 80 | Time Remaining:{' '} 81 | {`${remainingTime.days} days ${remainingTime.hours} hours ${remainingTime.minutes} minutes ${remainingTime.seconds} seconds`} 82 | 83 | 99 | 100 | 101 | 102 | 103 | 104 |
110 | {`Marks: ${3}`} 111 | {`Negative Marks: ${1}`} 112 |
113 | 114 | {/* ==================== MAIN QUESTION BODY ==================== */} 115 |
116 |
117 |
118 | {currentQuestion.type === 'multipleOptions' && ( 119 | 125 | )} 126 | 127 | {currentQuestion.type === 'mcq' && ( 128 | 134 | )} 135 | 136 | {currentQuestion.type === 'fillInTheBlanks' && ( 137 | 138 | )} 139 |
140 |
141 | 142 | {/* ==================== SEVERAL BUTTONS ==================== */} 143 |
144 | 151 | 152 | 159 | 160 | 167 | 168 | 178 |
179 | 180 | 181 | 182 | 183 | 184 |
185 | {questions.map((q, i) => ( 186 | 199 | ))} 200 |
201 |
202 |
203 | 204 | 205 | ); 206 | }; 207 | 208 | export default Page2; 209 | -------------------------------------------------------------------------------- /src/pages/give-exam/question-options/fill-blanks.jsx: -------------------------------------------------------------------------------- 1 | import TextInputField from 'components/text-input-field'; 2 | 3 | const FillBlanks = (props) => { 4 | const { questionId, answer, handleQAnswer } = props; 5 | 6 | return ( 7 | handleQAnswer(questionId, e.target.value)} 12 | /> 13 | ); 14 | }; 15 | 16 | export default FillBlanks; 17 | -------------------------------------------------------------------------------- /src/pages/give-exam/question-options/mcq-multiple.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import FormGroup from '@mui/material/FormGroup'; 3 | import FormControlLabel from '@mui/material/FormControlLabel'; 4 | import Checkbox from '@mui/material/Checkbox'; 5 | 6 | const MCQMultiple = (props) => { 7 | const { questionId, options, answer, handleQAnswer } = props; 8 | 9 | const handleOptionChange = (e, optionId) => { 10 | let tmpAns = !answer ? [] : [...answer]; 11 | optionId = +optionId; 12 | if (tmpAns.includes(optionId)) tmpAns = tmpAns.filter((o) => o !== optionId); 13 | else tmpAns.push(optionId); 14 | handleQAnswer(questionId, tmpAns); 15 | }; 16 | 17 | return ( 18 | 19 | {options.map((o) => ( 20 | handleOptionChange(e, o.id)} 25 | /> 26 | } 27 | label={o.data} 28 | /> 29 | ))} 30 | 31 | ); 32 | }; 33 | 34 | export default MCQMultiple; 35 | -------------------------------------------------------------------------------- /src/pages/give-exam/question-options/mcq-single.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import Radio from '@mui/material/Radio'; 3 | import RadioGroup from '@mui/material/RadioGroup'; 4 | import FormControlLabel from '@mui/material/FormControlLabel'; 5 | import FormControl from '@mui/material/FormControl'; 6 | 7 | const MCQSingle = (props) => { 8 | const { questionId, options, answer, handleQAnswer } = props; 9 | 10 | const handleOptionChange = (e) => { 11 | handleQAnswer(questionId, [+e.target.value]); 12 | }; 13 | 14 | return ( 15 | 16 | handleOptionChange(e)}> 17 | {options.map((o) => ( 18 | } label={o.data} /> 19 | ))} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default MCQSingle; 26 | -------------------------------------------------------------------------------- /src/pages/home/arrows.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTheme } from '@emotion/react'; 3 | 4 | import { VisibilityContext } from 'react-horizontal-scrolling-menu'; 5 | import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft'; 6 | import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight'; 7 | import { Box, Card, CardContent, Button, Typography } from '@mui/material'; 8 | 9 | function Arrow({ children, disabled, onClick }) { 10 | const theme = useTheme(); 11 | return ( 12 |
27 | {children} 28 |
29 | ); 30 | } 31 | 32 | export function LeftArrow() { 33 | const { 34 | isFirstItemVisible, 35 | scrollPrev, 36 | visibleItemsWithoutSeparators, 37 | initComplete, 38 | } = React.useContext(VisibilityContext); 39 | 40 | const [disabled, setDisabled] = React.useState( 41 | !initComplete || (initComplete && isFirstItemVisible) 42 | ); 43 | React.useEffect(() => { 44 | // NOTE: detect if whole component visible 45 | if (visibleItemsWithoutSeparators.length) { 46 | setDisabled(isFirstItemVisible); 47 | } 48 | }, [isFirstItemVisible, visibleItemsWithoutSeparators]); 49 | 50 | return ( 51 | scrollPrev()}> 52 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | export function RightArrow() { 72 | const { isLastItemVisible, scrollNext, visibleItemsWithoutSeparators } = 73 | React.useContext(VisibilityContext); 74 | 75 | // console.log({ isLastItemVisible }); 76 | const [disabled, setDisabled] = React.useState( 77 | !visibleItemsWithoutSeparators.length && isLastItemVisible 78 | ); 79 | React.useEffect(() => { 80 | if (visibleItemsWithoutSeparators.length) { 81 | setDisabled(isLastItemVisible); 82 | } 83 | }, [isLastItemVisible, visibleItemsWithoutSeparators]); 84 | 85 | return ( 86 | scrollNext()}> 87 | 100 | 101 | 102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /src/pages/home/exams-horizontal-scroll-section.jsx: -------------------------------------------------------------------------------- 1 | import { LeftArrow, RightArrow } from './arrows'; 2 | import { useState } from 'react'; 3 | import ExamDetailsCard2 from 'components/exam-details-card2'; 4 | import { ScrollMenu, VisibilityContext } from 'react-horizontal-scrolling-menu'; 5 | import { Box, Card, CardContent, Button, Typography } from '@mui/material'; 6 | 7 | const ExamsHorizontalScrollSection = ({ allData }) => { 8 | //swipable states=============================== 9 | 10 | const elemPrefix = 'test'; 11 | const getId = (index) => `${elemPrefix}${index}`; 12 | 13 | const getItems = () => { 14 | return allData.map((data, id) => { 15 | data[id] = id; 16 | return data; 17 | }); 18 | }; 19 | 20 | const [items, setItems] = useState(getItems); 21 | const [selected, setSelected] = useState([]); 22 | const [position, setPosition] = useState(0); 23 | 24 | const isItemSelected = (id) => !!selected.find((el) => el === id); 25 | 26 | const handleClick = 27 | (id) => 28 | ({ getItemById, scrollToItem }) => { 29 | const itemSelected = isItemSelected(id); 30 | 31 | setSelected((currentSelected) => 32 | itemSelected 33 | ? currentSelected.filter((el) => el !== id) 34 | : currentSelected.concat(id) 35 | ); 36 | }; 37 | //============================================== 38 | 39 | const [showFilters, setShowFilters] = useState(false); 40 | const [selectedFilters, setSelectedFilters] = useState({ 41 | startDate: null, 42 | examVisibility: null, 43 | examStatus: null, 44 | }); 45 | 46 | const handleFilterChange = (e) => 47 | setSelectedFilters((f) => ({ ...f, [e.target.name]: e.target.value })); 48 | const hideFilters = () => setShowFilters(false); 49 | 50 | return ( 51 | } 63 | RightArrow={} 64 | onWheel={onWheel} 65 | > 66 | {/* array.map(() => {}) */} 67 | {allData && 68 | allData.map((Obj, id) => ( 69 | 86 | ))} 87 | 88 | ); 89 | }; 90 | 91 | function onWheel(apiObj, ev) { 92 | console.log(apiObj, 'sss', ev); 93 | const isThouchpad = Math.abs(ev.deltaX) !== 0 || Math.abs(ev.deltaY) < 15; 94 | 95 | if (isThouchpad) { 96 | ev.stopPropagation(); 97 | return; 98 | } 99 | 100 | if (ev.deltaY < 0) { 101 | apiObj.scrollNext(); 102 | } else if (ev.deltaY > 0) { 103 | apiObj.scrollPrev(); 104 | } 105 | } 106 | 107 | export default ExamsHorizontalScrollSection; 108 | -------------------------------------------------------------------------------- /src/pages/home/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useTheme } from '@emotion/react'; 3 | import { Container } from '@mui/material'; 4 | import { Box, Button } from '@mui/material'; 5 | import useStyles from './styles'; 6 | import { getExams } from 'api/exam'; 7 | import Carousel from 'components/carousel'; 8 | import announcementWhite from 'assets/icons/announcementWhite.png'; 9 | import announcementBlack from 'assets/icons/announcementBlack.png'; 10 | import { useSnackbar } from 'notistack'; 11 | import ExamsHorizontalScroll from './exams-horizontal-scroll-section'; 12 | 13 | const examVisibilities = [ 14 | { label: 'Public', value: 'public' }, 15 | { label: 'Private', value: 'private' }, 16 | ]; 17 | 18 | const examStatuses = ['Scheduled', 'Ongoing', 'Finished']; 19 | 20 | const Home = () => { 21 | const [fetchAllExam, setFetchAllExam] = useState([]); 22 | const theme = useTheme(); 23 | const classes = useStyles(); 24 | const { enqueueSnackbar } = useSnackbar(); 25 | 26 | // ------------------------------------- 27 | // FETCH ALL EXAMS DATA 28 | // ------------------------------------- 29 | useEffect(async () => { 30 | try { 31 | const res = await getExams({}); 32 | console.log('ALL EXAMS : ', res); 33 | setFetchAllExam(res.data.data); 34 | } catch (err) { 35 | enqueueSnackbar(err.message, { variant: 'error' }); 36 | } 37 | }, []); 38 | 39 | return ( 40 |
41 | {/* ------------- CAROUSAL PART -------------------- */} 42 |
43 | 44 |
45 | 46 | {/* ------------- SECOND PART -------------------- */} 47 |
48 |
Events and Contests
49 | 50 |
51 | 52 | {/* ------------- THIRD PART -------------------- */} 53 |
54 |
More Exams
55 | 56 | 57 |
58 | 59 | {/* ------------------- LAST ANNOUNCEMENT SECTION ------------------- */} 60 |
61 | 62 |
Announcements
63 | {new Array(3).fill(0).map((announcement) => ( 64 |
65 |
71 | {' '} 79 |

New Exam

80 |
{new Date().toDateString()}
81 |
82 |
83 | Registrations for the 1st stages of the Indian Computing 84 | Olympiad 2022 - ZIO (Zonal Informatics Olympiad) & ZCO (Zonal 85 | Computing Olympiad) are ongoing. 86 |
87 |
88 | 89 |
90 |
91 | ))} 92 |
93 |
94 |
95 | ); 96 | }; 97 | export default Home; 98 | -------------------------------------------------------------------------------- /src/pages/home/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@mui/styles'; 2 | 3 | export default makeStyles((theme) => ({ 4 | imgSize: { 5 | width: '4rem', 6 | height: '4rem', 7 | marginRight: '1rem', 8 | }, 9 | homePage: { 10 | backgroundColor: 11 | theme.palette.mode === 'dark' 12 | ? theme.palette.common.black 13 | : theme.palette.grey[200], 14 | }, 15 | heading: { 16 | fontSize: '3.2rem', 17 | textAlign: 'center', 18 | marginBottom: '2rem', 19 | }, 20 | sectionHeading: { 21 | fontSize: '2.6rem', 22 | width: '85%', 23 | border: '0px solid red', 24 | margin: '1rem auto', 25 | fontWeight: '600', 26 | // color: theme.palette.grey[600], 27 | color: theme.palette.primary.main, 28 | border: '0px solid red', 29 | }, 30 | carouselSection: { 31 | padding: '4rem', 32 | backgroundColor: 33 | theme.palette.mode === 'dark' 34 | ? theme.palette.grey[900] 35 | : theme.palette.grey[100], 36 | marginBottom: '3rem', 37 | }, 38 | examListSection: { 39 | paddingTop: '4rem', 40 | paddingBottom: '4rem', 41 | border: '0px solid red', 42 | padding: '4rem', 43 | backgroundColor: 44 | theme.palette.mode === 'dark' 45 | ? theme.palette.grey[900] 46 | : theme.palette.grey[100], 47 | }, 48 | announcementSection: { 49 | backgroundColor: 50 | theme.palette.mode === 'dark' 51 | ? theme.palette.grey[900] 52 | : 'rgb(26, 178, 115,.09)', 53 | }, 54 | announcementCard: { 55 | padding: '1rem', 56 | borderTop: `0px solid ${ 57 | theme.palette.mode === 'dark' 58 | ? theme.palette.common.black 59 | : theme.palette.grey[300] 60 | }`, 61 | borderBottom: `2px solid ${ 62 | theme.palette.mode === 'dark' 63 | ? theme.palette.common.black 64 | : theme.palette.grey[300] 65 | }`, 66 | margin: '1rem', 67 | }, 68 | announcementContainer: { 69 | margin: '0 auto', 70 | paddingTop: '2rem', 71 | '& > div': { width: '85%', margin: '0 auto', marginTop: '3rem' }, 72 | '& > :nth-child(2)': { 73 | borderTop: `0px solid ${ 74 | theme.palette.mode === 'dark' 75 | ? theme.palette.common.black 76 | : theme.palette.grey[300] 77 | } !important`, 78 | }, 79 | '& > :last-child': { 80 | borderBottom: `0px solid ${ 81 | theme.palette.mode === 'dark' 82 | ? theme.palette.common.black 83 | : theme.palette.grey[300] 84 | } !important`, 85 | }, 86 | }, 87 | announcementCardDesc: { 88 | width: '87%', 89 | margin: '0 auto', 90 | }, 91 | announcementCardButtonDiv: { 92 | marginTop: '2rem', 93 | marginBottom: '2.6rem', 94 | width: '87%', 95 | margin: '0 auto', 96 | }, 97 | })); 98 | -------------------------------------------------------------------------------- /src/pages/profile/User.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useHistory, useParams } from 'react-router-dom'; 3 | import { Grid, Typography, Button, Box, TextField, Modal, Container, Paper, Tabs, Tab } from '@mui/material'; 4 | import { makeStyles, useTheme } from '@mui/styles'; 5 | import ProfileSection from './profile-section'; 6 | import DetailSection from './details-section'; 7 | 8 | const User = () => { 9 | const history = useHistory(); 10 | const theme = useTheme(); 11 | 12 | return ( 13 | <> 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default User; 29 | -------------------------------------------------------------------------------- /src/pages/profile/crop-image.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import Modal from '@mui/material/Modal'; 3 | import { Dialog, DialogContent } from 'components/dialog'; 4 | import Cropper from 'react-easy-crop'; 5 | import Slider from '@mui/material/Slider'; 6 | import getCroppedImg from './crop-img-utils'; 7 | import Button from '@mui/material/Button'; 8 | import Stack from '@mui/material/Stack'; 9 | import Box from '@mui/material/Box'; 10 | import { styled } from '@mui/material/styles'; 11 | import Paper from '@mui/material/Paper'; 12 | const Item = styled(Paper)(({ theme }) => ({ 13 | ...theme.typography.body2, 14 | padding: theme.spacing(1), 15 | textAlign: 'center', 16 | color: theme.palette.text.secondary, 17 | })); 18 | 19 | //import 'react-image-crop-component/style.css'; 20 | const style = { 21 | position: 'absolute', 22 | top: '50%', 23 | left: '50%', 24 | transform: 'translate(-50%, -50%)', 25 | width: 1000, 26 | height: 600, 27 | bgcolor: 'background.paper', 28 | boxShadow: 24, 29 | p: 4, 30 | }; 31 | export default function CropImageDialog(props) { 32 | const { open, setOpen } = props; 33 | const [croppedArea, setcroppedArea] = useState(null); 34 | const [crop, setCrop] = useState({ x: 0, y: 0 }); 35 | const [zoom, setZoom] = useState(1); 36 | const [rotation, setRotation] = useState(0); 37 | 38 | const handleClose = () => { 39 | props.setfileselect(null); 40 | setOpen(false); 41 | }; 42 | 43 | const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => { 44 | setcroppedArea(croppedAreaPixels); 45 | }, []); 46 | 47 | const cropSize = { height: props.height, width: props.width }; 48 | 49 | const reset = () => { 50 | setZoom(1); 51 | setRotation(0); 52 | setCrop({ x: 0, y: 0 }); 53 | }; 54 | 55 | const save = useCallback(async () => { 56 | try { 57 | const croppedImage = await getCroppedImg(props.fileselect, croppedArea, rotation); 58 | console.log('donee', croppedImage); 59 | props.setfileselect(null); 60 | props.setimageCropped(croppedImage); 61 | setOpen(false); 62 | } catch (e) { 63 | console.error(e); 64 | } 65 | }, [croppedArea, rotation]); 66 | 67 | return ( 68 | handleClose()} maxWidth='xl'> 69 | 70 |
71 | 83 |
84 | 85 |
86 | 87 |
88 |

Zoom

89 | setZoom(zoom)} 97 | classes={{ root: 'slider' }} 98 | style={{ width: '20vw' }} 99 | /> 100 |
101 |
102 |

Rotate

103 | setRotation(rotation)} 111 | classes={{ root: 'slider' }} 112 | style={{ width: '20vw' }} 113 | /> 114 |
115 |
116 | 117 |
118 | 121 | 124 |
125 |
126 |
127 |
128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /src/pages/profile/crop-img-utils.jsx: -------------------------------------------------------------------------------- 1 | const createImage = (url) => 2 | new Promise((resolve, reject) => { 3 | const image = new Image(); 4 | image.addEventListener('load', () => resolve(image)); 5 | image.addEventListener('error', (error) => reject(error)); 6 | image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox 7 | image.src = url; 8 | }); 9 | 10 | function getRadianAngle(degreeValue) { 11 | return (degreeValue * Math.PI) / 180; 12 | } 13 | 14 | /** 15 | * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop 16 | * @param {File} image - Image File url 17 | * @param {Object} pixelCrop - pixelCrop Object provided by react-easy-crop 18 | * @param {number} rotation - optional rotation parameter 19 | */ 20 | export default async function getCroppedImg(imageSrc, pixelCrop, rotation = 0) { 21 | const image = await createImage(imageSrc); 22 | const canvas = document.createElement('canvas'); 23 | const ctx = canvas.getContext('2d'); 24 | 25 | const maxSize = Math.max(image.width, image.height); 26 | const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2)); 27 | 28 | // set each dimensions to double largest dimension to allow for a safe area for the 29 | // image to rotate in without being clipped by canvas context 30 | canvas.width = safeArea; 31 | canvas.height = safeArea; 32 | 33 | // translate canvas context to a central location on image to allow rotating around the center. 34 | ctx.translate(safeArea / 2, safeArea / 2); 35 | ctx.rotate(getRadianAngle(rotation)); 36 | ctx.translate(-safeArea / 2, -safeArea / 2); 37 | 38 | // draw rotated image and store data. 39 | ctx.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5); 40 | const data = ctx.getImageData(0, 0, safeArea, safeArea); 41 | 42 | // set canvas width to final desired crop size - this will clear existing context 43 | canvas.width = pixelCrop.width; 44 | canvas.height = pixelCrop.height; 45 | 46 | // paste generated rotate image with correct offsets for x,y crop values. 47 | ctx.putImageData( 48 | data, 49 | Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x), 50 | Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y) 51 | ); 52 | 53 | // As Base64 string 54 | // return canvas.toDataURL('image/jpeg'); 55 | 56 | // As a blob 57 | return new Promise((resolve) => { 58 | canvas.toBlob((file) => { 59 | resolve(URL.createObjectURL(file)); 60 | }, 'image/jpeg'); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /src/pages/profile/details-section.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Grid, Typography, Button, Box, TextField, Modal, Container, Paper, Tabs, Tab } from '@mui/material'; 3 | import ExamsList from './exams-list'; 4 | 5 | const TabPanel = (props) => { 6 | const { children, value, index, ...other } = props; 7 | return ( 8 | 15 | ); 16 | }; 17 | 18 | const a11yProps = (index) => { 19 | return { 20 | id: `profile-tab-${index}`, 21 | }; 22 | }; 23 | 24 | // const tabs = ['Performance', 'Exams Hosted', 'Exams Given']; 25 | const tabs = ['Exams Hosted', 'Exams Given']; 26 | 27 | const Details = () => { 28 | const [currentTab, setCurrentTab] = useState(0); 29 | 30 | const handleTabChange = (event, newValue) => setCurrentTab(newValue); 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 | {tabs.map((x, i) => ( 38 | 39 | ))} 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default Details; 54 | -------------------------------------------------------------------------------- /src/pages/profile/exams-list.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { 3 | Box, 4 | Card, 5 | CardActions, 6 | CardContent, 7 | Button, 8 | Typography, 9 | Paper, 10 | Grid, 11 | } from '@mui/material'; 12 | import { useSnackbar } from 'notistack'; 13 | import TextInputField from 'components/text-input-field'; 14 | import DatePicker from 'components/date-time-picker/date'; 15 | import TimePicker from 'components/date-time-picker/time'; 16 | import MultiSelect from 'components/multi-select-dropdown'; 17 | import DropdownField from 'components/dropdown-field'; 18 | import FilterListIcon from '@mui/icons-material/FilterList'; 19 | import { 20 | DialogActions, 21 | DialogContent, 22 | DialogTitle, 23 | Dialog, 24 | } from 'components/dialog'; 25 | import ExamDetailsCard from 'components/exam-details-card'; 26 | import ExamDetailsCard2 from 'components/exam-details-card2'; 27 | import { getHostedExams, getGivenExams } from 'api/user'; 28 | 29 | const examVisibilities = [ 30 | { label: 'Public', value: 'public' }, 31 | { label: 'Private', value: 'private' }, 32 | ]; 33 | 34 | const examStatuses = ['Scheduled', 'Ongoing', 'Finished']; 35 | 36 | const ExamsList = (props) => { 37 | const { type } = props; 38 | const { enqueueSnackbar } = useSnackbar(); 39 | const [showFilters, setShowFilters] = useState(false); 40 | const [selectedFilters, setSelectedFilters] = useState({ 41 | startDate: null, 42 | examVisibility: null, 43 | examStatus: null, 44 | }); 45 | const [exams, setExams] = useState([]); 46 | 47 | useEffect(() => { 48 | fetchExams(); 49 | }, []); 50 | 51 | const fetchExams = async () => { 52 | try { 53 | let res; 54 | if (type === 'hosted') res = await getHostedExams(); 55 | if (type === 'given') res = await getGivenExams(); 56 | setExams(res.data.data); 57 | } catch (err) { 58 | enqueueSnackbar(err.message, { variant: 'error' }); 59 | } 60 | }; 61 | 62 | const handleFilterChange = (e) => 63 | setSelectedFilters((f) => ({ ...f, [e.target.name]: e.target.value })); 64 | 65 | const hideFilters = () => setShowFilters(false); 66 | return ( 67 | <> 68 | 74 | Filters 75 | 76 | 77 | 78 | handleFilterChange(e)} 84 | /> 85 | 86 | 87 | handleFilterChange(e)} 94 | /> 95 | 96 | 97 | handleFilterChange(e)} 104 | /> 105 | 106 | 107 | 115 | 118 | 119 | 120 | 121 | 122 | } 126 | endIconOnClick={() => setShowFilters(true)} 127 | /> 128 | 135 | {/* {exams.map((exam, i) => ( 136 | 144 | ))} */} 145 | {exams.map((exam, i) => ( 146 | 152 | ))} 153 | 154 | 155 | 156 | ); 157 | }; 158 | 159 | export default ExamsList; 160 | -------------------------------------------------------------------------------- /src/pages/profile/profile-section.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useHistory, useParams } from 'react-router-dom'; 3 | import { useSelector } from 'react-redux'; 4 | import { Grid, Typography, Button, Box, TextField, Modal, Container, Paper, Tabs, Tab } from '@mui/material'; 5 | import { makeStyles, useTheme } from '@mui/styles'; 6 | import { DialogActions, DialogContent, DialogTitle, Dialog } from 'components/dialog'; 7 | import TextInputField from 'components/text-input-field'; 8 | import CropImageDialog from './crop-image'; 9 | import EditIcon from '@mui/icons-material/Edit'; 10 | import PhoneEnabledIcon from '@mui/icons-material/PhoneEnabled'; 11 | import UserIcon from 'assets/icons/user.png'; 12 | import Visibility from '@mui/icons-material/Visibility'; 13 | import VisibilityOff from '@mui/icons-material/VisibilityOff'; 14 | 15 | const useStyles = makeStyles((theme) => ({ 16 | name: { 17 | fontSize: '2.5rem', 18 | fontWeight: 600, 19 | color: theme.palette.primary.main, 20 | marginTop: '1rem', 21 | }, 22 | title: { 23 | fontWeight: '700', 24 | fontSize: '1.4rem', 25 | }, 26 | subTitleKey: { 27 | fontSize: '1.5rem', 28 | fontWeight: 500, 29 | color: theme.palette.primary.main, 30 | }, 31 | subTitleValue: { 32 | fontSize: '1.5rem', 33 | fontWeight: 500, 34 | width: '100%', 35 | }, 36 | labelSmall: { 37 | fontSize: '1.5rem', 38 | }, 39 | profileImage: { 40 | borderRadius: '50%', 41 | width: 150, 42 | height: 150, 43 | }, 44 | })); 45 | 46 | const types = ['image/png', 'image/jpeg', 'image/jpg']; 47 | 48 | const Profile = () => { 49 | const classes = useStyles(); 50 | 51 | const { user } = useSelector((state) => state.auth); 52 | 53 | const [imageCropped, setimageCropped] = useState(null); 54 | const [fileselect, setfileselect] = useState(null); 55 | const [editDialogOpen, setEditDialogOpen] = useState(false); 56 | const [imageDialogOpen, setImageDialogOpen] = useState(false); 57 | const [showPassword, setShowPassword] = useState(false); 58 | 59 | useEffect(() => { 60 | if (fileselect) setImageDialogOpen(true); 61 | }, [fileselect]); 62 | 63 | const handleClickShowPassword = () => { 64 | setShowPassword((s) => !s); 65 | }; 66 | 67 | const handleEditDialogOpen = () => setEditDialogOpen(true); 68 | const handleEditDialogClose = () => setEditDialogOpen(false); 69 | 70 | const handleChange = (e) => { 71 | let selectedFile = e.target.files[0]; 72 | setfileselect(URL.createObjectURL(selectedFile)); 73 | }; 74 | 75 | return ( 76 | <> 77 | {/* ----------------------------- EDIT PROFILE MODAL PART ---------------------------------------------- */} 78 | 79 | 80 | Edit Profile 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | : } 124 | endIconOnClick={handleClickShowPassword} 125 | /> 126 | 127 | 134 | 137 | 138 | 139 | 140 | 141 | 142 | {/* -------------------------------------------------------------------------------------- */} 143 | 144 | 145 | 154 | 163 | 164 | 165 | 173 | 176 | 177 | 178 | 179 | {user.name} 180 | 181 | 182 | {user.email} 183 | 184 | 185 | 186 | 187 | Bio: 188 | 189 | 190 | {user.bio} 191 | 192 | 193 | Gender: 194 | 195 | 196 | {user.gender} 197 | 198 | 199 | School/College: 200 | 201 | 202 | {user.institution} 203 | 204 | 205 | Phone: 206 | 207 | 208 | {user.phoneNumber} 209 | 210 | 211 | Date of Birth: 212 | 213 | 214 | {new Date(user.dob)?.toDateString()} 215 | 216 | 217 | Address: 218 | 219 | 220 | {user.address} 221 | 222 | 223 | 224 | 225 | 228 | 229 | 230 | 231 | ); 232 | }; 233 | 234 | export default Profile; 235 | -------------------------------------------------------------------------------- /src/redux/slices/auth.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | user: null, 5 | }; 6 | 7 | export const authSlice = createSlice({ 8 | name: 'auth', 9 | initialState, 10 | reducers: { 11 | setUserAndToken: (state, action) => { 12 | // Redux Toolkit allows us to write "mutating" logic in reducers. It 13 | // doesn't actually mutate the state because it uses the Immer library, 14 | // which detects changes to a "draft state" and produces a brand new 15 | // immutable state based off those changes 16 | if (action.payload.user) state.user = action.payload.user; 17 | if (action.payload.token) localStorage.setItem('AUTH_TOKEN', action.payload.token); 18 | }, 19 | clearUserAndToken: (state) => { 20 | state.user = null; 21 | localStorage.removeItem('AUTH_TOKEN'); 22 | }, 23 | }, 24 | }); 25 | 26 | // Action creators are generated for each case reducer function 27 | export const { setUserAndToken, clearUserAndToken } = authSlice.actions; 28 | 29 | export default authSlice.reducer; 30 | -------------------------------------------------------------------------------- /src/redux/slices/confirmation-dialog.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | const initialState = { 4 | open: false, 5 | title: '', 6 | content: '', 7 | primaryBtnText: '', 8 | secondaryBtnText: '', 9 | onPrimaryBtnClick: () => {}, 10 | onSecondaryBtnClick: () => {}, 11 | }; 12 | 13 | export const confirmationSlice = createSlice({ 14 | name: 'confirmation-dialog', 15 | initialState, 16 | reducers: { 17 | showConfirmation: (state, action) => { 18 | Object.keys(initialState).forEach((key) => (state[key] = action.payload[key] || initialState[key])); 19 | state.open = true; 20 | }, 21 | hideConfirmation: (state) => { 22 | state.open = false; 23 | // Object.keys(initialState).forEach((key) => (state[key] = initialState[key])); // Dialog close animation lags 24 | }, 25 | }, 26 | }); 27 | 28 | export const { showConfirmation, hideConfirmation } = confirmationSlice.actions; 29 | 30 | export default confirmationSlice.reducer; 31 | -------------------------------------------------------------------------------- /src/redux/slices/dialog-visibility.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const dialogNames = { 4 | login: 'login', 5 | register: 'register', 6 | forgotPassword: 'forgotPassword', 7 | registerReminder: 'registerReminder', 8 | }; 9 | 10 | const createInitialState = (namesObj) => { 11 | const obj = {}; 12 | Object.values(namesObj).map((val) => { 13 | obj[val] = false; 14 | }); 15 | return obj; 16 | }; 17 | 18 | export const dialogVisibilitySlice = createSlice({ 19 | name: 'dialogVisibility', 20 | initialState: createInitialState(dialogNames), 21 | reducers: { 22 | enableVisibility: (state, action) => { 23 | const key = action.payload; 24 | state[key] = true; 25 | }, 26 | hideVisibility: (state, action) => { 27 | const key = action.payload; 28 | state[key] = false; 29 | }, 30 | }, 31 | }); 32 | 33 | // Action creators are generated for each case reducer function 34 | export const { enableVisibility, hideVisibility } = dialogVisibilitySlice.actions; 35 | 36 | export default dialogVisibilitySlice.reducer; 37 | -------------------------------------------------------------------------------- /src/redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import auth from './slices/auth'; 3 | import dialogVisibility from './slices/dialog-visibility'; 4 | import confirmationDialog from './slices/confirmation-dialog'; 5 | export default configureStore({ 6 | reducer: { 7 | auth, 8 | dialogVisibility, 9 | confirmationDialog, 10 | }, 11 | middleware: (getDefaultMiddleware) => 12 | getDefaultMiddleware({ 13 | serializableCheck: false, 14 | }), 15 | }); 16 | -------------------------------------------------------------------------------- /src/utilities/functions.js: -------------------------------------------------------------------------------- 1 | 2 | export const csvToJSON = (csv) => { 3 | let lines = csv.split('\n'); 4 | let result = []; 5 | let headers = lines[0].split(','); 6 | for (let i = 1; i < lines.length; i++) { 7 | if (!lines[i]) continue; 8 | let obj = {}; 9 | let currentline = lines[i].split(','); 10 | for (let j = 0; j < headers.length; j++) { 11 | obj[headers[j].trim()] = currentline[j].trim(); 12 | } 13 | result.push(obj); 14 | } 15 | return result; 16 | }; 17 | 18 | export const getUnitsFromDuration = (duration) => { 19 | duration = duration / 1000; 20 | const days = Math.floor(duration / 86400); 21 | duration -= days * 86400; 22 | const hours = Math.floor(duration / 3600) % 24; 23 | duration -= hours * 3600; 24 | const minutes = Math.floor(duration / 60) % 60; 25 | duration -= minutes * 60; 26 | const seconds = Math.floor(duration % 60); 27 | return { days, hours, minutes, seconds }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/utilities/theme.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const PRIMARY_COLOR = '#1ab273'; 4 | const SECONDARY_COLOR = '#ffd670'; 5 | 6 | const getDesignTokens = (mode) => ({ 7 | breakpoints: { 8 | values: { 9 | xs: 0, 10 | sm: 480, 11 | md: 768, 12 | lg: 1024, 13 | xl: 1400, 14 | }, 15 | }, 16 | typography: { 17 | htmlFontSize: 10, 18 | fontFamily: "'Inter', sans-serif", 19 | }, 20 | palette: { 21 | mode, 22 | common: { 23 | white: '#ffffff', 24 | black: '#000000', 25 | }, 26 | ...(mode === 'light' 27 | ? { 28 | primary: { 29 | main: PRIMARY_COLOR, 30 | contrastText: '#fff', 31 | grey: '#bab5b5', 32 | }, 33 | text: { 34 | primary: '#000', 35 | secondary: '#555', 36 | }, 37 | divider: PRIMARY_COLOR, 38 | background: { 39 | default: '#fff', 40 | }, 41 | } 42 | 43 | : 44 | 45 | { 46 | primary: { 47 | main: SECONDARY_COLOR, 48 | contrastText: '#000', 49 | grey: '#bab5b5', 50 | }, 51 | text: { 52 | primary: '#fff', 53 | secondary: '#ddd', 54 | }, 55 | divider: SECONDARY_COLOR, 56 | background: { 57 | default: '#111', 58 | paper: '#111', 59 | }, 60 | }), 61 | }, 62 | }); 63 | 64 | 65 | export default getDesignTokens; 66 | --------------------------------------------------------------------------------