Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
86 lines
4.0 KiB
TypeScript
86 lines
4.0 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { Card } from '@barev/ui';
|
|
import { useI18n, useTRPC } from '@barev/web';
|
|
import { useEffect, useState } from 'react';
|
|
import { LEVEL_NAME, LEVEL_TONE, PageHead, type Level } from '../lib.tsx';
|
|
import { Qr } from '../qr.tsx';
|
|
|
|
/**
|
|
* Клубная карта с живым QR (меняется каждую минуту — скриншот не сработает) и партнёры клуба:
|
|
* что положено на твоём уровне и сколько осталось по лимиту.
|
|
*/
|
|
export function Perks() {
|
|
const { t, locale } = useI18n();
|
|
const trpc = useTRPC();
|
|
const me = useQuery(trpc.client.me.queryOptions());
|
|
const qr = useQuery({ ...trpc.client.cardQr.queryOptions(), refetchInterval: 20_000, refetchIntervalInBackground: false });
|
|
const partners = useQuery(trpc.client.partners.queryOptions());
|
|
const level = (me.data?.membership.level ?? 'BAREV') as Level;
|
|
const tone = LEVEL_TONE[level];
|
|
const left = useSecondsLeft(qr.data?.expiresAt);
|
|
const name = (l: Record<string, string>) => l[locale] || l.ru || '';
|
|
|
|
return (
|
|
<main className="app">
|
|
<PageHead title={t('client.perks.title')} back={false} />
|
|
<section className="card-qr" style={{ background: level === 'BAREV' ? 'var(--bc-plum)' : tone.bg, color: level === 'BAREV' ? '#fff' : tone.ink }}>
|
|
<div className="card-qr__top">
|
|
<span>Barev Club · {LEVEL_NAME[level]}</span>
|
|
<b>{me.data?.firstName}</b>
|
|
</div>
|
|
<div className="card-qr__code">{qr.data ? <Qr value={qr.data.url} size={208} label={t('client.partners.qrLabel')} /> : <div style={{ width: 208, height: 208 }} />}</div>
|
|
<div className="card-qr__meta">
|
|
<span className="bc-num">{me.data?.cardCode}</span>
|
|
<span>{t('client.partners.qrRefresh', { seconds: left })}</span>
|
|
</div>
|
|
</section>
|
|
<p className="bc-small bc-muted" style={{ margin: 0 }}>
|
|
{t('client.partners.howTo')}
|
|
</p>
|
|
{partners.data?.length === 0 ? (
|
|
<Card tone="perks" className="bc-stack">
|
|
<h2 className="bc-h3">{t('client.perks.soonTitle')}</h2>
|
|
<p style={{ margin: 0 }}>{t('client.perks.soonText')}</p>
|
|
</Card>
|
|
) : (
|
|
<ul className="partners">
|
|
{partners.data?.map((p) => (
|
|
<li key={p.id} className="partner">
|
|
<span className="partner__cat">{t(`client.partners.cat.${p.category}`)}</span>
|
|
<b className="partner__name">{p.name}</b>
|
|
{p.address && <span className="bc-small bc-muted">{p.address}</span>}
|
|
<ul className="offers">
|
|
{(p.offers.length ? p.offers : [{ id: 'main', title: p.offer, eligible: true, remaining: null, minLevel: null, limit: 'NONE' as const, limitCount: 1 }]).map((o) => (
|
|
<li key={o.id} className="offer" data-eligible={o.eligible} data-used={o.remaining === 0}>
|
|
<span>{name(o.title)}</span>
|
|
<small>
|
|
{!o.eligible
|
|
? t('client.partners.needLevel', { level: LEVEL_NAME[(o.minLevel ?? 'BAREV') as Level] })
|
|
: o.remaining === null
|
|
? o.minLevel
|
|
? LEVEL_NAME[o.minLevel as Level]
|
|
: t('client.partners.allMembers')
|
|
: o.remaining > 0
|
|
? t(`client.partners.limit_${o.limit}`, { count: o.limitCount })
|
|
: t(`client.partners.used_${o.limit}`)}
|
|
</small>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</main>
|
|
);
|
|
}
|
|
|
|
function useSecondsLeft(expiresAt?: Date) {
|
|
const [now, setNow] = useState(Date.now());
|
|
useEffect(() => {
|
|
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
return () => clearInterval(id);
|
|
}, []);
|
|
return expiresAt ? Math.max(0, Math.ceil((new Date(expiresAt).getTime() - now) / 1000)) : 60;
|
|
}
|