diff --git a/frontend/app/club/gallery/page.tsx b/frontend/app/club/gallery/page.tsx new file mode 100644 index 0000000..f61600a --- /dev/null +++ b/frontend/app/club/gallery/page.tsx @@ -0,0 +1,90 @@ +'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([]); + + 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 ( +
+
+

Только для членов клуба

+

Закрытая галерея

+
+ + {state === 'loading' && ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+ )} + + {(state === 'forbidden' || state === 'guest') && ( +
+

+ {state === 'guest' + ? 'Войдите и получите уровень доступа, чтобы увидеть закрытые работы.' + : 'Этот раздел открыт только членам закрытого клуба.'} +

+ + Перейти в клуб + +
+ )} + + {state === 'ok' && items.length === 0 && ( +

Пока здесь нет работ. Загляните позже.

+ )} + + {state === 'ok' && items.length > 0 && ( +
+ {items.map((w) => ( +
+
+ {w.cover_url ? ( + // eslint-disable-next-line @next/next/no-img-element + {w.title} + ) : null} +
+
+

{w.title}

+ {w.medium &&

{w.medium}

} +
+
+ ))} +
+ )} +
+ ); +} diff --git a/frontend/app/club/page.tsx b/frontend/app/club/page.tsx new file mode 100644 index 0000000..2dc757c --- /dev/null +++ b/frontend/app/club/page.tsx @@ -0,0 +1,242 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; +import { + consumeHashToken, + fetchMe, + isAdmin, + yandexLoginUrl, + type AuthUser, +} from '@/lib/auth'; +import { + clubApi, + type ClubTier, + type ClubTierId, + type MyAccessRequest, + type RequestStatus, +} from '@/lib/club'; +import AdminRequests from '@/components/AdminRequests'; + +// Фолбэк-описание уровней (если API недоступен) — синхронно с backend CLUB_TIERS. +const FALLBACK_TIERS: ClubTier[] = [ + { id: 'vip', title: 'VIP', desc: 'Полный доступ ко всем закрытым работам, приоритет на аукционах, личные превью.' }, + { id: 'collector', title: 'Коллекционеры', desc: 'Ранний доступ к новым работам и коллекционным сериям, консультации по подбору.' }, + { id: 'partner', title: 'Партнёры', desc: 'Совместные проекты и коллаборации, специальные условия сотрудничества.' }, + { id: 'photographer', title: 'Фотографы', desc: 'Backstage-материалы и совместные съёмки body art.' }, + { id: 'organizer', title: 'Организаторы', desc: 'Работы и материалы для выставок, перформансов и мероприятий.' }, + { id: 'press', title: 'Пресса', desc: 'Пресс-материалы, изображения в высоком разрешении, интервью.' }, +]; + +const STATUS_LABEL: Record = { + pending: 'на рассмотрении', + approved: 'одобрена', + declined: 'отклонена', +}; + +function tierTitle(tiers: ClubTier[], id: ClubTierId): string { + return tiers.find((t) => t.id === id)?.title ?? id; +} + +export default function ClubPage() { + const [ready, setReady] = useState(false); + const [user, setUser] = useState(null); + const [tiers, setTiers] = useState(FALLBACK_TIERS); + const [clubTier, setClubTier] = useState(null); + const [myRequest, setMyRequest] = useState(null); + + // Форма заявки. + const [selected, setSelected] = useState(''); + const [note, setNote] = useState(''); + const [busy, setBusy] = useState(false); + const [msg, setMsg] = useState<{ ok: boolean; text: string } | null>(null); + + const refreshMine = useCallback(async () => { + try { + const r = await clubApi.myRequest(); + setClubTier(r.club_tier); + setMyRequest(r.request); + } catch { + /* не критично */ + } + }, []); + + useEffect(() => { + consumeHashToken(); + clubApi.tiers().then(setTiers).catch(() => setTiers(FALLBACK_TIERS)); + (async () => { + const me = await fetchMe(); + setUser(me); + if (me) await refreshMine(); + setReady(true); + })(); + }, [refreshMine]); + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!selected) return; + setBusy(true); + setMsg(null); + try { + await clubApi.submit(selected, note.trim()); + setMsg({ ok: true, text: 'Заявка отправлена. Мы сообщим о решении.' }); + setNote(''); + setSelected(''); + await refreshMine(); + } catch (err) { + const m = (err as { message?: string })?.message || 'Не удалось отправить заявку.'; + setMsg({ ok: false, text: m }); + } finally { + setBusy(false); + } + } + + const canRequest = user && !myRequest && !clubTier; + const pendingSame = myRequest?.status === 'pending'; + + return ( +
+ {/* Заголовок */} +
+

Только по приглашению

+

Закрытый клуб

+

+ Пространство для тех, кто ближе к искусству Ники: закрытые серии body art, + приватные превью, приоритет на аукционах и прямой диалог. Доступ — по заявке + и решению автора. +

+
+ + {/* Уровни доступа */} +
+ {tiers.map((t) => { + const active = clubTier === t.id; + return ( +
+
+

{t.title}

+ {active && ( + + ваш уровень + + )} +
+

{t.desc}

+
+ ); + })} +
+ + {/* Блок действия: вход / заявка / статус */} +
+ {!ready ? ( +
+ ) : !user ? ( +
+

Войдите, чтобы подать заявку

+

+ Быстрый вход через Яндекс ID — без пароля и регистрации. +

+ + + Я + + Войти через Яндекс + +
+ ) : clubTier ? ( +
+

Добро пожаловать

+

+ Вы — {tierTitle(tiers, clubTier)} +

+

+ Закрытая галерея открыта для вас. +

+ + Открыть закрытую галерею + +
+ ) : myRequest && pendingSame ? ( +
+

Заявка на рассмотрении

+

+ Уровень «{tierTitle(tiers, myRequest.tier)}» — статус:{' '} + {STATUS_LABEL[myRequest.status]}. + Мы сообщим о решении. +

+
+ ) : ( +
+

Подать заявку

+ {myRequest?.status === 'declined' && ( +

+ Предыдущая заявка («{tierTitle(tiers, myRequest.tier)}») была отклонена. Вы можете + подать новую. +

+ )} + + + + + +