diff --git a/src/components/stats/StatCard.tsx b/src/components/stats/StatCard.tsx index 8a52793..a5ed2b6 100644 --- a/src/components/stats/StatCard.tsx +++ b/src/components/stats/StatCard.tsx @@ -52,7 +52,7 @@ export function StatCard({ return (
- {label} + {label} {trailing}
{/* Chip is centred against the value line only (delta sits below the whole diff --git a/src/hooks/useFeatureFlags.ts b/src/hooks/useFeatureFlags.ts index 8da43e4..9b850aa 100644 --- a/src/hooks/useFeatureFlags.ts +++ b/src/hooks/useFeatureFlags.ts @@ -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; } diff --git a/src/hooks/useThemeColors.ts b/src/hooks/useThemeColors.ts index 470e398..508f4e2 100644 --- a/src/hooks/useThemeColors.ts +++ b/src/hooks/useThemeColors.ts @@ -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 diff --git a/src/locales/en.json b/src/locales/en.json index e284182..1fcd7da 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/locales/fa.json b/src/locales/fa.json index a041bbc..f064245 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -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": "برای شروع تعرفه‌ای انتخاب کنید", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4131984..2078234 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -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": "Выберите тариф, чтобы начать пользоваться сервисом", diff --git a/src/locales/zh.json b/src/locales/zh.json index 59fb2cc..093bac1 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -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": "选择套餐以开始使用", diff --git a/src/pages/Subscriptions.tsx b/src/pages/Subscriptions.tsx index be8f65f..5dad110 100644 --- a/src/pages/Subscriptions.tsx +++ b/src/pages/Subscriptions.tsx @@ -109,15 +109,15 @@ export default function Subscriptions() { return (
{/* Header */} -
-

+
+

{t('subscriptions.title', 'Мои подписки')}

{/* «+ Купить ещё» — только если уже есть платная активная подписка */} {!isLoading && hasActivePaid && (