@treely/strapi-slices 7.3.0 → 7.4.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treely/strapi-slices",
3
- "version": "7.3.0",
3
+ "version": "7.4.1",
4
4
  "license": "MIT",
5
5
  "author": "Tree.ly FlexCo",
6
6
  "description": "@treely/strapi-slices is a open source library maintained by Tree.ly.",
@@ -36,7 +36,7 @@ describe('The EventCard component', () => {
36
36
 
37
37
  expect(screen.getByRole('link')).toHaveAttribute(
38
38
  'href',
39
- defaultProps.event.button.url
39
+ defaultProps.event.button?.url
40
40
  );
41
41
 
42
42
  expect(screen.queryByText('Recommended')).not.toBeInTheDocument();
@@ -57,7 +57,7 @@ describe('The EventCard component', () => {
57
57
  const firstSpeakerImage = screen.getAllByRole('img')[2];
58
58
  expect(firstSpeakerImage).toHaveAttribute(
59
59
  'alt',
60
- defaultProps.event.speakers[0].image.alt
60
+ defaultProps.event.speakers?.[0]?.image.alt
61
61
  );
62
62
 
63
63
  await waitFor(() => userEvent.hover(firstSpeakerImage));
@@ -66,7 +66,7 @@ describe('The EventCard component', () => {
66
66
  const secondSpeakerImage = screen.getAllByRole('img')[3];
67
67
  expect(secondSpeakerImage).toHaveAttribute(
68
68
  'alt',
69
- defaultProps.event.speakers[1].image.alt
69
+ defaultProps.event.speakers?.[1]?.image.alt
70
70
  );
71
71
 
72
72
  await waitFor(() => userEvent.hover(secondSpeakerImage));
@@ -266,42 +266,46 @@ export const EventCard = ({ event }: EventCardProps): JSX.Element => {
266
266
  flexDir={mobile ? 'column-reverse' : 'row'}
267
267
  gap={mobile ? '4' : '0'}
268
268
  >
269
- <Flex width={mobile ? 'full' : 'auto'}>
270
- <StrapiLinkButton
271
- key={event.button.id}
272
- size="md"
273
- variant={event.buttonVariant}
274
- link={event.button}
275
- rightIcon={<CaretRight size="10" />}
276
- width="full"
277
- />
278
- </Flex>
279
- <Flex flexDir="row" gap="2">
280
- {event.speakers.map((speaker) => (
281
- <Box key={speaker.id}>
282
- <Box
283
- width={['10', null, null, '12']}
284
- height={['10', null, null, '12']}
285
- position="relative"
286
- borderRadius="2xl"
287
- >
288
- <Tooltip label={speaker.name}>
289
- <Image
290
- src={strapiMediaUrl(speaker.image.img, 'medium')}
291
- alt={speaker.image.alt}
292
- fill
293
- style={{
294
- objectFit: speaker.image.objectFit || 'cover',
295
- borderRadius: 'var(--boemly-radii-md)',
296
- border:
297
- '1px solid, var(--whiteAlpha-700, rgba(255, 255, 255, 0.64))',
298
- }}
299
- />
300
- </Tooltip>
269
+ {event.button && (
270
+ <Flex width={mobile ? 'full' : 'auto'}>
271
+ <StrapiLinkButton
272
+ key={event.button.id}
273
+ size="md"
274
+ variant={event.buttonVariant}
275
+ link={event.button}
276
+ rightIcon={<CaretRight size="10" />}
277
+ width="full"
278
+ />
279
+ </Flex>
280
+ )}
281
+ {event.speakers && event.speakers.length > 0 && (
282
+ <Flex flexDir="row" gap="2">
283
+ {event.speakers.map((speaker) => (
284
+ <Box key={speaker.id}>
285
+ <Box
286
+ width={['10', null, null, '12']}
287
+ height={['10', null, null, '12']}
288
+ position="relative"
289
+ borderRadius="2xl"
290
+ >
291
+ <Tooltip label={speaker.name}>
292
+ <Image
293
+ src={strapiMediaUrl(speaker.image.img, 'medium')}
294
+ alt={speaker.image.alt}
295
+ fill
296
+ style={{
297
+ objectFit: speaker.image.objectFit || 'cover',
298
+ borderRadius: 'var(--boemly-radii-md)',
299
+ border:
300
+ '1px solid, var(--whiteAlpha-700, rgba(255, 255, 255, 0.64))',
301
+ }}
302
+ />
303
+ </Tooltip>
304
+ </Box>
301
305
  </Box>
302
- </Box>
303
- ))}
304
- </Flex>
306
+ ))}
307
+ </Flex>
308
+ )}
305
309
  </Flex>
306
310
  </Flex>
307
311
  </Box>
@@ -19,10 +19,10 @@ export enum EventType {
19
19
  interface StrapiEvent {
20
20
  title: string;
21
21
  description: string;
22
- button: StrapiLink;
22
+ button?: StrapiLink;
23
23
  buttonVariant?: 'outline' | 'ghost' | 'link' | 'solid' | 'outlineWhite';
24
24
  recommended?: boolean;
25
- speakers: {
25
+ speakers?: {
26
26
  id: number;
27
27
  name: string;
28
28
  image: StrapiImage;
@@ -16,7 +16,7 @@ const Template: StoryFn<typeof Events> = (args) => <Events {...args} />;
16
16
  export const Minimal = Template.bind({});
17
17
  Minimal.args = {
18
18
  slice: {
19
- upcomingTitle: 'Tree.ly Events',
19
+ upcomingTitle: 'Ready to Learn more about Forest Climate Protection?',
20
20
  upcomingDescription: 'Join us for these amazing events',
21
21
  pastTitle: 'Past Events',
22
22
  pastDescription: 'Check out our past events',
@@ -27,7 +27,7 @@ Minimal.args = {
27
27
  export const WithFilterSearch = Template.bind({});
28
28
  WithFilterSearch.args = {
29
29
  slice: {
30
- upcomingTitle: 'Tree.ly Events',
30
+ upcomingTitle: 'Ready to Learn more about Forest Climate Protection?',
31
31
  upcomingDescription: 'Join us for these amazing events',
32
32
  pastTitle: 'Past Events',
33
33
  pastDescription: 'Check out our past events',
@@ -58,6 +58,12 @@ describe('The Events slice', () => {
58
58
  jest.useFakeTimers().setSystemTime(NOW);
59
59
  getKeyCalls = [];
60
60
 
61
+ global.fetch = jest.fn().mockResolvedValue({
62
+ json: jest.fn().mockResolvedValue({
63
+ data: [upcomingEventMock, pastEventMock],
64
+ }),
65
+ });
66
+
61
67
  (useEvents as jest.Mock).mockImplementation(({ getKey }) => {
62
68
  const key = getKey(0, null);
63
69
  getKeyCalls.push(key.toString());
@@ -239,7 +245,7 @@ describe('The Events slice', () => {
239
245
  expect.arrayContaining([
240
246
  expect.stringContaining('filters[start][$gte]'),
241
247
  expect.stringContaining(
242
- 'filters[$and][0][eventTypes][eventType]=Conference'
248
+ 'filters[$or][0][eventTypes][eventType]=Conference'
243
249
  ),
244
250
  ])
245
251
  );
@@ -274,7 +280,7 @@ describe('The Events slice', () => {
274
280
  expect.arrayContaining([
275
281
  expect.stringContaining('filters[start][$gte]'),
276
282
  expect.stringContaining(
277
- 'filters[$and][0][languages][language]=German'
283
+ 'filters[$or][0][languages][language]=German'
278
284
  ),
279
285
  ])
280
286
  );
@@ -57,12 +57,24 @@ const enum Sort {
57
57
  OLDEST_FIRST = 'oldest',
58
58
  }
59
59
 
60
+ interface EventOption {
61
+ value: string;
62
+ label: string;
63
+ }
64
+
60
65
  export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
61
66
  const { formatMessage, locale } = useContext(IntlContext);
62
67
  const [eventTypeFilter, setEventTypeFilter] = useState([] as string[]);
63
68
  const [languageFilter, setLanguageFilter] = useState([] as string[]);
64
69
  const [sort, setSort] = useState([Sort.NEWEST_FIRST] as string[]);
65
70
 
71
+ const [allEventTypeOptions, setAllEventTypeOptions] = useState<EventOption[]>(
72
+ []
73
+ );
74
+ const [allLanguageOptions, setAllLanguageOptions] = useState<EventOption[]>(
75
+ []
76
+ );
77
+
66
78
  const now = new Date().toISOString();
67
79
 
68
80
  const buildEventsUrl = (
@@ -86,12 +98,19 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
86
98
  url.searchParams.append('sort', 'start:desc');
87
99
  }
88
100
 
89
- if (languageFilter.length || eventTypeFilter.length) {
90
- [...languageFilter, ...eventTypeFilter].forEach((filter, i) => {
91
- const filterKey =
92
- i < languageFilter.length ? 'languages' : 'eventTypes';
101
+ if (eventTypeFilter.length) {
102
+ eventTypeFilter.forEach((filter, i) => {
103
+ url.searchParams.append(
104
+ `filters[$or][${i}][eventTypes][eventType]`,
105
+ filter
106
+ );
107
+ });
108
+ }
109
+
110
+ if (languageFilter.length) {
111
+ languageFilter.forEach((filter, i) => {
93
112
  url.searchParams.append(
94
- `filters[$and][${i}][${filterKey}][${filterKey.slice(0, -1)}]`,
113
+ `filters[$or][${eventTypeFilter.length + i}][languages][language]`,
95
114
  filter
96
115
  );
97
116
  });
@@ -131,6 +150,7 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
131
150
  data?.flatMap((d: any) => d?.body?.data)?.filter((t: any) => !!t) || []
132
151
  );
133
152
  };
153
+
134
154
  // Process upcoming events
135
155
  const upcomingEvents = useMemo(() => {
136
156
  return processEvents(upcomingData);
@@ -141,40 +161,53 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
141
161
  return processEvents(pastData);
142
162
  }, [pastData]);
143
163
 
144
- const combinedEvents = useMemo(() => {
145
- return upcomingEvents.concat(pastEvents);
146
- }, [upcomingEvents, pastEvents]);
147
-
148
- const getOptions = (items: any[], key: string, selectedFilters: string[]) => {
149
- const uniqueValues = new Set<string>();
150
-
151
- const options = items
152
- .flatMap((card) =>
153
- card.attributes[key].map((item: any) => ({
154
- value: item[key.slice(0, -1)],
155
- label: item[key.slice(0, -1)],
156
- }))
157
- )
158
- .filter((option) => {
159
- if (uniqueValues.has(option.value)) return false;
160
- uniqueValues.add(option.value);
161
- return true;
162
- });
164
+ // Function to fetch all possible options
165
+ const fetchAllOptions = useCallback(async () => {
166
+ const url = new URL(`/treely-events`, STRAPI_URI);
167
+ url.searchParams.append('locale', locale);
168
+ url.searchParams.append('populate', 'deep,6');
163
169
 
164
- return options.concat(
165
- selectedFilters
166
- .filter((selected) => !uniqueValues.has(selected))
167
- .map((selected) => ({ value: selected, label: selected }))
170
+ const response = await fetch(
171
+ `${STRAPI_URI}/api/treely-events${url.search}`
172
+ );
173
+ const data = await response.json();
174
+
175
+ const events = data?.data || [];
176
+
177
+ // Extract all event types
178
+ const allEventTypes = new Set<string>();
179
+ events.forEach((event: any) => {
180
+ if (event?.attributes?.eventTypes) {
181
+ event.attributes.eventTypes.forEach((item: any) => {
182
+ allEventTypes.add(item.eventType);
183
+ });
184
+ }
185
+ });
186
+
187
+ // Extract all languages
188
+ const allLanguages = new Set<string>();
189
+ events.forEach((event: any) => {
190
+ if (event?.attributes?.languages) {
191
+ event.attributes.languages.forEach((item: any) => {
192
+ allLanguages.add(item.language);
193
+ });
194
+ }
195
+ });
196
+
197
+ // Update state with all options
198
+ setAllEventTypeOptions(
199
+ Array.from(allEventTypes).map((value) => ({ value, label: value }))
168
200
  );
169
- };
170
201
 
171
- const eventTypeOptions = useMemo(() => {
172
- return getOptions(combinedEvents, 'eventTypes', eventTypeFilter);
173
- }, [combinedEvents]);
202
+ setAllLanguageOptions(
203
+ Array.from(allLanguages).map((value) => ({ value, label: value }))
204
+ );
205
+ }, [locale]);
174
206
 
175
- const languageOptions = useMemo(() => {
176
- return getOptions(combinedEvents, 'languages', languageFilter);
177
- }, [combinedEvents]);
207
+ // Fetch all options when component mounts
208
+ useEffect(() => {
209
+ fetchAllOptions();
210
+ }, [fetchAllOptions]);
178
211
 
179
212
  const removeFilter = (
180
213
  filterType: keyof FiltersProps,
@@ -203,6 +236,8 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
203
236
  <DefaultSectionHeader
204
237
  title={slice.upcomingTitle}
205
238
  text={slice.upcomingDescription}
239
+ titleProps={{ maxW: '3xl' }}
240
+ textProps={{ maxW: '3xl' }}
206
241
  />
207
242
  <Spacer h="10" />
208
243
  </>
@@ -241,7 +276,7 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
241
276
  searchPlaceholder={formatMessage({
242
277
  id: 'sections.events.eventsFilter.searchPlaceholder',
243
278
  })}
244
- options={eventTypeOptions}
279
+ options={allEventTypeOptions}
245
280
  value={eventTypeFilter ?? []}
246
281
  onChange={(selected: string[]) => {
247
282
  setEventTypeFilter(selected);
@@ -260,7 +295,7 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
260
295
  searchPlaceholder={formatMessage({
261
296
  id: 'sections.events.eventsFilter.searchPlaceholder',
262
297
  })}
263
- options={languageOptions}
298
+ options={allLanguageOptions}
264
299
  value={languageFilter ?? []}
265
300
  onChange={(selected: string[]) => {
266
301
  setLanguageFilter(selected);
@@ -272,7 +307,7 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
272
307
  {/* Filter Tags */}
273
308
  <Box display="flex" flexWrap="wrap" minHeight="6" gap="2">
274
309
  {eventTypeFilter.map((eventType) => {
275
- const event = eventTypeOptions.find(
310
+ const event = allEventTypeOptions.find(
276
311
  (option) => option.value === eventType
277
312
  );
278
313
  return (
@@ -287,7 +322,7 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
287
322
  })}
288
323
 
289
324
  {languageFilter.map((singleLanguage) => {
290
- const language = languageOptions.find(
325
+ const language = allLanguageOptions.find(
291
326
  (option) => option.value === singleLanguage
292
327
  );
293
328
  return (
@@ -355,11 +390,7 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
355
390
  placeItems="center"
356
391
  >
357
392
  {upcomingEvents.map((event: IStrapiData<StrapiEvent>) => (
358
- <Box
359
- key={event.id}
360
- width="full"
361
- height={['full', null, null, 'xl']}
362
- >
393
+ <Box key={event.id} width="full" height="full">
363
394
  <EventCard event={event.attributes} />
364
395
  </Box>
365
396
  ))}
@@ -387,6 +418,8 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
387
418
  <DefaultSectionHeader
388
419
  title={slice.pastTitle}
389
420
  text={slice.pastDescription}
421
+ titleProps={{ maxW: '3xl' }}
422
+ textProps={{ maxW: '3xl' }}
390
423
  />
391
424
 
392
425
  <Spacer h="10" />
@@ -409,11 +442,7 @@ export const Events: React.FC<EventsProps> = ({ slice }: EventsProps) => {
409
442
  mb={['10', null, null, '20']}
410
443
  >
411
444
  {pastEvents.map((event: IStrapiData<StrapiEvent>) => (
412
- <Box
413
- key={event.id}
414
- height={['full', null, null, 'xl']}
415
- width="full"
416
- >
445
+ <Box key={event.id} height="full" width="full">
417
446
  <EventCard event={event.attributes} />
418
447
  </Box>
419
448
  ))}