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

165 lines
6.1 KiB
TypeScript
Raw 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 { 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 { 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 [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 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>
);
}