63 | Next
64 | {theme.direction === 'rtl' ? : }
65 |
66 | }
67 | backButton={
68 |
72 | }
73 | />
74 | >
75 | );
76 | };
77 | export default Carousel;
78 |
--------------------------------------------------------------------------------
/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/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/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-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/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/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/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/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/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 | -
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 |
71 | );
72 | })}
73 |
74 | )}
75 |
76 |
77 |
80 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default Page3;
89 |
--------------------------------------------------------------------------------
/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 |
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/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/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/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 |
128 | );
129 | }
130 |
--------------------------------------------------------------------------------
/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/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 |
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/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/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/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/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/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 | '',
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/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 |
188 | );
189 | };
190 |
191 | export default Register;
192 |
--------------------------------------------------------------------------------
/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/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 |
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/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 |
37 | {value === index && (
38 |
39 | {children}
40 |
41 | )}
42 |
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 |
--------------------------------------------------------------------------------