richie-education 3.1.3-dev8 → 3.2.1-dev1

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 (115) hide show
  1. package/.storybook/__mocks__/utils/context.ts +4 -0
  2. package/i18n/locales/ar-SA.json +30 -10
  3. package/i18n/locales/es-ES.json +30 -10
  4. package/i18n/locales/fa-IR.json +30 -10
  5. package/i18n/locales/fr-CA.json +31 -11
  6. package/i18n/locales/fr-FR.json +32 -12
  7. package/i18n/locales/ko-KR.json +30 -10
  8. package/i18n/locales/pt-PT.json +30 -10
  9. package/i18n/locales/ru-RU.json +30 -10
  10. package/i18n/locales/vi-VN.json +30 -10
  11. package/js/api/joanie.ts +8 -8
  12. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
  13. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  14. package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
  15. package/js/components/CourseGlimpse/index.spec.tsx +18 -0
  16. package/js/components/CourseGlimpse/index.stories.tsx +75 -4
  17. package/js/components/CourseGlimpse/index.tsx +4 -0
  18. package/js/components/CourseGlimpse/utils.ts +35 -30
  19. package/js/components/CourseGlimpseList/utils.ts +2 -2
  20. package/js/components/PurchaseButton/index.tsx +3 -3
  21. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +6 -3
  22. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
  23. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  24. package/js/components/SaleTunnel/index.spec.tsx +131 -64
  25. package/js/components/SaleTunnel/index.stories.tsx +17 -2
  26. package/js/components/SaleTunnel/index.tsx +2 -2
  27. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  28. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  29. package/js/hooks/useContractArchive/index.ts +3 -3
  30. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  31. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  32. package/js/hooks/useCourseProducts.ts +4 -8
  33. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  34. package/js/hooks/useOffering/index.ts +32 -0
  35. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  36. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  37. package/js/pages/DashboardCourses/index.spec.tsx +14 -14
  38. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
  39. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
  40. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  41. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  42. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  43. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  44. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  45. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  46. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  47. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  48. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  49. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  50. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
  51. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  52. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  53. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
  54. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  55. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  56. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
  57. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  58. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  59. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  60. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  61. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  62. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  63. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  64. package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
  65. package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
  66. package/js/settings/index.ts +1 -0
  67. package/js/settings/settings.prod.ts +1 -0
  68. package/js/translations/ar-SA.json +1 -1
  69. package/js/translations/es-ES.json +1 -1
  70. package/js/translations/fa-IR.json +1 -1
  71. package/js/translations/fr-CA.json +1 -1
  72. package/js/translations/fr-FR.json +1 -1
  73. package/js/translations/ko-KR.json +1 -1
  74. package/js/translations/pt-PT.json +1 -1
  75. package/js/translations/ru-RU.json +1 -1
  76. package/js/translations/vi-VN.json +1 -1
  77. package/js/types/Course.ts +4 -0
  78. package/js/types/Joanie.ts +31 -22
  79. package/js/types/index.ts +6 -2
  80. package/js/utils/test/factories/joanie.ts +18 -11
  81. package/js/utils/test/factories/richie.ts +10 -2
  82. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  83. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  84. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  85. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  86. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  87. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  88. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  89. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  90. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  91. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
  92. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
  93. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  94. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  95. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  96. package/js/widgets/Slider/components/SlidePanel.tsx +9 -0
  97. package/js/widgets/Slider/index.stories.tsx +53 -0
  98. package/js/widgets/Slider/index.tsx +21 -2
  99. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +10 -14
  100. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +8 -1
  101. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +2 -2
  102. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
  103. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +116 -75
  104. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
  105. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +29 -30
  106. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
  107. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +36 -2
  108. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +36 -2
  109. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +58 -8
  110. package/package.json +2 -1
  111. package/scss/colors/_theme.scss +3 -0
  112. package/scss/components/templates/richie/slider/_slider.scss +19 -0
  113. package/scss/objects/_blogpost_glimpses.scss +5 -0
  114. package/scss/objects/_course_glimpses.scss +16 -0
  115. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -67,15 +67,15 @@ export const DashboardItemOrder = ({
67
67
  }: DashboardItemOrderProps) => {
68
68
  const { course } = order;
69
69
  const intl = useIntl();
70
- const { item: courseProductRelation } = useCourseProduct({
70
+ const { item: offering } = useCourseProduct({
71
71
  product_id: order.product_id,
72
72
  course_id: course.code,
73
73
  });
74
- const { product } = courseProductRelation || {};
74
+ const { product } = offering || {};
75
75
  const needsSignature = OrderHelper.orderNeedsSignature(order);
76
76
  const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
77
77
  const isActive = OrderHelper.isActive(order);
78
- const isProductPurchasable = ProductHelper.isPurchasable(courseProductRelation?.product);
78
+ const isProductPurchasable = ProductHelper.isPurchasable(offering?.product);
79
79
  const isNotResumable = !isActive && !isProductPurchasable;
80
80
  const canEnroll = OrderHelper.allowEnrollment(order);
81
81
 
@@ -57,7 +57,7 @@ describe('<DashboardItemOrder/> Contract', () => {
57
57
  contract: ContractFactory({ student_signed_on: null }).one(),
58
58
  }).one();
59
59
 
60
- // learner dashboard course page do one call to course product relation per order
60
+ // learner dashboard course page do one call to offering per order
61
61
  const { product } = mockCourseProductWithOrder(order);
62
62
 
63
63
  // overwrite useOmniscientOrders call
@@ -94,7 +94,7 @@ const Installment = ({ order }: Props) => {
94
94
 
95
95
  const PaymentMethodManager = ({ order }: Props) => {
96
96
  const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
97
- const { item: relation, states } = useCourseProduct({
97
+ const { item: offering, states } = useCourseProduct({
98
98
  course_id: order.course.code,
99
99
  product_id: order.product_id,
100
100
  });
@@ -118,9 +118,9 @@ const PaymentMethodManager = ({ order }: Props) => {
118
118
  )}
119
119
  <SaleTunnel
120
120
  {...modal}
121
- product={relation.product as CredentialProduct}
122
- course={relation.course}
123
- isWithdrawable={relation.is_withdrawable}
121
+ product={offering.product as CredentialProduct}
122
+ course={offering.course}
123
+ isWithdrawable={offering.is_withdrawable}
124
124
  />
125
125
  </>
126
126
  );
@@ -25,6 +25,6 @@ export const enrollment: Enrollment = {
25
25
  },
26
26
  languages: ['en'],
27
27
  },
28
- product_relations: [],
28
+ offerings: [],
29
29
  certificate_id: null,
30
30
  };
@@ -28,7 +28,7 @@ describe('<ContractNavLink />', () => {
28
28
  fetchMock.get('https://joanie.endpoint/api/v1.0/organizations/', []);
29
29
  });
30
30
 
31
- it('should render a ContractNavLink with route and label when neither organizationId and courseProductRelationId are given', () => {
31
+ it('should render a ContractNavLink with route and label when neither organizationId and offeringId are given', () => {
32
32
  const link: MenuLink = {
33
33
  to: '/dummy/url/',
34
34
  label: 'My contract navigation link',
@@ -45,31 +45,31 @@ describe('<ContractNavLink />', () => {
45
45
  it.each([
46
46
  {
47
47
  organizationId: faker.string.uuid(),
48
- courseProductRelationId: undefined,
48
+ offeringId: undefined,
49
49
  },
50
50
  {
51
51
  organizationId: faker.string.uuid(),
52
- courseProductRelationId: faker.string.uuid(),
52
+ offeringId: faker.string.uuid(),
53
53
  },
54
54
  {
55
55
  organizationId: undefined,
56
- courseProductRelationId: faker.string.uuid(),
56
+ offeringId: faker.string.uuid(),
57
57
  },
58
58
  {
59
59
  organizationId: undefined,
60
- courseProductRelationId: undefined,
60
+ offeringId: undefined,
61
61
  },
62
62
  ])(
63
- 'should never render Badge for organizationId: $organizationId and courseProductId: $courseProductRelationId',
64
- async ({ organizationId, courseProductRelationId }) => {
63
+ 'should never render Badge for organizationId: $organizationId and courseProductId: $offeringId',
64
+ async ({ organizationId, offeringId }) => {
65
65
  let contractQueryParams: ContractResourceQuery = {
66
66
  signature_state: ContractState.LEARNER_SIGNED,
67
67
  page: 1,
68
68
  page_size: PER_PAGE.teacherContractList,
69
69
  };
70
- if (courseProductRelationId) {
70
+ if (offeringId) {
71
71
  contractQueryParams = {
72
- course_product_relation_id: courseProductRelationId,
72
+ offering_id: offeringId,
73
73
  ...contractQueryParams,
74
74
  };
75
75
  }
@@ -94,7 +94,7 @@ describe('<ContractNavLink />', () => {
94
94
  label: 'My contract navigation link',
95
95
  }}
96
96
  organizationId={organizationId}
97
- courseProductRelationId={courseProductRelationId}
97
+ offeringId={offeringId}
98
98
  />,
99
99
  );
100
100
 
@@ -112,25 +112,25 @@ describe('<ContractNavLink />', () => {
112
112
  // with 1 contracts to sign
113
113
  {
114
114
  organizationId: faker.string.uuid(),
115
- courseProductRelationId: undefined,
115
+ offeringId: undefined,
116
116
  nbContractsToSign: 1,
117
117
  expectedBadgeCount: 1,
118
118
  },
119
119
  {
120
120
  organizationId: faker.string.uuid(),
121
- courseProductRelationId: faker.string.uuid(),
121
+ offeringId: faker.string.uuid(),
122
122
  nbContractsToSign: 1,
123
123
  expectedBadgeCount: 1,
124
124
  },
125
125
  {
126
126
  organizationId: undefined,
127
- courseProductRelationId: faker.string.uuid(),
127
+ offeringId: faker.string.uuid(),
128
128
  nbContractsToSign: 1,
129
129
  expectedBadgeCount: undefined,
130
130
  },
131
131
  {
132
132
  organizationId: undefined,
133
- courseProductRelationId: undefined,
133
+ offeringId: undefined,
134
134
  nbContractsToSign: 1,
135
135
  expectedBadgeCount: undefined,
136
136
  },
@@ -138,44 +138,39 @@ describe('<ContractNavLink />', () => {
138
138
  // with 0 contracts to sign
139
139
  {
140
140
  organizationId: faker.string.uuid(),
141
- courseProductRelationId: undefined,
141
+ offeringId: undefined,
142
142
  nbContractsToSign: 0,
143
143
  expectedBadgeCount: undefined,
144
144
  },
145
145
  {
146
146
  organizationId: faker.string.uuid(),
147
- courseProductRelationId: faker.string.uuid(),
147
+ offeringId: faker.string.uuid(),
148
148
  nbContractsToSign: 0,
149
149
  expectedBadgeCount: undefined,
150
150
  },
151
151
  {
152
152
  organizationId: undefined,
153
- courseProductRelationId: faker.string.uuid(),
153
+ offeringId: faker.string.uuid(),
154
154
  nbContractsToSign: 0,
155
155
  expectedBadgeCount: undefined,
156
156
  },
157
157
  {
158
158
  organizationId: undefined,
159
- courseProductRelationId: undefined,
159
+ offeringId: undefined,
160
160
  nbContractsToSign: 0,
161
161
  expectedBadgeCount: undefined,
162
162
  },
163
163
  ])(
164
- 'should render Badge (count: $expectedBadgeCount) for nb contracts to sign: $nbContractsToSign, organizationId: $organizationId and courseProductId: $courseProductRelationId',
165
- async ({
166
- nbContractsToSign,
167
- organizationId,
168
- courseProductRelationId,
169
- expectedBadgeCount,
170
- }) => {
164
+ 'should render Badge (count: $expectedBadgeCount) for nb contracts to sign: $nbContractsToSign, organizationId: $organizationId and courseProductId: $offeringId',
165
+ async ({ nbContractsToSign, organizationId, offeringId, expectedBadgeCount }) => {
171
166
  let contractQueryParams: ContractResourceQuery = {
172
167
  signature_state: ContractState.LEARNER_SIGNED,
173
168
  page: 1,
174
169
  page_size: PER_PAGE.teacherContractList,
175
170
  };
176
- if (courseProductRelationId) {
171
+ if (offeringId) {
177
172
  contractQueryParams = {
178
- course_product_relation_id: courseProductRelationId,
173
+ offering_id: offeringId,
179
174
  ...contractQueryParams,
180
175
  };
181
176
  }
@@ -199,7 +194,7 @@ describe('<ContractNavLink />', () => {
199
194
  label: 'My contract navigation link',
200
195
  }}
201
196
  organizationId={organizationId}
202
- courseProductRelationId={courseProductRelationId}
197
+ offeringId={offeringId}
203
198
  />,
204
199
  );
205
200
 
@@ -1,7 +1,7 @@
1
1
  import { createSearchParams } from 'react-router';
2
2
  import { useMemo } from 'react';
3
3
  import { MenuLink } from 'widgets/Dashboard/components/DashboardSidebar';
4
- import { ContractState, CourseProductRelation, Organization } from 'types/Joanie';
4
+ import { ContractState, Offering, Organization } from 'types/Joanie';
5
5
  import useTeacherPendingContractsCount from 'hooks/useTeacherPendingContractsCount';
6
6
  import { ContractActions } from 'utils/AbilitiesHelper/types';
7
7
  import useContractAbilities from 'hooks/useContractAbilities';
@@ -11,18 +11,14 @@ import MenuNavLink from '../MenuNavLink';
11
11
  interface ContractNavLinkProps {
12
12
  link: MenuLink;
13
13
  organizationId?: Organization['id'];
14
- courseProductRelationId?: CourseProductRelation['id'];
14
+ offeringId?: Offering['id'];
15
15
  }
16
16
 
17
- const ContractNavLink = ({
18
- link,
19
- organizationId,
20
- courseProductRelationId,
21
- }: ContractNavLinkProps) => {
17
+ const ContractNavLink = ({ link, organizationId, offeringId }: ContractNavLinkProps) => {
22
18
  const defaultOrganizationId = useDefaultOrganizationId();
23
19
  const { contracts: pendingContracts, pendingContractCount } = useTeacherPendingContractsCount({
24
20
  organizationId: organizationId || defaultOrganizationId,
25
- courseProductRelationId,
21
+ offeringId,
26
22
  });
27
23
  const contractAbilities = useContractAbilities(pendingContracts);
28
24
  const canSignContracts = contractAbilities.can(ContractActions.SIGN);
@@ -4,11 +4,7 @@ import { createIntl } from 'react-intl';
4
4
  import { generatePath } from 'react-router';
5
5
  import { CourseListItem } from 'types/Joanie';
6
6
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
- import {
8
- CourseFactory,
9
- CourseProductRelationFactory,
10
- OrganizationFactory,
11
- } from 'utils/test/factories/joanie';
7
+ import { CourseFactory, OfferingFactory, OrganizationFactory } from 'utils/test/factories/joanie';
12
8
  import { expectNoSpinner } from 'utils/test/expectSpinner';
13
9
  import { render } from 'utils/test/render';
14
10
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
@@ -65,14 +61,14 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
65
61
  label: 'course',
66
62
  course: CourseFactory().one(),
67
63
  organization: undefined,
68
- courseProductRelation: undefined,
64
+ offering: undefined,
69
65
  expectedRoutes: [TeacherDashboardPaths.COURSE_GENERAL_INFORMATION],
70
66
  },
71
67
  {
72
68
  label: 'training',
73
69
  course: CourseFactory().one(),
74
70
  organization: undefined,
75
- courseProductRelation: CourseProductRelationFactory().one(),
71
+ offering: OfferingFactory().one(),
76
72
  expectedRoutes: [
77
73
  TeacherDashboardPaths.COURSE_PRODUCT,
78
74
  TeacherDashboardPaths.COURSE_PRODUCT_CONTRACTS,
@@ -83,14 +79,14 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
83
79
  label: "organization's course",
84
80
  course: CourseFactory().one(),
85
81
  organization: OrganizationFactory().one(),
86
- courseProductRelation: undefined,
82
+ offering: undefined,
87
83
  expectedRoutes: [TeacherDashboardPaths.ORGANIZATION_COURSE_GENERAL_INFORMATION],
88
84
  },
89
85
  {
90
86
  label: "organization's training",
91
87
  course: CourseFactory().one(),
92
88
  organization: OrganizationFactory().one(),
93
- courseProductRelation: CourseProductRelationFactory().one(),
89
+ offering: OfferingFactory().one(),
94
90
  expectedRoutes: [
95
91
  TeacherDashboardPaths.ORGANIZATION_PRODUCT,
96
92
  TeacherDashboardPaths.ORGANIZATION_PRODUCT_CONTRACTS,
@@ -99,20 +95,20 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
99
95
  },
100
96
  ])(
101
97
  'should display menu items for "$label" route',
102
- async ({ course, organization, courseProductRelation, expectedRoutes }) => {
98
+ async ({ course, organization, offering, expectedRoutes }) => {
103
99
  // mock api for organization's training
104
- if (organization && courseProductRelation) {
100
+ if (organization && offering) {
105
101
  // fetching training's contracts
106
102
  nbApiRequest += 1;
107
103
  fetchMock.get(
108
- `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=25`,
104
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?offering_id=${offering.id}&signature_state=half_signed&page=1&page_size=25`,
109
105
  [],
110
106
  );
111
107
  // fetching organization's training
112
108
  nbApiRequest += 1;
113
109
  fetchMock.get(
114
- `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/${courseProductRelation.id}/`,
115
- courseProductRelation,
110
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/${offering.id}/`,
111
+ offering,
116
112
  );
117
113
  } else if (organization) {
118
114
  // fetching organization's course
@@ -121,16 +117,13 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
121
117
  `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/courses/${course.id}/`,
122
118
  course,
123
119
  );
124
- } else if (courseProductRelation) {
120
+ } else if (offering) {
125
121
  // fetching training
126
122
  nbApiRequest += 1;
127
- fetchMock.get(
128
- `https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
129
- courseProductRelation,
130
- );
123
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/`, offering);
131
124
  nbApiRequest += 1;
132
125
  fetchMock.get(
133
- `https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
126
+ `https://joanie.endpoint/api/v1.0/organizations/?offering_id=${offering.id}`,
134
127
  [],
135
128
  );
136
129
  } else {
@@ -142,9 +135,9 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
142
135
  let routePath = '/:courseId';
143
136
  let initialEntry = `/${course.id}`;
144
137
 
145
- if (courseProductRelation) {
146
- routePath += '/:courseProductRelationId';
147
- initialEntry += `/${courseProductRelation.id}`;
138
+ if (offering) {
139
+ routePath += '/:offeringId';
140
+ initialEntry += `/${offering.id}`;
148
141
  }
149
142
  if (organization) {
150
143
  routePath = '/:organizationId' + routePath;
@@ -168,7 +161,7 @@ describe('<TeacherDashboardCourseSidebar/>', () => {
168
161
  generatePath(expectedRoute, {
169
162
  organizationId: organization ? organization.id : null,
170
163
  courseId: course.id,
171
- courseProductRelationId: courseProductRelation ? courseProductRelation.id : null,
164
+ offeringId: offering ? offering.id : null,
172
165
  }),
173
166
  );
174
167
  });
@@ -2,12 +2,12 @@ import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
2
2
  import { generatePath, useParams } from 'react-router';
3
3
  import { useMemo } from 'react';
4
4
  import { capitalize } from 'lodash-es';
5
+ import { useOffering } from 'hooks/useOffering';
5
6
  import { DashboardSidebar, MenuLink } from 'widgets/Dashboard/components/DashboardSidebar';
6
7
  import { getDashboardRouteLabel } from 'widgets/Dashboard/utils/dashboardRoutes';
7
8
  import { useCourse } from 'hooks/useCourses';
8
9
  import { Spinner } from 'components/Spinner';
9
10
  import { Icon, IconTypeEnum } from 'components/Icon';
10
- import { useCourseProductRelation } from 'hooks/useCourseProductRelation';
11
11
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
12
12
  import ContractNavLink from '../DashboardSidebar/components/ContractNavLink';
13
13
  import { getMenuRoutes } from './utils';
@@ -41,11 +41,11 @@ export const TeacherDashboardCourseSidebar = () => {
41
41
  const {
42
42
  organizationId: routeOrganizationId,
43
43
  courseId: routeCourseId,
44
- courseProductRelationId: routeCourseProductRelationId = '',
44
+ offeringId: routeOfferingId = '',
45
45
  } = useParams<{
46
46
  organizationId?: string;
47
47
  courseId: string;
48
- courseProductRelationId: string;
48
+ offeringId: string;
49
49
  }>();
50
50
 
51
51
  const {
@@ -54,38 +54,35 @@ export const TeacherDashboardCourseSidebar = () => {
54
54
  } = useCourse(
55
55
  routeCourseId,
56
56
  { organization_id: routeOrganizationId },
57
- { enabled: !routeCourseProductRelationId },
57
+ { enabled: !routeOfferingId },
58
58
  );
59
59
 
60
60
  const {
61
- item: courseProductRelation,
62
- states: { fetching: courseProductRelationFetching },
63
- } = useCourseProductRelation(
64
- routeCourseProductRelationId,
61
+ item: offering,
62
+ states: { fetching: offeringFetching },
63
+ } = useOffering(
64
+ routeOfferingId,
65
65
  {
66
66
  organization_id: routeOrganizationId,
67
67
  },
68
- { enabled: !!routeCourseProductRelationId },
68
+ { enabled: !!routeOfferingId },
69
69
  );
70
70
 
71
71
  const fetching = useMemo(
72
- () => courseFetching || courseProductRelationFetching,
73
- [courseFetching, courseProductRelationFetching],
74
- );
75
- const product = useMemo(
76
- () => (courseProductRelation ? courseProductRelation.product : undefined),
77
- [courseProductRelation],
72
+ () => courseFetching || offeringFetching,
73
+ [courseFetching, offeringFetching],
78
74
  );
75
+ const product = useMemo(() => (offering ? offering.product : undefined), [offering]);
79
76
  const course = useMemo(
80
- () => (courseProductRelation ? courseProductRelation.course : singleCourse),
81
- [courseProductRelation, singleCourse],
77
+ () => (offering ? offering.course : singleCourse),
78
+ [offering, singleCourse],
82
79
  );
83
80
 
84
81
  const getMenuLinkFromPath = (basePath: TeacherDashboardPaths) => {
85
82
  const path = generatePath(basePath, {
86
83
  organizationId: routeOrganizationId ?? '',
87
84
  courseId: routeCourseId ?? '',
88
- courseProductRelationId: routeCourseProductRelationId ?? '',
85
+ offeringId: routeOfferingId ?? '',
89
86
  });
90
87
  const menuLink: MenuLink = {
91
88
  to: path,
@@ -102,7 +99,7 @@ export const TeacherDashboardCourseSidebar = () => {
102
99
  <ContractNavLink
103
100
  link={menuLink}
104
101
  organizationId={routeOrganizationId}
105
- courseProductRelationId={routeCourseProductRelationId}
102
+ offeringId={routeOfferingId}
106
103
  />
107
104
  );
108
105
  }
@@ -112,10 +109,10 @@ export const TeacherDashboardCourseSidebar = () => {
112
109
  const menuLinkList = useMemo(
113
110
  () =>
114
111
  getMenuRoutes({
115
- courseProductRelationId: routeCourseProductRelationId,
112
+ offeringId: routeOfferingId,
116
113
  organizationId: routeOrganizationId,
117
114
  }).map(getMenuLinkFromPath),
118
- [routeOrganizationId, routeCourseProductRelationId],
115
+ [routeOrganizationId, routeOfferingId],
119
116
  );
120
117
 
121
118
  return (
@@ -1,13 +1,13 @@
1
1
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
2
2
 
3
3
  interface GetMenuRoutesArgs {
4
- courseProductRelationId?: string;
4
+ offeringId?: string;
5
5
  organizationId?: string;
6
6
  }
7
7
 
8
- export const getMenuRoutes = ({ courseProductRelationId, organizationId }: GetMenuRoutesArgs) => {
8
+ export const getMenuRoutes = ({ offeringId, organizationId }: GetMenuRoutesArgs) => {
9
9
  if (organizationId) {
10
- if (courseProductRelationId) {
10
+ if (offeringId) {
11
11
  return [
12
12
  TeacherDashboardPaths.ORGANIZATION_PRODUCT,
13
13
  TeacherDashboardPaths.ORGANIZATION_PRODUCT_CONTRACTS,
@@ -17,7 +17,7 @@ export const getMenuRoutes = ({ courseProductRelationId, organizationId }: GetMe
17
17
  return [TeacherDashboardPaths.ORGANIZATION_COURSE_GENERAL_INFORMATION];
18
18
  }
19
19
 
20
- if (courseProductRelationId) {
20
+ if (offeringId) {
21
21
  return [
22
22
  TeacherDashboardPaths.COURSE_PRODUCT,
23
23
  TeacherDashboardPaths.COURSE_PRODUCT_CONTRACTS,
@@ -24,9 +24,9 @@ const messages = defineMessages({
24
24
  export const TeacherDashboardOrganizationSidebar = () => {
25
25
  const intl = useIntl();
26
26
  const getRouteLabel = getDashboardRouteLabel(intl);
27
- const { organizationId, courseProductRelationId } = useParams<{
27
+ const { organizationId, offeringId } = useParams<{
28
28
  organizationId: string;
29
- courseProductRelationId?: string;
29
+ offeringId?: string;
30
30
  }>();
31
31
  const {
32
32
  item: organization,
@@ -43,11 +43,7 @@ export const TeacherDashboardOrganizationSidebar = () => {
43
43
 
44
44
  if (basePath === TeacherDashboardPaths.ORGANIZATION_CONTRACTS) {
45
45
  menuLink.component = (
46
- <ContractNavLink
47
- link={menuLink}
48
- organizationId={organizationId}
49
- courseProductRelationId={courseProductRelationId}
50
- />
46
+ <ContractNavLink link={menuLink} organizationId={organizationId} offeringId={offeringId} />
51
47
  );
52
48
  }
53
49
 
@@ -9,14 +9,14 @@ export enum TeacherDashboardPaths {
9
9
  ORGANIZATION = `${ROOT}/organizations/:organizationId`,
10
10
  ORGANIZATION_CONTRACTS = `${ORGANIZATION}/contracts`,
11
11
  ORGANIZATION_COURSES = `${ORGANIZATION}/courses`,
12
- ORGANIZATION_PRODUCT = `${ORGANIZATION_COURSES}/:courseId/products/:courseProductRelationId`,
12
+ ORGANIZATION_PRODUCT = `${ORGANIZATION_COURSES}/:courseId/products/:offeringId`,
13
13
  ORGANIZATION_COURSE_CONTRACTS = `${ORGANIZATION_COURSES}/:courseId/contracts`,
14
- ORGANIZATION_PRODUCT_CONTRACTS = `${ORGANIZATION_COURSES}/:courseId/products/:courseProductRelationId/contracts`,
15
- ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST = `${ORGANIZATION_COURSES}/:courseId/products/:courseProductRelationId/learners`,
14
+ ORGANIZATION_PRODUCT_CONTRACTS = `${ORGANIZATION_COURSES}/:courseId/products/:offeringId/contracts`,
15
+ ORGANIZATION_COURSE_PRODUCT_LEARNER_LIST = `${ORGANIZATION_COURSES}/:courseId/products/:offeringId/learners`,
16
16
  ORGANIZATION_COURSE_GENERAL_INFORMATION = `${ORGANIZATION_COURSES}/:courseId/information`,
17
17
  COURSE = `${TEACHER_COURSES}/:courseId`,
18
18
  COURSE_GENERAL_INFORMATION = `${COURSE}/information`,
19
- COURSE_PRODUCT = `${COURSE}/products/:courseProductRelationId`,
19
+ COURSE_PRODUCT = `${COURSE}/products/:offeringId`,
20
20
  COURSE_PRODUCT_LEARNER_LIST = `${COURSE_PRODUCT}/learners`,
21
21
  COURSE_PRODUCT_CONTRACTS = `${COURSE_PRODUCT}/contracts`,
22
22
  }
@@ -20,6 +20,8 @@ type SlidePanelProps = {
20
20
  activeSlideIndex: number;
21
21
  isTransitioning: boolean;
22
22
  onBulletClick: (index: number) => void;
23
+ toggleAutoplay: () => void;
24
+ isAutoplaying: boolean;
23
25
  };
24
26
 
25
27
  /**
@@ -31,6 +33,8 @@ const SlidePanel = ({
31
33
  activeSlideIndex,
32
34
  onBulletClick,
33
35
  isTransitioning,
36
+ toggleAutoplay,
37
+ isAutoplaying,
34
38
  }: SlidePanelProps) => {
35
39
  const intl = useIntl();
36
40
  const hasSlideContent = slides.some((slide) => slide.content);
@@ -75,6 +79,11 @@ const SlidePanel = ({
75
79
  </span>
76
80
  </button>
77
81
  ))}
82
+ <div className="slider__autoplay">
83
+ <button type="button" onClick={toggleAutoplay}>
84
+ {isAutoplaying ? '⏸' : '⏵'}
85
+ </button>
86
+ </div>
78
87
  </div>
79
88
  </section>
80
89
  );
@@ -0,0 +1,53 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { IntlProvider } from 'react-intl';
3
+ import { Slide } from './types';
4
+ import Slider from '.';
5
+
6
+ const slides: Slide[] = [
7
+ {
8
+ pk: '1',
9
+ title: 'Slide 1',
10
+ content: 'Content for slide 1',
11
+ image: '/static/course_cover_image.jpg',
12
+ link_url: 'https://example.com/1',
13
+ link_open_blank: true,
14
+ },
15
+ {
16
+ pk: '2',
17
+ title: 'Slide 2',
18
+ content: 'Content for slide 2',
19
+ image: '/static/course_cover_image.jpg',
20
+ link_url: 'https://example.com/2',
21
+ link_open_blank: true,
22
+ },
23
+ {
24
+ pk: '3',
25
+ title: 'Slide 3',
26
+ content: 'Content for slide 3',
27
+ image: '/static/course_cover_image.jpg',
28
+ link_url: 'https://example.com/3',
29
+ link_open_blank: true,
30
+ },
31
+ ];
32
+
33
+ export default {
34
+ component: Slider,
35
+ title: 'Widgets/Slider',
36
+ decorators: [
37
+ (Story) => (
38
+ <IntlProvider locale="en">
39
+ <Story />
40
+ </IntlProvider>
41
+ ),
42
+ ],
43
+ } as Meta<typeof Slider>;
44
+
45
+ type Story = StoryObj<typeof Slider>;
46
+
47
+ export const Default: Story = {
48
+ args: {
49
+ pk: 'slider-1',
50
+ title: 'Example Slider',
51
+ slides,
52
+ },
53
+ };
@@ -1,7 +1,9 @@
1
1
  import useEmblaCarousel from 'embla-carousel-react';
2
- import { useCallback, useEffect, useState } from 'react';
2
+ import { useCallback, useEffect, useRef, useState } from 'react';
3
3
  import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
4
+ import Autoplay from 'embla-carousel-autoplay';
4
5
  import { defineMessages, FormattedMessage } from 'react-intl';
6
+ import { SLIDER_SETTINGS } from 'settings';
5
7
  import { Slide as SlideType } from './types';
6
8
  import SlidePanel from './components/SlidePanel';
7
9
  import Slideshow from './components/Slideshow';
@@ -22,9 +24,24 @@ type SliderProps = {
22
24
  };
23
25
 
24
26
  const Slider = ({ slides, title }: SliderProps) => {
25
- const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [WheelGesturesPlugin()]);
27
+ const autoplay = useRef(Autoplay({ delay: SLIDER_SETTINGS.autoplayDelay }));
28
+ const [emblaRef, emblaApi] = useEmblaCarousel({ loop: true }, [
29
+ WheelGesturesPlugin(),
30
+ autoplay.current,
31
+ ]);
26
32
  const [activeSlideIndex, setActiveSlideIndex] = useState(0);
27
33
  const [isTransitioning, setIsTransitioning] = useState(false);
34
+ const [isAutoplaying, setIsAutoplaying] = useState(true);
35
+
36
+ const toggleAutoplay = () => {
37
+ if (!autoplay.current) return;
38
+ if (isAutoplaying) {
39
+ autoplay.current.stop();
40
+ } else {
41
+ autoplay.current.play();
42
+ }
43
+ setIsAutoplaying(!isAutoplaying);
44
+ };
28
45
 
29
46
  const handleBulletClick = useCallback(
30
47
  (index: number) => {
@@ -101,6 +118,8 @@ const Slider = ({ slides, title }: SliderProps) => {
101
118
  activeSlideIndex={activeSlideIndex}
102
119
  onBulletClick={handleBulletClick}
103
120
  isTransitioning={isTransitioning}
121
+ toggleAutoplay={toggleAutoplay}
122
+ isAutoplaying={isAutoplaying}
104
123
  />
105
124
  <span className="offscreen" role="presentation" aria-live="polite" aria-atomic="true">
106
125
  <FormattedMessage