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

199 lines
7.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';
const MOBILE_BREAKPOINT = 767;
function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const mq = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`);
setIsMobile(mq.matches);
const listener = () => setIsMobile(mq.matches);
mq.addEventListener('change', listener);
return () => mq.removeEventListener('change', listener);
}, []);
return isMobile;
}
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 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 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>
<div
className="protected-layout-root"
style={{
display: 'flex',
flexDirection: 'column',
minHeight: '100vh',
height: '100vh',
}}
>
{!isFullWidthPage && <TopNavigationBar user={user} />}
<main
className="protected-main"
data-no-nav={isLiveKit ? true : undefined}
data-full-width={isFullWidthPage ? true : undefined}
style={{
flex: 1,
minHeight: 0,
overflow: 'auto',
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>
</SelectedChildProvider>
</NavBadgesProvider>
);
}