diff --git a/public/locales/en/user.json b/public/locales/en/user.json index 795193d53..73537b863 100644 --- a/public/locales/en/user.json +++ b/public/locales/en/user.json @@ -100,5 +100,20 @@ "transactions": "Transactions", "update_payment_details": "Update payment details", "weekly_subscription": "Weekly subscription" + }, + "profile": { + "adult": "Adult", + "avatar": "Avatar", + "content_rating": "Content rating", + "create": "Create profile", + "delete": "Delete profile", + "delete_description": "Permanently delete your profile along with all of your watch history and favorites.", + "delete_main": "The main profile cannot be deleted.", + "description": "Profiles allow you to watch content and assemble your own personal collection of favorites.", + "greeting": "Howdy", + "greeting_with_name": "Howdy, {{name}}", + "info": "Profile info", + "kids": "Kids", + "name": "User name" } } diff --git a/public/locales/es/user.json b/public/locales/es/user.json index 7d5b1fbb3..301d9189c 100644 --- a/public/locales/es/user.json +++ b/public/locales/es/user.json @@ -101,5 +101,20 @@ "transactions": "Transacciones", "update_payment_details": "Actualizar detalles de pago", "weekly_subscription": "SuscripciĆ³n semanal" + }, + "profile": { + "adult": "", + "avatar": "", + "content_rating": "", + "create": "", + "delete": "", + "delete_description": "", + "delete_main": "", + "description": "", + "greeting": "", + "greeting_with_name": "", + "info": "", + "kids": "", + "name": "" } } diff --git a/src/components/ProfileBox/ProfileBox.module.scss b/src/components/ProfileBox/ProfileBox.module.scss index 96420a311..c9f05f599 100644 --- a/src/components/ProfileBox/ProfileBox.module.scss +++ b/src/components/ProfileBox/ProfileBox.module.scss @@ -1,83 +1,83 @@ @use 'src/styles/variables'; .inner { - position: relative; - border: 1px solid rgba(255, 255, 255, 0.3); - border-radius: 4px; - &:hover { - border: 1px solid variables.$white; - opacity: 0.8; - } + position: relative; + box-sizing: border-box; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 4px; + &:hover { + border: 2px solid variables.$white; + opacity: 0.8; + } +} + +.selected { + border: 2px solid variables.$deep-blue !important; + border-radius: 4px; } + .wrapper { - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 10px; - cursor: pointer; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: min-content; + height: min-content; + cursor: pointer; - .image { - width: 100%; - height: 100%; - } + .image { + width: 100%; + height: 100%; + } - .box { - position: relative; - width: 140px; - height: 140px; - padding: 10px; - img { - position: relative; - transition: all 0.5s ease; - } - .kids { - position: absolute; - top: 5px; - right: 5px; - color: variables.$link; - } - &:hover { - img { - transform: translate(-5px, -5px); - } - } + .box { + position: relative; + width: 140px; + height: 140px; + padding: 10px; + img { + position: relative; + transition: all 0.5s ease; } - .circle { - display: flex; - justify-content: center; - align-items: center; - // border: 1px solid rgba(255, 255, 255, 0.3); - background: #1F232C; - border: 1px solid transparent; - border-radius: 100px; + .kidsLabel { + position: absolute; + top: 5px; + right: 5px; + color: variables.$link; } - .overlay { - position: absolute; - top: 0; - left: 0; - display: flex; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000000 95%); - border: 1px solid transparent; - border-radius: 4px; + &:hover { + img { + transform: translate(-5px, -5px); + } } + } + .circle { + display: flex; + justify-content: center; + align-items: center; + // border: 1px solid rgba(255, 255, 255, 0.3); + background: #1f232c; + border: 1px solid transparent; + border-radius: 100px; + } + .overlay { + position: absolute; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, #000000 95%); + border: 1px solid transparent; + border-radius: 4px; + } - &:hover { - .box { - border: 1px solid variables.$white; - opacity: 0.8; - } - h2 { - opacity: 0.8; - } + &:hover { + h2 { + opacity: 0.8; } - - - + } } - diff --git a/src/components/ProfileBox/ProfileBox.tsx b/src/components/ProfileBox/ProfileBox.tsx index 206606163..a2eedef81 100644 --- a/src/components/ProfileBox/ProfileBox.tsx +++ b/src/components/ProfileBox/ProfileBox.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import classNames from 'classnames'; import styles from './ProfileBox.module.scss'; @@ -11,15 +12,16 @@ type Props = { editMode?: boolean; onClick: () => void; onEdit: () => void; + selected?: boolean; }; -const ProfileBox = ({ name, image, adult = true, editMode = false, onClick, onEdit }: Props) => { +const ProfileBox = ({ name, image, adult = true, editMode = false, onClick, onEdit, selected = false }: Props) => { return (
-
+
- {!adult && Kids} + {!adult && Kids}
{editMode && (
diff --git a/src/components/Root/Root.tsx b/src/components/Root/Root.tsx index faa7b1b0d..33c05f1b4 100644 --- a/src/components/Root/Root.tsx +++ b/src/components/Root/Root.tsx @@ -1,7 +1,7 @@ import React, { FC, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from 'react-query'; -import { Navigate, useSearchParams } from 'react-router-dom'; +import { Navigate, useLocation, useSearchParams } from 'react-router-dom'; import ErrorPage from '#components/ErrorPage/ErrorPage'; import AccountModal from '#src/containers/AccountModal/AccountModal'; @@ -25,6 +25,7 @@ const Root: FC = () => { }); const [searchParams, setSearchParams] = useSearchParams(); + const location = useLocation(); const configSource = useMemo(() => getConfigSource(searchParams, settingsQuery.data), [searchParams, settingsQuery.data]); @@ -46,7 +47,7 @@ const Root: FC = () => { registerCustomScreens(); }, []); - const userData = useAccountStore((s) => ({ loading: s.loading, user: s.user })); + const userData = useAccountStore((s) => ({ loading: s.loading, user: s.user, profile: s.profile, canManageProfiles: s.canManageProfiles })); if (userData.user && !userData.loading && window.location.href.includes('#token')) { return ; // component instead of hook to prevent extra re-renders @@ -59,6 +60,10 @@ const Root: FC = () => { return ; } + if (userData.canManageProfiles && !userData.profile && !location.pathname.includes('/u/profiles')) { + return ; + } + if (settingsQuery.isError) { return ( { const { data, isFetching } = useListProfiles(); const profiles = data?.responseData.collection; - const selectProfile = useHandleProfileSelection(); + if (canManageProfiles && !profiles?.length) { + unpersistProfile(); + navigate('/u/profiles'); + } + + const selectProfile = useSelectProfile(); const onLogout = useCallback(async () => { if (onClick) { @@ -80,7 +85,7 @@ const UserMenu = ({ showPaymentsItem, small = false, onClick }: Props) => { } /> diff --git a/src/components/UserMenu/__snapshots__/UserMenu.test.tsx.snap b/src/components/UserMenu/__snapshots__/UserMenu.test.tsx.snap index 5cbec4048..c6b53112f 100644 --- a/src/components/UserMenu/__snapshots__/UserMenu.test.tsx.snap +++ b/src/components/UserMenu/__snapshots__/UserMenu.test.tsx.snap @@ -14,7 +14,7 @@ exports[` > renders and matches snapshot 1`] = `
{ const navigate = useNavigate(); const { canManageProfiles } = useAccountStore.getState(); const [fullName, setFullName] = useState(''); + const [avatarUrl, setAvatarUrl] = useState(''); useEffect(() => { if (!canManageProfiles) navigate('/'); @@ -45,7 +40,7 @@ const CreateProfile = () => { await createProfile({ name: formData.name, adult: formData.adult === 'true', - avatar_url: AVATARS[activeProfiles], + avatar_url: formData.avatar_url, }) )?.responseData; if (profile?.id) { @@ -70,12 +65,20 @@ const CreateProfile = () => {

Howdy{`${fullName && ','} ${fullName}`}

- +
-
+
); diff --git a/src/containers/Profiles/EditProfile.tsx b/src/containers/Profiles/EditProfile.tsx index 0e9aa2878..883896ba4 100644 --- a/src/containers/Profiles/EditProfile.tsx +++ b/src/containers/Profiles/EditProfile.tsx @@ -1,6 +1,8 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { useLocation, useNavigate, useParams } from 'react-router'; +import { useTranslation } from 'react-i18next'; +import classNames from 'classnames'; import profileStyles from './Profiles.module.scss'; import Form from './Form'; @@ -14,12 +16,21 @@ import Button from '#src/components/Button/Button'; import { addQueryParam } from '#src/utils/location'; import FormFeedback from '#src/components/FormFeedback/FormFeedback'; import { getProfileDetails, updateProfile } from '#src/stores/AccountController'; +import { useListProfiles } from '#src/hooks/useProfiles'; -const EditProfile = () => { - const { id } = useParams(); +type EditProfileProps = { + contained?: boolean; +}; + +const EditProfile = ({ contained = false }: EditProfileProps) => { + const params = useParams(); + const { id } = params; const location = useLocation(); const navigate = useNavigate(); const [fullName, setFullName] = useState(''); + const { t } = useTranslation('user'); + + const listProfiles = useListProfiles(); const { data, isLoading, isFetching } = useQuery(['getProfileDetails'], () => getProfileDetails({ id: id || '' }), { staleTime: 0, @@ -35,7 +46,13 @@ const EditProfile = () => { avatar_url: profileDetails?.avatar_url || '', pin: undefined, }; - }, [profileDetails]); + }, [profileDetails?.id, profileDetails?.name, profileDetails?.adult, profileDetails?.avatar_url]); + + const [selectedAvatar, setSelectedAvatar] = useState(initialValues?.avatar_url); + + useEffect(() => { + setSelectedAvatar(profileDetails?.avatar_url); + }, [profileDetails?.avatar_url]); if (!id) { navigate('/u/profiles'); @@ -54,6 +71,7 @@ const EditProfile = () => { if (profile?.id) { setSubmitting(false); + listProfiles.refetch(); navigate('/u/profiles'); } else { setErrors({ form: 'Something went wrong. Please try again later.' }); @@ -68,23 +86,40 @@ const EditProfile = () => { if (isLoading || isFetching) return ; return ( -
-
-
-
-

Howdy{`${fullName && ','} ${fullName}`}

- +
+ {!contained && ( +
+
+
+

{fullName ? t('profile.greeting_with_name', { fullName }) : t('profile.greeting')}

+ +
-
-
- + )} +
+
-

Delete profile

- {profileDetails?.default && The main profile cannot be deleted.} +
+

{t('profile.delete')}

+
+ {profileDetails?.default ? ( + {t('profile.delete_main')} + ) : ( +
{t('profile.delete_description')}
+ )}
diff --git a/src/containers/Profiles/Profiles.module.scss b/src/containers/Profiles/Profiles.module.scss index d23ead828..74b1a0acd 100644 --- a/src/containers/Profiles/Profiles.module.scss +++ b/src/containers/Profiles/Profiles.module.scss @@ -1,65 +1,100 @@ +@use 'src/styles/variables'; + .headings { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - gap: 24px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 24px; } .heading { - font-weight: bold; - font-size: 40px; + font-weight: bold; + font-size: 40px; +} + +.profileInfo { + display: flex; + justify-content: flex-start; + align-items: center; + padding-bottom: 20px; } + .paragarph { - margin: 0; - font-size: 36px; + margin: 0; + font-size: 36px; } .wrapper { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - width: 100%; - height: 100%; - gap: 50px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + gap: 50px; } .flex { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: center; - gap: 40px; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 40px; } .formFields { - display: flex; - flex-direction: column; - max-width: 350px; - gap: 10px; - margin-bottom: 20px; + display: flex; + flex-direction: column; + max-width: 350px; + gap: 10px; + margin-bottom: 20px; } .avatar { - img { - position: relative; - width: 100%; - } + img { + position: relative; + width: 100%; + } } .overlayBox { - position: relative; + position: relative; } .deleteModal { - .deleteButtons { - display: flex; - justify-content: flex-end; - } - h2 { - font-weight: 400; - font-size: 20px; - } - p { - font-size: 16px; - } + .deleteButtons { + display: flex; + justify-content: flex-end; + } + h2 { + font-weight: 400; + font-size: 20px; + } + p { + font-size: 16px; + } +} + +.contained { + margin: 0; +} + +.divider { + margin: 24px 0 24px; + border: none; + border-top: 1px solid rgba(variables.$white, 0.34); +} + +.noBottomBorder { + margin-bottom: none; + padding-bottom: 0; + border-bottom: none; +} + +.avatarsContainer { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + grid-gap: 16px; + width: 100%; + padding: 32px 0; + justify-items: center; } diff --git a/src/containers/Profiles/Profiles.tsx b/src/containers/Profiles/Profiles.tsx index 3b6600178..08e79799f 100644 --- a/src/containers/Profiles/Profiles.tsx +++ b/src/containers/Profiles/Profiles.tsx @@ -10,7 +10,7 @@ import type { Profile } from '#types/account'; import AddNewProfile from '#src/components/ProfileBox/AddNewProfile'; import LoadingOverlay from '#src/components/LoadingOverlay/LoadingOverlay'; import Button from '#src/components/Button/Button'; -import { useHandleProfileSelection, useListProfiles } from '#src/hooks/useProfiles'; +import { useSelectProfile, useListProfiles } from '#src/hooks/useProfiles'; const MAX_PROFILES = 4; type Props = { @@ -26,11 +26,15 @@ const Profiles = ({ editMode = false }: Props) => { if (!canManageProfiles) navigate('/'); }, [canManageProfiles, navigate]); - const { data, isLoading, isFetching } = useListProfiles(); + const { data, isLoading, isFetching, refetch } = useListProfiles(); const activeProfiles = data?.responseData.collection.length || 0; const canAddNew = activeProfiles < MAX_PROFILES; - const selectProfile = useHandleProfileSelection(); + const selectProfile = useSelectProfile(); + + useEffect(() => { + refetch(); + }, [refetch]); if (loading || isLoading || isFetching) return ; diff --git a/src/containers/Profiles/avatarUrls.json b/src/containers/Profiles/avatarUrls.json new file mode 100644 index 000000000..5bf85d5fb --- /dev/null +++ b/src/containers/Profiles/avatarUrls.json @@ -0,0 +1,6 @@ +[ + "https://gravatar.com/avatar/5e62c8c13582f94b74ae21cfeb83e28a?s=400&d=robohash&r=x", + "https://gravatar.com/avatar/a82dc2482b1ae8d9070462a37b5e19e9?s=400&d=robohash&r=x", + "https://gravatar.com/avatar/236030198309afe28c9fce9c3ebfec3b?s=400&d=robohash&r=x", + "https://gravatar.com/avatar/c97a042d43cc5cc28802f2bc7bf2e5ab?s=400&d=robohash&r=x" +] diff --git a/src/containers/Profiles/types.d.ts b/src/containers/Profiles/types.d.ts index b4e2cc212..2a1d144e1 100644 --- a/src/containers/Profiles/types.d.ts +++ b/src/containers/Profiles/types.d.ts @@ -1 +1,3 @@ +import type { ProfilePayload } from '#types/account'; + export type ProfileFormValues = Omit & { adult: string }; diff --git a/src/hooks/useProfiles.ts b/src/hooks/useProfiles.ts index abb141eb3..bf9dc0418 100644 --- a/src/hooks/useProfiles.ts +++ b/src/hooks/useProfiles.ts @@ -10,12 +10,17 @@ import type { AuthData } from '#types/account'; const PERSIST_KEY_ACCOUNT = 'auth'; const PERSIST_PROFILE = 'profile'; -type HandleProfileSelectionPayload = { +type ProfileSelectionPayload = { id: string; navigate: (path: string) => void; }; -const handleProfileSelection = async ({ id, navigate }: HandleProfileSelectionPayload) => { +export const unpersistProfile = () => { + persist.removeItem(PERSIST_PROFILE); + persist.removeItem(PERSIST_KEY_ACCOUNT); +}; + +const handleProfileSelection = async ({ id, navigate }: ProfileSelectionPayload) => { try { useAccountStore.setState({ loading: true }); const response = await enterProfile({ id }); @@ -44,7 +49,7 @@ const handleProfileSelection = async ({ id, navigate }: HandleProfileSelectionPa } }; -export const useHandleProfileSelection = () => +export const useSelectProfile = () => useMutation(handleProfileSelection, { onSettled: () => { getAccount(); diff --git a/src/pages/User/User.module.scss b/src/pages/User/User.module.scss index bc95872eb..b99564d85 100644 --- a/src/pages/User/User.module.scss +++ b/src/pages/User/User.module.scss @@ -85,3 +85,8 @@ margin-right: 10px; } } + +.profileIcon { + width: 20px; + height: 20px; +} diff --git a/src/pages/User/User.tsx b/src/pages/User/User.tsx index 5cdf7d674..777381b19 100644 --- a/src/pages/User/User.tsx +++ b/src/pages/User/User.tsx @@ -23,10 +23,10 @@ import Favorites from '#components/Favorites/Favorites'; import type { PlaylistItem } from '#types/playlist'; import { getReceipt, logout } from '#src/stores/AccountController'; import { clear as clearFavorites } from '#src/stores/FavoritesController'; -import ArrowLeftRight from '#src/icons/ArrowLeftRight'; import { getSubscriptionSwitches } from '#src/stores/CheckoutController'; import { useCheckoutStore } from '#src/stores/CheckoutStore'; import { addQueryParam } from '#src/utils/location'; +import EditProfile from '#src/containers/Profiles/EditProfile'; const User = (): JSX.Element => { const { accessModel, favoritesList } = useConfigStore( @@ -56,6 +56,7 @@ const User = (): JSX.Element => { canUpdatePaymentMethod, canShowReceipts, canManageProfiles, + profile, } = useAccountStore(); const offerSwitches = useCheckoutStore((state) => state.offerSwitches); const location = useLocation(); @@ -119,14 +120,20 @@ const User = (): JSX.Element => {
    -
  • -
  • {accessModel === 'SVOD' && canManageProfiles && (
  • -
  • )} +
  • +
  • {favoritesList && (