Skip to content

Commit 89d8f0b

Browse files
eleanorreemCopilot
andauthored
refactor: carousel component (#1557)
* refactor: carousel component * Update components/common/Carousel.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update components/common/ResourceCarousel.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update components/common/Carousel.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update components/common/Carousel.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: keys and indexes in CarouselItemContainer --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 1e4bc25 commit 89d8f0b

File tree

6 files changed

+161
-201
lines changed

6 files changed

+161
-201
lines changed

components/cards/RelatedContentCard.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ const categoryStyle = {
2929
},
3030
} as const;
3131

32+
const cardContentStyle = {
33+
minHeight: 200,
34+
padding: ['24px !important', '24px !important', '36px !important'],
35+
} as const;
36+
3237
interface RelatedContentProps {
3338
title: string;
3439
href: string;
@@ -57,7 +62,7 @@ export const RelatedContentCard = (props: RelatedContentProps) => {
5762
handleClick();
5863
}}
5964
>
60-
<CardContent sx={{ minHeight: 238 }}>
65+
<CardContent sx={cardContentStyle}>
6166
<Box position="relative" width="100%" paddingRight={3}>
6267
<Box>
6368
<Typography sx={categoryStyle}>

components/common/Carousel.tsx

Lines changed: 103 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,56 @@
11
'use client';
22

3-
import { useWidth } from '@/lib/utils/useWidth';
43
import theme from '@/styles/theme';
54
import { KeyboardArrowRight } from '@mui/icons-material';
65
import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft';
7-
import { Box, Breakpoint, IconButton } from '@mui/material';
6+
import { Box, IconButton } from '@mui/material';
87
import { Carousel as NukaCarousel, useCarousel } from 'nuka-carousel';
98

109
interface CarouselProps {
1110
items: Array<React.ReactNode>;
1211
theme: 'primary' | 'secondary';
13-
showArrows?: boolean;
14-
arrowPosition?: 'side' | 'bottom';
1512
title?: string;
16-
slidesPerView?: { xs: number; sm: number; md: number; lg: number; xl: number };
17-
afterSlideHandle?: (newSlideIndex: number) => void;
1813
}
1914

20-
const numberSlidesToWidthMap: { [key: number]: string } = {
15+
// These are purposely not exactly half or third of the screen width
16+
// because nuka-carousel struggles to calculate the width of slides correctly
17+
// when the parent container has a flex layout. This is a workaround to avoid that issue.
18+
const numSlidesToWidthMap: { [key: number]: string } = {
2119
1: '100%',
22-
2: '50%',
23-
3: '33.33%',
24-
4: '25%',
20+
2: '50.01%',
21+
3: '33.34%',
22+
4: '25.01%',
2523
};
2624

27-
const tabletSlidesToWidthMap: { [key: number]: string } = {
28-
1: '100%',
29-
2: '50%',
30-
3: '25%',
25+
export const getSlideWidth = (
26+
numMobileSlides: number,
27+
numTabletSlides: number,
28+
numDesktopSlides: number,
29+
) => {
30+
return {
31+
width: [
32+
numSlidesToWidthMap[numMobileSlides || 1],
33+
numSlidesToWidthMap[numTabletSlides || 1],
34+
numSlidesToWidthMap[numDesktopSlides || 1],
35+
],
36+
minWidth: [
37+
numSlidesToWidthMap[numMobileSlides || 1],
38+
numSlidesToWidthMap[numTabletSlides || 1],
39+
numSlidesToWidthMap[numDesktopSlides || 1],
40+
],
41+
};
3142
};
3243

3344
// Dots and arrows in 1 component because of the design
34-
const CustomDots = ({
35-
showArrows = false,
36-
arrowPosition = 'bottom',
37-
carouselTheme = 'primary',
38-
}: {
39-
showArrows: boolean;
40-
arrowPosition: 'side' | 'bottom';
41-
carouselTheme: 'primary' | 'secondary';
42-
}) => {
45+
const CustomDots = ({ carouselTheme = 'primary' }: { carouselTheme: 'primary' | 'secondary' }) => {
46+
// totalPages are not calculated correctly in the nuka-carousel so causes a bug.
47+
// In the case that the scroll width is less than 1.5 times the screen width, it will round down the number of pages
48+
// and cause the dot count to be incorrect.
49+
// If you go into useMeasurements hook in nuka-carousel, you can see that it calculates the total pages and rounds down the number of pages.
50+
// This is particularly an issue if you have 1.4 pages, this means the dots will not render!
51+
// Deciding to park this issue for now as it needs a bug report to nuka-carousel.
4352
const { currentPage, totalPages, goBack, goForward, goToPage } = useCarousel();
53+
if (totalPages < 2) return <></>;
4454

4555
const getBackground = (index: number) =>
4656
currentPage === index
@@ -61,23 +71,21 @@ const CustomDots = ({
6171
display="flex"
6272
marginTop={2}
6373
>
64-
{showArrows && arrowPosition == 'bottom' && (
65-
<Box alignContent="center">
66-
<IconButton
67-
onClick={goBack}
68-
sx={{
69-
backgroundColor: theme.palette.primary.dark,
70-
color: theme.palette.common.white,
71-
'&:hover': {
72-
backgroundColor: theme.palette.common.white,
73-
color: theme.palette.primary.dark,
74-
},
75-
}}
76-
>
77-
<KeyboardArrowLeft></KeyboardArrowLeft>
78-
</IconButton>
79-
</Box>
80-
)}
74+
<Box alignContent="center">
75+
<IconButton
76+
onClick={goBack}
77+
sx={{
78+
backgroundColor: theme.palette.primary.dark,
79+
color: theme.palette.common.white,
80+
'&:hover': {
81+
backgroundColor: theme.palette.common.white,
82+
color: theme.palette.primary.dark,
83+
},
84+
}}
85+
>
86+
<KeyboardArrowLeft></KeyboardArrowLeft>
87+
</IconButton>
88+
</Box>
8189
<Box>
8290
<Box display="flex" gap={1} alignContent="center" width="100%">
8391
{[...Array(totalPages)].map((_, index) => (
@@ -99,59 +107,36 @@ const CustomDots = ({
99107
))}
100108
</Box>
101109
</Box>
102-
{showArrows && arrowPosition == 'bottom' && (
103-
<Box alignContent="center">
104-
<IconButton
105-
onClick={goForward}
106-
sx={{
107-
backgroundColor: theme.palette.primary.dark,
108-
color: theme.palette.common.white,
109-
'&:hover': {
110-
backgroundColor: theme.palette.common.white,
111-
color: theme.palette.primary.dark,
112-
},
113-
}}
114-
>
115-
<KeyboardArrowRight></KeyboardArrowRight>
116-
</IconButton>
117-
</Box>
118-
)}
110+
<Box alignContent="center">
111+
<IconButton
112+
onClick={goForward}
113+
sx={{
114+
backgroundColor: theme.palette.primary.dark,
115+
color: theme.palette.common.white,
116+
'&:hover': {
117+
backgroundColor: theme.palette.common.white,
118+
color: theme.palette.primary.dark,
119+
},
120+
}}
121+
>
122+
<KeyboardArrowRight></KeyboardArrowRight>
123+
</IconButton>
124+
</Box>
119125
</Box>
120126
);
121127
};
122128

123129
const Carousel = (props: CarouselProps) => {
124-
const {
125-
items,
126-
showArrows = false,
127-
arrowPosition = 'bottom',
128-
title = 'carousel',
129-
theme = 'primary',
130-
slidesPerView = {
131-
xs: 1,
132-
sm: 1,
133-
md: 1,
134-
lg: 1,
135-
xl: 1,
136-
},
137-
} = props;
138-
const width = useWidth();
139-
const currentSlidePerView = slidesPerView[width];
140-
const navigationEnabled = isNavigationEnabled(width, items.length, slidesPerView);
141-
const scrollDistance =
142-
currentSlidePerView < 2 || items.length / currentSlidePerView < 2 ? 'slide' : 'screen';
130+
const { items, title = 'carousel', theme = 'primary' } = props;
131+
143132
return (
144133
<NukaCarousel
145134
id={title}
146-
showArrows={navigationEnabled && showArrows && arrowPosition == 'side'}
147-
showDots={navigationEnabled}
135+
showDots={true}
148136
swiping={true}
149-
dots={
150-
<CustomDots showArrows={showArrows} arrowPosition={arrowPosition} carouselTheme={theme} />
151-
}
137+
dots={<CustomDots carouselTheme={theme} />}
152138
title={title}
153-
afterSlide={props.afterSlideHandle}
154-
scrollDistance={scrollDistance}
139+
scrollDistance={'screen'}
155140
>
156141
{items}
157142
</NukaCarousel>
@@ -162,30 +147,41 @@ export default Carousel;
162147
// Note that if you use this function, the carousel will be buggy in some screen sizes as it struggles to calculate the width
163148
// of slides correctly when the parent container has a flex layout. Avoid using this function if possible.
164149
// Use it when you can't set a fixed width for the slides, like in the StoryblokCarousel component.
165-
export const getSlideWidth = (
166-
numberMobileSlides: number,
167-
numberTabletSlides: number,
168-
numberDesktopSlides: number,
169-
) => {
170-
return {
171-
width: [
172-
numberSlidesToWidthMap[numberMobileSlides || 1],
173-
tabletSlidesToWidthMap[numberTabletSlides || 1],
174-
numberSlidesToWidthMap[numberDesktopSlides || 1],
175-
],
176-
minWidth: [
177-
numberSlidesToWidthMap[numberMobileSlides || 1],
178-
tabletSlidesToWidthMap[numberTabletSlides || 1],
179-
numberSlidesToWidthMap[numberDesktopSlides || 1],
180-
],
181-
};
150+
151+
type CarouselItemContainerProps = {
152+
children: React.ReactNode;
153+
slidesPerScreen?: number[]; // [mobile, tablet, desktop]
154+
customPadding?: number;
155+
customWidth?: string | Array<string>;
182156
};
183157

184-
const isNavigationEnabled = (
185-
currentBreakpoint: Breakpoint,
186-
numberOfSlides: number,
187-
slidesPerBreakpoint: Record<Breakpoint, number>,
188-
) => {
189-
const currentSlidesPerBreakpoint = slidesPerBreakpoint[currentBreakpoint];
190-
return currentSlidesPerBreakpoint < numberOfSlides;
158+
export const CarouselItemContainer = ({
159+
children,
160+
slidesPerScreen = [1, 2, 3],
161+
customPadding = 0.5,
162+
customWidth,
163+
}: CarouselItemContainerProps) => {
164+
return (
165+
<Box
166+
sx={{
167+
boxSizing: 'border-box', // Ensure padding is included in width calculation
168+
padding: customPadding,
169+
display: 'inline-block',
170+
':first-of-type': {
171+
paddingLeft: ['0 !important', '0 !important', '0 !important'],
172+
},
173+
...(customWidth
174+
? { minWidth: customWidth, width: customWidth }
175+
: {
176+
...getSlideWidth(
177+
slidesPerScreen[0] || 1,
178+
slidesPerScreen[1] || 1,
179+
slidesPerScreen[2] || 1,
180+
),
181+
}),
182+
}}
183+
>
184+
{children}
185+
</Box>
186+
);
191187
};

components/common/ResourceCarousel.tsx

Lines changed: 32 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useLocale } from 'next-intl';
1111
import { useEffect, useState } from 'react';
1212
import { RelatedContentCard } from '../cards/RelatedContentCard';
1313
import { ShortsCard } from '../cards/ShortsCard';
14-
import Carousel, { getSlideWidth } from './Carousel';
14+
import Carousel, { CarouselItemContainer } from './Carousel';
1515

1616
export interface ResourceCarouselProps {
1717
resourceTypes?: string[];
@@ -57,45 +57,37 @@ const ResourceCarousel = ({
5757
}
5858

5959
return (
60-
<Carousel
61-
title={title}
62-
theme="primary"
63-
showArrows={true}
64-
slidesPerView={slidesPerView}
65-
items={carouselStories.map((story) => {
66-
return (
67-
(story.content.component === 'resource_short_video' && (
68-
<Box p={0.25} minWidth="260px" width="260px" key={story.name}>
69-
<ShortsCard
70-
title={story.content.name}
71-
category={RESOURCE_CATEGORIES.SHORT_VIDEO}
72-
href={getDefaultFullSlug(story.full_slug, locale)}
73-
duration={story.content.duration}
74-
image={story.content.preview_image}
75-
/>
76-
</Box>
77-
)) ||
78-
(story.content.component === 'resource_conversation' && (
79-
<Box
80-
sx={{
81-
...getSlideWidth(1, 2, 3),
82-
minWidth: '300px',
83-
}}
84-
p={0.25}
85-
padding={1}
86-
key={story.name}
87-
>
88-
<RelatedContentCard
89-
title={story.name}
90-
href={getDefaultFullSlug(story.full_slug, locale)}
91-
category={RESOURCE_CATEGORIES.CONVERSATION}
92-
duration={story.content.duration}
93-
/>
94-
</Box>
95-
))
96-
);
97-
})}
98-
/>
60+
<Box sx={{ width: '100%' }}>
61+
<Carousel
62+
title={title}
63+
theme="primary"
64+
items={carouselStories.map((story, index) => {
65+
return (
66+
(story.content.component === 'resource_short_video' && (
67+
<CarouselItemContainer customWidth={'260px'} key={index}>
68+
<ShortsCard
69+
title={story.content.name}
70+
category={RESOURCE_CATEGORIES.SHORT_VIDEO}
71+
href={getDefaultFullSlug(story.full_slug, locale)}
72+
duration={story.content.duration}
73+
image={story.content.preview_image}
74+
/>
75+
</CarouselItemContainer>
76+
)) ||
77+
(story.content.component === 'resource_conversation' && (
78+
<CarouselItemContainer slidesPerScreen={[1, 2, 3]} key={index}>
79+
<RelatedContentCard
80+
title={story.name}
81+
href={getDefaultFullSlug(story.full_slug, locale)}
82+
category={RESOURCE_CATEGORIES.CONVERSATION}
83+
duration={story.content.duration}
84+
/>
85+
</CarouselItemContainer>
86+
))
87+
);
88+
})}
89+
/>
90+
</Box>
9991
);
10092
};
10193

0 commit comments

Comments
 (0)