diff --git a/src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/index.tsx b/src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/index.tsx new file mode 100644 index 00000000000..1a33e3faa70 --- /dev/null +++ b/src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/index.tsx @@ -0,0 +1,87 @@ +import { use } from 'react'; +import { ServiceContext } from '#contexts/ServiceContext'; +import useTopicFollowButton, { + FollowAction, +} from '#app/hooks/useTopicFollowButton'; +import useClickTracker from '#app/hooks/useClickTrackerHandler'; +import useViewTracker from '#app/hooks/useViewTracker'; +import SaveButton from '#app/components/SaveButton'; + +import type { FollowTopicButtonProps } from '../index'; + +const FollowTopicButtonAuthenticated = ({ + topicData, +}: FollowTopicButtonProps) => { + const { translations } = use(ServiceContext); + const { followTopicButton } = translations || {}; + const { topicId } = topicData; + + const { isFollowed, isLoading, isUpdating, handleFollowAction } = + useTopicFollowButton(topicData); + + const clickComponentName = `follow-topic-button-click-${ + isFollowed ? FollowAction.UNFOLLOW : FollowAction.FOLLOW + }`; + + const viewTracker = useViewTracker({ + componentName: 'follow-topic-button-view', + }); + + const { onClick: onClickTrack } = useClickTracker({ + componentName: clickComponentName, + itemTracker: { + resourceId: topicId, + }, + }); + + if (!followTopicButton) return null; + + const getVisualLabel = () => { + if (isLoading) return followTopicButton.loading; + if (isUpdating) { + return isFollowed + ? followTopicButton.unfollowing + : followTopicButton.following; + } + if (isFollowed) return followTopicButton.followed; + return followTopicButton.follow; + }; + + const getAccessibleLabel = () => { + if (isLoading) return followTopicButton.loading; + if (isUpdating) { + return isFollowed + ? followTopicButton.unfollowing + : followTopicButton.following; + } + // When following, screen readers should hear the action the button performs next. + if (isFollowed) return followTopicButton.unfollowAccessible; + return followTopicButton.follow; + }; + + const hoverVisualLabel = + isFollowed && !isUpdating ? followTopicButton.unfollow : undefined; + + const handleClick = (event?: React.MouseEvent) => { + onClickTrack?.(event); + handleFollowAction( + isFollowed ? FollowAction.UNFOLLOW : FollowAction.FOLLOW, + ); + }; + + return ( + + ); +}; + +export default FollowTopicButtonAuthenticated; diff --git a/src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/lazy.tsx b/src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/lazy.tsx new file mode 100644 index 00000000000..215b2cac019 --- /dev/null +++ b/src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/lazy.tsx @@ -0,0 +1,14 @@ +import dynamic from 'next/dynamic'; +import FollowTopicButtonGuest from '../FollowTopicButtonGuest'; + +export default dynamic( + () => + import( + /* webpackChunkName: "follow_topic_button_authenticated" */ + '.' + ), + { + ssr: false, + loading: () => , + }, +); diff --git a/src/app/components/FollowTopicButton/FollowTopicButtonGuest/index.tsx b/src/app/components/FollowTopicButton/FollowTopicButtonGuest/index.tsx new file mode 100644 index 00000000000..a32dccad2d8 --- /dev/null +++ b/src/app/components/FollowTopicButton/FollowTopicButtonGuest/index.tsx @@ -0,0 +1,66 @@ +import { use, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { ServiceContext } from '#contexts/ServiceContext'; +import { AccountContext } from '#app/contexts/AccountContext'; +import SaveButton from '#app/components/SaveButton'; +import useHydrationDetection from '#app/hooks/useHydrationDetection'; +import AccountSignInModal from '#app/components/Account/AccountSignInModal'; +import useClickTracker from '#app/hooks/useClickTrackerHandler'; +import useViewTracker from '#app/hooks/useViewTracker'; + +interface FollowTopicButtonGuestProps { + topicId?: string; +} + +const FollowTopicButtonGuest = ({ topicId }: FollowTopicButtonGuestProps) => { + const { translations } = use(ServiceContext); + const { signInUrl, registerUrl } = use(AccountContext); + const isHydrated = useHydrationDetection(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const { followTopicButton } = translations || {}; + + const label = isHydrated + ? followTopicButton?.follow + : followTopicButton?.loading; + + const viewTracker = useViewTracker({ + componentName: 'follow-topic-button-guest-view', + }); + + const { onClick: onClickTrack } = useClickTracker({ + componentName: 'follow-topic-button-guest-click-follow', + itemTracker: { + resourceId: topicId, + }, + }); + + const handleClick = (e: React.MouseEvent) => { + onClickTrack?.(e); + setIsModalOpen(true); + }; + + return ( + <> + + {isModalOpen && + createPortal( + setIsModalOpen(false)} + signInUrl={signInUrl} + registerUrl={registerUrl} + />, + document.body, + )} + + ); +}; + +export default FollowTopicButtonGuest; diff --git a/src/app/components/FollowTopicButton/index.styles.ts b/src/app/components/FollowTopicButton/index.styles.ts new file mode 100644 index 00000000000..ffe47aefaf2 --- /dev/null +++ b/src/app/components/FollowTopicButton/index.styles.ts @@ -0,0 +1,15 @@ +import pixelsToRem from '#app/utilities/pixelsToRem'; +import { css, Theme } from '@emotion/react'; + +const styles = { + buttonWrapper: ({ spacings, mq }: Theme) => + css({ + marginBlock: `${spacings.DOUBLE}rem`, + + [mq.GROUP_3_MIN_WIDTH]: { + width: `${pixelsToRem(280)}rem`, + }, + }), +}; + +export default styles; diff --git a/src/app/components/FollowTopicButton/index.tsx b/src/app/components/FollowTopicButton/index.tsx new file mode 100644 index 00000000000..64c88efda1a --- /dev/null +++ b/src/app/components/FollowTopicButton/index.tsx @@ -0,0 +1,36 @@ +import { use } from 'react'; +import { AccountContext } from '#contexts/AccountContext'; +import type { TopicFollowData } from '#app/lib/uasApi/uasUtility'; +import styles from './index.styles'; +import FollowTopicButtonAuthenticated from './FollowTopicButtonAuthenticated/lazy'; +import FollowTopicButtonGuest from './FollowTopicButtonGuest'; + +export interface FollowTopicButtonProps { + topicData: TopicFollowData; +} + +const FOLLOW_TOPIC_BUTTON_ID = 'follow-topic-button'; + +const FollowTopicButton = ({ topicData }: FollowTopicButtonProps) => { + const { isPersonalizationAvailable, isPersonalizationEnabled } = + use(AccountContext); + + if (!isPersonalizationAvailable) return null; + + return ( + <> + +
+ {isPersonalizationEnabled ? ( + + ) : ( + + )} +
+ + ); +}; + +export default FollowTopicButton; diff --git a/src/app/components/SaveButton/index.tsx b/src/app/components/SaveButton/index.tsx index 1efa24872e1..c5a47b32cea 100644 --- a/src/app/components/SaveButton/index.tsx +++ b/src/app/components/SaveButton/index.tsx @@ -24,6 +24,8 @@ const SaveButton = ({ isSaved = false, onClick, testId, + // TODO :Ticket needed + // Add a buttonType prop (e.g. follow, favourites) to determine which icon to display. ...rest }: SaveButtonProps) => { const [isFocusedOrHovered, setIsFocusedOrHovered] = useState(false); diff --git a/src/app/hooks/createUASStatusHook.ts b/src/app/hooks/createUASStatusHook.ts new file mode 100644 index 00000000000..83066dc90e6 --- /dev/null +++ b/src/app/hooks/createUASStatusHook.ts @@ -0,0 +1,118 @@ +import { use } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import uasApiRequest from '#app/lib/uasApi'; +import { buildGlobalId, type ActivityType } from '#app/lib/uasApi/uasUtility'; +import { HTTP_NO_CONTENT } from '#app/lib/statusCodes.const'; +import { AccountContext } from '#app/contexts/AccountContext'; + +interface UseUASStatusHookConfig { + activityType: ActivityType; + resourceDomain: string; + resourceType: string; +} + +interface UseUASStatusHookParams { + config: UseUASStatusHookConfig; + queryKeyFn: (hashedUserId: string, resourceId: string) => unknown[]; + statusField: StatusField; + enabledFn?: (resourceId: string, hashedUserId: string) => boolean; +} + +type UseUASStatusHookReturn = Record< + StatusField, + boolean +> & { + isLoading: boolean; + error: Error | null; + metadata?: Record; +}; + +enum UASStatusField { + SAVED = 'isSaved', + FOLLOWED = 'isFollowed', +} + +/** + * Factory function that creates a UAS status fetch hook. + * Internal implementation - consumers should use the exported hooks (useUASFetchSaveStatus, useTopicFollowStatus). + * + * Returns a hook function that accepts a resourceId parameter and fetches its status. + * Handles: GET request, response parsing, query setup, error handling, caching. + * + * Example: + * const useArticleStatus = createUASStatusHook({ + * config: FAVOURITES_CONFIG, + * queryKeyFn: (userId, id) => uasKeys.saveStatus(userId, id), + * statusField: UASStatusField.SAVED, + * }); + */ +const createUASStatusHook = ( + params: UseUASStatusHookParams, +): ((resourceId: string) => UseUASStatusHookReturn) => { + const { config, queryKeyFn, statusField, enabledFn } = params; + + // eslint-disable-next-line react-hooks/rules-of-hooks + return (resourceId: string): UseUASStatusHookReturn => { + // eslint-disable-next-line react-hooks/rules-of-hooks + const { hashedUserId = '', isRefreshAvailable } = use(AccountContext); + + const isEnabled = enabledFn + ? enabledFn(resourceId, hashedUserId) + : !!resourceId && !!hashedUserId; + + // eslint-disable-next-line react-hooks/rules-of-hooks + const { + data = { + [statusField]: false, + metadata: undefined, + }, + isLoading, + error, + } = useQuery({ + queryKey: queryKeyFn(hashedUserId, resourceId), + queryFn: async () => { + const globalId = buildGlobalId( + resourceId, + config.resourceDomain, + config.resourceType, + ); + + const response = await uasApiRequest('GET', config.activityType, { + globalId, + isRefreshAvailable, + }); + + if (!response.ok || response.status === HTTP_NO_CONTENT) { + return { [statusField]: false }; + } + + try { + const responseData = (await response.json()) as { + metaData?: Record; + }; + + return { + [statusField]: true, + metadata: responseData.metaData, + }; + } catch { + return { + [statusField]: true, + metadata: undefined, + }; + } + }, + enabled: isEnabled, + }); + + return { + [statusField]: data[statusField] as boolean, + isLoading, + error: error as Error | null, + metadata: data.metadata, + } as UseUASStatusHookReturn; + }; +}; + +export { UASStatusField }; +export default createUASStatusHook; diff --git a/src/app/hooks/useTopicFollowButton/index.ts b/src/app/hooks/useTopicFollowButton/index.ts new file mode 100644 index 00000000000..a5db3f9295a --- /dev/null +++ b/src/app/hooks/useTopicFollowButton/index.ts @@ -0,0 +1,75 @@ +import { use } from 'react'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import uasApiRequest from '#app/lib/uasApi'; +import { + createFollowsPayload, + FOLLOWS_CONFIG, + type TopicFollowData, + buildGlobalId, +} from '#app/lib/uasApi/uasUtility'; +import uasKeys from '#app/lib/uasApi/queryKeys'; +import { AccountContext } from '#app/contexts/AccountContext'; +import useTopicFollowStatus from '#app/hooks/useTopicFollowStatus'; + +enum FollowAction { + FOLLOW = 'follow', + UNFOLLOW = 'unfollow', +} + +interface UseTopicFollowButtonReturn { + isFollowed: boolean; + isLoading: boolean; + isUpdating: boolean; + error: Error | null; + handleFollowAction: (action: FollowAction) => void; +} + +const useTopicFollowButton = ( + topicData: TopicFollowData, +): UseTopicFollowButtonReturn => { + const { topicId } = topicData; + const { hashedUserId = '', isRefreshAvailable } = use(AccountContext); + const queryClient = useQueryClient(); + const { isFollowed, isLoading, error } = useTopicFollowStatus(topicId); + + const mutation = useMutation({ + mutationFn: async (action: FollowAction) => { + if (action === FollowAction.FOLLOW) { + const body = createFollowsPayload(topicData); + await uasApiRequest('POST', FOLLOWS_CONFIG.activityType, { + body, + isRefreshAvailable, + }); + return; + } + const globalId = buildGlobalId( + topicId, + FOLLOWS_CONFIG.resourceDomain, + FOLLOWS_CONFIG.resourceType, + ); + await uasApiRequest('DELETE', FOLLOWS_CONFIG.activityType, { + globalId, + isRefreshAvailable, + }); + }, + onSuccess: (_result, action) => { + queryClient.setQueryData(uasKeys.followStatus(hashedUserId, topicId), { + isFollowed: action === FollowAction.FOLLOW, + }); + queryClient.invalidateQueries({ + queryKey: uasKeys.followsList(hashedUserId), + }); + }, + }); + + return { + isFollowed, + isLoading, + isUpdating: mutation.isPending, + error: mutation.error || error, + handleFollowAction: mutation.mutate, + }; +}; + +export { FollowAction }; +export default useTopicFollowButton; diff --git a/src/app/hooks/useTopicFollowStatus/index.ts b/src/app/hooks/useTopicFollowStatus/index.ts new file mode 100644 index 00000000000..a5f4284e8b0 --- /dev/null +++ b/src/app/hooks/useTopicFollowStatus/index.ts @@ -0,0 +1,22 @@ +import { FOLLOWS_CONFIG } from '#app/lib/uasApi/uasUtility'; +import uasKeys from '#app/lib/uasApi/queryKeys'; +import createUASStatusHook, { + UASStatusField, +} from '#app/hooks/createUASStatusHook'; + +/** + * POC (Follow Topics): fetches whether the signed-in user follows a topic. + * Wraps the generic createUASStatusHook factory with topic-specific config. + */ +const useTopicFollowStatus = createUASStatusHook({ + config: { + activityType: FOLLOWS_CONFIG.activityType, + resourceDomain: FOLLOWS_CONFIG.resourceDomain, + resourceType: FOLLOWS_CONFIG.resourceType, + }, + queryKeyFn: (hashedUserId, topicId) => + uasKeys.followStatus(hashedUserId, topicId) as unknown as unknown[], + statusField: UASStatusField.FOLLOWED, +}); + +export default useTopicFollowStatus; diff --git a/src/app/hooks/useUASFetchSaveStatus/index.ts b/src/app/hooks/useUASFetchSaveStatus/index.ts index e97153e5d4f..35e0f058d96 100644 --- a/src/app/hooks/useUASFetchSaveStatus/index.ts +++ b/src/app/hooks/useUASFetchSaveStatus/index.ts @@ -1,14 +1,13 @@ -import { use } from 'react'; -import { useQuery } from '@tanstack/react-query'; -import uasApiRequest from '#app/lib/uasApi'; -import { buildGlobalId, FAVOURITES_CONFIG } from '#app/lib/uasApi/uasUtility'; -import { HTTP_NO_CONTENT } from '#app/lib/statusCodes.const'; +import { FAVOURITES_CONFIG } from '#app/lib/uasApi/uasUtility'; import uasKeys from '#app/lib/uasApi/queryKeys'; -import { AccountContext } from '#app/contexts/AccountContext'; +import createUASStatusHook, { + UASStatusField, +} from '#app/hooks/createUASStatusHook'; -/** A hook that fetches an article's saved status from the UAS API, - * also returning saved article metadata if available. - * Returns the saved status, loading state, error, and metadata. */ +/** + * Fetches an article's saved status from UAS. + * Wraps the generic createUASStatusHook factory with article-specific config. + */ interface UseUASFetchSaveStatusReturn { isSaved: boolean; @@ -17,54 +16,23 @@ interface UseUASFetchSaveStatusReturn { savedMetadata?: Record; } -interface SavedArticleDetail { - metaData?: Record; -} - -const fetchSaveStatusWithMetadata = async ( - articleId: string, - isRefreshAvailable: boolean, -): Promise<{ isSaved: boolean; metadata?: Record }> => { - const globalId = buildGlobalId(articleId); - const response = await uasApiRequest('GET', FAVOURITES_CONFIG.activityType, { - globalId, - isRefreshAvailable, - }); - - if (!response.ok || response.status === HTTP_NO_CONTENT) { - return { isSaved: false }; - } - - try { - const responseData = (await response.json()) as SavedArticleDetail; - return { - isSaved: true, - metadata: responseData.metaData, - }; - } catch { - return { isSaved: true, metadata: undefined }; - } -}; +const useStatusHook = createUASStatusHook({ + config: FAVOURITES_CONFIG, + queryKeyFn: (hashedUserId, articleId) => + uasKeys.favouriteStatus(hashedUserId, articleId) as unknown as unknown[], + statusField: UASStatusField.SAVED, + enabledFn: articleId => !!articleId, +}); const useUASFetchSaveStatus = ( articleId: string, ): UseUASFetchSaveStatusReturn => { - const { hashedUserId = '', isRefreshAvailable } = use(AccountContext); - - const { - data = { isSaved: false }, - isLoading, - error, - } = useQuery({ - queryKey: uasKeys.favouriteStatus(hashedUserId, articleId), - queryFn: () => fetchSaveStatusWithMetadata(articleId, isRefreshAvailable), - enabled: !!articleId, - }); + const { isSaved, isLoading, error, metadata } = useStatusHook(articleId); return { - isSaved: data.isSaved, + isSaved, isLoading, - error: error as Error | null, - savedMetadata: data.metadata, + error, + savedMetadata: metadata, }; }; diff --git a/src/app/lib/config/services/hindi.ts b/src/app/lib/config/services/hindi.ts index 811a8dbb5ed..ce07a7b75ba 100644 --- a/src/app/lib/config/services/hindi.ts +++ b/src/app/lib/config/services/hindi.ts @@ -143,6 +143,16 @@ export const service: DefaultServiceConfig = { removeAccessible: 'सहेजा गया. मेरी ख़बरों से हटाएं', removing: 'हटाया जा रहा है', }, + // TBC : TODO: Ticket needed + followTopicButton: { + loading: 'Loading...', + follow: 'Follow', + following: 'Following...', + followed: 'Followed', + unfollow: 'Unfollow', + unfollowAccessible: 'Followed. Unfollow', + unfollowing: 'Unfollowing...', + }, myNews: { title: 'मेरी ख़बरें', guestTitle: 'मेरी ख़बरों में आपका स्वागत है', diff --git a/src/app/lib/uasApi/queryKeys.ts b/src/app/lib/uasApi/queryKeys.ts index 7959ea00330..e3b3f1a0871 100644 --- a/src/app/lib/uasApi/queryKeys.ts +++ b/src/app/lib/uasApi/queryKeys.ts @@ -8,6 +8,13 @@ const uasKeys = { [...uasKeys.favouritesList(userId), startIndex] as const, favouriteStatus: (userId: string, articleId: string) => [...uasKeys.favourites(userId), 'status', articleId] as const, + // POC (Follow Topics): mirrors the favourites key structure under a + // separate `follows` namespace so topic caches never collide with articles. + follows: (userId: string) => [...uasKeys.all(userId), 'follows'] as const, + followsList: (userId: string) => + [...uasKeys.follows(userId), 'list'] as const, + followStatus: (userId: string, topicId: string) => + [...uasKeys.follows(userId), 'status', topicId] as const, }; export default uasKeys; diff --git a/src/app/lib/uasApi/uasUtility.ts b/src/app/lib/uasApi/uasUtility.ts index 897affaa8ae..d906a172ed2 100644 --- a/src/app/lib/uasApi/uasUtility.ts +++ b/src/app/lib/uasApi/uasUtility.ts @@ -23,12 +23,26 @@ const FAVOURITES_CONFIG = { action: 'favourited', } as const; -export type ActivityType = (typeof FAVOURITES_CONFIG)['activityType']; +/** + * POC (Follow Topics): configuration for the UAS `follows` activity type. + * Mirrors FAVOURITES_CONFIG so the same generic `uasApiRequest` handler, + * `buildGlobalId`, error handling and TanStack Query patterns can be reused. + */ +const FOLLOWS_CONFIG = { + activityType: 'follows', + resourceDomain: 'world-service-news', + resourceType: 'topic', + action: 'followed', +} as const; + +export type ActivityType = + | (typeof FAVOURITES_CONFIG)['activityType'] + | (typeof FOLLOWS_CONFIG)['activityType']; const buildGlobalId = ( resourceId: string, - resourceDomain = FAVOURITES_CONFIG.resourceDomain, - resourceType = FAVOURITES_CONFIG.resourceType, + resourceDomain: string = FAVOURITES_CONFIG.resourceDomain, + resourceType: string = FAVOURITES_CONFIG.resourceType, ): string => `urn:bbc:${resourceDomain}:${resourceType}:${resourceId}`; interface MetadataComparisonResult { @@ -103,11 +117,61 @@ const createFavouritesPayload = ({ }), }); +/** + * POC (Follow Topics): the minimal set of topic fields we send to UAS so a + * followed topic can be rendered later (e.g. in a "Followed topics" list) + * without an extra lookup. + */ +export interface TopicFollowData { + topicId: string; + title: string; + service: Services; + url: string; + description?: string; + imageUrl?: string; +} + +const buildTopicMetadata = ({ + topicId, + title, + service, + url, + description, + imageUrl, +}: TopicFollowData): Record => ({ + topicId, + service, + title: sanitiseMetadataString(title), + locatorUrl: url, + description: sanitiseMetadataString(description), + imageUrl, +}); + +/** + * POC (Follow Topics): builds the UAS request body for following a topic. + * Structurally identical to `createFavouritesPayload`, only the config and + * metadata differ — demonstrating the activity-agnostic reuse of the UAS layer. + */ +const createFollowsPayload = ( + topicData: TopicFollowData, +): UasApiRequestBody => ({ + activityType: FOLLOWS_CONFIG.activityType, + resourceDomain: FOLLOWS_CONFIG.resourceDomain, + resourceType: FOLLOWS_CONFIG.resourceType, + resourceId: topicData.topicId, + action: FOLLOWS_CONFIG.action, + resourceTitle: topicData.service, + metaData: buildTopicMetadata(topicData), +}); + export { USER_ID_COOKIE_KEY, FAVOURITES_CONFIG, + FOLLOWS_CONFIG, buildGlobalId, createFavouritesPayload, + createFollowsPayload, + buildTopicMetadata, buildCurrentMetadata, compareMetadataWithSaved, sanitiseMetadataString, diff --git a/src/app/models/types/translations.ts b/src/app/models/types/translations.ts index c8c622c9a8b..e4589d718b1 100644 --- a/src/app/models/types/translations.ts +++ b/src/app/models/types/translations.ts @@ -73,6 +73,15 @@ export interface Translations { removeAccessible: string; removing: string; }; + followTopicButton?: { + loading: string; + follow: string; + following: string; + followed: string; + unfollow: string; + unfollowAccessible: string; + unfollowing: string; + }; myNews?: { title: string; guestTitle: string; diff --git a/src/app/pages/TopicPage/TopicPage.jsx b/src/app/pages/TopicPage/TopicPage.jsx index 5e93dd291f1..55c9b6c4f42 100644 --- a/src/app/pages/TopicPage/TopicPage.jsx +++ b/src/app/pages/TopicPage/TopicPage.jsx @@ -1,6 +1,8 @@ import { Fragment, use } from 'react'; import path from 'ramda/src/path'; import Curation from '#app/components/Curation'; +import FollowTopicButton from '#app/components/FollowTopicButton'; +import parseRoute from '#app/routes/utils/parseRoute'; import AdContainer from '../../components/Ad'; import ATIAnalytics from '../../components/ATIAnalytics'; import ChartbeatAnalytics from '../../components/ChartbeatAnalytics'; @@ -8,6 +10,7 @@ import LinkedData from '../../components/LinkedData'; import styles from './index.styles'; import MetadataContainer from '../../components/Metadata'; import { ServiceContext } from '../../contexts/ServiceContext'; +import { RequestContext } from '../../contexts/RequestContext'; import TopicImage from './TopicImage'; import TopicTitle from './TopicTitle'; import TopicDescription from './TopicDescription'; @@ -16,7 +19,8 @@ import getItemList from '../../lib/seoUtils/getItemList'; import getNthCurationByStyleAndProminence from '../utils/getNthCurationByStyleAndProminence'; const TopicPage = ({ pageData }) => { - const { lang, translations, brandName } = use(ServiceContext); + const { lang, translations, brandName, service } = use(ServiceContext); + const { pathname, canonicalLink } = use(RequestContext); const { title, description, @@ -28,8 +32,9 @@ const TopicPage = ({ pageData }) => { activePage, } = pageData; + const { assetId: topicId } = parseRoute(pathname); const topStoriesTitle = path(['topStoriesTitle'], translations); - + console.log('pageData', pageData); const { pageXOfY, previousPage, nextPage, page } = { pageXOfY: 'Page {x} of {y}', previousPage: 'Previous Page', @@ -76,6 +81,18 @@ const TopicPage = ({ pageData }) => { {title} {description && {description}} + {topicId && ( + + )} {curations.map( ({