294 lines
10 KiB
TypeScript
294 lines
10 KiB
TypeScript
'use client';
|
||
|
||
import { useEffect, useMemo, useState } from 'react';
|
||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||
import type { NavBadges } from '@/api/navBadges';
|
||
import { ChildSelectorCompact } from '@/components/navigation/ChildSelector';
|
||
|
||
interface NavigationItem {
|
||
label: string;
|
||
path: string;
|
||
icon: string;
|
||
isProfile?: boolean;
|
||
}
|
||
|
||
interface User {
|
||
id?: number;
|
||
first_name?: string;
|
||
last_name?: string;
|
||
email?: string;
|
||
avatar_url?: string | null;
|
||
avatar?: string | null;
|
||
}
|
||
|
||
interface BottomNavigationBarProps {
|
||
userRole?: string;
|
||
user?: User | null;
|
||
navBadges?: NavBadges | null;
|
||
/** Выдвижная панель справа (3 колонки). При клике по пункту вызывается onClose. */
|
||
slideout?: boolean;
|
||
onClose?: () => void;
|
||
}
|
||
|
||
function getAvatarUrl(user: User | null | undefined): string | null {
|
||
if (!user) return null;
|
||
const url = user.avatar_url || user.avatar;
|
||
if (!url) return null;
|
||
if (url.startsWith('http')) return url;
|
||
const base = typeof window !== 'undefined' ? `${window.location.protocol}//${window.location.hostname}:8123` : '';
|
||
return url.startsWith('/') ? `${base}${url}` : `${base}/${url}`;
|
||
}
|
||
|
||
function getBadgeCount(item: NavigationItem, navBadges: NavBadges | null | undefined): number {
|
||
if (!navBadges) return 0;
|
||
switch (item.path) {
|
||
case '/schedule':
|
||
return navBadges.lessons_today;
|
||
case '/chat':
|
||
return navBadges.chat_unread;
|
||
case '/homework':
|
||
return navBadges.homework_pending;
|
||
case '/feedback':
|
||
return navBadges.feedback_pending;
|
||
case '/students':
|
||
return navBadges.mentorship_requests_pending ?? 0;
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
export function BottomNavigationBar({ userRole, user, navBadges, slideout, onClose }: BottomNavigationBarProps) {
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const searchParams = useSearchParams();
|
||
const tabParam = searchParams?.get('tab');
|
||
const [activeIndex, setActiveIndex] = useState(0);
|
||
const [expanded, setExpanded] = useState(false);
|
||
const avatarUrl = getAvatarUrl(user);
|
||
|
||
// Определяем навигационные элементы в зависимости от роли
|
||
const navigationItems = useMemo<NavigationItem[]>(() => {
|
||
const baseItems: NavigationItem[] = [
|
||
{ label: 'Главная', path: '/dashboard', icon: 'home' },
|
||
{ label: 'Расписание', path: '/schedule', icon: 'calendar_month' },
|
||
{ label: 'Чат', path: '/chat', icon: 'chat' },
|
||
];
|
||
|
||
let roleItems: NavigationItem[] = [];
|
||
if (userRole === 'mentor') {
|
||
roleItems = [
|
||
{ label: 'Студенты', path: '/students', icon: 'group' },
|
||
{ label: 'Материалы', path: '/materials', icon: 'folder' },
|
||
{ label: 'Домашние задания', path: '/homework', icon: 'assignment' },
|
||
{ label: 'Обратная связь', path: '/feedback', icon: 'rate_review' },
|
||
];
|
||
} else if (userRole === 'client') {
|
||
roleItems = [
|
||
{ label: 'Материалы', path: '/materials', icon: 'folder' },
|
||
{ label: 'Домашние задания', path: '/homework', icon: 'assignment' },
|
||
{ label: 'Прогресс', path: '/my-progress', icon: 'trending_up' },
|
||
{ label: 'Мои менторы', path: '/request-mentor', icon: 'person_add' },
|
||
];
|
||
} else if (userRole === 'parent') {
|
||
// Родитель: те же страницы, что и студент, кроме материалов
|
||
roleItems = [
|
||
{ label: 'Домашние задания', path: '/homework', icon: 'assignment' },
|
||
{ label: 'Прогресс', path: '/my-progress', icon: 'trending_up' },
|
||
];
|
||
}
|
||
|
||
const common: NavigationItem[] = [
|
||
...baseItems,
|
||
...roleItems,
|
||
{ label: 'Профиль', path: '/profile', icon: 'person', isProfile: true },
|
||
];
|
||
// Аналитика, Тарифы и Рефералы только для ментора
|
||
if (userRole === 'mentor') {
|
||
common.push(
|
||
{ label: 'Аналитика', path: '/analytics', icon: 'analytics' },
|
||
{ label: 'Тарифы', path: '/payment', icon: 'credit_card' },
|
||
{ label: 'Рефералы', path: '/referrals', icon: 'group_add' }
|
||
);
|
||
}
|
||
return common;
|
||
}, [userRole]);
|
||
|
||
const firstRowItems = navigationItems.slice(0, 5);
|
||
const restItems = navigationItems.slice(5);
|
||
const hasMore = restItems.length > 0;
|
||
|
||
// Подсветка активного таба по текущему URL
|
||
useEffect(() => {
|
||
const idx = navigationItems.findIndex((item) => {
|
||
if (item.path === '/payment') return pathname === '/payment';
|
||
if (item.path === '/analytics') return pathname === '/analytics';
|
||
if (item.path === '/referrals') return pathname === '/referrals';
|
||
if (item.path === '/feedback') return pathname === '/feedback';
|
||
if (item.path === '/homework') return pathname === '/homework';
|
||
if (item.path === '/profile') return pathname === '/profile' && !tabParam;
|
||
if (item.path === '/request-mentor') return pathname === '/request-mentor';
|
||
return pathname?.startsWith(item.path);
|
||
});
|
||
if (idx !== -1) setActiveIndex(idx);
|
||
}, [pathname, navigationItems, tabParam]);
|
||
|
||
const handleTabClick = (index: number) => {
|
||
const item = navigationItems[index];
|
||
if (!item) return;
|
||
setActiveIndex(index);
|
||
setExpanded(false);
|
||
router.push(item.path);
|
||
onClose?.();
|
||
};
|
||
|
||
if (!navigationItems.length) return null;
|
||
|
||
const renderButton = (item: NavigationItem, index: number) => {
|
||
const isActive = index === activeIndex;
|
||
const showAvatar = item.isProfile && (avatarUrl || user);
|
||
const badgeCount = getBadgeCount(item, navBadges);
|
||
return (
|
||
<button
|
||
key={item.path}
|
||
type="button"
|
||
onClick={() => handleTabClick(index)}
|
||
className={
|
||
'ios26-bottom-nav-button' +
|
||
(isActive ? ' ios26-bottom-nav-button--active' : '')
|
||
}
|
||
>
|
||
<span style={{ position: 'relative', display: 'inline-flex' }}>
|
||
{showAvatar ? (
|
||
<span
|
||
className="ios26-bottom-nav-icon"
|
||
style={{
|
||
width: 24,
|
||
height: 24,
|
||
borderRadius: '50%',
|
||
overflow: 'hidden',
|
||
flexShrink: 0,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
background: 'var(--md-sys-color-primary-container)',
|
||
color: 'var(--md-sys-color-primary)',
|
||
fontSize: 12,
|
||
fontWeight: 600,
|
||
}}
|
||
>
|
||
{avatarUrl ? (
|
||
<img
|
||
src={avatarUrl}
|
||
alt=""
|
||
style={{
|
||
width: '100%',
|
||
height: '100%',
|
||
objectFit: 'cover',
|
||
}}
|
||
/>
|
||
) : (
|
||
user && (user.first_name?.charAt(0) || user.email?.charAt(0) || 'У')
|
||
)}
|
||
</span>
|
||
) : (
|
||
<span className="material-symbols-outlined ios26-bottom-nav-icon">
|
||
{item.icon}
|
||
</span>
|
||
)}
|
||
{badgeCount > 0 && (
|
||
<span
|
||
className="ios26-bottom-nav-badge"
|
||
style={{
|
||
position: 'absolute',
|
||
top: -8,
|
||
right: -16,
|
||
minWidth: 18,
|
||
height: 18,
|
||
borderRadius: 9,
|
||
background: 'var(--md-sys-color-error, #b3261e)',
|
||
color: '#fff',
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
padding: '0 4px',
|
||
boxSizing: 'border-box',
|
||
}}
|
||
>
|
||
{badgeCount > 99 ? '99+' : badgeCount}
|
||
</span>
|
||
)}
|
||
</span>
|
||
<span className="ios26-bottom-nav-label">
|
||
{item.label}
|
||
</span>
|
||
</button>
|
||
);
|
||
};
|
||
|
||
if (slideout) {
|
||
return (
|
||
<div className="ios26-bottom-nav-slideout">
|
||
<div className="ios26-bottom-nav ios26-bottom-nav-slideout-inner">
|
||
{userRole === 'parent' && (
|
||
<div style={{ gridColumn: '1 / -1', marginBottom: 8 }}>
|
||
<ChildSelectorCompact />
|
||
</div>
|
||
)}
|
||
{navigationItems.map((item, i) => renderButton(item, i))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div
|
||
className={
|
||
'ios26-bottom-nav-container' +
|
||
(expanded ? ' ios26-bottom-nav-container--expanded' : '')
|
||
}
|
||
>
|
||
{hasMore && (
|
||
<button
|
||
type="button"
|
||
className="ios26-bottom-nav-expand-trigger"
|
||
onClick={() => setExpanded((e) => !e)}
|
||
aria-label={expanded ? 'Свернуть' : 'Развернуть'}
|
||
>
|
||
<span
|
||
className="material-symbols-outlined ios26-bottom-nav-arrow"
|
||
style={{
|
||
transform: expanded ? 'rotate(180deg)' : 'none',
|
||
}}
|
||
>
|
||
keyboard_arrow_up
|
||
</span>
|
||
</button>
|
||
)}
|
||
<div className="ios26-bottom-nav">
|
||
<div
|
||
className={
|
||
'ios26-bottom-nav-first-row' +
|
||
(userRole === 'parent' ? ' ios26-bottom-nav-first-row--with-selector' : '')
|
||
}
|
||
>
|
||
{userRole === 'parent' && <ChildSelectorCompact />}
|
||
{userRole === 'parent' ? (
|
||
<div className="ios26-bottom-nav-first-row-buttons">
|
||
{firstRowItems.map((item, i) => renderButton(item, i))}
|
||
</div>
|
||
) : (
|
||
firstRowItems.map((item, i) => renderButton(item, i))
|
||
)}
|
||
</div>
|
||
<div
|
||
className={'ios26-bottom-nav-rest' + (expanded ? ' ios26-bottom-nav-rest--expanded' : '')}
|
||
>
|
||
{restItems.map((item, i) => renderButton(item, 5 + i))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|