11"use client" ;
22
3- import { useLocale } from ' next-intl' ;
4- import { Link } from ' @/i18n/routing' ;
3+ import { useLocale } from " next-intl" ;
4+ import { Link } from " @/i18n/routing" ;
55import { useState } from "react" ;
66import { useSearchParams } from "next/navigation" ;
77import { getPendingQuizResult , clearPendingQuizResult } from "@/lib/quiz/guest-quiz" ;
88import { Button } from "@/components/ui/button" ;
9- import { OAuthButtons } from ' @/components/auth/OAuthButtons' ;
9+ import { OAuthButtons } from " @/components/auth/OAuthButtons" ;
1010
1111export default function LoginPage ( ) {
1212 const searchParams = useSearchParams ( ) ;
1313 const returnTo = searchParams . get ( "returnTo" ) ;
1414 const locale = useLocale ( ) ;
15+
1516 const [ loading , setLoading ] = useState ( false ) ;
16- const [ error , setError ] = useState < string | null > ( null ) ;
17+ const [ errorMessage , setErrorMessage ] = useState < string | null > ( null ) ;
18+ const [ errorCode , setErrorCode ] = useState < string | null > ( null ) ;
19+ const [ email , setEmail ] = useState ( "" ) ;
20+ const [ verificationSent , setVerificationSent ] = useState ( false ) ;
21+ const [ showPassword , setShowPassword ] = useState ( false ) ;
1722
1823 async function onSubmit ( e : React . FormEvent < HTMLFormElement > ) {
1924 e . preventDefault ( ) ;
2025 setLoading ( true ) ;
21- setError ( null ) ;
26+ setErrorMessage ( null ) ;
27+ setErrorCode ( null ) ;
28+ setVerificationSent ( false ) ;
2229
2330 const formData = new FormData ( e . currentTarget ) ;
31+ const emailValue = String ( formData . get ( "email" ) || "" ) ;
32+ setEmail ( emailValue ) ;
2433
2534 const res = await fetch ( "/api/auth/login" , {
2635 method : "POST" ,
2736 headers : { "Content-Type" : "application/json" } ,
2837 body : JSON . stringify ( {
29- email : formData . get ( "email" ) ,
38+ email : emailValue ,
3039 password : formData . get ( "password" ) ,
3140 } ) ,
3241 } ) ;
3342
43+ const data = await res . json ( ) . catch ( ( ) => null ) ;
3444 setLoading ( false ) ;
3545
3646 if ( ! res . ok ) {
37- setError ( "Invalid email or password" ) ;
47+ setErrorCode ( data ?. code ?? null ) ;
48+
49+ if ( data ?. code === "EMAIL_NOT_VERIFIED" ) {
50+ setErrorMessage (
51+ "Your email address is not verified. Please check your inbox."
52+ ) ;
53+ } else {
54+ setErrorMessage ( "Invalid email or password" ) ;
55+ }
56+
3857 return ;
3958 }
4059
41- const data = await res . json ( ) ;
4260 const pendingResult = getPendingQuizResult ( ) ;
43- if ( pendingResult && data . userId ) {
61+
62+ if ( pendingResult && data ?. userId ) {
4463 try {
4564 const quizRes = await fetch ( "/api/quiz/guest-result" , {
4665 method : "POST" ,
@@ -53,32 +72,52 @@ export default function LoginPage() {
5372 timeSpentSeconds : pendingResult . timeSpentSeconds ,
5473 } ) ,
5574 } ) ;
75+
5676 if ( ! quizRes . ok ) {
5777 throw new Error ( `Failed to save quiz result: ${ quizRes . status } ` ) ;
5878 }
79+
5980 const result = await quizRes . json ( ) ;
6081
6182 if ( result . success ) {
62- sessionStorage . setItem ( 'quiz_just_saved' , JSON . stringify ( {
63- score : result . score ,
64- total : result . totalQuestions ,
65- percentage : result . percentage ,
66- pointsAwarded : result . pointsAwarded ,
67- quizSlug : pendingResult . quizSlug ,
68- } ) ) ;
83+ sessionStorage . setItem (
84+ "quiz_just_saved" ,
85+ JSON . stringify ( {
86+ score : result . score ,
87+ total : result . totalQuestions ,
88+ percentage : result . percentage ,
89+ pointsAwarded : result . pointsAwarded ,
90+ quizSlug : pendingResult . quizSlug ,
91+ } )
92+ ) ;
6993 }
7094 } catch ( err ) {
71- console . error ( ' Failed to save quiz result:' , err ) ;
95+ console . error ( " Failed to save quiz result:" , err ) ;
7296 } finally {
7397 clearPendingQuizResult ( ) ;
7498 }
7599
76100 window . location . href = `/${ locale } /dashboard` ;
77101 return ;
78102 }
103+
79104 window . location . href = returnTo || `/${ locale } /dashboard` ;
80105 }
81106
107+ async function resendVerification ( ) {
108+ if ( ! email ) return ;
109+
110+ await fetch ( "/api/auth/resend-verification" , {
111+ method : "POST" ,
112+ headers : { "Content-Type" : "application/json" } ,
113+ body : JSON . stringify ( { email } ) ,
114+ } ) ;
115+
116+ setVerificationSent ( true ) ;
117+ setErrorCode ( null ) ;
118+ setErrorMessage ( null ) ;
119+ }
120+
82121 return (
83122 < div className = "mx-auto max-w-sm py-12" >
84123 < h1 className = "mb-6 text-2xl font-semibold" > Log in</ h1 >
@@ -98,17 +137,62 @@ export default function LoginPage() {
98137 placeholder = "Email"
99138 required
100139 className = "w-full rounded border px-3 py-2"
140+ onChange = { e => setEmail ( e . target . value ) }
101141 />
102142
103- < input
104- name = "password"
105- type = "password"
106- placeholder = "Password"
107- required
108- className = "w-full rounded border px-3 py-2"
109- />
110-
111- { error && < p className = "text-sm text-red-600" > { error } </ p > }
143+ < div className = "relative" >
144+ < input
145+ name = "password"
146+ type = { showPassword ? "text" : "password" }
147+ placeholder = "Password"
148+ required
149+ className = "w-full rounded border px-3 py-2 pr-10"
150+ />
151+
152+ < button
153+ type = "button"
154+ aria-label = { showPassword ? "Hide password" : "Show password" }
155+ onClick = { ( ) => setShowPassword ( v => ! v ) }
156+ className = "absolute inset-y-0 right-2 flex items-center text-sm text-gray-500"
157+ >
158+ { showPassword ? "Hide" : "Show" }
159+ </ button >
160+ </ div >
161+
162+ < div className = "text-right" >
163+ < Link
164+ href = {
165+ returnTo
166+ ? `/forgot-password?returnTo=${ encodeURIComponent ( returnTo ) } `
167+ : "/forgot-password"
168+ }
169+ className = "text-sm underline text-gray-600"
170+ >
171+ Forgot password?
172+ </ Link >
173+ </ div >
174+
175+ { errorMessage && ! verificationSent && (
176+ < div className = "rounded-md border border-yellow-400 bg-yellow-50 p-3 text-sm text-yellow-800" >
177+ < p > { errorMessage } </ p >
178+
179+ { errorCode === "EMAIL_NOT_VERIFIED" && (
180+ < button
181+ type = "button"
182+ onClick = { resendVerification }
183+ className = "mt-2 underline"
184+ >
185+ Resend verification email
186+ </ button >
187+ ) }
188+ </ div >
189+ ) }
190+
191+ { verificationSent && (
192+ < div className = "rounded-md border border-green-400 bg-green-50 p-3 text-sm text-green-800" >
193+ Verification successfully sent to < strong > { email } </ strong >
194+ </ div >
195+ ) }
112196
113197 < Button type = "submit" disabled = { loading } className = "w-full" >
114198 { loading ? "Logging in..." : "Log in" }
@@ -117,10 +201,17 @@ export default function LoginPage() {
117201
118202 < p className = "mt-4 text-sm text-gray-600" >
119203 Don’t have an account?{ " " }
120- < Link href = { returnTo ? `/signup?returnTo=${ encodeURIComponent ( returnTo ) } ` : '/signup' } className = "underline" >
204+ < Link
205+ href = {
206+ returnTo
207+ ? `/signup?returnTo=${ encodeURIComponent ( returnTo ) } `
208+ : "/signup"
209+ }
210+ className = "underline"
211+ >
121212 Sign up
122213 </ Link >
123214 </ p >
124215 </ div >
125216 ) ;
126- }
217+ }
0 commit comments