richie-education 2.34.1-dev5 → 2.34.1-dev52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +1 -1
- package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +84 -13
- package/js/components/CourseGlimpse/index.spec.tsx +80 -5
- package/js/components/CourseGlimpse/index.tsx +92 -76
- package/js/components/CourseGlimpse/utils.ts +31 -1
- package/js/components/Icon/index.tsx +7 -0
- package/js/components/OpenEdxFullNameForm/index.spec.tsx +17 -7
- package/js/components/OpenEdxFullNameForm/index.tsx +13 -16
- package/js/components/SaleTunnel/index.full-process.spec.tsx +1 -1
- package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +9 -8
- package/js/types/Course.ts +18 -0
- package/js/types/index.ts +5 -0
- package/js/utils/test/expectAlert.ts +63 -0
- package/js/utils/test/factories/richie.ts +31 -1
- package/js/widgets/Slider/components/Slide.tsx +20 -0
- package/js/widgets/Slider/components/SlidePanel.tsx +83 -0
- package/js/widgets/Slider/components/Slideshow.tsx +58 -0
- package/js/widgets/Slider/index.spec.tsx +167 -0
- package/js/widgets/Slider/index.tsx +119 -0
- package/js/widgets/Slider/types/index.ts +8 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +107 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +107 -0
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +450 -5
- package/js/widgets/index.tsx +3 -0
- package/package.json +45 -43
- package/scss/colors/_theme.scss +26 -7
- package/scss/components/_header.scss +108 -14
- package/scss/components/_subheader.scss +35 -0
- package/scss/components/templates/courses/cms/_program_detail.scss +71 -0
- package/scss/components/templates/richie/slider/_slider.scss +165 -99
- package/scss/objects/_course_glimpses.scss +109 -10
- package/scss/objects/_selector.scss +1 -0
- package/scss/settings/_variables.scss +4 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import useEmblaCarousel from 'embla-carousel-react';
|
|
2
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
3
|
+
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
|
4
|
+
import { defineMessages, FormattedMessage } from 'react-intl';
|
|
5
|
+
import { Slide as SlideType } from './types';
|
|
6
|
+
import SlidePanel from './components/SlidePanel';
|
|
7
|
+
import Slideshow from './components/Slideshow';
|
|
8
|
+
|
|
9
|
+
const messages = defineMessages({
|
|
10
|
+
sliderSummary: {
|
|
11
|
+
id: 'widgets.Slider.sliderSummary',
|
|
12
|
+
defaultMessage: 'Slide {slideNumber} of {totalSlides}: {slideTitle}',
|
|
13
|
+
description: 'Aria live label which summarizes the slider state.',
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
type SliderProps = {
|
|
18
|
+
// eslint-disable-next-line react/no-unused-prop-types
|
|
19
|
+
pk: string;
|
|
20
|
+
title: string;
|
|
21
|
+
slides: readonly SlideType[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const Slider = ({ slides, title }: SliderProps) => {
|
|
25
|
+
const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [WheelGesturesPlugin()]);
|
|
26
|
+
const [activeSlideIndex, setActiveSlideIndex] = useState(0);
|
|
27
|
+
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
28
|
+
|
|
29
|
+
const handleBulletClick = useCallback(
|
|
30
|
+
(index: number) => {
|
|
31
|
+
setIsTransitioning(true);
|
|
32
|
+
emblaApi?.scrollTo(index);
|
|
33
|
+
},
|
|
34
|
+
[emblaApi],
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const handleKeyDown = useCallback(
|
|
38
|
+
(event: React.KeyboardEvent) => {
|
|
39
|
+
if (!emblaApi) return;
|
|
40
|
+
|
|
41
|
+
switch (event.key) {
|
|
42
|
+
case 'ArrowLeft':
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
emblaApi.scrollPrev();
|
|
45
|
+
break;
|
|
46
|
+
case 'ArrowRight':
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
emblaApi.scrollNext();
|
|
49
|
+
break;
|
|
50
|
+
case 'Home':
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
emblaApi.scrollTo(0);
|
|
53
|
+
break;
|
|
54
|
+
case 'End':
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
emblaApi.scrollTo(slides.length - 1);
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
[emblaApi, slides.length],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!emblaApi) return;
|
|
67
|
+
|
|
68
|
+
const handleSlidesChanged = (event: any) => {
|
|
69
|
+
setIsTransitioning(true);
|
|
70
|
+
setActiveSlideIndex(event.selectedScrollSnap());
|
|
71
|
+
};
|
|
72
|
+
emblaApi.on('select', handleSlidesChanged);
|
|
73
|
+
|
|
74
|
+
return () => {
|
|
75
|
+
emblaApi.off('select', handleSlidesChanged);
|
|
76
|
+
};
|
|
77
|
+
}, [emblaApi]);
|
|
78
|
+
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
// Remove the transitioning class immediately after a transitioned render
|
|
81
|
+
setIsTransitioning(false);
|
|
82
|
+
}, [activeSlideIndex]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
className="slider"
|
|
87
|
+
ref={emblaRef}
|
|
88
|
+
aria-roledescription="carousel"
|
|
89
|
+
aria-label={title}
|
|
90
|
+
role="button"
|
|
91
|
+
tabIndex={0}
|
|
92
|
+
onKeyDown={handleKeyDown}
|
|
93
|
+
>
|
|
94
|
+
<Slideshow
|
|
95
|
+
slides={slides}
|
|
96
|
+
onNextSlide={() => emblaApi?.scrollNext()}
|
|
97
|
+
onPreviousSlide={() => emblaApi?.scrollPrev()}
|
|
98
|
+
/>
|
|
99
|
+
<SlidePanel
|
|
100
|
+
slides={slides}
|
|
101
|
+
activeSlideIndex={activeSlideIndex}
|
|
102
|
+
onBulletClick={handleBulletClick}
|
|
103
|
+
isTransitioning={isTransitioning}
|
|
104
|
+
/>
|
|
105
|
+
<span className="offscreen" role="presentation" aria-live="polite" aria-atomic="true">
|
|
106
|
+
<FormattedMessage
|
|
107
|
+
{...messages.sliderSummary}
|
|
108
|
+
values={{
|
|
109
|
+
slideNumber: activeSlideIndex + 1,
|
|
110
|
+
totalSlides: slides.length,
|
|
111
|
+
slideTitle: slides[activeSlideIndex].title,
|
|
112
|
+
}}
|
|
113
|
+
/>
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default Slider;
|
|
@@ -46,6 +46,51 @@ const messages = defineMessages({
|
|
|
46
46
|
description: 'Course date of an opened course run block',
|
|
47
47
|
defaultMessage: 'From {startDate} {endDate, select, undefined {} other {to {endDate}}}',
|
|
48
48
|
},
|
|
49
|
+
coursePrice: {
|
|
50
|
+
id: 'components.SyllabusCourseRun.coursePrice',
|
|
51
|
+
description: 'Title of the course enrollment price section of an opened course run block',
|
|
52
|
+
defaultMessage: 'Enrollment price',
|
|
53
|
+
},
|
|
54
|
+
certificationPrice: {
|
|
55
|
+
id: 'components.SyllabusCourseRun.certificationPrice',
|
|
56
|
+
description: 'Title of the certification price section of an opened course run block',
|
|
57
|
+
defaultMessage: 'Certification price',
|
|
58
|
+
},
|
|
59
|
+
coursePaidOffer: {
|
|
60
|
+
id: 'components.SyllabusCourseRun.coursePaidOffer',
|
|
61
|
+
description: 'Message for the paid course offer of an opened course run block',
|
|
62
|
+
defaultMessage: 'The course content is paid.',
|
|
63
|
+
},
|
|
64
|
+
courseFreeOffer: {
|
|
65
|
+
id: 'components.SyllabusCourseRun.courseFreeOffer',
|
|
66
|
+
description: 'Message for the free course offer of an opened course run block',
|
|
67
|
+
defaultMessage: 'The course content is free.',
|
|
68
|
+
},
|
|
69
|
+
coursePartiallyFree: {
|
|
70
|
+
id: 'components.SyllabusCourseRun.coursePartiallyFree',
|
|
71
|
+
description: 'Message for the partially free course offer of an opened course run block',
|
|
72
|
+
defaultMessage: 'The course content is free.',
|
|
73
|
+
},
|
|
74
|
+
courseSubscriptionOffer: {
|
|
75
|
+
id: 'components.SyllabusCourseRun.courseSubscriptionOffer',
|
|
76
|
+
description: 'Message for the subscription course offer of an opened course run block',
|
|
77
|
+
defaultMessage: 'Subscribe to access the course content.',
|
|
78
|
+
},
|
|
79
|
+
certificatePaidOffer: {
|
|
80
|
+
id: 'components.SyllabusCourseRun.certificatePaidOffer',
|
|
81
|
+
description: 'Messagge for the paid certification offer of an opened course run block',
|
|
82
|
+
defaultMessage: 'The certification process is paid.',
|
|
83
|
+
},
|
|
84
|
+
certificateFreeOffer: {
|
|
85
|
+
id: 'components.SyllabusCourseRun.certificateFreeOffer',
|
|
86
|
+
description: 'Message for the free certification offer of an opened course run block',
|
|
87
|
+
defaultMessage: 'The certification process is free.',
|
|
88
|
+
},
|
|
89
|
+
certificateSubscriptionOffer: {
|
|
90
|
+
id: 'components.SyllabusCourseRun.certificateSubscriptionOffer',
|
|
91
|
+
description: 'Message for the subscription certification offer of an opened course run block',
|
|
92
|
+
defaultMessage: 'The certification process is offered through subscription.',
|
|
93
|
+
},
|
|
49
94
|
});
|
|
50
95
|
|
|
51
96
|
const OpenedCourseRun = ({
|
|
@@ -63,6 +108,44 @@ const OpenedCourseRun = ({
|
|
|
63
108
|
const enrollmentEnd = courseRun.enrollment_end ? formatDate(courseRun.enrollment_end) : '...';
|
|
64
109
|
const start = courseRun.start ? formatDate(courseRun.start) : '...';
|
|
65
110
|
const end = courseRun.end ? formatDate(courseRun.end) : '...';
|
|
111
|
+
let courseOfferMessage = null;
|
|
112
|
+
let certificationOfferMessage = null;
|
|
113
|
+
let enrollmentPrice = '';
|
|
114
|
+
let certificatePrice = '';
|
|
115
|
+
|
|
116
|
+
if (courseRun.offer) {
|
|
117
|
+
const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
|
|
118
|
+
courseOfferMessage = {
|
|
119
|
+
PAID: messages.coursePaidOffer,
|
|
120
|
+
FREE: messages.courseFreeOffer,
|
|
121
|
+
PARTIALLY_FREE: messages.coursePartiallyFree,
|
|
122
|
+
SUBSCRIPTION: messages.courseSubscriptionOffer,
|
|
123
|
+
}[offer];
|
|
124
|
+
|
|
125
|
+
if ((courseRun.price ?? -1) >= 0) {
|
|
126
|
+
enrollmentPrice = intl.formatNumber(courseRun.price!, {
|
|
127
|
+
style: 'currency',
|
|
128
|
+
currency: courseRun.price_currency,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (courseRun.certificate_offer) {
|
|
134
|
+
const certificationOffer = courseRun.certificate_offer.toUpperCase().replaceAll(' ', '');
|
|
135
|
+
certificationOfferMessage = {
|
|
136
|
+
PAID: messages.certificatePaidOffer,
|
|
137
|
+
FREE: messages.certificateFreeOffer,
|
|
138
|
+
SUBSCRIPTION: messages.certificateSubscriptionOffer,
|
|
139
|
+
}[certificationOffer];
|
|
140
|
+
|
|
141
|
+
if ((courseRun.certificate_price ?? -1) >= 0) {
|
|
142
|
+
certificatePrice = intl.formatNumber(courseRun.certificate_price!, {
|
|
143
|
+
style: 'currency',
|
|
144
|
+
currency: courseRun.price_currency,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
66
149
|
return (
|
|
67
150
|
<>
|
|
68
151
|
{courseRun.title && <h3>{StringHelper.capitalizeFirst(courseRun.title)}</h3>}
|
|
@@ -99,6 +182,30 @@ const OpenedCourseRun = ({
|
|
|
99
182
|
<dd>{IntlHelper.getLocalizedLanguages(courseRun.languages, intl)}</dd>
|
|
100
183
|
</>
|
|
101
184
|
)}
|
|
185
|
+
{courseOfferMessage && (
|
|
186
|
+
<>
|
|
187
|
+
<dt>
|
|
188
|
+
<FormattedMessage {...messages.coursePrice} />
|
|
189
|
+
</dt>
|
|
190
|
+
<dd>
|
|
191
|
+
<FormattedMessage {...courseOfferMessage} />
|
|
192
|
+
<br />
|
|
193
|
+
{enrollmentPrice}
|
|
194
|
+
</dd>
|
|
195
|
+
</>
|
|
196
|
+
)}
|
|
197
|
+
{certificationOfferMessage && (
|
|
198
|
+
<>
|
|
199
|
+
<dt>
|
|
200
|
+
<FormattedMessage {...messages.certificationPrice} />
|
|
201
|
+
</dt>
|
|
202
|
+
<dd>
|
|
203
|
+
<FormattedMessage {...certificationOfferMessage} />
|
|
204
|
+
<br />
|
|
205
|
+
{certificatePrice}
|
|
206
|
+
</dd>
|
|
207
|
+
</>
|
|
208
|
+
)}
|
|
102
209
|
</dl>
|
|
103
210
|
{findLmsBackend(courseRun.resource_link) ? (
|
|
104
211
|
<CourseRunEnrollment courseRun={courseRun} />
|
|
@@ -41,6 +41,51 @@ const messages = defineMessages({
|
|
|
41
41
|
description: 'Self paced course run block with no end date',
|
|
42
42
|
defaultMessage: 'Available',
|
|
43
43
|
},
|
|
44
|
+
coursePrice: {
|
|
45
|
+
id: 'components.SyllabusCourseRunCompacted.coursePrice',
|
|
46
|
+
description: 'Title of the course enrollment price section of an opened course run block',
|
|
47
|
+
defaultMessage: 'Enrollment price',
|
|
48
|
+
},
|
|
49
|
+
certificationPrice: {
|
|
50
|
+
id: 'components.SyllabusCourseRunCompacted.certificationPrice',
|
|
51
|
+
description: 'Title of the certification price section of an opened course run block',
|
|
52
|
+
defaultMessage: 'Certification price',
|
|
53
|
+
},
|
|
54
|
+
coursePaidOffer: {
|
|
55
|
+
id: 'components.SyllabusCourseRunCompacted.coursePaidOffer',
|
|
56
|
+
description: 'Message for the paid course offer of an opened course run block',
|
|
57
|
+
defaultMessage: 'The course content is paid.',
|
|
58
|
+
},
|
|
59
|
+
courseFreeOffer: {
|
|
60
|
+
id: 'components.SyllabusCourseRunCompacted.courseFreeOffer',
|
|
61
|
+
description: 'Message for the free course offer of an opened course run block',
|
|
62
|
+
defaultMessage: 'The course content is free.',
|
|
63
|
+
},
|
|
64
|
+
coursePartiallyFree: {
|
|
65
|
+
id: 'components.SyllabusCourseRunCompacted.coursePartiallyFree',
|
|
66
|
+
description: 'Message for the partially free course offer of an opened course run block',
|
|
67
|
+
defaultMessage: 'The course content is free.',
|
|
68
|
+
},
|
|
69
|
+
courseSubscriptionOffer: {
|
|
70
|
+
id: 'components.SyllabusCourseRunCompacted.courseSubscriptionOffer',
|
|
71
|
+
description: 'Message for the subscription course offer of an opened course run block',
|
|
72
|
+
defaultMessage: 'Subscribe to access the course content.',
|
|
73
|
+
},
|
|
74
|
+
certificatePaidOffer: {
|
|
75
|
+
id: 'components.SyllabusCourseRunCompacted.certificatePaidOffer',
|
|
76
|
+
description: 'Messagge for the paid certification offer of an opened course run block',
|
|
77
|
+
defaultMessage: 'The certification process is paid.',
|
|
78
|
+
},
|
|
79
|
+
certificateFreeOffer: {
|
|
80
|
+
id: 'components.SyllabusCourseRunCompacted.certificateFreeOffer',
|
|
81
|
+
description: 'Message for the free certification offer of an opened course run block',
|
|
82
|
+
defaultMessage: 'The certification process is free.',
|
|
83
|
+
},
|
|
84
|
+
certificateSubscriptionOffer: {
|
|
85
|
+
id: 'components.SyllabusCourseRunCompacted.certificateSubscriptionOffer',
|
|
86
|
+
description: 'Message for the subscription certification offer of an opened course run block',
|
|
87
|
+
defaultMessage: 'The certification process is offered through subscription.',
|
|
88
|
+
},
|
|
44
89
|
});
|
|
45
90
|
|
|
46
91
|
const OpenedSelfPacedCourseRun = ({
|
|
@@ -54,6 +99,44 @@ const OpenedSelfPacedCourseRun = ({
|
|
|
54
99
|
const intl = useIntl();
|
|
55
100
|
const end = courseRun.end ? formatDate(courseRun.end) : '...';
|
|
56
101
|
const hasEndDate = end !== '...';
|
|
102
|
+
let courseOfferMessage = null;
|
|
103
|
+
let certificationOfferMessage = null;
|
|
104
|
+
let enrollmentPrice = '';
|
|
105
|
+
let certificatePrice = '';
|
|
106
|
+
|
|
107
|
+
if (courseRun.offer) {
|
|
108
|
+
const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
|
|
109
|
+
courseOfferMessage = {
|
|
110
|
+
PAID: messages.coursePaidOffer,
|
|
111
|
+
FREE: messages.courseFreeOffer,
|
|
112
|
+
PARTIALLY_FREE: messages.coursePartiallyFree,
|
|
113
|
+
SUBSCRIPTION: messages.courseSubscriptionOffer,
|
|
114
|
+
}[offer];
|
|
115
|
+
|
|
116
|
+
if ((courseRun.price ?? -1) >= 0) {
|
|
117
|
+
enrollmentPrice = intl.formatNumber(courseRun.price!, {
|
|
118
|
+
style: 'currency',
|
|
119
|
+
currency: courseRun.price_currency,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (courseRun.certificate_offer) {
|
|
125
|
+
const certificationOffer = courseRun.certificate_offer.toUpperCase().replaceAll(' ', '');
|
|
126
|
+
certificationOfferMessage = {
|
|
127
|
+
PAID: messages.certificatePaidOffer,
|
|
128
|
+
FREE: messages.certificateFreeOffer,
|
|
129
|
+
SUBSCRIPTION: messages.certificateSubscriptionOffer,
|
|
130
|
+
}[certificationOffer];
|
|
131
|
+
|
|
132
|
+
if ((courseRun.certificate_price ?? -1) >= 0) {
|
|
133
|
+
certificatePrice = intl.formatNumber(courseRun.certificate_price!, {
|
|
134
|
+
style: 'currency',
|
|
135
|
+
currency: courseRun.price_currency,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
57
140
|
return (
|
|
58
141
|
<>
|
|
59
142
|
{courseRun.title && <h3>{StringHelper.capitalizeFirst(courseRun.title)}</h3>}
|
|
@@ -83,6 +166,30 @@ const OpenedSelfPacedCourseRun = ({
|
|
|
83
166
|
<dd>{IntlHelper.getLocalizedLanguages(courseRun.languages, intl)}</dd>
|
|
84
167
|
</>
|
|
85
168
|
)}
|
|
169
|
+
{courseOfferMessage && (
|
|
170
|
+
<>
|
|
171
|
+
<dt>
|
|
172
|
+
<FormattedMessage {...messages.coursePrice} />
|
|
173
|
+
</dt>
|
|
174
|
+
<dd>
|
|
175
|
+
<FormattedMessage {...courseOfferMessage} />
|
|
176
|
+
<br />
|
|
177
|
+
{enrollmentPrice}
|
|
178
|
+
</dd>
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
{certificationOfferMessage && (
|
|
182
|
+
<>
|
|
183
|
+
<dt>
|
|
184
|
+
<FormattedMessage {...messages.certificationPrice} />
|
|
185
|
+
</dt>
|
|
186
|
+
<dd>
|
|
187
|
+
<FormattedMessage {...certificationOfferMessage} />
|
|
188
|
+
<br />
|
|
189
|
+
{certificatePrice}
|
|
190
|
+
</dd>
|
|
191
|
+
</>
|
|
192
|
+
)}
|
|
86
193
|
</dl>
|
|
87
194
|
{findLmsBackend(courseRun.resource_link) ? (
|
|
88
195
|
<CourseRunEnrollment courseRun={courseRun} />
|