Browse Source

feat(web): implement email verification ui

pull/48/head
isra el 1 year ago
parent
commit
85dddc61fb
  1. 202
      web/app/(app)/(auth)/verify-email/page.tsx
  2. 3
      web/config/api.ts

202
web/app/(app)/(auth)/verify-email/page.tsx

@ -0,0 +1,202 @@
'use client'
import { useState, useEffect } from 'react'
import { useSearchParams } from 'next/navigation'
import Link from 'next/link'
import { Button } from '@/components/ui/button'
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Loader2, CheckCircle, XCircle, Mail, ArrowRight } from 'lucide-react'
import { useMutation } from '@tanstack/react-query'
import httpBrowserClient from '@/lib/httpBrowserClient'
import { ApiEndpoints } from '@/config/api'
const ErrorAlert = ({ message }: { message: string }) => (
<Alert
variant='destructive'
className='bg-red-50 text-red-700 border-red-200'
>
<XCircle className='h-5 w-5 text-red-600' />
<AlertTitle className='text-lg font-semibold'>Error</AlertTitle>
<AlertDescription>{message}</AlertDescription>
</Alert>
)
const SendVerificationButton = ({
onClick,
isLoading,
hasVerificationCode,
}: {
onClick: () => void
isLoading: boolean
hasVerificationCode: boolean
}) => (
<Button
className='w-full text-lg py-6'
onClick={onClick}
disabled={isLoading}
>
{isLoading ? (
<Loader2 className='mr-2 h-5 w-5 animate-spin' />
) : (
<Mail className='mr-2 h-5 w-5' />
)}
{hasVerificationCode
? 'Resend Verification Email'
: 'Send Verification Email'}
</Button>
)
const SendVerificationEmail = () => {
const [successMessage, setSuccessMessage] = useState<string>('')
const [error, setError] = useState<string>('')
const {
mutate: sendVerificationEmailMutation,
isPending: isSendingVerificationEmail,
} = useMutation({
mutationFn: () =>
httpBrowserClient.post(
ApiEndpoints.auth.sendEmailVerificationEmail(),
{}
),
onSuccess: () => {
setSuccessMessage('Verification email has been sent to your inbox')
},
onError: (error: any) => {
setError(error.message || 'Failed to send verification email')
},
})
const renderContent = () => {
if (successMessage)
return (
<Alert
variant='default'
className='bg-blue-50 text-blue-700 border-blue-200'
>
<Mail className='h-5 w-5 text-blue-600' />
<AlertTitle className='text-lg font-semibold'>Email Sent</AlertTitle>
<AlertDescription>{successMessage}</AlertDescription>
</Alert>
)
if (error) return <ErrorAlert message={error} />
return null
}
return (
<Card className='w-full max-w-md'>
<CardHeader>
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle>
<CardDescription>
Send a verification email to verify your account
</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>{renderContent()}</CardContent>
<CardFooter>
<SendVerificationButton
onClick={() => sendVerificationEmailMutation()}
isLoading={isSendingVerificationEmail}
hasVerificationCode={false}
/>
</CardFooter>
</Card>
)
}
const VerifyEmail = ({
userId,
verificationCode,
}: {
userId: string
verificationCode: string
}) => {
const [successMessage, setSuccessMessage] = useState<string>('')
const [error, setError] = useState<string>('')
const [isVerified, setIsVerified] = useState(false)
const { mutate: verifyEmailMutation, isPending: isVerifyingEmail } =
useMutation({
mutationFn: () =>
httpBrowserClient.post('/auth/verify-email', {
userId,
verificationCode,
}),
onSuccess: () => {
setIsVerified(true)
setSuccessMessage('Your email has been successfully verified')
},
onError: (error: any) => {
setError(error.message || 'Failed to verify email')
},
})
useEffect(() => {
verifyEmailMutation()
}, [verifyEmailMutation])
const renderContent = () => {
if (isVerifyingEmail)
return (
<div className='flex justify-center py-8'>
<Loader2 className='h-12 w-12 animate-spin text-primary' />
</div>
)
if (isVerified)
return (
<Alert
variant='default'
className='bg-green-50 text-green-700 border-green-200'
>
<CheckCircle className='h-5 w-5 text-green-600' />
<AlertTitle className='text-lg font-semibold'>Success</AlertTitle>
<AlertDescription>{successMessage}</AlertDescription>
</Alert>
)
if (error) return <ErrorAlert message={error} />
return null
}
return (
<Card className='w-full max-w-md'>
<CardHeader>
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle>
<CardDescription>Verifying your email address...</CardDescription>
</CardHeader>
<CardContent className='space-y-4'>{renderContent()}</CardContent>
<CardFooter className='flex flex-col gap-4'>
{isVerified && (
<Button className='w-full text-lg py-6' asChild>
<Link href='/dashboard'>
Go to Dashboard
<ArrowRight className='ml-2 h-5 w-5' />
</Link>
</Button>
)}
</CardFooter>
</Card>
)
}
export default function VerifyEmailPage() {
const searchParams = useSearchParams()
const userId = searchParams.get('userId')
const verificationCode = searchParams.get('verificationCode')
return (
<div className='flex min-h-screen items-center justify-center bg-gray-100 dark:bg-gray-900 p-4'>
{userId && verificationCode ? (
<VerifyEmail userId={userId} verificationCode={verificationCode} />
) : (
<SendVerificationEmail />
)}
</div>
)
}

3
web/config/api.ts

@ -8,6 +8,9 @@ export const ApiEndpoints = {
whoAmI: () => '/auth/who-am-i',
sendEmailVerificationEmail: () => '/auth/send-email-verification-email',
verifyEmail: () => '/auth/verify-email',
requestPasswordReset: () => '/auth/request-password-reset',
resetPassword: () => '/auth/reset-password',

Loading…
Cancel
Save