diff --git a/client/package-lock.json b/client/package-lock.json index 59e2458..e12893b 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,10 +8,12 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@react-oauth/google": "^0.13.4", "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router": "^7.13.1", + "react-router-dom": "^7.14.0", "sass": "^1.98.0" }, "devDependencies": { @@ -886,6 +888,16 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@react-oauth/google": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.4.tgz", + "integrity": "sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", @@ -2916,9 +2928,9 @@ } }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz", + "integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -2937,6 +2949,22 @@ } } }, + "node_modules/react-router-dom": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz", + "integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", diff --git a/client/package.json b/client/package.json index b7a59a5..2fa6eac 100644 --- a/client/package.json +++ b/client/package.json @@ -10,10 +10,12 @@ "preview": "vite preview" }, "dependencies": { + "@react-oauth/google": "^0.13.4", "axios": "^1.13.6", "react": "^19.2.4", "react-dom": "^19.2.4", "react-router": "^7.13.1", + "react-router-dom": "^7.14.0", "sass": "^1.98.0" }, "devDependencies": { diff --git a/client/src/App.jsx b/client/src/App.jsx index a6b70c0..a911cfd 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -3,18 +3,23 @@ import {router} from "./app.routes.jsx" import { AuthProvider } from "./features/auth/auth.context.jsx" import { InterviewProvider } from "./features/auth/interview/pages/interview.context.jsx" +// ✅ NEW IMPORT +import { GoogleOAuthProvider } from '@react-oauth/google' + function App() { return ( - + // ✅ SABSE OUTER WRAPPER + + + + + + + - - - - - - + ) } -export default App +export default App \ No newline at end of file diff --git a/client/src/app.routes.jsx b/client/src/app.routes.jsx index 595a528..c970dac 100644 --- a/client/src/app.routes.jsx +++ b/client/src/app.routes.jsx @@ -1,26 +1,58 @@ -import {createBrowserRouter} from "react-router" -import Login from "./features/auth/pages/Login.jsx" -import Register from "./features/auth/pages/Register.jsx" -import Protected from "./features/auth/components/Protected.jsx" -import Home from "./features/auth/interview/pages/Home.jsx" -import Interview from "./features/auth/interview/pages/Interview.jsx" +import { createBrowserRouter, Navigate } from "react-router-dom"; -export const router=createBrowserRouter([ - { - path:"/login", - element: - }, - { - path:"/register", - element: - }, - { - path:"/", - element: - }, - { - path:"/interview/:interviewId", - element: - } -]) +import Landing from "./features/auth/pages/Landing.jsx"; +import Login from "./features/auth/pages/Login.jsx"; +import Register from "./features/auth/pages/Register.jsx"; +import ForgotPassword from "./features/auth/pages/ForgotPassword.jsx"; +import VerifyCode from "./features/auth/pages/VerifyCode.jsx"; +import ResetPassword from "./features/auth/pages/ResetPassword.jsx"; +import Protected from "./features/auth/components/Protected.jsx"; +import Home from "./features/auth/interview/pages/Home.jsx"; +import Interview from "./features/auth/interview/pages/Interview.jsx"; +export const router = createBrowserRouter([ + { + path: "/", + element: , + }, + { + path: "/login", + element: , + }, + { + path: "/register", + element: , + }, + { + path: "/forgot-password", + element: , + }, + { + path: "/verify-code", + element: , + }, + { + path: "/reset-password", + element: , + }, + { + path: "/dashboard", + element: ( + + + + ), + }, + { + path: "/interview/:interviewId", + element: ( + + + + ), + }, + { + path: "*", + element: , + }, +]); \ No newline at end of file diff --git a/client/src/features/auth/auth.context.jsx b/client/src/features/auth/auth.context.jsx index 3cd4c41..474460c 100644 --- a/client/src/features/auth/auth.context.jsx +++ b/client/src/features/auth/auth.context.jsx @@ -1,14 +1,42 @@ -import {createContext, useState} from "react" +// src/features/auth/auth.context.jsx +import { createContext, useState, useEffect } from "react"; +import { getMe } from "../auth/services/auth.api"; -export const AuthContext = createContext() +export const AuthContext = createContext(); -export const AuthProvider = ({children}) => { - const [user, setUser] = useState(null) - const [loading, setLoading] = useState(true) // ✅ true rakho +export const AuthProvider = ({ children }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - return( - + // Check login status on app start + useEffect(() => { + const checkAuth = async () => { + try { + const data = await getMe(); + setUser(data.user || data); + } catch (err) { + setUser(null); + } finally { + setLoading(false); + } + }; + checkAuth(); + }, []); + + const clearError = () => setError(null); + + return ( + {children} - ) -} \ No newline at end of file + ); +}; \ No newline at end of file diff --git a/client/src/features/auth/auth.form.scss b/client/src/features/auth/auth.form.scss index 30a6fd8..56cd5d3 100644 --- a/client/src/features/auth/auth.form.scss +++ b/client/src/features/auth/auth.form.scss @@ -1,42 +1,276 @@ -main{ - min-height:100vh; - width:100%; +/** + * @stylesheet auth.form.scss + * @desc Styles for Login, Register, ForgotPassword, VerifyCode, ResetPassword pages + */ - display:flex; - justify-content:center; - align-items:center; +main { + min-height: 100vh; + width: 100%; + display: flex; + justify-content: center; + align-items: center; + background: #0f1117; + color: #e2e8f0; + font-family: "Inter", "Segoe UI", system-ui, sans-serif; + padding: 1rem; - .form-container{ - min-width:350px; - display: flex; - flex-direction: column; - gap:1rem; + /* ================= FORM CONTAINER ================= */ + .form-container { + min-width: 350px; + max-width: 420px; + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; + background: rgba(22, 27, 38, 0.96); + border: 1px solid #2d3348; + border-radius: 18px; + padding: 2rem 1.5rem; + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.2); + + h1 { + font-size: 1.6rem; + font-weight: 800; + color: #fff; + text-align: center; + margin-bottom: 0.25rem; + } + } + + /* ================= SUBTITLE ================= */ + .form-subtitle { + text-align: center; + font-size: 0.88rem; + color: #94a3b8; + line-height: 1.5; + margin-bottom: 0.5rem; + + strong { + color: #e2e8f0; + } + } + + /* ================= BACK LINK ================= */ + .back-link { + font-size: 0.88rem; + color: #94a3b8; + text-decoration: none; + margin-bottom: 0.5rem; + display: inline-block; + transition: color 0.18s ease; + + &:hover { + color: #f43f7a; + } + } + + /* ================= FORM ================= */ + form { + display: flex; + flex-direction: column; + gap: 0.85rem; + } + + /* ================= INPUT GROUP ================= */ + .input-group { + display: flex; + flex-direction: column; + gap: 0.45rem; + + label { + font-size: 0.88rem; + font-weight: 600; + color: #94a3b8; + } + + input { + border: 1px solid #334155; + outline: none; + padding: 0.78rem 1rem; + border-radius: 10px; + background: #0f141f; + color: #e2e8f0; + font-size: 0.92rem; + font-family: inherit; + width: 100%; + transition: border-color 0.18s ease, box-shadow 0.18s ease; + + &::placeholder { + color: #5b687f; + } + + &:focus { + border-color: #f43f7a; + box-shadow: 0 0 0 3px rgba(244, 63, 122, 0.12); + } + } + } + + /* ================= PASSWORD WITH EYE TOGGLE ================= */ + .password-wrapper { + position: relative; + display: flex; + align-items: center; + + input { + padding-right: 3rem; + } + + .eye-toggle { + position: absolute; + right: 0.75rem; + background: none; + border: none; + cursor: pointer; + font-size: 1.15rem; + padding: 0; + line-height: 1; + opacity: 0.7; + transition: opacity 0.18s ease; + width: auto; + + &:hover { + opacity: 1; + } + } + } + + /* ================= OTP INPUT BOXES ================= */ + .otp-container { + display: flex; + justify-content: center; + gap: 0.75rem; + margin: 0.5rem 0; + } + + .otp-input { + width: 52px; + height: 56px; + text-align: center; + font-size: 1.5rem; + font-weight: 800; + color: #fff; + background: #0f141f; + border: 2px solid #334155; + border-radius: 12px; + outline: none; + caret-color: #f43f7a; + transition: border-color 0.18s ease, box-shadow 0.18s ease; + + &:focus { + border-color: #f43f7a; + box-shadow: 0 0 0 3px rgba(244, 63, 122, 0.15); + } + } + + /* ================= BUTTONS ================= */ + .button, + button[type="submit"] { + width: 100%; + } + + .primary-button { + padding: 0.82rem; + border: none; + border-radius: 10px; + font-size: 0.95rem; + font-weight: 700; + cursor: pointer; + color: #fff; + background: linear-gradient(135deg, #f43f7a 0%, #e11d62 100%); + box-shadow: 0 10px 20px rgba(244, 63, 122, 0.22); + transition: transform 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 14px 24px rgba(244, 63, 122, 0.28); + } + + &:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none; } + } + + /* ================= RESEND BUTTON ================= */ + .resend-btn { + background: none; + border: none; + color: #f43f7a; + cursor: pointer; + font-weight: 700; + font-size: 0.88rem; + padding: 0; + text-decoration: underline; + transition: opacity 0.18s ease; - form{ - display: flex; - flex-direction: column; - gap:0.75rem; + &:hover { + opacity: 0.85; } - .input-group{ - display:flex; - flex-direction: column; - gap:0.5rem; + &:disabled { + opacity: 0.6; + cursor: not-allowed; + } + } + + /* ================= ERROR MESSAGE ================= */ + .error-message { + background: rgba(61, 26, 42, 0.95); + color: #f43f7a; + border: 1px solid rgba(244, 63, 122, 0.4); + padding: 0.75rem 1rem; + border-radius: 10px; + font-size: 0.88rem; + text-align: center; + } + + /* ================= SUCCESS MESSAGE ================= */ + .success-message { + background: rgba(16, 185, 129, 0.1); + color: #10b981; + border: 1px solid rgba(16, 185, 129, 0.3); + padding: 0.75rem 1rem; + border-radius: 10px; + font-size: 0.88rem; + text-align: center; + } - input{ - border:none; - outline:none; - padding-inline:1rem; - padding-block:0.75rem; - border-radius:0.75rem; - } + /* ================= LINKS ================= */ + p { + text-align: center; + font-size: 0.88rem; + color: #94a3b8; + margin: 0; + } + + a { + color: #f43f7a; + text-decoration: none; + font-weight: 600; + transition: opacity 0.18s ease; + + &:hover { + opacity: 0.85; + } + } + + /* ================= RESPONSIVE ================= */ + @media (max-width: 520px) { + .form-container { + min-width: auto; + padding: 1.5rem 1.2rem; + } + .otp-input { + width: 46px; + height: 50px; + font-size: 1.3rem; } - a{ - color: rgb(186, 37, 82); - text-decoration: none; - + .otp-container { + gap: 0.55rem; } + } } \ No newline at end of file diff --git a/client/src/features/auth/hooks/useAuth.js b/client/src/features/auth/hooks/useAuth.js index 37f8292..9042ed4 100644 --- a/client/src/features/auth/hooks/useAuth.js +++ b/client/src/features/auth/hooks/useAuth.js @@ -1,61 +1,77 @@ -import {useContext, useState, useEffect} from "react" -import { AuthContext } from "../auth.context" -import {login, register, logout, getMe} from "../services/auth.api" +// src/features/auth/hooks/useAuth.js +import { useContext } from "react"; +import { AuthContext } from "../auth.context"; +import { login, register, logout as logoutApi, getMe, googleLogin } from "../services/auth.api"; export const useAuth = () => { + const context = useContext(AuthContext); + const { user, setUser, loading, setLoading, error, setError, clearError } = context; - const context = useContext(AuthContext) - const {user, setUser, loading, setLoading} = context - - const handleLogin = async({email, password}) => { - setLoading(true) + const handleLogin = async ({ email, password }) => { + setLoading(true); + setError(null); try { - const data = await login({email, password}) - setUser(data.user) - } catch(err) { - console.log(err) + const data = await login({ email, password }); + setUser(data.user); + return { success: true }; + } catch (err) { + const errorMsg = err.response?.data?.message || "Login failed. Please try again."; + setError(errorMsg); + return { success: false, message: errorMsg }; } finally { - setLoading(false) + setLoading(false); } - } + }; - const handleRegister = async({username, email, password}) => { - setLoading(true) + const handleRegister = async ({ username, email, password }) => { + setLoading(true); + setError(null); try { - const data = await register({username, email, password}) - setUser(data.user) - } catch(err) { - console.log(err) + const data = await register({ username, email, password }); + setUser(data.user); + return { success: true }; + } catch (err) { + const errorMsg = err.response?.data?.message || "Registration failed."; + setError(errorMsg); + return { success: false, message: errorMsg }; } finally { - setLoading(false) + setLoading(false); } - } + }; - const handleLogout = async() => { - setLoading(true) + const handleGoogleLogin = async (token) => { + setLoading(true); + setError(null); try { - await logout() - setUser(null) - } catch(err) { - console.log(err) + const data = await googleLogin(token); + setUser(data.user); + return { success: true }; + } catch (err) { + const errorMsg = err.response?.data?.message || "Google login failed."; + setError(errorMsg); + return { success: false, message: errorMsg }; } finally { - setLoading(false) + setLoading(false); } - } + }; - useEffect(() => { - const getAndSetUser = async() => { - try { - const data = await getMe() - setUser(data.user) // ✅ try ke andar - } catch(err) { - setUser(null) // ✅ 401 aaye toh null - } finally { - setLoading(false) // ✅ hamesha band karo - } + const handleLogout = async () => { + try { + await logoutApi(); + setUser(null); + } catch (err) { + console.log(err); } - getAndSetUser() - }, []) + }; - return {user, loading, handleLogin, handleRegister, handleLogout} -} \ No newline at end of file + return { + user, + loading, + error, + clearError, + handleLogin, + handleRegister, + handleGoogleLogin, + handleLogout + }; +}; \ No newline at end of file diff --git a/client/src/features/auth/interview/pages/Home.jsx b/client/src/features/auth/interview/pages/Home.jsx index 071d239..a59453d 100644 --- a/client/src/features/auth/interview/pages/Home.jsx +++ b/client/src/features/auth/interview/pages/Home.jsx @@ -1,142 +1,140 @@ -import React, { useState, useRef, useEffect } from 'react' -import "./style/home.scss" -import { useInterview } from "../../hooks/useInterview" -import { useNavigate } from "react-router" +import React, { useState, useRef, useEffect } from 'react'; +import { useNavigate, Link } from "react-router-dom"; +import "./style/home.scss"; + +import { useAuth } from "../../hooks/useAuth"; +import { useInterview } from "../../hooks/useInterview"; const Home = () => { - const [jobDescription, setJobDescription] = useState("") - const [selfDescription, setSelfDescription] = useState("") - const [resumeFile, setResumeFile] = useState(null) - const [formError, setFormError] = useState("") - const [charCount, setCharCount] = useState(0) - const [reportCount, setReportCount] = useState(0) + const { user, handleLogout } = useAuth(); + + const [jobDescription, setJobDescription] = useState(""); + const [selfDescription, setSelfDescription] = useState(""); + const [resumeFile, setResumeFile] = useState(null); + const [formError, setFormError] = useState(""); + const [charCount, setCharCount] = useState(0); + const [reportCount, setReportCount] = useState(0); - const resumeInputRef = useRef(null) - const navigate = useNavigate() - const { loading, generateReport, getReports, error } = useInterview() + const resumeInputRef = useRef(null); + const navigate = useNavigate(); + const { loading, generateReport, getReports, error } = useInterview(); - // ✅ Fetch report count on page load useEffect(() => { const fetchReports = async () => { try { - const reports = await getReports() + const reports = await getReports(); if (reports && Array.isArray(reports)) { - setReportCount(reports.length) + setReportCount(reports.length); } } catch (err) { - console.error("Failed to fetch reports:", err) + console.error("Failed to fetch reports:", err); } - } - fetchReports() - }, []) + }; + fetchReports(); + }, []); - const handleUploadClick = () => { - resumeInputRef.current?.click() - } + const handleUploadClick = () => resumeInputRef.current?.click(); const handleFileChange = (e) => { - const file = e.target.files[0] - if (file) { - if (file.size > 5 * 1024 * 1024) { - setFormError("File size must be less than 5MB") - return - } - const validTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'] - if (!validTypes.includes(file.type)) { - setFormError("Please upload PDF or DOCX file only") - return - } - setResumeFile(file) - setFormError("") + const file = e.target.files[0]; + if (!file) return; + + if (file.size > 5 * 1024 * 1024) { + setFormError("File size must be less than 5MB"); + return; } - } + const validTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']; + if (!validTypes.includes(file.type)) { + setFormError("Please upload PDF or DOCX only"); + return; + } + setResumeFile(file); + setFormError(""); + }; const handleJobDescChange = (e) => { - const value = e.target.value - setJobDescription(value) - setCharCount(value.length) - } + const value = e.target.value; + setJobDescription(value); + setCharCount(value.length); + }; const validateForm = () => { if (!jobDescription.trim()) { - setFormError("Please provide the job description") - return false + setFormError("Please provide the job description"); + return false; } if (!resumeFile && !selfDescription.trim()) { - setFormError("Please upload a resume OR provide a self-description") - return false - } - if (jobDescription.trim().length < 50) { - setFormError("Job description should be at least 50 characters") - return false + setFormError("Please upload a resume OR provide self-description"); + return false; } - return true - } + return true; + }; const handleGenerateReport = async () => { - setFormError("") - - if (!validateForm()) { - return - } + setFormError(""); + if (!validateForm()) return; try { - const data = await generateReport({ - jobDescription, - selfDescription, - resumeFile - }) + const data = await generateReport({ jobDescription, selfDescription, resumeFile }); + console.log("✅ Generate Report Response:", data); // ← Debugging + if (data?._id) { - setReportCount(prev => prev + 1) - navigate(`/interview/${data._id}`) + navigate(`/interview/${data._id}`); } else { - setFormError("Failed to generate report. Please try again.") + setFormError("Failed to generate report - No ID received"); } } catch (err) { - setFormError(err.message || "Something went wrong. Please try again.") + console.error("❌ Generate Report Error:", err); + setFormError(err.message || "Something went wrong"); } - } + }; const clearForm = () => { - setJobDescription("") - setSelfDescription("") - setResumeFile(null) - setFormError("") - setCharCount(0) - if (resumeInputRef.current) { - resumeInputRef.current.value = "" - } - } + setJobDescription(""); + setSelfDescription(""); + setResumeFile(null); + setFormError(""); + setCharCount(0); + if (resumeInputRef.current) resumeInputRef.current.value = ""; + }; if (loading) { return (

Generating your interview plan...

-

This may take up to 60 seconds. Please don't close this page.

+

This may take up to 60 seconds.

- ) + ); } return (
+
+
+

InterviewAI

+
+ {user ? ( + <> + Welcome, {user.username} + + + ) : ( + + )} +
+
+
+

Create Your Custom Interview Plan

Let our AI analyze the job requirements and your unique profile to build a winning strategy.

- {/* ✅ Report Count Stats */}
-
- - - - - - -
+
📄
{reportCount} Reports Generated @@ -144,11 +142,7 @@ const Home = () => {
-
- - - -
+
AI-Powered Smart Analysis @@ -156,34 +150,23 @@ const Home = () => {
- {/* Error Display */} {(formError || error) && (
- - - - - {formError || error}
)}
- {/* Left Panel - Job Description */}
-
- 💼 Target Job Description -
+
💼 Target Job Description
Required