Skip to content

Commit e340b88

Browse files
committed
✨(frontend) build page teacher dashboard classrooms
1 parent bc329b4 commit e340b88

File tree

18 files changed

+251
-28
lines changed

18 files changed

+251
-28
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FormattedMessage, defineMessages } from 'react-intl';
2+
import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard';
3+
4+
const messages = defineMessages({
5+
loading: {
6+
defaultMessage: 'Loading classrooms ...',
7+
description: 'Message displayed while loading a classrooms',
8+
id: 'components.TeacherCourseClassroomsDashboardLoader.StudentsSection.loading',
9+
},
10+
teacherListTitle: {
11+
defaultMessage: 'Educational team',
12+
description: 'Message displayed in classrooms page as teacher section title',
13+
id: 'components.TeacherCourseClassroomsDashboardLoader.teacherListTitle',
14+
},
15+
});
16+
17+
const StudentsSection = () => {
18+
return (
19+
<DashboardCard
20+
header={
21+
<h2 className="teacher-course-page__course-title">
22+
<FormattedMessage {...messages.teacherListTitle} />
23+
</h2>
24+
}
25+
expandable={false}
26+
/>
27+
);
28+
};
29+
export default StudentsSection;
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { FormattedMessage, defineMessages } from 'react-intl';
2+
import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard';
3+
4+
const messages = defineMessages({
5+
loading: {
6+
defaultMessage: 'Loading classrooms ...',
7+
description: 'Message displayed while loading a classrooms',
8+
id: 'components.TeacherCourseClassroomsDashboardLoader.TeachersSection.loading',
9+
},
10+
studentsListTitle: {
11+
defaultMessage: 'Learners registered for training',
12+
description: 'Message displayed in classrooms page as students section title',
13+
id: 'components.TeacherCourseClassroomsDashboardLoader.TeachersSection.studentsListTitle',
14+
},
15+
});
16+
17+
const TeachersSection = () => {
18+
return (
19+
<DashboardCard
20+
header={
21+
<h2 className="teacher-course-page__course-title">
22+
<FormattedMessage {...messages.studentsListTitle} />
23+
</h2>
24+
}
25+
expandable={false}
26+
/>
27+
);
28+
};
29+
export default TeachersSection;
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
2+
import { useParams } from 'react-router-dom';
3+
4+
import { capitalize } from 'lodash-es';
5+
import { useMemo } from 'react';
6+
import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
7+
import { TeacherCourseDashboardSidebar } from 'widgets/Dashboard/components/TeacherCourseDashboardSidebar';
8+
import { useCourse } from 'hooks/useCourses';
9+
import { Spinner } from 'components/Spinner';
10+
import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard';
11+
import { Icon, IconTypeEnum } from 'components/Icon';
12+
import { CourseRun } from 'types/Joanie';
13+
import SyllabusLink from 'widgets/Dashboard/components/SyllabusLink';
14+
import { getCourseUrl } from 'widgets/Dashboard/utils/course';
15+
import StudentsSection from './StudentsSection';
16+
import TeachersSection from './TeachersSection';
17+
18+
const messages = defineMessages({
19+
loading: {
20+
defaultMessage: 'Loading classrooms ...',
21+
description: 'Message displayed while loading a classrooms',
22+
id: 'components.TeacherCourseClassroomsDashboardLoader.loading',
23+
},
24+
syllabusLinkLabel: {
25+
defaultMessage: 'Access the course',
26+
description: 'Message displayed in classrooms page for the syllabus link label',
27+
id: 'components.TeacherCourseClassroomsDashboardLoader.syllabusLinkLabel',
28+
},
29+
classroomPeriod: {
30+
defaultMessage: 'Session from {from} to {to}',
31+
description: 'Message displayed in classrooms page for classroom period',
32+
id: 'components.TeacherCourseClassroomsDashboardLoader.classroomPeriod',
33+
},
34+
});
35+
36+
export const TeacherCourseClassroomsDashboardLoader = () => {
37+
const intl = useIntl();
38+
const { courseCode, courseRunId } = useParams<{ courseCode: string; courseRunId: string }>();
39+
const {
40+
item: course,
41+
states: { fetching },
42+
} = useCourse(courseCode!);
43+
const courseRun: CourseRun | undefined = useMemo(
44+
() => course?.course_runs.find((courseCourseRun) => courseCourseRun.id === courseRunId),
45+
[course, courseRunId],
46+
);
47+
48+
return (
49+
<DashboardLayout sidebar={<TeacherCourseDashboardSidebar />}>
50+
{fetching ? (
51+
<Spinner aria-labelledby="loading-courses-data">
52+
<span id="loading-courses-data">
53+
<FormattedMessage {...messages.loading} />
54+
</span>
55+
</Spinner>
56+
) : (
57+
<>
58+
<DashboardLayout.Section>
59+
<DashboardCard
60+
header={
61+
<h2 className="teacher-course-page__course-title">
62+
<span className="teacher-course-page__course-title__text">
63+
{capitalize(course.title)}
64+
</span>
65+
66+
<SyllabusLink href={getCourseUrl(course.code, intl)}>
67+
<FormattedMessage {...messages.syllabusLinkLabel} />
68+
</SyllabusLink>
69+
</h2>
70+
}
71+
expandable={false}
72+
>
73+
{courseRun && (
74+
<>
75+
<Icon name={IconTypeEnum.CAMERA} />
76+
<FormattedMessage
77+
{...messages.classroomPeriod}
78+
values={{
79+
from: intl.formatDate(new Date(courseRun.start)),
80+
to: intl.formatDate(new Date(courseRun.end)),
81+
}}
82+
/>
83+
</>
84+
)}
85+
</DashboardCard>
86+
</DashboardLayout.Section>
87+
88+
<DashboardLayout.Section>
89+
<StudentsSection />
90+
</DashboardLayout.Section>
91+
92+
<DashboardLayout.Section>
93+
<TeachersSection />
94+
</DashboardLayout.Section>
95+
</>
96+
)}
97+
</DashboardLayout>
98+
);
99+
};

src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.spec.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ import { render, screen } from '@testing-library/react';
22
import { IntlProvider } from 'react-intl';
33
import { CunninghamProvider } from '@openfun/cunningham-react';
44
import { capitalize } from 'lodash-es';
5-
import { CourseRunFactory } from 'utils/test/factories/joanie';
5+
import { CourseFactory, CourseRunFactory } from 'utils/test/factories/joanie';
66
import CourseRunList from '.';
77

88
describe('pages/TeacherCourseDashboardLoader/CourseRunList', () => {
99
it('should render', () => {
10-
const courseRuns = CourseRunFactory().many(2);
10+
const course = CourseFactory({
11+
course_runs: CourseRunFactory().many(2),
12+
}).one();
1113
render(
1214
<IntlProvider locale="en">
1315
<CunninghamProvider>
14-
<CourseRunList courseRuns={courseRuns} />
16+
<CourseRunList courseRuns={course.course_runs} courseCode={course.code} />
1517
</CunninghamProvider>
1618
</IntlProvider>,
1719
);
18-
const [courseRunOne, courseRunTwo] = courseRuns;
20+
const [courseRunOne, courseRunTwo] = course.course_runs;
1921
expect(screen.getByTitle(capitalize(courseRunOne.title))).toBeInTheDocument();
2022
expect(screen.getByTitle(capitalize(courseRunTwo.title))).toBeInTheDocument();
2123
expect(screen.getAllByRole('button', { name: 'go to classroom' }).length).toEqual(2);

src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
import { useIntl } from 'react-intl';
22
import { DataList } from '@openfun/cunningham-react';
3+
import { useNavigate } from 'react-router-dom';
34
import { CourseRun } from 'types/Joanie';
45

6+
import { CourseMock } from 'api/mocks/joanie/courses';
57
import { buildCourseRunData } from './utils';
68

79
interface CourseRunListProps {
10+
courseCode: CourseMock['code'];
811
courseRuns: CourseRun[];
912
}
1013

11-
const CourseRunList = ({ courseRuns }: CourseRunListProps) => {
14+
const CourseRunList = ({ courseCode, courseRuns }: CourseRunListProps) => {
1215
const intl = useIntl();
16+
const navigate = useNavigate();
1317
const columns = ['title', 'period', 'status', 'action'].map((field: string) => ({ field }));
1418

1519
return (
1620
<div className="teacher-dashboard-course-run-list">
17-
<DataList columns={columns} rows={buildCourseRunData(intl, courseRuns)} />
21+
<DataList
22+
columns={columns}
23+
rows={buildCourseRunData(intl, navigate, courseCode, courseRuns)}
24+
/>
1825
</div>
1926
);
2027
};

src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.spec.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
import { IntlProvider, createIntl } from 'react-intl';
22
import { render, screen } from '@testing-library/react';
33
import { capitalize } from 'lodash-es';
4+
import { NavigateFunction, To } from 'react-router-dom';
45
import { CourseRunFactory } from 'utils/test/factories/joanie';
6+
import { CourseMock } from 'api/mocks/joanie/courses';
57
import { buildCourseRunData, messages } from './utils';
68

79
describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData', () => {
10+
const navigate: NavigateFunction = (to: To | number) => { }; // eslint-disable-line
11+
const courseCode: CourseMock['code'] = 'akeuj';
812
it('should return the right keys', () => {
913
const courseRunList = CourseRunFactory().many(1);
1014
const intl = createIntl({ locale: 'en' });
11-
const listData = buildCourseRunData(intl, courseRunList);
15+
const listData = buildCourseRunData(intl, navigate, courseCode, courseRunList);
1216
expect(listData.length).toBe(1);
1317

1418
const listItem = listData[0];
@@ -17,7 +21,7 @@ describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData',
1721
it('should contain a valid title', () => {
1822
const courseRun = CourseRunFactory().one();
1923
const intl = createIntl({ locale: 'en' });
20-
const listItem = buildCourseRunData(intl, [courseRun])[0];
24+
const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0];
2125

2226
render(listItem.title);
2327
expect(screen.getByText(capitalize(courseRun.title), { exact: false })).toBeInTheDocument();
@@ -26,7 +30,7 @@ describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData',
2630
it('should contain a valid period', () => {
2731
const courseRun = CourseRunFactory().one();
2832
const intl = createIntl({ locale: 'en' });
29-
const listItem = buildCourseRunData(intl, [courseRun])[0];
33+
const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0];
3034

3135
render(listItem.period);
3236
expect(
@@ -39,15 +43,15 @@ describe('pages/TeacherCourseDashboardLoader/CourseRunList/buildCourseRunData',
3943
it('should contain a valid status', () => {
4044
const courseRun = CourseRunFactory().one();
4145
const intl = createIntl({ locale: 'en' });
42-
const listItem = buildCourseRunData(intl, [courseRun])[0];
46+
const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0];
4347

4448
render(listItem.status);
4549
expect(screen.getByText(courseRun.state.text, { exact: false })).toBeInTheDocument();
4650
});
4751
it('should contain a valid action', () => {
4852
const courseRun = CourseRunFactory().one();
4953
const intl = createIntl({ locale: 'en' });
50-
const listItem = buildCourseRunData(intl, [courseRun])[0];
54+
const listItem = buildCourseRunData(intl, navigate, courseCode, [courseRun])[0];
5155

5256
render(<IntlProvider locale="en">{listItem.action}</IntlProvider>);
5357
expect(

src/frontend/js/pages/TeacherCourseDashboardLoader/CourseRunList/utils.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import { FormattedMessage, IntlShape, defineMessages } from 'react-intl';
22
import { capitalize } from 'lodash-es';
33
import { Button } from '@openfun/cunningham-react';
4+
import { NavigateFunction } from 'react-router-dom';
45
import { IconTypeEnum } from 'components/Icon';
56
import { CourseStateTextEnum, Priority } from 'types';
67
import { CourseRun } from 'types/Joanie';
8+
import { getDashboardRoutePath } from 'widgets/Dashboard/utils/dashboardRoutes';
9+
import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherRouteMessages';
10+
import { CourseMock } from 'api/mocks/joanie/courses';
711
import CourseRunListCell from './CourseRunListCell';
812

913
export const messages = defineMessages({
@@ -19,7 +23,13 @@ export const messages = defineMessages({
1923
},
2024
});
2125

22-
export const buildCourseRunData = (intl: IntlShape, courseRuns: CourseRun[]) => {
26+
export const buildCourseRunData = (
27+
intl: IntlShape,
28+
navigate: NavigateFunction,
29+
courseCode: CourseMock['code'],
30+
courseRuns: CourseRun[],
31+
) => {
32+
const getRoutePath = getDashboardRoutePath(intl);
2333
const CourseStateIconMap: Record<CourseStateTextEnum, IconTypeEnum> = {
2434
[CourseStateTextEnum.CLOSING_ON]: IconTypeEnum.MORE,
2535
[CourseStateTextEnum.STARTING_ON]: IconTypeEnum.CHECK_ROUNDED,
@@ -67,7 +77,18 @@ export const buildCourseRunData = (intl: IntlShape, courseRuns: CourseRun[]) =>
6777
),
6878
action: (
6979
<CourseRunListCell variant={CourseRunListCell.ALIGN_RIGHT}>
70-
<Button size="small" color="secondary">
80+
<Button
81+
size="small"
82+
color="secondary"
83+
onClick={() =>
84+
navigate(
85+
getRoutePath(TeacherDashboardPaths.COURSE_CLASSROOMS, {
86+
courseCode,
87+
courseRunId: courseRun.id,
88+
}),
89+
)
90+
}
91+
>
7192
<FormattedMessage {...messages.dataCourseRunLink} />
7293
</Button>
7394
</CourseRunListCell>

src/frontend/js/pages/TeacherCourseDashboardLoader/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export const TeacherCourseDashboardLoader = () => {
6868
expandable={false}
6969
fullWidth
7070
>
71-
<CourseRunList courseRuns={course.course_runs} />
71+
<CourseRunList courseCode={course.code} courseRuns={course.course_runs} />
7272
</DashboardCard>
7373
</div>
7474
)}

src/frontend/js/settings.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const REACT_QUERY_SETTINGS = {
1616
throttleTime: 500,
1717
},
1818
// Cache is garbage collected after this delay
19-
cacheTime: 24 * 60 * 60 * 1000, // 24h in ms
19+
cacheTime: 0, // 24 * 60 * 60 * 1000, // 24h in ms
2020
// Data are considered as stale after this delay
2121
staleTimes: {
2222
default: 0, // Stale immediately
@@ -35,4 +35,4 @@ export const PAYMENT_SETTINGS = {
3535
pollLimit: 30,
3636
};
3737

38-
export const MOCK_SERVICE_WORKER_ENABLED = false;
38+
export const MOCK_SERVICE_WORKER_ENABLED = true;

src/frontend/js/widgets/Dashboard/components/DashboardLayout/_styles.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ $sidebar-spacing-top: $dashboard-breadcrumb-height + $dashboard-filters-height +
3030
}
3131
}
3232

33+
&__section {
34+
margin-top: rem-calc(20px);
35+
&:first-child {
36+
margin-top: 0;
37+
}
38+
}
39+
3340
&__filters {
3441
min-height: $dashboard-filters-height;
3542
margin-top: $dashboard-breadcrumb-filters-spacing;

0 commit comments

Comments
 (0)