Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Modules/CandidateAssessment/CandidateAssessment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ const CandidateAssessment = () => {
const userData = {
emailId: values.email,
name: values.name,
};
examId: examId
};
createCandidate(userData)
.then((candidateData: Candidate) => {
setLoading(false);
Expand Down
8 changes: 7 additions & 1 deletion src/Modules/CandidateAssessment/QuestionsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -78,7 +84,7 @@ const ChallengesListComponent = () => {
<Title level={2}>Welcome to the Assessment</Title>
<div className="flex-container justify-between">
<Title level={4}>Instructions</Title>
<Timer timeLeft={timeLeft} />
<Timer timeLeft={timeLeft} onTimeout={handleTimeout}/>
</div>
<p>
Welcome to the coding test for your interview! Please select a challenge from the list below to begin.
Expand Down
33 changes: 28 additions & 5 deletions src/Modules/CandidateAssessment/components/Timer.tsx
Original file line number Diff line number Diff line change
@@ -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<IProps> = (props: IProps) => {
Expand All @@ -15,26 +17,47 @@ const Timer: React.FC<IProps> = (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 (
<Title level={4}>
<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>
</Title>
);
Expand Down
4 changes: 4 additions & 0 deletions src/Modules/CandidateAssessment/styles/Assessment.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
29 changes: 26 additions & 3 deletions src/Modules/Exam/ExamDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,18 @@ import { ExamAPIService } from './services/Exam.API';
import { ROUTES } from '../../constants/Route.constants';

export type ChallengeResult = Awaited<ReturnType<typeof ChallengeAPIService.getAll>>;
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<ChallengeResult>([]);
const [time, setTime] = useState<Time>({hr: ~~(exam.duration/60), min: exam.duration%60})
// const [duration, setDuration] = useState(exam?.duration || 0);
const [selectedChallenges, setSelectedChallenges] = useState<Set<string>>(new Set());
const [loading, setLoading] = useState<boolean>(true);
const [saveLoading, setSaveLoading] = useState<boolean>(false);
Expand All @@ -44,8 +50,12 @@ const ExamDetail = () => {
});
};

const handleSettingsChange = () => {
return null;
const handleSettingsChange = (value) => {
const Timeduration = value.split(" ");
setTime({
...time,
[Timeduration[1]] : Number(Timeduration[0])
})
};

const fetchChallenges = async () => {
Expand Down Expand Up @@ -73,15 +83,28 @@ const ExamDetail = () => {
[...challengesFromExam].every((challengeId) => selectedChallenges.has(challengeId))
);
const hasNameChanged = name !== exam.name;
const hasDurationChanged = ((time.hr*60) + time.min) !== exam.duration;
if (hasChallengesChanged) await ExamAPIService.updateExamChallenges(exam.id, selectedChallenges);
if (hasNameChanged) await saveExamName(name);
if(hasDurationChanged) await saveDuration((time.hr*60) + time.min);
} catch (error) {
toast.error(error?.message || 'Error saving exam');
} finally {
setSaveLoading(false);
}
};

const saveDuration = async (duration: number) => {
try {
await ExamAPIService.update({
id: exam.id,
duration
})
} catch (error) {
toast.error(error?.message || 'Error saving exam');
}
}

const saveExamName = async (name: string) => {
try {
await ExamAPIService.update({
Expand Down Expand Up @@ -149,7 +172,7 @@ const ExamDetail = () => {
</TabPane>

<TabPane tab="Settings" key="2">
<ExamSettings onDelete={onDelete} onChange={handleSettingsChange} />
<ExamSettings onDelete={onDelete} duration={time} addDuration={handleSettingsChange} />
</TabPane>
</Tabs>
</div>
Expand Down
3 changes: 3 additions & 0 deletions src/Modules/Exam/ExamList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ const ExamList = () => {
<Col span={6}>
<Statistic title="Challenges" value={exam.challenge.length} />
</Col>
<Col span={6}>
<Statistic title="Duration" value={exam.duration? `${~~(exam.duration/60)}hr ${exam.duration%60}mins`: 0} />
</Col>
</Row>
<Divider></Divider>
<Row className="actions-container">
Expand Down
27 changes: 11 additions & 16 deletions src/Modules/Exam/components/ExamSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,25 @@ import { Button, Form, Layout, Popconfirm, Select, Space } from 'antd';

const { Option } = Select;

const handleTimeLimitChange = (value) => {
// Handle the selected time limit
console.log(value);
};

const ExamSettings = ({ onChange, onDelete }) => {
const ExamSettings = ({ addDuration, onDelete, duration }) => {

return (
<Layout style={{ padding: '24px' }}>
<Form>
<Form.Item label="Time limit">
<Space direction="horizontal">
<Select placeholder="No hour limit" style={{ width: 180 }} onChange={handleTimeLimitChange}>
<Option value="0">0 hr</Option>
<Option value="1">1 hr</Option>
<Option value="2">2 hr</Option>
<Option value="3">3 hr</Option>
<Select placeholder="No hour limit" value={`${duration.hr} hr`} style={{ width: 180 }} onChange={addDuration}>
<Option value="0 hr">0 hr</Option>
<Option value="1 hr">1 hr</Option>
<Option value="2 hr">2 hr</Option>
<Option value="3 hr">3 hr</Option>
{/* Add more options as needed */}
</Select>
<Select placeholder="No minute limit" style={{ width: 180 }} onChange={handleTimeLimitChange}>
<Option value="00">00</Option>
<Option value="15">15</Option>
<Option value="30">30</Option>
<Option value="45">45</Option>
<Select placeholder="No minute limit" value={`${duration.min} min`} style={{ width: 180 }} onChange={addDuration}>
<Option value="00 min">00 min</Option>
<Option value="15 min">15 min</Option>
<Option value="30 min">30 min</Option>
<Option value="45 min">45 min</Option>
{/* Add more options as needed */}
</Select>
</Space>
Expand Down
1 change: 1 addition & 0 deletions src/Modules/Exam/services/Exam.API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
14 changes: 13 additions & 1 deletion src/Modules/common/CodeEditor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -183,6 +188,10 @@ const Editor = ({ challenge, assessment, candidate }: IProps) => {
setSubmitLoading(false);
};

const handleTimeout = ()=>{
handleSubmit();
}

useAutosave({ data: code, onSave: saveCode, interval: 1000 });

return (
Expand Down Expand Up @@ -213,7 +222,10 @@ const Editor = ({ challenge, assessment, candidate }: IProps) => {
/>
</Pane>
<Pane>
<div className="output-container" style={{ padding: '1rem' }}>
{expiry && <div style={{padding: '0 2rem'}}>
<Timer timeLeft={timeLeft} onTimeout={handleTimeout}/>
</div>}
<div className="output-container" style={{ padding: '0 1rem' }}>
<Output
output={output}
runLoading={runLoading}
Expand Down
8 changes: 7 additions & 1 deletion src/types/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,23 @@ export type Database = {
id: string
name: string | null
token: string | null
examId : string | null
}
Insert: {
created_at?: string | null
emailId?: string | null
id?: string
name?: string | null
token?: string | null
examId? : string | null
}
Update: {
created_at?: string | null
emailId?: string | null
id?: string
name?: string | null
token?: string | null
examId? : string | null
}
Relationships: []
}
Expand Down Expand Up @@ -139,18 +142,21 @@ export type Database = {
created_by: string | null
id: string
name: string | null
duration: number | null
}
Insert: {
created_at?: string | null
created_by?: string | null
id?: string
name?: string | null
duration?: number | null
}
Update: {
created_at?: string | null
created_by?: string | null
id?: string
name?: string | null
duration?: number | null
}
Relationships: []
}
Expand All @@ -165,7 +171,7 @@ export type Database = {
challenge_id: string
created_at?: string | null
exam_id: string
id?: number
id?: number,
}
Update: {
challenge_id?: string
Expand Down
6 changes: 3 additions & 3 deletions supabase/functions/_shared/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ const { create, getNumericDate, verify } = djwt


const algorithm = "HS512"
export const createJWT = async (data) => {
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) => {
Expand Down
11 changes: 6 additions & 5 deletions supabase/functions/create-candidate/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down