diff --git a/src/app/components/TabbedTopics/index.tsx b/src/app/components/TabbedTopics/index.tsx new file mode 100644 index 00000000000..c5eb1df95ec --- /dev/null +++ b/src/app/components/TabbedTopics/index.tsx @@ -0,0 +1,257 @@ +import { useState, use, useContext } from 'react'; +import { ScrollableNavigation } from '#app/legacy/psammead/psammead-navigation/src/ScrollableNavigation'; +import { css, Theme } from '@emotion/react'; +import { Summary } from '#app/models/types/curationData'; +import SectionLabel from '#psammead/psammead-section-label/src'; +import { GREY_2 } from '#app/components/ThemeProvider/palette'; +import { ServiceContext } from '#app/contexts/ServiceContext'; +import CurationGrid from '../Curation/CurationGrid'; + +const tabStyles = { + container: css({ + display: 'flex', + borderBottom: '1px solid #ccc', + marginBottom: '1rem', + gap: '0.25rem', + minHeight: '3.5rem', + width: '100%', + overflowX: 'auto', + overflowY: 'hidden', + position: 'relative', + scrollBehavior: 'smooth', + touchAction: 'pan-x', + // Hide scrollbars + scrollbarWidth: 'none', + '-ms-overflow-style': 'none', + '&::-webkit-scrollbar': { + display: 'none', + }, + // Remove white-space: nowrap here, handled by ScrollableNavigation + }), + wrapper: css({ + backgroundColor: '#fff', + padding: '1rem 0', + marginLeft: '1rem', + marginRight: '1rem', + }), + tab: (theme: Theme) => + css({ + padding: '0.5rem 1.5rem', + cursor: 'pointer', + border: '1px solid #ccc', + borderBottom: 'none', + background: '#fff', + fontWeight: 600, + color: '#222', + outline: 'none', + borderTopLeftRadius: '6px', + borderTopRightRadius: '6px', + marginBottom: '-1px', + zIndex: 1, + position: 'relative', + flex: '0 0 auto', + whiteSpace: 'normal', + overflow: 'visible', + textOverflow: 'unset', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + textAlign: 'center', + lineHeight: '1.2', + height: 'auto', + fontSize: '1rem', + overflowWrap: 'break-word', + wordBreak: 'break-word', + ':focus': { + borderColor: theme.palette.POSTBOX, + }, + }), + activeTab: (theme: Theme) => + css({ + borderBottom: `4px solid ${theme.palette.POSTBOX}`, + color: theme.palette.POSTBOX, + fontWeight: 700, + zIndex: 2, + }), +}; + +interface TopicCuration { + title: string; + summaries: Summary[]; + link?: string; +} + +interface TabbedTopicsProps { + topics: TopicCuration[]; + css?: import('@emotion/react').Interpolation; +} + +const TabbedTopics = ({ topics }: TabbedTopicsProps) => { + const [activeIndex, setActiveIndex] = useState(0); + const { translations, dir } = useContext(ServiceContext); + const heading = translations?.relatedTopics ?? 'Related Topics'; + + if (!topics || topics.length === 0) return null; + + return ( +
+
+
+ + {heading} + +
+
+ +
+ {topics.map((topic, idx) => ( + + ))} +
+
+ {/* Gradient overlay for scroll indication, border grey */} +
+ {/* Red ellipsis icon above scroll edge */} +
+ +
+
+ +
+
+ ); +}; + +export default TabbedTopics; diff --git a/src/app/models/types/optimo.ts b/src/app/models/types/optimo.ts index 3fb8028d8e0..f45f4cb3a93 100644 --- a/src/app/models/types/optimo.ts +++ b/src/app/models/types/optimo.ts @@ -131,6 +131,30 @@ export type ArticlePromo = { }; }; +export type TopicTagsCurations = { + topicId: string; + curation: { + title: string; + curationType: string; + curationId: string; + link: string; + summaries: Array<{ + type: string; + isLive: boolean; + title: string; + firstPublished: string; + lastPublished: string; + link: string; + imageUrl: string; + description: string; + imageAlt: string; + isPortraitImage: boolean; + id: string; + duration?: string; + }>; + }; +}; + export type SecondaryColumn = { billboardCuration?: Curation; mediaCuration?: Curation; @@ -167,4 +191,5 @@ export type Article = { recommendations?: Recommendation[]; relatedContent?: RelatedContent; portraitVideoItems?: PortraitVideoItems; + topicTagsCurations?: TopicTagsCurations[]; }; diff --git a/src/app/pages/ArticlePage/ArticlePage.tsx b/src/app/pages/ArticlePage/ArticlePage.tsx index bad4efe99cb..e70490f1bf0 100644 --- a/src/app/pages/ArticlePage/ArticlePage.tsx +++ b/src/app/pages/ArticlePage/ArticlePage.tsx @@ -33,7 +33,7 @@ import { getLang, } from '#lib/utilities/parseAssetData'; import filterForBlockType from '#lib/utilities/blockHandlers'; -import RelatedTopics from '#app/components/RelatedTopics'; +import TabbedTopics from '#app/components/TabbedTopics'; import NielsenAnalytics from '#containers/NielsenAnalytics'; import InlinePodcastPromo from '#containers/PodcastPromo/Inline'; import { @@ -196,7 +196,7 @@ const getContinueReadingButton = const ArticlePage = ({ pageData }: { pageData: Article }) => { const [showAllContent, setShowAllContent] = useState(false); const { isApp, isAmp, isLite } = use(RequestContext); - + console.log('pageData in ArticlePage is', pageData); const { articleAuthor, isTrustProjectParticipant, @@ -262,6 +262,8 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { const topics = pageData?.metadata?.topics ?? []; const blocks = pageData?.content?.model?.blocks ?? []; const mediaCurationContent = pageData?.secondaryColumn?.mediaCuration; + const { topicTagsCurations } = pageData; + console.log('topicTagsCuration in ArticlePage is', topicTagsCurations); const startsWithHeading = blocks?.[0]?.type === 'headline' || false; const bylineBlock = blocks.find( @@ -469,15 +471,20 @@ const ArticlePage = ({ pageData }: { pageData: Article }) => { {showTopics && ( - ({ + title: t.curation.title, + summaries: t.curation.summaries, + link: t.curation.link, + })) ?? [] + } /> )} {showPortraitVideoCarousel && ( diff --git a/ws-nextjs-app/pages/[service]/articles/[[...variant]].page.tsx b/ws-nextjs-app/pages/[service]/articles/[[...variant]].page.tsx index 3ac9914b90d..fa237dbd161 100644 --- a/ws-nextjs-app/pages/[service]/articles/[[...variant]].page.tsx +++ b/ws-nextjs-app/pages/[service]/articles/[[...variant]].page.tsx @@ -19,6 +19,7 @@ const PageTypeToRender = withOptimizelyProvider(function PageTypeToRender({ pageType, ...rest }: PageProps) { + console.log('rest in PageTypeToRender is', rest); switch (pageType) { case ARTICLE_PAGE: return ; diff --git a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts index 168ab7a8a81..762544323a1 100644 --- a/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts +++ b/ws-nextjs-app/pages/[service]/articles/handleArticleRoute.ts @@ -111,6 +111,7 @@ export default async (context: GetServerSidePropsContext) => { billboardCuration = null, mediaCuration = null, portraitVideoItems = null, + topicTagsCurations = null, } = secondaryData || {}; const transformedArticleData = transformPageData()(article); @@ -122,7 +123,8 @@ export default async (context: GetServerSidePropsContext) => { }); const derivedPageType = getDerivedArticleType(article.metadata); - + console.log('article in handleArticleRoute is', secondaryData); + console.log(JSON.stringify(secondaryData.topicTagsCurations, null, 2)); return { props: { country, @@ -138,6 +140,7 @@ export default async (context: GetServerSidePropsContext) => { }, mostRead, portraitVideoItems, + topicTagsCurations, }, pageType: derivedPageType, pathname: resolvedUrlWithoutQuery,