Implement logo preloading on page load and enhance branding image display with fade-in effect

This commit is contained in:
PEDZEO
2026-01-19 23:36:05 +03:00
parent 19842d39e6
commit 0039a8a9b3
4 changed files with 82 additions and 57 deletions

View File

@@ -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<void> => {
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<BrandingInfo> => {

View File

@@ -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) {
<div className="flex justify-between items-center h-16 lg:h-20">
{/* Logo */}
<Link to="/" className={`flex items-center gap-2.5 flex-shrink-0 ${!appName ? 'lg:mr-4' : ''}`}>
<div className="w-10 h-10 sm:w-12 sm:h-12 lg:w-14 lg:h-14 rounded-xl bg-gradient-to-br from-accent-400 to-accent-600 flex items-center justify-center overflow-hidden shadow-lg shadow-accent-500/20 flex-shrink-0">
{hasCustomLogo && logoUrl ? (
<img src={logoUrl} alt={appName || 'Logo'} className="w-full h-full object-contain" />
) : (
<span className="text-white font-bold text-lg sm:text-xl lg:text-2xl">{logoLetter}</span>
<div className="w-10 h-10 sm:w-12 sm:h-12 lg:w-14 lg:h-14 rounded-xl bg-gradient-to-br from-accent-400 to-accent-600 flex items-center justify-center overflow-hidden shadow-lg shadow-accent-500/20 flex-shrink-0 relative">
{/* Always show letter as fallback */}
<span className={`text-white font-bold text-lg sm:text-xl lg:text-2xl absolute transition-opacity duration-200 ${hasCustomLogo && logoLoaded ? 'opacity-0' : 'opacity-100'}`}>
{logoLetter}
</span>
{/* Logo image with smooth fade-in */}
{hasCustomLogo && logoUrl && (
<img
src={logoUrl}
alt={appName || 'Logo'}
className={`w-full h-full object-contain absolute transition-opacity duration-200 ${logoLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setLogoLoaded(true)}
/>
)}
</div>
{appName && (

View File

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

View File

@@ -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<BrandingInfo>({
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() {
<div className="relative max-w-md w-full space-y-8">
{/* Logo */}
<div className="text-center">
<div className="mx-auto w-16 h-16 rounded-2xl bg-gradient-to-br from-accent-400 to-accent-600 flex items-center justify-center mb-6 shadow-lg shadow-accent-500/30 overflow-hidden">
{branding?.has_custom_logo && logoUrl ? (
<img src={logoUrl} alt={appName || 'Logo'} className="w-full h-full object-cover" />
) : (
<span className="text-white font-bold text-2xl">{appLogo}</span>
<div className="mx-auto w-16 h-16 rounded-2xl bg-gradient-to-br from-accent-400 to-accent-600 flex items-center justify-center mb-6 shadow-lg shadow-accent-500/30 overflow-hidden relative">
{/* Always show letter as fallback */}
<span className={`text-white font-bold text-2xl absolute transition-opacity duration-200 ${branding?.has_custom_logo && logoLoaded ? 'opacity-0' : 'opacity-100'}`}>
{appLogo}
</span>
{/* Logo image with smooth fade-in */}
{branding?.has_custom_logo && logoUrl && (
<img
src={logoUrl}
alt={appName || 'Logo'}
className={`w-full h-full object-cover absolute transition-opacity duration-200 ${logoLoaded ? 'opacity-100' : 'opacity-0'}`}
onLoad={() => setLogoLoaded(true)}
/>
)}
</div>
{appName && (