From 27d60d42ba794b2cafc1e25bc71c51529808e4d6 Mon Sep 17 00:00:00 2001 From: kudlajz Date: Thu, 4 Jul 2024 13:16:26 +0200 Subject: [PATCH 1/2] Add support for masonry layout --- app/[localeCode]/(index)/page.tsx | 12 +++- .../StaggeredLayout.module.scss | 10 +++ .../StaggeredLayout/StaggeredLayout.tsx | 63 +++++++++++++++++++ components/StaggeredLayout/index.tsx | 1 + modules/InfiniteStories/InfiniteStories.tsx | 34 +++++----- modules/InfiniteStories/StoriesList.tsx | 30 ++++++--- modules/Stories/Stories.tsx | 20 ++++-- theme-settings.ts | 2 + utils/parsePreviewSearchParams.ts | 10 +++ 9 files changed, 154 insertions(+), 28 deletions(-) create mode 100644 components/StaggeredLayout/StaggeredLayout.module.scss create mode 100644 components/StaggeredLayout/StaggeredLayout.tsx create mode 100644 components/StaggeredLayout/index.tsx diff --git a/app/[localeCode]/(index)/page.tsx b/app/[localeCode]/(index)/page.tsx index ac7db80de..1d5175384 100644 --- a/app/[localeCode]/(index)/page.tsx +++ b/app/[localeCode]/(index)/page.tsx @@ -6,6 +6,7 @@ import { app, generatePageMetadata, routing } from '@/adapters/server'; import { Contacts } from '@/modules/Contacts'; import { FeaturedCategories } from '@/modules/FeaturedCategories'; import { Stories } from '@/modules/Stories'; +import type { ThemeSettings } from 'theme-settings'; import { parseNumber, parsePreviewSearchParams } from 'utils'; interface Props { @@ -43,8 +44,9 @@ export default async function StoriesIndexPage({ params, searchParams }: Props) <> @@ -58,3 +60,11 @@ export default async function StoriesIndexPage({ params, searchParams }: Props) ); } + +function getPageSize(layout: ThemeSettings['layout']) { + if (layout === 'masonry') { + return DEFAULT_PAGE_SIZE + 1; + } + + return DEFAULT_PAGE_SIZE; +} diff --git a/components/StaggeredLayout/StaggeredLayout.module.scss b/components/StaggeredLayout/StaggeredLayout.module.scss new file mode 100644 index 000000000..5b97ed2c8 --- /dev/null +++ b/components/StaggeredLayout/StaggeredLayout.module.scss @@ -0,0 +1,10 @@ +.container { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: $grid-gutter-tablet; + + &.desktop { + grid-template-columns: 1fr 1fr 1fr; + column-gap: $grid-gutter-desktop; + } +} diff --git a/components/StaggeredLayout/StaggeredLayout.tsx b/components/StaggeredLayout/StaggeredLayout.tsx new file mode 100644 index 000000000..5c4a2c1ba --- /dev/null +++ b/components/StaggeredLayout/StaggeredLayout.tsx @@ -0,0 +1,63 @@ +'use client'; + +import classNames from 'classnames'; +import { Children, useMemo, useRef } from 'react'; +import type { PropsWithChildren } from 'react'; + +import { useDevice } from '@/hooks'; + +import styles from './StaggeredLayout.module.scss'; + +export function StaggeredLayout({ children }: PropsWithChildren<{}>) { + const isLayoutInitializedRef = useRef(false); + const { isMobile, isTablet } = useDevice(); + + const columnCount = useMemo(() => { + if (isMobile) { + isLayoutInitializedRef.current = false; + return 0; + } + + // Only use the calculated layout if we're not on mobile screen + isLayoutInitializedRef.current = true; + + if (isTablet) { + return 2; + } + + return 3; + }, [isMobile, isTablet]); + + const childrenInColumns = useMemo(() => { + const itemsCols = new Array(columnCount); + const items = Children.toArray(children); + + items.forEach((item, i) => { + const columnIndex = i % columnCount; + + if (!itemsCols[columnIndex]) { + itemsCols[columnIndex] = []; + } + + itemsCols[columnIndex].push(item); + }); + + return itemsCols; + }, [children, columnCount]); + + if (!isLayoutInitializedRef.current) { + return
{children}
; + } + + return ( +
+ {childrenInColumns.map((columnItems, i) => ( +
{columnItems}
+ ))} +
+ ); +} diff --git a/components/StaggeredLayout/index.tsx b/components/StaggeredLayout/index.tsx new file mode 100644 index 000000000..8e3429689 --- /dev/null +++ b/components/StaggeredLayout/index.tsx @@ -0,0 +1 @@ +export { StaggeredLayout } from './StaggeredLayout'; diff --git a/modules/InfiniteStories/InfiniteStories.tsx b/modules/InfiniteStories/InfiniteStories.tsx index 240ad726a..b261ceff8 100644 --- a/modules/InfiniteStories/InfiniteStories.tsx +++ b/modules/InfiniteStories/InfiniteStories.tsx @@ -7,6 +7,7 @@ import { useCallback } from 'react'; import { FormattedMessage, http, useLocale } from '@/adapters/client'; import { Button } from '@/components/Button'; +import type { ThemeSettings } from 'theme-settings'; import type { ListStory } from 'types'; import { StoriesList } from './StoriesList'; @@ -14,16 +15,17 @@ import { StoriesList } from './StoriesList'; import styles from './InfiniteStories.module.scss'; type Props = { - newsroomName: string; - initialStories: ListStory[]; - pageSize: number; - total: number; - category?: Pick; categories?: Category[]; - isCategoryList?: boolean; + category?: Pick; excludedStoryUuids?: Story['uuid'][]; + initialStories: ListStory[]; + isCategoryList?: boolean; + layout: ThemeSettings['layout']; + newsroomName: string; + pageSize: number; showDate: boolean; showSubtitle: boolean; + total: number; }; function fetchStories( @@ -43,16 +45,17 @@ function fetchStories( } export function InfiniteStories({ - newsroomName, - initialStories, - pageSize, - total, - category, categories, - isCategoryList, + category, excludedStoryUuids, + initialStories, + isCategoryList, + layout, + newsroomName, + pageSize, showDate, showSubtitle, + total, }: Props) { const locale = useLocale(); const { load, loading, data, done } = useInfiniteLoading( @@ -66,13 +69,14 @@ export function InfiniteStories({ return (
{!done && ( diff --git a/modules/InfiniteStories/StoriesList.tsx b/modules/InfiniteStories/StoriesList.tsx index 85e6b97ec..188162b58 100644 --- a/modules/InfiniteStories/StoriesList.tsx +++ b/modules/InfiniteStories/StoriesList.tsx @@ -5,6 +5,7 @@ import { translations } from '@prezly/theme-kit-nextjs'; import { useMemo } from 'react'; import { FormattedMessage, useLocale } from '@/adapters/client'; +import { StaggeredLayout } from '@/components/StaggeredLayout'; import { HighlightedStoryCard, StoryCard } from '@/components/StoryCards'; import type { ListStory } from 'types'; @@ -16,23 +17,25 @@ import Illustration from '@/public/images/no-stories-illustration.svg'; import styles from './StoriesList.module.scss'; type Props = { - newsroomName: string; - stories: ListStory[]; - category?: Pick; categories?: Category[]; + category?: Pick; isCategoryList?: boolean; + layout?: 'grid' | 'masonry'; + newsroomName: string; showDate: boolean; showSubtitle: boolean; + stories: ListStory[]; }; export function StoriesList({ - newsroomName, - stories, - category, categories = [], + category, isCategoryList = false, + layout = 'grid', + newsroomName, showDate, showSubtitle, + stories, }: Props) { const locale = useLocale(); const hasCategories = categories.length > 0; @@ -87,7 +90,7 @@ export function StoriesList({ locale={locale} /> )} - {restStories.length > 0 && ( + {restStories.length > 0 && layout === 'grid' && (
{restStories.map((story, index) => ( )} + {restStories.length > 0 && layout === 'masonry' && ( + + {restStories.map((story) => ( + + ))} + + )} ); } diff --git a/modules/Stories/Stories.tsx b/modules/Stories/Stories.tsx index 8061310e6..7575a42a1 100644 --- a/modules/Stories/Stories.tsx +++ b/modules/Stories/Stories.tsx @@ -2,18 +2,27 @@ import type { Category } from '@prezly/sdk'; import type { Locale } from '@prezly/theme-kit-nextjs'; import { app } from '@/adapters/server'; +import type { ThemeSettings } from 'theme-settings'; import { InfiniteStories } from '../InfiniteStories'; interface Props { categoryId: Category['id'] | undefined; + layout: ThemeSettings['layout']; localeCode: Locale.Code; pageSize: number; showDate: boolean; showSubtitle: boolean; } -export async function Stories({ categoryId, localeCode, pageSize, showDate, showSubtitle }: Props) { +export async function Stories({ + categoryId, + layout, + localeCode, + pageSize, + showDate, + showSubtitle, +}: Props) { const newsroom = await app().newsroom(); const languageSettings = await app().languageOrDefault(localeCode); @@ -26,15 +35,16 @@ export async function Stories({ categoryId, localeCode, pageSize, showDate, show return ( ); } diff --git a/theme-settings.ts b/theme-settings.ts index 6b8566bde..d04fe92e2 100644 --- a/theme-settings.ts +++ b/theme-settings.ts @@ -13,6 +13,7 @@ export interface ThemeSettings { header_background_color: string; header_image_placement: 'above' | 'below'; header_link_color: string; + layout: 'grid' | 'masonry'; logo_size: string; main_logo: string | null; main_site_url: string | null; @@ -28,6 +29,7 @@ export const DEFAULT_THEME_SETTINGS: ThemeSettings = { header_background_color: '#ffffff', header_image_placement: 'below', header_link_color: '#4b5563', + layout: 'grid', logo_size: 'medium', main_logo: null, main_site_url: null, diff --git a/utils/parsePreviewSearchParams.ts b/utils/parsePreviewSearchParams.ts index 673078573..ad41268c0 100644 --- a/utils/parsePreviewSearchParams.ts +++ b/utils/parsePreviewSearchParams.ts @@ -15,6 +15,7 @@ export function parsePreviewSearchParams( header_background_color, header_image_placement, header_link_color, + layout, logo_size, main_site_url, show_date, @@ -29,6 +30,7 @@ export function parsePreviewSearchParams( header_background_color, header_image_placement: parseHeaderImagePlacement(header_image_placement), header_link_color, + layout: parseLayout(layout), logo_size, main_site_url, show_date: show_date ? parseBoolean(show_date) : undefined, @@ -49,3 +51,11 @@ function parseHeaderImagePlacement(headerImagePlacement: string | undefined) { return undefined; } + +function parseLayout(layout: string | undefined) { + if (layout === 'grid' || layout === 'masonry') { + return layout; + } + + return undefined; +} From fce46f16f500ff3591f8d3e16684c8e80afaf49f Mon Sep 17 00:00:00 2001 From: kudlajz Date: Thu, 4 Jul 2024 13:30:21 +0200 Subject: [PATCH 2/2] Provide layout prop to Category page as well --- app/[localeCode]/(index)/page.tsx | 14 ++----------- app/[localeCode]/category/[slug]/page.tsx | 12 ++++++++--- modules/Category/Category.tsx | 25 ++++++++++++++++------- utils/getStoryListPageSize.ts | 11 ++++++++++ utils/index.ts | 1 + 5 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 utils/getStoryListPageSize.ts diff --git a/app/[localeCode]/(index)/page.tsx b/app/[localeCode]/(index)/page.tsx index 1d5175384..430b08331 100644 --- a/app/[localeCode]/(index)/page.tsx +++ b/app/[localeCode]/(index)/page.tsx @@ -1,13 +1,11 @@ import type { Locale } from '@prezly/theme-kit-nextjs'; -import { DEFAULT_PAGE_SIZE } from '@prezly/theme-kit-nextjs'; import type { Metadata } from 'next'; import { app, generatePageMetadata, routing } from '@/adapters/server'; import { Contacts } from '@/modules/Contacts'; import { FeaturedCategories } from '@/modules/FeaturedCategories'; import { Stories } from '@/modules/Stories'; -import type { ThemeSettings } from 'theme-settings'; -import { parseNumber, parsePreviewSearchParams } from 'utils'; +import { getStoryListPageSize, parseNumber, parsePreviewSearchParams } from 'utils'; interface Props { params: { @@ -46,7 +44,7 @@ export default async function StoriesIndexPage({ params, searchParams }: Props) categoryId={searchParams.category ? parseNumber(searchParams.category) : undefined} layout={themeSettings.layout} localeCode={params.localeCode} - pageSize={getPageSize(themeSettings.layout)} + pageSize={getStoryListPageSize(themeSettings.layout)} showDate={themeSettings.show_date} showSubtitle={themeSettings.show_subtitle} /> @@ -60,11 +58,3 @@ export default async function StoriesIndexPage({ params, searchParams }: Props) ); } - -function getPageSize(layout: ThemeSettings['layout']) { - if (layout === 'masonry') { - return DEFAULT_PAGE_SIZE + 1; - } - - return DEFAULT_PAGE_SIZE; -} diff --git a/app/[localeCode]/category/[slug]/page.tsx b/app/[localeCode]/category/[slug]/page.tsx index 2d7b63777..4864911c1 100644 --- a/app/[localeCode]/category/[slug]/page.tsx +++ b/app/[localeCode]/category/[slug]/page.tsx @@ -1,18 +1,19 @@ import { Category } from '@prezly/sdk'; import type { Locale } from '@prezly/theme-kit-nextjs'; -import { DEFAULT_PAGE_SIZE } from '@prezly/theme-kit-nextjs'; import type { Metadata } from 'next'; import { notFound } from 'next/navigation'; import { app, generateCategoryPageMetadata, routing } from '@/adapters/server'; import { BroadcastTranslations } from '@/modules/Broadcast'; import { Category as CategoryIndex } from '@/modules/Category'; +import { getStoryListPageSize, parsePreviewSearchParams } from 'utils'; interface Props { params: { localeCode: Locale.Code; slug: NonNullable; }; + searchParams: Record; } async function resolve({ localeCode, slug }: Props['params']) { @@ -31,16 +32,21 @@ export async function generateMetadata({ params }: Props): Promise { return generateCategoryPageMetadata({ locale: localeCode, category }); } -export default async function CategoryPage({ params }: Props) { +export default async function CategoryPage({ params, searchParams }: Props) { const { category, translatedCategory } = await resolve(params); + const themeSettings = await app().themeSettings(); + const settings = parsePreviewSearchParams(searchParams, themeSettings); return ( <> ); diff --git a/modules/Category/Category.tsx b/modules/Category/Category.tsx index 81cccd33d..6217818ee 100644 --- a/modules/Category/Category.tsx +++ b/modules/Category/Category.tsx @@ -2,16 +2,27 @@ import type { Category as CategoryType, TranslatedCategory } from '@prezly/sdk'; import { app } from '@/adapters/server'; import { PageTitle } from '@/components/PageTitle'; +import type { ThemeSettings } from 'theme-settings'; import { InfiniteStories } from '../InfiniteStories'; interface Props { category: CategoryType; + layout: ThemeSettings['layout']; pageSize: number; + showDate: boolean; + showSubtitle: boolean; translatedCategory: TranslatedCategory; } -export async function Category({ category, pageSize, translatedCategory }: Props) { +export async function Category({ + category, + layout, + pageSize, + showDate, + showSubtitle, + translatedCategory, +}: Props) { const { stories, pagination } = await app().stories({ limit: pageSize, category, @@ -20,20 +31,20 @@ export async function Category({ category, pageSize, translatedCategory }: Props const newsroom = await app().newsroom(); const languageSettings = await app().languageOrDefault(translatedCategory.locale); - const settings = await app().themeSettings(); return ( <> ); diff --git a/utils/getStoryListPageSize.ts b/utils/getStoryListPageSize.ts new file mode 100644 index 000000000..cb804879e --- /dev/null +++ b/utils/getStoryListPageSize.ts @@ -0,0 +1,11 @@ +import { DEFAULT_PAGE_SIZE } from '@prezly/theme-kit-nextjs'; + +import type { ThemeSettings } from 'theme-settings'; + +export function getStoryListPageSize(layout: ThemeSettings['layout']) { + if (layout === 'masonry') { + return DEFAULT_PAGE_SIZE + 1; + } + + return DEFAULT_PAGE_SIZE; +} diff --git a/utils/index.ts b/utils/index.ts index 4d95ca6a1..833aedaa4 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -1,3 +1,4 @@ +export { getStoryListPageSize } from './getStoryListPageSize'; export { getUploadcareImage } from './getUploadcareImage'; export { onPlainLeftClick } from './onPlainLeftClick'; export { parseBoolean } from './parseBoolean';