@@ -12,6 +12,12 @@ import { Label } from "@/app/ui/label";
1212import { Separator } from "@/app/ui/separator" ;
1313import { useAnalyticsTracking } from "@/features/analytics/tracking/useAnalyticsTracking" ;
1414import { authClient } from "@/lib/auth-client" ;
15+ import { cn } from "@/lib/utils" ;
16+
17+ type FeedbackState = {
18+ kind : "error" | "success" ;
19+ message : string ;
20+ } | null ;
1521
1622function getCallbackURL ( ) : string {
1723 const params = new URLSearchParams ( window . location . search ) ;
@@ -38,30 +44,75 @@ export function LoginForm({
3844} ) {
3945 const [ email , setEmail ] = useState ( "" ) ;
4046 const [ password , setPassword ] = useState ( "" ) ;
41- const [ error , setError ] = useState ( "" ) ;
47+ const [ feedback , setFeedback ] = useState < FeedbackState > ( null ) ;
4248 const [ loading , setLoading ] = useState ( false ) ;
49+ const [ requestingPasswordReset , setRequestingPasswordReset ] = useState ( false ) ;
4350 const { trackAuthenticationAction } = useAnalyticsTracking ( {
4451 pageName : "login" ,
4552 } ) ;
4653
4754 async function handleSubmit ( e : React . FormEvent ) {
4855 e . preventDefault ( ) ;
56+ setFeedback ( null ) ;
4957 trackAuthenticationAction ( {
5058 actionName : "sign_in" ,
5159 sourceComponent : "login_form" ,
5260 authMethod : "email_password" ,
5361 } ) ;
54- setError ( "" ) ;
5562 setLoading ( true ) ;
5663 const { error } = await authClient . signIn . email ( { email, password } ) ;
5764 setLoading ( false ) ;
5865 if ( error ) {
59- setError ( error . message ?? "Sign in failed" ) ;
66+ setFeedback ( {
67+ kind : "error" ,
68+ message : error . message ?? "Sign in failed" ,
69+ } ) ;
70+ }
71+ }
72+
73+ async function handleRequestPasswordReset ( ) {
74+ if ( ! email . trim ( ) ) {
75+ const emailField = document . getElementById ( "email" ) ;
76+ if ( emailField instanceof HTMLInputElement ) {
77+ emailField . focus ( ) ;
78+ }
79+ setFeedback ( {
80+ kind : "error" ,
81+ message :
82+ "Enter your email first and we will send the reset link there." ,
83+ } ) ;
84+ return ;
6085 }
86+
87+ trackAuthenticationAction ( {
88+ actionName : "request_password_reset" ,
89+ sourceComponent : "login_form" ,
90+ authMethod : "email_password" ,
91+ } ) ;
92+ setFeedback ( null ) ;
93+ setRequestingPasswordReset ( true ) ;
94+ const { error } = await authClient . requestPasswordReset ( {
95+ email,
96+ redirectTo : `${ window . location . origin } /reset-password` ,
97+ } ) ;
98+ setRequestingPasswordReset ( false ) ;
99+
100+ if ( error ) {
101+ setFeedback ( {
102+ kind : "error" ,
103+ message : error . message ?? "Could not send password reset email" ,
104+ } ) ;
105+ return ;
106+ }
107+
108+ setFeedback ( {
109+ kind : "success" ,
110+ message : `If an account exists for ${ email . trim ( ) } , a reset link is on its way.` ,
111+ } ) ;
61112 }
62113
63114 async function handleSocialSignIn ( provider : "google" | "github" ) {
64- setError ( "" ) ;
115+ setFeedback ( null ) ;
65116 trackAuthenticationAction ( {
66117 actionName : "sign_in" ,
67118 sourceComponent : "login_form" ,
@@ -72,7 +123,10 @@ export function LoginForm({
72123 callbackURL : getCallbackURL ( ) ,
73124 } ) ;
74125 if ( error ) {
75- setError ( error . message ?? `Sign in with ${ provider } failed` ) ;
126+ setFeedback ( {
127+ kind : "error" ,
128+ message : error . message ?? `Sign in with ${ provider } failed` ,
129+ } ) ;
76130 }
77131 }
78132
@@ -90,25 +144,70 @@ export function LoginForm({
90144 < Label htmlFor = "email" > Email</ Label >
91145 < Input
92146 id = "email"
147+ name = "email"
93148 type = "email"
149+ autoComplete = "email"
94150 placeholder = "you@example.com"
95151 value = { email }
96- onChange = { ( e ) => setEmail ( e . target . value ) }
152+ onChange = { ( e ) => {
153+ setEmail ( e . target . value ) ;
154+ if ( feedback ) {
155+ setFeedback ( null ) ;
156+ }
157+ } }
97158 required
98159 />
99160 </ div >
100161 < div className = "flex flex-col gap-2" >
101- < Label htmlFor = "password" > Password</ Label >
162+ < div className = "flex items-center justify-between gap-3" >
163+ < Label htmlFor = "password" > Password</ Label >
164+ < Button
165+ type = "button"
166+ variant = "ghost"
167+ size = "xs"
168+ onClick = { ( ) => {
169+ void handleRequestPasswordReset ( ) ;
170+ } }
171+ disabled = { requestingPasswordReset }
172+ className = "text-muted-foreground hover:text-foreground"
173+ >
174+ { requestingPasswordReset
175+ ? "Sending link..."
176+ : feedback ?. kind === "success"
177+ ? "Resend link"
178+ : "Forgot password?" }
179+ </ Button >
180+ </ div >
102181 < Input
103182 id = "password"
183+ name = "password"
104184 type = "password"
185+ autoComplete = "current-password"
105186 value = { password }
106- onChange = { ( e ) => setPassword ( e . target . value ) }
187+ onChange = { ( e ) => {
188+ setPassword ( e . target . value ) ;
189+ if ( feedback ?. kind === "error" ) {
190+ setFeedback ( null ) ;
191+ }
192+ } }
107193 required
108194 />
109195 </ div >
110- { error && < p className = "text-sm text-destructive" > { error } </ p > }
111- < Button type = "submit" disabled = { loading } >
196+ { feedback ? (
197+ < div
198+ role = { feedback . kind === "error" ? "alert" : "status" }
199+ aria-live = "polite"
200+ className = { cn (
201+ "rounded-3xl px-3 py-2 text-sm leading-5 ring-1" ,
202+ feedback . kind === "error"
203+ ? "bg-destructive/5 text-destructive ring-destructive/15"
204+ : "bg-muted/35 text-muted-foreground ring-border/60" ,
205+ ) }
206+ >
207+ { feedback . message }
208+ </ div >
209+ ) : null }
210+ < Button type = "submit" disabled = { loading || requestingPasswordReset } >
112211 { loading ? "Signing in..." : "Sign in" }
113212 </ Button >
114213 </ form >
0 commit comments