uchill/front_material/components/navigation/BottomNavigationBar.tsx

294 lines
10 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, 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>
);
}