-
Notifications
You must be signed in to change notification settings - Fork 280
WS-2956 POC - FOllow topic module with UAS endpoints #14274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
jinidev
wants to merge
4
commits into
latest
Choose a base branch
from
ws-2956-POC-followtopic
base: latest
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
87 changes: 87 additions & 0 deletions
87
src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <SaveButton | ||
| onClick={handleClick} | ||
| isLoading={isLoading} | ||
| isUpdating={isUpdating} | ||
| isSaved={isFollowed} | ||
| visualLabel={getVisualLabel()} | ||
| hoverVisualLabel={hoverVisualLabel} | ||
| accessibleLabel={getAccessibleLabel()} | ||
| testId="follow-topic-btn-authorized" | ||
| {...viewTracker} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default FollowTopicButtonAuthenticated; |
14 changes: 14 additions & 0 deletions
14
src/app/components/FollowTopicButton/FollowTopicButtonAuthenticated/lazy.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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: () => <FollowTopicButtonGuest />, | ||
| }, | ||
| ); |
66 changes: 66 additions & 0 deletions
66
src/app/components/FollowTopicButton/FollowTopicButtonGuest/index.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<HTMLButtonElement>) => { | ||
| onClickTrack?.(e); | ||
| setIsModalOpen(true); | ||
| }; | ||
|
|
||
| return ( | ||
| <> | ||
| <SaveButton | ||
| onClick={handleClick} | ||
| visualLabel={label ?? ''} | ||
| accessibleLabel={label ?? ''} | ||
| testId="follow-topic-btn-guest" | ||
| isLoading={!isHydrated} | ||
| {...viewTracker} | ||
| /> | ||
| {isModalOpen && | ||
| createPortal( | ||
| <AccountSignInModal | ||
| onClose={() => setIsModalOpen(false)} | ||
| signInUrl={signInUrl} | ||
| registerUrl={registerUrl} | ||
| />, | ||
| document.body, | ||
| )} | ||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default FollowTopicButtonGuest; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <> | ||
| <noscript> | ||
| <style>{`#${FOLLOW_TOPIC_BUTTON_ID} { display: none; }`}</style> | ||
| </noscript> | ||
| <div css={styles.buttonWrapper} id={FOLLOW_TOPIC_BUTTON_ID}> | ||
| {isPersonalizationEnabled ? ( | ||
| <FollowTopicButtonAuthenticated topicData={topicData} /> | ||
| ) : ( | ||
| <FollowTopicButtonGuest topicId={topicData.topicId} /> | ||
| )} | ||
| </div> | ||
|
jinidev marked this conversation as resolved.
|
||
| </> | ||
| ); | ||
| }; | ||
|
|
||
| export default FollowTopicButton; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<StatusField extends string> { | ||
| config: UseUASStatusHookConfig; | ||
| queryKeyFn: (hashedUserId: string, resourceId: string) => unknown[]; | ||
| statusField: StatusField; | ||
| enabledFn?: (resourceId: string, hashedUserId: string) => boolean; | ||
| } | ||
|
|
||
| type UseUASStatusHookReturn<StatusField extends string> = Record< | ||
| StatusField, | ||
| boolean | ||
| > & { | ||
| isLoading: boolean; | ||
| error: Error | null; | ||
| metadata?: Record<string, unknown>; | ||
| }; | ||
|
|
||
| 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 = <StatusField extends string>( | ||
| params: UseUASStatusHookParams<StatusField>, | ||
| ): ((resourceId: string) => UseUASStatusHookReturn<StatusField>) => { | ||
| const { config, queryKeyFn, statusField, enabledFn } = params; | ||
|
|
||
| // eslint-disable-next-line react-hooks/rules-of-hooks | ||
| return (resourceId: string): UseUASStatusHookReturn<StatusField> => { | ||
| // 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<string, unknown>; | ||
| }; | ||
|
|
||
| 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<StatusField>; | ||
| }; | ||
| }; | ||
|
|
||
| export { UASStatusField }; | ||
| export default createUASStatusHook; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.