diff --git a/client/package.json b/client/package.json index 32c4436..669c724 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/public/images/DoubleChatBubble.png b/client/public/images/DoubleChatBubble.png new file mode 100644 index 0000000..64243d2 Binary files /dev/null and b/client/public/images/DoubleChatBubble.png differ diff --git a/client/public/images/InterviewifyLogo.png b/client/public/images/InterviewifyLogo.png new file mode 100644 index 0000000..ffdf52d Binary files /dev/null and b/client/public/images/InterviewifyLogo.png differ diff --git a/client/src/app/(main)/(interview)/components/InterviewPrompt.tsx b/client/src/app/(main)/(interview)/components/InterviewPrompt.tsx new file mode 100644 index 0000000..e7acb57 --- /dev/null +++ b/client/src/app/(main)/(interview)/components/InterviewPrompt.tsx @@ -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>; +} + +const InterviewPrompt = ({ setPrompt } : InterviewPromptProps) => { + const { interview } = useInterviewContext(); + + return ( +
+
+
+ chat bubble icon +

Behavorial Interview Practice

+
+

This interview will be for a {interview?.profession}

+

This interview will consist of {interview?.questionCount} questions, each of which you will have two minutes to answer. You will have one opportunity to redo your response if something unexpected occurs.

+

Please ensure that you grant microphone permission to answer these questions when prompted.

+
+ +
+
+ ) +} + +export default InterviewPrompt; \ No newline at end of file diff --git a/client/src/app/(main)/(interview)/components/interviewQuestion.tsx b/client/src/app/(main)/(interview)/components/interviewQuestion.tsx new file mode 100644 index 0000000..02d43c1 --- /dev/null +++ b/client/src/app/(main)/(interview)/components/interviewQuestion.tsx @@ -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>; + started: boolean; + startResponse: () => void; + nextQuestion: () => void; +}; + +type StartButtonProps = { + startResponse: () => void; +} + +const StartButton = ( { startResponse } : StartButtonProps ) => { + return ( + + ); +}; + +type DuringButtonsProps = { + muted: boolean; + setMuted: React.Dispatch>; + nextQuestion: () => void; +} + +// TODO: Parameters: muted, setMuted, submitBlob, RestartButton +const DuringButtons = ( { muted, setMuted, nextQuestion } : DuringButtonsProps ) => { + return
+ {/* Mute Button */} + + {/* Submit Button */} + +
; +}; + +const InterviewQuestion = ({ + current, + question, + setBlobs, + started, + startResponse, + nextQuestion +}: InterviewQuestionProps) => { + function handleAudio(recordedBlob: any) { + setBlobs((prevBlobs) => [...prevBlobs, recordedBlob.blob]); + } + + const [muted, setMuted] = useState(false); + + return ( +
+

+ {question} +

+ +
+ {started ? : } +
+
+ ); +}; + +export default InterviewQuestion; diff --git a/client/src/app/(main)/(interview)/components/mainInterview.tsx b/client/src/app/(main)/(interview)/components/mainInterview.tsx new file mode 100644 index 0000000..cc9054d --- /dev/null +++ b/client/src/app/(main)/(interview)/components/mainInterview.tsx @@ -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>; + interview: Interview | null; + questionIndex: number; + startResponse: () => void; + nextQuestion: () => void; +}; + +const MainInterview = ({ + started, + setBlobs, + interview, + questionIndex, + startResponse, + nextQuestion +}: MainInterviewProps) => { + + if (!interview) { + return
Error with getting interview
; + } + + return ( +
+
+
+ {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 ( + + ); + })} +
+ {/* TO DO: Move to feedback page */} + {/*
+
+ +
+ +
+
*/} +
+
+ ); +}; + +export default MainInterview; diff --git a/client/src/app/(main)/(interview)/components/questionBars.tsx b/client/src/app/(main)/(interview)/components/questionBars.tsx new file mode 100644 index 0000000..6158d80 --- /dev/null +++ b/client/src/app/(main)/(interview)/components/questionBars.tsx @@ -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 ( +
+
+ {interview?.questions.map((question) => { + return ( +
+
+
+ ); + })} +
+
+ ); +}; + +export default QuestionBars; diff --git a/client/src/app/(main)/(interview)/components/timer.tsx b/client/src/app/(main)/(interview)/components/timer.tsx new file mode 100644 index 0000000..dfb592e --- /dev/null +++ b/client/src/app/(main)/(interview)/components/timer.tsx @@ -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 ( +
+ +

+ {timeConvert(Math.floor(time / 60))}:{timeConvert(time % 60)} / 2:00 +

+
+ ); +}; + +export default Timer; \ No newline at end of file diff --git a/client/src/app/(main)/(interview)/interview/page.tsx b/client/src/app/(main)/(interview)/interview/page.tsx new file mode 100644 index 0000000..0b2321c --- /dev/null +++ b/client/src/app/(main)/(interview)/interview/page.tsx @@ -0,0 +1,146 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import { useInterviewContext } from '../../../contexts/interview-context'; +import InterviewPrompt from '../components/InterviewPrompt'; +import Timer from '../components/timer'; +import QuestionBars from '../components/questionBars'; +import MainInterview from '../components/mainInterview'; +import axios from 'axios'; +import { useRouter } from 'next/navigation'; + +const Interview = () => { + const { interview } = useInterviewContext(); + const router = useRouter(); + + if (!interview) { + return
Error with getting interview
; + } + + const [prompt, setPrompt] = useState(true); + + // States for Question + const [questionIndex, setQuestionIndex] = useState(0); + const [started, setStarted] = useState(false); + + // States for timer + const [time, setTime] = useState(30); + const [intervalID, setIntervalID] = useState(null); + + // States for evaluation and audio + const [blobs, setBlobs] = useState([]); + const [evaluations, setEvaluations] = useState([]); + + useEffect(() => { + const saveToMongo = async () => { + const data = { + evaluations: evaluations, + position: interview.profession, + }; + const resp = await axios.post( + 'http://localhost:4000/mongo/save-interview', + data, + { + headers: { + 'Content-Type': 'application/json', + }, + withCredentials: true, + }, + ); + console.log('Success', evaluations, resp); + // router.push('/start/feedback'); + }; + if (evaluations.length == interview.questionCount) saveToMongo(); + }, [evaluations]); + + // TODO: Need to discuss ways to handle sending request -> redirect to new page -> receiving info + const evaluate = async () => { + const formData = new FormData(); + + let currIdx = blobs.length - 1; + formData.append('question', interview.questions[currIdx] as string); + formData.append('profession', interview.profession as string); + formData.append('audio', blobs[currIdx]); + const resp = await axios.post( + 'http://localhost:4000/gpt/evaluate', + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }, + ); + setEvaluations((prevEvals) => [...prevEvals, resp.data]); + }; + + // when blobs gets updated, evalaute new blob + useEffect(() => { + if (blobs.length > 0) evaluate(); + }, [blobs]); + + const startResponse = () => { + setStarted(true); + timer('start'); + }; + + const nextQuestion = () => { + setTime(30); + timer('end'); + setStarted(false); + if (questionIndex < interview?.questionCount - 1) + setQuestionIndex(questionIndex + 1); + }; + + const timer = (action: string) => { + if (action === 'start') { + const intId = setInterval(() => { + if (time > 0) { + setTime((prevTime) => prevTime - 1); + } else { + clearInterval(intId); + } + }, 1000); + setIntervalID(intId); + } else if (action === 'end') { + if (intervalID !== null) { + clearInterval(intervalID); + } + } + }; + + if (prompt === true) { + return ( +
+ +
+ ); + } + + return ( +
+
+
+

+ {interview?.profession}{' '} + Interview +

+
+
+ +
+ +
+
+ +
+
+ ); +}; + +export default Interview; diff --git a/client/src/app/(main)/(interview)/layout.tsx b/client/src/app/(main)/(interview)/layout.tsx new file mode 100644 index 0000000..80c7e4b --- /dev/null +++ b/client/src/app/(main)/(interview)/layout.tsx @@ -0,0 +1,25 @@ +"use client" +import React, { useEffect } from 'react'; +import { useInterviewContext } from "@/app/contexts/interview-context"; +import { useRouter } from "next/navigation"; + +export default function MainLayout({ + children, +}: { + children: React.ReactNode; +}) { + const { interview } = useInterviewContext(); + const router = useRouter(); + + useEffect(() => { + if (!interview) { + router.push('/dashboard'); + } + }, [interview, router]); + + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/client/src/app/(main)/components/numberQuestions.tsx b/client/src/app/(main)/components/numberQuestions.tsx new file mode 100644 index 0000000..4590913 --- /dev/null +++ b/client/src/app/(main)/components/numberQuestions.tsx @@ -0,0 +1,24 @@ +import clsx from "clsx"; +import React from "react"; + +const NumberQuestions = ({ numberQuestions, setNumberQuestions }) => { + const numberProps = [1, 2, 3, 4] + return ( +
+

Number of Questions

+
+ {numberProps.map((number) => { + return ( + + ) + })} +
+
+ ) +}; + +export default NumberQuestions; \ No newline at end of file diff --git a/client/src/app/(main)/components/professionDropdown.tsx b/client/src/app/(main)/components/professionDropdown.tsx new file mode 100644 index 0000000..71d7ee8 --- /dev/null +++ b/client/src/app/(main)/components/professionDropdown.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { ProfessionType } from '@/app/interfaces'; +import { MenuProps } from 'antd'; +import { DownOutlined, UserOutlined } from '@ant-design/icons'; +import { Button, Dropdown, Space } from 'antd'; + +type ScheduleDropdownProps = { + profession: ProfessionType, + setProfession: React.Dispatch> +} + +const ScheduleDropdown = ({ profession, setProfession } : ScheduleDropdownProps) => { + const handleMenuClick: MenuProps['onClick'] = (e) => { + // TO DO: Fix type error here + setProfession(e.key); + }; + + const items: MenuProps['items'] = [ + { + label: ProfessionType.SOFTWARE_ENGINEER, + key: ProfessionType.SOFTWARE_ENGINEER, + icon: , + }, + { + label: ProfessionType.DATA_SCIENTIST, + key: ProfessionType.DATA_SCIENTIST, + icon: , + }, + { + label: ProfessionType.PRODUCT_MANAGER, + key: ProfessionType.PRODUCT_MANAGER, + icon: , + }, + ]; + + const menuProps = { + items, + onClick: handleMenuClick, + }; + return ( +
+

Position

+ + + +
+ ); +}; + +export default ScheduleDropdown; diff --git a/client/src/app/(main)/components/scheduleInterview.tsx b/client/src/app/(main)/components/scheduleInterview.tsx new file mode 100644 index 0000000..70bd9aa --- /dev/null +++ b/client/src/app/(main)/components/scheduleInterview.tsx @@ -0,0 +1,45 @@ +import { VideoCameraOutlined, LoadingOutlined } from "@ant-design/icons"; +import React, { useState } from "react"; +import { useRouter } from "next/navigation"; +import ProfessionDropdown from "../components/professionDropdown"; +import NumberQuestions from "../components/numberQuestions"; +import { useInterviewContext } from "../../contexts/interview-context"; +import { ProfessionType } from "../../interfaces"; +import GetQuestions from "../../utils/get-questions"; + +const ScheduleInterview = () => { + const [isButtonLoading, setIsButtonLoading] = useState(false); + + const [profession, setProfession] = useState(ProfessionType.SOFTWARE_ENGINEER); + const [numberQuestions, setNumberQuestions] = useState(1); + const router = useRouter(); + const { setInterview } = useInterviewContext(); + + const handleCreateInterview = () => { + setIsButtonLoading(true); + const questions = GetQuestions(profession, numberQuestions); + setInterview({questionCount: numberQuestions, profession: profession, questions: questions}); + setTimeout(() => router.push('/interview'), 500); + } + + return ( +
+
+ +

Schedule Interview

+
+
+
+
+ + +
+ +
+
+ ) +}; + +export default ScheduleInterview; \ No newline at end of file diff --git a/client/src/app/(main)/components/sidenav.tsx b/client/src/app/(main)/components/sidenav.tsx new file mode 100644 index 0000000..b7d74ae --- /dev/null +++ b/client/src/app/(main)/components/sidenav.tsx @@ -0,0 +1,36 @@ +"use client" +import React from 'react'; +import Logo from '../../components/logo'; +import Link from 'next/link'; +import clsx from 'clsx'; +import { usePathname } from 'next/navigation'; +import { HomeOutlined, HistoryOutlined } from '@ant-design/icons'; + +const SideNav = () => { + const pathName = usePathname(); + + return ( +
+
+ +
+

DASHBOARD

+ +
+ +

Home

+
+ + +
+ +

History

+
+ +
+
+
+ ) +} + +export default SideNav; \ No newline at end of file diff --git a/client/src/app/(main)/dashboard/page.tsx b/client/src/app/(main)/dashboard/page.tsx new file mode 100644 index 0000000..eb79545 --- /dev/null +++ b/client/src/app/(main)/dashboard/page.tsx @@ -0,0 +1,57 @@ +"use client" +import React from 'react'; +import { useEffect, useState } from 'react'; +import axios from 'axios'; +import { LoadingOutlined } from '@ant-design/icons'; +import { useRouter } from 'next/navigation'; +import { User } from '../../interfaces'; +import ScheduleInterview from '../components/scheduleInterview'; + +const Dashboard = () => { + const [user, setUser] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); + + useEffect(() => { + const getUser = async () => { + try { + const res = await axios.get( + 'http://localhost:4000/auth/login/success', + { + withCredentials: true, + }, + ); + if(res.data.user) { + setUser(res.data.user) + } else { + // TO DO: error handling for user + router.push('/') + } + } catch (error) { + console.log(error); + } + }; + setIsLoading(true); + getUser().then(() => setTimeout(() => {setIsLoading(false)}, 500)); + }, []); + + if(isLoading === true && user === null){ + return ( +
+ +
+ ) + } + + return ( +
+
+

Hello, {user?.username} 👋

+

Keep pushing forward! Every practice session brings you one step closer to acing your next interview.

+
+ +
+ ) +} + +export default Dashboard; \ No newline at end of file diff --git a/client/src/app/(main)/layout.tsx b/client/src/app/(main)/layout.tsx new file mode 100644 index 0000000..02404ba --- /dev/null +++ b/client/src/app/(main)/layout.tsx @@ -0,0 +1,53 @@ +"use client" +import React, { useEffect, useState } from 'react'; +import SideNav from './components/sidenav' +import axios from 'axios'; +import { useRouter } from 'next/navigation'; +import { LoadingOutlined } from '@ant-design/icons'; +import ThemeContextProvider from '../contexts/interview-context'; + +export default function MainLayout({ + children, +}: { + children: React.ReactNode; +}) { + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); + + useEffect(() => { + const getUser = async () => { + try { + const res = await axios.get( + 'http://localhost:4000/auth/login/success', + { + withCredentials: true, + }, + ); + if(res.data.user) { + setIsLoading(false); + } else { + router.push('/') + } + } catch (error) { + console.log(error); + router.push('/') + } + }; + getUser(); + }, []); + + if(isLoading){ + return
+ } + + return ( +
+ +
+ + {children} + +
+
+ ); +} \ No newline at end of file diff --git a/client/src/app/(welcome)/layout.tsx b/client/src/app/(welcome)/layout.tsx new file mode 100644 index 0000000..0a90420 --- /dev/null +++ b/client/src/app/(welcome)/layout.tsx @@ -0,0 +1,43 @@ +'use client'; +import axios from 'axios'; +import React from 'react'; +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { LoadingOutlined } from "@ant-design/icons" + +export default function Layout({ children }: React.PropsWithChildren<{}>) { + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); + + useEffect(() => { + const getUser = async () => { + try { + const res = await axios.get( + 'http://localhost:4000/auth/login/success', + { + withCredentials: true, + }, + ); + if(res.data.user) { + router.push('/dashboard'); + } else { + setIsLoading(false); + } + } catch (error) { + console.log(error); + setIsLoading(false); + } + }; + getUser(); + }, []); + + if(isLoading){ + return
+ } + + return ( +
+ {children} +
+ ); +} diff --git a/client/src/app/welcome/page.tsx b/client/src/app/(welcome)/page.tsx similarity index 100% rename from client/src/app/welcome/page.tsx rename to client/src/app/(welcome)/page.tsx diff --git a/client/src/app/components/logo.tsx b/client/src/app/components/logo.tsx new file mode 100644 index 0000000..a4fb64e --- /dev/null +++ b/client/src/app/components/logo.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import Image from 'next/image'; + +const LogoSVG = '/images/InterviewifyLogo.png'; + +const Logo = () => { + return ( +
+ Interviewify logo +
+ ) +} + +export default Logo; \ No newline at end of file diff --git a/client/src/app/contexts/interview-context.tsx b/client/src/app/contexts/interview-context.tsx new file mode 100644 index 0000000..940af76 --- /dev/null +++ b/client/src/app/contexts/interview-context.tsx @@ -0,0 +1,33 @@ +"use client" +import React, { createContext, useContext, useState } from "react"; +import { Interview } from "../interfaces"; + +type InterviewContextProviderProps = { + children: React.ReactNode; +}; + +type InterviewContext = { + interview: Interview | null; + setInterview: React.Dispatch>; +} + +export const InterviewContext = createContext(null); + +export default function ThemeContextProvider({ + children +} : InterviewContextProviderProps) { + const [interview, setInterview] = useState(null); + return ( + + {children} + + ) +} + +export function useInterviewContext(){ + const interview = useContext(InterviewContext); + if (interview === null) { + throw new Error("useInterviewContext must be used within a InterviewContextProvider"); + } + return interview; +} \ No newline at end of file diff --git a/client/src/app/globals.css b/client/src/app/globals.css index 3235482..1a32ba0 100644 --- a/client/src/app/globals.css +++ b/client/src/app/globals.css @@ -1,12 +1,19 @@ -/* styles/tailwind.css */ +@import url('https://fonts.googleapis.com/css2?family=Red+Hat+Text:ital,wght@0,300..700;1,300..700&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap'); + @tailwind base; @tailwind components; @tailwind utilities; @import './styles/loading.css'; -@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@500&display=swap'); :root { + /* Primary is used for the background color (lighter color) */ + --primary-color: 248 248 255; + /* Secondary is used for components that require attention (buttons, sections, etc) */ + --secondary-color: 55 114 255; + /* Accent is primarily for text */ + --accent-color: 18 18 24; + --scrollbar-width: 0px; --angleNormal: -3deg; diff --git a/client/src/app/interfaces.ts b/client/src/app/interfaces.ts index f5e83be..9fe732f 100644 --- a/client/src/app/interfaces.ts +++ b/client/src/app/interfaces.ts @@ -5,6 +5,12 @@ export interface User { _id: string; } +export interface Interview { + questionCount: number; + profession: ProfessionType; + questions: string[]; +} + export enum ProfessionType { ALL = 'all', SOFTWARE_ENGINEER = 'Software Engineer', diff --git a/client/src/app/loading.tsx b/client/src/app/loading.tsx deleted file mode 100644 index 461d392..0000000 --- a/client/src/app/loading.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Spin } from 'antd'; -import React from 'react'; -import { LoadingOutlined } from '@ant-design/icons'; - -export default function Loading() { - // You can add any UI inside Loading, including a Skeleton. - return ( -
- } - size="large" - /> -
- ); -} diff --git a/client/src/app/page.tsx b/client/src/app/page.tsx deleted file mode 100644 index 92e8123..0000000 --- a/client/src/app/page.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; -import axios from 'axios'; -import React from 'react'; -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import Loading from './components/loading'; - -export default function Home() { - const [user, setUser] = useState(false); - const router = useRouter(); - - useEffect(() => { - const getUser = async () => { - try { - const res = await axios.get( - 'http://localhost:4000/auth/login/success', - { - withCredentials: true, - }, - ); - setUser(res.data.user); - if (res.data.user) { - router.push('start/dashboard'); - } - } catch (error) { - console.log(error); - } - }; - - getUser(); - if (user) { - router.push('/dashboard'); - } else { - router.push('/welcome'); - } - }, []); - - return ; -} diff --git a/client/src/app/start/dashboard/page.tsx b/client/src/app/start/dashboard/page.tsx deleted file mode 100644 index 25ff264..0000000 --- a/client/src/app/start/dashboard/page.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client'; -import { useState } from 'react'; -import React from 'react'; -import { Button, Dropdown, Space } from 'antd'; -import { DownOutlined, UserOutlined } from '@ant-design/icons'; -import { MenuProps } from 'antd'; -import { useRouter } from 'next/navigation'; -import { ProfessionType } from '@/app/interfaces'; - -export default function Dashboard() { - const [profession, setProfession] = useState('Software Engineer'); - const [numberQuestions, setNumberQuestions] = useState(1); - const router = useRouter(); - - const handleMenuClick: MenuProps['onClick'] = (e) => { - setProfession(e.key); - }; - - const handleNumberClick: MenuProps['onClick'] = (e) => { - setNumberQuestions(parseInt(e.key)); - }; - - const startInterview = () => { - router.push( - `/start/interview?profession=${profession}&numberQs=${numberQuestions}`, - ); - }; - - const items: MenuProps['items'] = [ - { - label: ProfessionType.SOFTWARE_ENGINEER, - key: 'Software Engineer', - icon: , - }, - { - label: ProfessionType.DATA_SCIENTIST, - key: 'Data Scientist', - icon: , - }, - { - label: ProfessionType.PRODUCT_MANAGER, - key: 'Product Manager', - icon: , - }, - ]; - - const menuProps = { - items, - onClick: handleMenuClick, - }; - - const numberProps = { - items: [ - { - label: '1', - key: '1', - }, - { - label: '2', - key: '2', - }, - { - label: '3', - key: '3', - }, - { - label: '4', - key: '4', - }, - ], - onClick: handleNumberClick, - }; - - return ( -
-
-

Profession

-
- - - -
-
-
-
-

- Mock Behavorial Interview -

-
-

- Number of Questions -

- - - -
-
- -
-
-

- Leveraging the power of OpenAIs GPT-4, an advanced language model, you - will be given comprehensive feedback and constructive criticism on - audio responses to question prompts. -

-
-
- ); -} diff --git a/client/src/app/start/layout.tsx b/client/src/app/start/layout.tsx index 96a7df7..cce9b58 100644 --- a/client/src/app/start/layout.tsx +++ b/client/src/app/start/layout.tsx @@ -4,9 +4,6 @@ import '../globals.css'; import React, { useEffect, useState } from 'react'; import { Layout, Menu, MenuProps, Modal } from 'antd'; const { Footer, Sider } = Layout; -import { Suspense } from 'react'; -import Loading from './loading'; -import Image from 'next/image'; import { ClockCircleOutlined, UserOutlined, @@ -15,9 +12,7 @@ import { } from '@ant-design/icons'; import axios, { AxiosError } from 'axios'; import { usePathname, useRouter } from 'next/navigation'; -import CustomFooter from '../components/footer'; import { User } from '../interfaces'; -import Logo from '../../../public/images/LogoV2.png'; type MenuItem = Required['items'][number]; @@ -71,12 +66,11 @@ export default function RegularLayout({ setIsModalOpen(false); }; - const handleLogout = () => { - axios + const handleLogout = async () => { + await axios .get('http://localhost:4000/auth/logout') - .then((res) => console.log(res)) + .then((res) => router.push('/welcome')) .catch((err) => console.log(err)); - router.push('/welcome'); }; useEffect(() => { @@ -113,7 +107,7 @@ export default function RegularLayout({ ]; return ( - }> + <>
{/* TO DO: Have Sider animation where when it loads up have it slide up from the side */} @@ -133,12 +127,6 @@ export default function RegularLayout({ } }} > - logo of interviewify

Interviewify

- }>{children} + {children}
@@ -182,6 +170,6 @@ export default function RegularLayout({ >

Your progress will not be saved!

- + ); } diff --git a/client/src/app/start/loading.tsx b/client/src/app/start/loading.tsx deleted file mode 100644 index 461d392..0000000 --- a/client/src/app/start/loading.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Spin } from 'antd'; -import React from 'react'; -import { LoadingOutlined } from '@ant-design/icons'; - -export default function Loading() { - // You can add any UI inside Loading, including a Skeleton. - return ( -
- } - size="large" - /> -
- ); -} diff --git a/client/src/app/utils/get-questions.ts b/client/src/app/utils/get-questions.ts index d0e58e2..3a686f0 100644 --- a/client/src/app/utils/get-questions.ts +++ b/client/src/app/utils/get-questions.ts @@ -7,17 +7,18 @@ export default function GetQuestions( ): string[] { const res: string[] = []; if (profession && numberQs) { - for (let i = 0; i < numberQs; i++) { + while (res.length < numberQs) { let randomIndex = Math.floor(Math.random() * questionBank.length); + let question = questionBank[randomIndex][0]; + let questionProfession = questionBank[randomIndex][1]; + if ( - questionBank[randomIndex][1] !== profession || - questionBank[randomIndex][1] !== ProfessionType.ALL || - res.includes(questionBank[randomIndex][0]) + (questionProfession === profession || questionProfession === ProfessionType.ALL) && + !res.includes(question) ) { - randomIndex = Math.floor(Math.random() * questionBank.length); + res.push(question); } - res.push(questionBank[randomIndex][0]); } } return res; -} +} \ No newline at end of file diff --git a/client/tailwind.config.ts b/client/tailwind.config.ts index b525a56..6c17309 100644 --- a/client/tailwind.config.ts +++ b/client/tailwind.config.ts @@ -7,7 +7,16 @@ const config: Config = { './src/app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { + fontFamily: { + urbanist: ["Urbanist", "sans"], + redHatText: ["Red Hat Text", "sans"], + }, extend: { + colors: { + primary: "rgb(var(--primary-color) / )", + secondary: "rgb(var(--secondary-color) / )", + accent: "rgb(var(--accent-color) / )" + }, backgroundImage: { 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-conic': diff --git a/client/yarn.lock b/client/yarn.lock index 197a085..9db00bb 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -852,6 +852,11 @@ client-only@0.0.1: resolved "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz" integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" @@ -2065,6 +2070,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lucide-react@^0.379.0: + version "0.379.0" + resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.379.0.tgz#29e34eeffae7fb241b64b09868cbe3ab888ef7cc" + integrity sha512-KcdeVPqmhRldldAAgptb8FjIunM2x2Zy26ZBh1RsEUcdLIvsEmbcw7KpzFYUy5BbpGeWhPu9Z9J5YXfStiXwhg== + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"