Skip to content

Commit 8882f8a

Browse files
committed
feat: mobile designs for details pages
1 parent 2014261 commit 8882f8a

File tree

3 files changed

+190
-88
lines changed

3 files changed

+190
-88
lines changed

src/learningpath/CourseDetails.jsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,10 @@ const CourseDetailContent = ({
8181
</ModalCloseButton>
8282
</Row>
8383
)}
84-
<Card orientation="horizontal">
84+
<Card orientation={isSmall ? 'vertical' : 'horizontal'}>
85+
{isSmall && (
86+
<Card.ImageCap src={buildAssetUrl(courseImageAssetPath)} logoSrc={orgData.logo} className="mb-4" />
87+
)}
8588
<Card.Body>
8689
{!isModalView && (
8790
<Card.Section>
@@ -100,39 +103,41 @@ const CourseDetailContent = ({
100103
</p>
101104
</Card.Section>
102105
</Card.Body>
103-
<Card.ImageCap src={buildAssetUrl(courseImageAssetPath)} logoSrc={orgData.logo} />
106+
{!isSmall && (
107+
<Card.ImageCap src={buildAssetUrl(courseImageAssetPath)} logoSrc={orgData.logo} />
108+
)}
104109
</Card>
105-
<Row className="my-4 mx-0 px-6 d-flex hero-info course-hero-info">
110+
<Row className="my-4 mx-0 px-5 px-md-6 flex-column flex-md-row align-items-start hero-info course-hero-info">
106111
{dateDisplay && (
107112
<div className="d-flex align-items-center">
108113
<Icon src={AccessTimeFilled} className="mr-4 mb-3" />
109114
<div>
110-
<p className="mb-1 font-weight-bold">{dateDisplay}</p>
111-
<p className="text-muted">Access ends</p>
115+
<p className="mb-0 font-weight-bold">{dateDisplay}</p>
116+
<p className="mb-2 text-muted">Access ends</p>
112117
</div>
113118
</div>
114119
)}
115120
<div className="d-flex align-items-center">
116121
<Icon src={Award} className="mr-4 mb-4" />
117122
<div>
118-
<p className="mb-1 font-weight-bold">Certificate</p>
119-
<p className="text-muted">Earn a certificate</p>
123+
<p className="mb-0 font-weight-bold">Certificate</p>
124+
<p className="mb-2 text-muted">Earn a certificate</p>
120125
</div>
121126
</div>
122127
{duration && (
123128
<div className="d-flex align-items-center">
124129
<Icon src={Calendar} className="mr-4 mb-4" />
125130
<div>
126-
<p className="mb-1 font-weight-bold">{duration}</p>
127-
<p className="text-muted">Approx. duration</p>
131+
<p className="mb-0 font-weight-bold">{duration}</p>
132+
<p className="mb-2 text-muted">Approx. duration</p>
128133
</div>
129134
</div>
130135
)}
131136
<div className="d-flex align-items-center">
132137
<Icon src={Person} className="mr-4 mb-4" />
133138
<div>
134-
<p className="mb-1 font-weight-bold">{selfPaced ? 'Self-paced' : 'Instructor-paced'}</p>
135-
<p className="text-muted">
139+
<p className="mb-0 font-weight-bold">{selfPaced ? 'Self-paced' : 'Instructor-paced'}</p>
140+
<p className="mb-2 text-muted">
136141
{selfPaced ? 'Progress at your own speed' : 'Follow the course schedule'}
137142
</p>
138143
</div>

src/learningpath/LearningPathDetails.jsx

Lines changed: 166 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useMemo, useState, useEffect } from 'react';
22
import { useParams, Link } from 'react-router-dom';
33
import {
4-
Row, Spinner, Nav, Icon, ModalLayer, Button, Chip, Card,
4+
Row, Spinner, Nav, Icon, ModalLayer, Button, Chip, Card, Collapsible,
55
} from '@openedx/paragon';
66
import {
77
Person,
@@ -16,17 +16,24 @@ import {
1616
} from './data/queries';
1717
import { CourseCardWithEnrollment } from './CourseCard';
1818
import CourseDetailPage from './CourseDetails';
19+
import { useScreenSize } from '../hooks/useScreenSize';
1920

2021
const LearningPathDetailPage = () => {
22+
const { isSmall } = useScreenSize();
2123
const { key } = useParams();
2224
const [selectedCourseKey, setSelectedCourseKey] = useState(null);
2325
const [enrolling, setEnrolling] = useState(false);
26+
const [openCollapsible, setOpenCollapsible] = useState(null);
2427

2528
const [activeTab, setActiveTab] = useState(null);
2629
const handleTabSelect = (selectedKey) => {
2730
setActiveTab(selectedKey);
2831
};
2932

33+
const handleCollapsibleToggle = (collapsibleId) => {
34+
setOpenCollapsible(openCollapsible === collapsibleId ? null : collapsibleId);
35+
};
36+
3037
// Scroll to the top when the component mounts.
3138
useEffect(() => {
3239
// Add a timeout to ensure DOM updates are complete.
@@ -138,15 +145,18 @@ const LearningPathDetailPage = () => {
138145
// Hero section - same for both full view and enrolled view.
139146
const heroSection = (
140147
<div className="hero">
141-
<Card orientation="horizontal">
148+
<Card orientation={isSmall ? 'vertical' : 'horizontal'} className={isSmall ? 'border-0' : ''}>
142149
<Card.Body>
143150
<Card.Section>
144151
<Link to="/" className="d-flex align-items-center back-link pl-4">
145152
<Icon src={ChevronLeft} />
146153
<span>Go Back</span>
147154
</Link>
148155
</Card.Section>
149-
<Card.Section className="pl-5 pr-6">
156+
{isSmall && (
157+
<Card.ImageCap src={image} logoSrc={orgData.logo} className="mb-4" />
158+
)}
159+
<Card.Section className="px-sm-4 pl-5 pr-6">
150160
<Chip iconBefore={FormatListBulleted} className="lp-chip">LEARNING PATH</Chip>
151161
<h1 className="my-3 mt-4.5">{displayName}</h1>
152162
<p className="text-muted">
@@ -155,9 +165,27 @@ const LearningPathDetailPage = () => {
155165
</p>
156166
</Card.Section>
157167
</Card.Body>
158-
<Card.ImageCap src={image} logoSrc={orgData.logo} />
168+
{!isSmall && (
169+
<Card.ImageCap src={image} logoSrc={orgData.logo} />
170+
)}
159171
</Card>
160-
<Row className="my-4 mx-0 px-6 d-flex hero-info lp-hero-info">
172+
{isSmall && (
173+
<div className="mx-4">
174+
<Button
175+
variant={enrollmentDate ? 'secondary' : 'primary'}
176+
className="px-3 w-100"
177+
onClick={handleEnrollClick}
178+
disabled={enrolling || enrollmentDate}
179+
>
180+
{(() => {
181+
if (enrolling) { return 'Enrolling...'; }
182+
if (enrollmentDate) { return 'Enrolled'; }
183+
return 'Enroll';
184+
})()}
185+
</Button>
186+
</div>
187+
)}
188+
<Row className="my-4 mx-0 px-sm-4 px-5 px-md-6 flex-column flex-md-row align-items-start hero-info lp-hero-info">
161189
{accessUntilDate && (
162190
<div className="d-flex">
163191
<Icon src={AccessTimeFilled} className="mr-4 mb-3" />
@@ -199,79 +227,143 @@ const LearningPathDetailPage = () => {
199227
content = (
200228
<div className="detail-page learning-path-detail-page">
201229
{heroSection}
202-
<div className="tabs d-flex align-items-center pl-5.5 pr-0">
203-
<Nav
204-
variant="tabs"
205-
onSelect={handleTabSelect}
206-
className="border-bottom-0"
207-
activeKey={activeTab}
208-
>
209-
<Nav.Item>
210-
<Nav.Link eventKey="about" className="font-weight-normal">About</Nav.Link>
211-
</Nav.Item>
212-
<Nav.Item>
213-
<Nav.Link eventKey="courses" className="font-weight-normal">Courses</Nav.Link>
214-
</Nav.Item>
215-
{requiredSkills && requiredSkills.length > 0 && (
230+
{!isSmall && (
231+
<div className="tabs d-flex align-items-center pl-5.5 pr-0">
232+
<Nav
233+
variant="tabs"
234+
onSelect={handleTabSelect}
235+
className="border-bottom-0"
236+
activeKey={activeTab}
237+
>
216238
<Nav.Item>
217-
<Nav.Link eventKey="requirements" className="font-weight-normal">Requirements</Nav.Link>
239+
<Nav.Link eventKey="about" className="font-weight-normal">About</Nav.Link>
218240
</Nav.Item>
219-
)}
220-
</Nav>
221-
<Button
222-
variant={enrollmentDate ? 'secondary' : 'primary'}
223-
className="ml-auto rounded-0 px-5.5 align-self-stretch"
224-
onClick={handleEnrollClick}
225-
disabled={enrolling || enrollmentDate}
226-
>
227-
{(() => {
228-
if (enrolling) { return 'Enrolling...'; }
229-
if (enrollmentDate) { return 'Enrolled'; }
230-
return 'Enroll';
231-
})()}
232-
</Button>
233-
</div>
241+
<Nav.Item>
242+
<Nav.Link eventKey="courses" className="font-weight-normal">Courses</Nav.Link>
243+
</Nav.Item>
244+
{requiredSkills && requiredSkills.length > 0 && (
245+
<Nav.Item>
246+
<Nav.Link eventKey="requirements" className="font-weight-normal">Requirements</Nav.Link>
247+
</Nav.Item>
248+
)}
249+
</Nav>
250+
<Button
251+
variant={enrollmentDate ? 'secondary' : 'primary'}
252+
className="ml-auto rounded-0 px-5.5 align-self-stretch"
253+
onClick={handleEnrollClick}
254+
disabled={enrolling || enrollmentDate}
255+
>
256+
{(() => {
257+
if (enrolling) { return 'Enrolling...'; }
258+
if (enrollmentDate) { return 'Enrolled'; }
259+
return 'Enroll';
260+
})()}
261+
</Button>
262+
</div>
263+
)}
234264
<div className="py-3 lp-info">
235-
{activeTab === 'about' && (
236-
<section id="about">
237-
<h2>About</h2>
238-
<p>
239-
{/* eslint-disable-next-line react/no-danger */}
240-
<div dangerouslySetInnerHTML={{ __html: description || 'No description available.' }} />
241-
</p>
242-
</section>
243-
)}
244-
{activeTab === 'courses' && (
245-
<div id="courses-section-wrapper">
246-
<section id="courses" className="mx-auto">
247-
<h2>Courses</h2>
248-
{!loadingCourses && !coursesError && (!coursesForPath || coursesForPath.length === 0) && (
249-
<p>No sub-courses found in this learning path.</p>
250-
)}
251-
{!loadingCourses && !coursesError && coursesForPath && coursesForPath.length > 0 && (
252-
coursesForPath.map(course => (
253-
<div key={course.id} className="mb-3">
254-
<CourseCardWithEnrollment
255-
course={course}
256-
learningPathId={key}
257-
enrollmentDateInLearningPath={enrollmentDate}
258-
onClick={() => handleCourseViewButton(course.id)}
259-
/>
260-
</div>
261-
))
262-
)}
263-
</section>
265+
{isSmall ? (
266+
<div className="mobile-content px-4">
267+
<Collapsible
268+
title="About"
269+
open={openCollapsible === 'about'}
270+
onToggle={() => handleCollapsibleToggle('about')}
271+
className="mb-3"
272+
>
273+
<section id="about">
274+
{/* eslint-disable-next-line react/no-danger */}
275+
<div dangerouslySetInnerHTML={{ __html: description || 'No description available.' }} />
276+
</section>
277+
</Collapsible>
278+
279+
<Collapsible
280+
title="Courses"
281+
open={openCollapsible === 'courses'}
282+
onToggle={() => handleCollapsibleToggle('courses')}
283+
className="mb-3"
284+
>
285+
<div id="courses-section-wrapper">
286+
<section id="courses" className="mx-4">
287+
{!loadingCourses && !coursesError && (!coursesForPath || coursesForPath.length === 0) && (
288+
<p>No sub-courses found in this learning path.</p>
289+
)}
290+
{!loadingCourses && !coursesError && coursesForPath && coursesForPath.length > 0 && (
291+
coursesForPath.map(course => (
292+
<div key={course.id} className="mb-3">
293+
<CourseCardWithEnrollment
294+
course={course}
295+
learningPathId={key}
296+
enrollmentDateInLearningPath={enrollmentDate}
297+
onClick={() => handleCourseViewButton(course.id)}
298+
/>
299+
</div>
300+
))
301+
)}
302+
</section>
303+
</div>
304+
</Collapsible>
305+
306+
{requiredSkills && requiredSkills.length > 0 && (
307+
<Collapsible
308+
title="Requirements"
309+
open={openCollapsible === 'requirements'}
310+
onToggle={() => handleCollapsibleToggle('requirements')}
311+
className="mb-3"
312+
>
313+
<section id="requirements">
314+
{requiredSkills.map((skillObj) => (
315+
<p key={`requirement-${skillObj.skill.displayName.replace(/\s+/g, '-').substring(0, 40)}`}>
316+
{skillObj.skill.displayName}
317+
</p>
318+
))}
319+
</section>
320+
</Collapsible>
321+
)}
322+
</div>
323+
) : (
324+
<div className="desktop-content">
325+
{activeTab === 'about' && (
326+
<section id="about">
327+
<h2>About</h2>
328+
<p>
329+
{/* eslint-disable-next-line react/no-danger */}
330+
<div dangerouslySetInnerHTML={{ __html: description || 'No description available.' }} />
331+
</p>
332+
</section>
333+
)}
334+
{activeTab === 'courses' && (
335+
<div id="courses-section-wrapper">
336+
<section id="courses" className="mx-auto">
337+
<h2>Courses</h2>
338+
{!loadingCourses && !coursesError && (!coursesForPath || coursesForPath.length === 0) && (
339+
<p>No sub-courses found in this learning path.</p>
340+
)}
341+
{!loadingCourses && !coursesError && coursesForPath && coursesForPath.length > 0 && (
342+
coursesForPath.map(course => (
343+
<div key={course.id} className="mb-3">
344+
<CourseCardWithEnrollment
345+
course={course}
346+
learningPathId={key}
347+
enrollmentDateInLearningPath={enrollmentDate}
348+
onClick={() => handleCourseViewButton(course.id)}
349+
/>
350+
</div>
351+
))
352+
)}
353+
</section>
354+
</div>
355+
)}
356+
{activeTab === 'requirements' && (
357+
<section id="requirements">
358+
<h2>Requirements</h2>
359+
{requiredSkills.map((skillObj) => (
360+
<p key={`requirement-${skillObj.skill.displayName.replace(/\s+/g, '-').substring(0, 40)}`}>
361+
{skillObj.skill.displayName}
362+
</p>
363+
))}
364+
</section>
365+
)}
264366
</div>
265-
)}
266-
{activeTab === 'requirements' && (
267-
<section id="requirements">
268-
<h2>Requirements</h2>
269-
{requiredSkills.map((skillObj) => (
270-
<p key={`requirement-${skillObj.skill.displayName.replace(/\s+/g, '-').substring(0, 40)}`}>
271-
{skillObj.skill.displayName}
272-
</p>
273-
))}
274-
</section>
275367
)}
276368
</div>
277369
</div>

0 commit comments

Comments
 (0)