uchill/front_material/app/(protected)/layout.tsx

202 lines
8.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useEffect, useState, useCallback, Suspense } from 'react';
import { useIsMobile } from '@/hooks/useIsMobile';
import { useRouter, usePathname } from 'next/navigation';
import { BottomNavigationBar } from '@/components/navigation/BottomNavigationBar';
import { TopNavigationBar } from '@/components/navigation/TopNavigationBar';
import { NotificationBell } from '@/components/notifications/NotificationBell';
import { useAuth } from '@/contexts/AuthContext';
import { LoadingSpinner } from '@/components/common/LoadingSpinner';
import { NavBadgesProvider } from '@/contexts/NavBadgesContext';
import { SelectedChildProvider } from '@/contexts/SelectedChildContext';
import { OnboardingProvider } from '@/contexts/OnboardingContext';
import { getNavBadges } from '@/api/navBadges';
import { getActiveSubscription } from '@/api/subscriptions';
import { setReferrer, REFERRAL_STORAGE_KEY } from '@/api/referrals';
import type { NavBadges } from '@/api/navBadges';
export default function ProtectedLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const pathname = usePathname();
const { user, loading } = useAuth();
const isMobile = useIsMobile();
const [navBadges, setNavBadges] = useState<NavBadges | null>(null);
const [subscriptionChecked, setSubscriptionChecked] = useState(false);
const refreshNavBadges = useCallback(async () => {
try {
const next = await getNavBadges();
setNavBadges(next);
} catch {
setNavBadges(null);
}
}, []);
useEffect(() => {
if (!user) return;
refreshNavBadges();
}, [user, refreshNavBadges]);
// После входа: если в localStorage сохранён реферальный код (переход по ссылке /register?ref=...), привязываем реферера
useEffect(() => {
if (!user) return;
const code = typeof window !== 'undefined' ? localStorage.getItem(REFERRAL_STORAGE_KEY) : null;
if (!code || !code.trim()) return;
setReferrer(code.trim())
.then(() => {
localStorage.removeItem(REFERRAL_STORAGE_KEY);
})
.catch(() => {});
}, [user]);
// Для ментора: редирект на /payment, если нет активной подписки (кроме самой страницы /payment)
useEffect(() => {
if (!user || user.role !== 'mentor' || pathname === '/payment') {
if (user?.role === 'mentor' && pathname === '/payment') setSubscriptionChecked(true);
return;
}
let cancelled = false;
setSubscriptionChecked(false);
getActiveSubscription()
.then((sub) => {
if (cancelled) return;
setSubscriptionChecked(true);
if (!sub) router.replace('/payment');
})
.catch(() => {
if (!cancelled) setSubscriptionChecked(true);
});
return () => { cancelled = true; };
}, [user, pathname, router]);
useEffect(() => {
// Проверяем токен в localStorage напрямую, чтобы избежать race condition
const accessToken = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refresh_token') : null;
console.log('[ProtectedLayout] Auth state:', { user: !!user, loading, hasAccessToken: !!accessToken, hasRefreshToken: !!refreshToken, pathname });
if (!loading && !user) {
// Если есть refresh токен, пробуем обновить сессию вместо редиректа
if (refreshToken) {
console.log('[ProtectedLayout] User lost but refresh token exists, trying to restore session...');
import('@/api/auth').then(({ refreshToken: doRefresh }) => {
doRefresh(refreshToken)
.then(({ access }) => {
if (access) {
localStorage.setItem('access_token', access);
console.log('[ProtectedLayout] Session restored, reloading user...');
window.location.reload();
} else {
console.log('[ProtectedLayout] Refresh failed, redirecting to login');
router.replace('/login');
}
})
.catch(() => {
console.log('[ProtectedLayout] Refresh error, redirecting to login');
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
router.replace('/login');
});
});
} else {
console.log('[ProtectedLayout] No user and no refresh token, redirecting to login');
router.replace('/login');
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user, loading]);
// Стабильный loading layout - предотвращает дёрганье
const loadingLayout = (
<div className="protected-layout-root">
<main className="protected-main" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
<LoadingSpinner size="large" />
</main>
</div>
);
if (loading) {
return loadingLayout;
}
if (!user) {
return loadingLayout;
}
// Ментор не на /payment: ждём результат проверки подписки, чтобы не показывать контент перед редиректом
const isMentorCheckingSubscription =
user.role === 'mentor' && pathname !== '/payment' && !subscriptionChecked;
if (isMentorCheckingSubscription) {
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<LoadingSpinner size="large" />
</div>
);
}
// Не показываем навигацию на страницах авторизации
if (pathname?.startsWith('/login') || pathname?.startsWith('/register')) {
return <>{children}</>;
}
// Для dashboard, schedule, chat, students, materials не показываем header и используем полную ширину
const isDashboard = pathname === '/dashboard';
const isSchedule = pathname === '/schedule';
const isChat = pathname === '/chat';
const isStudents = pathname === '/students';
const isMaterials = pathname === '/materials';
const isProfile = pathname === '/profile';
const isPayment = pathname === '/payment';
const isAnalytics = pathname === '/analytics';
const isReferrals = pathname === '/referrals';
const isFeedback = pathname === '/feedback';
const isHomework = pathname === '/homework';
const isLiveKit = pathname?.startsWith('/livekit');
const isMyProgress = pathname === '/my-progress';
const isRequestMentor = pathname === '/request-mentor';
const isFullWidthPage = isDashboard || isSchedule || isChat || isStudents || isMaterials || isProfile || isPayment || isAnalytics || isReferrals || isFeedback || isHomework || isLiveKit || isMyProgress || isRequestMentor;
return (
<NavBadgesProvider refreshNavBadges={refreshNavBadges}>
<SelectedChildProvider>
<OnboardingProvider>
<div className="protected-layout-root">
{!isFullWidthPage && <TopNavigationBar user={user} />}
<main
className="protected-main"
data-no-nav={isLiveKit ? true : undefined}
data-full-width={isFullWidthPage ? true : undefined}
style={{
padding: isFullWidthPage ? '0' : '16px',
maxWidth: isFullWidthPage ? '100%' : '1200px',
margin: isFullWidthPage ? '0' : '0 auto',
}}
>
{children}
</main>
{!isLiveKit && (
<Suspense fallback={null}>
<BottomNavigationBar
userRole={user?.role}
user={user}
navBadges={navBadges}
notificationsSlot={isMobile && user ? <NotificationBell embedded /> : null}
/>
</Suspense>
)}
{!isLiveKit && user && !isMobile && (
<NotificationBell />
)}
</div>
</OnboardingProvider>
</SelectedChildProvider>
</NavBadgesProvider>
);
}