mirror of
https://github.com/vernu/textbee.git
synced 2026-05-19 05:46:23 -04:00
refactor(web): refactor email verification page
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { useSearchParams, useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
@@ -14,11 +14,12 @@ import {
|
||||
} 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 { useMutation, useQuery } from '@tanstack/react-query'
|
||||
import httpBrowserClient from '@/lib/httpBrowserClient'
|
||||
import { ApiEndpoints } from '@/config/api'
|
||||
import { Routes } from '@/config/routes'
|
||||
|
||||
// Reusable components
|
||||
const ErrorAlert = ({ message }: { message: string }) => (
|
||||
<Alert
|
||||
variant='destructive'
|
||||
@@ -30,258 +31,291 @@ const ErrorAlert = ({ message }: { message: string }) => (
|
||||
</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'}
|
||||
const SuccessAlert = ({ title, message }: { title: string; message: string }) => (
|
||||
<Alert 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'>{title}</AlertTitle>
|
||||
<AlertDescription>{message}</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
|
||||
const InfoAlert = ({ title, message }: { title: string; message: string }) => (
|
||||
<Alert 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'>{title}</AlertTitle>
|
||||
<AlertDescription>{message}</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
|
||||
const LoadingSpinner = () => (
|
||||
<div className='flex justify-center py-6'>
|
||||
<Loader2 className='h-10 w-10 animate-spin text-primary' />
|
||||
</div>
|
||||
)
|
||||
|
||||
const DashboardButton = () => (
|
||||
<Button className='w-full py-5 mt-2' asChild>
|
||||
<Link href={Routes.dashboard}>
|
||||
Go to Dashboard
|
||||
<ArrowRight className='ml-2 h-5 w-5' />
|
||||
</Link>
|
||||
</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>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<div>{successMessage}</div>
|
||||
<Link href={Routes.dashboard} className='font-medium underline'>
|
||||
Go to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
const CheckEmailPrompt = () => {
|
||||
const {
|
||||
mutate: sendVerificationEmailMutation,
|
||||
isPending,
|
||||
isError,
|
||||
isSuccess,
|
||||
} = useMutation({
|
||||
mutationFn: () =>
|
||||
httpBrowserClient.post(
|
||||
ApiEndpoints.auth.sendEmailVerificationEmail(),
|
||||
{}
|
||||
),
|
||||
})
|
||||
|
||||
return (
|
||||
<Card className='w-full max-w-md'>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-2xl font-bold'>Check your email</CardTitle>
|
||||
<CardDescription>
|
||||
We've sent you a verification email. Please check your inbox and click
|
||||
the link to verify your account.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
{isSuccess && (
|
||||
<Alert 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'>
|
||||
Email Sent
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
A new verification email has been sent to your inbox
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{isError && (
|
||||
<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>
|
||||
Failed to resend verification email
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex flex-col gap-4'>
|
||||
<div className='flex items-center gap-2 justify-center w-full'>
|
||||
<span className='text-sm text-gray-600'>
|
||||
Didn't receive the email?
|
||||
</span>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={() => sendVerificationEmailMutation()}
|
||||
disabled={isPending}
|
||||
className='text-sm p-0 h-auto font-semibold'
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
'Click to resend'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
const LoginButton = () => (
|
||||
<Button className='w-full py-5 mt-2' asChild>
|
||||
<Link href='/login'>
|
||||
Go to Login
|
||||
<ArrowRight className='ml-2 h-5 w-5' />
|
||||
</Link>
|
||||
</Button>
|
||||
)
|
||||
|
||||
export default function VerifyEmailPage() {
|
||||
const router = useRouter()
|
||||
const searchParams = useSearchParams()
|
||||
const userId = searchParams.get('userId')
|
||||
const verificationCode = searchParams.get('verificationCode')
|
||||
const verificationEmailSent = searchParams.get('verificationEmailSent')
|
||||
|
||||
const [successMessage, setSuccessMessage] = useState<string>('')
|
||||
const [errorMessage, setErrorMessage] = useState<string>('')
|
||||
|
||||
// Check user authentication and email verification status
|
||||
const {
|
||||
data: whoAmIData,
|
||||
isPending: isCheckingAuth,
|
||||
isError: isAuthError
|
||||
} = useQuery({
|
||||
queryKey: ['whoAmI'],
|
||||
queryFn: () => httpBrowserClient.get(ApiEndpoints.auth.whoAmI()),
|
||||
retry: 1,
|
||||
})
|
||||
|
||||
const user = whoAmIData?.data?.data
|
||||
const isEmailVerified = !!user?.emailVerifiedAt
|
||||
const isLoggedIn = !isAuthError && !!user
|
||||
|
||||
// Verify email mutation
|
||||
const {
|
||||
mutate: verifyEmail,
|
||||
isPending: isVerifying
|
||||
} = useMutation({
|
||||
mutationFn: () => httpBrowserClient.post('/auth/verify-email', {
|
||||
userId,
|
||||
verificationCode,
|
||||
}),
|
||||
onSuccess: () => {
|
||||
setSuccessMessage('Your email has been successfully verified')
|
||||
setErrorMessage('')
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setErrorMessage(error.message || 'Failed to verify email')
|
||||
},
|
||||
})
|
||||
|
||||
// Send verification email mutation
|
||||
const {
|
||||
mutate: sendVerificationEmail,
|
||||
isPending: isSending
|
||||
} = useMutation({
|
||||
mutationFn: () => httpBrowserClient.post(
|
||||
ApiEndpoints.auth.sendEmailVerificationEmail(),
|
||||
{}
|
||||
),
|
||||
onSuccess: () => {
|
||||
if (!verificationEmailSent) {
|
||||
router.push('/verify-email?verificationEmailSent=true')
|
||||
} else {
|
||||
setSuccessMessage('Verification email has been sent to your inbox')
|
||||
setErrorMessage('')
|
||||
}
|
||||
},
|
||||
onError: (error: any) => {
|
||||
setErrorMessage(error.message || 'Failed to send verification email')
|
||||
},
|
||||
})
|
||||
|
||||
// Handle verification when code is provided
|
||||
useEffect(() => {
|
||||
if (userId && verificationCode && !isVerifying && !successMessage && !errorMessage) {
|
||||
if (isEmailVerified) {
|
||||
setSuccessMessage('Your email has already been verified')
|
||||
} else if (!isCheckingAuth) {
|
||||
verifyEmail()
|
||||
}
|
||||
}
|
||||
}, [userId, verificationCode, isCheckingAuth, isEmailVerified, isVerifying, successMessage, errorMessage, verifyEmail])
|
||||
|
||||
// Render content based on current state
|
||||
const renderContent = () => {
|
||||
// Show loading state
|
||||
if (isCheckingAuth) {
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle>
|
||||
<CardDescription>Checking verification status...</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LoadingSpinner />
|
||||
</CardContent>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle verification process
|
||||
if (userId && verificationCode) {
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle>
|
||||
<CardDescription>
|
||||
{isVerifying ? 'Verifying your email address...' : 'Email Verification Status'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
{isVerifying ? (
|
||||
<LoadingSpinner />
|
||||
) : successMessage ? (
|
||||
<SuccessAlert title="Success" message={successMessage} />
|
||||
) : errorMessage ? (
|
||||
<ErrorAlert message={errorMessage} />
|
||||
) : null}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{successMessage && <DashboardButton />}
|
||||
</CardFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle "check your email" state
|
||||
if (verificationEmailSent) {
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-2xl font-bold'>Check Your Email</CardTitle>
|
||||
<CardDescription>
|
||||
We've sent you a verification email. Please check your inbox and click
|
||||
the link to verify your account.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
{successMessage && (
|
||||
<InfoAlert title="Email Sent" message={successMessage} />
|
||||
)}
|
||||
{errorMessage && (
|
||||
<ErrorAlert message={errorMessage} />
|
||||
)}
|
||||
{isEmailVerified && (
|
||||
<SuccessAlert
|
||||
title="Already Verified"
|
||||
message="Your email has already been verified"
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className='flex flex-col gap-3'>
|
||||
{isEmailVerified ? (
|
||||
<DashboardButton />
|
||||
) : (
|
||||
<div className='flex items-center gap-2 justify-center w-full'>
|
||||
<span className='text-sm text-gray-600'>
|
||||
Didn't receive the email?
|
||||
</span>
|
||||
<Button
|
||||
variant='link'
|
||||
onClick={() => sendVerificationEmail()}
|
||||
disabled={isSending}
|
||||
className='text-sm p-0 h-auto font-semibold'
|
||||
>
|
||||
{isSending ? (
|
||||
<>
|
||||
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
|
||||
Sending...
|
||||
</>
|
||||
) : (
|
||||
'Click to resend'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// Handle "send verification email" state
|
||||
return (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle className='text-2xl font-bold'>Email Verification</CardTitle>
|
||||
<CardDescription>
|
||||
{isLoggedIn
|
||||
? isEmailVerified
|
||||
? 'Your email is already verified'
|
||||
: 'Verify your email address to access all features'
|
||||
: 'You need to be logged in to verify your email'
|
||||
}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className='space-y-4'>
|
||||
{successMessage && (
|
||||
<InfoAlert title="Email Sent" message={successMessage} />
|
||||
)}
|
||||
{errorMessage && (
|
||||
<ErrorAlert message={errorMessage} />
|
||||
)}
|
||||
{isEmailVerified && (
|
||||
<SuccessAlert
|
||||
title="Already Verified"
|
||||
message="Your email has already been verified"
|
||||
/>
|
||||
)}
|
||||
{!isLoggedIn && (
|
||||
<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'>Not Logged In</AlertTitle>
|
||||
<AlertDescription>
|
||||
You need to be logged in to verify your email
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
{isLoggedIn ? (
|
||||
isEmailVerified ? (
|
||||
<DashboardButton />
|
||||
) : (
|
||||
<Button
|
||||
className='w-full py-5'
|
||||
onClick={() => sendVerificationEmail()}
|
||||
disabled={isSending}
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className='mr-2 h-5 w-5 animate-spin' />
|
||||
) : (
|
||||
<Mail className='mr-2 h-5 w-5' />
|
||||
)}
|
||||
Send Verification Email
|
||||
</Button>
|
||||
)
|
||||
) : (
|
||||
<LoginButton />
|
||||
)}
|
||||
</CardFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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} />
|
||||
) : verificationEmailSent ? (
|
||||
<CheckEmailPrompt />
|
||||
) : (
|
||||
<SendVerificationEmail />
|
||||
)}
|
||||
<Card className='w-full max-w-md shadow-lg border-gray-200 dark:border-gray-800'>
|
||||
{renderContent()}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user