richie-education 2.34.1-dev4 → 2.34.1-dev44
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/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/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/package.json +43 -43
- package/scss/colors/_theme.scss +20 -3
- 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/objects/_course_glimpses.scss +103 -7
- package/scss/objects/_selector.scss +1 -0
- package/scss/settings/_variables.scss +4 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ButtonElement, Input } from '@openfun/cunningham-react';
|
|
1
|
+
import { ButtonElement, Input, Alert, VariantType } from '@openfun/cunningham-react';
|
|
2
2
|
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
|
3
3
|
import { FormProvider, useForm } from 'react-hook-form';
|
|
4
4
|
import * as Yup from 'yup';
|
|
@@ -8,7 +8,6 @@ import { useSession } from 'contexts/SessionContext';
|
|
|
8
8
|
import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
|
|
9
9
|
import Form, { getLocalizedCunninghamErrorProp } from 'components/Form';
|
|
10
10
|
import { Spinner } from 'components/Spinner';
|
|
11
|
-
import Banner, { BannerType } from 'components/Banner';
|
|
12
11
|
import { useSaleTunnelContext } from 'components/SaleTunnel/GenericSaleTunnel';
|
|
13
12
|
|
|
14
13
|
const messages = defineMessages({
|
|
@@ -19,14 +18,15 @@ const messages = defineMessages({
|
|
|
19
18
|
},
|
|
20
19
|
fullNameInputLabel: {
|
|
21
20
|
id: 'components.OpenEdxFullNameForm.fullNameInputLabel',
|
|
22
|
-
description: 'Label of "
|
|
23
|
-
defaultMessage: '
|
|
21
|
+
description: 'Label of "First name and last name" field of the openEdx full name form',
|
|
22
|
+
defaultMessage: 'First name and last name',
|
|
24
23
|
},
|
|
25
24
|
fullNameInputDescription: {
|
|
26
25
|
id: 'components.OpenEdxFullNameForm.fullNameInputDescription',
|
|
27
|
-
description:
|
|
26
|
+
description:
|
|
27
|
+
'Description about the "First name and last name" field of the openEdx full name form.',
|
|
28
28
|
defaultMessage:
|
|
29
|
-
'Please check that your
|
|
29
|
+
'Please check that your first name and last name are correct. They will be used on official document (e.g: certificate, contract, etc.)',
|
|
30
30
|
},
|
|
31
31
|
submitButtonLabel: {
|
|
32
32
|
id: 'components.OpenEdxFullNameForm.submitButtonLabel',
|
|
@@ -128,27 +128,24 @@ const OpenEdxFullNameForm = () => {
|
|
|
128
128
|
|
|
129
129
|
if (error) {
|
|
130
130
|
// display get error message
|
|
131
|
-
return <
|
|
131
|
+
return <Alert type={VariantType.ERROR}>{error}</Alert>;
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
return (
|
|
135
135
|
<FormProvider {...form}>
|
|
136
136
|
<Form name="openedx-fullname-form" noValidate>
|
|
137
|
+
<Alert type={formState.errors.name?.message ? VariantType.ERROR : VariantType.WARNING}>
|
|
138
|
+
<FormattedMessage {...messages.fullNameInputDescription} />
|
|
139
|
+
</Alert>
|
|
137
140
|
<Input
|
|
138
141
|
{...register('name')}
|
|
139
|
-
className="form-field"
|
|
142
|
+
className="form-field mt-s"
|
|
140
143
|
required
|
|
141
144
|
fullWidth
|
|
142
145
|
label={intl.formatMessage(messages.fullNameInputLabel)}
|
|
143
146
|
value={formState.defaultValues?.name}
|
|
144
|
-
state={
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
text={
|
|
148
|
-
error ||
|
|
149
|
-
getLocalizedCunninghamErrorProp(intl, formState.errors.name?.message).text ||
|
|
150
|
-
intl.formatMessage(messages.fullNameInputDescription)
|
|
151
|
-
}
|
|
147
|
+
state={error || formState.errors.name?.message ? 'error' : 'default'}
|
|
148
|
+
text={error || getLocalizedCunninghamErrorProp(intl, formState.errors.name?.message).text}
|
|
152
149
|
/>
|
|
153
150
|
</Form>
|
|
154
151
|
</FormProvider>
|
|
@@ -184,7 +184,7 @@ describe('SaleTunnel', () => {
|
|
|
184
184
|
*/
|
|
185
185
|
screen.getByText('Information');
|
|
186
186
|
|
|
187
|
-
const nameInput = screen.getByLabelText('
|
|
187
|
+
const nameInput = screen.getByLabelText('First name and last name');
|
|
188
188
|
await user.type(nameInput, 'John Doe');
|
|
189
189
|
|
|
190
190
|
/**
|
package/js/types/Course.ts
CHANGED
|
@@ -3,6 +3,19 @@ import { Resource } from 'types/Resource';
|
|
|
3
3
|
import { Nullable } from 'types/utils';
|
|
4
4
|
import { CourseListItem as JoanieCourse } from 'types/Joanie';
|
|
5
5
|
|
|
6
|
+
export enum CourseOffer {
|
|
7
|
+
PAID = 'paid',
|
|
8
|
+
FREE = 'free',
|
|
9
|
+
PARTIALLY_FREE = 'partially_free',
|
|
10
|
+
SUBSCRIPTION = 'subscription',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export enum CourseCertificateOffer {
|
|
14
|
+
PAID = 'paid',
|
|
15
|
+
FREE = 'free',
|
|
16
|
+
SUBSCRIPTION = 'subscription',
|
|
17
|
+
}
|
|
18
|
+
|
|
6
19
|
export interface Course extends Resource {
|
|
7
20
|
absolute_url: string;
|
|
8
21
|
categories: string[];
|
|
@@ -29,6 +42,11 @@ export interface Course extends Resource {
|
|
|
29
42
|
}>;
|
|
30
43
|
organizations: string[];
|
|
31
44
|
state: CourseState;
|
|
45
|
+
certificate_offer: Nullable<CourseCertificateOffer>;
|
|
46
|
+
offer: Nullable<CourseOffer>;
|
|
47
|
+
certificate_price: Nullable<number>;
|
|
48
|
+
price: Nullable<number>;
|
|
49
|
+
price_currency: string;
|
|
32
50
|
}
|
|
33
51
|
|
|
34
52
|
export function isRichieCourse(course: Course | JoanieCourse): course is Course {
|
package/js/types/index.ts
CHANGED
|
@@ -35,6 +35,11 @@ export interface CourseRun {
|
|
|
35
35
|
title?: string;
|
|
36
36
|
snapshot?: string;
|
|
37
37
|
display_mode: CourseRunDisplayMode;
|
|
38
|
+
price?: number;
|
|
39
|
+
price_currency?: string;
|
|
40
|
+
offer?: string;
|
|
41
|
+
certificate_price?: number;
|
|
42
|
+
certificate_offer?: string;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
export enum Priority {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { waitFor } from '@testing-library/react';
|
|
2
|
+
import { VariantType } from '@openfun/cunningham-react';
|
|
3
|
+
|
|
4
|
+
export const expectAlertError = (message: string, rootElement: ParentNode = document) => {
|
|
5
|
+
return expectAlert(VariantType.ERROR, message, rootElement);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const expectAlertInfo = (message: string, rootElement: ParentNode = document) => {
|
|
9
|
+
return expectAlert(VariantType.INFO, message, rootElement);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const expectAlertSuccess = (message: string, rootElement: ParentNode = document) => {
|
|
13
|
+
return expectAlert(VariantType.SUCCESS, message, rootElement);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const expectAlertWarning = (message: string, rootElement: ParentNode = document) => {
|
|
17
|
+
return expectAlert(VariantType.WARNING, message, rootElement);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const expectAlert = (
|
|
21
|
+
type: VariantType,
|
|
22
|
+
message: string,
|
|
23
|
+
rootElement: ParentNode = document,
|
|
24
|
+
) => {
|
|
25
|
+
return waitFor(async () => {
|
|
26
|
+
// Cunningham Alert has a class with the alert variant type
|
|
27
|
+
const alert = rootElement.querySelector(`.c__alert--${type}`) as HTMLElement;
|
|
28
|
+
expect(alert).not.toBeNull();
|
|
29
|
+
expect(alert).toHaveTextContent(message);
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const expectNoAlert = (
|
|
34
|
+
type: VariantType,
|
|
35
|
+
message: string,
|
|
36
|
+
rootElement: ParentNode = document,
|
|
37
|
+
) => {
|
|
38
|
+
return waitFor(() => {
|
|
39
|
+
// Check that no alert exists with this message and type
|
|
40
|
+
const alerts = rootElement.querySelectorAll('.c__alert');
|
|
41
|
+
const matchingAlert = Array.from(alerts).find(
|
|
42
|
+
(alert) =>
|
|
43
|
+
alert.classList.contains(`c__alert--${type}`) && alert.textContent?.includes(message),
|
|
44
|
+
);
|
|
45
|
+
expect(matchingAlert).toBeUndefined();
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const expectNoAlertError = (message: string, rootElement: ParentNode = document) => {
|
|
50
|
+
return expectNoAlert(VariantType.ERROR, message, rootElement);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const expectNoAlertInfo = (message: string, rootElement: ParentNode = document) => {
|
|
54
|
+
return expectNoAlert(VariantType.INFO, message, rootElement);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const expectNoAlertSuccess = (message: string, rootElement: ParentNode = document) => {
|
|
58
|
+
return expectNoAlert(VariantType.SUCCESS, message, rootElement);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const expectNoAlertWarning = (message: string, rootElement: ParentNode = document) => {
|
|
62
|
+
return expectNoAlert(VariantType.WARNING, message, rootElement);
|
|
63
|
+
};
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
PacedCourse,
|
|
11
11
|
Priority,
|
|
12
12
|
} from 'types';
|
|
13
|
-
import { Course } from 'types/Course';
|
|
13
|
+
import { CourseCertificateOffer, Course, CourseOffer } from 'types/Course';
|
|
14
14
|
import { FactoryHelper } from 'utils/test/factories/helper';
|
|
15
15
|
import { factory } from './factories';
|
|
16
16
|
|
|
@@ -45,7 +45,27 @@ export const CourseStateFutureOpenFactory = factory<CourseState>(() => {
|
|
|
45
45
|
};
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
+
enum OfferType {
|
|
49
|
+
PAID = 'PAID',
|
|
50
|
+
FREE = 'FREE',
|
|
51
|
+
PARTIALLY_FREE = 'PARTIALLY_FREE',
|
|
52
|
+
SUBSCRIPTION = 'SUBSCRIPTION',
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
export const CourseRunFactory = factory<CourseRun>(() => {
|
|
56
|
+
const offerValues = Object.values(OfferType);
|
|
57
|
+
const offer = offerValues[Math.floor(Math.random() * offerValues.length)];
|
|
58
|
+
const certificateOfferValues = [OfferType.PAID, OfferType.FREE, OfferType.SUBSCRIPTION];
|
|
59
|
+
const certificateOffer =
|
|
60
|
+
certificateOfferValues[Math.floor(Math.random() * certificateOfferValues.length)];
|
|
61
|
+
const currency = faker.finance.currency().code;
|
|
62
|
+
const price = [OfferType.FREE, OfferType.PARTIALLY_FREE].includes(offer)
|
|
63
|
+
? 0
|
|
64
|
+
: parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
|
|
65
|
+
const certificatePrice =
|
|
66
|
+
certificateOffer === OfferType.FREE
|
|
67
|
+
? 0
|
|
68
|
+
: parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
|
|
49
69
|
return {
|
|
50
70
|
id: faker.number.int(),
|
|
51
71
|
resource_link: FactoryHelper.unique(faker.internet.url),
|
|
@@ -58,6 +78,11 @@ export const CourseRunFactory = factory<CourseRun>(() => {
|
|
|
58
78
|
dashboard_link: null,
|
|
59
79
|
title: faker.lorem.sentence(3),
|
|
60
80
|
display_mode: CourseRunDisplayMode.DETAILED,
|
|
81
|
+
price,
|
|
82
|
+
price_currency: currency,
|
|
83
|
+
offer,
|
|
84
|
+
certificate_price: certificatePrice,
|
|
85
|
+
certificate_offer: certificateOffer,
|
|
61
86
|
};
|
|
62
87
|
});
|
|
63
88
|
|
|
@@ -214,5 +239,10 @@ export const CourseLightFactory = factory<Course>(() => {
|
|
|
214
239
|
},
|
|
215
240
|
organizations: [organizationName],
|
|
216
241
|
state: CourseStateFactory().one(),
|
|
242
|
+
certificate_offer: CourseCertificateOffer.FREE,
|
|
243
|
+
offer: CourseOffer.FREE,
|
|
244
|
+
certificate_price: null,
|
|
245
|
+
price: null,
|
|
246
|
+
price_currency: 'EUR',
|
|
217
247
|
};
|
|
218
248
|
});
|
|
@@ -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} />
|