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);
});
});