first commit
This commit is contained in:
93
frontend/src/pages/VerifyEmailPage.tsx
Normal file
93
frontend/src/pages/VerifyEmailPage.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user