richie-education 3.1.3-dev3 → 3.1.3-dev30

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.
Files changed (93) hide show
  1. package/.storybook/__mocks__/utils/context.ts +4 -0
  2. package/js/api/joanie.ts +8 -8
  3. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
  4. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  5. package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
  6. package/js/components/CourseGlimpse/index.spec.tsx +18 -0
  7. package/js/components/CourseGlimpse/index.stories.tsx +75 -4
  8. package/js/components/CourseGlimpse/index.tsx +4 -0
  9. package/js/components/CourseGlimpse/utils.ts +35 -30
  10. package/js/components/CourseGlimpseList/utils.ts +2 -2
  11. package/js/components/PurchaseButton/index.tsx +3 -3
  12. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
  13. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +13 -1
  14. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
  15. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
  16. package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
  17. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  18. package/js/components/SaleTunnel/index.spec.tsx +171 -29
  19. package/js/components/SaleTunnel/index.stories.tsx +17 -3
  20. package/js/components/SaleTunnel/index.tsx +2 -2
  21. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  22. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  23. package/js/hooks/useContractArchive/index.ts +3 -3
  24. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  25. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  26. package/js/hooks/useCourseProducts.ts +4 -8
  27. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  28. package/js/hooks/useOffering/index.ts +32 -0
  29. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  30. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  31. package/js/pages/DashboardCourses/index.spec.tsx +14 -14
  32. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
  33. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
  34. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  35. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  36. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  37. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  38. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  39. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  40. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  42. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  43. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  44. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
  45. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  46. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  47. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
  48. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  49. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  50. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
  51. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  52. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  53. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  54. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  55. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  56. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  57. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  58. package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
  59. package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
  60. package/js/types/Course.ts +4 -0
  61. package/js/types/Joanie.ts +36 -29
  62. package/js/types/index.ts +6 -2
  63. package/js/utils/ProductHelper/index.ts +1 -5
  64. package/js/utils/test/factories/joanie.ts +19 -25
  65. package/js/utils/test/factories/richie.ts +10 -2
  66. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  67. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  68. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  69. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  70. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  71. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  72. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  73. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  74. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  75. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
  76. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
  77. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  78. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  79. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  80. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +19 -34
  81. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +35 -8
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
  83. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
  84. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
  85. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
  86. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
  87. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
  88. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +14 -0
  89. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +14 -0
  90. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +54 -8
  91. package/package.json +1 -1
  92. package/scss/objects/_course_glimpses.scss +16 -0
  93. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -1,7 +1,7 @@
1
1
  import { Children, useEffect, useMemo } from 'react';
2
2
  import { defineMessages, FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
3
3
  import c from 'classnames';
4
- import { ProductType, Product, CredentialOrder } from 'types/Joanie';
4
+ import { Offering, CredentialOrder, Product, ProductType } from 'types/Joanie';
5
5
  import { useCourseProduct } from 'hooks/useCourseProducts';
6
6
  import { Spinner } from 'components/Spinner';
7
7
  import { Icon, IconTypeEnum } from 'components/Icon';
@@ -38,6 +38,31 @@ const messages = defineMessages({
38
38
  description: 'Course run languages',
39
39
  id: 'components.CourseProductItem.availableIn',
40
40
  },
41
+ original_price: {
42
+ defaultMessage: 'Original price:',
43
+ description: 'Label for the original price of a product',
44
+ id: 'components.CourseProductItem.original_price',
45
+ },
46
+ discounted_price: {
47
+ defaultMessage: 'Discounted price:',
48
+ description: 'Label for the discounted price of a product',
49
+ id: 'components.CourseProductItem.discounted_price',
50
+ },
51
+ discount_rate: {
52
+ defaultMessage: '-{rate}%',
53
+ description: 'Discount rate information',
54
+ id: 'components.CourseProductItem.discount_rate',
55
+ },
56
+ from: {
57
+ defaultMessage: 'from {from}',
58
+ description: 'Discount start date information',
59
+ id: 'components.CourseProductItem.from',
60
+ },
61
+ to: {
62
+ defaultMessage: 'to {to}',
63
+ description: 'Discount end date information',
64
+ id: 'components.CourseProductItem.to',
65
+ },
41
66
  });
42
67
 
43
68
  export interface CourseProductItemProps {
@@ -52,8 +77,9 @@ type HeaderProps = {
52
77
  canPurchase: boolean;
53
78
  order: Maybe<CredentialOrder>;
54
79
  product: Product;
80
+ offering: Offering;
55
81
  };
56
- const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderProps) => {
82
+ const Header = ({ product, order, offering, hasPurchased, canPurchase, compact }: HeaderProps) => {
57
83
  const intl = useIntl();
58
84
  const formatDate = useDateFormat();
59
85
 
@@ -72,21 +98,90 @@ const Header = ({ product, order, hasPurchased, canPurchase, compact }: HeaderPr
72
98
  return ProductHelper.getLanguages(product, true, intl);
73
99
  }, [canShowMetadata, product, intl]);
74
100
 
75
- return (
76
- <header className="product-widget__header">
77
- <div className="product-widget__header-main">
78
- <h3 className="product-widget__title">{product.title}</h3>
79
- <strong className="product-widget__price h6">
80
- {hasPurchased && <FormattedMessage {...messages.purchased} />}
81
- {canPurchase && (
101
+ const displayPrice = useMemo(() => {
102
+ if (!canPurchase) {
103
+ return null;
104
+ }
105
+
106
+ if (offering.rules.discounted_price != null) {
107
+ return (
108
+ <>
109
+ <span id="original-price" className="offscreen">
110
+ <FormattedMessage {...messages.original_price} />
111
+ </span>
112
+ <del aria-describedby="original-price" className="product-widget__price-discounted">
82
113
  <FormattedNumber
83
114
  currency={product.price_currency}
84
115
  value={product.price}
85
116
  style="currency"
86
117
  />
87
- )}
88
- </strong>
118
+ </del>
119
+ <span id="discount-price" className="offscreen">
120
+ <FormattedMessage {...messages.discounted_price} />
121
+ </span>
122
+ <ins aria-describedby="discount-price" className="product-widget__price-discount">
123
+ <FormattedNumber
124
+ currency={product.price_currency}
125
+ value={offering.rules.discounted_price}
126
+ style="currency"
127
+ />
128
+ </ins>
129
+ </>
130
+ );
131
+ }
132
+
133
+ return (
134
+ <FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
135
+ );
136
+ }, [canPurchase, offering.rules.discounted_price, product.price]);
137
+
138
+ return (
139
+ <header className="product-widget__header">
140
+ <div className="product-widget__header-main">
141
+ <h3 className="product-widget__title">{product.title}</h3>
89
142
  </div>
143
+ <strong className="product-widget__price h6">
144
+ {hasPurchased && <FormattedMessage {...messages.purchased} />}
145
+ {displayPrice}
146
+ </strong>
147
+ {offering?.rules.description && (
148
+ <p className="product-widget__header-description">{offering.rules.description}</p>
149
+ )}
150
+ {offering?.rules.discounted_price && (
151
+ <p className="product-widget__header-discount">
152
+ {offering.rules.discount_rate ? (
153
+ <span className="product-widget__header-discount-rate">
154
+ <FormattedNumber value={-offering.rules.discount_rate} style="percent" />
155
+ </span>
156
+ ) : (
157
+ <span className="product-widget__header-discount-amount">
158
+ <FormattedNumber
159
+ currency={product.price_currency}
160
+ value={-offering.rules.discount_amount!}
161
+ style="currency"
162
+ />
163
+ </span>
164
+ )}
165
+ {offering.rules.discount_start && (
166
+ <span className="product-widget__header-discount-date">
167
+ &nbsp;
168
+ <FormattedMessage
169
+ {...messages.from}
170
+ values={{ from: formatDate(offering.rules.discount_start) }}
171
+ />
172
+ </span>
173
+ )}
174
+ {offering.rules.discount_end && (
175
+ <span className="product-widget__header-discount-date">
176
+ &nbsp;
177
+ <FormattedMessage
178
+ {...messages.to}
179
+ values={{ to: formatDate(offering.rules.discount_end) }}
180
+ />
181
+ </span>
182
+ )}
183
+ </p>
184
+ )}
90
185
  {canShowMetadata && (
91
186
  <>
92
187
  <p
@@ -131,7 +226,7 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
131
226
  <ol className="product-widget__content">
132
227
  {Children.toArray(
133
228
  targetCourses.map((target_course) => (
134
- <CourseRunItem targetCourse={target_course} order={order} />
229
+ <CourseRunItem key={target_course.code} targetCourse={target_course} order={order} />
135
230
  )),
136
231
  )}
137
232
  {product.certificate_definition && (
@@ -144,12 +239,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
144
239
  const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
145
240
  // FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
146
241
  // CredentialOrder
147
- const { item: courseProductRelation, states: productQueryStates } = useCourseProduct({
242
+ const { item: offering, states: productQueryStates } = useCourseProduct({
148
243
  product_id: productId,
149
244
  course_id: course.code,
150
245
  });
151
246
 
152
- const product = courseProductRelation?.product;
247
+ const product = offering?.product;
153
248
  const { item: productOrder, states: orderQueryStates } = useProductOrder({
154
249
  productId,
155
250
  courseCode: course.code,
@@ -179,13 +274,6 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
179
274
  return null;
180
275
  }
181
276
 
182
- const orderGroups = courseProductRelation
183
- ? ProductHelper.getActiveOrderGroups(courseProductRelation)
184
- : [];
185
- const orderGroupsAvailable = orderGroups.filter(
186
- (orderGroup) => orderGroup.nb_available_seats > 0,
187
- );
188
-
189
277
  return (
190
278
  <section
191
279
  className={c('product-widget', {
@@ -214,6 +302,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
214
302
  <Header
215
303
  product={product}
216
304
  order={order}
305
+ offering={offering}
217
306
  canPurchase={canPurchase}
218
307
  hasPurchased={hasPurchased}
219
308
  compact={compact}
@@ -222,9 +311,7 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
222
311
  <footer className="product-widget__footer">
223
312
  <CourseProductItemFooter
224
313
  course={course}
225
- courseProductRelation={courseProductRelation}
226
- orderGroups={orderGroups}
227
- orderGroupsAvailable={orderGroupsAvailable}
314
+ offering={offering}
228
315
  canPurchase={canPurchase}
229
316
  />
230
317
  </footer>
@@ -0,0 +1,81 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { CourseRunFactory, PacedCourseFactory } from 'utils/test/factories/richie';
3
+ import { StorybookHelper } from 'utils/StorybookHelper';
4
+ import { CourseCertificateOffer, CourseOffer } from '../../../../types/Course';
5
+ import { SyllabusCourseRun } from '.';
6
+
7
+ export default {
8
+ component: SyllabusCourseRun,
9
+ render: (args) => {
10
+ return StorybookHelper.wrapInApp(<SyllabusCourseRun {...args} />);
11
+ },
12
+ } as Meta<typeof SyllabusCourseRun>;
13
+
14
+ type Story = StoryObj<typeof SyllabusCourseRun>;
15
+
16
+ const courseRun = CourseRunFactory().one();
17
+
18
+ export const certificateSyllabusCourseRun: Story = {
19
+ args: {
20
+ courseRun: {
21
+ ...courseRun,
22
+ title: 'Certificate Product',
23
+ price_currency: 'EUR',
24
+ offer: CourseOffer.FREE,
25
+ certificate_offer: CourseCertificateOffer.PAID,
26
+ certificate_price: 100,
27
+ discounted_price: null,
28
+ discount: null,
29
+ },
30
+ course: PacedCourseFactory().one(),
31
+ showLanguages: true,
32
+ },
33
+ };
34
+ export const certificateDiscountSyllabusCourseRun: Story = {
35
+ args: {
36
+ courseRun: {
37
+ ...courseRun,
38
+ title: 'Certificate Product',
39
+ price_currency: 'EUR',
40
+ offer: CourseOffer.FREE,
41
+ certificate_offer: CourseCertificateOffer.PAID,
42
+ certificate_price: 100,
43
+ certificate_discounted_price: 80,
44
+ certificate_discount: '-20 €',
45
+ },
46
+ course: PacedCourseFactory().one(),
47
+ showLanguages: true,
48
+ },
49
+ };
50
+ export const credentialSyllabusCourseRun: Story = {
51
+ args: {
52
+ courseRun: {
53
+ ...courseRun,
54
+ title: 'Certificate Product',
55
+ price_currency: 'EUR',
56
+ offer: CourseOffer.PAID,
57
+ price: 100,
58
+ certificate_offer: CourseCertificateOffer.FREE,
59
+ discounted_price: null,
60
+ discount: null,
61
+ },
62
+ course: PacedCourseFactory().one(),
63
+ showLanguages: true,
64
+ },
65
+ };
66
+ export const credentialDiscountSyllabusCourseRun: Story = {
67
+ args: {
68
+ courseRun: {
69
+ ...courseRun,
70
+ title: 'Certificate Product',
71
+ price_currency: 'EUR',
72
+ offer: CourseOffer.PAID,
73
+ price: 100,
74
+ certificate_offer: CourseCertificateOffer.FREE,
75
+ discounted_price: 80,
76
+ discount: '-20 €',
77
+ },
78
+ course: PacedCourseFactory().one(),
79
+ showLanguages: true,
80
+ },
81
+ };
@@ -128,6 +128,13 @@ const OpenedCourseRun = ({
128
128
  currency: courseRun.price_currency,
129
129
  });
130
130
  }
131
+
132
+ if ((courseRun.discounted_price ?? -1) >= 0) {
133
+ enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
134
+ style: 'currency',
135
+ currency: courseRun.price_currency,
136
+ });
137
+ }
131
138
  }
132
139
 
133
140
  if (courseRun.certificate_offer) {
@@ -144,6 +151,13 @@ const OpenedCourseRun = ({
144
151
  currency: courseRun.price_currency,
145
152
  });
146
153
  }
154
+
155
+ if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
156
+ certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
157
+ style: 'currency',
158
+ currency: courseRun.price_currency,
159
+ });
160
+ }
147
161
  }
148
162
 
149
163
  return (
@@ -119,6 +119,13 @@ const OpenedSelfPacedCourseRun = ({
119
119
  currency: courseRun.price_currency,
120
120
  });
121
121
  }
122
+
123
+ if ((courseRun.discounted_price ?? -1) >= 0) {
124
+ enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
125
+ style: 'currency',
126
+ currency: courseRun.price_currency,
127
+ });
128
+ }
122
129
  }
123
130
 
124
131
  if (courseRun.certificate_offer) {
@@ -135,6 +142,13 @@ const OpenedSelfPacedCourseRun = ({
135
142
  currency: courseRun.price_currency,
136
143
  });
137
144
  }
145
+
146
+ if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
147
+ certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
148
+ style: 'currency',
149
+ currency: courseRun.price_currency,
150
+ });
151
+ }
138
152
  }
139
153
 
140
154
  return (
@@ -22,8 +22,8 @@ import {
22
22
  import SyllabusCourseRunsList from 'widgets/SyllabusCourseRunsList/index';
23
23
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
24
24
  import { CourseRun, Priority } from 'types';
25
- import { CourseProductRelation } from 'types/Joanie';
26
- import { CourseProductRelationFactory } from 'utils/test/factories/joanie';
25
+ import { Offering } from 'types/Joanie';
26
+ import { OfferingFactory } from 'utils/test/factories/joanie';
27
27
  import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
28
28
  import { StringHelper } from 'utils/StringHelper';
29
29
  import { computeStates } from 'utils/CourseRuns';
@@ -211,9 +211,9 @@ describe('<SyllabusCourseRunsList/>', () => {
211
211
  });
212
212
  };
213
213
 
214
- const expectCourseProduct = async (container: HTMLElement, relation: CourseProductRelation) => {
214
+ const expectCourseProduct = async (container: HTMLElement, offering: Offering) => {
215
215
  const heading = await findByRole(container, 'heading', {
216
- name: relation.product.title,
216
+ name: offering.product.title,
217
217
  });
218
218
  expect(Array.from(heading.classList)).toContain('product-widget__title');
219
219
  };
@@ -383,9 +383,9 @@ describe('<SyllabusCourseRunsList/>', () => {
383
383
 
384
384
  it('has one opened product', async () => {
385
385
  const course = PacedCourseFactory().one();
386
- const relation = CourseProductRelationFactory().one();
387
- const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${relation.product.id}/`;
388
- fetchMock.get(resourceLink, relation);
386
+ const offering = OfferingFactory().one();
387
+ const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offering.product.id}/`;
388
+ fetchMock.get(resourceLink, offering);
389
389
 
390
390
  const courseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
391
391
  resource_link: resourceLink,
@@ -406,7 +406,7 @@ describe('<SyllabusCourseRunsList/>', () => {
406
406
  expect(getHeaderContainer().querySelectorAll('.course-detail__run-descriptions').length).toBe(
407
407
  1,
408
408
  );
409
- await expectCourseProduct(getHeaderContainer(), relation);
409
+ await expectCourseProduct(getHeaderContainer(), offering);
410
410
 
411
411
  // Portal.
412
412
  expectEmptyPortalContainer();
@@ -1465,4 +1465,50 @@ describe('<SyllabusCourseRunsList/>', () => {
1465
1465
  expect(content).not.toContain('The certification process is');
1466
1466
  expect(content).not.toContain('<br>€59.99');
1467
1467
  });
1468
+
1469
+ it('renders price discount on SyllabusCourseRun', async () => {
1470
+ const course = PacedCourseFactory().one();
1471
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1472
+ languages: ['en'],
1473
+ price_currency: 'EUR',
1474
+ offer: 'paid',
1475
+ price: 49.99,
1476
+ certificate_offer: undefined,
1477
+ certificate_price: undefined,
1478
+ discounted_price: 30.0,
1479
+ discount: '-20%',
1480
+ }).one();
1481
+
1482
+ render(
1483
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1484
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1485
+ </div>,
1486
+ );
1487
+
1488
+ const content = getHeaderContainer().innerHTML;
1489
+ expect(content).toContain('<dd>Paid access<br>€30.00</dd>');
1490
+ });
1491
+
1492
+ it('renders certificate discount on SyllabusCourseRun', async () => {
1493
+ const course = PacedCourseFactory().one();
1494
+ const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
1495
+ languages: ['en'],
1496
+ price_currency: 'EUR',
1497
+ offer: 'free',
1498
+ price: undefined,
1499
+ certificate_offer: 'paid',
1500
+ certificate_price: 100.0,
1501
+ certificate_discounted_price: 70.0,
1502
+ certificate_discount: '-30%',
1503
+ }).one();
1504
+
1505
+ render(
1506
+ <div className="course-detail__row course-detail__runs course-detail__runs--open">
1507
+ <SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
1508
+ </div>,
1509
+ );
1510
+
1511
+ const content = getHeaderContainer().innerHTML;
1512
+ expect(content).toContain('<dd>Paid certificate<br>€70.00</dd>');
1513
+ });
1468
1514
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.1.3-dev3",
3
+ "version": "3.1.3-dev30",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -396,6 +396,12 @@ $course-glimpse-content-padding-sides: 0.7rem !default;
396
396
  }
397
397
  }
398
398
 
399
+ .offer_prices {
400
+ display: flex;
401
+ flex-direction: column;
402
+ align-items: flex-end;
403
+ }
404
+
399
405
  .offer__price {
400
406
  $visibility: r-theme-val(course-glimpse, offer-price-visibility);
401
407
  @if $visibility == hidden {
@@ -404,6 +410,16 @@ $course-glimpse-content-padding-sides: 0.7rem !default;
404
410
  visibility: $visibility;
405
411
  // Align vertically the price with the icon
406
412
  margin-top: calc(1ex - 1cap);
413
+
414
+ &--striked,
415
+ &--discounted {
416
+ display: inline-block;
417
+ }
418
+
419
+ &--striked {
420
+ text-decoration: line-through;
421
+ opacity: 0.5;
422
+ }
407
423
  }
408
424
  }
409
425
  }
@@ -1,44 +0,0 @@
1
- import { defineMessages } from 'react-intl';
2
- import { useJoanieApi } from 'contexts/JoanieApiContext';
3
- import { API, CourseProductRelation, CourseProductRelationQueryFilters } from 'types/Joanie';
4
- import { useResource, useResources, UseResourcesProps } from 'hooks/useResources';
5
-
6
- const messages = defineMessages({
7
- errorGet: {
8
- id: 'hooks.useCourseProductRelations.errorGet',
9
- description:
10
- 'Error message shown to the user when course product relation fetch request fails.',
11
- defaultMessage: 'An error occurred while fetching trainings. Please retry later.',
12
- },
13
- errorNotFound: {
14
- id: 'hooks.useCourseProductRelations.errorNotFound',
15
- description: 'Error message shown to the user when no course product relation matches.',
16
- defaultMessage: 'Cannot find the training.',
17
- },
18
- });
19
-
20
- /**
21
- * Joanie Api hook to retrieve/create/update/delete course
22
- * owned by the authenticated user.
23
- */
24
- const props: UseResourcesProps<
25
- CourseProductRelation,
26
- CourseProductRelationQueryFilters,
27
- API['courseProductRelations']
28
- > = {
29
- queryKey: ['courseProductRelations'],
30
- apiInterface: () => useJoanieApi().courseProductRelations,
31
- session: true,
32
- messages,
33
- };
34
-
35
- export const useCourseProductRelations = useResources<
36
- CourseProductRelation,
37
- CourseProductRelationQueryFilters,
38
- API['courseProductRelations']
39
- >(props);
40
-
41
- export const useCourseProductRelation = useResource<
42
- CourseProductRelation,
43
- CourseProductRelationQueryFilters
44
- >(props);