Files
nika-gallery/frontend/app/login/page.tsx
T
germanandClaude Opus 4.8 a355188301 feat(club): frontend закрытого клуба + вход через Яндекс
Страница /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>
2026-08-05 21:49:15 +03:00

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