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

183 lines
7.0 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 { 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 { 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) {
console.log('[ProtectedLayout] No user found, redirecting to login');
router.replace('/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 (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', background: 'var(--md-sys-color-background)' }}>
<div style={{ textAlign: 'center', color: 'var(--md-sys-color-on-surface)' }}>
<LoadingSpinner size="large" />
<p style={{ marginTop: '16px', fontSize: '14px', opacity: 0.8 }}>Проверка авторизации...</p>
</div>
</div>
);
}
// Ментор не на /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">
{!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>
</SelectedChildProvider>
</NavBadgesProvider>
);
}