Первоначальная версия VidConf

This commit is contained in:
2026-07-23 01:04:01 +03:00
commit 896455381a
335 changed files with 61527 additions and 0 deletions

View 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>
)
}