diff --git a/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/CourseRunSection/index.tsx b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/CourseRunSection/index.tsx new file mode 100644 index 0000000000..314f12609a --- /dev/null +++ b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/CourseRunSection/index.tsx @@ -0,0 +1,55 @@ +import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; +import { capitalize } from 'lodash-es'; +import { CourseMock } from 'api/mocks/joanie/courses'; +import { CourseRun } from 'types/Joanie'; +import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard'; +import SyllabusLink from 'widgets/Dashboard/components/SyllabusLink'; +import { getCourseUrl } from 'widgets/Dashboard/utils/course'; +import CourseRunLabel, { + CourseRunLabelVariantEnum, +} from 'widgets/Dashboard/components/CourseRunLabel'; + +const messages = defineMessages({ + syllabusLinkLabel: { + defaultMessage: 'Access the course', + description: 'Message displayed in classrooms page for the syllabus link label', + id: 'components.TeacherCourseClassroomsDashboardLoader.syllabusLinkLabel', + }, + classroomPeriod: { + defaultMessage: 'Session from {from} to {to}', + description: 'Message displayed in classrooms page for classroom period', + id: 'components.TeacherCourseClassroomsDashboardLoader.classroomPeriod', + }, +}); + +interface StudentsSectionProps { + course: CourseMock; + courseRun: CourseRun; +} + +const CourseRunSection = ({ course, courseRun }: StudentsSectionProps) => { + const intl = useIntl(); + + return ( + +

+ {capitalize(course.title)} +

+ + + + + } + expandable={false} + > + {courseRun && ( + + + + )} +
+ ); +}; +export default CourseRunSection; diff --git a/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/StudentsSection/index.tsx b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/StudentsSection/index.tsx new file mode 100644 index 0000000000..3ae1ee1a28 --- /dev/null +++ b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/StudentsSection/index.tsx @@ -0,0 +1,31 @@ +import { FormattedMessage, defineMessages } from 'react-intl'; +import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard'; + +const messages = defineMessages({ + loading: { + defaultMessage: 'Loading classrooms ...', + description: 'Message displayed while loading a classrooms', + id: 'components.TeacherCourseClassroomsDashboardLoader.TeachersSection.loading', + }, + studentsListTitle: { + defaultMessage: 'Learners registered for training', + description: 'Message displayed in classrooms page as students section title', + id: 'components.TeacherCourseClassroomsDashboardLoader.TeachersSection.studentsListTitle', + }, +}); + +const StudentsSection = () => { + return ( + +

+ +

+ + } + expandable={false} + /> + ); +}; +export default StudentsSection; diff --git a/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/TeachersSection/index.tsx b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/TeachersSection/index.tsx new file mode 100644 index 0000000000..1697c4d3a3 --- /dev/null +++ b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/TeachersSection/index.tsx @@ -0,0 +1,31 @@ +import { FormattedMessage, defineMessages } from 'react-intl'; +import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard'; + +const messages = defineMessages({ + loading: { + defaultMessage: 'Loading classrooms ...', + description: 'Message displayed while loading a classrooms', + id: 'components.TeacherCourseClassroomsDashboardLoader.StudentsSection.loading', + }, + teacherListTitle: { + defaultMessage: 'Educational team', + description: 'Message displayed in classrooms page as teacher section title', + id: 'components.TeacherCourseClassroomsDashboardLoader.teacherListTitle', + }, +}); + +const StudentsSection = () => { + return ( + +

+ +

+ + } + expandable={false} + /> + ); +}; +export default StudentsSection; diff --git a/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/_styles.scss b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/_styles.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/index.tsx b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/index.tsx new file mode 100644 index 0000000000..a7afd98c12 --- /dev/null +++ b/src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/index.tsx @@ -0,0 +1,71 @@ +import { FormattedMessage, defineMessages } from 'react-intl'; +import { useParams } from 'react-router-dom'; + +import { useMemo } from 'react'; +import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout'; +import { TeacherCourseDashboardSidebar } from 'widgets/Dashboard/components/TeacherCourseDashboardSidebar'; +import { useCourse } from 'hooks/useCourses'; +import { Spinner } from 'components/Spinner'; +import { CourseRun } from 'types/Joanie'; +import CourseRunSection from './CourseRunSection'; +import StudentsSection from './StudentsSection'; +import TeachersSection from './TeachersSection'; + +const messages = defineMessages({ + loading: { + defaultMessage: 'Loading classrooms ...', + description: 'Message displayed while loading a classrooms', + id: 'components.TeacherCourseClassroomsDashboardLoader.loading', + }, + noCourseRun: { + defaultMessage: "This course run does't exist", + description: "Message displayed when requested classroom's course run doesn't exist", + id: 'components.TeacherCourseClassroomsDashboardLoader.noCourseRun', + }, +}); + +export const TeacherCourseClassroomsDashboardLoader = () => { + const { courseCode, courseRunId } = useParams<{ courseCode: string; courseRunId: string }>(); + const { + item: course, + states: { fetching }, + } = useCourse(courseCode!); + const courseRun: CourseRun | undefined = useMemo( + () => course?.course_runs.find((courseCourseRun) => courseCourseRun.id === courseRunId), + [course, courseRunId], + ); + + return ( + }> + {fetching && ( + + + + + + )} + + {!fetching && courseRun === undefined && ( +

+ +

+ )} + + {!fetching && courseRun !== undefined && ( +
+ + + + + + + + + + + +
+ )} +
+ ); +}; diff --git a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.spec.tsx b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.spec.tsx index 860681480a..3c3a729d1e 100644 --- a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.spec.tsx +++ b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.spec.tsx @@ -2,20 +2,22 @@ import { render, screen } from '@testing-library/react'; import { IntlProvider } from 'react-intl'; import { CunninghamProvider } from '@openfun/cunningham-react'; import { capitalize } from 'lodash-es'; -import { CourseRunFactory } from 'utils/test/factories/joanie'; +import { CourseFactory, CourseRunFactory } from 'utils/test/factories/joanie'; import CourseRunList from '.'; describe('pages/TeacherCourseDashboardLoader/CourseRunList', () => { it('should render', () => { - const courseRuns = CourseRunFactory().many(2); + const course = CourseFactory({ + course_runs: CourseRunFactory().many(2), + }).one(); render( - + , ); - const [courseRunOne, courseRunTwo] = courseRuns; + const [courseRunOne, courseRunTwo] = course.course_runs; expect(screen.getByTitle(capitalize(courseRunOne.title))).toBeInTheDocument(); expect(screen.getByTitle(capitalize(courseRunTwo.title))).toBeInTheDocument(); expect(screen.getAllByRole('button', { name: 'go to classroom' }).length).toEqual(2); diff --git a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.tsx b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.tsx index 30dea8326c..5b8f31e625 100644 --- a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.tsx +++ b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.tsx @@ -1,20 +1,27 @@ import { useIntl } from 'react-intl'; import { DataList } from '@openfun/cunningham-react'; +import { useNavigate } from 'react-router-dom'; import { CourseRun } from 'types/Joanie'; +import { CourseMock } from 'api/mocks/joanie/courses'; import { buildCourseRunData } from './utils'; interface CourseRunListProps { + courseCode: CourseMock['code']; courseRuns: CourseRun[]; } -const CourseRunList = ({ courseRuns }: CourseRunListProps) => { +const CourseRunList = ({ courseCode, courseRuns }: CourseRunListProps) => { const intl = useIntl(); + const navigate = useNavigate(); const columns = ['title', 'period', 'status', 'action'].map((field: string) => ({ field })); return (
- +
); }; diff --git a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.spec.tsx b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.spec.tsx index 11828637ad..99a81b7c02 100644 --- a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.spec.tsx +++ b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.spec.tsx @@ -1,14 +1,18 @@ import { IntlProvider, createIntl } from 'react-intl'; import { render, screen } from '@testing-library/react'; import { capitalize } from 'lodash-es'; +import { NavigateFunction, To } from 'react-router-dom'; import { CourseRunFactory } from 'utils/test/factories/joanie'; +import { CourseMock } from 'api/mocks/joanie/courses'; import { buildCourseRunData, messages } from './utils'; describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData', () => { + const navigate: NavigateFunction = (to: To | number) => { }; // eslint-disable-line + const courseCode: CourseMock['code'] = 'akeuj'; it('should return the right keys', () => { const courseRunList = CourseRunFactory().many(1); const intl = createIntl({ locale: 'en' }); - const listData = buildCourseRunData(intl, courseRunList); + const listData = buildCourseRunData(intl, navigate, courseCode, courseRunList); expect(listData.length).toBe(1); const listItem = listData[0]; @@ -17,7 +21,7 @@ describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData', it('should contain a valid title', () => { const courseRun = CourseRunFactory().one(); const intl = createIntl({ locale: 'en' }); - const listItem = buildCourseRunData(intl, [courseRun])[0]; + const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0]; render(listItem.title); expect(screen.getByText(capitalize(courseRun.title), { exact: false })).toBeInTheDocument(); @@ -26,7 +30,7 @@ describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData', it('should contain a valid period', () => { const courseRun = CourseRunFactory().one(); const intl = createIntl({ locale: 'en' }); - const listItem = buildCourseRunData(intl, [courseRun])[0]; + const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0]; render(listItem.period); expect( @@ -39,7 +43,7 @@ describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData', it('should contain a valid status', () => { const courseRun = CourseRunFactory().one(); const intl = createIntl({ locale: 'en' }); - const listItem = buildCourseRunData(intl, [courseRun])[0]; + const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0]; render(listItem.status); expect(screen.getByText(courseRun.state.text, { exact: false })).toBeInTheDocument(); @@ -47,7 +51,7 @@ describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData', it('should contain a valid action', () => { const courseRun = CourseRunFactory().one(); const intl = createIntl({ locale: 'en' }); - const listItem = buildCourseRunData(intl, [courseRun])[0]; + const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0]; render({listItem.action}); expect( diff --git a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.tsx b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.tsx index c8f6f74649..fd5632dd29 100644 --- a/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.tsx +++ b/src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.tsx @@ -1,9 +1,13 @@ import { FormattedMessage, IntlShape, defineMessages } from 'react-intl'; import { capitalize } from 'lodash-es'; import { Button } from '@openfun/cunningham-react'; +import { NavigateFunction } from 'react-router-dom'; import { IconTypeEnum } from 'components/Icon'; import { CourseStateTextEnum, Priority } from 'types'; import { CourseRun } from 'types/Joanie'; +import { getDashboardRoutePath } from 'widgets/Dashboard/utils/dashboardRoutes'; +import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherRouteMessages'; +import { CourseMock } from 'api/mocks/joanie/courses'; import CourseRunListCell from './CourseRunListCell'; export const messages = defineMessages({ @@ -19,7 +23,13 @@ export const messages = defineMessages({ }, }); -export const buildCourseRunData = (intl: IntlShape, courseRuns: CourseRun[]) => { +export const buildCourseRunData = ( + intl: IntlShape, + navigate: NavigateFunction, + courseCode: CourseMock['code'], + courseRuns: CourseRun[], +) => { + const getRoutePath = getDashboardRoutePath(intl); const CourseStateIconMap: Record = { [CourseStateTextEnum.CLOSING_ON]: IconTypeEnum.MORE, [CourseStateTextEnum.STARTING_ON]: IconTypeEnum.CHECK_ROUNDED, @@ -67,7 +77,18 @@ export const buildCourseRunData = (intl: IntlShape, courseRuns: CourseRun[]) => ), action: ( - diff --git a/src/frontend/js/pages/TeacherCourseDashboardLoader/_styles.scss b/src/frontend/js/pages/TeacherCourseDashboardLoader/_styles.scss index d47ea5085b..0817095c97 100644 --- a/src/frontend/js/pages/TeacherCourseDashboardLoader/_styles.scss +++ b/src/frontend/js/pages/TeacherCourseDashboardLoader/_styles.scss @@ -1,13 +1,24 @@ .teacher-course-page { &__course-title { color: r-color(secondary-900); - margin: rem-calc(17px) 0 rem-calc(22px); - font-size: rem-calc(18px); font-weight: bold; display: flex; align-items: center; + margin: 0; + &__text { margin-left: rem-calc(8px); } + + &__container, + &__container-small { + margin: rem-calc(17px) 0 rem-calc(22px); + display: flex; + align-items: center; + } + + &__container-small { + margin: rem-calc(9px) 0; + } } } diff --git a/src/frontend/js/pages/TeacherCourseDashboardLoader/index.tsx b/src/frontend/js/pages/TeacherCourseDashboardLoader/index.tsx index 3c9e06346c..53fb6e10a1 100644 --- a/src/frontend/js/pages/TeacherCourseDashboardLoader/index.tsx +++ b/src/frontend/js/pages/TeacherCourseDashboardLoader/index.tsx @@ -58,17 +58,19 @@ export const TeacherCourseDashboardLoader = () => { - - - {capitalize(course.title)} - - +
+

+ + + {capitalize(course.title)} + +

+
} expandable={false} fullWidth > - +
)} diff --git a/src/frontend/js/settings.ts b/src/frontend/js/settings.ts index ec56aeb528..19d168b3ec 100644 --- a/src/frontend/js/settings.ts +++ b/src/frontend/js/settings.ts @@ -16,7 +16,7 @@ export const REACT_QUERY_SETTINGS = { throttleTime: 500, }, // Cache is garbage collected after this delay - cacheTime: 24 * 60 * 60 * 1000, // 24h in ms + cacheTime: 0, // 24 * 60 * 60 * 1000, // 24h in ms // Data are considered as stale after this delay staleTimes: { default: 0, // Stale immediately @@ -35,4 +35,4 @@ export const PAYMENT_SETTINGS = { pollLimit: 30, }; -export const MOCK_SERVICE_WORKER_ENABLED = false; +export const MOCK_SERVICE_WORKER_ENABLED = true; diff --git a/src/frontend/js/widgets/Dashboard/components/CourseRunLabel/_styles.scss b/src/frontend/js/widgets/Dashboard/components/CourseRunLabel/_styles.scss new file mode 100644 index 0000000000..299c3aa417 --- /dev/null +++ b/src/frontend/js/widgets/Dashboard/components/CourseRunLabel/_styles.scss @@ -0,0 +1,6 @@ +.course-run-label { + font-size: rem-calc(16px); + display: flex; + align-items: center; + gap: rem-calc(15px); +} diff --git a/src/frontend/js/widgets/Dashboard/components/CourseRunLabel/index.tsx b/src/frontend/js/widgets/Dashboard/components/CourseRunLabel/index.tsx new file mode 100644 index 0000000000..02a776d559 --- /dev/null +++ b/src/frontend/js/widgets/Dashboard/components/CourseRunLabel/index.tsx @@ -0,0 +1,42 @@ +import { defineMessages, useIntl } from 'react-intl'; +import { Icon, IconTypeEnum } from 'components/Icon'; +import { CourseRun } from 'types/Joanie'; + +const messages = defineMessages({ + courseRunLabelDate: { + defaultMessage: 'Session from {from} to {to}', + description: 'Message displayed as course run label displayed as dates', + id: 'components.CourseRunLabel.courseRunLabelDate', + }, +}); + +export enum CourseRunLabelVariantEnum { + TITLE = 'title', + DATE = 'date', +} + +interface CourseRunLabelProps { + courseRun: CourseRun; + variant?: CourseRunLabelVariantEnum; +} +const CourseRunLabel = ({ + courseRun, + variant = CourseRunLabelVariantEnum.TITLE, +}: CourseRunLabelProps) => { + const intl = useIntl(); + const displayedLabel = { + [CourseRunLabelVariantEnum.TITLE]: courseRun.title, + [CourseRunLabelVariantEnum.DATE]: intl.formatMessage(messages.courseRunLabelDate, { + from: intl.formatDate(new Date(courseRun.start)), + to: intl.formatDate(new Date(courseRun.end)), + }), + }[variant]; + return ( +
+ + {displayedLabel} +
+ ); +}; + +export default CourseRunLabel; diff --git a/src/frontend/js/widgets/Dashboard/components/DashboardCard/_styles.scss b/src/frontend/js/widgets/Dashboard/components/DashboardCard/_styles.scss index fd19d9a1b4..17597f7e69 100644 --- a/src/frontend/js/widgets/Dashboard/components/DashboardCard/_styles.scss +++ b/src/frontend/js/widgets/Dashboard/components/DashboardCard/_styles.scss @@ -25,7 +25,12 @@ font-weight: 600; font-size: 16px; justify-content: space-between; - padding: 1rem 1rem 1rem 2rem; + padding: rem-calc(16px) rem-calc(16px) rem-calc(16px) rem-calc(32px); + + &__left { + flex: 1 1 auto; + display: flex; + } .icon { cursor: pointer; diff --git a/src/frontend/js/widgets/Dashboard/components/DashboardCard/index.tsx b/src/frontend/js/widgets/Dashboard/components/DashboardCard/index.tsx index 00f18358c5..98a92a3a12 100644 --- a/src/frontend/js/widgets/Dashboard/components/DashboardCard/index.tsx +++ b/src/frontend/js/widgets/Dashboard/components/DashboardCard/index.tsx @@ -60,7 +60,7 @@ export const DashboardCard = ({ return (
-
{header}
+
{header}
{expandable && (