mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(ui): finish design-audit follow-ups
- useFeatureFlags: кэш флагов в localStorage — нижняя навигация больше не прыгает на холодном старте (таб «Рефералы»/«Колесо» появлялся после загрузки terms/config) - applyThemeColors: частичный/битый ответ /branding/colors добивается дефолтами вместо падения всего приложения в ErrorBoundary - карточка «Связаться с поддержкой»: кнопка больше не дублирует заголовок («Написать», без переноса) - «Мои подписки»: заголовок и «Купить ещё» в одну строку на 390px - StatCard: подписи плиток переносятся на две строки вместо обрезки многоточием («Бонус новому польз…»)
This commit is contained in:
@@ -52,7 +52,7 @@ export function StatCard({
|
||||
return (
|
||||
<div className="h-full rounded-xl bg-dark-800/30 p-3 transition-colors hover:bg-dark-800/50">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="truncate text-xs text-dark-500 sm:text-sm">{label}</span>
|
||||
<span className="line-clamp-2 text-xs leading-tight text-dark-500 sm:text-sm">{label}</span>
|
||||
{trailing}
|
||||
</div>
|
||||
{/* Chip is centred against the value line only (delta sits below the whole
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '@/store/auth';
|
||||
import { brandingApi } from '@/api/branding';
|
||||
@@ -6,8 +7,31 @@ import { wheelApi } from '@/api/wheel';
|
||||
import { contestsApi } from '@/api/contests';
|
||||
import { pollsApi } from '@/api/polls';
|
||||
|
||||
// Последние известные значения флагов. Пока запросы в полёте, флаги были
|
||||
// undefined -> табы «Рефералы»/«Колесо» появлялись с задержкой и нижняя
|
||||
// навигация прыгала на каждом холодном старте. Кэш убирает layout shift;
|
||||
// при изменении флага на бэке UI догонит после ответа запроса.
|
||||
const FLAGS_CACHE_KEY = 'cabinet-feature-flags';
|
||||
|
||||
type CachedFlags = {
|
||||
referralEnabled?: boolean;
|
||||
wheelEnabled?: boolean;
|
||||
hasContests?: boolean;
|
||||
hasPolls?: boolean;
|
||||
giftEnabled?: boolean;
|
||||
};
|
||||
|
||||
function readFlagsCache(): CachedFlags {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(FLAGS_CACHE_KEY) || '{}') as CachedFlags;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function useFeatureFlags() {
|
||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||
const cached = readFlagsCache();
|
||||
|
||||
const { data: referralTerms } = useQuery({
|
||||
queryKey: ['referral-terms'],
|
||||
@@ -49,11 +73,29 @@ export function useFeatureFlags() {
|
||||
retry: false,
|
||||
});
|
||||
|
||||
return {
|
||||
referralEnabled: referralTerms?.is_enabled,
|
||||
wheelEnabled: wheelConfig?.is_enabled,
|
||||
hasContests: (contestsCount?.count ?? 0) > 0,
|
||||
hasPolls: (pollsCount?.count ?? 0) > 0,
|
||||
giftEnabled: giftConfig?.enabled,
|
||||
const flags = {
|
||||
referralEnabled: referralTerms ? referralTerms.is_enabled : cached.referralEnabled,
|
||||
wheelEnabled: wheelConfig ? wheelConfig.is_enabled : cached.wheelEnabled,
|
||||
hasContests: contestsCount ? contestsCount.count > 0 : cached.hasContests,
|
||||
hasPolls: pollsCount ? pollsCount.count > 0 : cached.hasPolls,
|
||||
giftEnabled: giftConfig ? giftConfig.enabled : cached.giftEnabled,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!referralTerms && !wheelConfig && !contestsCount && !pollsCount && !giftConfig) return;
|
||||
try {
|
||||
localStorage.setItem(FLAGS_CACHE_KEY, JSON.stringify(flags));
|
||||
} catch {
|
||||
// квоты/приватный режим — некритично
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
flags.referralEnabled,
|
||||
flags.wheelEnabled,
|
||||
flags.hasContests,
|
||||
flags.hasPolls,
|
||||
flags.giftEnabled,
|
||||
]);
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
@@ -110,7 +110,10 @@ function onColorFor(bgTriplet: string): string {
|
||||
}
|
||||
|
||||
// Apply theme colors as CSS variables (RGB format for Tailwind opacity support)
|
||||
export function applyThemeColors(colors: ThemeColors): void {
|
||||
export function applyThemeColors(themeColors: ThemeColors): void {
|
||||
// Частичный/битый ответ /branding/colors раньше ронял ВСЁ приложение в
|
||||
// ErrorBoundary (hexToRgb(undefined)). Недостающие поля добиваем дефолтами.
|
||||
const colors: ThemeColors = { ...DEFAULT_THEME_COLORS, ...themeColors };
|
||||
const root = document.documentElement;
|
||||
|
||||
// Generate palettes from status colors
|
||||
|
||||
@@ -1045,7 +1045,8 @@
|
||||
"openSupport": "Open Support",
|
||||
"contactSupport": "Please contact {{username}} for support",
|
||||
"contactUs": "Contact Support",
|
||||
"create_ticket": "Create ticket"
|
||||
"create_ticket": "Create ticket",
|
||||
"writeButton": "Message"
|
||||
},
|
||||
"wheel": {
|
||||
"title": "Fortune Wheel",
|
||||
@@ -5242,7 +5243,7 @@
|
||||
},
|
||||
"subscriptions": {
|
||||
"buy": "Buy subscription",
|
||||
"buyAnother": "Buy another subscription",
|
||||
"buyAnother": "Buy another",
|
||||
"browsePlans": "View plans and subscribe",
|
||||
"empty": "You have no subscriptions yet",
|
||||
"emptyDesc": "Choose a tariff to get started",
|
||||
|
||||
@@ -928,7 +928,8 @@
|
||||
"openSupport": "باز کردن پشتیبانی",
|
||||
"ticketsDisabled": "تیکتها غیرفعال است",
|
||||
"useExternalLink": "لطفاً از لینک خارجی برای دریافت پشتیبانی استفاده کنید",
|
||||
"create_ticket": "ایجاد تیکت"
|
||||
"create_ticket": "ایجاد تیکت",
|
||||
"writeButton": "پیام"
|
||||
},
|
||||
"wheel": {
|
||||
"title": "چرخ شانس",
|
||||
@@ -4876,7 +4877,7 @@
|
||||
},
|
||||
"subscriptions": {
|
||||
"buy": "خرید اشتراک",
|
||||
"buyAnother": "خرید اشتراک دیگر",
|
||||
"buyAnother": "خرید دیگر",
|
||||
"browsePlans": "مشاهده تعرفهها و خرید اشتراک",
|
||||
"empty": "هنوز اشتراکی ندارید",
|
||||
"emptyDesc": "برای شروع تعرفهای انتخاب کنید",
|
||||
|
||||
@@ -1074,7 +1074,8 @@
|
||||
"openSupport": "Открыть поддержку",
|
||||
"contactSupport": "Для получения поддержки обратитесь к {{username}}",
|
||||
"contactUs": "Связаться с поддержкой",
|
||||
"create_ticket": "Создать тикет"
|
||||
"create_ticket": "Создать тикет",
|
||||
"writeButton": "Написать"
|
||||
},
|
||||
"wheel": {
|
||||
"title": "Колесо удачи",
|
||||
@@ -5804,7 +5805,7 @@
|
||||
},
|
||||
"subscriptions": {
|
||||
"buy": "Купить подписку",
|
||||
"buyAnother": "Купить ещё подписку",
|
||||
"buyAnother": "Купить ещё",
|
||||
"browsePlans": "Посмотреть тарифы и купить подписку",
|
||||
"empty": "У вас пока нет подписок",
|
||||
"emptyDesc": "Выберите тариф, чтобы начать пользоваться сервисом",
|
||||
|
||||
@@ -928,7 +928,8 @@
|
||||
"openSupport": "打开支持",
|
||||
"ticketsDisabled": "工单已禁用",
|
||||
"useExternalLink": "请使用外部链接获取支持",
|
||||
"create_ticket": "创建工单"
|
||||
"create_ticket": "创建工单",
|
||||
"writeButton": "发消息"
|
||||
},
|
||||
"wheel": {
|
||||
"title": "幸运转盘",
|
||||
@@ -4875,7 +4876,7 @@
|
||||
},
|
||||
"subscriptions": {
|
||||
"buy": "购买订阅",
|
||||
"buyAnother": "再购买一个订阅",
|
||||
"buyAnother": "再购买",
|
||||
"browsePlans": "查看套餐并订阅",
|
||||
"empty": "您还没有订阅",
|
||||
"emptyDesc": "选择套餐以开始使用",
|
||||
|
||||
@@ -109,15 +109,15 @@ export default function Subscriptions() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold" style={{ color: g.text }}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h1 className="truncate text-xl font-bold" style={{ color: g.text }}>
|
||||
{t('subscriptions.title', 'Мои подписки')}
|
||||
</h1>
|
||||
{/* «+ Купить ещё» — только если уже есть платная активная подписка */}
|
||||
{!isLoading && hasActivePaid && (
|
||||
<button
|
||||
onClick={() => navigate('/subscription/purchase')}
|
||||
className="flex items-center gap-1.5 rounded-xl px-4 py-2 text-sm font-medium transition-colors"
|
||||
className="flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-xl px-4 py-2 text-sm font-medium transition-colors"
|
||||
style={{
|
||||
background: 'rgba(var(--color-accent-400), 0.1)',
|
||||
color: 'rgb(var(--color-accent-400))',
|
||||
|
||||
@@ -363,6 +363,7 @@ export default function Support() {
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="shrink-0 whitespace-nowrap"
|
||||
onClick={() => {
|
||||
const username = supportConfig.support_username!.startsWith('@')
|
||||
? supportConfig.support_username!.slice(1)
|
||||
@@ -370,7 +371,7 @@ export default function Support() {
|
||||
openTelegramLink(`https://t.me/${username}`);
|
||||
}}
|
||||
>
|
||||
{t('support.contactUs')}
|
||||
{t('support.writeButton', 'Написать')}
|
||||
</Button>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user