From 1f953568e38531380a82b1edb4912b238392abc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Wed, 11 Mar 2026 17:56:58 +0000 Subject: [PATCH 1/2] wip --- src/app/components/TabbedTopics/index.tsx | 108 ++++++++++++++++++ src/app/models/types/optimo.ts | 25 ++++ src/app/pages/ArticlePage/ArticlePage.tsx | 17 ++- .../articles/[[...variant]].page.tsx | 1 + .../[service]/articles/handleArticleRoute.ts | 5 +- 5 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 src/app/components/TabbedTopics/index.tsx diff --git a/src/app/components/TabbedTopics/index.tsx b/src/app/components/TabbedTopics/index.tsx new file mode 100644 index 00000000000..d1e5458c17a --- /dev/null +++ b/src/app/components/TabbedTopics/index.tsx @@ -0,0 +1,108 @@ +import { useState } from 'react'; +import { css, Theme } from '@emotion/react'; +import { Summary } from '#app/models/types/curationData'; +import CurationGrid from '../Curation/CurationGrid'; + +const tabStyles = { + container: css({ + display: 'flex', + borderBottom: '1px solid #ccc', + marginBottom: '1rem', + gap: '0.25rem', + }), + wrapper: css({ + backgroundColor: '#fff', + padding: '1rem 0', + marginLeft: '1rem', + marginRight: '1rem', + }), + tab: (theme: Theme) => + css({ + padding: '0.5rem 1rem', + 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', + ':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, css: customCss }: TabbedTopicsProps) => { + const [activeIndex, setActiveIndex] = useState(0); + + if (!topics || topics.length === 0) return null; + + return ( +
+
+
+ {topics.map((topic, idx) => ( + + ))} +
+
+ +
+
+
+ ); +}; + +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, From f04a99bab061b4efb13cbbcc46a33d468267b578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CLilyL0u=E2=80=9D?= Date: Wed, 11 Mar 2026 19:11:57 +0000 Subject: [PATCH 2/2] scrollable --- src/app/components/TabbedTopics/index.tsx | 187 +++++++++++++++++++--- 1 file changed, 168 insertions(+), 19 deletions(-) diff --git a/src/app/components/TabbedTopics/index.tsx b/src/app/components/TabbedTopics/index.tsx index d1e5458c17a..c5eb1df95ec 100644 --- a/src/app/components/TabbedTopics/index.tsx +++ b/src/app/components/TabbedTopics/index.tsx @@ -1,6 +1,10 @@ -import { useState } from 'react'; +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 = { @@ -9,6 +13,20 @@ const tabStyles = { 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', @@ -18,7 +36,7 @@ const tabStyles = { }), tab: (theme: Theme) => css({ - padding: '0.5rem 1rem', + padding: '0.5rem 1.5rem', cursor: 'pointer', border: '1px solid #ccc', borderBottom: 'none', @@ -31,6 +49,19 @@ const tabStyles = { 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, }, @@ -55,8 +86,10 @@ interface TabbedTopicsProps { css?: import('@emotion/react').Interpolation; } -const TabbedTopics = ({ topics, css: customCss }: TabbedTopicsProps) => { +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; @@ -67,23 +100,103 @@ const TabbedTopics = ({ topics, css: customCss }: TabbedTopicsProps) => { css={css({ backgroundColor: '#fff' })} >
-
- {topics.map((topic, idx) => ( - + ))} +
+ + {/* Gradient overlay for scroll indication, border grey */} +
+ {/* Red ellipsis icon above scroll edge */} +
+ +
{ groupTracker: { name: topics[activeIndex].title }, }} /> +