/
opt
/
canhelp
/
apps
/
web
/
src
/
app
/
(auth)
/
login
/
/opt/canhelp/apps/web/src/app/(auth)/login
mkdir
upload
Name
Size
Mode
Actions
page.tsx
8142
0644
edit
dl
rm
Edit:
/opt/canhelp/apps/web/src/app/(auth)/login/page.tsx
(8142B)
'use client' import { useState } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import Link from 'next/link' import { signIn, authClient } from '@/lib/auth' import { useLocale } from '@/context/locale' export default function LoginPage() { const router = useRouter() const searchParams = useSearchParams() const { t } = useLocale() const [error, setError] = useState('') const [loading, setLoading] = useState(false) const [googleLoading, setGoogleLoading] = useState(false) const [unverifiedEmail, setUnverifiedEmail] = useState<string | null>(null) const [resendState, setResendState] = useState<'idle' | 'sending' | 'sent'>('idle') const redirectParam = searchParams.get('redirect') const redirectPath = redirectParam && redirectParam.startsWith('/') ? redirectParam : '/dashboard' const redirectQuery = redirectPath !== '/dashboard' ? `?redirect=${encodeURIComponent(redirectPath)}` : '' async function handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() setError('') setUnverifiedEmail(null) setLoading(true) const form = new FormData(e.currentTarget) const email = form.get('email') as string try { const result = await signIn.email({ email, password: form.get('password') as string, }) if (result?.error) { // Better Auth returns code EMAIL_NOT_VERIFIED (status 403) when verification is required if ( result.error.status === 403 || (result.error.code as string) === 'EMAIL_NOT_VERIFIED' ) { setUnverifiedEmail(email) } else { setError(t('login.error')) } } else { router.push(redirectPath) } } catch { setError(t('login.error')) } setLoading(false) } async function resendVerification() { if (!unverifiedEmail || resendState !== 'idle') return setResendState('sending') const callbackURL = `${window.location.origin}/dashboard` const result = await authClient.sendVerificationEmail({ email: unverifiedEmail, callbackURL }) if (result?.error) { console.error('[resend]', result.error) setError(`${t('login.unverified.resend')}: ${result.error.message ?? result.error.status}`) setResendState('idle') } else { setResendState('sent') } } async function handleGoogleSignIn() { setError('') setGoogleLoading(true) try { await authClient.signIn.social({ provider: 'google', callbackURL: `${window.location.origin}${redirectPath}`, }) } catch { setError(t('login.error')) setGoogleLoading(false) } } return ( <div className="min-h-screen flex items-center justify-center px-4"> <div className="w-full max-w-md"> <div className="text-center mb-8"> <h1 className="text-3xl font-bold text-white">{t('login.title')}</h1> <p className="text-white/70 mt-2">{t('login.subtitle')}</p> </div> <form onSubmit={handleSubmit} className="bg-white rounded-xl border border-gray-200 p-6 space-y-4"> {error && ( <div className="bg-red-50 text-red-700 p-3 rounded-lg text-sm">{error}</div> )} {unverifiedEmail && ( <div className="bg-amber-50 border border-amber-200 rounded-lg p-4 space-y-3"> <div className="flex items-start gap-2"> <svg className="text-amber-500 mt-0.5 shrink-0" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" > <rect x="3" y="5" width="18" height="14" rx="2" /> <path d="m3 7 9 6 9-6" /> </svg> <div> <p className="text-amber-800 font-medium text-sm">{t('login.unverified.title')}</p> <p className="text-amber-700 text-sm mt-0.5">{t('login.unverified.desc')}</p> </div> </div> <button type="button" onClick={resendVerification} disabled={resendState !== 'idle'} className="w-full py-2 text-sm font-medium rounded-lg border border-amber-300 bg-white text-amber-700 hover:bg-amber-50 disabled:opacity-60 transition" > {resendState === 'sending' ? t('login.unverified.sending') : resendState === 'sent' ? t('login.unverified.sent') : t('login.unverified.resend')} </button> </div> )} <div> <label className="block text-sm font-medium text-gray-700 mb-1">{t('login.email')}</label> <input name="email" type="email" required autoComplete="email" className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400" /> </div> <div> <div className="flex items-center justify-between mb-1"> <label className="block text-sm font-medium text-gray-700">{t('login.password')}</label> <Link href="/forgot-password" className="text-xs text-green-600 hover:underline"> {t('login.forgot_password')} </Link> </div> <input name="password" type="password" required autoComplete="current-password" className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-400" /> </div> <button type="submit" disabled={loading} className="w-full bg-green-600 text-white py-3 rounded-xl font-semibold hover:bg-green-700 disabled:opacity-50" > {loading ? t('login.submitting') : t('login.submit')} </button> {/* Divider */} <div className="flex items-center gap-3 py-1"> <div className="flex-1 h-px bg-gray-200" /> <span className="text-xs text-gray-400">{t('login.or_continue_with')}</span> <div className="flex-1 h-px bg-gray-200" /> </div> {/* Google Sign-In */} <button type="button" onClick={handleGoogleSignIn} disabled={googleLoading} className="w-full flex items-center justify-center gap-3 py-2.5 px-4 rounded-xl border border-gray-200 bg-white hover:bg-gray-50 disabled:opacity-50 transition font-medium text-gray-700 text-sm" > <svg width="18" height="18" viewBox="0 0 48 48" fill="none"> <path d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z" fill="#FFC107"/> <path d="M6.3 14.7l7 5.1C15 16.1 19.1 13 24 13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 16.3 2 9.7 7.4 6.3 14.7z" fill="#FF3D00"/> <path d="M24 46c5.5 0 10.5-1.9 14.4-5.1l-6.7-5.5C29.5 37 26.9 38 24 38c-6.1 0-11.2-4-13-9.5l-7 5.4C7.6 41.9 15.2 46 24 46z" fill="#4CAF50"/> <path d="M44.5 20H24v8.5h11.8c-.9 2.8-2.8 5.1-5.3 6.6l6.7 5.5C41.5 37.3 45 31.1 45 24c0-1.3-.2-2.7-.5-4z" fill="#1976D2"/> </svg> {googleLoading ? t('login.submitting') : t('login.continue_google')} </button> <p className="text-center text-sm text-gray-500"> {t('login.no_account')}{' '} <Link href={`/register${redirectQuery}`} className="text-green-600 hover:underline font-medium"> {t('login.register_link')} </Link> </p> </form> </div> </div> ) }
Save
cmd:
run