diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7059b05..f596038 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import LandingPage from "./components/LandingPage"; import LoginPage from "./features/auth/LoginPage"; import SignupPage from "./features/auth/SignupPage"; +import VerifyEmailPage from "./features/auth/VerifyEmailPage"; import OrgAuthPage from "./features/org/OrgAuthPage"; import ProfileSetupPage from "./features/user/ProfileSetupPage"; import DashboardPage from "./features/user/DashboardPage"; @@ -22,6 +23,7 @@ type Page = | "login" | "signup" | "org-auth" + | "verify-email" | "profile-setup" | "dashboard" | "org-dashboard" @@ -92,7 +94,7 @@ function App() { const handleSignupSuccess = (data: TokenResponse) => { setAuth({ token: data.token, user: data.user, isNewUser: true }); - setPage("profile-setup"); + setPage("verify-email"); }; const handleProfileComplete = (updatedUser: UserResponse) => { @@ -150,6 +152,17 @@ function App() { /> ); + case "verify-email": + if (!auth) return null; + return ( + setPage("signup")} + onContinue={() => setPage("profile-setup")} + onLoginClick={() => setPage("login")} + /> + ); + case "profile-setup": if (!auth) return null; return ( diff --git a/frontend/src/features/auth/LoginPage.tsx b/frontend/src/features/auth/LoginPage.tsx index 259754f..74a5ce7 100644 --- a/frontend/src/features/auth/LoginPage.tsx +++ b/frontend/src/features/auth/LoginPage.tsx @@ -1,8 +1,19 @@ -import React from "react"; -import Orb from "../../components/Orb"; -import { useLogin } from "./hooks/useLogin"; import { startGoogleOAuth } from "../../services/auth.service"; import type { TokenResponse } from "../../services/auth.service"; +import { useLogin } from "./hooks/useLogin"; +import { + ArrowIcon, + AuthButton, + AuthCard, + AuthDivider, + AuthInput, + AuthShell, + AuthSocials, + FormAlert, + LockIcon, + SpinnerIcon, + UserIcon, +} from "./components/AuthShell"; export interface LoginPageProps { onLoginSuccess?: (data: TokenResponse) => void; @@ -10,902 +21,95 @@ export interface LoginPageProps { onBack?: () => void; } -const STUDENT_HIGHLIGHTS = [ - { - icon: ( - - - - ), - label: "Built for students", - }, - { - icon: ( - - - - ), - label: "15-min sessions", - }, - { - icon: ( - - - - ), - label: "Free for learners", - }, -]; - -const LoginPage: React.FC = ({ +const LoginPage = ({ onLoginSuccess, onSignupClick, onBack, -}) => { +}: LoginPageProps) => { const { form, isLoading, error, handleChange, handleSubmit } = useLogin(onLoginSuccess); return ( -
- - - -
- {/* LEFT: student-focused welcome */} -
- -

- Welcome back, -
- future star. -

-

- Sign in to continue your interview prep. Pick up where you left off - and keep your confidence streak alive. -

- -
- {STUDENT_HIGHLIGHTS.map((h) => ( -
- {h.icon} - - {h.label} - -
- ))} -
- - -
- - {/* RIGHT: login card + decorative orb */} -
-
- -
- -
-

- Sign in to your account -

-

- Good to see you again 👋 -

- - - - - -
- {error && } - - - - - } - required - /> - - - - - } - required - /> - -
- - - Forgot password? - -
- - - {isLoading ? ( - <> - Signing in… - - ) : ( - <> - Sign In - - - )} - - - -
- New to InterXAI?{" "} - -
-
-
-
-
- ); -}; - -const StreakCard = () => ( -
-
- 🔥 -
-
-
- Keep your streak alive -
-
- Students who practise daily improve scores 2.4× faster. -
-
-
-); - -const BgBlobs = () => ( - <> -
-
- -); - -const TopNav: React.FC<{ onBack?: () => void }> = ({ onBack }) => ( - -); - -const Pill: React.FC<{ text: string }> = ({ text }) => ( -
- - - - - {text} - -
-); - -const SocialAuthRow: React.FC<{ disabled?: boolean }> = ({ disabled }) => ( -
- - - - - - -
-); - -const SocialBtn: React.FC<{ - disabled?: boolean; - label: string; - onClick?: () => void; - children: React.ReactNode; -}> = ({ disabled, label, onClick, children }) => ( - -); - -const Divider: React.FC<{ label: string }> = ({ label }) => ( -
- - - {label} - - -
-); - -const ErrorAlert: React.FC<{ message: string }> = ({ message }) => ( -
- - - - - {message} -
-); -interface LightInputProps extends React.InputHTMLAttributes { - label: string; - id: string; - icon?: React.ReactNode; -} - -const LightInput: React.FC = ({ - label, - id, - type = "text", - icon, - ...props -}) => { - const [revealed, setRevealed] = React.useState(false); - const isPassword = type === "password"; - const inputType = isPassword ? (revealed ? "text" : "password") : type; + } + required + /> - return ( -
- -
- {icon && ( -
- {icon} +
+ + Forgot password?
- )} - { - e.currentTarget.style.borderColor = "rgba(59,130,246,0.7)"; - e.currentTarget.style.boxShadow = - "inset 0 1px 2px rgba(15,23,42,0.04), 0 0 0 3px rgba(59,130,246,0.15)"; - }} - onBlur={(e) => { - e.currentTarget.style.borderColor = "rgba(203,213,225,0.7)"; - e.currentTarget.style.boxShadow = - "inset 0 1px 2px rgba(15,23,42,0.04)"; - }} - {...props} - /> - {isPassword && ( - - )} -
-
+

+ + ); }; -const PrimaryButton: React.FC<{ - disabled?: boolean; - fullWidth?: boolean; - children: React.ReactNode; -}> = ({ disabled, fullWidth, children }) => ( - -); - -const ArrowIcon = () => ( - - - -); - -const Spinner = () => ( - - - - - -); - -const GoogleIcon = () => ( - - - - - - -); - -const LinkedInIcon = () => ( - - - -); - export default LoginPage; diff --git a/frontend/src/features/auth/SignupPage.tsx b/frontend/src/features/auth/SignupPage.tsx index 8bb9a6a..485faaa 100644 --- a/frontend/src/features/auth/SignupPage.tsx +++ b/frontend/src/features/auth/SignupPage.tsx @@ -1,8 +1,21 @@ -import React from "react"; -import Orb from "../../components/Orb"; -import { useSignup } from "./hooks/useSignup"; import { startGoogleOAuth } from "../../services/auth.service"; import type { TokenResponse } from "../../services/auth.service"; +import { useSignup } from "./hooks/useSignup"; +import { + ArrowIcon, + AuthButton, + AuthCard, + AuthDivider, + AuthInput, + AuthShell, + AuthSocials, + EmailIcon, + FormAlert, + LockIcon, + SpinnerIcon, + Stepper, + UserIcon, +} from "./components/AuthShell"; export interface SignupPageProps { onSignupSuccess?: (data: TokenResponse) => void; @@ -10,985 +23,105 @@ export interface SignupPageProps { onBack?: () => void; } -const BENEFITS = [ - { - icon: ( - - - - ), - title: "AI-led interviews", - desc: "Practice with realistic, adaptive sessions on demand.", - }, - { - icon: ( - - - - ), - title: "Actionable feedback", - desc: "Get scored on clarity, structure, and confidence after each round.", - }, - { - icon: ( - - - - ), - title: "Track your growth", - desc: "Watch your confidence score climb across sessions.", - }, -]; - -const SignupPage: React.FC = ({ +const SignupPage = ({ onSignupSuccess, onLoginClick, onBack, -}) => { +}: SignupPageProps) => { const { form, isLoading, error, handleChange, handleSubmit } = useSignup(onSignupSuccess); return ( -
- - - -
} + title="Create your account" + description="Join InterXAI and get your first mock interview ready in minutes." > - {/* LEFT: marketing copy + benefits + tiny orb */} -
- -

- Land your -
- dream role. -

-

- Sign up to run AI-led interviews, get instant feedback, and grow - your confidence — all in one place. -

- -
- {BENEFITS.map((b) => ( -
-
- {b.icon} -
-
-
- {b.title} -
-
- {b.desc} -
-
-
- ))} -
- - + + +
+ {error && } + + } + required /> -
- - {/* RIGHT: signup card + orb on top */} -
- {/* Orb floats behind/above the card as a hero decoration */} -
- -
- -
- -

- Create your account -

-

- Free 14-day trial · No credit card required -

- - - - - - - {error && } - - - - } - required - /> - - - - } - required - /> - - - - - } - required - /> - - - {isLoading ? ( - <> - Creating account… - - ) : ( - <> - Create Account - - - )} - - - -

- By signing up, you agree to our{" "} - - Terms - {" "} - and{" "} - - Privacy Policy - - . -

- -
- Already have an account?{" "} - -
-
-
-
-
- ); -}; - -const BgBlobs = () => ( - <> -
-
- -); - -const TopNav: React.FC<{ onBack?: () => void }> = ({ onBack }) => ( - -); -const Pill: React.FC<{ text: string }> = ({ text }) => ( -
- - - - - {text} - -
-); - -const StepIndicator: React.FC<{ current: number; total: number }> = ({ - current, - total, -}) => ( -
- {Array.from({ length: total }).map((_, i) => { - const active = i + 1 <= current; - return ( - -
- {i + 1} -
- {i < total - 1 && ( -
- )} - - ); - })} - - Step {current} of {total} - -
-); - -const SocialAuthRow: React.FC<{ disabled?: boolean }> = ({ disabled }) => ( -
- - - - - - -
-); - -const SocialBtn: React.FC<{ - disabled?: boolean; - label: string; - onClick?: () => void; - children: React.ReactNode; -}> = ({ disabled, label, onClick, children }) => ( - -); - -const Divider: React.FC<{ label: string }> = ({ label }) => ( -
- - - {label} - - -
-); - -const SocialProof: React.FC<{ avatars: string[]; text: string }> = ({ - avatars, - text, -}) => ( -
-
- {avatars.map((c, i) => ( -
- ))} -
- - {text} - -
-); - -const ErrorAlert: React.FC<{ message: string }> = ({ message }) => ( -
- - - - - {message} -
-); - -interface LightInputProps extends React.InputHTMLAttributes { - label: string; - id: string; - icon?: React.ReactNode; -} - -const LightInput: React.FC = ({ - label, - id, - type = "text", - icon, - ...props -}) => { - const [revealed, setRevealed] = React.useState(false); - const isPassword = type === "password"; - const inputType = isPassword ? (revealed ? "text" : "password") : type; + } + required + /> - return ( -
- -
- {icon && ( -
- {icon} -
- )} - { - e.currentTarget.style.borderColor = "rgba(59,130,246,0.7)"; - e.currentTarget.style.boxShadow = - "inset 0 1px 2px rgba(15,23,42,0.04), 0 0 0 3px rgba(59,130,246,0.15)"; - }} - onBlur={(e) => { - e.currentTarget.style.borderColor = "rgba(203,213,225,0.7)"; - e.currentTarget.style.boxShadow = - "inset 0 1px 2px rgba(15,23,42,0.04)"; - }} - {...props} - /> - {isPassword && ( - - )} -
-
+

+ + ); }; -const PrimaryButton: React.FC<{ - disabled?: boolean; - fullWidth?: boolean; - children: React.ReactNode; -}> = ({ disabled, fullWidth, children }) => ( - -); - -const ArrowIcon = () => ( - - - -); - -const Spinner = () => ( - - - - - -); - -const GoogleIcon = () => ( - - - - - - -); - -const LinkedInIcon = () => ( - - - -); - export default SignupPage; diff --git a/frontend/src/features/auth/VerifyEmailPage.tsx b/frontend/src/features/auth/VerifyEmailPage.tsx new file mode 100644 index 0000000..ac3a5ca --- /dev/null +++ b/frontend/src/features/auth/VerifyEmailPage.tsx @@ -0,0 +1,65 @@ +import { + AuthButton, + AuthCard, + AuthShell, + MailCheckIllustration, + Stepper, +} from "./components/AuthShell"; + +export interface VerifyEmailPageProps { + email?: string; + onBackToSignup?: () => void; + onContinue?: () => void; + onLoginClick?: () => void; +} + +const VerifyEmailPage = ({ + email, + onBackToSignup, + onContinue, + onLoginClick, +}: VerifyEmailPageProps) => { + const displayEmail = email || "your inbox"; + + return ( + + } + title="Verify your email" + description={`We sent a verification link to ${displayEmail}.`} + > + + +
+ + Continue to profile setup + + + +
+
+
+ ); +}; + +export default VerifyEmailPage; diff --git a/frontend/src/features/auth/components/AuthShell.tsx b/frontend/src/features/auth/components/AuthShell.tsx new file mode 100644 index 0000000..00dd64e --- /dev/null +++ b/frontend/src/features/auth/components/AuthShell.tsx @@ -0,0 +1,403 @@ +import { useState } from "react"; +import type { + ButtonHTMLAttributes, + InputHTMLAttributes, + ReactNode, +} from "react"; +import "./auth-pages.css"; + +type AuthMode = "login" | "signup" | "verify"; + +interface AuthShellProps { + eyebrow: string; + title: string; + subtitle: string; + mode: AuthMode; + highlights: string[]; + children: ReactNode; + onBack?: () => void; +} + +export const AuthShell = ({ + eyebrow, + title, + subtitle, + mode, + highlights, + children, + onBack, +}: AuthShellProps) => ( +
+
+
+
+ + + +
+
+
{eyebrow}
+

{title}

+

{subtitle}

+ +
+ {highlights.map((highlight) => ( + + + {highlight} + + ))} +
+ + + +
{children}
+
+
+); + +interface AuthCardProps { + title: string; + description: string; + children: ReactNode; + center?: boolean; + stepLabel?: ReactNode; +} + +export const AuthCard = ({ + title, + description, + children, + center = false, + stepLabel, +}: AuthCardProps) => ( +
+ {stepLabel &&
{stepLabel}
} +

{title}

+

{description}

+ {children} +
+); + +interface AuthInputProps extends InputHTMLAttributes { + label: string; + id: string; + icon?: ReactNode; +} + +export const AuthInput = ({ + label, + id, + icon, + type = "text", + ...props +}: AuthInputProps) => { + const [revealed, setRevealed] = useState(false); + const isPassword = type === "password"; + + return ( +
+ +
+ {icon && {icon}} + + {isPassword && ( + + )} +
+
+ ); +}; + +export const AuthButton = ({ + children, + type = "submit", + ...props +}: ButtonHTMLAttributes) => ( + +); + +export const AuthDivider = ({ label }: { label: string }) => ( +
+ + {label} + +
+); + +export const AuthSocials = ({ + disabled, + onGoogleClick, +}: { + disabled?: boolean; + onGoogleClick: () => void; +}) => ( +
+ + +
+); + +export const Stepper = ({ + activeStep, + totalSteps, +}: { + activeStep: number; + totalSteps: number; +}) => ( +
+ {Array.from({ length: totalSteps }).map((_, index) => { + const step = index + 1; + return ( + + {step} + + ); + })} +
+); + +export const FormAlert = ({ message }: { message: string }) => ( +
+ + {message} +
+); + +export const MailCheckIllustration = () => ( +