Страница /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>
91 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|