Snapshot of uncommitted work: landing, bots, prod infra, partner cabinet, backoffice stage 1
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
45d7512d2b
commit
d452824c62
314 files changed
+44394
-121
No files matched your search
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user