Skip to content

Commit dc048de

Browse files
committed
✨(frontend) build page teacher dashboard classrooms
1 parent d0f1071 commit dc048de

File tree

26 files changed

+378
-40
lines changed

26 files changed

+378
-40
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
2+
import { capitalize } from 'lodash-es';
3+
import { CourseMock } from 'api/mocks/joanie/courses';
4+
import { CourseRun } from 'types/Joanie';
5+
import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard';
6+
import SyllabusLink from 'widgets/Dashboard/components/SyllabusLink';
7+
import { getCourseUrl } from 'widgets/Dashboard/utils/course';
8+
import CourseRunLabel, {
9+
CourseRunLabelVariantEnum,
10+
} from 'widgets/Dashboard/components/CourseRunLabel';
11+
12+
const messages = defineMessages({
13+
syllabusLinkLabel: {
14+
defaultMessage: 'Access the course',
15+
description: 'Message displayed in classrooms page for the syllabus link label',
16+
id: 'components.TeacherCourseClassroomsDashboardLoader.syllabusLinkLabel',
17+
},
18+
classroomPeriod: {
19+
defaultMessage: 'Session from {from} to {to}',
20+
description: 'Message displayed in classrooms page for classroom period',
21+
id: 'components.TeacherCourseClassroomsDashboardLoader.classroomPeriod',
22+
},
23+
});
24+
25+
interface StudentsSectionProps {
26+
course: CourseMock;
27+
courseRun: CourseRun;
28+
}
29+
30+
const CourseRunSection = ({ course, courseRun }: StudentsSectionProps) => {
31+
const intl = useIntl();
32+
33+
return (
34+
<DashboardCard
35+
header={
36+
<div className="teacher-course-page__course-title__container-small dashboard-card__header__left">
37+
<h2 className="dashboard__title-h1 teacher-course-page__course-title dashboard-card__header__left">
38+
{capitalize(course.title)}
39+
</h2>
40+
<SyllabusLink href={getCourseUrl(course.code, intl)}>
41+
<FormattedMessage {...messages.syllabusLinkLabel} />
42+
</SyllabusLink>
43+
</div>
44+
}
45+
expandable={false}
46+
>
47+
{courseRun && (
48+
<span>
49+
<CourseRunLabel courseRun={courseRun} variant={CourseRunLabelVariantEnum.DATE} />
50+
</span>
51+
)}
52+
</DashboardCard>
53+
);
54+
};
55+
export default CourseRunSection;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 StudentsSection = () => {
18+
return (
19+
<DashboardCard
20+
header={
21+
<div className="teacher-course-page__course-title__container-small">
22+
<h2 className="dashboard__title-h1 teacher-course-page__course-title ">
23+
<FormattedMessage {...messages.studentsListTitle} />
24+
</h2>
25+
</div>
26+
}
27+
expandable={false}
28+
/>
29+
);
30+
};
31+
export default StudentsSection;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
<div className="teacher-course-page__course-title__container-small">
22+
<h2 className="dashboard__title-h1 teacher-course-page__course-title ">
23+
<FormattedMessage {...messages.teacherListTitle} />
24+
</h2>
25+
</div>
26+
}
27+
expandable={false}
28+
/>
29+
);
30+
};
31+
export default StudentsSection;

src/frontend/js/pages/TeacherCourseClassroomsDashboardLoader/_styles.scss

Whitespace-only changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { FormattedMessage, defineMessages } from 'react-intl';
2+
import { useParams } from 'react-router-dom';
3+
4+
import { useMemo } from 'react';
5+
import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
6+
import { TeacherCourseDashboardSidebar } from 'widgets/Dashboard/components/TeacherCourseDashboardSidebar';
7+
import { useCourse } from 'hooks/useCourses';
8+
import { Spinner } from 'components/Spinner';
9+
import { CourseRun } from 'types/Joanie';
10+
import CourseRunSection from './CourseRunSection';
11+
import StudentsSection from './StudentsSection';
12+
import TeachersSection from './TeachersSection';
13+
14+
const messages = defineMessages({
15+
loading: {
16+
defaultMessage: 'Loading classrooms ...',
17+
description: 'Message displayed while loading a classrooms',
18+
id: 'components.TeacherCourseClassroomsDashboardLoader.loading',
19+
},
20+
noCourseRun: {
21+
defaultMessage: "This course run does't exist",
22+
description: "Message displayed when requested classroom's course run doesn't exist",
23+
id: 'components.TeacherCourseClassroomsDashboardLoader.noCourseRun',
24+
},
25+
});
26+
27+
export const TeacherCourseClassroomsDashboardLoader = () => {
28+
const { courseCode, courseRunId } = useParams<{ courseCode: string; courseRunId: string }>();
29+
const {
30+
item: course,
31+
states: { fetching },
32+
} = useCourse(courseCode!);
33+
const courseRun: CourseRun | undefined = useMemo(
34+
() => course?.course_runs.find((courseCourseRun) => courseCourseRun.id === courseRunId),
35+
[course, courseRunId],
36+
);
37+
38+
return (
39+
<DashboardLayout sidebar={<TeacherCourseDashboardSidebar />}>
40+
{fetching && (
41+
<Spinner aria-labelledby="loading-teacher-course-classroom-data">
42+
<span id="loading-teacher-course-classroom-data">
43+
<FormattedMessage {...messages.loading} />
44+
</span>
45+
</Spinner>
46+
)}
47+
48+
{!fetching && courseRun === undefined && (
49+
<p>
50+
<FormattedMessage {...messages.noCourseRun} />
51+
</p>
52+
)}
53+
54+
{!fetching && courseRun !== undefined && (
55+
<div className="teacher-classroom-page">
56+
<DashboardLayout.Section>
57+
<CourseRunSection course={course} courseRun={courseRun} />
58+
</DashboardLayout.Section>
59+
60+
<DashboardLayout.Section>
61+
<StudentsSection />
62+
</DashboardLayout.Section>
63+
64+
<DashboardLayout.Section>
65+
<TeachersSection />
66+
</DashboardLayout.Section>
67+
</div>
68+
)}
69+
</DashboardLayout>
70+
);
71+
};

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>
Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
.teacher-course-page {
22
&__course-title {
33
color: r-color(secondary-900);
4-
margin: rem-calc(17px) 0 rem-calc(22px);
5-
font-size: rem-calc(18px);
64
font-weight: bold;
75
display: flex;
86
align-items: center;
7+
margin: 0;
8+
99
&__text {
1010
margin-left: rem-calc(8px);
1111
}
12+
13+
&__container,
14+
&__container-small {
15+
margin: rem-calc(17px) 0 rem-calc(22px);
16+
display: flex;
17+
align-items: center;
18+
}
19+
20+
&__container-small {
21+
margin: rem-calc(9px) 0;
22+
}
1223
}
1324
}

0 commit comments

Comments
 (0)