From 0039a8a9b300b9201dcaec2ffe3a6337e81a8e67 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Mon, 19 Jan 2026 23:36:05 +0300 Subject: [PATCH] Implement logo preloading on page load and enhance branding image display with fade-in effect --- src/api/branding.ts | 36 ++++++++++++++++ src/components/layout/Layout.tsx | 25 ++++++++--- src/main.tsx | 4 ++ src/pages/Login.tsx | 74 ++++++++++---------------------- 4 files changed, 82 insertions(+), 57 deletions(-) diff --git a/src/api/branding.ts b/src/api/branding.ts index 0067175..8d21793 100644 --- a/src/api/branding.ts +++ b/src/api/branding.ts @@ -12,6 +12,7 @@ export interface AnimationEnabled { } const BRANDING_CACHE_KEY = 'cabinet_branding' +const LOGO_PRELOADED_KEY = 'cabinet_logo_preloaded' // Get cached branding from localStorage export const getCachedBranding = (): BrandingInfo | null => { @@ -35,6 +36,41 @@ export const setCachedBranding = (branding: BrandingInfo) => { } } +// Preload logo image for instant display +export const preloadLogo = (branding: BrandingInfo): Promise => { + return new Promise((resolve) => { + if (!branding.has_custom_logo || !branding.logo_url) { + resolve() + return + } + + const logoUrl = `${import.meta.env.VITE_API_URL || ''}${branding.logo_url}` + + // Check if already preloaded in this session + const preloaded = sessionStorage.getItem(LOGO_PRELOADED_KEY) + if (preloaded === logoUrl) { + resolve() + return + } + + const img = new Image() + img.onload = () => { + sessionStorage.setItem(LOGO_PRELOADED_KEY, logoUrl) + resolve() + } + img.onerror = () => resolve() + img.src = logoUrl + }) +} + +// Initialize logo preload from cache on page load +export const initLogoPreload = () => { + const cached = getCachedBranding() + if (cached) { + preloadLogo(cached) + } +} + export const brandingApi = { // Get current branding (public, no auth required) getBranding: async (): Promise => { diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 01cfd90..3c242ed 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -9,7 +9,7 @@ import TicketNotificationBell from '../TicketNotificationBell' import AnimatedBackground from '../AnimatedBackground' import { contestsApi } from '../../api/contests' import { pollsApi } from '../../api/polls' -import { brandingApi, getCachedBranding, setCachedBranding } from '../../api/branding' +import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo } from '../../api/branding' import { wheelApi } from '../../api/wheel' import { themeColorsApi } from '../../api/themeColors' import { promoApi } from '../../api/promo' @@ -167,12 +167,17 @@ export default function Layout({ children }: LayoutProps) { } }, [mobileMenuOpen]) + // State to track if logo image has loaded + const [logoLoaded, setLogoLoaded] = useState(false) + // Fetch branding settings with localStorage cache for instant load const { data: branding } = useQuery({ queryKey: ['branding'], queryFn: async () => { const data = await brandingApi.getBranding() setCachedBranding(data) // Update cache + // Preload logo in background + preloadLogo(data) return data }, initialData: getCachedBranding() ?? undefined, // Use cached data immediately @@ -286,11 +291,19 @@ export default function Layout({ children }: LayoutProps) {
{/* Logo */} -
- {hasCustomLogo && logoUrl ? ( - {appName - ) : ( - {logoLetter} +
+ {/* Always show letter as fallback */} + + {logoLetter} + + {/* Logo image with smooth fade-in */} + {hasCustomLogo && logoUrl && ( + {appName setLogoLoaded(true)} + /> )}
{appName && ( diff --git a/src/main.tsx b/src/main.tsx index 6297c78..8deee7f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,9 +5,13 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import App from './App' import { ThemeColorsProvider } from './providers/ThemeColorsProvider' import { ToastProvider } from './components/Toast' +import { initLogoPreload } from './api/branding' import './i18n' import './styles/globals.css' +// Preload logo from cache immediately on page load +initLogoPreload() + const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index d6a8c85..7b98c03 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -3,47 +3,11 @@ import { useNavigate, useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' -import { brandingApi, type BrandingInfo } from '../api/branding' +import { brandingApi, getCachedBranding, setCachedBranding, preloadLogo, type BrandingInfo } from '../api/branding' import { getAndClearReturnUrl } from '../utils/token' import LanguageSwitcher from '../components/LanguageSwitcher' import TelegramLoginButton from '../components/TelegramLoginButton' -const BRANDING_CACHE_KEY = 'cabinet-branding-cache' -const BRANDING_CACHE_TTL = 1000 * 60 * 60 // 1 hour - -const getCachedBranding = (): BrandingInfo | undefined => { - if (typeof window === 'undefined') { - return undefined - } - try { - const raw = localStorage.getItem(BRANDING_CACHE_KEY) - if (!raw) return undefined - const parsed = JSON.parse(raw) as { data?: BrandingInfo; timestamp?: number } - if (!parsed?.data || !parsed.timestamp) return undefined - if (Date.now() - parsed.timestamp > BRANDING_CACHE_TTL) { - localStorage.removeItem(BRANDING_CACHE_KEY) - return undefined - } - return parsed.data - } catch { - return undefined - } -} - -const cacheBranding = (data: BrandingInfo) => { - if (typeof window === 'undefined') { - return - } - try { - localStorage.setItem( - BRANDING_CACHE_KEY, - JSON.stringify({ data, timestamp: Date.now() }) - ) - } catch { - // Ignore storage errors (e.g., private mode) - } -} - export default function Login() { const { t } = useTranslation() const navigate = useNavigate() @@ -55,6 +19,7 @@ export default function Login() { const [error, setError] = useState('') const [isLoading, setIsLoading] = useState(false) const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) + const [logoLoaded, setLogoLoaded] = useState(false) // Получаем URL для возврата после авторизации const getReturnUrl = useCallback(() => { @@ -72,22 +37,21 @@ export default function Login() { return '/' }, [location.state]) - // Fetch branding + // Fetch branding with unified cache const cachedBranding = useMemo(() => getCachedBranding(), []) const { data: branding } = useQuery({ queryKey: ['branding'], - queryFn: brandingApi.getBranding, + queryFn: async () => { + const data = await brandingApi.getBranding() + setCachedBranding(data) + preloadLogo(data) + return data + }, staleTime: 60000, - placeholderData: cachedBranding, + initialData: cachedBranding ?? undefined, }) - useEffect(() => { - if (branding) { - cacheBranding(branding) - } - }, [branding]) - const botUsername = import.meta.env.VITE_TELEGRAM_BOT_USERNAME || '' const appName = branding ? branding.name : (import.meta.env.VITE_APP_NAME || 'VPN') const appLogo = branding?.logo_letter || import.meta.env.VITE_APP_LOGO || 'V' @@ -168,11 +132,19 @@ export default function Login() {
{/* Logo */}
-
- {branding?.has_custom_logo && logoUrl ? ( - {appName - ) : ( - {appLogo} +
+ {/* Always show letter as fallback */} + + {appLogo} + + {/* Logo image with smooth fade-in */} + {branding?.has_custom_logo && logoUrl && ( + {appName setLogoLoaded(true)} + /> )}
{appName && (