Skip to content

Commit 98e8f25

Browse files
authored
Merge pull request #1180 from prezly/feature/dev-13389-implement-category-strip-in-bea
[DEV-13389] Implement category bar & boxed story card tweaks
2 parents ba1fda0 + cd7087d commit 98e8f25

File tree

11 files changed

+339
-107
lines changed

11 files changed

+339
-107
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
$more-button-width: 84px;
2+
3+
:export {
4+
/* stylelint-disable-next-line property-no-unknown */
5+
MORE_BUTTON_WIDTH: $more-button-width;
6+
}
7+
8+
.wrapper {
9+
background-color: var(--prezly-header-background-color);
10+
color: var(--prezly-header-link-color);
11+
border-top: 1px solid var(--prezly-border-color);
12+
border-bottom: 1px solid var(--prezly-border-color);
13+
z-index: 1;
14+
15+
@include mobile-only {
16+
display: none;
17+
}
18+
}
19+
20+
.container {
21+
@include container;
22+
23+
display: flex;
24+
align-items: center;
25+
}
26+
27+
.link {
28+
display: inline-flex;
29+
align-items: center;
30+
padding: $spacing-2;
31+
color: var(--prezly-header-link-color);
32+
border: 1px solid transparent;
33+
font-weight: $font-weight-medium;
34+
line-height: $line-height-xs;
35+
white-space: nowrap;
36+
text-decoration: none;
37+
38+
&.active {
39+
border-bottom: 2px solid var(--prezly-header-link-color);
40+
border-radius: 0;
41+
}
42+
43+
&:hover {
44+
opacity: .8;
45+
}
46+
47+
&:active {
48+
border-color: transparent;
49+
}
50+
51+
&:first-child {
52+
margin-left: -$spacing-2;
53+
}
54+
}
55+
56+
.more {
57+
color: var(--prezly-header-link-color) !important;
58+
}
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { TranslatedCategory } from '@prezly/sdk';
2+
import { FormattedMessage, translations } from '@prezly/theme-kit-nextjs';
3+
import { useMeasure } from '@react-hookz/web';
4+
import classNames from 'classnames';
5+
import { useParams } from 'next/navigation';
6+
import { useMemo } from 'react';
7+
8+
import { useLocale } from 'adapters/client';
9+
10+
import { Dropdown } from '../Dropdown';
11+
import { Link } from '../Link';
12+
13+
import { CategoryItem } from './CategoryItem';
14+
15+
import styles from './CategoriesBar.module.scss';
16+
17+
const MORE_BUTTON_WIDTH = +styles.MORE_BUTTON_WIDTH.replace('px', '');
18+
19+
type Props = {
20+
translatedCategories: TranslatedCategory[];
21+
};
22+
23+
export function CategoriesBar({ translatedCategories }: Props) {
24+
const locale = useLocale();
25+
const params = useParams();
26+
const [measurements, containerRef] = useMeasure<HTMLDivElement>();
27+
const containerWidth = measurements?.width ?? 0;
28+
29+
const [visibleCategories, hiddenCategories] = useMemo<
30+
[TranslatedCategory[], TranslatedCategory[]]
31+
>(() => {
32+
if (!containerRef.current) {
33+
return [translatedCategories, []];
34+
}
35+
36+
const { paddingLeft, paddingRight } = getComputedStyle(containerRef.current);
37+
const containerWidthWithoutPadding =
38+
containerWidth - parseInt(paddingLeft) - parseInt(paddingRight);
39+
40+
if (containerRef.current.scrollWidth <= containerWidthWithoutPadding) {
41+
return [translatedCategories, []];
42+
}
43+
44+
let index = 0;
45+
let width = 0;
46+
const widthLimit = containerWidthWithoutPadding - MORE_BUTTON_WIDTH;
47+
const nodesArray = Array.from(containerRef.current.children);
48+
49+
for (let i = 0; i < nodesArray.length; i += 1) {
50+
const { width: nodeWidth } = nodesArray[i].getBoundingClientRect();
51+
52+
if (nodeWidth + width > widthLimit) {
53+
break;
54+
}
55+
width += nodeWidth;
56+
index = i + 1;
57+
}
58+
59+
return [translatedCategories.slice(0, index), translatedCategories.slice(index)];
60+
}, [translatedCategories, containerRef, containerWidth]);
61+
62+
if (!visibleCategories.length) {
63+
return null;
64+
}
65+
66+
return (
67+
<div className={styles.wrapper}>
68+
<div className={styles.container} ref={containerRef}>
69+
{visibleCategories.map((category) => (
70+
<Link
71+
key={category.id}
72+
href={{
73+
routeName: 'category',
74+
params: { slug: category.slug },
75+
}}
76+
className={classNames(styles.link, {
77+
[styles.active]: category.slug === params.slug,
78+
})}
79+
title={category.description || undefined}
80+
>
81+
{category.name}
82+
</Link>
83+
))}
84+
{hiddenCategories.length > 0 && (
85+
<Dropdown
86+
label={<FormattedMessage locale={locale} for={translations.actions.more} />}
87+
buttonClassName={styles.more}
88+
>
89+
{hiddenCategories.map((category) => (
90+
<CategoryItem category={category} key={category.id} />
91+
))}
92+
</Dropdown>
93+
)}
94+
</div>
95+
</div>
96+
);
97+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.title {
2+
display: block;
3+
font-weight: $font-weight-medium;
4+
}
5+
6+
.description {
7+
@include text-small;
8+
@include line-clamp(2);
9+
10+
display: block;
11+
margin-top: $spacing-1;
12+
color: var(--prezly-text-color-secondary);
13+
font-weight: $font-weight-regular;
14+
15+
@include mobile-only {
16+
display: none;
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { TranslatedCategory } from '@prezly/sdk';
2+
3+
import { DropdownItem } from '@/components/Dropdown';
4+
5+
import styles from './CategoryItem.module.scss';
6+
7+
type Props = {
8+
category: TranslatedCategory;
9+
};
10+
11+
export function CategoryItem({ category }: Props) {
12+
const { name, description } = category;
13+
14+
return (
15+
<DropdownItem href={{ routeName: 'category', params: { slug: category.slug } }}>
16+
<span className={styles.title}>{name}</span>
17+
{description && <span className={styles.description}>{description}</span>}
18+
</DropdownItem>
19+
);
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { CategoryItem } from './CategoryItem';

components/CategoriesBar/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { CategoriesBar } from './CategoriesBar';

components/StoryCards/StoryCard.module.scss

+5-1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@
7272

7373
@include tablet-up {
7474
@include ensure-max-text-height(3, 140%);
75+
76+
&.expanded {
77+
@include ensure-max-text-height(4, 140%);
78+
}
7579
}
7680
}
7781

@@ -184,7 +188,7 @@
184188
}
185189

186190
.title {
187-
@include heading-2;
191+
@include heading-3;
188192
}
189193

190194
@include mobile-only {

components/StoryCards/StoryCard.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,11 @@ export function StoryCard({
8585
/>
8686
)}
8787
</div>
88-
<HeadingTag className={styles.title}>
88+
<HeadingTag
89+
className={classNames(styles.title, {
90+
[styles.expanded]: !showSubtitle || !subtitle,
91+
})}
92+
>
8993
<Link
9094
href={{ routeName: 'story', params: { slug } }}
9195
className={styles.titleLink}

modules/Header/Header.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export async function Header({ localeCode }: Props) {
3232
translatedCategories={displayedCategories}
3333
displayedLanguages={displayedLanguages.length}
3434
displayedGalleries={newsroom.public_galleries_number}
35+
categoriesLayout={settings.categories_layout}
3536
logoSize={settings.logo_size}
3637
mainSiteUrl={settings.main_site_url}
3738
>

0 commit comments

Comments
 (0)