Страница /club (уровни, форма заявки, статус, админ-модерация), /club/gallery (закрытая галерея), /login (кнопка «Войти через Яндекс» + email-вход, разбор #token). Хелперы lib/auth.ts, lib/club.ts, компонент AdminRequests. Ссылки «Клуб»/«Войти» в Nav. Эстетика по существующим токенам (paper/ink/accent, Cormorant/Inter). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
import { useRouter } from 'next/navigation';
|
|
import { API_BASE } from '@/lib/api';
|
|
import {
|
|
consumeHashToken,
|
|
fetchMe,
|
|
getToken,
|
|
setTokens,
|
|
yandexLoginUrl,
|
|
type AuthUser,
|
|
} from '@/lib/auth';
|
|
|
|
// Иконка Яндекса (моно, вписана в кнопку).
|
|
function YandexMark() {
|
|
return (
|
|
<span className="flex h-5 w-5 items-center justify-center rounded-full bg-[#FC3F1D] text-[13px] font-bold leading-none text-white">
|
|
Я
|
|
</span>
|
|
);
|
|
}
|
|
|
|
export default function LoginPage() {
|
|
const router = useRouter();
|
|
const [user, setUser] = useState<AuthUser | null>(null);
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
// На случай, если Яндекс вернул токен именно на /login.
|
|
consumeHashToken();
|
|
if (getToken()) fetchMe().then(setUser);
|
|
}, []);
|
|
|
|
async function onSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
const res = await fetch(`${API_BASE}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data?.error || 'Не удалось войти.');
|
|
setTokens(data.access_token, data.refresh_token);
|
|
router.push('/club');
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Ошибка входа.');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
if (user) {
|
|
return (
|
|
<section className="mx-auto max-w-md px-6 py-24 text-center md:px-10">
|
|
<p className="text-sm uppercase tracking-[0.25em] text-muted">Вход</p>
|
|
<h1 className="mt-4 font-serif text-4xl tracking-tightest">
|
|
Вы вошли как {user.name}
|
|
</h1>
|
|
<Link
|
|
href="/club"
|
|
className="mt-8 inline-block rounded-full bg-ink px-6 py-3 text-sm text-paper transition-colors hover:bg-accent"
|
|
>
|
|
Перейти в закрытый клуб
|
|
</Link>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section className="mx-auto max-w-md px-6 py-24 md:px-10">
|
|
<p className="text-center text-sm uppercase tracking-[0.25em] text-muted">Личный кабинет</p>
|
|
<h1 className="mt-4 text-center font-serif text-4xl tracking-tightest">Вход</h1>
|
|
|
|
<a
|
|
href={yandexLoginUrl()}
|
|
className="mt-10 flex w-full items-center justify-center gap-3 rounded-full border border-ink/15 bg-white px-6 py-3.5 text-sm font-medium text-ink shadow-sm transition-colors hover:border-ink/40 dark:bg-white/5 dark:text-paper"
|
|
>
|
|
<YandexMark />
|
|
Войти через Яндекс
|
|
</a>
|
|
|
|
<div className="my-8 flex items-center gap-4 text-xs uppercase tracking-widest text-muted">
|
|
<span className="h-px flex-1 bg-line" />
|
|
или по e-mail
|
|
<span className="h-px flex-1 bg-line" />
|
|
</div>
|
|
|
|
<form onSubmit={onSubmit} className="flex flex-col gap-3">
|
|
<input
|
|
type="email"
|
|
required
|
|
placeholder="e-mail"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className="rounded-2xl border border-line bg-transparent px-5 py-3 text-sm outline-none placeholder:text-muted focus:border-ink dark:border-white/15"
|
|
/>
|
|
<input
|
|
type="password"
|
|
required
|
|
placeholder="пароль"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
className="rounded-2xl border border-line bg-transparent px-5 py-3 text-sm outline-none placeholder:text-muted focus:border-ink dark:border-white/15"
|
|
/>
|
|
{error && <p className="text-sm text-[#b23b3b]">{error}</p>}
|
|
<button
|
|
type="submit"
|
|
disabled={busy}
|
|
className="mt-2 rounded-full bg-ink px-6 py-3 text-sm text-paper transition-colors hover:bg-accent disabled:opacity-60"
|
|
>
|
|
{busy ? 'Входим…' : 'Войти'}
|
|
</button>
|
|
</form>
|
|
|
|
<p className="mt-8 text-center text-sm text-muted">
|
|
Закрытая платформа. Регистрация — по приглашению или через{' '}
|
|
<Link href="/club" className="text-accent underline-offset-4 hover:underline">
|
|
заявку в клуб
|
|
</Link>
|
|
.
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|