├── .firebase
├── ai-mock-interview-yt-react-ts
│ └── hosting
│ │ ├── assets
│ │ ├── img
│ │ │ ├── bg.png
│ │ │ ├── hero.jpg
│ │ │ ├── logo
│ │ │ │ ├── firebase.png
│ │ │ │ ├── meet.png
│ │ │ │ ├── microsoft.png
│ │ │ │ ├── react.png
│ │ │ │ ├── tailwindcss.png
│ │ │ │ └── zoom.png
│ │ │ └── office.jpg
│ │ ├── index-IIY2ctNp.css
│ │ ├── index-QXGEuDCU.js
│ │ └── svg
│ │ │ ├── logo.svg
│ │ │ ├── not-found.svg
│ │ │ └── vite.svg
│ │ └── index.html
└── hosting.LmZpcmViYXNlL2FpLW1vY2staW50ZXJ2aWV3LXl0LXJlYWN0LXRzL2hvc3Rpbmc.cache
├── .firebaserc
├── .gitignore
├── README.md
├── components.json
├── eslint.config.js
├── firebase.json
├── index.html
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
└── assets
│ ├── img
│ ├── bg.png
│ ├── hero.jpg
│ ├── logo
│ │ ├── firebase.png
│ │ ├── meet.png
│ │ ├── microsoft.png
│ │ ├── react.png
│ │ ├── tailwindcss.png
│ │ └── zoom.png
│ └── office.jpg
│ └── svg
│ ├── logo.svg
│ ├── not-found.svg
│ └── vite.svg
├── src
├── App.tsx
├── assets
│ └── react.svg
├── components
│ ├── container.tsx
│ ├── custom-bread-crumb.tsx
│ ├── footer.tsx
│ ├── form-mock-interview.tsx
│ ├── generate.tsx
│ ├── header.tsx
│ ├── headings.tsx
│ ├── logo-container.tsx
│ ├── marquee-img.tsx
│ ├── modal.tsx
│ ├── navigation-routes.tsx
│ ├── pin.tsx
│ ├── profile-container.tsx
│ ├── question-section.tsx
│ ├── record-answer.tsx
│ ├── save-modal.tsx
│ ├── toggle-container.tsx
│ ├── tooltip-button.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── dialog.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── sonner.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ └── tooltip.tsx
├── config
│ └── firebase.config.ts
├── handlers
│ └── auth-handler.tsx
├── index.css
├── layouts
│ ├── auth-layout.tsx
│ ├── main-layout.tsx
│ ├── protected-routes.tsx
│ └── public-layout.tsx
├── lib
│ ├── helpers.ts
│ └── utils.ts
├── main.tsx
├── provider
│ └── toast-provider.tsx
├── routes
│ ├── create-edit-page.tsx
│ ├── dashboard.tsx
│ ├── feedback.tsx
│ ├── home.tsx
│ ├── loader-page.tsx
│ ├── mock-interview-page.tsx
│ ├── mock-load-page.tsx
│ ├── sign-in.tsx
│ └── sign-up.tsx
├── scripts
│ └── index.ts
├── types
│ └── index.ts
└── vite-env.d.ts
├── tailwind.config.js
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/bg.png
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/hero.jpg
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/firebase.png
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/meet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/meet.png
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/microsoft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/microsoft.png
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/react.png
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/tailwindcss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/tailwindcss.png
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/logo/zoom.png
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/office.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/img/office.jpg
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/index-IIY2ctNp.css:
--------------------------------------------------------------------------------
1 | *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--background: 0 0% 100%;--foreground: 0 0% 3.9%;--card: 0 0% 100%;--card-foreground: 0 0% 3.9%;--popover: 0 0% 100%;--popover-foreground: 0 0% 3.9%;--primary: 0 0% 9%;--primary-foreground: 0 0% 98%;--secondary: 0 0% 96.1%;--secondary-foreground: 0 0% 9%;--muted: 0 0% 96.1%;--muted-foreground: 0 0% 45.1%;--accent: 0 0% 96.1%;--accent-foreground: 0 0% 9%;--destructive: 0 84.2% 60.2%;--destructive-foreground: 0 0% 98%;--border: 0 0% 89.8%;--input: 0 0% 89.8%;--ring: 0 0% 3.9%;--chart-1: 12 76% 61%;--chart-2: 173 58% 39%;--chart-3: 197 37% 24%;--chart-4: 43 74% 66%;--chart-5: 27 87% 67%;--radius: .5rem}*{border-color:hsl(var(--border))}body{background-color:hsl(var(--background));color:hsl(var(--foreground))}.text-outline{color:#fff;text-shadow:-1px -1px 0 rgba(0,0,0,.6),1px -1px 0 rgba(0,0,0,.6),-1px 1px 0 rgba(0,0,0,.6),1px 1px 0 rgba(0,0,0,.6)}.container{width:100%}@media (min-width: 640px){.container{max-width:640px}}@media (min-width: 768px){.container{max-width:768px}}@media (min-width: 1024px){.container{max-width:1024px}}@media (min-width: 1280px){.container{max-width:1280px}}@media (min-width: 1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.inset-x-0{left:0;right:0}.inset-y-0{top:0;bottom:0}.bottom-0{bottom:0}.bottom-4{bottom:1rem}.left-0{left:0}.left-4{left:1rem}.left-\[50\%\]{left:50%}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-4{top:1rem}.top-\[50\%\]{top:50%}.z-50{z-index:50}.col-span-1{grid-column:span 1 / span 1}.mx-12{margin-left:3rem;margin-right:3rem}.mx-auto{margin-left:auto;margin-right:auto}.my-12{margin-top:3rem;margin-bottom:3rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.my-8{margin-top:2rem;margin-bottom:2rem}.-mt-3{margin-top:-.75rem}.mb-1{margin-bottom:.25rem}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.block{display:block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-11{height:2.75rem}.h-12{height:3rem}.h-24{height:6rem}.h-3{height:.75rem}.h-4{height:1rem}.h-44{height:11rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-96{height:24rem}.h-\[1px\]{height:1px}.h-\[400px\]{height:400px}.h-\[420px\]{height:420px}.h-\[70vh\]{height:70vh}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.min-h-10{min-height:2.5rem}.min-h-24{min-height:6rem}.min-h-4{min-height:1rem}.min-h-5{min-height:1.25rem}.min-h-6{min-height:1.5rem}.min-h-96{min-height:24rem}.min-h-\[80px\]{min-height:80px}.w-10{width:2.5rem}.w-3{width:.75rem}.w-3\/4{width:75%}.w-4{width:1rem}.w-44{width:11rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-80{width:20rem}.w-9{width:2.25rem}.w-\[1px\]{width:1px}.w-full{width:100%}.w-screen{width:100vw}.min-w-10{min-width:2.5rem}.min-w-24{min-width:6rem}.min-w-4{min-width:1rem}.min-w-5{min-width:1.25rem}.min-w-6{min-width:1.5rem}.max-w-lg{max-width:32rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.translate-x-\[-50\%\]{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-\[-50\%\]{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-evenly{justify-content:space-evenly}.gap-1\.5{gap:.375rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-12>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(3rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(3rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(2rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.break-words{overflow-wrap:break-word}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:var(--radius)}.rounded-md{border-radius:calc(var(--radius) - 2px)}.rounded-sm{border-radius:calc(var(--radius) - 4px)}.rounded-xl{border-radius:.75rem}.rounded-b-lg{border-bottom-right-radius:var(--radius);border-bottom-left-radius:var(--radius)}.rounded-t-lg{border-top-left-radius:var(--radius);border-top-right-radius:var(--radius)}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-none{border-style:none}.border-destructive\/50{border-color:hsl(var(--destructive) / .5)}.border-input{border-color:hsl(var(--input))}.border-sky-200{--tw-border-opacity: 1;border-color:rgb(186 230 253 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.bg-background{background-color:hsl(var(--background))}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/80{background-color:#000c}.bg-border{background-color:hsl(var(--border))}.bg-card{background-color:hsl(var(--card))}.bg-destructive{background-color:hsl(var(--destructive))}.bg-emerald-600{--tw-bg-opacity: 1;background-color:rgb(5 150 105 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-muted{background-color:hsl(var(--muted))}.bg-neutral-100{--tw-bg-opacity: 1;background-color:rgb(245 245 245 / var(--tw-bg-opacity, 1))}.bg-popover{background-color:hsl(var(--popover))}.bg-primary{background-color:hsl(var(--primary))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-secondary{background-color:hsl(var(--secondary))}.bg-sky-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-white\/40{background-color:#fff6}.bg-white\/60{background-color:#fff9}.bg-yellow-100\/50{background-color:#fef9c380}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.from-purple-50{--tw-gradient-from: #faf5ff var(--tw-gradient-from-position);--tw-gradient-to: rgb(250 245 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-blue-50{--tw-gradient-to: #eff6ff var(--tw-gradient-to-position)}.to-gray-700{--tw-gradient-to: #374151 var(--tw-gradient-to-position)}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0{padding:0}.p-1{padding:.25rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-24{padding-bottom:6rem}.pb-4{padding-bottom:1rem}.pt-0{padding-top:0}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-sans{font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[12px\]{font-size:12px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-extrabold{font-weight:800}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wide{letter-spacing:.025em}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-blue-500{--tw-text-opacity: 1;color:rgb(59 130 246 / var(--tw-text-opacity, 1))}.text-blue-700{--tw-text-opacity: 1;color:rgb(29 78 216 / var(--tw-text-opacity, 1))}.text-card-foreground{color:hsl(var(--card-foreground))}.text-destructive{color:hsl(var(--destructive))}.text-destructive-foreground{color:hsl(var(--destructive-foreground))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.text-emerald-500{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.text-foreground{color:hsl(var(--foreground))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-50{--tw-text-opacity: 1;color:rgb(249 250 251 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-muted-foreground{color:hsl(var(--muted-foreground))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity, 1))}.text-neutral-600{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.text-neutral-800{--tw-text-opacity: 1;color:rgb(38 38 38 / var(--tw-text-opacity, 1))}.text-neutral-900{--tw-text-opacity: 1;color:rgb(23 23 23 / var(--tw-text-opacity, 1))}.text-pink-500{--tw-text-opacity: 1;color:rgb(236 72 153 / var(--tw-text-opacity, 1))}.text-popover-foreground{color:hsl(var(--popover-foreground))}.text-primary{color:hsl(var(--primary))}.text-primary-foreground{color:hsl(var(--primary-foreground))}.text-red-500{--tw-text-opacity: 1;color:rgb(239 68 68 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-secondary-foreground{color:hsl(var(--secondary-foreground))}.text-sky-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-sky-700{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.text-sky-800{--tw-text-opacity: 1;color:rgb(7 89 133 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-700{--tw-text-opacity: 1;color:rgb(161 98 7 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.underline-offset-4{text-underline-offset:4px}.opacity-20{opacity:.2}.opacity-70{opacity:.7}.shadow-inner{--tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / .05);--tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-none{--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-gray-100{--tw-shadow-color: #f3f4f6;--tw-shadow: var(--tw-shadow-colored)}.outline{outline-style:solid}.ring-offset-background{--tw-ring-offset-color: hsl(var(--background))}.drop-shadow-md{--tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / .07)) drop-shadow(0 2px 2px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.grayscale{--tw-grayscale: grayscale(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur: blur(12px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-200{transition-duration:.2s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}@keyframes enter{0%{opacity:var(--tw-enter-opacity, 1);transform:translate3d(var(--tw-enter-translate-x, 0),var(--tw-enter-translate-y, 0),0) scale3d(var(--tw-enter-scale, 1),var(--tw-enter-scale, 1),var(--tw-enter-scale, 1)) rotate(var(--tw-enter-rotate, 0))}}@keyframes exit{to{opacity:var(--tw-exit-opacity, 1);transform:translate3d(var(--tw-exit-translate-x, 0),var(--tw-exit-translate-y, 0),0) scale3d(var(--tw-exit-scale, 1),var(--tw-exit-scale, 1),var(--tw-exit-scale, 1)) rotate(var(--tw-exit-rotate, 0))}}.animate-in{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.fade-in-0{--tw-enter-opacity: 0}.zoom-in-95{--tw-enter-scale: .95}.duration-150{animation-duration:.15s}.duration-200{animation-duration:.2s}.ease-in-out{animation-timing-function:cubic-bezier(.4,0,.2,1)}.file\:border-0::file-selector-button{border-width:0px}.file\:bg-transparent::file-selector-button{background-color:transparent}.file\:text-sm::file-selector-button{font-size:.875rem;line-height:1.25rem}.file\:font-medium::file-selector-button{font-weight:500}.file\:text-foreground::file-selector-button{color:hsl(var(--foreground))}.placeholder\:text-muted-foreground::-moz-placeholder{color:hsl(var(--muted-foreground))}.placeholder\:text-muted-foreground::placeholder{color:hsl(var(--muted-foreground))}.hover\:border-emerald-400:hover{--tw-border-opacity: 1;border-color:rgb(52 211 153 / var(--tw-border-opacity, 1))}.hover\:bg-accent:hover{background-color:hsl(var(--accent))}.hover\:bg-destructive\/80:hover{background-color:hsl(var(--destructive) / .8)}.hover\:bg-destructive\/90:hover{background-color:hsl(var(--destructive) / .9)}.hover\:bg-emerald-50:hover{--tw-bg-opacity: 1;background-color:rgb(236 253 245 / var(--tw-bg-opacity, 1))}.hover\:bg-emerald-800:hover{--tw-bg-opacity: 1;background-color:rgb(6 95 70 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-primary\/80:hover{background-color:hsl(var(--primary) / .8)}.hover\:bg-primary\/90:hover{background-color:hsl(var(--primary) / .9)}.hover\:bg-secondary\/80:hover{background-color:hsl(var(--secondary) / .8)}.hover\:text-accent-foreground:hover{color:hsl(var(--accent-foreground))}.hover\:text-emerald-500:hover{--tw-text-opacity: 1;color:rgb(16 185 129 / var(--tw-text-opacity, 1))}.hover\:text-emerald-900:hover{--tw-text-opacity: 1;color:rgb(6 78 59 / var(--tw-text-opacity, 1))}.hover\:text-foreground:hover{color:hsl(var(--foreground))}.hover\:text-gray-100:hover{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.hover\:text-sky-500:hover{--tw-text-opacity: 1;color:rgb(14 165 233 / var(--tw-text-opacity, 1))}.hover\:text-yellow-500:hover{--tw-text-opacity: 1;color:rgb(234 179 8 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:no-underline:hover{text-decoration-line:none}.hover\:opacity-100:hover{opacity:1}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-ring:focus{--tw-ring-color: hsl(var(--ring))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.focus-visible\:outline-none:focus-visible{outline:2px solid transparent;outline-offset:2px}.focus-visible\:ring-2:focus-visible{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus-visible\:ring-ring:focus-visible{--tw-ring-color: hsl(var(--ring))}.focus-visible\:ring-offset-2:focus-visible{--tw-ring-offset-width: 2px}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group.toaster .group-\[\.toaster\]\:border-border{border-color:hsl(var(--border))}.group.toast .group-\[\.toast\]\:bg-muted{background-color:hsl(var(--muted))}.group.toast .group-\[\.toast\]\:bg-primary{background-color:hsl(var(--primary))}.group.toaster .group-\[\.toaster\]\:bg-background{background-color:hsl(var(--background))}.group.toast .group-\[\.toast\]\:text-muted-foreground{color:hsl(var(--muted-foreground))}.group.toast .group-\[\.toast\]\:text-primary-foreground{color:hsl(var(--primary-foreground))}.group.toaster .group-\[\.toaster\]\:text-foreground{color:hsl(var(--foreground))}.group.toaster .group-\[\.toaster\]\:shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.peer:disabled~.peer-disabled\:cursor-not-allowed{cursor:not-allowed}.peer:disabled~.peer-disabled\:opacity-70{opacity:.7}@keyframes accordion-up{0%{height:var(--radix-accordion-content-height)}to{height:0}}.data-\[state\=closed\]\:animate-accordion-up[data-state=closed]{animation:accordion-up .2s ease-out}@keyframes accordion-down{0%{height:0}to{height:var(--radix-accordion-content-height)}}.data-\[state\=open\]\:animate-accordion-down[data-state=open]{animation:accordion-down .2s ease-out}.data-\[state\=active\]\:bg-background[data-state=active]{background-color:hsl(var(--background))}.data-\[state\=active\]\:bg-emerald-200[data-state=active]{--tw-bg-opacity: 1;background-color:rgb(167 243 208 / var(--tw-bg-opacity, 1))}.data-\[state\=open\]\:bg-accent[data-state=open]{background-color:hsl(var(--accent))}.data-\[state\=open\]\:bg-secondary[data-state=open]{background-color:hsl(var(--secondary))}.data-\[state\=active\]\:text-foreground[data-state=active]{color:hsl(var(--foreground))}.data-\[state\=open\]\:text-muted-foreground[data-state=open]{color:hsl(var(--muted-foreground))}.data-\[state\=active\]\:shadow-md[data-state=active]{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=active\]\:shadow-sm[data-state=active]{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.data-\[state\=closed\]\:duration-300[data-state=closed]{transition-duration:.3s}.data-\[state\=open\]\:duration-500[data-state=open]{transition-duration:.5s}.data-\[state\=open\]\:animate-in[data-state=open]{animation-name:enter;animation-duration:.15s;--tw-enter-opacity: initial;--tw-enter-scale: initial;--tw-enter-rotate: initial;--tw-enter-translate-x: initial;--tw-enter-translate-y: initial}.data-\[state\=closed\]\:animate-out[data-state=closed]{animation-name:exit;animation-duration:.15s;--tw-exit-opacity: initial;--tw-exit-scale: initial;--tw-exit-rotate: initial;--tw-exit-translate-x: initial;--tw-exit-translate-y: initial}.data-\[state\=closed\]\:fade-out-0[data-state=closed]{--tw-exit-opacity: 0}.data-\[state\=open\]\:fade-in-0[data-state=open]{--tw-enter-opacity: 0}.data-\[state\=closed\]\:zoom-out-95[data-state=closed]{--tw-exit-scale: .95}.data-\[state\=open\]\:zoom-in-95[data-state=open]{--tw-enter-scale: .95}.data-\[side\=bottom\]\:slide-in-from-top-2[data-side=bottom]{--tw-enter-translate-y: -.5rem}.data-\[side\=left\]\:slide-in-from-right-2[data-side=left]{--tw-enter-translate-x: .5rem}.data-\[side\=right\]\:slide-in-from-left-2[data-side=right]{--tw-enter-translate-x: -.5rem}.data-\[side\=top\]\:slide-in-from-bottom-2[data-side=top]{--tw-enter-translate-y: .5rem}.data-\[state\=closed\]\:slide-out-to-bottom[data-state=closed]{--tw-exit-translate-y: 100%}.data-\[state\=closed\]\:slide-out-to-left[data-state=closed]{--tw-exit-translate-x: -100%}.data-\[state\=closed\]\:slide-out-to-left-1\/2[data-state=closed]{--tw-exit-translate-x: -50%}.data-\[state\=closed\]\:slide-out-to-right[data-state=closed]{--tw-exit-translate-x: 100%}.data-\[state\=closed\]\:slide-out-to-top[data-state=closed]{--tw-exit-translate-y: -100%}.data-\[state\=closed\]\:slide-out-to-top-\[48\%\][data-state=closed]{--tw-exit-translate-y: -48%}.data-\[state\=open\]\:slide-in-from-bottom[data-state=open]{--tw-enter-translate-y: 100%}.data-\[state\=open\]\:slide-in-from-left[data-state=open]{--tw-enter-translate-x: -100%}.data-\[state\=open\]\:slide-in-from-left-1\/2[data-state=open]{--tw-enter-translate-x: -50%}.data-\[state\=open\]\:slide-in-from-right[data-state=open]{--tw-enter-translate-x: 100%}.data-\[state\=open\]\:slide-in-from-top[data-state=open]{--tw-enter-translate-y: -100%}.data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state=open]{--tw-enter-translate-y: -48%}.data-\[state\=closed\]\:duration-300[data-state=closed]{animation-duration:.3s}.data-\[state\=open\]\:duration-500[data-state=open]{animation-duration:.5s}.dark\:border-destructive:is(.dark *){border-color:hsl(var(--destructive))}@media (min-width: 640px){.sm\:max-w-sm{max-width:24rem}.sm\:flex-row{flex-direction:row}.sm\:justify-end{justify-content:flex-end}.sm\:gap-2\.5{gap:.625rem}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:rounded-lg{border-radius:var(--radius)}.sm\:text-left{text-align:left}}@media (min-width: 768px){.md\:col-span-2{grid-column:span 2 / span 2}.md\:col-span-3{grid-column:span 3 / span 3}.md\:block{display:block}.md\:flex{display:flex}.md\:grid{display:grid}.md\:hidden{display:none}.md\:h-32{height:8rem}.md\:w-96{width:24rem}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:items-center{align-items:center}.md\:justify-end{justify-content:flex-end}.md\:px-12{padding-left:3rem;padding-right:3rem}.md\:px-8{padding-left:2rem;padding-right:2rem}.md\:py-16{padding-top:4rem;padding-bottom:4rem}.md\:text-left{text-align:left}.md\:text-3xl{font-size:1.875rem;line-height:2.25rem}.md\:text-6xl{font-size:3.75rem;line-height:1}.md\:text-8xl{font-size:6rem;line-height:1}.md\:text-sm{font-size:.875rem;line-height:1.25rem}.md\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width: 1280px){.xl\:mx-16{margin-left:4rem;margin-right:4rem}.xl\:h-52{height:13rem}.xl\:w-52{width:13rem}}.\[\&\>svg\+div\]\:translate-y-\[-3px\]>svg+div{--tw-translate-y: -3px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\[\&\>svg\]\:absolute>svg{position:absolute}.\[\&\>svg\]\:left-4>svg{left:1rem}.\[\&\>svg\]\:top-4>svg{top:1rem}.\[\&\>svg\]\:h-3\.5>svg{height:.875rem}.\[\&\>svg\]\:w-3\.5>svg{width:.875rem}.\[\&\>svg\]\:text-destructive>svg{color:hsl(var(--destructive))}.\[\&\>svg\]\:text-foreground>svg{color:hsl(var(--foreground))}.\[\&\>svg\~\*\]\:pl-7>svg~*{padding-left:1.75rem}.\[\&\[data-state\=open\]\>svg\]\:rotate-180[data-state=open]>svg{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.\[\&_p\]\:leading-relaxed p{line-height:1.625}.\[\&_svg\]\:pointer-events-none svg{pointer-events:none}.\[\&_svg\]\:size-4 svg{width:1rem;height:1rem}.\[\&_svg\]\:shrink-0 svg{flex-shrink:0}
2 |
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/svg/not-found.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/assets/svg/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.firebase/ai-mock-interview-yt-react-ts/hosting/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/.firebase/hosting.LmZpcmViYXNlL2FpLW1vY2staW50ZXJ2aWV3LXl0LXJlYWN0LXRzL2hvc3Rpbmc.cache:
--------------------------------------------------------------------------------
1 | index.html,1737764222524,c47500ea142dc6c790bd4d5b38fbefdafa201c2f44aaefb2e7cf52b4533833c5
2 | assets/svg/logo.svg,1737764222526,0c2c0a42f97bd6e9e5f5ff1834abc43b89b544d312c80cc071a305d237addcbe
3 | assets/svg/vite.svg,1737764222525,d3bbbc44b3ea71906a72bf2ec1a4716903e2e3d9f85a5007205a65d1f12e2923
4 | assets/svg/not-found.svg,1737764222525,37942a939ec97cb7fa3eeee9f52c02865efe49afbe758a4d0551e8f9ec5a8874
5 | assets/index-IIY2ctNp.css,1737764222528,b317308a7bbcfbc0f62b6373e7ef95b7d22547a44b3a527853538d06d441ecf0
6 | assets/img/logo/react.png,1737764222533,758685d31559e371334b6ed138bebbc6f5c2c29522cf7b631ad3fcde0957bebd
7 | assets/img/logo/microsoft.png,1737764222534,7668a185fd4eaaafaa8ab5cdd4e02c14a66fe23c7920990cb2841c7413754ce1
8 | assets/img/logo/firebase.png,1737764222537,7ffa3968429c14f092e0a16e2ec972c6647abf92a6ca79cd35c6dd3d9d1cfe54
9 | assets/img/logo/zoom.png,1737764222532,2bf0dffee4f61396121c4b34cae3ead5167650f51a990f419673a7ef88cca2ab
10 | assets/img/logo/tailwindcss.png,1737764222533,91dc5f5f070942129d0da010cc74d913ac3183bdcf569418a46b96b4c9974722
11 | assets/img/logo/meet.png,1737764222536,777497010d68e25e0cd3a35c4271c835b7e8861c9b32ea6bcebb8b5b7d82551d
12 | assets/img/bg.png,1737764222547,896f58e79d70e7ef0403bb8d1e0d1972c60d6a83f959fc4361e3b2ce46ae6170
13 | assets/img/hero.jpg,1737764222541,b910eeea949488a622c64228b5195420636ba9a3548d69f5afbfaec4150378ba
14 | assets/img/office.jpg,1737764222531,72ee47a8f18dc3d88d0797aee2c3e2c265b86c824c66cb2965a62ce9679111a6
15 | assets/index-QXGEuDCU.js,1737764222527,2d0e84fffca18671ebae7f0a7fd41500b89528f9bea4b204127d8d3585af75a3
16 |
--------------------------------------------------------------------------------
/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "ai-mock-interview-yt-react-ts"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | # enivronment variables
27 | .env
28 | .env.*
29 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React + TypeScript + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## Expanding the ESLint configuration
11 |
12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
13 |
14 | - Configure the top-level `parserOptions` property like this:
15 |
16 | ```js
17 | export default tseslint.config({
18 | languageOptions: {
19 | // other options...
20 | parserOptions: {
21 | project: ['./tsconfig.node.json', './tsconfig.app.json'],
22 | tsconfigRootDir: import.meta.dirname,
23 | },
24 | },
25 | })
26 | ```
27 |
28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
29 | - Optionally add `...tseslint.configs.stylisticTypeChecked`
30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
31 |
32 | ```js
33 | // eslint.config.js
34 | import react from 'eslint-plugin-react'
35 |
36 | export default tseslint.config({
37 | // Set the react version
38 | settings: { react: { version: '18.3' } },
39 | plugins: {
40 | // Add the react plugin
41 | react,
42 | },
43 | rules: {
44 | // other rules...
45 | // Enable its recommended rules
46 | ...react.configs.recommended.rules,
47 | ...react.configs['jsx-runtime'].rules,
48 | },
49 | })
50 | ```
51 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": {
3 | "source": ".",
4 | "ignore": [
5 | "firebase.json",
6 | "**/.*",
7 | "**/node_modules/**"
8 | ],
9 | "frameworksBackend": {
10 | "region": "us-central1"
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ai-mock-interview-react-type",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@clerk/clerk-react": "^5.22.6",
14 | "@google/generative-ai": "^0.21.0",
15 | "@hookform/resolvers": "^3.10.0",
16 | "@radix-ui/react-accordion": "^1.2.2",
17 | "@radix-ui/react-dialog": "^1.1.5",
18 | "@radix-ui/react-label": "^2.1.1",
19 | "@radix-ui/react-separator": "^1.1.1",
20 | "@radix-ui/react-slot": "^1.1.1",
21 | "@radix-ui/react-tabs": "^1.1.2",
22 | "@radix-ui/react-tooltip": "^1.1.7",
23 | "class-variance-authority": "^0.7.1",
24 | "clsx": "^2.1.1",
25 | "firebase": "^11.2.0",
26 | "lucide-react": "^0.474.0",
27 | "next-themes": "^0.4.4",
28 | "react": "^18.3.1",
29 | "react-dom": "^18.3.1",
30 | "react-fast-marquee": "^1.6.5",
31 | "react-hook-form": "^7.54.2",
32 | "react-hook-speech-to-text": "^0.8.0",
33 | "react-router-dom": "^7.1.3",
34 | "react-webcam": "^7.2.0",
35 | "sonner": "^1.7.2",
36 | "tailwind-merge": "^2.6.0",
37 | "tailwindcss-animate": "^1.0.7",
38 | "zod": "^3.24.1"
39 | },
40 | "devDependencies": {
41 | "@eslint/js": "^9.17.0",
42 | "@types/node": "^22.10.10",
43 | "@types/react": "^18.3.18",
44 | "@types/react-dom": "^18.3.5",
45 | "@vitejs/plugin-react": "^4.3.4",
46 | "autoprefixer": "^10.4.20",
47 | "eslint": "^9.17.0",
48 | "eslint-plugin-react-hooks": "^5.0.0",
49 | "eslint-plugin-react-refresh": "^0.4.16",
50 | "globals": "^15.14.0",
51 | "postcss": "^8.5.1",
52 | "tailwindcss": "3.4.17",
53 | "typescript": "~5.6.2",
54 | "typescript-eslint": "^8.18.2",
55 | "vite": "^6.0.5"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/assets/img/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/bg.png
--------------------------------------------------------------------------------
/public/assets/img/hero.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/hero.jpg
--------------------------------------------------------------------------------
/public/assets/img/logo/firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/logo/firebase.png
--------------------------------------------------------------------------------
/public/assets/img/logo/meet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/logo/meet.png
--------------------------------------------------------------------------------
/public/assets/img/logo/microsoft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/logo/microsoft.png
--------------------------------------------------------------------------------
/public/assets/img/logo/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/logo/react.png
--------------------------------------------------------------------------------
/public/assets/img/logo/tailwindcss.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/logo/tailwindcss.png
--------------------------------------------------------------------------------
/public/assets/img/logo/zoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/logo/zoom.png
--------------------------------------------------------------------------------
/public/assets/img/office.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mahalakshmi-Design-Studioz/ai-mock-interview-react-vite-typescript-january-2025/15423dc005fcffd1d0393c0175dfeee1a9d0a650/public/assets/img/office.jpg
--------------------------------------------------------------------------------
/public/assets/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/assets/svg/not-found.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/public/assets/svg/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
2 |
3 | import { PublicLayout } from "@/layouts/public-layout";
4 | import AuthenticationLayout from "@/layouts/auth-layout";
5 | import ProtectRoutes from "@/layouts/protected-routes";
6 | import { MainLayout } from "@/layouts/main-layout";
7 |
8 | import HomePage from "@/routes/home";
9 | import { SignInPage } from "./routes/sign-in";
10 | import { SignUpPage } from "./routes/sign-up";
11 | import { Generate } from "./components/generate";
12 | import { Dashboard } from "./routes/dashboard";
13 | import { CreateEditPage } from "./routes/create-edit-page";
14 | import { MockLoadPage } from "./routes/mock-load-page";
15 | import { MockInterviewPage } from "./routes/mock-interview-page";
16 | import { Feedback } from "./routes/feedback";
17 |
18 | const App = () => {
19 | return (
20 |
21 |
22 | {/* public routes */}
23 | }>
24 | } />
25 |
26 |
27 | {/* authentication layout */}
28 | }>
29 | } />
30 | } />
31 |
32 |
33 | {/* protected routes */}
34 |
37 |
38 |
39 | }
40 | >
41 | {/* add all the protect routes */}
42 | } path="/generate">
43 | } />
44 | } />
45 | } />
46 | }
49 | />
50 | } />
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default App;
59 |
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/container.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | interface ContainerProps {
4 | children: React.ReactNode;
5 | className?: string;
6 | }
7 |
8 | export const Container = ({ children, className }: ContainerProps) => {
9 | return (
10 |
13 | {children}
14 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/components/custom-bread-crumb.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Breadcrumb,
3 | BreadcrumbItem,
4 | BreadcrumbLink,
5 | BreadcrumbList,
6 | BreadcrumbPage,
7 | BreadcrumbSeparator,
8 | } from "@/components/ui/breadcrumb";
9 | import { Home } from "lucide-react";
10 | import React from "react";
11 |
12 | interface CustomBreadCrumbProps {
13 | breadCrumbPage: string;
14 | breadCrumpItems?: { link: string; label: string }[];
15 | }
16 |
17 | export const CustomBreadCrumb = ({
18 | breadCrumbPage,
19 | breadCrumpItems,
20 | }: CustomBreadCrumbProps) => {
21 | return (
22 |
23 |
24 |
25 |
29 |
30 | Home
31 |
32 |
33 |
34 | {breadCrumpItems &&
35 | breadCrumpItems.map((item, i) => (
36 |
37 |
38 |
39 |
43 | {item.label}
44 |
45 |
46 |
47 | ))}
48 |
49 |
50 | {breadCrumbPage}
51 |
52 |
53 |
54 | );
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { Facebook, Twitter, Instagram, Linkedin } from "lucide-react"; // Import Lucide icons
4 | import { Link } from "react-router-dom";
5 | import { Container } from "@/components/container";
6 | import { MainRoutes } from "@/lib/helpers";
7 |
8 | interface SocialLinkProps {
9 | href: string;
10 | icon: React.ReactNode;
11 | hoverColor: string;
12 | }
13 |
14 | const SocialLink: React.FC = ({ href, icon, hoverColor }) => {
15 | return (
16 |
22 | {icon}
23 |
24 | );
25 | };
26 |
27 | interface FooterLinkProps {
28 | to: string;
29 | children: React.ReactNode;
30 | }
31 |
32 | const FooterLink: React.FC = ({ to, children }) => {
33 | return (
34 |
35 |
39 | {children}
40 |
41 |
42 | );
43 | };
44 |
45 | export const Footer = () => {
46 | return (
47 |
48 |
49 |
50 | {/* First Column: Links */}
51 |
52 |
Quick Links
53 |
54 | {MainRoutes.map((route) => (
55 |
56 | {route.label}
57 |
58 | ))}
59 |
60 |
61 |
62 | {/* Second Column: About Us */}
63 |
64 |
About Us
65 |
66 | We are committed to helping you unlock your full potential with
67 | AI-powered tools. Our platform offers a wide range of resources to
68 | improve your interview skills and chances of success.
69 |
70 |
71 |
72 | {/* Third Column: Services */}
73 |
74 |
Services
75 |
76 |
77 | Interview Preparation
78 |
79 |
80 | Career Coaching
81 |
82 |
83 | Resume Building
84 |
85 |
86 |
87 |
88 | {/* Fourth Column: Address and Social Media */}
89 |
90 |
Contact Us
91 |
123 AI Street, Tech City, 12345
92 |
93 | }
96 | hoverColor="text-blue-500"
97 | />
98 | }
101 | hoverColor="text-blue-400"
102 | />
103 | }
106 | hoverColor="text-pink-500"
107 | />
108 | }
111 | hoverColor="text-blue-700"
112 | />
113 |
114 |
115 |
116 |
117 |
118 | );
119 | };
120 |
--------------------------------------------------------------------------------
/src/components/form-mock-interview.tsx:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { zodResolver } from "@hookform/resolvers/zod";
3 | import { FormProvider, useForm } from "react-hook-form";
4 |
5 | import { Interview } from "@/types";
6 |
7 | import { CustomBreadCrumb } from "./custom-bread-crumb";
8 | import { useEffect, useState } from "react";
9 | import { useNavigate } from "react-router-dom";
10 | import { useAuth } from "@clerk/clerk-react";
11 | import { toast } from "sonner";
12 | import { Headings } from "./headings";
13 | import { Button } from "./ui/button";
14 | import { Loader, Trash2 } from "lucide-react";
15 | import { Separator } from "./ui/separator";
16 | import {
17 | FormControl,
18 | FormField,
19 | FormItem,
20 | FormLabel,
21 | FormMessage,
22 | } from "./ui/form";
23 | import { Input } from "./ui/input";
24 | import { Textarea } from "./ui/textarea";
25 | import { chatSession } from "@/scripts";
26 | import {
27 | addDoc,
28 | collection,
29 | doc,
30 | serverTimestamp,
31 | updateDoc,
32 | } from "firebase/firestore";
33 | import { db } from "@/config/firebase.config";
34 |
35 | interface FormMockInterviewProps {
36 | initialData: Interview | null;
37 | }
38 |
39 | const formSchema = z.object({
40 | position: z
41 | .string()
42 | .min(1, "Position is required")
43 | .max(100, "Position must be 100 characters or less"),
44 | description: z.string().min(10, "Description is required"),
45 | experience: z.coerce
46 | .number()
47 | .min(0, "Experience cannot be empty or negative"),
48 | techStack: z.string().min(1, "Tech stack must be at least a character"),
49 | });
50 |
51 | type FormData = z.infer;
52 |
53 | export const FormMockInterview = ({ initialData }: FormMockInterviewProps) => {
54 | const form = useForm({
55 | resolver: zodResolver(formSchema),
56 | defaultValues: initialData || {},
57 | });
58 |
59 | const { isValid, isSubmitting } = form.formState;
60 | const [loading, setLoading] = useState(false);
61 | const navigate = useNavigate();
62 | const { userId } = useAuth();
63 |
64 | const title = initialData
65 | ? initialData.position
66 | : "Create a new mock interview";
67 |
68 | const breadCrumpPage = initialData ? initialData?.position : "Create";
69 | const actions = initialData ? "Save Changes" : "Create";
70 | const toastMessage = initialData
71 | ? { title: "Updated..!", description: "Changes saved successfully..." }
72 | : { title: "Created..!", description: "New Mock Interview created..." };
73 |
74 | const cleanAiResponse = (responseText: string) => {
75 | // Step 1: Trim any surrounding whitespace
76 | let cleanText = responseText.trim();
77 |
78 | // Step 2: Remove any occurrences of "json" or code block symbols (``` or `)
79 | cleanText = cleanText.replace(/(json|```|`)/g, "");
80 |
81 | // Step 3: Extract a JSON array by capturing text between square brackets
82 | const jsonArrayMatch = cleanText.match(/\[.*\]/s);
83 | if (jsonArrayMatch) {
84 | cleanText = jsonArrayMatch[0];
85 | } else {
86 | throw new Error("No JSON array found in response");
87 | }
88 |
89 | // Step 4: Parse the clean JSON text into an array of objects
90 | try {
91 | return JSON.parse(cleanText);
92 | } catch (error) {
93 | throw new Error("Invalid JSON format: " + (error as Error)?.message);
94 | }
95 | };
96 |
97 | const generateAiResponse = async (data: FormData) => {
98 | const prompt = `
99 | As an experienced prompt engineer, generate a JSON array containing 5 technical interview questions along with detailed answers based on the following job information. Each object in the array should have the fields "question" and "answer", formatted as follows:
100 |
101 | [
102 | { "question": "", "answer": "" },
103 | ...
104 | ]
105 |
106 | Job Information:
107 | - Job Position: ${data?.position}
108 | - Job Description: ${data?.description}
109 | - Years of Experience Required: ${data?.experience}
110 | - Tech Stacks: ${data?.techStack}
111 |
112 | The questions should assess skills in ${data?.techStack} development and best practices, problem-solving, and experience handling complex requirements. Please format the output strictly as an array of JSON objects without any additional labels, code blocks, or explanations. Return only the JSON array with questions and answers.
113 | `;
114 |
115 | const aiResult = await chatSession.sendMessage(prompt);
116 | const cleanedResponse = cleanAiResponse(aiResult.response.text());
117 |
118 | return cleanedResponse;
119 | };
120 |
121 | const onSubmit = async (data: FormData) => {
122 | try {
123 | setLoading(true);
124 |
125 | if (initialData) {
126 | // update
127 | if (isValid) {
128 | const aiResult = await generateAiResponse(data);
129 |
130 | await updateDoc(doc(db, "interviews", initialData?.id), {
131 | questions: aiResult,
132 | ...data,
133 | updatedAt: serverTimestamp(),
134 | }).catch((error) => console.log(error));
135 | toast(toastMessage.title, { description: toastMessage.description });
136 | }
137 | } else {
138 | // create a new mock interview
139 | if (isValid) {
140 | const aiResult = await generateAiResponse(data);
141 |
142 | await addDoc(collection(db, "interviews"), {
143 | ...data,
144 | userId,
145 | questions: aiResult,
146 | createdAt: serverTimestamp(),
147 | });
148 |
149 | toast(toastMessage.title, { description: toastMessage.description });
150 | }
151 | }
152 |
153 | navigate("/generate", { replace: true });
154 | } catch (error) {
155 | console.log(error);
156 | toast.error("Error..", {
157 | description: `Something went wrong. Please try again later`,
158 | });
159 | } finally {
160 | setLoading(false);
161 | }
162 | };
163 |
164 | useEffect(() => {
165 | if (initialData) {
166 | form.reset({
167 | position: initialData.position,
168 | description: initialData.description,
169 | experience: initialData.experience,
170 | techStack: initialData.techStack,
171 | });
172 | }
173 | }, [initialData, form]);
174 |
175 | return (
176 |
177 |
181 |
182 |
183 |
184 |
185 | {initialData && (
186 |
187 |
188 |
189 | )}
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
312 |
313 |
314 | );
315 | };
316 |
--------------------------------------------------------------------------------
/src/components/generate.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 |
3 | export const Generate = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/header.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { useAuth } from "@clerk/clerk-react";
3 | import { Container } from "./container";
4 | import { LogoContainer } from "./logo-container";
5 | import { NavigationRoutes } from "./navigation-routes";
6 | import { NavLink } from "react-router-dom";
7 | import { ProfileContainer } from "./profile-container";
8 | import { ToggleContainer } from "./toggle-container";
9 |
10 | const Header = () => {
11 | const { userId } = useAuth();
12 |
13 | return (
14 |
17 |
18 |
19 | {/* logo section */}
20 |
21 |
22 | {/* navigation section */}
23 |
24 |
25 | {userId && (
26 |
29 | cn(
30 | "text-base text-neutral-600",
31 | isActive && "text-neutral-900 font-semibold"
32 | )
33 | }
34 | >
35 | Take An Interview
36 |
37 | )}
38 |
39 |
40 |
41 | {/* profile section */}
42 |
43 |
44 | {/* mobile toggle section */}
45 |
46 |
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default Header;
54 |
--------------------------------------------------------------------------------
/src/components/headings.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 |
3 | interface HeadingsProps {
4 | title: string;
5 | description?: string;
6 | isSubHeading?: boolean;
7 | }
8 |
9 | export const Headings = ({
10 | title,
11 | description,
12 | isSubHeading = false,
13 | }: HeadingsProps) => {
14 | return (
15 |
16 |
22 | {title}
23 |
24 | {description && (
25 |
{description}
26 | )}
27 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/logo-container.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 |
3 | export const LogoContainer = () => {
4 | return (
5 |
6 |
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/marquee-img.tsx:
--------------------------------------------------------------------------------
1 | export const MarqueImg = ({ img }: { img: string }) => {
2 | return (
3 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Dialog,
3 | DialogContent,
4 | DialogDescription,
5 | DialogHeader,
6 | DialogTitle,
7 | } from "@/components/ui/dialog";
8 |
9 | interface ModalProps {
10 | title: string;
11 | description: string;
12 | isOpen: boolean;
13 | onClose: () => void;
14 | children?: React.ReactNode;
15 | }
16 |
17 | const Modal = ({
18 | title,
19 | description,
20 | isOpen,
21 | onClose,
22 | children,
23 | }: ModalProps) => {
24 | const onChange = (open: boolean) => {
25 | if (!open) {
26 | onClose();
27 | }
28 | };
29 |
30 | return (
31 |
32 |
33 |
34 | {title}
35 | {description}
36 |
37 |
38 | {children}
39 |
40 |
41 | );
42 | };
43 |
44 | export default Modal;
45 |
--------------------------------------------------------------------------------
/src/components/navigation-routes.tsx:
--------------------------------------------------------------------------------
1 | import { MainRoutes } from "@/lib/helpers";
2 | import { cn } from "@/lib/utils";
3 | import { NavLink } from "react-router-dom";
4 |
5 | interface NavigationRoutesProps {
6 | isMobile?: boolean;
7 | }
8 |
9 | export const NavigationRoutes = ({
10 | isMobile = false,
11 | }: NavigationRoutesProps) => {
12 | return (
13 |
19 | {MainRoutes.map((route) => (
20 |
24 | cn(
25 | "text-base text-neutral-600",
26 | isActive && "text-neutral-900 font-semibold"
27 | )
28 | }
29 | >
30 | {route.label}
31 |
32 | ))}
33 |
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/pin.tsx:
--------------------------------------------------------------------------------
1 | import { Interview } from "@/types";
2 | import { useNavigate } from "react-router-dom";
3 | import {
4 | Card,
5 | CardDescription,
6 | CardFooter,
7 | CardTitle,
8 | } from "@/components/ui/card";
9 | import { Badge } from "./ui/badge";
10 | import { cn } from "@/lib/utils";
11 | import { TooltipButton } from "./tooltip-button";
12 | import { Eye, Newspaper, Sparkles } from "lucide-react";
13 |
14 | interface InterviewPinProps {
15 | interview: Interview;
16 | onMockPage?: boolean;
17 | }
18 |
19 | export const InterviewPin = ({
20 | interview,
21 | onMockPage = false,
22 | }: InterviewPinProps) => {
23 | const navigate = useNavigate();
24 |
25 | return (
26 |
27 | {interview?.position}
28 | {interview?.description}
29 |
30 | {interview?.techStack.split(",").map((word, index) => (
31 |
36 | {word}
37 |
38 | ))}
39 |
40 |
41 |
47 |
48 | {`${new Date(interview?.createdAt.toDate()).toLocaleDateString(
49 | "en-US",
50 | { dateStyle: "long" }
51 | )} - ${new Date(interview?.createdAt.toDate()).toLocaleTimeString(
52 | "en-US",
53 | { timeStyle: "short" }
54 | )}`}
55 |
56 |
57 | {!onMockPage && (
58 |
59 | {
63 | navigate(`/generate/${interview?.id}`, { replace: true });
64 | }}
65 | disbaled={false}
66 | buttonClassName="hover:text-sky-500"
67 | icon={ }
68 | loading={false}
69 | />
70 |
71 | {
75 | navigate(`/generate/feedback/${interview?.id}`, {
76 | replace: true,
77 | });
78 | }}
79 | disbaled={false}
80 | buttonClassName="hover:text-yellow-500"
81 | icon={ }
82 | loading={false}
83 | />
84 |
85 | {
89 | navigate(`/generate/interview/${interview?.id}`, {
90 | replace: true,
91 | });
92 | }}
93 | disbaled={false}
94 | buttonClassName="hover:text-sky-500"
95 | icon={ }
96 | loading={false}
97 | />
98 |
99 | )}
100 |
101 |
102 | );
103 | };
104 |
--------------------------------------------------------------------------------
/src/components/profile-container.tsx:
--------------------------------------------------------------------------------
1 | import { useAuth, UserButton } from "@clerk/clerk-react";
2 | import { Loader } from "lucide-react";
3 | import { Button } from "./ui/button";
4 | import { Link } from "react-router-dom";
5 |
6 | export const ProfileContainer = () => {
7 | const { isSignedIn, isLoaded } = useAuth();
8 |
9 | if (!isLoaded) {
10 | return (
11 |
12 |
13 |
14 | );
15 | }
16 |
17 | return (
18 |
19 | {isSignedIn ? (
20 |
21 | ) : (
22 |
23 | Get Started
24 |
25 | )}
26 |
27 | );
28 | };
29 |
--------------------------------------------------------------------------------
/src/components/question-section.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
3 | import { cn } from "@/lib/utils";
4 | import { TooltipButton } from "./tooltip-button";
5 | import { Volume2, VolumeX } from "lucide-react";
6 | import { RecordAnswer } from "./record-answer";
7 |
8 | interface QuestionSectionProps {
9 | questions: { question: string; answer: string }[];
10 | }
11 |
12 | export const QuestionSection = ({ questions }: QuestionSectionProps) => {
13 | const [isPlaying, setIsPlaying] = useState(false);
14 | const [isWebCam, setIsWebCam] = useState(false);
15 |
16 | const [currentSpeech, setCurrentSpeech] =
17 | useState(null);
18 |
19 | const handlePlayQuestion = (qst: string) => {
20 | if (isPlaying && currentSpeech) {
21 | // stop the speech if already playing
22 | window.speechSynthesis.cancel();
23 | setIsPlaying(false);
24 | setCurrentSpeech(null);
25 | } else {
26 | if ("speechSynthesis" in window) {
27 | const speech = new SpeechSynthesisUtterance(qst);
28 | window.speechSynthesis.speak(speech);
29 | setIsPlaying(true);
30 | setCurrentSpeech(speech);
31 |
32 | // handle the speech end
33 | speech.onend = () => {
34 | setIsPlaying(false);
35 | setCurrentSpeech(null);
36 | };
37 | }
38 | }
39 | };
40 |
41 | return (
42 |
43 |
48 |
49 | {questions?.map((tab, i) => (
50 |
57 | {`Question #${i + 1}`}
58 |
59 | ))}
60 |
61 |
62 | {questions?.map((tab, i) => (
63 |
64 |
65 | {tab.question}
66 |
67 |
68 |
69 |
74 | ) : (
75 |
76 | )
77 | }
78 | onClick={() => handlePlayQuestion(tab.question)}
79 | />
80 |
81 |
82 |
87 |
88 | ))}
89 |
90 |
91 | );
92 | };
93 |
--------------------------------------------------------------------------------
/src/components/record-answer.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { useAuth } from "@clerk/clerk-react";
3 | import {
4 | CircleStop,
5 | Loader,
6 | Mic,
7 | RefreshCw,
8 | Save,
9 | Video,
10 | VideoOff,
11 | WebcamIcon,
12 | } from "lucide-react";
13 | import { useEffect, useState } from "react";
14 | import useSpeechToText, { ResultType } from "react-hook-speech-to-text";
15 | import { useParams } from "react-router-dom";
16 | import WebCam from "react-webcam";
17 | import { TooltipButton } from "./tooltip-button";
18 | import { toast } from "sonner";
19 | import { chatSession } from "@/scripts";
20 | import { SaveModal } from "./save-modal";
21 | import {
22 | addDoc,
23 | collection,
24 | getDocs,
25 | query,
26 | serverTimestamp,
27 | where,
28 | } from "firebase/firestore";
29 | import { db } from "@/config/firebase.config";
30 |
31 | interface RecordAnswerProps {
32 | question: { question: string; answer: string };
33 | isWebCam: boolean;
34 | setIsWebCam: (value: boolean) => void;
35 | }
36 |
37 | interface AIResponse {
38 | ratings: number;
39 | feedback: string;
40 | }
41 |
42 | export const RecordAnswer = ({
43 | question,
44 | isWebCam,
45 | setIsWebCam,
46 | }: RecordAnswerProps) => {
47 | const {
48 | interimResult,
49 | isRecording,
50 | results,
51 | startSpeechToText,
52 | stopSpeechToText,
53 | } = useSpeechToText({
54 | continuous: true,
55 | useLegacyResults: false,
56 | });
57 |
58 | const [userAnswer, setUserAnswer] = useState("");
59 | const [isAiGenerating, setIsAiGenerating] = useState(false);
60 | const [aiResult, setAiResult] = useState(null);
61 | const [open, setOpen] = useState(false);
62 | const [loading, setLoading] = useState(false);
63 |
64 | const { userId } = useAuth();
65 | const { interviewId } = useParams();
66 |
67 | const recordUserAnswer = async () => {
68 | if (isRecording) {
69 | stopSpeechToText();
70 |
71 | if (userAnswer?.length < 30) {
72 | toast.error("Error", {
73 | description: "Your answer should be more than 30 characters",
74 | });
75 |
76 | return;
77 | }
78 |
79 | // ai result
80 | const aiResult = await generateResult(
81 | question.question,
82 | question.answer,
83 | userAnswer
84 | );
85 |
86 | setAiResult(aiResult);
87 | } else {
88 | startSpeechToText();
89 | }
90 | };
91 |
92 | const cleanJsonResponse = (responseText: string) => {
93 | // Step 1: Trim any surrounding whitespace
94 | let cleanText = responseText.trim();
95 |
96 | // Step 2: Remove any occurrences of "json" or code block symbols (``` or `)
97 | cleanText = cleanText.replace(/(json|```|`)/g, "");
98 |
99 | // Step 3: Parse the clean JSON text into an array of objects
100 | try {
101 | return JSON.parse(cleanText);
102 | } catch (error) {
103 | throw new Error("Invalid JSON format: " + (error as Error)?.message);
104 | }
105 | };
106 |
107 | const generateResult = async (
108 | qst: string,
109 | qstAns: string,
110 | userAns: string
111 | ): Promise => {
112 | setIsAiGenerating(true);
113 | const prompt = `
114 | Question: "${qst}"
115 | User Answer: "${userAns}"
116 | Correct Answer: "${qstAns}"
117 | Please compare the user's answer to the correct answer, and provide a rating (from 1 to 10) based on answer quality, and offer feedback for improvement.
118 | Return the result in JSON format with the fields "ratings" (number) and "feedback" (string).
119 | `;
120 |
121 | try {
122 | const aiResult = await chatSession.sendMessage(prompt);
123 |
124 | const parsedResult: AIResponse = cleanJsonResponse(
125 | aiResult.response.text()
126 | );
127 | return parsedResult;
128 | } catch (error) {
129 | console.log(error);
130 | toast("Error", {
131 | description: "An error occurred while generating feedback.",
132 | });
133 | return { ratings: 0, feedback: "Unable to generate feedback" };
134 | } finally {
135 | setIsAiGenerating(false);
136 | }
137 | };
138 |
139 | const recordNewAnswer = () => {
140 | setUserAnswer("");
141 | stopSpeechToText();
142 | startSpeechToText();
143 | };
144 |
145 | const saveUserAnswer = async () => {
146 | setLoading(true);
147 |
148 | if (!aiResult) {
149 | return;
150 | }
151 |
152 | const currentQuestion = question.question;
153 | try {
154 | // query the firbase to check if the user answer already exists for this question
155 |
156 | const userAnswerQuery = query(
157 | collection(db, "userAnswers"),
158 | where("userId", "==", userId),
159 | where("question", "==", currentQuestion)
160 | );
161 |
162 | const querySnap = await getDocs(userAnswerQuery);
163 |
164 | // if the user already answerd the question dont save it again
165 | if (!querySnap.empty) {
166 | console.log("Query Snap Size", querySnap.size);
167 | toast.info("Already Answered", {
168 | description: "You have already answered this question",
169 | });
170 | return;
171 | } else {
172 | // save the user answer
173 |
174 | await addDoc(collection(db, "userAnswers"), {
175 | mockIdRef: interviewId,
176 | question: question.question,
177 | correct_ans: question.answer,
178 | user_ans: userAnswer,
179 | feedback: aiResult.feedback,
180 | rating: aiResult.ratings,
181 | userId,
182 | createdAt: serverTimestamp(),
183 | });
184 |
185 | toast("Saved", { description: "Your answer has been saved.." });
186 | }
187 |
188 | setUserAnswer("");
189 | stopSpeechToText();
190 | } catch (error) {
191 | toast("Error", {
192 | description: "An error occurred while generating feedback.",
193 | });
194 | console.log(error);
195 | } finally {
196 | setLoading(false);
197 | setOpen(!open);
198 | }
199 | };
200 |
201 | useEffect(() => {
202 | const combineTranscripts = results
203 | .filter((result): result is ResultType => typeof result !== "string")
204 | .map((result) => result.transcript)
205 | .join(" ");
206 |
207 | setUserAnswer(combineTranscripts);
208 | }, [results]);
209 |
210 | return (
211 |
212 | {/* save modal */}
213 |
setOpen(false)}
216 | onConfirm={saveUserAnswer}
217 | loading={loading}
218 | />
219 |
220 |
221 | {isWebCam ? (
222 | setIsWebCam(true)}
224 | onUserMediaError={() => setIsWebCam(false)}
225 | className="w-full h-full object-cover rounded-md"
226 | />
227 | ) : (
228 |
229 | )}
230 |
231 |
232 |
233 |
238 | ) : (
239 |
240 | )
241 | }
242 | onClick={() => setIsWebCam(!isWebCam)}
243 | />
244 |
245 |
250 | ) : (
251 |
252 | )
253 | }
254 | onClick={recordUserAnswer}
255 | />
256 |
257 | }
260 | onClick={recordNewAnswer}
261 | />
262 |
263 |
268 | ) : (
269 |
270 | )
271 | }
272 | onClick={() => setOpen(!open)}
273 | disbaled={!aiResult}
274 | />
275 |
276 |
277 |
278 |
Your Answer:
279 |
280 |
281 | {userAnswer || "Start recording to see your ansewer here"}
282 |
283 |
284 | {interimResult && (
285 |
286 | Current Speech:
287 | {interimResult}
288 |
289 | )}
290 |
291 |
292 | );
293 | };
294 |
--------------------------------------------------------------------------------
/src/components/save-modal.tsx:
--------------------------------------------------------------------------------
1 | import Modal from "./modal";
2 | import { Button } from "./ui/button";
3 |
4 | interface SaveModalProps {
5 | isOpen: boolean;
6 | onClose: () => void;
7 | onConfirm: () => void;
8 | loading: boolean;
9 | }
10 |
11 | export const SaveModal = ({
12 | isOpen,
13 | onClose,
14 | onConfirm,
15 | loading,
16 | }: SaveModalProps) => {
17 | return (
18 |
24 |
25 |
26 | Cancel
27 |
28 |
33 | Continue
34 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/src/components/toggle-container.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Sheet,
3 | SheetContent,
4 | SheetHeader,
5 | SheetTitle,
6 | SheetTrigger,
7 | } from "@/components/ui/sheet";
8 | import { Menu } from "lucide-react";
9 | import { NavigationRoutes } from "./navigation-routes";
10 | import { useAuth } from "@clerk/clerk-react";
11 | import { NavLink } from "react-router-dom";
12 | import { cn } from "@/lib/utils";
13 |
14 | export const ToggleContainer = () => {
15 | const { userId } = useAuth();
16 | return (
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {userId && (
29 |
32 | cn(
33 | "text-base text-neutral-600 ",
34 | isActive && "text-neutral-900 font-semibold"
35 | )
36 | }
37 | >
38 | Take An Interview
39 |
40 | )}
41 |
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/components/tooltip-button.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Tooltip,
3 | TooltipContent,
4 | TooltipProvider,
5 | TooltipTrigger,
6 | } from "@/components/ui/tooltip";
7 | import { Button } from "@/components/ui/button";
8 | import { Loader } from "lucide-react";
9 |
10 | // assuming the button variants types are something like following
11 | type ButtonVariant =
12 | | "ghost"
13 | | "link"
14 | | "default"
15 | | "destructive"
16 | | "outline"
17 | | "secondary"
18 | | null
19 | | undefined;
20 |
21 | interface TooltipButtonProps {
22 | content: string;
23 | icon: React.ReactNode;
24 | onClick: () => void;
25 | buttonVariant?: ButtonVariant;
26 | buttonClassName?: string;
27 | delay?: number;
28 | disbaled?: boolean;
29 | loading?: boolean;
30 | }
31 |
32 | export const TooltipButton = ({
33 | content,
34 | icon,
35 | onClick,
36 | buttonVariant = "ghost",
37 | buttonClassName = "",
38 | delay = 0,
39 | disbaled = false,
40 | loading = false,
41 | }: TooltipButtonProps) => {
42 | return (
43 |
44 |
45 |
48 |
55 | {loading ? (
56 |
57 | ) : (
58 | icon
59 | )}
60 |
61 |
62 |
63 | {loading ? "Loading..." : content}
64 |
65 |
66 |
67 | );
68 | };
69 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 |
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
55 |
56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
57 |
--------------------------------------------------------------------------------
/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-background text-foreground",
12 | destructive:
13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | }
20 | )
21 |
22 | const Alert = React.forwardRef<
23 | HTMLDivElement,
24 | React.HTMLAttributes & VariantProps
25 | >(({ className, variant, ...props }, ref) => (
26 |
32 | ))
33 | Alert.displayName = "Alert"
34 |
35 | const AlertTitle = React.forwardRef<
36 | HTMLParagraphElement,
37 | React.HTMLAttributes
38 | >(({ className, ...props }, ref) => (
39 |
44 | ))
45 | AlertTitle.displayName = "AlertTitle"
46 |
47 | const AlertDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | AlertDescription.displayName = "AlertDescription"
58 |
59 | export { Alert, AlertTitle, AlertDescription }
60 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLDivElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
29 |
30 | const DialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, children, ...props }, ref) => (
34 |
35 |
36 |
44 | {children}
45 |
46 |
47 | Close
48 |
49 |
50 |
51 | ))
52 | DialogContent.displayName = DialogPrimitive.Content.displayName
53 |
54 | const DialogHeader = ({
55 | className,
56 | ...props
57 | }: React.HTMLAttributes) => (
58 |
65 | )
66 | DialogHeader.displayName = "DialogHeader"
67 |
68 | const DialogFooter = ({
69 | className,
70 | ...props
71 | }: React.HTMLAttributes) => (
72 |
79 | )
80 | DialogFooter.displayName = "DialogFooter"
81 |
82 | const DialogTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
94 | ))
95 | DialogTitle.displayName = DialogPrimitive.Title.displayName
96 |
97 | const DialogDescription = React.forwardRef<
98 | React.ElementRef,
99 | React.ComponentPropsWithoutRef
100 | >(({ className, ...props }, ref) => (
101 |
106 | ))
107 | DialogDescription.displayName = DialogPrimitive.Description.displayName
108 |
109 | export {
110 | Dialog,
111 | DialogPortal,
112 | DialogOverlay,
113 | DialogClose,
114 | DialogTrigger,
115 | DialogContent,
116 | DialogHeader,
117 | DialogFooter,
118 | DialogTitle,
119 | DialogDescription,
120 | }
121 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SheetPrimitive from "@radix-ui/react-dialog"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Sheet = SheetPrimitive.Root
9 |
10 | const SheetTrigger = SheetPrimitive.Trigger
11 |
12 | const SheetClose = SheetPrimitive.Close
13 |
14 | const SheetPortal = SheetPrimitive.Portal
15 |
16 | const SheetOverlay = React.forwardRef<
17 | React.ElementRef,
18 | React.ComponentPropsWithoutRef
19 | >(({ className, ...props }, ref) => (
20 |
28 | ))
29 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
30 |
31 | const sheetVariants = cva(
32 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
33 | {
34 | variants: {
35 | side: {
36 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
37 | bottom:
38 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
39 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
40 | right:
41 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
42 | },
43 | },
44 | defaultVariants: {
45 | side: "right",
46 | },
47 | }
48 | )
49 |
50 | interface SheetContentProps
51 | extends React.ComponentPropsWithoutRef,
52 | VariantProps {}
53 |
54 | const SheetContent = React.forwardRef<
55 | React.ElementRef,
56 | SheetContentProps
57 | >(({ side = "right", className, children, ...props }, ref) => (
58 |
59 |
60 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | ))
73 | SheetContent.displayName = SheetPrimitive.Content.displayName
74 |
75 | const SheetHeader = ({
76 | className,
77 | ...props
78 | }: React.HTMLAttributes) => (
79 |
86 | )
87 | SheetHeader.displayName = "SheetHeader"
88 |
89 | const SheetFooter = ({
90 | className,
91 | ...props
92 | }: React.HTMLAttributes) => (
93 |
100 | )
101 | SheetFooter.displayName = "SheetFooter"
102 |
103 | const SheetTitle = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | SheetTitle.displayName = SheetPrimitive.Title.displayName
114 |
115 | const SheetDescription = React.forwardRef<
116 | React.ElementRef,
117 | React.ComponentPropsWithoutRef
118 | >(({ className, ...props }, ref) => (
119 |
124 | ))
125 | SheetDescription.displayName = SheetPrimitive.Description.displayName
126 |
127 | export {
128 | Sheet,
129 | SheetPortal,
130 | SheetOverlay,
131 | SheetTrigger,
132 | SheetClose,
133 | SheetContent,
134 | SheetHeader,
135 | SheetFooter,
136 | SheetTitle,
137 | SheetDescription,
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes"
2 | import { Toaster as Sonner } from "sonner"
3 |
4 | type ToasterProps = React.ComponentProps
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme()
8 |
9 | return (
10 |
26 | )
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/src/config/firebase.config.ts:
--------------------------------------------------------------------------------
1 | import { getApp, getApps, initializeApp } from "firebase/app";
2 | import { getFirestore } from "firebase/firestore";
3 |
4 | const firebaseConfig = {
5 | apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
6 | authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
7 | projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
8 | storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
9 | messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
10 | appId: import.meta.env.VITE_FIREBASE_APP_ID,
11 | };
12 |
13 | const app = getApps.length > 0 ? getApp() : initializeApp(firebaseConfig);
14 |
15 | const db = getFirestore(app);
16 |
17 | export { db };
18 |
--------------------------------------------------------------------------------
/src/handlers/auth-handler.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@/config/firebase.config";
2 | import { LoaderPage } from "@/routes/loader-page";
3 | import { User } from "@/types";
4 | import { useAuth, useUser } from "@clerk/clerk-react";
5 | import { doc, getDoc, serverTimestamp, setDoc } from "firebase/firestore";
6 | import { useEffect, useState } from "react";
7 | import { useLocation, useNavigate } from "react-router-dom";
8 |
9 | const AuthHanlder = () => {
10 | const { isSignedIn } = useAuth();
11 | const { user } = useUser();
12 |
13 | const pathname = useLocation().pathname;
14 | const navigate = useNavigate();
15 |
16 | const [loading, setLoading] = useState(false);
17 |
18 | useEffect(() => {
19 | const storeUserData = async () => {
20 | if (isSignedIn && user) {
21 | setLoading(true);
22 | try {
23 | const userSanp = await getDoc(doc(db, "users", user.id));
24 | if (!userSanp.exists()) {
25 | const userData: User = {
26 | id: user.id,
27 | name: user.fullName || user.firstName || "Anonymous",
28 | email: user.primaryEmailAddress?.emailAddress || "N/A",
29 | imageUrl: user.imageUrl,
30 | createdAt: serverTimestamp(),
31 | updateAt: serverTimestamp(),
32 | };
33 |
34 | await setDoc(doc(db, "users", user.id), userData);
35 | }
36 | } catch (error) {
37 | console.log("Error on storing the user data : ", error);
38 | } finally {
39 | setLoading(false);
40 | }
41 | }
42 | };
43 |
44 | storeUserData();
45 | }, [isSignedIn, user, pathname, navigate]);
46 |
47 | if (loading) {
48 | return ;
49 | }
50 |
51 | return null;
52 | };
53 |
54 | export default AuthHanlder;
55 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 | @layer base {
5 | :root {
6 | --background: 0 0% 100%;
7 | --foreground: 0 0% 3.9%;
8 | --card: 0 0% 100%;
9 | --card-foreground: 0 0% 3.9%;
10 | --popover: 0 0% 100%;
11 | --popover-foreground: 0 0% 3.9%;
12 | --primary: 0 0% 9%;
13 | --primary-foreground: 0 0% 98%;
14 | --secondary: 0 0% 96.1%;
15 | --secondary-foreground: 0 0% 9%;
16 | --muted: 0 0% 96.1%;
17 | --muted-foreground: 0 0% 45.1%;
18 | --accent: 0 0% 96.1%;
19 | --accent-foreground: 0 0% 9%;
20 | --destructive: 0 84.2% 60.2%;
21 | --destructive-foreground: 0 0% 98%;
22 | --border: 0 0% 89.8%;
23 | --input: 0 0% 89.8%;
24 | --ring: 0 0% 3.9%;
25 | --chart-1: 12 76% 61%;
26 | --chart-2: 173 58% 39%;
27 | --chart-3: 197 37% 24%;
28 | --chart-4: 43 74% 66%;
29 | --chart-5: 27 87% 67%;
30 | --radius: 0.5rem;
31 | }
32 | .dark {
33 | --background: 0 0% 3.9%;
34 | --foreground: 0 0% 98%;
35 | --card: 0 0% 3.9%;
36 | --card-foreground: 0 0% 98%;
37 | --popover: 0 0% 3.9%;
38 | --popover-foreground: 0 0% 98%;
39 | --primary: 0 0% 98%;
40 | --primary-foreground: 0 0% 9%;
41 | --secondary: 0 0% 14.9%;
42 | --secondary-foreground: 0 0% 98%;
43 | --muted: 0 0% 14.9%;
44 | --muted-foreground: 0 0% 63.9%;
45 | --accent: 0 0% 14.9%;
46 | --accent-foreground: 0 0% 98%;
47 | --destructive: 0 62.8% 30.6%;
48 | --destructive-foreground: 0 0% 98%;
49 | --border: 0 0% 14.9%;
50 | --input: 0 0% 14.9%;
51 | --ring: 0 0% 83.1%;
52 | --chart-1: 220 70% 50%;
53 | --chart-2: 160 60% 45%;
54 | --chart-3: 30 80% 55%;
55 | --chart-4: 280 65% 60%;
56 | --chart-5: 340 75% 55%;
57 | }
58 | }
59 | @layer base {
60 | * {
61 | @apply border-border;
62 | }
63 | body {
64 | @apply bg-background text-foreground;
65 | }
66 |
67 | .text-outline {
68 | color: white;
69 | text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.6),
70 | /* Top-left */ 1px -1px 0 rgba(0, 0, 0, 0.6),
71 | /* Top-right */ -1px 1px 0 rgba(0, 0, 0, 0.6),
72 | /* Bottom-left */ 1px 1px 0 rgba(0, 0, 0, 0.6); /* Bottom-right */
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/layouts/auth-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from "react-router-dom";
2 |
3 | const AuthenticationLayout = () => {
4 | return (
5 |
6 |
11 |
12 |
13 | );
14 | };
15 |
16 | export default AuthenticationLayout;
17 |
--------------------------------------------------------------------------------
/src/layouts/main-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Container } from "@/components/container";
2 | import { Footer } from "@/components/footer";
3 |
4 | import Header from "@/components/header";
5 | import { Outlet } from "react-router-dom";
6 |
7 | export const MainLayout = () => {
8 | return (
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/src/layouts/protected-routes.tsx:
--------------------------------------------------------------------------------
1 | import { LoaderPage } from "@/routes/loader-page";
2 | import { useAuth } from "@clerk/clerk-react";
3 | import { Navigate } from "react-router-dom";
4 |
5 | const ProtectRoutes = ({ children }: { children: React.ReactNode }) => {
6 | const { isLoaded, isSignedIn } = useAuth();
7 |
8 | if (!isLoaded) {
9 | return ;
10 | }
11 |
12 | if (!isSignedIn) {
13 | return ;
14 | }
15 |
16 | return children;
17 | };
18 |
19 | export default ProtectRoutes;
20 |
--------------------------------------------------------------------------------
/src/layouts/public-layout.tsx:
--------------------------------------------------------------------------------
1 | import { Footer } from "@/components/footer";
2 | import Header from "@/components/header";
3 | import AuthHanlder from "@/handlers/auth-handler";
4 | import { Outlet } from "react-router-dom";
5 |
6 | export const PublicLayout = () => {
7 | return (
8 |
9 | {/* handler to store the user data */}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/src/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | export const MainRoutes = [
2 | {
3 | label: "Home",
4 | href: "/",
5 | },
6 | {
7 | label: "Contact Us",
8 | href: "/contact",
9 | },
10 | {
11 | label: "About Us",
12 | href: "/about",
13 | },
14 | {
15 | label: "Services",
16 | href: "/services",
17 | },
18 | ];
19 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 |
4 | import { ClerkProvider } from "@clerk/clerk-react";
5 |
6 | import "./index.css";
7 | import App from "./App.tsx";
8 | import { ToasterProvider } from "./provider/toast-provider.tsx";
9 |
10 | // Import your Publishable Key
11 | const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
12 |
13 | if (!PUBLISHABLE_KEY) {
14 | throw new Error("Missing Publishable Key");
15 | }
16 |
17 | createRoot(document.getElementById("root")!).render(
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
--------------------------------------------------------------------------------
/src/provider/toast-provider.tsx:
--------------------------------------------------------------------------------
1 | import { Toaster } from "@/components/ui/sonner";
2 |
3 | export const ToasterProvider = () => {
4 | return (
5 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/src/routes/create-edit-page.tsx:
--------------------------------------------------------------------------------
1 | import { FormMockInterview } from "@/components/form-mock-interview";
2 | import { db } from "@/config/firebase.config";
3 | import { Interview } from "@/types";
4 | import { doc, getDoc } from "firebase/firestore";
5 | import { useEffect, useState } from "react";
6 | import { useParams } from "react-router-dom";
7 |
8 | export const CreateEditPage = () => {
9 | const { interviewId } = useParams<{ interviewId: string }>();
10 | const [interview, setInterview] = useState(null);
11 |
12 | useEffect(() => {
13 | const fetchInterview = async () => {
14 | if (interviewId) {
15 | try {
16 | const interviewDoc = await getDoc(doc(db, "interviews", interviewId));
17 | if (interviewDoc.exists()) {
18 | setInterview({
19 | id: interviewDoc.id,
20 | ...interviewDoc.data(),
21 | } as Interview);
22 | }
23 | } catch (error) {
24 | console.log(error);
25 | }
26 | }
27 | };
28 |
29 | fetchInterview();
30 | }, [interviewId]);
31 |
32 | return (
33 |
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/src/routes/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import { Headings } from "@/components/headings";
2 | import { InterviewPin } from "@/components/pin";
3 | import { Button } from "@/components/ui/button";
4 | import { Separator } from "@/components/ui/separator";
5 | import { Skeleton } from "@/components/ui/skeleton";
6 | import { db } from "@/config/firebase.config";
7 | import { Interview } from "@/types";
8 | import { useAuth } from "@clerk/clerk-react";
9 | import { collection, onSnapshot, query, where } from "firebase/firestore";
10 | import { Plus } from "lucide-react";
11 | import { useEffect, useState } from "react";
12 | import { Link } from "react-router-dom";
13 | import { toast } from "sonner";
14 |
15 | export const Dashboard = () => {
16 | const [interviews, setInterviews] = useState([]);
17 | const [loading, setLoading] = useState(false);
18 | const { userId } = useAuth();
19 |
20 | useEffect(() => {
21 | setLoading(true);
22 | const interviewQuery = query(
23 | collection(db, "interviews"),
24 | where("userId", "==", userId)
25 | );
26 |
27 | const unsubscribe = onSnapshot(
28 | interviewQuery,
29 | (snapshot) => {
30 | const interviewList: Interview[] = snapshot.docs.map((doc) => {
31 | const id = doc.id;
32 | return {
33 | id,
34 | ...doc.data(),
35 | };
36 | }) as Interview[];
37 | setInterviews(interviewList);
38 | setLoading(false);
39 | },
40 | (error) => {
41 | console.log("Error on fetching : ", error);
42 | toast.error("Error..", {
43 | description: "SOmething went wrong.. Try again later..",
44 | });
45 | setLoading(false);
46 | }
47 | );
48 |
49 | return () => unsubscribe();
50 | }, [userId]);
51 |
52 | return (
53 | <>
54 |
55 | {/* headings */}
56 |
60 |
61 |
62 | Add New
63 |
64 |
65 |
66 |
67 |
68 | {/* content section */}
69 |
70 |
71 | {loading ? (
72 | Array.from({ length: 6 }).map((_, index) => (
73 |
74 | ))
75 | ) : interviews.length > 0 ? (
76 | interviews.map((interview) => (
77 |
78 | ))
79 | ) : (
80 |
81 |
86 |
87 |
88 | No Data Found
89 |
90 |
91 |
92 | There is no available data to show. Please add some new mock
93 | interviews
94 |
95 |
96 |
97 |
98 |
99 | Add New
100 |
101 |
102 |
103 | )}
104 |
105 | >
106 | );
107 | };
108 |
--------------------------------------------------------------------------------
/src/routes/feedback.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@/config/firebase.config";
2 | import { Interview, UserAnswer } from "@/types";
3 | import { useAuth } from "@clerk/clerk-react";
4 | import {
5 | collection,
6 | doc,
7 | getDoc,
8 | getDocs,
9 | query,
10 | where,
11 | } from "firebase/firestore";
12 | import { useEffect, useMemo, useState } from "react";
13 | import { useNavigate, useParams } from "react-router-dom";
14 | import { toast } from "sonner";
15 | import { LoaderPage } from "./loader-page";
16 | import { CustomBreadCrumb } from "@/components/custom-bread-crumb";
17 | import { Headings } from "@/components/headings";
18 | import { InterviewPin } from "@/components/pin";
19 | import {
20 | Accordion,
21 | AccordionContent,
22 | AccordionItem,
23 | AccordionTrigger,
24 | } from "@/components/ui/accordion";
25 | import { cn } from "@/lib/utils";
26 | import { CircleCheck, Star } from "lucide-react";
27 | import { Card, CardDescription, CardTitle } from "@/components/ui/card";
28 |
29 | export const Feedback = () => {
30 | const { interviewId } = useParams<{ interviewId: string }>();
31 | const [interview, setInterview] = useState(null);
32 | const [isLoading, setIsLoading] = useState(false);
33 | const [feedbacks, setFeedbacks] = useState([]);
34 | const [activeFeed, setActiveFeed] = useState("");
35 | const { userId } = useAuth();
36 | const navigate = useNavigate();
37 |
38 | if (!interviewId) {
39 | navigate("/generate", { replace: true });
40 | }
41 | useEffect(() => {
42 | if (interviewId) {
43 | const fetchInterview = async () => {
44 | if (interviewId) {
45 | try {
46 | const interviewDoc = await getDoc(
47 | doc(db, "interviews", interviewId)
48 | );
49 | if (interviewDoc.exists()) {
50 | setInterview({
51 | id: interviewDoc.id,
52 | ...interviewDoc.data(),
53 | } as Interview);
54 | }
55 | } catch (error) {
56 | console.log(error);
57 | }
58 | }
59 | };
60 |
61 | const fetchFeedbacks = async () => {
62 | setIsLoading(true);
63 | try {
64 | const querSanpRef = query(
65 | collection(db, "userAnswers"),
66 | where("userId", "==", userId),
67 | where("mockIdRef", "==", interviewId)
68 | );
69 |
70 | const querySnap = await getDocs(querSanpRef);
71 |
72 | const interviewData: UserAnswer[] = querySnap.docs.map((doc) => {
73 | return { id: doc.id, ...doc.data() } as UserAnswer;
74 | });
75 |
76 | setFeedbacks(interviewData);
77 | } catch (error) {
78 | console.log(error);
79 | toast("Error", {
80 | description: "Something went wrong. Please try again later..",
81 | });
82 | } finally {
83 | setIsLoading(false);
84 | }
85 | };
86 | fetchInterview();
87 | fetchFeedbacks();
88 | }
89 | }, [interviewId, navigate, userId]);
90 |
91 | // calculate the ratings out of 10
92 |
93 | const overAllRating = useMemo(() => {
94 | if (feedbacks.length === 0) return "0.0";
95 |
96 | const totalRatings = feedbacks.reduce(
97 | (acc, feedback) => acc + feedback.rating,
98 | 0
99 | );
100 |
101 | return (totalRatings / feedbacks.length).toFixed(1);
102 | }, [feedbacks]);
103 |
104 | if (isLoading) {
105 | return ;
106 | }
107 |
108 | return (
109 |
110 |
111 |
121 |
122 |
123 |
127 |
128 |
129 | Your overall interview ratings :{" "}
130 |
131 | {overAllRating} / 10
132 |
133 |
134 |
135 | {interview &&
}
136 |
137 |
138 |
139 | {feedbacks && (
140 |
141 | {feedbacks.map((feed) => (
142 |
147 | setActiveFeed(feed.id)}
149 | className={cn(
150 | "px-5 py-3 flex items-center justify-between text-base rounded-t-lg transition-colors hover:no-underline",
151 | activeFeed === feed.id
152 | ? "bg-gradient-to-r from-purple-50 to-blue-50"
153 | : "hover:bg-gray-50"
154 | )}
155 | >
156 | {feed.question}
157 |
158 |
159 |
160 |
161 |
162 | Rating : {feed.rating}
163 |
164 |
165 |
166 |
167 |
168 | Expected Answer
169 |
170 |
171 |
172 | {feed.correct_ans}
173 |
174 |
175 |
176 |
177 |
178 |
179 | Your Answer
180 |
181 |
182 |
183 | {feed.user_ans}
184 |
185 |
186 |
187 |
188 |
189 |
190 | Feedback
191 |
192 |
193 |
194 | {feed.feedback}
195 |
196 |
197 |
198 |
199 | ))}
200 |
201 | )}
202 |
203 | );
204 | };
205 |
--------------------------------------------------------------------------------
/src/routes/home.tsx:
--------------------------------------------------------------------------------
1 | import { Sparkles } from "lucide-react";
2 | import Marquee from "react-fast-marquee";
3 |
4 | import { Container } from "@/components/container";
5 | import { Button } from "@/components/ui/button";
6 | import { MarqueImg } from "@/components/marquee-img";
7 | import { Link } from "react-router-dom";
8 |
9 | const HomePage = () => {
10 | return (
11 |
12 |
13 |
14 |
15 |
16 | AI Superpower
17 |
18 |
19 | - A better way to
20 |
21 |
22 | improve your interview chances and skills
23 |
24 |
25 |
26 | Boost your interview skills and increase your success rate with
27 | AI-driven insights. Discover a smarter way to prepare, practice, and
28 | stand out.
29 |
30 |
31 |
32 |
33 |
34 | 250k+
35 |
36 | Offers Recieved
37 |
38 |
39 |
40 | 1.2M+
41 |
42 | Interview Aced
43 |
44 |
45 |
46 |
47 | {/* image section */}
48 |
49 |
54 |
55 |
56 | Inteviews Copilot©
57 |
58 |
59 |
60 |
Developer
61 |
62 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Numquam
63 | distinctio natus, quos voluptatibus magni sapiente.
64 |
65 |
66 |
67 | Generate
68 |
69 |
70 |
71 |
72 |
73 | {/* marquee section */}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 | Unleash your potential with personalized AI insights and targeted
90 | interview practice.
91 |
92 |
93 |
94 |
95 |
100 |
101 |
102 |
103 |
104 | Transform the way you prepare, gain confidence, and boost your
105 | chances of landing your dream job. Let AI be your edge in
106 | today's competitive job market.
107 |
108 |
109 |
110 |
111 | Generate
112 |
113 |
114 |
115 |
116 |
117 |
118 | );
119 | };
120 |
121 | export default HomePage;
122 |
--------------------------------------------------------------------------------
/src/routes/loader-page.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils";
2 | import { Loader } from "lucide-react";
3 |
4 | export const LoaderPage = ({ className }: { className?: string }) => {
5 | return (
6 |
12 |
13 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/src/routes/mock-interview-page.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { Interview } from "@/types";
3 | import { useEffect, useState } from "react";
4 | import { useNavigate, useParams } from "react-router-dom";
5 | import { LoaderPage } from "./loader-page";
6 | import { doc, getDoc } from "firebase/firestore";
7 | import { db } from "@/config/firebase.config";
8 | import { CustomBreadCrumb } from "@/components/custom-bread-crumb";
9 |
10 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
11 | import { Lightbulb } from "lucide-react";
12 | import { QuestionSection } from "@/components/question-section";
13 |
14 | export const MockInterviewPage = () => {
15 | const { interviewId } = useParams<{ interviewId: string }>();
16 | const [interview, setInterview] = useState(null);
17 |
18 | const [isLoading, setIsLoading] = useState(false);
19 |
20 | const navigate = useNavigate();
21 |
22 | useEffect(() => {
23 | setIsLoading(true);
24 | const fetchInterview = async () => {
25 | if (interviewId) {
26 | try {
27 | const interviewDoc = await getDoc(doc(db, "interviews", interviewId));
28 | if (interviewDoc.exists()) {
29 | setInterview({
30 | id: interviewDoc.id,
31 | ...interviewDoc.data(),
32 | } as Interview);
33 | }
34 | } catch (error) {
35 | console.log(error);
36 | } finally {
37 | setIsLoading(false);
38 | }
39 | }
40 | };
41 |
42 | fetchInterview();
43 | }, [interviewId, navigate]);
44 |
45 | if (isLoading) {
46 | return ;
47 | }
48 |
49 | if (!interviewId) {
50 | navigate("/generate", { replace: true });
51 | }
52 |
53 | if (!interview) {
54 | navigate("/generate", { replace: true });
55 | }
56 |
57 | return (
58 |
59 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | Important Note
76 |
77 |
78 | Press "Record Answer" to begin answering the question. Once you
79 | finish the interview, you'll receive feedback comparing your
80 | responses with the ideal answers.
81 |
82 |
83 | Note: {" "}
84 | Your video is never recorded. {" "}
85 | You can disable the webcam anytime if preferred.
86 |
87 |
88 |
89 |
90 |
91 | {interview?.questions && interview?.questions.length > 0 && (
92 |
93 |
94 |
95 | )}
96 |
97 | );
98 | };
99 |
--------------------------------------------------------------------------------
/src/routes/mock-load-page.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unused-vars */
2 | import { db } from "@/config/firebase.config";
3 | import { Interview } from "@/types";
4 | import { doc, getDoc } from "firebase/firestore";
5 | import { useEffect, useState } from "react";
6 | import { Link, useNavigate, useParams } from "react-router-dom";
7 | import { LoaderPage } from "./loader-page";
8 | import { CustomBreadCrumb } from "@/components/custom-bread-crumb";
9 | import { Button } from "@/components/ui/button";
10 | import { Lightbulb, Sparkles, WebcamIcon } from "lucide-react";
11 | import { InterviewPin } from "@/components/pin";
12 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
13 | import WebCam from "react-webcam";
14 |
15 | export const MockLoadPage = () => {
16 | const { interviewId } = useParams<{ interviewId: string }>();
17 | const [interview, setInterview] = useState(null);
18 | const [isLoading, setIsLoading] = useState(false);
19 | const [isWebCamEnabled, setIsWebCamEnabled] = useState(false);
20 |
21 | const navigate = useNavigate();
22 |
23 | useEffect(() => {
24 | setIsLoading(true);
25 | const fetchInterview = async () => {
26 | if (interviewId) {
27 | try {
28 | const interviewDoc = await getDoc(doc(db, "interviews", interviewId));
29 | if (interviewDoc.exists()) {
30 | setInterview({
31 | id: interviewDoc.id,
32 | ...interviewDoc.data(),
33 | } as Interview);
34 | }
35 | } catch (error) {
36 | console.log(error);
37 | } finally {
38 | setIsLoading(false);
39 | }
40 | }
41 | };
42 |
43 | fetchInterview();
44 | }, [interviewId, navigate]);
45 |
46 | if (isLoading) {
47 | return ;
48 | }
49 |
50 | if (!interviewId) {
51 | navigate("/generate", { replace: true });
52 | }
53 |
54 | if (!interview) {
55 | navigate("/generate", { replace: true });
56 | }
57 |
58 | return (
59 |
60 |
61 |
65 |
66 |
67 |
68 | Start
69 |
70 |
71 |
72 |
73 | {interview &&
}
74 |
75 |
76 |
77 |
78 |
79 | Important Information
80 |
81 |
82 | Please enable your webcam and microphone to start the AI-generated
83 | mock interview. The interview consists of five questions. You’ll
84 | receive a personalized report based on your responses at the end.{" "}
85 |
86 |
87 | Note: Your video is{" "}
88 | never recorded . You can disable your webcam at any
89 | time.
90 |
91 |
92 |
93 |
94 |
95 |
96 | {isWebCamEnabled ? (
97 | setIsWebCamEnabled(true)}
99 | onUserMediaError={() => setIsWebCamEnabled(false)}
100 | className="w-full h-full object-cover rounded-md"
101 | />
102 | ) : (
103 |
104 | )}
105 |
106 |
107 |
108 |
109 | setIsWebCamEnabled(!isWebCamEnabled)}>
110 | {isWebCamEnabled ? "Disable Webcam" : "Enable Webcam"}
111 |
112 |
113 |
114 | );
115 | };
116 |
--------------------------------------------------------------------------------
/src/routes/sign-in.tsx:
--------------------------------------------------------------------------------
1 | import { SignIn } from "@clerk/clerk-react";
2 |
3 | export const SignInPage = () => {
4 | return ;
5 | };
6 |
--------------------------------------------------------------------------------
/src/routes/sign-up.tsx:
--------------------------------------------------------------------------------
1 | import { SignUp } from "@clerk/clerk-react";
2 |
3 | export const SignUpPage = () => {
4 | return ;
5 | };
6 |
--------------------------------------------------------------------------------
/src/scripts/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | GoogleGenerativeAI,
3 | HarmCategory,
4 | HarmBlockThreshold,
5 | } from "@google/generative-ai";
6 |
7 | const apiKey = import.meta.env.VITE_GEMINI_API_KEY!;
8 | const genAI = new GoogleGenerativeAI(apiKey);
9 |
10 | const model = genAI.getGenerativeModel({
11 | model: "gemini-2.0-flash-exp",
12 | });
13 |
14 | const generationConfig = {
15 | temperature: 1,
16 | topP: 0.95,
17 | topK: 40,
18 | maxOutputTokens: 8192,
19 | responseMimeType: "text/plain",
20 | };
21 |
22 | const safetySettings = [
23 | {
24 | category: HarmCategory.HARM_CATEGORY_HARASSMENT,
25 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
26 | },
27 | {
28 | category: HarmCategory.HARM_CATEGORY_HATE_SPEECH,
29 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
30 | },
31 | {
32 | category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
33 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
34 | },
35 | {
36 | category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
37 | threshold: HarmBlockThreshold.BLOCK_MEDIUM_AND_ABOVE,
38 | },
39 | ];
40 |
41 | export const chatSession = model.startChat({
42 | generationConfig,
43 | safetySettings,
44 | });
45 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import { FieldValue, Timestamp } from "firebase/firestore";
2 |
3 | export interface User {
4 | id: string;
5 | name: string;
6 | email: string;
7 | imageUrl: string;
8 | createdAt: Timestamp | FieldValue;
9 | updateAt: Timestamp | FieldValue;
10 | }
11 |
12 | export interface Interview {
13 | id: string;
14 | position: string;
15 | description: string;
16 | experience: number;
17 | userId: string;
18 | techStack: string;
19 | questions: { question: string; answer: string }[];
20 | createdAt: Timestamp;
21 | updateAt: Timestamp;
22 | }
23 |
24 | export interface UserAnswer {
25 | id: string;
26 | mockIdRef: string;
27 | question: string;
28 | correct_ans: string;
29 | user_ans: string;
30 | feedback: string;
31 | rating: number;
32 | userId: string;
33 | createdAt: Timestamp;
34 | updateAt: Timestamp;
35 | }
36 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: ["class"],
4 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
5 | theme: {
6 | extend: {
7 | borderRadius: {
8 | lg: 'var(--radius)',
9 | md: 'calc(var(--radius) - 2px)',
10 | sm: 'calc(var(--radius) - 4px)'
11 | },
12 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | DEFAULT: 'hsl(var(--secondary))',
29 | foreground: 'hsl(var(--secondary-foreground))'
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))'
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))'
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))'
42 | },
43 | border: 'hsl(var(--border))',
44 | input: 'hsl(var(--input))',
45 | ring: 'hsl(var(--ring))',
46 | chart: {
47 | '1': 'hsl(var(--chart-1))',
48 | '2': 'hsl(var(--chart-2))',
49 | '3': 'hsl(var(--chart-3))',
50 | '4': 'hsl(var(--chart-4))',
51 | '5': 'hsl(var(--chart-5))'
52 | }
53 | },
54 | keyframes: {
55 | 'accordion-down': {
56 | from: {
57 | height: '0'
58 | },
59 | to: {
60 | height: 'var(--radix-accordion-content-height)'
61 | }
62 | },
63 | 'accordion-up': {
64 | from: {
65 | height: 'var(--radix-accordion-content-height)'
66 | },
67 | to: {
68 | height: '0'
69 | }
70 | }
71 | },
72 | animation: {
73 | 'accordion-down': 'accordion-down 0.2s ease-out',
74 | 'accordion-up': 'accordion-up 0.2s ease-out'
75 | }
76 | }
77 | },
78 | plugins: [require("tailwindcss-animate")],
79 | };
80 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true,
24 | "baseUrl": ".",
25 | "paths": {
26 | "@/*": ["./src/*"]
27 | }
28 | },
29 | "include": ["src"]
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ],
7 | "compilerOptions": {
8 | "baseUrl": ".",
9 | "paths": {
10 | "@/*": ["./src/*"]
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 | import { defineConfig } from "vite";
3 | import react from "@vitejs/plugin-react";
4 |
5 | // https://vite.dev/config/
6 | export default defineConfig({
7 | plugins: [react()],
8 | resolve: {
9 | alias: {
10 | "@": path.resolve(__dirname, "./src"),
11 | },
12 | },
13 | });
14 |
--------------------------------------------------------------------------------