Skip to content

Commit

Permalink
Merge pull request #1109 from prezly/feature/dev-12642-implement-feat…
Browse files Browse the repository at this point in the history
…ured-categories-filtering-on-homepage

[DEV-12642] Feature - Display featured categories on homepage and allow filtering by them
  • Loading branch information
kudlajz authored Mar 19, 2024
2 parents 03edf8d + b81c546 commit 36d4975
Show file tree
Hide file tree
Showing 12 changed files with 251 additions and 67 deletions.
13 changes: 11 additions & 2 deletions app/[localeCode]/(index)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ interface Props {
params: {
localeCode: Locale.Code;
};
searchParams: {
category?: string;
};
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
Expand All @@ -29,6 +32,12 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
);
}

export default async function StoriesIndexPage({ params }: Props) {
return <Stories localeCode={params.localeCode} pageSize={DEFAULT_PAGE_SIZE} />;
export default async function StoriesIndexPage({ params, searchParams }: Props) {
return (
<Stories
categoryId={searchParams.category ? parseInt(searchParams.category, 10) : undefined}
localeCode={params.localeCode}
pageSize={DEFAULT_PAGE_SIZE}
/>
);
}
1 change: 1 addition & 0 deletions modules/Category/Category.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export async function Category({ category, pageSize }: Props) {
category={category}
total={pagination.matched_records_number}
newsroomName={languageSettings.company_information.name || newsroom.name}
isCategoryList
/>
</>
);
Expand Down
20 changes: 15 additions & 5 deletions modules/InfiniteStories/InfiniteStories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import type { Category } from '@prezly/sdk';
import type { Category, Story } from '@prezly/sdk';
import type { Locale } from '@prezly/theme-kit-nextjs';
import { translations, useInfiniteLoading } from '@prezly/theme-kit-nextjs';
import { useCallback } from 'react';
Expand All @@ -19,19 +19,24 @@ type Props = {
pageSize: number;
total: number;
category?: Pick<Category, 'id'>;
categories?: Category[];
isCategoryList?: boolean;
excludedStoryUuids?: Story['uuid'][];
};

function fetchStories(
localeCode: Locale.Code,
offset: number,
limit: number,
category: Props['category'],
excludedStoryUuids: Story['uuid'][] | undefined,
) {
return http.get<{ data: ListStory[]; total: number }>('/api/stories', {
limit,
offset,
locale: localeCode,
category: category?.id,
query: excludedStoryUuids && JSON.stringify({ uuid: { $nin: excludedStoryUuids } }),
});
}

Expand All @@ -41,22 +46,27 @@ export function InfiniteStories({
pageSize,
total,
category,
categories,
isCategoryList,
excludedStoryUuids,
}: Props) {
const locale = useLocale();
const { load, loading, data, done } = useInfiniteLoading(
useCallback(
(offset) => fetchStories(locale, offset, pageSize, category),
[locale, pageSize, category],
(offset) => fetchStories(locale, offset, pageSize, category, excludedStoryUuids),
[category, excludedStoryUuids, locale, pageSize],
),
{ data: initialStories, total },
);

return (
<div>
<StoriesList
newsoomName={newsroomName}
newsroomName={newsroomName}
stories={data}
isCategoryList={Boolean(category)}
category={category}
categories={categories}
isCategoryList={isCategoryList}
/>

{!done && (
Expand Down
8 changes: 4 additions & 4 deletions modules/InfiniteStories/StoriesList.module.scss
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
.highlightedStoriesContainer {
margin-bottom: $spacing-8;
margin-bottom: $spacing-7;
}

@include desktop-up {
margin-bottom: $spacing-9;
}
.filtersContainer {
margin-bottom: $spacing-6;
}

.storiesContainer {
Expand Down
32 changes: 26 additions & 6 deletions modules/InfiniteStories/StoriesList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import type { Category } from '@prezly/sdk';
import { translations } from '@prezly/theme-kit-nextjs';
import { useMemo } from 'react';

Expand All @@ -8,31 +9,42 @@ import { HighlightedStoryCard, StoryCard } from '@/components/StoryCards';
import type { ListStory } from 'types';

import { useStoryCardLayout } from './lib';
import { CategoriesFilters } from './ui';

import Illustration from '@/public/images/no-stories-illustration.svg';

import styles from './StoriesList.module.scss';

type Props = {
newsoomName: string;
newsroomName: string;
stories: ListStory[];
category?: Pick<Category, 'id'>;
categories?: Category[];
isCategoryList?: boolean;
};

export function StoriesList({ newsoomName, stories, isCategoryList = false }: Props) {
export function StoriesList({
newsroomName,
stories,
category,
categories = [],
isCategoryList = false,
}: Props) {
const locale = useLocale();
const hasCategories = categories.length > 0;

const [highlightedStories, restStories] = useMemo(() => {
if (isCategoryList) {
return [[], stories];
}
// When there are only two stories, they should be both displayed as highlighted
if (stories.length === 2) {
// When there are only two stories and no categories to filter,
// they should be both displayed as highlighted
if (stories.length === 2 && !hasCategories) {
return [stories, []];
}

return [stories.slice(0, 1), stories.slice(1)];
}, [isCategoryList, stories]);
}, [hasCategories, isCategoryList, stories]);

const getStoryCardSize = useStoryCardLayout(isCategoryList, restStories.length);

Expand All @@ -44,7 +56,7 @@ export function StoriesList({ newsoomName, stories, isCategoryList = false }: Pr
<FormattedMessage
locale={locale}
for={translations.noStories.title}
values={{ newsroom: newsoomName }}
values={{ newsroom: newsroomName }}
/>
</h1>
<p className={styles.noStoriesSubtitle}>
Expand All @@ -63,6 +75,14 @@ export function StoriesList({ newsoomName, stories, isCategoryList = false }: Pr
))}
</div>
)}
{hasCategories && (
<CategoriesFilters
activeCategory={category}
categories={categories}
className={styles.filtersContainer}
locale={locale}
/>
)}
{restStories.length > 0 && (
<div className={styles.storiesContainer}>
{restStories.map((story, index) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.title {
margin-bottom: $spacing-5;
}

.filters {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: $spacing-2;
}

.badge {
display: flex;
align-items: center;
flex-shrink: 0;
padding: $spacing-2 $spacing-3;
text-decoration: none;
color: $color-text;
background: $color-base-white;
border: 1px solid $color-base-200;
border-radius: 100px;

&.active {
color: var(--prezly-accent-color-button-text);
background-color: var(--prezly-accent-color);
border-color: var(--prezly-accent-color);
}
}
51 changes: 51 additions & 0 deletions modules/InfiniteStories/ui/CategoriesFilters/CategoriesFilters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { Category } from '@prezly/sdk/dist/types';
import type { Locale } from '@prezly/theme-kit-nextjs';
import classNames from 'classnames';
import Link from 'next/link';

import { PageTitle } from '@/components/PageTitle';

import styles from './CategoriesFilters.module.scss';

interface Props {
activeCategory: Pick<Category, 'id'> | undefined;
categories: Category[];
className?: string;
locale: Locale.Code;
}

export function CategoriesFilters({ activeCategory, categories, className, locale }: Props) {
return (
<div className={className}>
<PageTitle className={styles.title} title="Latest stories" />
<div className={styles.filters}>
<Filter isActive={activeCategory === undefined}>All stories</Filter>
{categories.map(({ id, display_name, i18n }) => (
<Filter categoryId={id} isActive={activeCategory?.id === id} key={id}>
{i18n[locale]?.name || display_name}
</Filter>
))}
</div>
</div>
);
}

export function Filter(props: {
categoryId?: Category['id'];
children: string;
isActive: boolean;
}) {
const { categoryId, children, isActive } = props;

return (
<Link
href={categoryId ? { query: { category: categoryId } } : { query: { category: null } }}
className={classNames(styles.badge, {
[styles.active]: isActive,
})}
scroll={false}
>
{children}
</Link>
);
}
1 change: 1 addition & 0 deletions modules/InfiniteStories/ui/CategoriesFilters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CategoriesFilters';
1 change: 1 addition & 0 deletions modules/InfiniteStories/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CategoriesFilters';
71 changes: 67 additions & 4 deletions modules/Stories/Stories.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,91 @@
import type { Category } from '@prezly/sdk';
import type { Locale } from '@prezly/theme-kit-nextjs';

import { app } from '@/adapters/server';

import { InfiniteStories } from '../InfiniteStories';

interface Props {
categoryId: Category['id'] | undefined;
localeCode: Locale.Code;
pageSize: number;
}

export async function Stories({ localeCode, pageSize }: Props) {
export async function Stories({ categoryId, localeCode, pageSize }: Props) {
const newsroom = await app().newsroom();
const languageSettings = await app().languageOrDefault(localeCode);
const { stories, pagination } = await app().stories({
limit: pageSize,
locale: { code: localeCode },
const categories = await app().categories();
const featuredCategories = categories.filter(
({ is_featured, i18n }) => is_featured && i18n[localeCode]?.public_stories_number > 0,
);
const hasFeaturedCategories = featuredCategories.length > 0;

const { stories, pagination, excludedStoryUuids } = await getStories({
categoryId,
hasFeaturedCategories,
localeCode,
pageSize,
});

return (
<InfiniteStories
key={categoryId}
category={categoryId ? { id: categoryId } : undefined}
categories={featuredCategories}
newsroomName={languageSettings.company_information.name || newsroom.name}
pageSize={pageSize}
initialStories={stories}
total={pagination.matched_records_number}
excludedStoryUuids={excludedStoryUuids}
/>
);
}

async function getStories({
categoryId,
hasFeaturedCategories,
localeCode,
pageSize,
}: {
categoryId: number | undefined;
hasFeaturedCategories: boolean;
localeCode: Locale.Code;
pageSize: number;
}) {
if (hasFeaturedCategories) {
const { stories: pinnedOrMostRecentStories } = await app().stories({
limit: 1,
locale: { code: localeCode },
});

const pinnedOrMostRecentStory = pinnedOrMostRecentStories[0];

// Exclude the pinned/most recent story from the initial stories list
// so it's not duplicated below the categories filters
const query = pinnedOrMostRecentStory
? {
uuid: { $nin: [pinnedOrMostRecentStory.uuid] },
}
: undefined;

const { stories, pagination } = await app().stories({
category: categoryId ? { id: categoryId } : undefined,
limit: pageSize - 1,
locale: { code: localeCode },
query,
});

return {
stories: [...pinnedOrMostRecentStories, ...stories],
pagination,
excludedStoryUuids: pinnedOrMostRecentStories.map((story) => story.uuid),
};
}

const { stories, pagination } = await app().stories({
limit: pageSize,
locale: { code: localeCode },
});

return { stories, pagination };
}
Loading

0 comments on commit 36d4975

Please sign in to comment.