Skip to content

Commit 9b6a0c8

Browse files
authored
enrollment dashboard mobile layout (#2187)
* basic adjustment to the padding / margins outside of the cards as well as borders * first pass on mobile layout * align header with figma * fix border * fix context menu position * fix tests * fix context menu display on desktop * refactor tests * restrict width of title link on mobile * refactor tests again
1 parent 6b068d5 commit 9b6a0c8

File tree

5 files changed

+255
-60
lines changed

5 files changed

+255
-60
lines changed

frontends/api/src/mitxonline/test-utils/factories/enrollment.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import type {
1010
const courseEnrollment: PartialFactory<CourseRunEnrollment> = (
1111
overrides = {},
1212
) => {
13-
const title = faker.word.words(3)
13+
const title =
14+
overrides.run?.title ?? overrides.run?.course?.title ?? faker.word.words(3)
1415
return mergeOverrides<CourseRunEnrollment>(
1516
{
1617
id: faker.number.int(),

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.test.tsx

+79-27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from "react"
2-
import { renderWithProviders, screen } from "@/test-utils"
2+
import { renderWithProviders, screen, within } from "@/test-utils"
33
import { DashboardCard } from "./DashboardCard"
44
import { dashboardCourse } from "./test-utils"
55
import { faker } from "@faker-js/faker/locale/en"
@@ -11,10 +11,12 @@ describe("EnrollmentCard", () => {
1111
const course = dashboardCourse()
1212
renderWithProviders(<DashboardCard dashboardResource={course} />)
1313

14-
const courseLink = screen.getByRole("link", {
14+
const courseLinks = screen.getAllByRole("link", {
1515
name: course.title,
1616
})
17-
expect(courseLink).toHaveAttribute("href", course.marketingUrl)
17+
for (const courseLink of courseLinks) {
18+
expect(courseLink).toHaveAttribute("href", course.marketingUrl)
19+
}
1820
})
1921

2022
test("Courseware button is disabled if course has not started", () => {
@@ -24,11 +26,13 @@ describe("EnrollmentCard", () => {
2426
},
2527
})
2628
renderWithProviders(<DashboardCard dashboardResource={course} />)
27-
const coursewareButton = screen.getByRole("button", {
29+
const coursewareButtons = screen.getAllByRole("button", {
2830
name: "Continue Course",
2931
hidden: true,
3032
})
31-
expect(coursewareButton).toBeDisabled()
33+
for (const coursewareButton of coursewareButtons) {
34+
expect(coursewareButton).toBeDisabled()
35+
}
3236
})
3337

3438
test("Courseware button is enabled if course has started AND NOT ended", () => {
@@ -39,10 +43,12 @@ describe("EnrollmentCard", () => {
3943
},
4044
})
4145
renderWithProviders(<DashboardCard dashboardResource={course} />)
42-
const coursewareLink = screen.getByRole("link", {
46+
const coursewareLinks = screen.getAllByRole("link", {
4347
name: "Continue Course",
4448
})
45-
expect(coursewareLink).toHaveAttribute("href", course.run.coursewareUrl)
49+
for (const coursewareLink of coursewareLinks) {
50+
expect(coursewareLink).toHaveAttribute("href", course.run.coursewareUrl)
51+
}
4652
})
4753

4854
test("Courseware button says 'View Course' if course has ended", () => {
@@ -53,10 +59,12 @@ describe("EnrollmentCard", () => {
5359
},
5460
})
5561
renderWithProviders(<DashboardCard dashboardResource={course} />)
56-
const coursewareLink = screen.getByRole("link", {
62+
const coursewareLinks = screen.getAllByRole("link", {
5763
name: "View Course",
5864
})
59-
expect(coursewareLink).toHaveAttribute("href", course.run.coursewareUrl)
65+
for (const coursewareLink of coursewareLinks) {
66+
expect(coursewareLink).toHaveAttribute("href", course.run.coursewareUrl)
67+
}
6068
})
6169

6270
test.each([
@@ -93,8 +101,14 @@ describe("EnrollmentCard", () => {
93101
({ overrides, expectation }) => {
94102
const course = dashboardCourse(overrides)
95103
renderWithProviders(<DashboardCard dashboardResource={course} />)
96-
const upgradeRoot = screen.queryByTestId("upgrade-root")
97-
expect(!!upgradeRoot).toBe(expectation.visible)
104+
const upgradeRootDesktop = within(
105+
screen.getByTestId("enrollment-card-desktop"),
106+
).queryByTestId("upgrade-root")
107+
const upgradeRootMobile = within(
108+
screen.getByTestId("enrollment-card-mobile"),
109+
).queryByTestId("upgrade-root")
110+
expect(!!upgradeRootDesktop).toBe(expectation.visible)
111+
expect(!!upgradeRootMobile).toBe(expectation.visible)
98112
},
99113
)
100114

@@ -117,13 +131,20 @@ describe("EnrollmentCard", () => {
117131

118132
renderWithProviders(<DashboardCard dashboardResource={course} />)
119133

120-
const upgradeRoot = screen.getByTestId("upgrade-root")
121-
expect(upgradeRoot).toBeVisible()
122-
123-
expect(upgradeRoot).toHaveTextContent(/5 days remaining/)
124-
expect(upgradeRoot).toHaveTextContent(
125-
`Add a certificate for $${certificateUpgradePrice}`,
126-
)
134+
const upgradeRootDesktop = within(
135+
screen.getByTestId("enrollment-card-desktop"),
136+
).queryByTestId("upgrade-root")
137+
const upgradeRootMobile = within(
138+
screen.getByTestId("enrollment-card-mobile"),
139+
).queryByTestId("upgrade-root")
140+
for (const upgradeRoot of [upgradeRootDesktop, upgradeRootMobile]) {
141+
expect(upgradeRoot).toBeVisible()
142+
143+
expect(upgradeRoot).toHaveTextContent(/5 days remaining/)
144+
expect(upgradeRoot).toHaveTextContent(
145+
`Add a certificate for $${certificateUpgradePrice}`,
146+
)
147+
}
127148
})
128149

129150
test("Shows number of days until course starts", () => {
@@ -141,18 +162,43 @@ describe("EnrollmentCard", () => {
141162
})
142163

143164
test.each([
144-
{ enrollmentStatus: EnrollmentStatus.Completed, hasCompleted: true },
145-
{ enrollmentStatus: EnrollmentStatus.Enrolled, hasCompleted: false },
165+
{
166+
enrollmentStatus: EnrollmentStatus.Completed,
167+
hasCompleted: true,
168+
showNotComplete: false,
169+
},
170+
{
171+
enrollmentStatus: EnrollmentStatus.Enrolled,
172+
hasCompleted: false,
173+
showNotComplete: false,
174+
},
175+
{
176+
enrollmentStatus: EnrollmentStatus.Completed,
177+
hasCompleted: true,
178+
showNotComplete: true,
179+
},
180+
{
181+
enrollmentStatus: EnrollmentStatus.Enrolled,
182+
hasCompleted: false,
183+
showNotComplete: true,
184+
},
146185
])(
147186
"Shows completed icon if course is completed",
148-
({ enrollmentStatus, hasCompleted }) => {
187+
({ enrollmentStatus, hasCompleted, showNotComplete }) => {
149188
const course = dashboardCourse({
150189
enrollment: { status: enrollmentStatus },
151190
})
152-
renderWithProviders(<DashboardCard dashboardResource={course} />)
153-
154-
const completedIcon = screen.queryByRole("img", { name: "Completed" })
155-
expect(!!completedIcon).toBe(hasCompleted)
191+
renderWithProviders(
192+
<DashboardCard
193+
dashboardResource={course}
194+
showNotComplete={showNotComplete}
195+
/>,
196+
)
197+
198+
const completedIcon = screen.queryAllByRole("img", { name: "Completed" })
199+
for (const icon of completedIcon) {
200+
expect(!!icon).toBe(hasCompleted)
201+
}
156202
},
157203
)
158204
})
@@ -173,7 +219,13 @@ test.each([
173219
/>,
174220
)
175221

176-
const notCompletedIcon = screen.queryByTestId("not-complete-icon")
177-
expect(!!notCompletedIcon).toBe(showNotComplete)
222+
const notCompletedIconDesktop = within(
223+
screen.getByTestId("enrollment-card-desktop"),
224+
).queryByTestId("not-complete-icon")
225+
const notCompletedIconMobile = within(
226+
screen.getByTestId("enrollment-card-mobile"),
227+
).queryByTestId("not-complete-icon")
228+
expect(!!notCompletedIconDesktop).toBe(showNotComplete)
229+
expect(!!notCompletedIconMobile).toBe(showNotComplete)
178230
},
179231
)

frontends/main/src/app-pages/DashboardPage/CoursewareDisplay/DashboardCard.tsx

+124-16
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,26 @@ import { calendarDaysUntil, isInPast, NoSSR } from "ol-utilities"
1515

1616
import CompleteCheck from "@/public/images/icons/complete-check.svg"
1717

18+
const DesktopOnly = styled.div(({ theme }) => ({
19+
[theme.breakpoints.up("md")]: {
20+
display: "list-item",
21+
},
22+
[theme.breakpoints.down("md")]: {
23+
display: "none",
24+
},
25+
}))
26+
27+
const MobileOnly = styled.div(({ theme }) => ({
28+
[theme.breakpoints.down("md")]: {
29+
display: "list-item",
30+
},
31+
[theme.breakpoints.up("md")]: {
32+
display: "none",
33+
},
34+
}))
35+
1836
const CardRoot = styled.div(({ theme }) => ({
37+
position: "relative",
1938
border: `1px solid ${theme.custom.colors.lightGray2}`,
2039
borderRadius: "8px",
2140
backgroundColor: theme.custom.colors.white,
@@ -24,11 +43,30 @@ const CardRoot = styled.div(({ theme }) => ({
2443
display: "flex",
2544
gap: "8px",
2645
alignItems: "center",
46+
[theme.breakpoints.down("md")]: {
47+
border: "none",
48+
borderBottom: `1px solid ${theme.custom.colors.lightGray2}`,
49+
borderRadius: "0px",
50+
boxShadow: "none",
51+
flexDirection: "column",
52+
gap: "16px",
53+
},
54+
}))
55+
56+
const TitleLink = styled(Link)(({ theme }) => ({
57+
[theme.breakpoints.down("md")]: {
58+
maxWidth: "calc(100% - 16px)",
59+
},
2760
}))
2861

29-
const MenuButton = styled(ActionButton)({
62+
const MenuButton = styled(ActionButton)(({ theme }) => ({
3063
marginLeft: "-8px",
31-
})
64+
[theme.breakpoints.down("md")]: {
65+
position: "absolute",
66+
top: "0",
67+
right: "0",
68+
},
69+
}))
3270

3371
const getCoursewareText = (endDate?: string | null) => {
3472
if (!endDate) return "Continue Course"
@@ -139,13 +177,17 @@ const UpgradeBanner: React.FC<
139177
)
140178
}
141179

142-
const CountdownRoot = styled.div({
180+
const CountdownRoot = styled.div(({ theme }) => ({
143181
width: "142px",
144182
marginRight: "32px",
145183
display: "flex",
146184
justifyContent: "center",
147185
alignSelf: "end",
148-
})
186+
[theme.breakpoints.down("md")]: {
187+
marginRight: "0px",
188+
justifyContent: "flex-start",
189+
},
190+
}))
149191
const CourseStartCountdown: React.FC<{
150192
startDate: string
151193
className?: string
@@ -213,12 +255,22 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
213255
showNotComplete = true,
214256
}) => {
215257
const { title, marketingUrl, enrollment, run } = dashboardResource
216-
return (
217-
<CardRoot data-testid="enrollment-card">
258+
const contextMenu = (
259+
<SimpleMenu
260+
items={getMenuItems()}
261+
trigger={
262+
<MenuButton size="small" variant="text" aria-label="More options">
263+
<RiMore2Line />
264+
</MenuButton>
265+
}
266+
/>
267+
)
268+
const desktopLayout = (
269+
<CardRoot data-testid="enrollment-card-desktop">
218270
<Stack justifyContent="start" alignItems="stretch" gap="8px" flex={1}>
219-
<Link size="medium" color="black" href={marketingUrl}>
271+
<TitleLink size="medium" color="black" href={marketingUrl}>
220272
{title}
221-
</Link>
273+
</TitleLink>
222274
{enrollment?.status === EnrollmentStatus.Completed ? (
223275
<SubtitleLink href="#">
224276
{<RiAwardLine size="16px" />}
@@ -246,21 +298,77 @@ const DashboardCard: React.FC<DashboardCardProps> = ({
246298
href={run.coursewareUrl}
247299
endDate={run.endDate}
248300
/>
249-
<SimpleMenu
250-
items={getMenuItems()}
251-
trigger={
252-
<MenuButton size="small" variant="text" aria-label="More options">
253-
<RiMore2Line />
254-
</MenuButton>
255-
}
256-
/>
301+
{contextMenu}
257302
</Stack>
258303
{run.startDate ? (
259304
<CourseStartCountdown startDate={run.startDate} />
260305
) : null}
261306
</Stack>
262307
</CardRoot>
263308
)
309+
310+
const mobileLayout = (
311+
<CardRoot data-testid="enrollment-card-mobile">
312+
<Stack
313+
direction="row"
314+
justifyContent="space-between"
315+
alignItems="stretch"
316+
flex={1}
317+
width="100%"
318+
>
319+
<Stack direction="column" gap="8px">
320+
<TitleLink size="medium" color="black" href={marketingUrl}>
321+
{title}
322+
</TitleLink>
323+
{enrollment?.status === EnrollmentStatus.Completed ? (
324+
<SubtitleLink href="#">
325+
{<RiAwardLine size="16px" />}
326+
View Certificate
327+
</SubtitleLink>
328+
) : null}
329+
{enrollment?.mode !== EnrollmentMode.Verified ? (
330+
<UpgradeBanner
331+
data-testid="upgrade-root"
332+
canUpgrade={run.canUpgrade}
333+
certificateUpgradeDeadline={run.certificateUpgradeDeadline}
334+
certificateUpgradePrice={run.certificateUpgradePrice}
335+
/>
336+
) : null}
337+
</Stack>
338+
{contextMenu}
339+
</Stack>
340+
<Stack
341+
direction="row"
342+
alignItems="center"
343+
justifyContent="space-between"
344+
width="100%"
345+
>
346+
{run.startDate ? (
347+
<Stack justifyContent="start">
348+
<CourseStartCountdown startDate={run.startDate} />
349+
</Stack>
350+
) : null}
351+
<Stack direction="row" gap="8px" alignItems="center">
352+
{enrollment?.status === EnrollmentStatus.Completed ? (
353+
<Completed src={CompleteCheck} alt="Completed" />
354+
) : showNotComplete ? (
355+
<NotComplete data-testid="not-complete-icon" />
356+
) : null}
357+
<CoursewareButton
358+
startDate={run.startDate}
359+
href={run.coursewareUrl}
360+
endDate={run.endDate}
361+
/>
362+
</Stack>
363+
</Stack>
364+
</CardRoot>
365+
)
366+
return (
367+
<>
368+
<DesktopOnly>{desktopLayout}</DesktopOnly>
369+
<MobileOnly>{mobileLayout}</MobileOnly>
370+
</>
371+
)
264372
}
265373

266374
export { DashboardCard }

0 commit comments

Comments
 (0)