Files
nika-gallery/frontend/app/club/gallery/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

91 lines
3.4 KiB
TypeScript

'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { authFetch, consumeHashToken, getToken } from '@/lib/auth';
import type { Work } from '@/lib/types';
export default function ClubGalleryPage() {
const [state, setState] = useState<'loading' | 'ok' | 'forbidden' | 'guest'>('loading');
const [items, setItems] = useState<Work[]>([]);
useEffect(() => {
consumeHashToken();
if (!getToken()) {
setState('guest');
return;
}
authFetch<{ items: Work[] }>('/club/gallery')
.then((r) => {
setItems(r.items);
setState('ok');
})
.catch((err) => {
setState((err as { status?: number })?.status === 403 ? 'forbidden' : 'guest');
});
}, []);
return (
<div className="mx-auto max-w-content px-6 py-20 md:px-10">
<header className="mx-auto max-w-2xl text-center">
<p className="text-sm uppercase tracking-[0.25em] text-accent">Только для членов клуба</p>
<h1 className="mt-4 font-serif text-5xl tracking-tightest">Закрытая галерея</h1>
</header>
{state === 'loading' && (
<div className="mt-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="aspect-[4/5] animate-shimmer rounded-2xl border border-line dark:border-white/10" />
))}
</div>
)}
{(state === 'forbidden' || state === 'guest') && (
<div className="mx-auto mt-16 max-w-md rounded-3xl border border-line p-8 text-center dark:border-white/10">
<p className="text-muted">
{state === 'guest'
? 'Войдите и получите уровень доступа, чтобы увидеть закрытые работы.'
: 'Этот раздел открыт только членам закрытого клуба.'}
</p>
<Link
href="/club"
className="mt-6 inline-block rounded-full bg-ink px-6 py-3 text-sm text-paper transition-colors hover:bg-accent"
>
Перейти в клуб
</Link>
</div>
)}
{state === 'ok' && items.length === 0 && (
<p className="mt-16 text-center text-muted">Пока здесь нет работ. Загляните позже.</p>
)}
{state === 'ok' && items.length > 0 && (
<div className="mt-16 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{items.map((w) => (
<article
key={w.id}
className="group overflow-hidden rounded-2xl border border-line dark:border-white/10"
>
<div className="aspect-[4/5] overflow-hidden bg-line/40">
{w.cover_url ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={w.cover_thumb || w.cover_url}
alt={w.title}
className="h-full w-full object-cover transition-transform duration-700 ease-gallery group-hover:scale-105"
/>
) : null}
</div>
<div className="p-4">
<h3 className="font-serif text-xl tracking-tightest">{w.title}</h3>
{w.medium && <p className="mt-1 text-sm text-muted">{w.medium}</p>}
</div>
</article>
))}
</div>
)}
</div>
);
}