mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
fix(ui): не перезапускать анимированный фон при смене страницы
This commit is contained in:
@@ -31,6 +31,7 @@ import {
|
|||||||
ServiceUnavailableScreen,
|
ServiceUnavailableScreen,
|
||||||
} from './components/blocking';
|
} from './components/blocking';
|
||||||
import { ErrorBoundary } from './components/ErrorBoundary';
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
||||||
|
import { BackgroundHost } from './components/backgrounds/BackgroundHost';
|
||||||
import { PermissionRoute } from '@/components/auth/PermissionRoute';
|
import { PermissionRoute } from '@/components/auth/PermissionRoute';
|
||||||
import { saveReturnUrl } from './utils/token';
|
import { saveReturnUrl } from './utils/token';
|
||||||
import { useAnalyticsCounters } from './hooks/useAnalyticsCounters';
|
import { useAnalyticsCounters } from './hooks/useAnalyticsCounters';
|
||||||
@@ -259,6 +260,8 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* Живёт над <Routes>: анимация фона не перезапускается при навигации */}
|
||||||
|
<BackgroundHost />
|
||||||
<BlockingOverlay />
|
<BlockingOverlay />
|
||||||
<Routes>
|
<Routes>
|
||||||
{/* Public routes */}
|
{/* Public routes */}
|
||||||
|
|||||||
45
src/components/backgrounds/BackgroundHost.tsx
Normal file
45
src/components/backgrounds/BackgroundHost.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { BackgroundRenderer } from './BackgroundRenderer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Держит анимированный фон смонтированным ПОВЕРХ смены роутов.
|
||||||
|
*
|
||||||
|
* Каждый роут оборачивается в собственный <Layout>, поэтому AppShell
|
||||||
|
* перемонтируется при каждой навигации — если рендерить BackgroundRenderer
|
||||||
|
* прямо из AppShell, анимация фона стартует заново на каждой странице.
|
||||||
|
*
|
||||||
|
* Вместо этого AppShell лишь регистрирует своё присутствие через
|
||||||
|
* useBackgroundConsumer(), а BackgroundHost живёт в App (над <Routes>, не
|
||||||
|
* перемонтируется) и держит один экземпляр BackgroundRenderer, пока есть хоть
|
||||||
|
* один потребитель. Страницы без AppShell (логин, QuickPurchase со своим
|
||||||
|
* экземпляром) фон, как и раньше, не получают.
|
||||||
|
*/
|
||||||
|
const useBackgroundConsumers = create<{ count: number }>(() => ({ count: 0 }));
|
||||||
|
|
||||||
|
/** Вызывается из AppShell: «на этом роуте должен быть анимированный фон». */
|
||||||
|
export function useBackgroundConsumer() {
|
||||||
|
useEffect(() => {
|
||||||
|
useBackgroundConsumers.setState((s) => ({ count: s.count + 1 }));
|
||||||
|
return () => useBackgroundConsumers.setState((s) => ({ count: s.count - 1 }));
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BackgroundHost() {
|
||||||
|
const active = useBackgroundConsumers((s) => s.count > 0);
|
||||||
|
const [mounted, setMounted] = useState(active);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (active) {
|
||||||
|
setMounted(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Грейс-период на случай, когда между размонтированием старого AppShell и
|
||||||
|
// маунтом нового есть зазор (StrictMode, Suspense): без него фон моргал бы
|
||||||
|
// и на таких переходах.
|
||||||
|
const timer = setTimeout(() => setMounted(false), 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [active]);
|
||||||
|
|
||||||
|
return mounted ? <BackgroundRenderer /> : null;
|
||||||
|
}
|
||||||
@@ -39,7 +39,7 @@ import {
|
|||||||
|
|
||||||
import { MobileBottomNav } from './MobileBottomNav';
|
import { MobileBottomNav } from './MobileBottomNav';
|
||||||
import { AppHeader } from './AppHeader';
|
import { AppHeader } from './AppHeader';
|
||||||
import { BackgroundRenderer } from '@/components/backgrounds/BackgroundRenderer';
|
import { useBackgroundConsumer } from '@/components/backgrounds/BackgroundHost';
|
||||||
|
|
||||||
interface AppShellProps {
|
interface AppShellProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -60,6 +60,9 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
const { appName, logoLetter, hasCustomLogo, logoUrl } = useBranding();
|
const { appName, logoLetter, hasCustomLogo, logoUrl } = useBranding();
|
||||||
const { referralEnabled, wheelEnabled, hasContests, hasPolls, giftEnabled } = useFeatureFlags();
|
const { referralEnabled, wheelEnabled, hasContests, hasPolls, giftEnabled } = useFeatureFlags();
|
||||||
useScrollRestoration();
|
useScrollRestoration();
|
||||||
|
// Анимированный фон рендерит BackgroundHost в App (не перемонтируется при
|
||||||
|
// смене роута) — здесь только регистрируем, что на этом роуте он нужен.
|
||||||
|
useBackgroundConsumer();
|
||||||
|
|
||||||
// Theme toggle visibility
|
// Theme toggle visibility
|
||||||
const { data: enabledThemes } = useQuery({
|
const { data: enabledThemes } = useQuery({
|
||||||
@@ -180,9 +183,6 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-viewport">
|
<div className="min-h-viewport">
|
||||||
{/* Animated background renders via portal on document.body at z-index: -1 */}
|
|
||||||
<BackgroundRenderer />
|
|
||||||
|
|
||||||
{/* Global components */}
|
{/* Global components */}
|
||||||
<WebSocketNotifications />
|
<WebSocketNotifications />
|
||||||
<CampaignBonusNotifier />
|
<CampaignBonusNotifier />
|
||||||
|
|||||||
Reference in New Issue
Block a user