152 lines
5.5 KiB
TypeScript
152 lines
5.5 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useState, useCallback, Suspense } from 'react';
|
||
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 { getNavBadges } from '@/api/navBadges';
|
||
import { getActiveSubscription } from '@/api/subscriptions';
|
||
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 [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]);
|
||
|
||
// Для ментора: редирект на /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 token = typeof window !== 'undefined' ? localStorage.getItem('access_token') : null;
|
||
|
||
console.log('[ProtectedLayout] Auth state:', { user: !!user, loading, hasToken: !!token, pathname });
|
||
|
||
if (!loading && !user && !token) {
|
||
console.log('[ProtectedLayout] Redirecting to login');
|
||
router.push('/login');
|
||
}
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [user, loading]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
justifyContent: 'center',
|
||
alignItems: 'center',
|
||
height: '100vh',
|
||
}}
|
||
>
|
||
<LoadingSpinner size="large" />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!user) {
|
||
return null;
|
||
}
|
||
|
||
// Ментор не на /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>
|
||
{!isFullWidthPage && <TopNavigationBar user={user} />}
|
||
<main
|
||
data-no-nav={isLiveKit ? 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} />
|
||
</Suspense>
|
||
)}
|
||
{!isLiveKit && user && (
|
||
<NotificationBell />
|
||
)}
|
||
</SelectedChildProvider>
|
||
</NavBadgesProvider>
|
||
);
|
||
}
|