diff --git a/.commitlintrc.js b/.commitlintrc.js index 906310dd5..190044ba2 100644 --- a/.commitlintrc.js +++ b/.commitlintrc.js @@ -9,6 +9,7 @@ module.exports = { 'videodetail', 'series', 'search', + 'user', 'watchhistory', 'favorites', 'analytics', diff --git a/src/components/Account/Account.module.scss b/src/components/Account/Account.module.scss new file mode 100644 index 000000000..4026567a5 --- /dev/null +++ b/src/components/Account/Account.module.scss @@ -0,0 +1,11 @@ +@use '../../styles/variables'; +@use '../../styles/theme'; +@use '../../styles/mixins/responsive'; + +.checkbox { + display: flex; + align-items: center; + > input { + margin-right: 10px; + } +} diff --git a/src/components/Account/Account.test.tsx b/src/components/Account/Account.test.tsx new file mode 100644 index 000000000..d09cfc816 --- /dev/null +++ b/src/components/Account/Account.test.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import type { Customer } from 'types/account'; + +import Account from './Account'; + +describe('', () => { + test('renders and matches snapshot', () => { + const customer: Customer = { + id: '1', + email: 'todo@test.nl', + locale: 'en_en', + country: 'England', + currency: 'Euro', + lastUserIp: 'temp', + }; + const { container } = render( console.info(customer)} />); + + // todo + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/Account/Account.tsx b/src/components/Account/Account.tsx new file mode 100644 index 000000000..4f6b67605 --- /dev/null +++ b/src/components/Account/Account.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import type { Customer } from 'types/account'; + +import Button from '../../components/Button/Button'; + +import styles from './Account.module.scss'; + +type Props = { + customer: Customer; + update: (customer: Customer) => void; + panelClassName?: string; + panelHeaderClassName?: string; +}; + +const Account = ({ customer, update, panelClassName, panelHeaderClassName }: Props): JSX.Element => { + const { t } = useTranslation('user'); + + return ( + <> +
+
+

{t('account.email')}

+
+
+ {t('account.email')} +

{customer.email}

+
+
+
+
+

{t('account.security')}

+
+
+ {t('account.password')} +

****************

+
+
+
+
+

{t('account.about_you')}

+
+
+ {t('account.firstname')} +

{customer.firstName}

+ {t('account.lastname')} +

{customer.lastName}

+
+
+
+
+

{'Terms & tracking'}

+
+
+ +
+
+ + ); +}; + +export default Account; diff --git a/src/components/Account/__snapshots__/Account.test.tsx.snap b/src/components/Account/__snapshots__/Account.test.tsx.snap new file mode 100644 index 000000000..4a3cdc38b --- /dev/null +++ b/src/components/Account/__snapshots__/Account.test.tsx.snap @@ -0,0 +1,108 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders and matches snapshot 1`] = ` +
+
+
+

+ account.email +

+
+
+ + account.email + +

+ todo@test.nl +

+ +
+
+
+
+

+ account.security +

+
+
+ + account.password + +

+ **************** +

+ +
+
+
+
+

+ account.about_you +

+
+
+ + account.firstname + +

+ + account.lastname + +

+ +

+
+
+
+

+ Terms & tracking +

+
+
+ + +
+
+
+`; diff --git a/src/components/Payment/Payment.module.scss b/src/components/Payment/Payment.module.scss new file mode 100644 index 000000000..ff3246570 --- /dev/null +++ b/src/components/Payment/Payment.module.scss @@ -0,0 +1,38 @@ +@use '../../styles/variables'; +@use '../../styles/theme'; +@use '../../styles/mixins/responsive'; + +.infoBox { + display: flex; + justify-content: space-between; + margin-bottom: variables.$base-spacing; + padding: variables.$base-spacing / 2 variables.$base-spacing; + + font-size: 14px; + line-height: 18px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 3px 4px rgba(0, 0, 0, 0.12), 0 1px 5px rgba(0, 0, 0, 0.2); + background: rgba(61, 59, 59, 0.08); + border-radius: 4px; + > strong { + line-height: 16px; + letter-spacing: 0.25px; + } +} + +.price { + font-size: 14px; + line-height: 18px; + > strong { + font-weight: bold; + font-size: 24px; + line-height: 26px; + } +} + +.cardDetails { + display: flex; +} + +.expiryDate { + width: 250px; +} diff --git a/src/components/Payment/Payment.test.tsx b/src/components/Payment/Payment.test.tsx new file mode 100644 index 000000000..31c6f03b6 --- /dev/null +++ b/src/components/Payment/Payment.test.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import type { Subscription } from 'types/subscription'; + +import Payment from './Payment'; + +describe('', () => { + test('renders and matches snapshot', () => { + const subscription = {} as Subscription; + + const { container } = render( console.info(subscription)} />); + + // todo + expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/components/Payment/Payment.tsx b/src/components/Payment/Payment.tsx new file mode 100644 index 000000000..234577254 --- /dev/null +++ b/src/components/Payment/Payment.tsx @@ -0,0 +1,81 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import type { Subscription } from 'types/subscription'; + +import Button from '../Button/Button'; + +import styles from './Payment.module.scss'; + +type Props = { + subscription: Subscription; + onEditSubscriptionClick: (subscription: Subscription) => void; + panelClassName?: string; + panelHeaderClassName?: string; +}; + +const Payment = ({ subscription, onEditSubscriptionClick, panelClassName, panelHeaderClassName }: Props): JSX.Element => { + const { t } = useTranslation('user'); + const showAllTransactions = () => console.info('show all'); + + return ( + <> +
+
+

{t('payment.subscription_details')}

+
+
+

+ {t('payment.monthly_subscription')}
+ {t('payment.next_billing_date_on')} + {''} +

+

+ {'€ 14.76'} + {'/'} + {t('payment.month')} +

+
+
+
+
+

{t('payment.payment_method')}

+
+
+ {t('payment.card_number')} +

xxxx xxxx xxxx 3456

+
+
+ {t('payment.expiry_date')} +

{subscription.expiresAt}

+
+
+ {t('payment.cvc_cvv')} +

******

+
+
+
+
+
+
+

{t('payment.transactions')}

+
+
+

+ {t('payment.monthly_subscription')}
+ {t('payment.price_payed_with_card')} +

+

+ {''} +
+ {''} +

+
+

{t('payment.more_transactions', { amount: 4 })}

+
+ + ); +}; + +export default Payment; diff --git a/src/components/Payment/__snapshots__/Payment.test.tsx.snap b/src/components/Payment/__snapshots__/Payment.test.tsx.snap new file mode 100644 index 000000000..fd7eb1c60 --- /dev/null +++ b/src/components/Payment/__snapshots__/Payment.test.tsx.snap @@ -0,0 +1,111 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` renders and matches snapshot 1`] = ` +
+
+
+

+ payment.subscription_details +

+
+
+

+ + payment.monthly_subscription + + +
+ payment.next_billing_date_on + <date> +

+

+ + € 14.76 + + / + payment.month +

+
+ +
+
+
+

+ payment.payment_method +

+
+
+ + payment.card_number + +

+ xxxx xxxx xxxx 3456 +

+
+
+ + payment.expiry_date + +

+

+
+ + payment.cvc_cvv + +

+ ****** +

+
+
+
+
+
+
+

+ payment.transactions +

+
+
+

+ + payment.monthly_subscription + + +
+ payment.price_payed_with_card +

+

+ <Invoice code> +
+ <Date> +

+
+

+ payment.more_transactions +

+ +
+
+`; diff --git a/src/components/Root/Root.tsx b/src/components/Root/Root.tsx index 95665d101..9245fa914 100644 --- a/src/components/Root/Root.tsx +++ b/src/components/Root/Root.tsx @@ -2,11 +2,11 @@ import React, { FC } from 'react'; import { Route, Switch } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import User from '../../screens/User/User'; import Series from '../../screens/Series/Series'; import Layout from '../Layout/Layout'; import Home from '../../screens/Home/Home'; import Playlist from '../../screens/Playlist/Playlist'; -import Settings from '../../screens/Settings/Settings'; import Movie from '../../screens/Movie/Movie'; import Search from '../../screens/Search/Search'; import ErrorPage from '../ErrorPage/ErrorPage'; @@ -31,10 +31,10 @@ const Root: FC = ({ error }: Props) => { - + ); diff --git a/src/containers/Customer/Customer.test.ts b/src/containers/Customer/Customer.test.ts new file mode 100644 index 000000000..e973770ad --- /dev/null +++ b/src/containers/Customer/Customer.test.ts @@ -0,0 +1,11 @@ +// import React from 'react'; + +describe('', () => { + test('renders and matches snapshot', () => { + // todo + // const item = { + // }; + // const { container } = render( null} onPause={() => null} />); + // expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/containers/Customer/Customer.ts b/src/containers/Customer/Customer.ts new file mode 100644 index 000000000..fbf22e8b0 --- /dev/null +++ b/src/containers/Customer/Customer.ts @@ -0,0 +1,26 @@ +import type { Customer } from 'types/account'; + +type ChildrenParams = { + customer: Customer; + update: () => void; +}; + +type Props = { + children: (data: ChildrenParams) => JSX.Element; +}; + +const Account = ({ children }: Props): JSX.Element => { + const customer: Customer = { + id: '1', + email: 'todo@test.nl', + locale: 'en_en', + country: 'England', + currency: 'Euro', + lastUserIp: 'temp', + }; + const update = (customer: Customer) => console.info('update', customer); + + return children({ customer, update } as ChildrenParams); +}; + +export default Account; diff --git a/src/containers/Subscription/Subscription.test.ts b/src/containers/Subscription/Subscription.test.ts new file mode 100644 index 000000000..00fe6fc00 --- /dev/null +++ b/src/containers/Subscription/Subscription.test.ts @@ -0,0 +1,11 @@ +// import React from 'react'; + +describe('', () => { + test('renders and matches snapshot', () => { + // todo + // const item = { + // }; + // const { container } = render( null} onPause={() => null} />); + // expect(container).toMatchSnapshot(); + }); +}); diff --git a/src/containers/Subscription/Subscription.ts b/src/containers/Subscription/Subscription.ts new file mode 100644 index 000000000..85852a391 --- /dev/null +++ b/src/containers/Subscription/Subscription.ts @@ -0,0 +1,31 @@ +import type { Subscription } from 'types/subscription'; + +type ChildrenParams = { + subscription: Subscription; + update: () => void; +}; + +type Props = { + children: (data: ChildrenParams) => JSX.Element; +}; + +const SubscriptionContainer = ({ children }: Props): JSX.Element => { + const subscription: Subscription = { + subscriptionId: 1, + offerId: '2', + status: 'active', + expiresAt: 2000, + nextPaymentPrice: 20, + nextPaymentCurrency: 'euro', + paymentGateway: 'todo', + paymentMethod: 'todo', + offerTitle: 'Temporary offer', + period: 'month', + totalPrice: 300, + }; + const update = (values: Subscription) => console.info('update', values); + + return children({ subscription, update } as ChildrenParams); +}; + +export default SubscriptionContainer; diff --git a/src/i18n/locales/en_US.ts b/src/i18n/locales/en_US.ts index 76a9f9319..ec7be0340 100644 --- a/src/i18n/locales/en_US.ts +++ b/src/i18n/locales/en_US.ts @@ -6,3 +6,4 @@ export { default as error } from './en_US/error.json'; export { default as menu } from './en_US/menu.json'; export { default as search } from './en_US/search.json'; export { default as video } from './en_US/video.json'; +export { default as user } from './en_US/user.json'; diff --git a/src/i18n/locales/en_US/user.json b/src/i18n/locales/en_US/user.json new file mode 100644 index 000000000..36130cf50 --- /dev/null +++ b/src/i18n/locales/en_US/user.json @@ -0,0 +1,30 @@ +{ + "account": { + "email": "Email", + "edit_account": "Edit account", + "security": "Security", + "password": "Password", + "edit_password": "Edit password", + "about_you": "About you", + "firstname": "First name", + "lastname": "Last name", + "edit_information": "Edit information", + "terms_and_tracking": "Terms & tracking", + "update_consents": "Update consents" + }, + "payment": { + "subscription_details": "Subscription details", + "monthly_subscription": "Monthly subscription", + "next_billing_date_on": "Next billing date is on", + "month": "month", + "edit_subscription": "Edit subscription", + "payment_method": "Payment method", + "card_number": "Card number", + "expiry_date": "Expiry date", + "cvc_cvv": "CVC / CVV", + "transactions": "Transactions", + "price_payed_with_card": "Price payed with card", + "more_transactions": "{{ amount }} more transactions", + "show_all": "Show all" + } +} diff --git a/src/i18n/locales/nl_NL.ts b/src/i18n/locales/nl_NL.ts index 27379f9a7..dc79decca 100644 --- a/src/i18n/locales/nl_NL.ts +++ b/src/i18n/locales/nl_NL.ts @@ -6,3 +6,4 @@ export { default as error } from './nl_NL/error.json'; export { default as menu } from './nl_NL/menu.json'; export { default as search } from './nl_NL/search.json'; export { default as video } from './nl_NL/video.json'; +export { default as user } from './nl_NL/user.json'; diff --git a/src/i18n/locales/nl_NL/user.json b/src/i18n/locales/nl_NL/user.json new file mode 100644 index 000000000..1649cd42d --- /dev/null +++ b/src/i18n/locales/nl_NL/user.json @@ -0,0 +1,30 @@ +{ + "account": { + "email": "", + "edit_account": "", + "security": "", + "password": "", + "edit_password": "", + "about_you": "", + "firstname": "", + "lastname": "", + "edit_information": "", + "terms_and_tracking": "", + "update_consents": "" + }, + "payment": { + "subscription_details": "", + "monthly_subscription": "", + "next_billing_date_on": "", + "month": "", + "edit_subscription": "", + "payment_method": "", + "card_number": "", + "expiry_date": "", + "cvc_cvv": "", + "transactions": "", + "price_payed_with_card": "", + "more_transactions": "", + "show_all": "" + } +} diff --git a/src/icons/AccountCircle.tsx b/src/icons/AccountCircle.tsx new file mode 100644 index 000000000..c6f7be028 --- /dev/null +++ b/src/icons/AccountCircle.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +import createIcon from './Icon'; + +export default createIcon( + '0 0 24 24', + + + , +); diff --git a/src/icons/BalanceWallet.tsx b/src/icons/BalanceWallet.tsx new file mode 100644 index 000000000..2d845ec67 --- /dev/null +++ b/src/icons/BalanceWallet.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +import createIcon from './Icon'; + +export default createIcon( + '0 0 24 24', + + + , +); diff --git a/src/icons/Exit.tsx b/src/icons/Exit.tsx new file mode 100644 index 000000000..a72b125f9 --- /dev/null +++ b/src/icons/Exit.tsx @@ -0,0 +1,10 @@ +import React from 'react'; + +import createIcon from './Icon'; + +export default createIcon( + '0 0 24 24', + + + , +); diff --git a/src/providers/ConfigProvider.tsx b/src/providers/ConfigProvider.tsx index 106017327..df9960784 100644 --- a/src/providers/ConfigProvider.tsx +++ b/src/providers/ConfigProvider.tsx @@ -53,7 +53,7 @@ const ConfigProvider: FunctionComponent = ({ children, configLoca // @todo refactor this provider to use the ConfigStore exclusively setConfig(configWithDefaults); - ConfigStore.update(s => { + ConfigStore.update((s) => { s.config = configWithDefaults; }); diff --git a/src/screens/User/User.module.scss b/src/screens/User/User.module.scss new file mode 100644 index 000000000..ab481c008 --- /dev/null +++ b/src/screens/User/User.module.scss @@ -0,0 +1,86 @@ +@use '../../styles/variables'; +@use '../../styles/theme'; +@use '../../styles/mixins/responsive'; + +.user { + display: flex; + justify-content: center; + margin: variables.$base-spacing * 3.5 variables.$base-spacing * 4; + color: var(--primary-color); + font-family: var(--body-alt-font-family); + + @include responsive.mobile-only() { + margin: 0 variables.$base-spacing; + } + + @include responsive.tablet-only() { + margin: 0 variables.$base-spacing * 2; + } +} +.leftColumn { + width: 250px; + margin-right: 24px; + padding-left: 19px; + + font-weight: bold; + font-size: 18px; + font-style: normal; + line-height: 20px; + letter-spacing: 0.5px; +} +ul { + margin: 0; + padding: 0; + list-style-type: none; +} + +.button { + margin-bottom: variables.$base-spacing; +} +.logoutLi { + margin-bottom: 0; + padding-top: variables.$base-spacing; + border-top: 1px solid rgba(255, 255, 255, 0.32); + > a { + margin-bottom: 0; + } +} + +.mainColumn { + width: 100%; + max-width: 750px; +} + +.panel { + margin-bottom: variables.$base-spacing * 1.5; + padding: variables.$base-spacing; + + font-weight: normal; + font-size: 16px; + font-style: normal; + line-height: 18px; + letter-spacing: 0.15px; + background: theme.$panel-bg; + box-shadow: theme.$panel-box-shadow; +} + +.panelHeader { + margin-bottom: variables.$base-spacing; + padding-bottom: variables.$base-spacing; + border-bottom: theme.$panel-header-border-bottom; + + > h3 { + font-weight: bold; + font-size: 24px; + line-height: 26px; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.14), 0 3px 4px rgba(0, 0, 0, 0.12), 0 1px 5px rgba(0, 0, 0, 0.2); + } +} + +.checkbox { + display: flex; + align-items: center; + > input { + margin-right: 10px; + } +} diff --git a/src/screens/User/User.test.tsx b/src/screens/User/User.test.tsx new file mode 100644 index 000000000..fd83caa97 --- /dev/null +++ b/src/screens/User/User.test.tsx @@ -0,0 +1,14 @@ +// import React from 'react'; +// import { render } from '@testing-library/react'; + +import { mockMatchMedia } from '../../testUtils'; + +// import Home from './Home'; + +describe('Home Component tests', () => { + mockMatchMedia(); + test('dummy test', () => { + // render(); + // expect(screen.getByText('hello world')).toBeInTheDocument(); + }); +}); diff --git a/src/screens/User/User.tsx b/src/screens/User/User.tsx new file mode 100644 index 000000000..515ede3ab --- /dev/null +++ b/src/screens/User/User.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; + +import Customer from '../../containers/Customer/Customer'; +import SubscriptionContainer from '../../containers/Subscription/Subscription'; +import useBreakpoint, { Breakpoint } from '../../hooks/useBreakpoint'; +import Button from '../../components/Button/Button'; +import Account from '../../components/Account/Account'; +import Payment from '../../components/Payment/Payment'; +import AccountCircle from '../../icons/AccountCircle'; +import Favorite from '../../icons/Favorite'; +import BalanceWallet from '../../icons/BalanceWallet'; +import Exit from '../../icons/Exit'; + +import styles from './User.module.scss'; + +const User = (): JSX.Element => { + const breakpoint = useBreakpoint(); + const isLargeScreen = breakpoint >= Breakpoint.md; + + return ( +
+ {isLargeScreen && ( +
+
+
    +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
  • +
+
+
+ )} +
+ + + + {({ customer, update }) => ( + + )} + + + +
Favorites
+
+ + + {({ subscription, update }) => ( + + )} + + + + + + + + +
+
+
+ ); +}; + +export default User; diff --git a/src/services/account.service.ts b/src/services/account.service.ts index d17ae7a22..b583721c2 100644 --- a/src/services/account.service.ts +++ b/src/services/account.service.ts @@ -1,9 +1,4 @@ -import type { - ChangePassword, GetCustomer, - Login, - Register, - ResetPassword, -} from '../../types/account'; +import type { ChangePassword, GetCustomer, Login, Register, ResetPassword } from '../../types/account'; import { post, put, patch, get } from './cleeng.service'; diff --git a/src/stores/AccountStore.ts b/src/stores/AccountStore.ts index 618b67ce8..693629ea0 100644 --- a/src/stores/AccountStore.ts +++ b/src/stores/AccountStore.ts @@ -1,4 +1,4 @@ -import { Store } from 'pullstate' +import { Store } from 'pullstate'; import jwtDecode from 'jwt-decode'; import * as accountService from '../services/account.service'; @@ -7,8 +7,8 @@ import type { AuthData, Customer, JwtDetails } from '../../types/account'; import { ConfigStore } from './ConfigStore'; type AccountStore = { - auth: AuthData | null, - user: Customer | null, + auth: AuthData | null; + user: Customer | null; }; export const AccountStore = new Store({ @@ -24,14 +24,16 @@ const afterLogin = async (sandbox: boolean, auth: AuthData) => { if (response.errors.length) throw new Error(response.errors[0]); - AccountStore.update(s => { + AccountStore.update((s) => { s.auth = auth; s.user = response.responseData; }); }; export const login = async (email: string, password: string) => { - const { config: { cleengId, cleengSandbox } } = ConfigStore.getRawState(); + const { + config: { cleengId, cleengSandbox }, + } = ConfigStore.getRawState(); if (!cleengId) throw new Error('cleengId is not configured'); diff --git a/src/styles/_theme.scss b/src/styles/_theme.scss index 530fe5174..25c094839 100644 --- a/src/styles/_theme.scss +++ b/src/styles/_theme.scss @@ -168,3 +168,8 @@ $video-details-tag-hover-bg: darken($dark-color, 5%) !default; $video-details-tag-color: variables.$white !default; $video-details-tag-hover-color: variables.$white !default; $video-details-tag-shadow: 0 1px 0 variables.$black !default; + +// UserScreen +$panel-bg: rgba(255, 255, 255, 0.08); +$panel-box-shadow: 0 6px 10px rgb(0 0 0 / 14%), 0 1px 18px rgb(0 0 0 / 12%), 0 3px 5px rgb(0 0 0 / 20%); +$panel-header-border-bottom: 1px solid rgba(255, 255, 255, 0.32);