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 && (