diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index 08b199752..06c8c0ee1 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -60,7 +60,7 @@ export const CACHE_TIME = 60 * 1000 * 20; // 20 minutes export const STALE_TIME = 60 * 1000 * 20; -export const CARD_ASPECT_RATIOS = ['2:1', '16:9', '5:3', '4:3', '1:1', '9:13', '2:3', '9:16'] as const; +export const CARD_ASPECT_RATIOS = ['1:1', '2:1', '2:3', '4:3', '5:3', '16:9', '9:13', '9:16'] as const; export const DEFAULT_FEATURES = { canUpdateEmail: false, diff --git a/packages/common/src/env.ts b/packages/common/src/env.ts index 5d4066571..50c854c56 100644 --- a/packages/common/src/env.ts +++ b/packages/common/src/env.ts @@ -3,8 +3,13 @@ export type Env = { APP_API_BASE_URL: string; APP_PLAYER_ID: string; APP_FOOTER_TEXT: string; + APP_DEFAULT_LANGUAGE: string; + APP_DEFAULT_CONFIG_SOURCE?: string; APP_PLAYER_LICENSE_KEY?: string; + + APP_BODY_FONT?: string; + APP_BODY_ALT_FONT?: string; }; const env: Env = { @@ -12,6 +17,7 @@ const env: Env = { APP_API_BASE_URL: 'https://cdn.jwplayer.com', APP_PLAYER_ID: 'M4qoGvUk', APP_FOOTER_TEXT: '', + APP_DEFAULT_LANGUAGE: 'en', }; export const configureEnv = (options: Partial) => { @@ -19,9 +25,13 @@ export const configureEnv = (options: Partial) => { env.APP_API_BASE_URL = options.APP_API_BASE_URL || env.APP_API_BASE_URL; env.APP_PLAYER_ID = options.APP_PLAYER_ID || env.APP_PLAYER_ID; env.APP_FOOTER_TEXT = options.APP_FOOTER_TEXT || env.APP_FOOTER_TEXT; + env.APP_DEFAULT_LANGUAGE = options.APP_DEFAULT_LANGUAGE || env.APP_DEFAULT_LANGUAGE; env.APP_DEFAULT_CONFIG_SOURCE ||= options.APP_DEFAULT_CONFIG_SOURCE; env.APP_PLAYER_LICENSE_KEY ||= options.APP_PLAYER_LICENSE_KEY; + + env.APP_BODY_FONT = options.APP_BODY_FONT || env.APP_BODY_FONT; + env.APP_BODY_ALT_FONT = options.APP_BODY_ALT_FONT || env.APP_BODY_ALT_FONT; }; export default env; diff --git a/packages/common/src/utils/common.ts b/packages/common/src/utils/common.ts index 5b3395186..f3666cb2c 100644 --- a/packages/common/src/utils/common.ts +++ b/packages/common/src/utils/common.ts @@ -5,6 +5,30 @@ export function debounce void>(callback: T, wait = timeout = setTimeout(() => callback(...args), wait); }; } +export function throttle unknown>(func: T, limit: number): (...args: Parameters) => void { + let lastFunc: NodeJS.Timeout | undefined; + let lastRan: number | undefined; + + return function (this: ThisParameterType, ...args: Parameters): void { + const timeSinceLastRan = lastRan ? Date.now() - lastRan : limit; + + if (timeSinceLastRan >= limit) { + func.apply(this, args); + lastRan = Date.now(); + } else if (!lastFunc) { + lastFunc = setTimeout(() => { + if (lastRan) { + const timeSinceLastRan = Date.now() - lastRan; + if (timeSinceLastRan >= limit) { + func.apply(this, args); + lastRan = Date.now(); + } + } + lastFunc = undefined; + }, limit - timeSinceLastRan); + } + }; +} export const unicodeToChar = (text: string) => { return text.replace(/\\u[\dA-F]{4}/gi, (match) => { diff --git a/packages/hooks-react/src/usePlaylists.ts b/packages/hooks-react/src/usePlaylists.ts new file mode 100644 index 000000000..c430e816f --- /dev/null +++ b/packages/hooks-react/src/usePlaylists.ts @@ -0,0 +1,73 @@ +import { PersonalShelf, PersonalShelves, PLAYLIST_LIMIT } from '@jwp/ott-common/src/constants'; +import ApiService from '@jwp/ott-common/src/services/ApiService'; +import { getModule } from '@jwp/ott-common/src/modules/container'; +import { useFavoritesStore } from '@jwp/ott-common/src/stores/FavoritesStore'; +import { useWatchHistoryStore } from '@jwp/ott-common/src/stores/WatchHistoryStore'; +import { generatePlaylistPlaceholder } from '@jwp/ott-common/src/utils/collection'; +import { isTruthyCustomParamValue } from '@jwp/ott-common/src/utils/common'; +import { isScheduledOrLiveMedia } from '@jwp/ott-common/src/utils/liveEvent'; +import type { Content } from '@jwp/ott-common/types/config'; +import type { Playlist } from '@jwp/ott-common/types/playlist'; +import { useQueries, useQueryClient } from 'react-query'; + +const placeholderData = generatePlaylistPlaceholder(30); + +type UsePlaylistResult = { + data: Playlist | undefined; + isLoading: boolean; + isSuccess?: boolean; + error?: unknown; +}[]; + +const usePlaylists = (content: Content[], rowsToLoad: number | undefined = undefined) => { + const page_limit = PLAYLIST_LIMIT.toString(); + const queryClient = useQueryClient(); + const apiService = getModule(ApiService); + + const favorites = useFavoritesStore((state) => state.getPlaylist()); + const watchHistory = useWatchHistoryStore((state) => state.getPlaylist()); + + const playlistQueries = useQueries( + content.map(({ contentId, type }, index) => ({ + enabled: !!contentId && (!rowsToLoad || rowsToLoad > index) && !PersonalShelves.some((pType) => pType === type), + queryKey: ['playlist', contentId], + queryFn: async () => { + const playlist = await apiService.getPlaylistById(contentId, { page_limit }); + + // This pre-caches all playlist items and makes navigating a lot faster. + playlist?.playlist?.forEach((playlistItem) => { + queryClient.setQueryData(['media', playlistItem.mediaid], playlistItem); + }); + + return playlist; + }, + placeholderData: !!contentId && placeholderData, + refetchInterval: (data: Playlist | undefined) => { + if (!data) return false; + + const autoRefetch = isTruthyCustomParamValue(data.refetch) || data.playlist.some(isScheduledOrLiveMedia); + + return autoRefetch ? 1000 * 30 : false; + }, + retry: false, + })), + ); + + const result: UsePlaylistResult = content.map(({ type }, index) => { + if (type === PersonalShelf.Favorites) return { data: favorites, isLoading: false, isSuccess: true }; + if (type === PersonalShelf.ContinueWatching) return { data: watchHistory, isLoading: false, isSuccess: true }; + + const { data, isLoading, isSuccess, error } = playlistQueries[index]; + + return { + data, + isLoading, + isSuccess, + error, + }; + }); + + return result; +}; + +export default usePlaylists; diff --git a/packages/testing/fixtures/favorites.json b/packages/testing/fixtures/favorites.json new file mode 100644 index 000000000..ab2c6a930 --- /dev/null +++ b/packages/testing/fixtures/favorites.json @@ -0,0 +1,417 @@ +{ + "feedid": "KKOhckQL", + "title": "Favorites", + "playlist": [ + { + "title": "SVOD 002: Caminandes 1 llama drama", + "mediaid": "1TJAvj2S", + "link": "https://cdn.jwplayer.com/previews/1TJAvj2S", + "image": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=720", + "images": [ + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=320", + "width": 320, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=480", + "width": 480, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=640", + "width": 640, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=720", + "width": 720, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=1280", + "width": 1280, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/poster.jpg?width=1920", + "width": 1920, + "type": "image/jpeg" + } + ], + "feedid": "bdH6HTUi", + "duration": 90, + "pubdate": 1703166863, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla ligula, sollicitudin id felis eu, consequat aliquam ante. Suspendisse lacinia felis a quam laoreet, non tristique quam efficitur. Morbi ultrices, nibh et fringilla aliquet, ligula tortor porta libero, ut elementum nibh nisl vel odio. Praesent ornare luctus arcu nec condimentum. Vestibulum fringilla egestas neque, feugiat sollicitudin eros aliquet non. In enim augue, sodales eget dignissim eget, sagittis sit amet eros. Nulla tristique nisi iaculis dui egestas mollis. Aenean libero odio, vestibulum quis sodales pulvinar, consequat ut nisl.", + "tags": "svod", + "recommendations": "https://cdn.jwplayer.com/v2/playlists/bdH6HTUi?related_media_id=1TJAvj2S", + "sources": [ + { + "file": "https://cdn.jwplayer.com/manifests/1TJAvj2S.m3u8", + "type": "application/vnd.apple.mpegurl" + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-xAdPQ1TN.m4a", + "type": "audio/mp4", + "label": "AAC Audio", + "bitrate": 113513, + "filesize": 1277026 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-1Yfbe0HO.mp4", + "type": "video/mp4", + "height": 180, + "width": 320, + "label": "180p", + "bitrate": 241872, + "filesize": 2721071, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-JjxC7ylo.mp4", + "type": "video/mp4", + "height": 270, + "width": 480, + "label": "270p", + "bitrate": 356443, + "filesize": 4009992, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-kqEB96Md.mp4", + "type": "video/mp4", + "height": 360, + "width": 640, + "label": "360p", + "bitrate": 401068, + "filesize": 4512018, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-MskLmv79.mp4", + "type": "video/mp4", + "height": 406, + "width": 720, + "label": "406p", + "bitrate": 466271, + "filesize": 5245549, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-MCyoQl96.mp4", + "type": "video/mp4", + "height": 540, + "width": 960, + "label": "540p", + "bitrate": 713837, + "filesize": 8030667, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-yYzmiSbt.mp4", + "type": "video/mp4", + "height": 720, + "width": 1280, + "label": "720p", + "bitrate": 1088928, + "filesize": 12250450, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/1TJAvj2S-H4t30RCN.mp4", + "type": "video/mp4", + "height": 1080, + "width": 1920, + "label": "1080p", + "bitrate": 2391552, + "filesize": 26904961, + "framerate": 24 + } + ], + "tracks": [ + { + "file": "https://cdn.jwplayer.com/strips/1TJAvj2S-120.vtt", + "kind": "thumbnails" + } + ], + "variations": {}, + "cardImage": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/images/card.webp?poster_fallback=1", + "channelLogoImage": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/images/channel_logo.webp?poster_fallback=1", + "backgroundImage": "https://cdn.jwplayer.com/v2/media/1TJAvj2S/images/background.webp?poster_fallback=1" + }, + { + "title": "SVOD 003: Caminandes 2 gran dillama", + "mediaid": "rnibIt0n", + "link": "https://cdn.jwplayer.com/previews/rnibIt0n", + "image": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=720", + "images": [ + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=320", + "width": 320, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=480", + "width": 480, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=640", + "width": 640, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=720", + "width": 720, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=1280", + "width": 1280, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/rnibIt0n/poster.jpg?width=1920", + "width": 1920, + "type": "image/jpeg" + } + ], + "feedid": "bdH6HTUi", + "duration": 146, + "pubdate": 1703166863, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla ligula, sollicitudin id felis eu, consequat aliquam ante. Suspendisse lacinia felis a quam laoreet, non tristique quam efficitur. Morbi ultrices, nibh et fringilla aliquet, ligula tortor porta libero, ut elementum nibh nisl vel odio. Praesent ornare luctus arcu nec condimentum. Vestibulum fringilla egestas neque, feugiat sollicitudin eros aliquet non. In enim augue, sodales eget dignissim eget, sagittis sit amet eros. Nulla tristique nisi iaculis dui egestas mollis. Aenean libero odio, vestibulum quis sodales pulvinar, consequat ut nisl.", + "tags": "svod", + "recommendations": "https://cdn.jwplayer.com/v2/playlists/bdH6HTUi?related_media_id=rnibIt0n", + "sources": [ + { + "file": "https://cdn.jwplayer.com/manifests/rnibIt0n.m3u8", + "type": "application/vnd.apple.mpegurl" + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-xAdPQ1TN.m4a", + "type": "audio/mp4", + "label": "AAC Audio", + "bitrate": 113503, + "filesize": 2071433 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-1Yfbe0HO.mp4", + "type": "video/mp4", + "height": 180, + "width": 320, + "label": "180p", + "bitrate": 342175, + "filesize": 6244705, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-JjxC7ylo.mp4", + "type": "video/mp4", + "height": 270, + "width": 480, + "label": "270p", + "bitrate": 501738, + "filesize": 9156729, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-kqEB96Md.mp4", + "type": "video/mp4", + "height": 360, + "width": 640, + "label": "360p", + "bitrate": 579321, + "filesize": 10572609, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-MskLmv79.mp4", + "type": "video/mp4", + "height": 406, + "width": 720, + "label": "406p", + "bitrate": 673083, + "filesize": 12283769, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-MCyoQl96.mp4", + "type": "video/mp4", + "height": 540, + "width": 960, + "label": "540p", + "bitrate": 984717, + "filesize": 17971095, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-yYzmiSbt.mp4", + "type": "video/mp4", + "height": 720, + "width": 1280, + "label": "720p", + "bitrate": 1527270, + "filesize": 27872694, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/rnibIt0n-H4t30RCN.mp4", + "type": "video/mp4", + "height": 1080, + "width": 1920, + "label": "1080p", + "bitrate": 3309652, + "filesize": 60401155, + "framerate": 24 + } + ], + "tracks": [ + { + "file": "https://cdn.jwplayer.com/strips/rnibIt0n-120.vtt", + "kind": "thumbnails" + } + ], + "variations": {}, + "genre": "Animation", + "cardImage": "https://cdn.jwplayer.com/v2/media/rnibIt0n/images/card.webp?poster_fallback=1", + "channelLogoImage": "https://cdn.jwplayer.com/v2/media/rnibIt0n/images/channel_logo.webp?poster_fallback=1", + "backgroundImage": "https://cdn.jwplayer.com/v2/media/rnibIt0n/images/background.webp?poster_fallback=1" + }, + { + "title": "SVOD 001: Tears of Steel", + "mediaid": "MaCvdQdE", + "link": "https://cdn.jwplayer.com/previews/MaCvdQdE", + "image": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=720", + "images": [ + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=320", + "width": 320, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=480", + "width": 480, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=640", + "width": 640, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=720", + "width": 720, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=1280", + "width": 1280, + "type": "image/jpeg" + }, + { + "src": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/poster.jpg?width=1920", + "width": 1920, + "type": "image/jpeg" + } + ], + "feedid": "E2uaFiUM", + "duration": 734, + "pubdate": 1703166863, + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur nulla ligula, sollicitudin id felis eu, consequat aliquam ante. Suspendisse lacinia felis a quam laoreet, non tristique quam efficitur. Morbi ultrices, nibh et fringilla aliquet, ligula tortor porta libero, ut elementum nibh nisl vel odio. Praesent ornare luctus arcu nec condimentum. Vestibulum fringilla egestas neque, feugiat sollicitudin eros aliquet non. In enim augue, sodales eget dignissim eget, sagittis sit amet eros. Nulla tristique nisi iaculis dui egestas mollis. Aenean libero odio, vestibulum quis sodales pulvinar, consequat ut nisl.", + "tags": "svod", + "sources": [ + { + "file": "https://cdn.jwplayer.com/manifests/MaCvdQdE.m3u8", + "type": "application/vnd.apple.mpegurl" + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-xAdPQ1TN.m4a", + "type": "audio/mp4", + "label": "AAC Audio", + "bitrate": 113413, + "filesize": 10405724 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-1Yfbe0HO.mp4", + "type": "video/mp4", + "height": 134, + "width": 320, + "label": "180p", + "bitrate": 388986, + "filesize": 35689542, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-JjxC7ylo.mp4", + "type": "video/mp4", + "height": 200, + "width": 480, + "label": "270p", + "bitrate": 575378, + "filesize": 52790944, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-kqEB96Md.mp4", + "type": "video/mp4", + "height": 266, + "width": 640, + "label": "360p", + "bitrate": 617338, + "filesize": 56640812, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-MskLmv79.mp4", + "type": "video/mp4", + "height": 300, + "width": 720, + "label": "406p", + "bitrate": 715724, + "filesize": 65667691, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-MCyoQl96.mp4", + "type": "video/mp4", + "height": 400, + "width": 960, + "label": "540p", + "bitrate": 1029707, + "filesize": 94475629, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-yYzmiSbt.mp4", + "type": "video/mp4", + "height": 534, + "width": 1280, + "label": "720p", + "bitrate": 1570612, + "filesize": 144103685, + "framerate": 24 + }, + { + "file": "https://cdn.jwplayer.com/videos/MaCvdQdE-H4t30RCN.mp4", + "type": "video/mp4", + "height": 800, + "width": 1920, + "label": "1080p", + "bitrate": 3081227, + "filesize": 282702650, + "framerate": 24 + } + ], + "tracks": [ + { + "file": "https://cdn.jwplayer.com/strips/MaCvdQdE-120.vtt", + "kind": "thumbnails" + } + ], + "variations": {}, + "cardImage": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/images/card.webp?poster_fallback=1", + "channelLogoImage": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/images/channel_logo.webp?poster_fallback=1", + "backgroundImage": "https://cdn.jwplayer.com/v2/media/MaCvdQdE/images/background.webp?poster_fallback=1" + } + ] +} diff --git a/packages/ui-react/src/components/Account/Account.tsx b/packages/ui-react/src/components/Account/Account.tsx index ed715b695..71c4c2c2f 100644 --- a/packages/ui-react/src/components/Account/Account.tsx +++ b/packages/ui-react/src/components/Account/Account.tsx @@ -13,6 +13,7 @@ import { formatConsents, formatConsentsFromValues, formatConsentsToRegisterField import useToggle from '@jwp/ott-hooks-react/src/useToggle'; import Visibility from '@jwp/ott-theme/assets/icons/visibility.svg?react'; import VisibilityOff from '@jwp/ott-theme/assets/icons/visibility_off.svg?react'; +import env from '@jwp/ott-common/src/env'; import type { FormSectionContentArgs, FormSectionProps } from '../Form/FormSection'; import Alert from '../Alert/Alert'; @@ -25,6 +26,7 @@ import HelperText from '../HelperText/HelperText'; import CustomRegisterField from '../CustomRegisterField/CustomRegisterField'; import Icon from '../Icon/Icon'; import { modalURLFromLocation } from '../../utils/location'; +import { useAriaAnnouncer } from '../../containers/AnnouncementProvider/AnnoucementProvider'; import styles from './Account.module.scss'; @@ -45,13 +47,15 @@ interface FormErrors { const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true }: Props): JSX.Element => { const accountController = getModule(AccountController); - const { t } = useTranslation('user'); + const { t, i18n } = useTranslation('user'); + const announce = useAriaAnnouncer(); const navigate = useNavigate(); const location = useLocation(); const [viewPassword, toggleViewPassword] = useToggle(); const exportData = useMutation(accountController.exportAccountData); const [isAlertVisible, setIsAlertVisible] = useState(false); const exportDataMessage = exportData.isSuccess ? t('account.export_data_success') : t('account.export_data_error'); + const htmlLang = i18n.language !== env.APP_DEFAULT_LANGUAGE ? env.APP_DEFAULT_LANGUAGE : undefined; useEffect(() => { if (exportData.isSuccess || exportData.isError) { @@ -203,15 +207,17 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } return ( <> +

{t('nav.account')}

+
{[ formSection({ label: t('account.about_you'), editButton: t('account.edit_information'), - onSubmit: (values) => { + onSubmit: async (values) => { const consents = formatConsentsFromValues(publisherConsents, { ...values.metadata, ...values.consentsValues }); - return accountController.updateUser({ + const response = await accountController.updateUser({ firstName: values.firstName || '', lastName: values.lastName || '', metadata: { @@ -220,6 +226,10 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } consents: JSON.stringify(consents), }, }); + + announce(t('account.update_success', { section: t('account.about_you') }), 'success'); + + return response; }, content: (section) => ( <> @@ -233,6 +243,7 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } helperText={section.errors?.firstName} disabled={section.isBusy} editing={section.isEditing} + lang={htmlLang} /> ), }), formSection({ label: t('account.email'), - onSubmit: (values) => - accountController.updateUser({ + onSubmit: async (values) => { + const response = await accountController.updateUser({ email: values.email || '', confirmationPassword: values.confirmationPassword, - }), + }); + + announce(t('account.update_success', { section: t('account.email') }), 'success'); + + return response; + }, canSave: (values) => !!(values.email && values.confirmationPassword), editButton: t('account.edit_account'), readOnly: !canUpdateEmail, @@ -304,7 +321,13 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } formSection({ label: t('account.terms_and_tracking'), saveButton: t('account.update_consents'), - onSubmit: (values) => accountController.updateConsents(formatConsentsFromValues(publisherConsents, values.consentsValues)), + onSubmit: async (values) => { + const response = await accountController.updateConsents(formatConsentsFromValues(publisherConsents, values.consentsValues)); + + announce(t('account.update_success', { section: t('account.terms_and_tracking') }), 'success'); + + return response; + }, content: (section) => ( <> {termsConsents?.map((consent, index) => ( @@ -315,6 +338,7 @@ const Account = ({ panelClassName, panelHeaderClassName, canUpdateEmail = true } onChange={section.onChange} label={formatConsentLabel(consent.label)} disabled={consent.required || section.isBusy} + lang={htmlLang} /> ))} diff --git a/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap b/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap index 175e6fe0c..6578f12f2 100644 --- a/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap +++ b/packages/ui-react/src/components/Account/__snapshots__/Account.test.tsx.snap @@ -2,6 +2,11 @@ exports[` > renders and matches snapshot 1`] = `
+

+ nav.account +

> renders and matches snapshot 1`] = ` > account.firstname -

- John -

+
> renders and matches snapshot 1`] = ` > account.lastname -

- Doe -

+
> renders and matches snapshot 1`] = ` > account.email -

- email@domain.com -

+
> renders and matches snapshot 1`] = ` class="_row_531f07" > > renders and matches snapshot 1`] = ` /> diff --git a/packages/ui-react/src/components/Button/Button.module.scss b/packages/ui-react/src/components/Button/Button.module.scss index 429a2530e..40577575a 100644 --- a/packages/ui-react/src/components/Button/Button.module.scss +++ b/packages/ui-react/src/components/Button/Button.module.scss @@ -1,5 +1,6 @@ @use '@jwp/ott-ui-react/src/styles/variables'; @use '@jwp/ott-ui-react/src/styles/theme'; +@use '@jwp/ott-ui-react/src/styles/accessibility'; @use '@jwp/ott-ui-react/src/styles/mixins/responsive'; $small-button-height: 28px; @@ -31,15 +32,7 @@ $large-button-height: 40px; &:hover, &:focus { z-index: 1; - transform: scale(1.1); - } - - &:focus:not(:focus-visible):not(:hover) { - transform: scale(1); - } - - &:focus-visible { - transform: scale(1.1); + transform: scale(1.05); } } } @@ -65,6 +58,10 @@ $large-button-height: 40px; &.primary { color: var(--highlight-contrast-color, theme.$btn-primary-color); background-color: var(--highlight-color, theme.$btn-primary-bg); + + &:focus { + @include accessibility.accessibleOutlineContrast; + } } &.outlined { @@ -76,6 +73,7 @@ $large-button-height: 40px; color: var(--highlight-contrast-color, theme.$btn-primary-color); background-color: var(--highlight-color, theme.$btn-primary-bg); border-color: var(--highlight-color, theme.$btn-primary-bg); + outline: none; } } } diff --git a/packages/ui-react/src/components/Button/Button.tsx b/packages/ui-react/src/components/Button/Button.tsx index fc4bf0c72..842e92934 100644 --- a/packages/ui-react/src/components/Button/Button.tsx +++ b/packages/ui-react/src/components/Button/Button.tsx @@ -42,7 +42,7 @@ const Button: React.FC = ({ size = 'medium', disabled, busy, - type, + type = 'button', to, as = 'button', onClick, diff --git a/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap b/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap index 766a87a85..aa5e13b5a 100644 --- a/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap +++ b/packages/ui-react/src/components/Button/__snapshots__/Button.test.tsx.snap @@ -4,6 +4,7 @@ exports[`
diff --git a/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap b/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap index e741084ab..e1f960683 100644 --- a/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap +++ b/packages/ui-react/src/components/CancelSubscriptionForm/__snapshots__/CancelSubscriptionForm.test.tsx.snap @@ -14,6 +14,7 @@ exports[` > renders and matches snapshot 1`] = ` + + + + +`; diff --git a/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap b/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap index 6bf873a36..8fc29d1be 100644 --- a/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap +++ b/packages/ui-react/src/components/Filter/__snapshots__/Filter.test.tsx.snap @@ -10,6 +10,7 @@ exports[` > renders Filter 1`] = `