Files
vidconf/frontend/src/pages/VerifyEmailPage.tsx
Max Ronzhin 8757bec8ac
Some checks failed
CI / backend (push) Has been cancelled
CI / frontend (push) Has been cancelled
first commit
2026-07-23 02:38:05 +03:00

94 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react'
import { Link, useSearchParams } from 'react-router-dom'
import { AlertTriangle, ArrowRight, CheckCircle2, Loader2 } from 'lucide-react'
import { AuthLayout } from '@/components/auth/AuthLayout'
import { verifyEmail } from '@/api/auth'
type VerifyState = 'verifying' | 'success' | 'error'
/**
* Кэш промисов подтверждения по токену (модульный, вне компонента) — тот же
* приём дедупликации, что и у refreshPromise в api/client.ts. Защищает от
* повторного POST /auth/verify-email при двойном монтировании эффекта в
* React StrictMode (dev): без кэша второй запрос уходит с уже погашенным
* токеном и backend отвечает 400 «токен уже использован», хотя первый запрос
* уже успешно подтвердил почту.
*/
const verifyRequests = new Map<string, Promise<void>>()
function verifyEmailOnce(token: string): Promise<void> {
let request = verifyRequests.get(token)
if (!request) {
request = verifyEmail(token)
verifyRequests.set(token, request)
}
return request
}
/**
* Экран подтверждения email по ссылке из письма: /verify-email?token=...
* (см. design/mockups/auth.html, блок «подтверждение email»).
*/
export function VerifyEmailPage() {
const [searchParams] = useSearchParams()
const token = searchParams.get('token')
const [state, setState] = useState<VerifyState>(token ? 'verifying' : 'error')
useEffect(() => {
if (!token) return
let cancelled = false
verifyEmailOnce(token)
.then(() => {
if (!cancelled) setState('success')
})
.catch(() => {
if (!cancelled) setState('error')
})
return () => {
cancelled = true
}
}, [token])
return (
<AuthLayout>
<section className="screen-block">
{state === 'verifying' && (
<div className="confirm-card pending">
<div className="confirm-icon">
<Loader2 className="lucide spin" aria-hidden="true" />
</div>
<h3>Проверяем ссылку</h3>
<p>Подтверждаем вашу почту, это займёт пару секунд.</p>
</div>
)}
{state === 'success' && (
<div className="confirm-card">
<div className="confirm-icon">
<CheckCircle2 className="lucide" aria-hidden="true" />
</div>
<h3>Почта подтверждена</h3>
<p>Аккаунт активирован. Теперь доступно бронирование переговорных и вход в конференции.</p>
<Link to="/login" className="btn btn-primary btn-block btn-lg">
Перейти ко входу <ArrowRight className="lucide" aria-hidden="true" />
</Link>
</div>
)}
{state === 'error' && (
<div className="confirm-card error">
<div className="confirm-icon">
<AlertTriangle className="lucide" aria-hidden="true" />
</div>
<h3>Ссылка недействительна</h3>
<p>Срок действия ссылки истёк или она уже была использована. Запросите письмо ещё раз со страницы регистрации.</p>
<Link to="/register" className="btn btn-secondary btn-block btn-lg">
Вернуться к регистрации
</Link>
</div>
)}
</section>
</AuthLayout>
)
}