From fa90fc18f62c2a1071c757b5ddecba71feb54792 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson Date: Tue, 27 Jun 2017 17:10:15 -0700 Subject: [PATCH] feat: Add desktop home page (fix #2665) --- src/amo/components/Home/index.js | 185 ++++++++++++++++++++++ src/amo/components/Home/styles.scss | 195 ++++++++++++++++++++++++ src/amo/components/LandingPage/index.js | 10 +- src/amo/containers/Home.js | 109 ------------- src/amo/css/Home.scss | 176 --------------------- src/amo/routes.js | 2 +- tests/unit/amo/containers/TestHome.js | 117 +++++++++++--- 7 files changed, 482 insertions(+), 312 deletions(-) create mode 100644 src/amo/components/Home/index.js create mode 100644 src/amo/components/Home/styles.scss delete mode 100644 src/amo/containers/Home.js delete mode 100644 src/amo/css/Home.scss diff --git a/src/amo/components/Home/index.js b/src/amo/components/Home/index.js new file mode 100644 index 00000000000..e99335ec221 --- /dev/null +++ b/src/amo/components/Home/index.js @@ -0,0 +1,185 @@ +import classNames from 'classnames'; +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { setViewContext } from 'amo/actions/viewContext'; +import Link from 'amo/components/Link'; +import { + CLIENT_APP_ANDROID, + CLIENT_APP_FIREFOX, + VIEW_CONTEXT_HOME, +} from 'core/constants'; +import translate from 'core/i18n/translate'; +import Card from 'ui/components/Card'; + +import './styles.scss'; + + +export const CategoryLink = ({ children, name, slug, type }) => { + return ( +
  • + + {children} + +
  • + ); +}; +CategoryLink.propTypes = { + children: PropTypes.node.isRequired, + name: PropTypes.string.isRequired, + slug: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, +}; + +export const ExtensionLink = (props) => { + return ; +}; + +export const ThemeLink = (props) => { + return ; +}; + +export class HomeBase extends React.Component { + static propTypes = { + clientApp: PropTypes.string.isRequired, + dispatch: PropTypes.func.isRequired, + i18n: PropTypes.object.isRequired, + } + + componentWillMount() { + const { dispatch } = this.props; + + dispatch(setViewContext(VIEW_CONTEXT_HOME)); + } + + extensionsCategoriesForClientApp() { + const { clientApp, i18n } = this.props; + + let linkHTML = null; + + if (clientApp === CLIENT_APP_ANDROID) { + linkHTML = ( +
      + + {i18n.gettext('Block ads')} + + + {i18n.gettext('Screenshot')} + + + {i18n.gettext('Find news')} + + + {i18n.gettext('Shop online')} + + + {i18n.gettext('Be social')} + + + {i18n.gettext('Play games')} + +
    + ); + } + + if (clientApp === CLIENT_APP_FIREFOX) { + linkHTML = ( +
      + + {i18n.gettext('Block ads')} + + + {i18n.gettext('Screenshot')} + + + {i18n.gettext('Find news')} + + + {i18n.gettext('Shop online')} + + + {i18n.gettext('Be social')} + + + {i18n.gettext('Play games')} + +
    + ); + } + + return linkHTML; + } + + themesCategoriesForClientApp() { + const { i18n } = this.props; + + return ( +
      + {i18n.gettext('Wild')} + {i18n.gettext('Abstract')} + {i18n.gettext('Holiday')} + {i18n.gettext('Scenic')} + {i18n.gettext('Sporty')} + {i18n.gettext('Solid')} +
    + ); + } + + render() { + const { i18n } = this.props; + + return ( +
    + + {i18n.gettext('Browse all extensions')} + } + > +
    +

    + {i18n.gettext('You can change how Firefox works…')} +

    +

    + {i18n.gettext( + 'Install powerful tools that make browsing faster and safer, add-ons make your browser yours.')} +

    +
    + + {this.extensionsCategoriesForClientApp()} +
    + + + {i18n.gettext('Browse all themes')} + } + > +
    +

    + {i18n.gettext('…or what it looks like')} +

    +

    + {i18n.gettext( + "Change your browser's appearance. Choose from thousands of themes to give Firefox the look you want.")} +

    +
    + + {this.themesCategoriesForClientApp()} +
    +
    + ); + } +} + +export function mapStateToProps(state) { + return { clientApp: state.api.clientApp }; +} + +export default compose( + // This allows us to dispatch from our component. + connect(mapStateToProps), + translate({ withRef: true }), +)(HomeBase); diff --git a/src/amo/components/Home/styles.scss b/src/amo/components/Home/styles.scss new file mode 100644 index 00000000000..b27c7e68bf5 --- /dev/null +++ b/src/amo/components/Home/styles.scss @@ -0,0 +1,195 @@ +@import "~core/css/inc/mixins"; +@import "~amo/css/inc/vars"; +@import "~ui/css/vars"; + +.Home { + padding: $padding-page; +} + +.Home-category-card { + margin-bottom: 10px; + + .Card-contents { + @include respond-to(medium) { + display: flex; + flex-flow: row nowrap; + } + } +} + +.Home-text-wrapper { + align-self: auto; + + @include respond-to(medium) { + align-self: center; + } + + @include respond-to(large) { + max-width: 200px; + } +} + +.Home-subheading { + line-height: 1.2; + margin-bottom: -10px; + margin-top: 18px; + text-align: center; + + @include respond-to(medium) { + @include text-align-start(); + } +} + +.Home-description { + display: none; + + @include respond-to(medium) { + display: block; + } +} + +.Home-category-list { + display: flex; + flex-flow: row wrap; + overflow: auto; + margin-bottom: 0; + padding: 0; + width: 100%; + + @include respond-to(large) { + flex-wrap: nowrap; + } +} + +.Home-category-li { + background-color: $base-color; + background-size: 50% auto; + background-position: 50% 35%; + background-repeat: no-repeat; + border-radius: 6px; + display: block; + flex-grow: 1; + list-style-type: none; + margin: 5px; + overflow: auto; + padding: 0; + position: relative; + text-align: center; + width: 25%; + + a:link { + font-size: 10px; + text-decoration: none; + padding-top: 70%; + display: block; + } + + a:hover, + a:focus { + text-decoration: underline; + } + + span { + display: block; + padding: 5px 10px; + text-align: center; + text-decoration: inherit; + } +} + +.Home-category-link, +.Home-extensions-link, +.Home-themes-link { + &, + &:visited { + color: $link-color; + } +} + +.Home-extensions-link:link, +.Home-themes-link:link { + align-items: center; + background: $base-color; + border-radius: 6px; + display: flex; + font-size: 10px; + justify-content: center; + margin: 0 auto 20px; + padding: 10px; + text-align: center; + text-decoration: none; + text-transform: uppercase; + width: calc(100% - 40px); + + &:hover, + &:focus { + text-decoration: underline; + } + + &::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + margin: 0 5px; + } +} + +.Home-extensions-link:link::before { + background: url('~amo/img/icons/addon-outline.svg') no-repeat 50% 50%; + background-size: cover; +} + +.Home-themes-link:link::before { + background: url('~amo/img/icons/theme-outline.svg') no-repeat 50% 50%; + background-size: cover; +} + +.Home-block-ads { + background-image: url('~amo/img/home/block-ads.svg'); +} + +.Home-screenshot { + background-image: url('~amo/img/home/screenshot.svg'); +} + +.Home-find-news { + background-image: url('~amo/img/home/find-news.svg'); + background-size: 40% auto; +} + +.Home-shop-online { + background-image: url('~amo/img/home/shop-online.svg'); +} + +.Home-be-social { + background-image: url('~amo/img/home/be-social.svg'); +} + +.Home-play-games { + background-image: url('~amo/img/home/play-games.svg'); +} + +.Home-wild { + background-image: url('~amo/img/home/wild.svg'); +} + +.Home-abstract { + background-image: url('~amo/img/home/abstract.svg'); +} + +.Home-holiday { + background-image: url('~amo/img/home/holiday.svg'); +} + +.Home-scenic { + background-image: url('~amo/img/home/scenic.svg'); +} + +.Home-sporty { + background-image: url('~amo/img/home/sporty.svg'); +} + +.Home-solid { + background-image: url('~amo/img/home/solid.svg'); +} diff --git a/src/amo/components/LandingPage/index.js b/src/amo/components/LandingPage/index.js index cc3a81cfdac..adc41689eff 100644 --- a/src/amo/components/LandingPage/index.js +++ b/src/amo/components/LandingPage/index.js @@ -1,4 +1,5 @@ import classNames from 'classnames'; +import { oneLine } from 'common-tags'; import React from 'react'; import PropTypes from 'prop-types'; import { compose } from 'redux'; @@ -177,10 +178,11 @@ export class LandingPageBase extends React.Component { [ADDON_TYPE_EXTENSION]: i18n.gettext('Extensions'), }; const contentText = { - [ADDON_TYPE_THEME]: i18n.gettext( - "Change your browser's appearance. Choose from thousands of themes to give Firefox the look you want."), - [ADDON_TYPE_EXTENSION]: i18n.gettext( - 'Install powerful tools that make browsing faster and safer, add-ons make your browser yours.'), + [ADDON_TYPE_THEME]: i18n.gettext(oneLine`Change your browser's + appearance. Choose from thousands of themes to give Firefox the look + you want.`), + [ADDON_TYPE_EXTENSION]: i18n.gettext(oneLine`Install powerful tools that + make browsing faster and safer, add-ons make your browser yours.`), }; return ( diff --git a/src/amo/containers/Home.js b/src/amo/containers/Home.js deleted file mode 100644 index 0dbb05c8175..00000000000 --- a/src/amo/containers/Home.js +++ /dev/null @@ -1,109 +0,0 @@ -import classNames from 'classnames'; -import React from 'react'; -import PropTypes from 'prop-types'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { setViewContext } from 'amo/actions/viewContext'; -import Link from 'amo/components/Link'; -import { VIEW_CONTEXT_HOME } from 'core/constants'; -import translate from 'core/i18n/translate'; - -import 'amo/css/Home.scss'; - -const CategoryLink = ({ children, name, slug, type }) => ( -
  • - - {children} - -
  • -); -CategoryLink.propTypes = { - children: PropTypes.node.isRequired, - name: PropTypes.string.isRequired, - slug: PropTypes.string.isRequired, - type: PropTypes.string.isRequired, -}; - -const ExtensionLink = (props) => ; - -const ThemeLink = (props) => ; - -export class HomeBase extends React.Component { - static propTypes = { - dispatch: PropTypes.func.isRequired, - i18n: PropTypes.object.isRequired, - } - - componentWillMount() { - const { dispatch } = this.props; - - dispatch(setViewContext(VIEW_CONTEXT_HOME)); - } - - render() { - const { i18n } = this.props; - return ( -
    -
    -

    - {i18n.gettext(`Extensions are special features you can add to Firefox. - Themes let you change your browser's appearance.`)} -

    -
    - - {i18n.gettext('Extensions')} - - - {i18n.gettext('Themes')} - -
    -
    - -

    {i18n.gettext('You can change how Firefox works…')}

    -
      - - {i18n.gettext('Block ads')} - - - {i18n.gettext('Screenshot')} - - - {i18n.gettext('Find news')} - - - {i18n.gettext('Shop online')} - - - {i18n.gettext('Be social')} - - - {i18n.gettext('Play games')} - -
    - - {i18n.gettext('Browse all extensions')} - - -

    {i18n.gettext('…or what it looks like')}

    -
      - {i18n.gettext('Wild')} - {i18n.gettext('Abstract')} - {i18n.gettext('Holiday')} - {i18n.gettext('Scenic')} - {i18n.gettext('Sporty')} - {i18n.gettext('Solid')} -
    - - {i18n.gettext('Browse all themes')} - -
    - ); - } -} - -export default compose( - // This allows us to dispatch from our component. - connect(), - translate({ withRef: true }), -)(HomeBase); diff --git a/src/amo/css/Home.scss b/src/amo/css/Home.scss deleted file mode 100644 index 515ff245257..00000000000 --- a/src/amo/css/Home.scss +++ /dev/null @@ -1,176 +0,0 @@ -@import "~core/css/inc/mixins"; -@import "~amo/css/inc/vars"; -@import "~ui/css/vars"; - -.HomePage { - padding: $padding-page; -} - -.HomePage-welcome { - background: $header-base-color; - color: $white; - margin: -10px -10px 0; - padding: 20px; -} - -.HomePage-welcome-text { - text-align: center; - margin: 0 0 20px; -} - -.HomePage-welcome-links { - display: flex; - - .HomePage-extensions-link:link, - .HomePage-themes-link:link { - margin: 0 5px; - max-width: 50%; - } -} - -.HomePage-subheading { - text-align: center; - margin-top: 18px; - margin-bottom: -10px; -} - -.HomePage-category-list { - display: flex; - flex-wrap: wrap; - overflow: auto; - padding: 0; - width: 100%; -} - -.HomePage-category-li { - background-color: $base-color; - background-size: 50% auto; - background-position: 50% 35%; - background-repeat: no-repeat; - border-radius: 6px; - display: block; - flex-grow: 1; - list-style-type: none; - margin: 5px; - overflow: auto; - padding: 0; - position: relative; - text-align: center; - width: 30%; - - a:link { - font-size: 10px; - text-decoration: none; - padding-top: 70%; - display: block; - } - - span { - display: block; - padding: 5px 10px; - text-align: center; - text-decoration: inherit; - } - - a:hover, - a:focus { - text-decoration: underline; - } -} - -.HomePage-category-link, -.HomePage-extensions-link, -.HomePage-themes-link { - &, - &:visited { - color: $link-color; - } -} - -.HomePage-extensions-link:link, -.HomePage-themes-link:link { - align-items: center; - background: $base-color; - border-radius: 6px; - display: flex; - font-size: 10px; - justify-content: center; - margin: 0 auto 20px; - padding: 10px; - text-align: center; - text-decoration: none; - text-transform: uppercase; - width: calc(100% - 40px); - - &:hover, - &:focus { - text-decoration: underline; - } - - &::before { - content: ''; - display: inline-block; - width: 16px; - height: 16px; - margin: 0 5px; - } -} - -.HomePage-extensions-link:link::before { - background: url('../img/icons/addon-outline.svg') no-repeat 50% 50%; - background-size: cover; -} - -.HomePage-themes-link:link::before { - background: url('../img/icons/theme-outline.svg') no-repeat 50% 50%; - background-size: cover; -} - -.HomePage-block-ads { - background-image: url('../img/home/block-ads.svg'); -} - -.HomePage-screenshot { - background-image: url('../img/home/screenshot.svg'); -} - -.HomePage-find-news { - background-image: url('../img/home/find-news.svg'); - background-size: 40% auto; -} - -.HomePage-shop-online { - background-image: url('../img/home/shop-online.svg'); -} - -.HomePage-be-social { - background-image: url('../img/home/be-social.svg'); -} - -.HomePage-play-games { - background-image: url('../img/home/play-games.svg'); -} - -.HomePage-wild { - background-image: url('../img/home/wild.svg'); -} - -.HomePage-abstract { - background-image: url('../img/home/abstract.svg'); -} - -.HomePage-holiday { - background-image: url('../img/home/holiday.svg'); -} - -.HomePage-scenic { - background-image: url('../img/home/scenic.svg'); -} - -.HomePage-sporty { - background-image: url('../img/home/sporty.svg'); -} - -.HomePage-solid { - background-image: url('../img/home/solid.svg'); -} diff --git a/src/amo/routes.js b/src/amo/routes.js index 35d03bb3b23..295f338b145 100644 --- a/src/amo/routes.js +++ b/src/amo/routes.js @@ -16,7 +16,7 @@ import Categories from './components/Categories'; import Category from './components/Category'; import FeaturedAddons from './components/FeaturedAddons'; import LandingPage from './components/LandingPage'; -import Home from './containers/Home'; +import Home from './components/Home'; import Addon from './components/Addon'; import NotAuthorized from './components/ErrorPage/NotAuthorized'; import NotFound from './components/ErrorPage/NotFound'; diff --git a/tests/unit/amo/containers/TestHome.js b/tests/unit/amo/containers/TestHome.js index 8b57b01ba9a..6c45eb92361 100644 --- a/tests/unit/amo/containers/TestHome.js +++ b/tests/unit/amo/containers/TestHome.js @@ -1,31 +1,104 @@ +import { shallow } from 'enzyme'; import React from 'react'; -import { findDOMNode } from 'react-dom'; -import { - findRenderedComponentWithType, - renderIntoDocument, -} from 'react-addons-test-utils'; -import { Provider } from 'react-redux'; -import { HomeBase } from 'amo/containers/Home'; +import { + CategoryLink, + ExtensionLink, + HomeBase, + ThemeLink, + mapStateToProps, +} from 'amo/components/Home'; +import Link from 'amo/components/Link'; +import { CLIENT_APP_ANDROID, CLIENT_APP_FIREFOX } from 'core/constants'; import { dispatchSignInActions } from 'tests/unit/amo/helpers'; import { getFakeI18nInst } from 'tests/unit/helpers'; describe('Home', () => { - it('renders a heading', () => { - const { store } = dispatchSignInActions(); - - const root = findRenderedComponentWithType(renderIntoDocument( - - - - ), HomeBase); - const rootNode = findDOMNode(root); - const content = [ - 'You can change how Firefox works…', - '…or what it looks like', - ]; - Array.from(rootNode.querySelectorAll('.HomePage-subheading')) - .map((el, index) => expect(el.textContent).toEqual(content[index])); + function render(props) { + const fakeDispatch = sinon.stub(); + + return shallow( + + ); + } + + it('renders headings', () => { + const root = render(); + + expect( + root.find('.Home-category-card--extensions .Home-subheading') + ).toIncludeText('You can change how Firefox works…'); + expect( + root.find('.Home-category-card--themes .Home-subheading') + ).toIncludeText('…or what it looks like'); + }); + + it('renders add-on type descriptions', () => { + const root = render(); + + expect( + root.find('.Home-category-card--extensions .Home-description') + ).toIncludeText('Install powerful tools that make browsing faster'); + expect( + root.find('.Home-category-card--themes .Home-description') + ).toIncludeText("Change your browser's appearance."); + }); + + it('renders Firefox URLs for categories', () => { + const root = render({ clientApp: CLIENT_APP_FIREFOX }); + const links = shallow(root.instance().extensionsCategoriesForClientApp()); + + expect(links.find(ExtensionLink).find('[name="block-ads"]')) + .toHaveProp('slug', 'privacy-security'); + }); + + it('renders Android URLs for categories', () => { + const root = render({ clientApp: CLIENT_APP_ANDROID }); + const links = shallow(root.instance().extensionsCategoriesForClientApp()); + + expect(links.find(ExtensionLink).find('[name="block-ads"]')) + .toHaveProp('slug', 'security-privacy'); + }); + + it('renders an ExtensionLink', () => { + const root = shallow( + Hello + ); + + expect(root.find(CategoryLink)).toHaveProp('children', 'Hello'); + expect(root.find(CategoryLink)).toHaveProp('name', 'scenic'); + expect(root.find(CategoryLink)).toHaveProp('slug', 'test'); + expect(root.find(CategoryLink)).toHaveProp('type', 'extensions'); + }); + + it('renders a ThemeLink', () => { + const root = shallow( + Hello + ); + + expect(root.find(CategoryLink)).toHaveProp('children', 'Hello'); + expect(root.find(CategoryLink)).toHaveProp('name', 'scenic'); + expect(root.find(CategoryLink)).toHaveProp('slug', 'test'); + expect(root.find(CategoryLink)).toHaveProp('type', 'themes'); + }); + + it('renders a CategoryLink', () => { + const root = shallow( + + ); + + expect(root.find(Link)).toHaveProp('to', '/themes/test/'); + }); + + it('maps clientApp to props from state', () => { + const { state } = dispatchSignInActions({ clientApp: CLIENT_APP_ANDROID }); + + expect(mapStateToProps(state).clientApp).toEqual(CLIENT_APP_ANDROID); }); });