Skip to content
Draft
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
2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
"antd": "^5.8.5",
"autoprefixer": "^10.4.15",
"axios": "^1.5.0",
"clsx": "^2.1.1",
"eslint": "^8.48.0",
"eslint-config-next": "13.4.19",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "^7.33.2",
"lucide-react": "^0.379.0",
"next": "13.4.19",
"postcss": "^8.4.29",
"postcss-nesting": "^12.0.1",
Expand Down
Binary file added client/public/images/DoubleChatBubble.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/public/images/InterviewifyLogo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions client/src/app/(main)/(interview)/components/InterviewPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use client"
import React from "react";
import Image from "next/image";
import { useInterviewContext } from "@/app/contexts/interview-context";

const ChatBubble = '/images/DoubleChatBubble.png';

type InterviewPromptProps = {
setPrompt: React.Dispatch<React.SetStateAction<boolean>>;
}

const InterviewPrompt = ({ setPrompt } : InterviewPromptProps) => {
const { interview } = useInterviewContext();

return (
<div className="w-10/12 lg:w-8/12 xl:w-5/12 p-6 rounded-xl bg-white border-accent/10 border-2">
<div className="w-full h-full flex flex-col gap-6">
<div className="flex flex-col gap-3 text-base font-redHatText">
<Image src={ChatBubble} width={36} height={30} alt="chat bubble icon" />
<p className="text-xl font-urbanist font-semibold text-accent">Behavorial Interview Practice</p>
</div>
<p className="font-medium text-accent/80">This interview will be for a <span className="text-secondary">{interview?.profession}</span></p>
<p className="font-medium text-accent/80">This interview will consist of <span className="text-secondary">{interview?.questionCount}</span> questions, each of which you will have <span className="text-secondary">two minutes</span> to answer. You will have one opportunity to redo your response if something unexpected occurs.</p>
<p className="font-medium text-accent/80">Please ensure that you grant <span className="text-secondary">microphone permission</span> to answer these questions when prompted.</p>
<hr className="w-full h-0.5 border-t-2 border-accent/10 border-dashed"></hr>
<button className="p-2 px-4 rounded-md bg-secondary/80 hover:bg-secondary w-fit" onClick={() => setPrompt(false)}><p className="font-redHatText font-medium text-base text-white">Start Interview</p></button>
</div>
</div>
)
}

export default InterviewPrompt;
98 changes: 98 additions & 0 deletions client/src/app/(main)/(interview)/components/interviewQuestion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { CirclePlay, CircleStop, Mic, MicOff } from 'lucide-react';
import clsx from 'clsx';
import { ReactMic } from 'react-mic';

type InterviewQuestionProps = {
current: boolean;
question: string;
setBlobs: React.Dispatch<React.SetStateAction<any[]>>;
started: boolean;
startResponse: () => void;
nextQuestion: () => void;
};

type StartButtonProps = {
startResponse: () => void;
}

const StartButton = ( { startResponse } : StartButtonProps ) => {
return (
<button
className="p-2 px-4 rounded-md bg-secondary/80 hover:bg-secondary transition-colors h-10 w-48 flex items-center justify-center gap-3"
onClick={() => startResponse()}
>
<CirclePlay className='w-5 h-5 text-white' />
<p className="font-redHatText font-medium text-base text-white">
Start Response
</p>
</button>
);
};

type DuringButtonsProps = {
muted: boolean;
setMuted: React.Dispatch<React.SetStateAction<boolean>>;
nextQuestion: () => void;
}

// TODO: Parameters: muted, setMuted, submitBlob, RestartButton
const DuringButtons = ( { muted, setMuted, nextQuestion } : DuringButtonsProps ) => {
return <div className='flex items-center gap-4'>
{/* Mute Button */}
<button className='bg-transparent text-accent/60' onClick={() => setMuted(prev => !prev)}>
{muted ? <MicOff className='w-6 h-6' /> : <Mic className='w-6 h-6' />}
</button>
{/* Submit Button */}
<button
className="p-2 px-4 rounded-md bg-secondary/80 hover:bg-secondary h-10 w-48 flex items-center justify-center gap-3"
onClick={() => nextQuestion()}
>
<CircleStop className='w-4 h-4 text-white' />
<p className="font-redHatText font-medium text-base text-white">
Submit Response
</p>
</button>
</div>;
};

const InterviewQuestion = ({
current,
question,
setBlobs,
started,
startResponse,
nextQuestion
}: InterviewQuestionProps) => {
function handleAudio(recordedBlob: any) {
setBlobs((prevBlobs) => [...prevBlobs, recordedBlob.blob]);
}

const [muted, setMuted] = useState<boolean>(false);

return (
<div className="h-full w-full flex flex-col gap-8 items-center justify-center">
<p
className={clsx(
'font-urbanist text-3xl font-medium w-9/12 mx-auto text-center',
current ? 'text-accent' : 'text-accent/50',
)}
>
{question}
</p>
<ReactMic
record={started}
className="w-6/12 h-11"
visualSetting="sinewave"
onStop={handleAudio}
strokeColor="#1677ff"
backgroundColor="#F8F8FF"
/>
<div className="flex justify-center items-center">
{started ? <DuringButtons muted={muted} setMuted={setMuted} nextQuestion={nextQuestion} /> : <StartButton startResponse={startResponse} />}
</div>
</div>
);
};

export default InterviewQuestion;
70 changes: 70 additions & 0 deletions client/src/app/(main)/(interview)/components/mainInterview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React, { useState } from 'react';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import { Interview } from '../../../interfaces';
import InterviewQuestion from '../components/interviewQuestion';
import clsx from 'clsx';

type MainInterviewProps = {
started: boolean;
setBlobs: React.Dispatch<React.SetStateAction<any[]>>;
interview: Interview | null;
questionIndex: number;
startResponse: () => void;
nextQuestion: () => void;
};

const MainInterview = ({
started,
setBlobs,
interview,
questionIndex,
startResponse,
nextQuestion
}: MainInterviewProps) => {

if (!interview) {
return <div>Error with getting interview</div>;
}

return (
<div className="w-full h-full flex items-center justify-center">
<div className="h-full w-full flex justify-center items-center overflow-hidden">
<div
className="h-full w-full transition-transform"
style={{ transform: `translateY(-${100 * questionIndex}%)` }}
>
{interview?.questions.map((question) => {
// Make a interview component for each interview, give a set blob function through it so you can keep code clean
const current = question === interview.questions[questionIndex];
return (
<InterviewQuestion
current={current}
question={question}
setBlobs={setBlobs}
started={started}
startResponse={startResponse}
nextQuestion={nextQuestion}
/>
);
})}
</div>
{/* TO DO: Move to feedback page */}
{/* <div className="row-span-1 col-span-2 flex items-center justify-center">
<div className="flex flex-col h-20 w-10 rounded-lg border-2 border-accent/10">
<button className="h-3/6 w-full flex items-center justify-center" disabled={questionIndex === 0 ? true : false} onClick={(e) => {
e.preventDefault();
setQuestionIndex(questionIndex - 1)
}}><UpOutlined className={clsx("text-xs", questionIndex === 0 ? "text-accent/20" : "text-accent")} /></button>
<hr className="w-full h-0.5 bg-accent/10"></hr>
<button className="h-3/6 w-full flex items-center justify-center" disabled={questionIndex === interview.questions.length - 1 ? true : false} onClick={(e) => {
e.preventDefault();
setQuestionIndex(questionIndex + 1)
}}><DownOutlined className={clsx("text-xs", questionIndex === interview.questions.length - 1 ? "text-accent/20" : "text-accent")} /></button>
</div>
</div> */}
</div>
</div>
);
};

export default MainInterview;
28 changes: 28 additions & 0 deletions client/src/app/(main)/(interview)/components/questionBars.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { Interview } from '@/app/interfaces';
import clsx from 'clsx';

type QuestionBarsProps = {
interview: Interview | null,
questionIndex: number
}

const QuestionBars = ( { interview, questionIndex } : QuestionBarsProps ) => {
return (
<div className="flex items-center justify-center row-span-1 col-span-7">
<div
className={`w-full h-full flex items-center justify-center gap-3`}
>
{interview?.questions.map((question) => {
return (
<div className="w-44">
<div className={clsx("w-full h-[6px] rounded-full", interview.questions[questionIndex] == question ? "bg-accent" : "bg-accent/10")}></div>
</div>
);
})}
</div>
</div>
);
};

export default QuestionBars;
31 changes: 31 additions & 0 deletions client/src/app/(main)/(interview)/components/timer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { Clock } from 'lucide-react';

type TimerProps = {
time: number;
}

const Timer = ({ time } : TimerProps) => {

const timeConvert = (time: number) => {
if (time < 0) {
return '00';
}
if (time < 10) {
return '0' + String(time);
} else {
return String(time);
}
};

return (
<div className="flex items-center justify-end row-span-1 col-span-1 gap-2">
<Clock className="w-4 h-4 text-accent" />
<p className="text-base font-redHatText font-medium text-accent">
{timeConvert(Math.floor(time / 60))}:{timeConvert(time % 60)} <span className="text-accent/60">/ 2:00</span>
</p>
</div>
);
};

export default Timer;
Loading