Skip to content

Commit b196815

Browse files
authored
feat: add single video page / somatics (#1562)
1 parent 89d8f0b commit b196815

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+4361
-3080
lines changed

app/[locale]/[slug]/page.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export async function generateStaticParams() {
4848
'about-our-courses',
4949
'messaging',
5050
'shorts',
51+
'videos',
5152
'conversations',
5253
];
5354

app/[locale]/courses/page.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { STORYBLOK_ENVIRONMENT } from '@/lib/constants/common';
2-
import { FeatureFlag } from '@/lib/featureFlag';
2+
import { STORYBLOK_TAGS } from '@/lib/constants/enums';
33
import { getStoryblokStories } from '@/lib/storyblok';
44
import { generateMetadataBasic } from '@/lib/utils/generateMetadataBase';
55
import { ISbStoriesParams, ISbStoryData } from '@storyblok/react/rsc';
@@ -29,6 +29,7 @@ export default async function Page({ params }: { params: Params }) {
2929
const sbCoursesParams: ISbStoriesParams = {
3030
...baseProps,
3131
starts_with: 'courses/',
32+
language: locale,
3233
filter_query: {
3334
component: {
3435
in: 'Course',
@@ -39,20 +40,29 @@ export default async function Page({ params }: { params: Params }) {
3940
const sbConversationsParams: ISbStoriesParams = {
4041
...baseProps,
4142
starts_with: 'conversations/',
43+
language: locale,
4244
};
4345

4446
const sbShortsParams: ISbStoriesParams = {
4547
...baseProps,
4648
starts_with: 'shorts/',
49+
language: locale,
50+
};
51+
52+
const sbSingleVideoParams: ISbStoriesParams = {
53+
...baseProps,
54+
starts_with: 'videos/',
55+
language: locale,
56+
with_tag: STORYBLOK_TAGS.SOMATICS,
4757
};
4858

4959
const coursesStories = (await getStoryblokStories(locale, sbCoursesParams)) || [];
5060
const conversationsStories = (await getStoryblokStories(locale, sbConversationsParams)) || [];
5161
const shortsStories = (await getStoryblokStories(locale, sbShortsParams)) || [];
62+
const somaticsStories = (await getStoryblokStories(locale, sbSingleVideoParams)) || [];
5263

5364
const contentLanguagesString = locale === 'en' ? 'default' : locale;
5465

55-
// ✅ NEW logic for filtering courses by Storyblok 'languages' config
5666
const courses = coursesStories?.filter((course: ISbStoryData) =>
5767
course.content.languages?.includes(contentLanguagesString),
5868
);
@@ -65,5 +75,16 @@ export default async function Page({ params }: { params: Params }) {
6575
short.content.languages?.includes(contentLanguagesString),
6676
);
6777

68-
return <CoursesPage courseStories={courses} conversations={conversations} shorts={shorts} />;
78+
const somatics = somaticsStories?.filter((video: ISbStoryData) =>
79+
video.content.languages?.includes(contentLanguagesString),
80+
);
81+
82+
return (
83+
<CoursesPage
84+
courseStories={courses}
85+
conversations={conversations}
86+
shorts={shorts}
87+
somatics={somatics}
88+
/>
89+
);
6990
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import StoryblokResourceSingleVideoPage, {
2+
StoryblokResourceSingleVideoPageProps,
3+
} from '@/components/storyblok/StoryblokResourceSingleVideoPage';
4+
import { routing } from '@/i18n/routing';
5+
import { STORYBLOK_ENVIRONMENT } from '@/lib/constants/common';
6+
import { STORYBLOK_TAGS } from '@/lib/constants/enums';
7+
import { getStoryblokStory } from '@/lib/storyblok';
8+
import { generateMetadataBasic } from '@/lib/utils/generateMetadataBase';
9+
import { getStoryblokApi, ISbStoriesParams } from '@storyblok/react/rsc';
10+
import { getTranslations } from 'next-intl/server';
11+
import { notFound } from 'next/navigation';
12+
13+
export const dynamicParams = false;
14+
export const revalidate = 14400; // invalidate every 4 hours
15+
16+
type Params = Promise<{ locale: string; slug: string }>;
17+
18+
async function getStory(locale: string, slug: string) {
19+
return await getStoryblokStory(`videos/${slug}`, locale, {
20+
resolve_relations: [
21+
'resource_single_video.related_content',
22+
'resource_single_video.related_session',
23+
'resource_single_video.related_session.course',
24+
],
25+
});
26+
}
27+
28+
export async function generateMetadata({ params }: { params: Params }) {
29+
const { locale, slug } = await params;
30+
const t = await getTranslations({ locale, namespace: 'Resources' });
31+
const story = await getStory(locale, slug);
32+
33+
if (!story) return;
34+
35+
return generateMetadataBasic({
36+
title: story.content.name,
37+
titleParent: t('videos'),
38+
description: story.content.seo_description,
39+
});
40+
}
41+
42+
export async function generateStaticParams() {
43+
let paths: { slug: string; locale: string }[] = [];
44+
45+
const locales = routing.locales;
46+
const storyblokApi = getStoryblokApi();
47+
48+
let sbParams: ISbStoriesParams = {
49+
version: STORYBLOK_ENVIRONMENT,
50+
starts_with: 'videos/',
51+
filter_query: {
52+
component: {
53+
in: 'resource_single_video',
54+
},
55+
},
56+
};
57+
58+
const { data } = await storyblokApi.get('cdn/links/', sbParams);
59+
60+
Object.keys(data.links).forEach((linkKey) => {
61+
const story = data.links[linkKey];
62+
63+
if (!story.slug || !story.published) return;
64+
65+
const slug = story.slug.split('/')[1];
66+
67+
for (const locale of locales) {
68+
paths.push({ slug, locale });
69+
}
70+
});
71+
return paths;
72+
}
73+
74+
export default async function Page({ params }: { params: Params }) {
75+
const { locale, slug } = await params;
76+
77+
const story = await getStory(locale, slug);
78+
79+
if (!story) {
80+
notFound();
81+
}
82+
83+
return (
84+
<StoryblokResourceSingleVideoPage
85+
{...(story.content as StoryblokResourceSingleVideoPageProps)}
86+
tags={story.tag_list as STORYBLOK_TAGS[]}
87+
storyUuid={story.uuid}
88+
/>
89+
);
90+
}

components/cards/CourseCard.tsx

Lines changed: 52 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Link as i18nLink } from '@/i18n/routing';
55
import { PROGRESS_STATUS } from '@/lib/constants/enums';
66
import { getDefaultFullSlug } from '@/lib/utils/getDefaultFullSlug';
77
import { getImageSizes } from '@/lib/utils/imageSizes';
8-
import { iconTextRowStyle, rowStyle } from '@/styles/common';
8+
import { rowStyle } from '@/styles/common';
99
import KeyboardArrowDown from '@mui/icons-material/KeyboardArrowDown';
1010
import {
1111
Box,
@@ -28,61 +28,55 @@ const cardStyle = {
2828
backgroundColor: 'background.default',
2929
display: 'flex',
3030
flexDirection: 'column',
31+
mt: 0,
32+
'&:hover': {
33+
backgroundColor: 'background.default',
34+
},
3135
} as const;
3236

33-
const cardContentStyle = {
37+
const cardHeaderStyle = {
3438
...rowStyle,
35-
gap: 2,
36-
padding: { xs: 2, md: 3 },
37-
paddingBottom: '0rem !important',
39+
gap: 3,
40+
alignItems: 'center',
41+
padding: { xs: 2, sm: 3 },
42+
paddingBottom: '0.5rem !important',
3843
minHeight: { xs: 124, md: 136 },
3944
} as const;
4045

4146
const cardActionStyle = {
47+
borderRadius: 0,
48+
borderBottom: '2px solid background.default',
4249
'&:hover': {
43-
borderBottom: '2px solid grey',
4450
borderBottomColor: 'primary.main',
45-
marginBottom: '-2px',
4651
},
4752
} as const;
4853

49-
const imageContainerStyle = {
50-
position: 'relative',
51-
width: '100%',
52-
maxHeight: '110px',
53-
minHeight: '72px',
54-
} as const;
55-
56-
const titleContainerStyle = {
57-
display: 'flex',
58-
flexDirection: 'column',
59-
justifyContent: 'center',
60-
} as const;
61-
62-
const titleStyle = {
63-
display: '-webkit-box',
64-
WebkitLineClamp: 3,
65-
WebkitBoxOrient: 'vertical',
66-
overflow: 'hidden',
67-
textOverflow: 'ellipsis',
68-
} as const;
69-
7054
const collapseContentStyle = {
55+
background: 'white',
7156
padding: { xs: 2, md: 3 },
7257
paddingTop: { xs: 0, md: 0 },
7358
} as const;
7459

7560
const cardActionsStyle = {
7661
paddingLeft: 4,
77-
paddingTop: 0,
62+
paddingTop: 1,
63+
gap: 1,
7864
justifyContent: 'flex-end',
7965
alignItems: 'center',
66+
borderTop: '2px solid background.default',
67+
color: 'text.secondary',
68+
fontFamily: 'merriweather',
69+
70+
'&:hover': {
71+
backgroundColor: 'white',
72+
borderTopColor: 'primary.main',
73+
},
8074
} as const;
8175

82-
const statusRowStyle = {
83-
...iconTextRowStyle,
84-
marginTop: 0,
85-
marginLeft: 1,
76+
const imageContainerStyle = {
77+
position: 'relative',
78+
width: { xs: 100, md: 100, lg: 120 },
79+
height: { xs: 100, md: 100, lg: 120 },
8680
};
8781

8882
interface CourseCardProps {
@@ -101,6 +95,25 @@ const CourseCard = (props: CourseCardProps) => {
10195
setExpanded(!expanded);
10296
};
10397

98+
const cardHeader = (
99+
<CardContent sx={cardHeaderStyle}>
100+
<Box sx={imageContainerStyle}>
101+
<Image
102+
alt={course.content.image_with_background.alt}
103+
src={course.content.image_with_background.filename}
104+
fill
105+
sizes={getImageSizes(imageContainerStyle.width)}
106+
style={{
107+
objectFit: 'contain',
108+
}}
109+
/>
110+
</Box>
111+
<Typography component="h3" variant="h3" flex={1} mb={0}>
112+
{course.content.name}
113+
</Typography>
114+
</CardContent>
115+
);
116+
104117
return (
105118
<Card sx={cardStyle}>
106119
{clickable ? (
@@ -110,55 +123,19 @@ const CourseCard = (props: CourseCardProps) => {
110123
href={getDefaultFullSlug(course.full_slug, locale)}
111124
aria-label={`${t('navigateToCourse')} ${course.content.name}`}
112125
>
113-
<CardContent sx={cardContentStyle}>
114-
<Box flex={[2, 1]} sx={imageContainerStyle}>
115-
<Image
116-
alt={course.content.image_with_background.alt}
117-
src={course.content.image_with_background.filename}
118-
fill
119-
sizes={getImageSizes(imageContainerStyle.width)}
120-
style={{
121-
objectFit: 'contain',
122-
}}
123-
/>
124-
</Box>
125-
<Box flex={[3, 2]} sx={titleContainerStyle}>
126-
<Typography component="h3" variant="h3" sx={expanded ? {} : titleStyle}>
127-
{course.content.name}
128-
</Typography>
129-
</Box>
130-
</CardContent>
126+
{cardHeader}
131127
</CardActionArea>
132128
) : (
133-
<CardContent sx={cardContentStyle}>
134-
<Box sx={imageContainerStyle}>
135-
<Image
136-
alt={course.content.image.alt}
137-
src={course.content.image.filename}
138-
fill
139-
sizes={getImageSizes(imageContainerStyle.width)}
140-
style={{
141-
objectFit: 'contain',
142-
}}
143-
/>
144-
</Box>
145-
<Box flex={1} sx={titleContainerStyle}>
146-
<Typography component="h3" variant="h3" sx={expanded ? {} : titleStyle}>
147-
{course.content.name}
148-
</Typography>
149-
{!!courseProgress && courseProgress !== PROGRESS_STATUS.NOT_STARTED && (
150-
<ProgressStatus status={courseProgress} />
151-
)}
152-
</Box>
153-
</CardContent>
129+
<>{cardHeader}</>
154130
)}
155-
<CardActions sx={cardActionsStyle}>
131+
<CardActions
132+
sx={{ ...cardActionsStyle, backgroundColor: expanded ? 'white' : 'background.default' }}
133+
>
156134
{!!courseProgress && courseProgress !== PROGRESS_STATUS.NOT_STARTED && (
157135
<ProgressStatus status={courseProgress} />
158136
)}
159-
160137
<IconButton
161-
sx={{ marginLeft: 'auto' }}
138+
sx={{ marginLeft: 'auto', transform: expanded ? 'rotate(180deg)' : 'none' }}
162139
aria-label={`${t('expandSummary')} ${course.content.name}`}
163140
onClick={handleExpandClick}
164141
size="small"

components/cards/RelatedContentCard.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ import { useTranslations } from 'next-intl';
1212
const cardStyle = {
1313
mt: 0,
1414
width: '100%',
15+
minWidth: { xs: '100%', md: '17rem' },
1516
mb: { xs: '1rem', sm: '1.5rem' },
1617
backgroundColor: 'paleSecondaryLight',
1718
} as const;
1819

1920
const categoryStyle = {
2021
fontFamily: 'Montserrat, sans-serif',
2122
fontSize: '0.875rem !important',
22-
fontweight: 500,
2323
textTransform: 'uppercase',
2424
mb: '0.5rem !important',
2525
'& .before-dot:before': {
@@ -30,8 +30,9 @@ const categoryStyle = {
3030
} as const;
3131

3232
const cardContentStyle = {
33-
minHeight: 200,
34-
padding: ['24px !important', '24px !important', '36px !important'],
33+
minHeight: { xs: 160, md: 190 },
34+
paddingY: { xs: '1.5rem', md: '2rem !important' },
35+
paddingX: { xs: '1.5rem', md: '1.75rem !important' },
3536
} as const;
3637

3738
interface RelatedContentProps {
@@ -48,7 +49,7 @@ export const RelatedContentCard = (props: RelatedContentProps) => {
4849
const partnerAdmin = useTypedSelector((state) => state.partnerAdmin);
4950
const eventUserData = getEventUserData(userCreatedAt, partnerAccesses, partnerAdmin);
5051

51-
const t = useTranslations('Shared.relatedContent');
52+
const t = useTranslations('Resources.relatedContent');
5253
const handleClick = () => {
5354
logEvent(RELATED_CONTENT_CARD_CLICK, eventUserData);
5455
};
@@ -63,7 +64,7 @@ export const RelatedContentCard = (props: RelatedContentProps) => {
6364
}}
6465
>
6566
<CardContent sx={cardContentStyle}>
66-
<Box position="relative" width="100%" paddingRight={3}>
67+
<Box position="relative" width="100%" paddingRight={1}>
6768
<Box>
6869
<Typography sx={categoryStyle}>
6970
{t(category)}

0 commit comments

Comments
 (0)