richie-education 3.1.3-dev31 → 3.1.3-dev37

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.
@@ -29,4 +29,5 @@ export const {
29
29
  MOCK_SERVICE_WORKER_ENABLED,
30
30
  DEBUG_UNION_RESOURCES_HOOK,
31
31
  CURRENT_JOANIE_DEV_DEMO_USER,
32
+ SLIDER_SETTINGS,
32
33
  } = settings;
@@ -61,3 +61,4 @@ export const PER_PAGE = {
61
61
 
62
62
  export const MOCK_SERVICE_WORKER_ENABLED = false;
63
63
  export const DEBUG_UNION_RESOURCES_HOOK = false;
64
+ export const SLIDER_SETTINGS = { autoplayDelay: 3000 };
@@ -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
@@ -239,10 +239,16 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
239
239
  const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
240
240
  // FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
241
241
  // CredentialOrder
242
- const { item: offering, states: productQueryStates } = useCourseProduct({
243
- product_id: productId,
244
- course_id: course.code,
245
- });
242
+ const { item: offering, states: productQueryStates } = useCourseProduct(
243
+ {
244
+ product_id: productId,
245
+ course_id: course.code,
246
+ },
247
+ {
248
+ refetchOnMount: 'always',
249
+ refetchOnWindowFocus: 'always',
250
+ },
251
+ );
246
252
 
247
253
  const product = offering?.product;
248
254
  const { item: productOrder, states: orderQueryStates } = useProductOrder({
@@ -111,7 +111,9 @@ const OpenedCourseRun = ({
111
111
  let courseOfferMessage = null;
112
112
  let certificationOfferMessage = null;
113
113
  let enrollmentPrice = '';
114
+ let enrollmentDiscountedPrice = '';
114
115
  let certificatePrice = '';
116
+ let certificateDiscountedPrice = '';
115
117
 
116
118
  if (courseRun.offer) {
117
119
  const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
@@ -130,7 +132,7 @@ const OpenedCourseRun = ({
130
132
  }
131
133
 
132
134
  if ((courseRun.discounted_price ?? -1) >= 0) {
133
- enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
135
+ enrollmentDiscountedPrice = intl.formatNumber(courseRun.discounted_price!, {
134
136
  style: 'currency',
135
137
  currency: courseRun.price_currency,
136
138
  });
@@ -153,7 +155,7 @@ const OpenedCourseRun = ({
153
155
  }
154
156
 
155
157
  if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
156
- certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
158
+ certificateDiscountedPrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
157
159
  style: 'currency',
158
160
  currency: courseRun.price_currency,
159
161
  });
@@ -204,7 +206,16 @@ const OpenedCourseRun = ({
204
206
  <dd>
205
207
  <FormattedMessage {...courseOfferMessage} />
206
208
  <br />
207
- {enrollmentPrice}
209
+ {enrollmentDiscountedPrice ? (
210
+ <>
211
+ <del>{enrollmentPrice}</del>
212
+ <span>&nbsp;({courseRun.discount})</span>
213
+ <br />
214
+ <strong>{enrollmentDiscountedPrice}</strong>
215
+ </>
216
+ ) : (
217
+ enrollmentPrice
218
+ )}
208
219
  </dd>
209
220
  </>
210
221
  )}
@@ -216,7 +227,16 @@ const OpenedCourseRun = ({
216
227
  <dd>
217
228
  <FormattedMessage {...certificationOfferMessage} />
218
229
  <br />
219
- {certificatePrice}
230
+ {certificateDiscountedPrice ? (
231
+ <>
232
+ <del>{certificatePrice}</del>
233
+ <span>&nbsp;({courseRun.certificate_discount})</span>
234
+ <br />
235
+ <strong>{certificateDiscountedPrice}</strong>
236
+ </>
237
+ ) : (
238
+ certificatePrice
239
+ )}
220
240
  </dd>
221
241
  </>
222
242
  )}
@@ -102,7 +102,9 @@ const OpenedSelfPacedCourseRun = ({
102
102
  let courseOfferMessage = null;
103
103
  let certificationOfferMessage = null;
104
104
  let enrollmentPrice = '';
105
+ let enrollmentDiscountedPrice = '';
105
106
  let certificatePrice = '';
107
+ let certificateDiscountedPrice = '';
106
108
 
107
109
  if (courseRun.offer) {
108
110
  const offer = courseRun.offer.toUpperCase().replaceAll(' ', '_');
@@ -121,7 +123,7 @@ const OpenedSelfPacedCourseRun = ({
121
123
  }
122
124
 
123
125
  if ((courseRun.discounted_price ?? -1) >= 0) {
124
- enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
126
+ enrollmentDiscountedPrice = intl.formatNumber(courseRun.discounted_price!, {
125
127
  style: 'currency',
126
128
  currency: courseRun.price_currency,
127
129
  });
@@ -144,7 +146,7 @@ const OpenedSelfPacedCourseRun = ({
144
146
  }
145
147
 
146
148
  if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
147
- certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
149
+ certificateDiscountedPrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
148
150
  style: 'currency',
149
151
  currency: courseRun.price_currency,
150
152
  });
@@ -188,7 +190,16 @@ const OpenedSelfPacedCourseRun = ({
188
190
  <dd>
189
191
  <FormattedMessage {...courseOfferMessage} />
190
192
  <br />
191
- {enrollmentPrice}
193
+ {enrollmentDiscountedPrice ? (
194
+ <>
195
+ <del>{enrollmentPrice}</del>
196
+ <span>&nbsp;({courseRun.discount})</span>
197
+ <br />
198
+ <strong>{enrollmentDiscountedPrice}</strong>
199
+ </>
200
+ ) : (
201
+ enrollmentPrice
202
+ )}
192
203
  </dd>
193
204
  </>
194
205
  )}
@@ -200,7 +211,16 @@ const OpenedSelfPacedCourseRun = ({
200
211
  <dd>
201
212
  <FormattedMessage {...certificationOfferMessage} />
202
213
  <br />
203
- {certificatePrice}
214
+ {certificateDiscountedPrice ? (
215
+ <>
216
+ <del>{certificatePrice}</del>
217
+ <span>&nbsp;({courseRun.certificate_discount})</span>
218
+ <br />
219
+ <strong>{certificateDiscountedPrice}</strong>
220
+ </>
221
+ ) : (
222
+ certificatePrice
223
+ )}
204
224
  </dd>
205
225
  </>
206
226
  )}
@@ -1486,7 +1486,9 @@ describe('<SyllabusCourseRunsList/>', () => {
1486
1486
  );
1487
1487
 
1488
1488
  const content = getHeaderContainer().innerHTML;
1489
- expect(content).toContain('<dd>Paid access<br>€30.00</dd>');
1489
+ expect(content).toContain(
1490
+ '<dd>Paid access<br><del>€49.99</del><span>&nbsp;(-20%)</span><br><strong>€30.00</strong></dd>',
1491
+ );
1490
1492
  });
1491
1493
 
1492
1494
  it('renders certificate discount on SyllabusCourseRun', async () => {
@@ -1509,6 +1511,8 @@ describe('<SyllabusCourseRunsList/>', () => {
1509
1511
  );
1510
1512
 
1511
1513
  const content = getHeaderContainer().innerHTML;
1512
- expect(content).toContain('<dd>Paid certificate<br>€70.00</dd>');
1514
+ expect(content).toContain(
1515
+ '<dd>Paid certificate<br><del>€100.00</del><span>&nbsp;(-30%)</span><br><strong>€70.00</strong></dd>',
1516
+ );
1513
1517
  });
1514
1518
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.1.3-dev31",
3
+ "version": "3.1.3-dev37",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -90,6 +90,7 @@
90
90
  "cljs-merge": "1.1.1",
91
91
  "core-js": "3.41.0",
92
92
  "downshift": "9.0.9",
93
+ "embla-carousel-autoplay": "8.6.0",
93
94
  "embla-carousel-react": "8.5.2",
94
95
  "embla-carousel-wheel-gestures": "8.0.1",
95
96
  "eslint": ">=8.57.0 <9",
@@ -166,6 +166,9 @@ $r-theme: (
166
166
  index-color: r-color('battleship-grey'),
167
167
  index-hover-color: r-color('indianred3'),
168
168
  index-active-color: r-color('firebrick6'),
169
+ autoplay-color: r-color('white'),
170
+ autoplay-background-color: r-color('battleship-grey'),
171
+ autoplay-background-hover-color: r-color('charcoal'),
169
172
  ),
170
173
  blogpost-glimpse: (
171
174
  card-background: r-color('white'),
@@ -155,6 +155,25 @@ $r-slider-content-line-clamp: 4 !default;
155
155
  }
156
156
  }
157
157
 
158
+ .slider__autoplay {
159
+ display: flex;
160
+ justify-content: flex-end;
161
+ font-weight: bold;
162
+ margin-top: 0.5rem;
163
+ button {
164
+ cursor: pointer;
165
+ border-radius: 50px;
166
+ padding: 0.25rem 0.5rem;
167
+ border: none;
168
+ color: r-theme-val(slider-plugin, autoplay-color);
169
+ background-color: r-theme-val(slider-plugin, autoplay-background-color);
170
+ &:hover,
171
+ &:focus {
172
+ background-color: r-theme-val(slider-plugin, autoplay-background-hover-color);
173
+ }
174
+ }
175
+ }
176
+
158
177
  .slide__content {
159
178
  max-width: 680px;
160
179
 
@@ -55,6 +55,11 @@ $glimpse-gutter: 0.6rem;
55
55
  align-self: flex-end;
56
56
  }
57
57
  }
58
+ .section__grid--33x33x33 > &:nth-child(0n + 2) {
59
+ @include media-breakpoint-up(lg) {
60
+ align-self: unset;
61
+ }
62
+ }
58
63
 
59
64
  // Apply card styles for elements
60
65
  @include m-o-card(