Skip to content

Commit cd3c42a

Browse files
authored
Merge pull request #1181 from prezly/feature/dev-13393-implement-full-width-hero-card
[DEV-13393] Implement Lena-style hero card
2 parents 98e8f25 + df6f666 commit cd3c42a

File tree

13 files changed

+225
-30
lines changed

13 files changed

+225
-30
lines changed

app/[localeCode]/(index)/page.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export default async function StoriesIndexPage({ params, searchParams }: Props)
4242
<>
4343
<Stories
4444
categoryId={searchParams.category ? parseNumber(searchParams.category) : undefined}
45+
fullWidthFeaturedStory={themeSettings.full_width_featured_story}
4546
layout={themeSettings.layout}
4647
localeCode={params.localeCode}
4748
pageSize={getStoryListPageSize(themeSettings.layout)}

components/CategoriesBar/CategoriesBar.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function CategoriesBar({ translatedCategories }: Props) {
7171
key={category.id}
7272
href={{
7373
routeName: 'category',
74-
params: { slug: category.slug },
74+
params: { localeCode: locale, slug: category.slug },
7575
}}
7676
className={classNames(styles.link, {
7777
[styles.active]: category.slug === params.slug,
@@ -87,7 +87,11 @@ export function CategoriesBar({ translatedCategories }: Props) {
8787
buttonClassName={styles.more}
8888
>
8989
{hiddenCategories.map((category) => (
90-
<CategoryItem category={category} key={category.id} />
90+
<CategoryItem
91+
key={category.id}
92+
category={category}
93+
localeCode={locale}
94+
/>
9195
))}
9296
</Dropdown>
9397
)}

components/CategoriesBar/CategoryItem/CategoryItem.tsx

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import type { TranslatedCategory } from '@prezly/sdk';
2+
import type { Locale } from '@prezly/theme-kit-nextjs';
23

34
import { DropdownItem } from '@/components/Dropdown';
45

56
import styles from './CategoryItem.module.scss';
67

78
type Props = {
89
category: TranslatedCategory;
10+
localeCode: Locale.Code;
911
};
1012

11-
export function CategoryItem({ category }: Props) {
12-
const { name, description } = category;
13+
export function CategoryItem({ category, localeCode }: Props) {
14+
const { name, description, slug } = category;
1315

1416
return (
15-
<DropdownItem href={{ routeName: 'category', params: { slug: category.slug } }}>
17+
<DropdownItem href={{ routeName: 'category', params: { localeCode, slug } }}>
1618
<span className={styles.title}>{name}</span>
1719
{description && <span className={styles.description}>{description}</span>}
1820
</DropdownItem>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
.container {
2+
position: relative;
3+
display: flex;
4+
align-items: flex-end;
5+
min-height: 500px;
6+
margin: (-$spacing-8) (-$spacing-5) $spacing-7;
7+
8+
@include tablet-up {
9+
height: 500px;
10+
}
11+
12+
@include desktop-up {
13+
height: 700px;
14+
margin: 0 0 $spacing-8;
15+
16+
&:hover .image {
17+
transform: scale(1.05);
18+
}
19+
20+
&.rounded {
21+
@include border-radius-m;
22+
23+
overflow: hidden;
24+
}
25+
}
26+
}
27+
28+
.link {
29+
text-decoration: none;
30+
31+
.mask {
32+
position: absolute;
33+
inset: 0;
34+
}
35+
}
36+
37+
.overlay {
38+
position: absolute;
39+
inset: 0;
40+
background: linear-gradient(270deg, rgb(3 7 18 / 0%) 50%, rgb(3 7 18 / 40%) 100%), linear-gradient(180deg, rgb(3 7 18 / 0%) 40%, rgb(3 7 18 / 20%) 62%, rgb(3 7 18 / 40%) 100%);
41+
}
42+
43+
.image {
44+
position: absolute;
45+
inset: 0;
46+
z-index: -1;
47+
48+
@include desktop-up {
49+
transform: scale(1);
50+
transition: transform 0.25s ease-in-out;
51+
}
52+
}
53+
54+
.content {
55+
padding: $spacing-8 $spacing-4;
56+
z-index: 2;
57+
58+
@include tablet-up {
59+
padding: $spacing-8 $spacing-5;
60+
}
61+
62+
@include desktop-up {
63+
max-width: 600px;
64+
padding: $spacing-8;
65+
box-sizing: content-box;
66+
}
67+
}
68+
69+
.categories {
70+
display: flex;
71+
gap: $spacing-1;
72+
margin-bottom: $spacing-3;
73+
74+
.link {
75+
z-index: 2;
76+
}
77+
78+
.badge {
79+
color: var(--prezly-white);
80+
background-color: color-mix(in srgb, var(--prezly-white) 20%, transparent);
81+
backdrop-filter: blur(8px);
82+
}
83+
}
84+
85+
.title {
86+
@include ensure-max-text-height(3, 140%);
87+
88+
color: var(--prezly-white);
89+
font-size: $font-size-xl;
90+
font-weight: $font-weight-bold;
91+
margin: 0;
92+
93+
&.expanded {
94+
@include ensure-max-text-height(4, 140%);
95+
}
96+
97+
@include tablet-up {
98+
font-size: 2.25rem;
99+
line-height: 140%;
100+
}
101+
}
102+
103+
.subtitle {
104+
@include ensure-max-text-height(3, 160%);
105+
106+
margin: 0;
107+
margin-top: $spacing-2;
108+
color: var(--prezly-white);
109+
font-size: $font-size-s;
110+
font-weight: $font-weight-regular;
111+
line-height: 160%;
112+
113+
@include tablet-up {
114+
font-size: $font-size-m;
115+
}
116+
}

components/StoryCards/HighlightedStoryCard.tsx

+62-8
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,81 @@
11
'use client';
22

33
import { Category } from '@prezly/sdk';
4-
import { useMemo } from 'react';
4+
import classNames from 'classnames';
55

66
import { useLocale } from '@/adapters/client';
77
import type { ListStory } from 'types';
88

9+
import { Badge } from '../Badge';
10+
import { Link } from '../Link';
11+
import { StoryImage } from '../StoryImage';
12+
913
import { StoryCard } from './StoryCard';
1014

15+
import styles from './HighlightedStoryCard.module.scss';
16+
1117
type Props = {
18+
fullWidth: boolean;
19+
rounded: boolean;
1220
showDate: boolean;
1321
showSubtitle: boolean;
1422
story: ListStory;
1523
};
1624

17-
export function HighlightedStoryCard({ showDate, showSubtitle, story }: Props) {
18-
const localeCode = useLocale();
19-
const { categories } = story;
25+
export function HighlightedStoryCard({ fullWidth, rounded, showDate, showSubtitle, story }: Props) {
26+
const locale = useLocale();
27+
const { categories, slug, subtitle } = story;
2028

21-
const translatedCategories = useMemo(
22-
() => Category.translations(categories, localeCode),
23-
[categories, localeCode],
24-
);
29+
const translatedCategories = Category.translations(categories, locale);
30+
31+
if (fullWidth) {
32+
return (
33+
<div
34+
className={classNames(styles.container, {
35+
[styles.rounded]: rounded,
36+
})}
37+
>
38+
<StoryImage
39+
size="full-width"
40+
className={styles.image}
41+
thumbnailImage={story.thumbnail_image}
42+
title={story.title}
43+
/>
44+
<div className={styles.overlay} />
45+
<div className={styles.content}>
46+
{translatedCategories.length > 0 && (
47+
<div className={styles.categories}>
48+
{translatedCategories.map((category) => (
49+
<Link
50+
className={styles.link}
51+
href={{
52+
routeName: 'category',
53+
params: { localeCode: locale, slug: category.slug },
54+
}}
55+
key={category.id}
56+
>
57+
<Badge className={styles.badge} size="small">
58+
{category.name}
59+
</Badge>
60+
</Link>
61+
))}
62+
</div>
63+
)}
64+
<Link className={styles.link} href={{ routeName: 'story', params: { slug } }}>
65+
<h2
66+
className={classNames(styles.title, {
67+
[styles.expanded]: !showSubtitle || !subtitle,
68+
})}
69+
>
70+
{story.title}
71+
<span className={styles.mask} />
72+
</h2>
73+
</Link>
74+
{showSubtitle && subtitle && <p className={styles.subtitle}>{subtitle}</p>}
75+
</div>
76+
</div>
77+
);
78+
}
2579

2680
return (
2781
<StoryCard

components/StoryCards/StoryCard.tsx

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
'use client';
2-
31
import type { TranslatedCategory } from '@prezly/sdk';
42
import classNames from 'classnames';
53
import type { ReactNode } from 'react';

components/StoryImage/StoryImage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import type { ListStory } from 'types';
77
import { getUploadcareImage } from 'utils';
88

99
import { useFallback } from './FallbackProvider';
10-
import { type CardSize, getCardImageSizes, getStoryThumbnail } from './lib';
10+
import { getCardImageSizes, getStoryThumbnail, type ImageSize } from './lib';
1111

1212
import styles from './StoryImage.module.scss';
1313

1414
type Props = {
1515
className?: string;
1616
isStatic?: boolean;
1717
placeholderClassName?: string;
18-
size: CardSize;
18+
size: ImageSize;
1919
thumbnailImage: ListStory['thumbnail_image'];
2020
title: string;
2121
};

components/StoryImage/lib.ts

+16-12
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Story } from '@prezly/sdk';
22
import type { UploadcareImageDetails } from '@prezly/uploadcare-image/build/types';
33

4-
export type CardSize = 'small' | 'medium' | 'big' | 'hero' | 'tiny';
4+
export type ImageSize = 'small' | 'medium' | 'big' | 'hero' | 'full-width' | 'tiny';
55

66
export function getStoryThumbnail(
77
thumbnailImage: Story.ExtraFields['thumbnail_image'],
@@ -13,29 +13,31 @@ export function getStoryThumbnail(
1313
return null;
1414
}
1515

16-
export function getCardImageSizes(cardSize: CardSize) {
17-
if (cardSize === 'tiny') {
16+
export function getCardImageSizes(imageSize: ImageSize) {
17+
if (imageSize === 'tiny') {
1818
return '120px';
1919
}
2020

2121
return [
22-
`(max-width: 767px) ${getPhoneImageSize(cardSize)}`,
23-
`(max-width: 1023px) ${getTabletImageSize(cardSize)}`,
24-
getDesktopImageSize(cardSize),
22+
`(max-width: 767px) ${getPhoneImageSize(imageSize)}`,
23+
`(max-width: 1023px) ${getTabletImageSize(imageSize)}`,
24+
getDesktopImageSize(imageSize),
2525
].join(', ');
2626
}
2727

28-
function getPhoneImageSize(cardSize: CardSize) {
29-
switch (cardSize) {
28+
function getPhoneImageSize(imageSize: ImageSize) {
29+
switch (imageSize) {
30+
case 'full-width':
3031
case 'hero':
3132
return '100w';
3233
default:
3334
return '95w';
3435
}
3536
}
3637

37-
function getTabletImageSize(cardSize: CardSize) {
38-
switch (cardSize) {
38+
function getTabletImageSize(imageSize: ImageSize) {
39+
switch (imageSize) {
40+
case 'full-width':
3941
case 'hero':
4042
return '100w';
4143
case 'small':
@@ -45,8 +47,10 @@ function getTabletImageSize(cardSize: CardSize) {
4547
}
4648
}
4749

48-
function getDesktopImageSize(cardSize: CardSize) {
49-
switch (cardSize) {
50+
function getDesktopImageSize(imageSize: ImageSize) {
51+
switch (imageSize) {
52+
case 'full-width':
53+
return '1200px';
5054
case 'medium':
5155
return '370px';
5256
case 'small':

modules/InfiniteStories/InfiniteStories.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type Props = {
1818
categories?: Category[];
1919
category?: Pick<Category, 'id'>;
2020
excludedStoryUuids?: Story['uuid'][];
21+
fullWidthFeaturedStory?: boolean;
2122
initialStories: ListStory[];
2223
isCategoryList?: boolean;
2324
layout: ThemeSettings['layout'];
@@ -49,6 +50,7 @@ export function InfiniteStories({
4950
categories,
5051
category,
5152
excludedStoryUuids,
53+
fullWidthFeaturedStory = false,
5254
initialStories,
5355
isCategoryList,
5456
layout,
@@ -73,6 +75,7 @@ export function InfiniteStories({
7375
<StoriesList
7476
categories={categories}
7577
category={category}
78+
fullWidthFeaturedStory={fullWidthFeaturedStory}
7679
isCategoryList={isCategoryList}
7780
layout={layout}
7881
newsroomName={newsroomName}

0 commit comments

Comments
 (0)