fix(ui): finish design-audit follow-ups

- useFeatureFlags: кэш флагов в localStorage — нижняя навигация больше
  не прыгает на холодном старте (таб «Рефералы»/«Колесо» появлялся после
  загрузки terms/config)
- applyThemeColors: частичный/битый ответ /branding/colors добивается
  дефолтами вместо падения всего приложения в ErrorBoundary
- карточка «Связаться с поддержкой»: кнопка больше не дублирует заголовок
  («Написать», без переноса)
- «Мои подписки»: заголовок и «Купить ещё» в одну строку на 390px
- StatCard: подписи плиток переносятся на две строки вместо обрезки
  многоточием («Бонус новому польз…»)
This commit is contained in:
c0mrade
2026-06-11 16:00:19 +03:00
parent 7f68e2cd5e
commit 9456c7f54e
9 changed files with 70 additions and 20 deletions

View File

@@ -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;
}

View File

@@ -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