Добавлены хлебные крошки для навигации в компонентах и страницах, включая CourseList, LessonList, UserPage и Attendance. Обновлены локализации для новых элементов навигации. Реализован контекст для управления состоянием хлебных крошек через BreadcrumbsProvider и useBreadcrumbs. Обновлен компонент AppHeader для отображения хлебных крошек в зависимости от текущей страницы.
This commit is contained in:
parent
4e27e3d1c6
commit
c92be3d7dd
@ -7,6 +7,12 @@
|
||||
"journal.pl.close": "Close",
|
||||
"journal.pl.title": "Attendance Journal",
|
||||
|
||||
"journal.pl.breadcrumbs.home": "Home",
|
||||
"journal.pl.breadcrumbs.course": "Course",
|
||||
"journal.pl.breadcrumbs.lesson": "Lesson",
|
||||
"journal.pl.breadcrumbs.user": "User",
|
||||
"journal.pl.breadcrumbs.attendance": "Attendance",
|
||||
|
||||
"journal.pl.common.add": "Add",
|
||||
"journal.pl.common.edit": "Edit",
|
||||
"journal.pl.common.delete": "Delete",
|
||||
|
@ -7,6 +7,12 @@
|
||||
"journal.pl.close": "Закрыть",
|
||||
"journal.pl.title": "Журнал посещаемости",
|
||||
|
||||
"journal.pl.breadcrumbs.home": "Главная",
|
||||
"journal.pl.breadcrumbs.course": "Курс",
|
||||
"journal.pl.breadcrumbs.lesson": "Лекция",
|
||||
"journal.pl.breadcrumbs.user": "Пользователь",
|
||||
"journal.pl.breadcrumbs.attendance": "Посещаемость",
|
||||
|
||||
"journal.pl.common.students": "студентов",
|
||||
"journal.pl.common.teachers": "преподавателей",
|
||||
"journal.pl.common.noData": "Нет данных",
|
||||
|
@ -1,22 +1,74 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Box,
|
||||
Flex,
|
||||
IconButton,
|
||||
useColorMode,
|
||||
Button,
|
||||
HStack
|
||||
HStack,
|
||||
VStack,
|
||||
Container,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
Heading,
|
||||
useBreakpointValue,
|
||||
Text,
|
||||
Tooltip,
|
||||
useMediaQuery,
|
||||
} from '@chakra-ui/react';
|
||||
import { MoonIcon, SunIcon } from '@chakra-ui/icons';
|
||||
import { MoonIcon, SunIcon, ChevronRightIcon, InfoIcon, ChevronDownIcon } from '@chakra-ui/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { getNavigationValue } from '@brojs/cli';
|
||||
|
||||
interface AppHeaderProps {
|
||||
serviceMenuContainerRef?: React.RefObject<HTMLDivElement>;
|
||||
breadcrumbs?: Array<{
|
||||
title: string;
|
||||
path?: string;
|
||||
isCurrentPage?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const AppHeader = ({ serviceMenuContainerRef }: AppHeaderProps) => {
|
||||
export const AppHeader = ({ serviceMenuContainerRef, breadcrumbs }: AppHeaderProps) => {
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { t, i18n } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
// Получаем путь к главной странице
|
||||
const mainPagePath = getNavigationValue('journal.main');
|
||||
|
||||
// Функция для формирования правильного пути с учетом mainPagePath
|
||||
const getFullPath = (path?: string): string => {
|
||||
if (!path) return '#';
|
||||
if (path === '/') return mainPagePath;
|
||||
|
||||
// Если путь уже начинается с mainPagePath, оставляем как есть
|
||||
if (path.startsWith(mainPagePath)) return path;
|
||||
|
||||
// Если путь начинается со слеша, добавляем mainPagePath
|
||||
if (path.startsWith('/')) return `${mainPagePath}${path}`;
|
||||
|
||||
// Иначе просто объединяем пути
|
||||
return `${mainPagePath}/${path}`;
|
||||
};
|
||||
|
||||
// Определяем размеры для разных устройств
|
||||
const fontSize = useBreakpointValue({ base: 'xs', sm: 'xs', md: 'sm' });
|
||||
|
||||
// Проверяем, на каком устройстве находимся
|
||||
const [isLargerThan768] = useMediaQuery("(min-width: 768px)");
|
||||
const [isLargerThan480] = useMediaQuery("(min-width: 480px)");
|
||||
|
||||
// Вертикальное отображение на мобильных устройствах
|
||||
const isMobile = !isLargerThan480;
|
||||
|
||||
// Горизонтальный сепаратор для десктопов
|
||||
const horizontalSeparator = useBreakpointValue({
|
||||
sm: <ChevronRightIcon color="gray.400" fontSize="xs" />,
|
||||
md: <ChevronRightIcon color="gray.400" />
|
||||
});
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = i18n.language === 'ru' ? 'en' : 'ru';
|
||||
@ -24,47 +76,200 @@ export const AppHeader = ({ serviceMenuContainerRef }: AppHeaderProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
<Box
|
||||
as="header"
|
||||
width="100%"
|
||||
py={4}
|
||||
px={8}
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
py={{ base: 2, md: 3 }}
|
||||
bg={colorMode === 'light' ? 'white' : 'gray.800'}
|
||||
boxShadow="sm"
|
||||
position="sticky"
|
||||
top={0}
|
||||
zIndex={10}
|
||||
bg={colorMode === 'light' ? 'white' : 'gray.800'}
|
||||
boxShadow="sm"
|
||||
>
|
||||
{serviceMenuContainerRef && <div id="dots" ref={serviceMenuContainerRef}></div>}
|
||||
<Box>
|
||||
|
||||
</Box>
|
||||
<HStack spacing={4}>
|
||||
<Button
|
||||
onClick={toggleLanguage}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={i18n.language === 'ru'
|
||||
? t('journal.pl.lang.switchToEn')
|
||||
: t('journal.pl.lang.switchToRu')
|
||||
}
|
||||
>
|
||||
{i18n.language === 'ru' ? 'EN' : 'RU'}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
aria-label={colorMode === 'light'
|
||||
? t('journal.pl.theme.switchDark')
|
||||
: t('journal.pl.theme.switchLight')
|
||||
}
|
||||
icon={colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size="md"
|
||||
{/* Рендеринг dots контейнера вне условной логики, всегда присутствует в DOM */}
|
||||
{serviceMenuContainerRef && (
|
||||
<Box
|
||||
id="dots"
|
||||
ref={serviceMenuContainerRef}
|
||||
position="absolute"
|
||||
top="3"
|
||||
left="0"
|
||||
height="0"
|
||||
width="0"
|
||||
overflow="visible"
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<Container maxW="container.xl" px={{ base: 2, sm: 4 }}>
|
||||
{isMobile ? (
|
||||
<>
|
||||
{/* Мобильная версия: верхняя строка с кнопками */}
|
||||
<Flex justifyContent="space-between" alignItems="center" h={{ base: "40px" }}>
|
||||
<Box>
|
||||
{/* Пустой контейнер для поддержания расположения */}
|
||||
</Box>
|
||||
|
||||
<HStack spacing={{ base: 1 }} flexShrink={0}>
|
||||
<Button
|
||||
onClick={toggleLanguage}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={i18n.language === 'ru'
|
||||
? t('journal.pl.lang.switchToEn')
|
||||
: t('journal.pl.lang.switchToRu')
|
||||
}
|
||||
fontSize={fontSize}
|
||||
px={{ base: 1 }}
|
||||
minW={{ base: "30px" }}
|
||||
h={{ base: "30px" }}
|
||||
>
|
||||
{i18n.language === 'ru' ? 'EN' : 'RU'}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
aria-label={colorMode === 'light'
|
||||
? t('journal.pl.theme.switchDark')
|
||||
: t('journal.pl.theme.switchLight')
|
||||
}
|
||||
icon={colorMode === 'light' ? <MoonIcon boxSize={{ base: "14px" }} /> : <SunIcon boxSize={{ base: "14px" }} />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size={{ base: "sm" }}
|
||||
minW={{ base: "30px" }}
|
||||
h={{ base: "30px" }}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
|
||||
{/* Вертикальные хлебные крошки */}
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<VStack
|
||||
align="flex-start"
|
||||
spacing={0}
|
||||
mt={1}
|
||||
>
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<Flex
|
||||
key={index}
|
||||
align="center"
|
||||
w="100%"
|
||||
pl={index > 0 ? 3 : 1}
|
||||
py={1}
|
||||
borderRadius="md"
|
||||
_hover={!crumb.isCurrentPage && crumb.path ? {
|
||||
bg: colorMode === 'light' ? 'gray.50' : 'gray.700',
|
||||
} : {}}
|
||||
>
|
||||
{index > 0 && (
|
||||
<ChevronDownIcon
|
||||
color="gray.400"
|
||||
fontSize="10px"
|
||||
mr={2}
|
||||
transform="translateY(-2px)"
|
||||
/>
|
||||
)}
|
||||
|
||||
{crumb.path && !crumb.isCurrentPage ? (
|
||||
<Link to={getFullPath(crumb.path)}>
|
||||
<Text
|
||||
fontWeight="medium"
|
||||
color={undefined}
|
||||
fontSize={fontSize}
|
||||
noOfLines={1}
|
||||
title={crumb.title}
|
||||
>
|
||||
{crumb.title}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<Text
|
||||
fontWeight={crumb.isCurrentPage ? "bold" : "medium"}
|
||||
color={crumb.isCurrentPage ? (colorMode === 'light' ? 'cyan.600' : 'cyan.300') : undefined}
|
||||
fontSize={fontSize}
|
||||
noOfLines={1}
|
||||
title={crumb.title}
|
||||
>
|
||||
{crumb.title}
|
||||
</Text>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
</VStack>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
/* Десктопная версия: всё в одну строку */
|
||||
<Flex justifyContent="space-between" alignItems="center" h="40px">
|
||||
<Flex align="center" overflow="hidden" flex={1} minW={0}>
|
||||
{/* Контейнер для разметки */}
|
||||
<Box w="24px" mr={{ sm: 3, md: 4 }} flexShrink={0} />
|
||||
|
||||
{breadcrumbs && breadcrumbs.length > 0 && (
|
||||
<Breadcrumb
|
||||
fontWeight="medium"
|
||||
fontSize={fontSize}
|
||||
separator={horizontalSeparator}
|
||||
spacing={{ sm: "1" }}
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
minW={0}
|
||||
>
|
||||
{breadcrumbs.map((crumb, index) => (
|
||||
<BreadcrumbItem key={index} isCurrentPage={crumb.isCurrentPage}>
|
||||
<BreadcrumbLink
|
||||
as={crumb.path ? Link : undefined}
|
||||
to={getFullPath(crumb.path)}
|
||||
href={!crumb.path ? "#" : undefined}
|
||||
maxWidth={{ sm: "120px", md: "200px", lg: "300px" }}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
display="inline-block"
|
||||
fontWeight={crumb.isCurrentPage ? "bold" : "medium"}
|
||||
color={crumb.isCurrentPage ? (colorMode === 'light' ? 'cyan.600' : 'cyan.300') : undefined}
|
||||
title={crumb.title}
|
||||
>
|
||||
{crumb.title}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<HStack spacing={{ sm: 2, md: 4 }} flexShrink={0} ml={{ sm: 2, md: 3 }}>
|
||||
<Button
|
||||
onClick={toggleLanguage}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label={i18n.language === 'ru'
|
||||
? t('journal.pl.lang.switchToEn')
|
||||
: t('journal.pl.lang.switchToRu')
|
||||
}
|
||||
fontSize={fontSize}
|
||||
px={{ sm: 2, md: 3 }}
|
||||
minW={{ sm: "40px" }}
|
||||
h={{ sm: "34px" }}
|
||||
>
|
||||
{i18n.language === 'ru' ? 'EN' : 'RU'}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
aria-label={colorMode === 'light'
|
||||
? t('journal.pl.theme.switchDark')
|
||||
: t('journal.pl.theme.switchLight')
|
||||
}
|
||||
icon={colorMode === 'light' ? <MoonIcon boxSize={{ sm: "14px", md: "16px" }} /> : <SunIcon boxSize={{ sm: "14px", md: "16px" }} />}
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
size={{ sm: "sm", md: "md" }}
|
||||
minW={{ sm: "34px" }}
|
||||
h={{ sm: "34px" }}
|
||||
/>
|
||||
</HStack>
|
||||
</Flex>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
};
|
45
src/components/breadcrumbs/breadcrumbs-context.tsx
Normal file
45
src/components/breadcrumbs/breadcrumbs-context.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
export type Breadcrumb = {
|
||||
title: string;
|
||||
path?: string;
|
||||
isCurrentPage?: boolean;
|
||||
};
|
||||
|
||||
type BreadcrumbsContextType = {
|
||||
breadcrumbs: Breadcrumb[];
|
||||
setBreadcrumbs: (breadcrumbs: Breadcrumb[]) => void;
|
||||
};
|
||||
|
||||
const BreadcrumbsContext = createContext<BreadcrumbsContextType | undefined>(undefined);
|
||||
|
||||
export const BreadcrumbsProvider: React.FC<{children: ReactNode}> = ({ children }) => {
|
||||
const [breadcrumbs, setBreadcrumbs] = useState<Breadcrumb[]>([]);
|
||||
|
||||
return (
|
||||
<BreadcrumbsContext.Provider value={{ breadcrumbs, setBreadcrumbs }}>
|
||||
{children}
|
||||
</BreadcrumbsContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useBreadcrumbs = () => {
|
||||
const context = useContext(BreadcrumbsContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useBreadcrumbs must be used within a BreadcrumbsProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useSetBreadcrumbs = (newBreadcrumbs: Breadcrumb[]) => {
|
||||
const { setBreadcrumbs } = useBreadcrumbs();
|
||||
|
||||
React.useEffect(() => {
|
||||
setBreadcrumbs(newBreadcrumbs);
|
||||
|
||||
return () => {
|
||||
// При размонтировании компонента очищаем хлебные крошки
|
||||
setBreadcrumbs([]);
|
||||
};
|
||||
}, [setBreadcrumbs, JSON.stringify(newBreadcrumbs)]);
|
||||
};
|
1
src/components/breadcrumbs/index.ts
Normal file
1
src/components/breadcrumbs/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './breadcrumbs-context';
|
@ -1,4 +1,5 @@
|
||||
export { PageLoader } from './page-loader/page-loader';
|
||||
export { XlSpinner } from './xl-spinner/xl-spinner';
|
||||
export { ErrorBoundary } from './error-boundary';
|
||||
export { AppHeader } from './app-header';
|
||||
export { AppHeader } from './app-header';
|
||||
export { BreadcrumbsProvider, useBreadcrumbs, useSetBreadcrumbs } from './breadcrumbs';
|
@ -12,7 +12,7 @@ import {
|
||||
UserPage,
|
||||
AttendancePage,
|
||||
} from './pages'
|
||||
import { ErrorBoundary, AppHeader } from './components'
|
||||
import { ErrorBoundary, AppHeader, BreadcrumbsProvider, useBreadcrumbs } from './components'
|
||||
import { keycloak } from './__data__/kc'
|
||||
|
||||
const MENU_SCRIPT_URL = 'https://admin.bro-js.ru/remote-assets/lib/serviceMenu/serviceMenu.js'
|
||||
@ -62,6 +62,12 @@ const Wrapper = ({ children }: { children: React.ReactElement }) => (
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
// Компонент, который соединяет хлебные крошки с AppHeader
|
||||
const HeaderWithBreadcrumbs = ({ serviceMenuContainerRef }: { serviceMenuContainerRef: React.RefObject<HTMLDivElement> }) => {
|
||||
const { breadcrumbs } = useBreadcrumbs();
|
||||
return <AppHeader serviceMenuContainerRef={serviceMenuContainerRef} breadcrumbs={breadcrumbs} />;
|
||||
};
|
||||
|
||||
interface DashboardProps {
|
||||
store: any; // Используем any, поскольку точный тип store не указан
|
||||
}
|
||||
@ -111,49 +117,51 @@ export const Dashboard = ({ store }: DashboardProps) => {
|
||||
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<AppHeader serviceMenuContainerRef={serviceMenuContainerRef} />
|
||||
<Routes>
|
||||
<Route
|
||||
path={getNavigationValue('journal.main')}
|
||||
element={
|
||||
<Wrapper>
|
||||
<CourseListPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}/lessons-list/:courseId`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<LessonListPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}/u/:lessonId/:accessId`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<UserPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}/lesson/:courseId/:lessonId`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<LessonDetailsPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}${getNavigationValue('link.journal.attendance')}`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<AttendancePage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<BreadcrumbsProvider>
|
||||
<HeaderWithBreadcrumbs serviceMenuContainerRef={serviceMenuContainerRef} />
|
||||
<Routes>
|
||||
<Route
|
||||
path={getNavigationValue('journal.main')}
|
||||
element={
|
||||
<Wrapper>
|
||||
<CourseListPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}/lessons-list/:courseId`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<LessonListPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}/u/:lessonId/:accessId`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<UserPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}/lesson/:courseId/:lessonId`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<LessonDetailsPage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={`${getNavigationValue('journal.main')}${getNavigationValue('link.journal.attendance')}`}
|
||||
element={
|
||||
<Wrapper>
|
||||
<AttendancePage />
|
||||
</Wrapper>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</BreadcrumbsProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { PageLoader } from '../../components/page-loader/page-loader'
|
||||
import { useSetBreadcrumbs } from '../../components'
|
||||
import { useAttendanceData, useAttendanceStats } from './hooks'
|
||||
import { AttendanceTable, StatsCard } from './components'
|
||||
|
||||
@ -21,6 +22,22 @@ export const Attendance = () => {
|
||||
const { t } = useTranslation()
|
||||
const data = useAttendanceData(courseId)
|
||||
const stats = useAttendanceStats(data)
|
||||
|
||||
// Устанавливаем хлебные крошки
|
||||
useSetBreadcrumbs([
|
||||
{
|
||||
title: t('journal.pl.breadcrumbs.home'),
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
title: data.courseInfo?.name || t('journal.pl.breadcrumbs.course'),
|
||||
path: `/lessons-list/${courseId}`
|
||||
},
|
||||
{
|
||||
title: t('journal.pl.breadcrumbs.attendance'),
|
||||
isCurrentPage: true
|
||||
}
|
||||
])
|
||||
|
||||
if (data.isLoading) {
|
||||
return <PageLoader />
|
||||
|
@ -15,6 +15,7 @@ import { useAppSelector } from '../../__data__/store'
|
||||
import { api } from '../../__data__/api/api'
|
||||
import { isTeacher } from '../../utils/user'
|
||||
import { PageLoader } from '../../components/page-loader/page-loader'
|
||||
import { useSetBreadcrumbs } from '../../components'
|
||||
import { useGroupedCourses } from './hooks'
|
||||
import { CreateCourseForm, YearGroup, CoursesOverview } from './components'
|
||||
import { Lesson } from '../../__data__/model'
|
||||
@ -29,6 +30,15 @@ export const CoursesList = () => {
|
||||
const { t } = useTranslation()
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
// Устанавливаем хлебные крошки для главной страницы
|
||||
useSetBreadcrumbs([
|
||||
{
|
||||
title: t('journal.pl.breadcrumbs.home'),
|
||||
path: '/',
|
||||
isCurrentPage: true
|
||||
}
|
||||
])
|
||||
|
||||
// Получаем значения фичей
|
||||
const features = getFeatures('journal')
|
||||
const coursesStatistics = features?.['courses.statistics']
|
||||
|
@ -6,9 +6,6 @@ import { getConfigValue, getNavigationValue } from '@brojs/cli'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import {
|
||||
Box,
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
Container,
|
||||
VStack,
|
||||
Heading,
|
||||
@ -21,11 +18,11 @@ import { api } from '../__data__/api/api'
|
||||
import { User } from '../__data__/model'
|
||||
import { UserCard } from '../components/user-card'
|
||||
import { formatDate } from '../utils/dayjs-config'
|
||||
import { useSetBreadcrumbs } from '../components'
|
||||
|
||||
import {
|
||||
QRCanvas,
|
||||
StudentList,
|
||||
BreadcrumbsWrapper,
|
||||
} from './style'
|
||||
import { useAppSelector } from '../__data__/store'
|
||||
import { isTeacher } from '../utils/user'
|
||||
@ -46,6 +43,26 @@ const LessonDetail = () => {
|
||||
const { t } = useTranslation()
|
||||
const { colorMode } = useColorMode()
|
||||
|
||||
// Получаем данные о курсе и уроке
|
||||
const { data: courseData } = api.useGetCourseByIdQuery(courseId)
|
||||
const { data: lessonData } = api.useLessonByIdQuery(lessonId)
|
||||
|
||||
// Устанавливаем хлебные крошки
|
||||
useSetBreadcrumbs([
|
||||
{
|
||||
title: t('journal.pl.breadcrumbs.home'),
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
title: courseData?.name || t('journal.pl.breadcrumbs.course'),
|
||||
path: `${getNavigationValue('journal.main')}/lessons-list/${courseId}`
|
||||
},
|
||||
{
|
||||
title: lessonData?.body?.name || t('journal.pl.breadcrumbs.lesson'),
|
||||
isCurrentPage: true
|
||||
}
|
||||
])
|
||||
|
||||
// Создаем ref для отслеживания ранее присутствовавших студентов
|
||||
const prevPresentStudentsRef = useRef(new Set<string>())
|
||||
|
||||
@ -201,28 +218,6 @@ const LessonDetail = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<BreadcrumbsWrapper>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink as={Link} to={getNavigationValue('journal.main')}>
|
||||
{t('journal.pl.common.journal')}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink
|
||||
as={Link}
|
||||
to={`${getNavigationValue('journal.main')}/lessons-list/${courseId}`}
|
||||
>
|
||||
{t('journal.pl.common.course')}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
<BreadcrumbLink href="#">{t('journal.pl.common.lesson')}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</BreadcrumbsWrapper>
|
||||
<Container maxW="2280px">
|
||||
<VStack align="left">
|
||||
<Heading as="h3" mt="4" mb="3">
|
||||
|
@ -3,9 +3,6 @@ import dayjs, { formatDate } from '../../utils/dayjs-config'
|
||||
import { generatePath, Link, useParams } from 'react-router-dom'
|
||||
import { getNavigationValue, getFeatures } from '@brojs/cli'
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
Container,
|
||||
Box,
|
||||
Button,
|
||||
@ -40,13 +37,12 @@ import { useAppSelector } from '../../__data__/store'
|
||||
import { api } from '../../__data__/api/api'
|
||||
import { isTeacher } from '../../utils/user'
|
||||
import { Lesson } from '../../__data__/model'
|
||||
import { XlSpinner } from '../../components/xl-spinner'
|
||||
import { XlSpinner, useSetBreadcrumbs } from '../../components'
|
||||
import { qrCode } from '../../assets'
|
||||
|
||||
import { LessonForm } from './components/lessons-form'
|
||||
import { Bar } from './components/bar'
|
||||
import { LessonItems } from './components/lesson-items'
|
||||
import { BreadcrumbsWrapper } from './style'
|
||||
import { CourseStatistics } from './components/statistics'
|
||||
|
||||
const features = getFeatures('journal')
|
||||
@ -59,6 +55,7 @@ const LessonList = () => {
|
||||
const { courseId } = useParams()
|
||||
const user = useAppSelector((s) => s.user)
|
||||
const { data, isLoading, error, isSuccess } = api.useLessonListQuery(courseId)
|
||||
const { data: courseData } = api.useGetCourseByIdQuery(courseId)
|
||||
const [generateLessonsMutation, {
|
||||
data: generateLessons,
|
||||
isLoading: isLoadingGenerateLessons,
|
||||
@ -79,6 +76,19 @@ const LessonList = () => {
|
||||
const [editLesson, setEditLesson] = useState<Lesson>(null)
|
||||
const [suggestedLessonToCreate, setSuggestedLessonToCreate] = useState(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Устанавливаем хлебные крошки для страницы списка уроков
|
||||
useSetBreadcrumbs([
|
||||
{
|
||||
title: t('journal.pl.breadcrumbs.home'),
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
title: courseData?.name || t('journal.pl.breadcrumbs.course'),
|
||||
isCurrentPage: true
|
||||
}
|
||||
])
|
||||
|
||||
const sorted = useMemo(
|
||||
() => [...(data?.body || [])]?.sort((a, b) => (a.date > b.date ? 1 : -1)),
|
||||
[data, data?.body],
|
||||
@ -330,19 +340,6 @@ const LessonList = () => {
|
||||
</AlertDialogContent>
|
||||
</AlertDialogOverlay>
|
||||
</AlertDialog>
|
||||
<BreadcrumbsWrapper>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink as={Link} to={getNavigationValue('journal.main')}>
|
||||
{t('journal.pl.common.journal')}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
|
||||
<BreadcrumbItem isCurrentPage>
|
||||
<BreadcrumbLink href="#">{t('journal.pl.common.course')}</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
</Breadcrumb>
|
||||
</BreadcrumbsWrapper>
|
||||
<Container maxW="container.xl" position="relative">
|
||||
{isTeacher(user) && (
|
||||
<Box mt="15" mb="15">
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
} from '@chakra-ui/react'
|
||||
import { UserCard } from '../components/user-card'
|
||||
import { StudentListView } from './style'
|
||||
import { useSetBreadcrumbs } from '../components'
|
||||
|
||||
const UserPage = () => {
|
||||
const { lessonId, accessId } = useParams()
|
||||
@ -33,6 +34,18 @@ const UserPage = () => {
|
||||
skipPollingIfUnfocused: true,
|
||||
})
|
||||
|
||||
// Устанавливаем хлебные крошки
|
||||
useSetBreadcrumbs([
|
||||
{
|
||||
title: t('journal.pl.breadcrumbs.home'),
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
title: t('journal.pl.breadcrumbs.user'),
|
||||
isCurrentPage: true
|
||||
}
|
||||
])
|
||||
|
||||
// Эффект для поэтапного появления карточек студентов
|
||||
useEffect(() => {
|
||||
if (ls.data?.body?.students?.length) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user