diff --git a/src/Modules/CandidateAssessment/CandidateAssessment.tsx b/src/Modules/CandidateAssessment/CandidateAssessment.tsx index e0a0fc2..15f4fad 100644 --- a/src/Modules/CandidateAssessment/CandidateAssessment.tsx +++ b/src/Modules/CandidateAssessment/CandidateAssessment.tsx @@ -76,7 +76,8 @@ const CandidateAssessment = () => { const userData = { emailId: values.email, name: values.name, - }; + examId: examId + }; createCandidate(userData) .then((candidateData: Candidate) => { setLoading(false); diff --git a/src/Modules/CandidateAssessment/QuestionsPage.tsx b/src/Modules/CandidateAssessment/QuestionsPage.tsx index 0bff2c4..be302be 100644 --- a/src/Modules/CandidateAssessment/QuestionsPage.tsx +++ b/src/Modules/CandidateAssessment/QuestionsPage.tsx @@ -16,6 +16,7 @@ import CommonUtils from '../common/utils/Common.utils'; const { Title } = Typography; const ChallengesListComponent = () => { + const navigate = useNavigate(); const { examId, candidateId } = useParams(); const candidate = useSelector((state: IRootState) => state.candidate); const [loading, setLoading] = useState(true); @@ -69,6 +70,11 @@ const ChallengesListComponent = () => { } }; + const handleTimeout = ()=>{ + dispatch.assessment.clear(); + navigate(ROUTES.ASSESSMENT_OVER); + } + const expiry = (jwt_decode(candidate?.token) as { exp: number })?.exp; const now = Date.now() / 1000; const timeLeft = Math.round(expiry - now); @@ -78,7 +84,7 @@ const ChallengesListComponent = () => { Welcome to the Assessment
Instructions - +

Welcome to the coding test for your interview! Please select a challenge from the list below to begin. diff --git a/src/Modules/CandidateAssessment/components/Timer.tsx b/src/Modules/CandidateAssessment/components/Timer.tsx index 98500a5..46c27b1 100644 --- a/src/Modules/CandidateAssessment/components/Timer.tsx +++ b/src/Modules/CandidateAssessment/components/Timer.tsx @@ -1,9 +1,11 @@ import { Typography } from 'antd'; import { useState, useEffect } from 'react'; +import { toast } from 'react-toastify'; const { Title } = Typography; interface IProps { timeLeft: number; + onTimeout: () => void; } const Timer: React.FC = (props: IProps) => { @@ -15,26 +17,47 @@ const Timer: React.FC = (props: IProps) => { useEffect(() => { const timer = setInterval(() => { - setTimeLeft((prevTimeLeft) => prevTimeLeft - 1); + setTimeLeft((prevTimeLeft) => { + if (prevTimeLeft === 0) return 0; + return prevTimeLeft - 1; + }); }, 1000); return () => clearInterval(timer); }, []); + useEffect(() => { + const hours = Math.floor(timeLeft / 3600); + const minutes = Math.floor((timeLeft % 3600) / 60); + const seconds = timeLeft % 60; + + if (hours === 0 && minutes === 5 && seconds === 0) toast.warning('Only 5 minutes left'); + + if (hours === 0 && minutes === 1 && seconds === 0) toast.warning('Only 1 minutes left'); + + if(hours === 0 && minutes === 0 && seconds === 20) toast.warning("Assessment will be submitted automatically in 10 seconds"); + + if(hours === 0 && minutes === 0 && seconds === 10){ + props.onTimeout(); + toast.success("Assessment submitted successfully!") + } + + }, [timeLeft]); + const formatTime = (time: number) => { const hours = Math.floor(time / 3600); const minutes = Math.floor((time % 3600) / 60); const seconds = time % 60; - return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds + return `${hours.toString().padStart(2, '0')} : ${minutes.toString().padStart(2, '0')} : ${seconds .toString() .padStart(2, '0')}`; }; return ( - - <div className='flex-container row timer' > - Time Left <span>{formatTime(timeLeft)}</span> + <Title level={4} style={{ margin: 0, color: 'red' }}> + <div className="flex-container row timer"> + Time Left : <span>{formatTime(timeLeft)}</span> </div> ); diff --git a/src/Modules/CandidateAssessment/styles/Assessment.css b/src/Modules/CandidateAssessment/styles/Assessment.css index 89fc3fb..a4ea126 100644 --- a/src/Modules/CandidateAssessment/styles/Assessment.css +++ b/src/Modules/CandidateAssessment/styles/Assessment.css @@ -2,6 +2,10 @@ background-color: var(--lumel-brand-color) !important; } +.timer{ + color: var(--lumel-brand-color) !important; +} + .timer span { min-width: 100px; margin-left: 10px; diff --git a/src/Modules/Exam/ExamDetail.tsx b/src/Modules/Exam/ExamDetail.tsx index a234903..8c634ea 100644 --- a/src/Modules/Exam/ExamDetail.tsx +++ b/src/Modules/Exam/ExamDetail.tsx @@ -13,12 +13,18 @@ import { ExamAPIService } from './services/Exam.API'; import { ROUTES } from '../../constants/Route.constants'; export type ChallengeResult = Awaited>; +interface Time { + hr: number, + min: number +} const ExamDetail = () => { const { state } = useLocation(); const exam = state?.exam as ExamQueryResult[number]; const [name, setName] = useState(exam?.name || ''); const [challenges, setChallenges] = useState([]); + const [time, setTime] = useState

- + + + + {/* Add more options as needed */} - + + + + {/* Add more options as needed */} diff --git a/src/Modules/Exam/services/Exam.API.ts b/src/Modules/Exam/services/Exam.API.ts index b858966..08526fb 100644 --- a/src/Modules/Exam/services/Exam.API.ts +++ b/src/Modules/Exam/services/Exam.API.ts @@ -40,6 +40,7 @@ export class ExamAPIService { return data || null; } + static async create(exam: ExamInsertDto) { const { data, error } = await supabase.from('exam').insert(exam).select('*,challenge(*)'); if (error) { diff --git a/src/Modules/common/CodeEditor/Editor.tsx b/src/Modules/common/CodeEditor/Editor.tsx index a1d7d18..174a33d 100644 --- a/src/Modules/common/CodeEditor/Editor.tsx +++ b/src/Modules/common/CodeEditor/Editor.tsx @@ -29,6 +29,8 @@ import { ROUTES } from '../../../constants/Route.constants'; import './styles/Editor.css'; import { Typography } from 'antd'; import { invokeSupabaseFunction } from '../../API/APIUtils'; +import jwt_decode from 'jwt-decode'; +import Timer from '../../CandidateAssessment/components/Timer'; const { Title } = Typography; interface IProps { @@ -50,6 +52,9 @@ const Editor = ({ challenge, assessment, candidate }: IProps) => { const [saveLoading, setSaveLoading] = useState(false); const [testCaseLoading, setTestCaseLoading] = useState(false); const [lastSaved, setlastSaved] = useState(null); + const expiry = (jwt_decode(candidate?.token) as { exp: number })?.exp; + const now = Date.now() / 1000; + const timeLeft = Math.round(expiry - now); const evaluator = new CodeEvaluator( selectEditorLanguage.name, @@ -183,6 +188,10 @@ const Editor = ({ challenge, assessment, candidate }: IProps) => { setSubmitLoading(false); }; + const handleTimeout = ()=>{ + handleSubmit(); + } + useAutosave({ data: code, onSave: saveCode, interval: 1000 }); return ( @@ -213,7 +222,10 @@ const Editor = ({ challenge, assessment, candidate }: IProps) => { /> -
+ {expiry &&
+ +
} +
{ - const TWO_AND_A_HALF_HOURS = 60 * 60 * 2.5; - return await create({ alg: algorithm, typ: "JWT" }, { ...data, exp: getNumericDate(TWO_AND_A_HALF_HOURS) }, Deno.env.get('JWT_SECRET')) +export const createJWT = async (data, duration : number) => { + const DURATION = (duration !== 0 && duration !== null ? 60 * duration : 60 * 60 * 2); + return await create({ alg: algorithm, typ: "JWT" }, { ...data, exp: getNumericDate(DURATION) }, Deno.env.get('JWT_SECRET')) } export const verifyJWT = async (token) => { diff --git a/supabase/functions/create-candidate/index.ts b/supabase/functions/create-candidate/index.ts index 9f92a00..f03f6a4 100644 --- a/supabase/functions/create-candidate/index.ts +++ b/supabase/functions/create-candidate/index.ts @@ -9,21 +9,22 @@ serve(async (req) => { return new Response('ok', { headers: corsHeaders }) } - const { emailId, name } = await req.json(); - + const { emailId, name, examId } = await req.json(); + const candidate = { emailId, - name + name, }; + const supabase = createClient( Deno.env.get('SUPABASE_URL'), Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')) try { - - const token = await createJWT(candidate) + const {data : duration} = await supabase.from('exam').select('duration').eq('id', examId).single(); + const token = await createJWT(candidate, duration.duration) const response = await supabase .from('candidate')