From 22078478c9968cf4370cf1a810752ca57caca1fb Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson Date: Fri, 23 Jun 2017 16:44:28 +0100 Subject: [PATCH 1/2] chore: Add awesome illustrations --- src/ui/components/Icon/artistic-unicorn.svg | 192 ++++++++++++++++++ .../components/Icon/multitasking-octopus.svg | 124 +++++++++++ src/ui/components/Icon/styles.scss | 14 ++ 3 files changed, 330 insertions(+) create mode 100644 src/ui/components/Icon/artistic-unicorn.svg create mode 100644 src/ui/components/Icon/multitasking-octopus.svg diff --git a/src/ui/components/Icon/artistic-unicorn.svg b/src/ui/components/Icon/artistic-unicorn.svg new file mode 100644 index 00000000000..64e7a1d91be --- /dev/null +++ b/src/ui/components/Icon/artistic-unicorn.svg @@ -0,0 +1,192 @@ + + + + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/components/Icon/multitasking-octopus.svg b/src/ui/components/Icon/multitasking-octopus.svg new file mode 100644 index 00000000000..07698b6f3eb --- /dev/null +++ b/src/ui/components/Icon/multitasking-octopus.svg @@ -0,0 +1,124 @@ + + + + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ui/components/Icon/styles.scss b/src/ui/components/Icon/styles.scss index b9bd2729605..c4b916321c4 100644 --- a/src/ui/components/Icon/styles.scss +++ b/src/ui/components/Icon/styles.scss @@ -97,3 +97,17 @@ min-width: 46.3px; width: 46.3px; } + +.Icon-artistic-unicorn { + background: url('./artistic-unicorn.svg') center no-repeat; + background-size: contain; + height: 198.6px; + width: 394px; +} + +.Icon-multitasking-octopus { + background: url('./multitasking-octopus.svg') center no-repeat; + background-size: contain; + height: 199.3px; + width: 366px; +} From 28f3fe80ba1bd9291ba03e6050cf23318833d184 Mon Sep 17 00:00:00 2001 From: Matthew Riley MacPherson Date: Fri, 23 Jun 2017 17:07:12 +0100 Subject: [PATCH 2/2] feat: Add Langing Pages for desktop Fixes #2561 Fixes #2562 --- src/amo/components/CategoriesHeader/index.js | 116 ++++++++++++++++++ .../components/CategoriesHeader/styles.scss | 38 ++++++ src/amo/components/LandingPage.scss | 58 --------- .../{LandingPage.js => LandingPage/index.js} | 67 ++++++---- src/amo/components/LandingPage/styles.scss | 61 +++++++++ src/ui/components/Button/Button.scss | 2 + .../amo/components/TestCategoriesHeader.js | 100 +++++++++++++++ tests/unit/amo/components/TestLandingPage.js | 29 ++++- 8 files changed, 382 insertions(+), 89 deletions(-) create mode 100644 src/amo/components/CategoriesHeader/index.js create mode 100644 src/amo/components/CategoriesHeader/styles.scss delete mode 100644 src/amo/components/LandingPage.scss rename src/amo/components/{LandingPage.js => LandingPage/index.js} (82%) create mode 100644 src/amo/components/LandingPage/styles.scss create mode 100644 tests/unit/amo/components/TestCategoriesHeader.js diff --git a/src/amo/components/CategoriesHeader/index.js b/src/amo/components/CategoriesHeader/index.js new file mode 100644 index 00000000000..593ef4fc0dc --- /dev/null +++ b/src/amo/components/CategoriesHeader/index.js @@ -0,0 +1,116 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { categoriesFetch } from 'core/actions/categories'; +import translate from 'core/i18n/translate'; +import { getCategoryColor, visibleAddonType } from 'core/utils'; +import Button from 'ui/components/Button'; +import Card from 'ui/components/Card'; +import type { DispatchFunc } from 'core/types/redux'; +import LoadingText from 'ui/components/LoadingText'; + +import './styles.scss'; + + +type CategoriesHeaderProps = { + addonType: string, + dispatch: DispatchFunc, + categories: Object, + clientApp: string, + error: boolean | null, + loading: boolean, + i18n: Object, +} + +export class CategoriesHeaderBase extends React.Component { + componentWillMount() { + const { addonType, clientApp, dispatch } = this.props; + const categories = this.props.categories[addonType] || {}; + + if (!Object.values(categories).length) { + dispatch(categoriesFetch({ addonType, clientApp })); + } + } + + props: CategoriesHeaderProps; + + render() { + const { addonType, error, loading, i18n } = this.props; + const categories = this.props.categories[addonType] ? + Object.values(this.props.categories[addonType]) : []; + + if (error) { + return ( + +

{i18n.gettext('Failed to load categories.')}

+
+ ); + } + + if (!loading && !categories.length) { + return ( + +

{i18n.gettext('No categories found.')}

+
+ ); + } + + return ( + + {loading ? +
+ + {i18n.gettext('Loading categories.')} + + {Array(8).fill(0).map((value, index) => { + return ( + + ); + })} +
+ : +
    + {categories.map((category, index) => ( +
  • + +
  • + ))} +
+ } +
+ ); + } +} + +export function mapStateToProps(state) { + const clientApp = state.api.clientApp; + const categories = state.categories.categories[clientApp]; + + return { + categories, + clientApp, + error: state.categories.error, + loading: state.categories.loading, + }; +} + +export default compose( + connect(mapStateToProps), + translate({ withRef: true }), +)(CategoriesHeaderBase); diff --git a/src/amo/components/CategoriesHeader/styles.scss b/src/amo/components/CategoriesHeader/styles.scss new file mode 100644 index 00000000000..a46ee6d1e4a --- /dev/null +++ b/src/amo/components/CategoriesHeader/styles.scss @@ -0,0 +1,38 @@ +@import "~amo/css/inc/vars"; +@import "~core/css/inc/mixins"; +@import "~ui/css/vars"; + +.CategoriesHeader .Card-contents { + display: flex; +} + +.CategoriesHeader-loadingText { + @include margin-end(5px); +} + +.CategoriesHeader-list { + display: block; + list-style: none; + margin: 0 auto; + padding: 0; +} + +.CategoriesHeader-item { + @include margin-end(5px); + + display: inline-block; + margin: 0 0 5px; + padding: 0; +} + +@for $i from 1 through 12 { + .CategoriesHeader--category-color-#{$i} { + background: desaturate(nth($category-colors, $i), 20); + } +} + +@for $i from 1 through 10 { + .CategoriesHeader--type-extension.CategoriesHeader--category-color-#{$i} { + background: desaturate(nth($category-colors-extensions, $i), 20); + } +} diff --git a/src/amo/components/LandingPage.scss b/src/amo/components/LandingPage.scss deleted file mode 100644 index 3e5b755e6dc..00000000000 --- a/src/amo/components/LandingPage.scss +++ /dev/null @@ -1,58 +0,0 @@ -@import "~amo/css/inc/vars"; -@import "~core/css/inc/mixins"; - -.LandingPage { - padding: $padding-page; -} - -.LandingPage-header { - align-items: center; - display: flex; - flex-direction: column; - margin: $padding-page; -} - -.LandingPage-header-top { - align-items: center; - display: flex; - justify-content: center; - margin-bottom: $padding-page * 2; -} - -.LandingPage-header-text { - @include margin-start(20px); - - display: inline-block; - max-width: 350px; -} - -.LandingPage-heading { - font-size: $font-size-default; - margin: 0; -} - -.LandingPage-heading-content { - margin: 5px 0 0; - font-size: $font-size-s; -} - -.LandingPage-header-bottom { - display: flex; - width: 100%; -} - -.LandingPage-browse-button, -.LandingPage-browse-button:link { - align-items: center; - display: flex; - font-size: $font-size-default; - font-variant: all-small-caps; - justify-content: center; - margin: 0 $padding-page; - padding: 10px; - width: 100%; -} - -.LandingPage-browse-icon { - @include margin-end(10px); -} diff --git a/src/amo/components/LandingPage.js b/src/amo/components/LandingPage/index.js similarity index 82% rename from src/amo/components/LandingPage.js rename to src/amo/components/LandingPage/index.js index cb71a4d5e85..cc3a81cfdac 100644 --- a/src/amo/components/LandingPage.js +++ b/src/amo/components/LandingPage/index.js @@ -7,6 +7,8 @@ import { connect } from 'react-redux'; import { setViewContext } from 'amo/actions/viewContext'; import LandingAddonsCard from 'amo/components/LandingAddonsCard'; import NotFound from 'amo/components/ErrorPage/NotFound'; +import CategoriesHeader + from 'amo/components/CategoriesHeader'; import { loadLandingAddons } from 'amo/utils'; import { ADDON_TYPE_EXTENSION, @@ -22,12 +24,17 @@ import { visibleAddonType as getVisibleAddonType, } from 'core/utils'; import translate from 'core/i18n/translate'; -import Button from 'ui/components/Button/index'; +import Button from 'ui/components/Button'; import Icon from 'ui/components/Icon/index'; -import './LandingPage.scss'; +import './styles.scss'; +const ICON_MAP = { + [ADDON_TYPE_EXTENSION]: 'multitasking-octopus', + [ADDON_TYPE_THEME]: 'artistic-unicorn', +}; + export class LandingPageBase extends React.Component { static propTypes = { apiAddonType: PropTypes.func.isRequired, @@ -126,6 +133,18 @@ export class LandingPageBase extends React.Component { return { addonType, html: contentForTypes[addonType] }; } + icon(addonType) { + return ( + + ); + } + render() { const { featuredAddons, @@ -159,37 +178,35 @@ export class LandingPageBase extends React.Component { }; const contentText = { [ADDON_TYPE_THEME]: i18n.gettext( - "Change your browser's appearance. Choose from thousands of themes to give Firefox the look you want."), + "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.'), + 'Install powerful tools that make browsing faster and safer, add-ons make your browser yours.'), }; return ( -
+
-
- -
-

- {headingText[addonType]} -

-

- {contentText[addonType]} -

-
-
- -
- + {this.icon(addonType)} + +
+

+ {headingText[addonType]} +

+

+ {contentText[addonType]} +

+ + + + ', () => { + function render({ ...props }) { + const fakeDispatch = sinon.stub(); + + return shallow( + + ); + } + + it('it renders a CategoriesHeader', () => { + const root = render({ addonType: ADDON_TYPE_EXTENSION, categories: {} }); + + expect(root).toHaveClassName('CategoriesHeader'); + }); + + it('it renders loading text when loading', () => { + const root = render({ + addonType: ADDON_TYPE_EXTENSION, + categories: {}, + loading: true, + }); + + expect(root.find('.CategoriesHeader-loading-info')) + .toIncludeText('Loading categories.'); + }); + + it('it renders LoadingText components when loading', () => { + const root = render({ + addonType: ADDON_TYPE_EXTENSION, + categories: {}, + loading: true, + }); + + expect(root.find('.CategoriesHeader-loading-text').find(LoadingText)) + .toHaveLength(8); + }); + + it('it renders an error message if there was an error', () => { + const root = render({ + addonType: ADDON_TYPE_EXTENSION, + categories: {}, + error: true, + }); + + expect(root.find('.CategoriesHeader p')) + .toIncludeText('Failed to load categories'); + }); + + it('it renders categories if they exist', () => { + const categoriesResponse = { + result: [ + { + application: 'android', + name: 'Games', + slug: 'Games', + type: ADDON_TYPE_EXTENSION, + }, + { + application: 'android', + name: 'Travel', + slug: 'travel', + type: ADDON_TYPE_EXTENSION, + }, + ], + }; + + const { store } = dispatchClientMetadata(); + store.dispatch(categoriesLoad(categoriesResponse)); + const { categories } = mapStateToProps(store.getState()); + + const root = render({ + addonType: ADDON_TYPE_EXTENSION, + categories, + }); + + expect(root.find('.CategoriesHeader-list').childAt(0).find(Button)) + .toHaveProp('children', 'Games'); + expect(root.find('.CategoriesHeader-list').childAt(1).find(Button)) + .toHaveProp('children', 'Travel'); + }); +}); diff --git a/tests/unit/amo/components/TestLandingPage.js b/tests/unit/amo/components/TestLandingPage.js index 865bacbf244..9dc9b33a423 100644 --- a/tests/unit/amo/components/TestLandingPage.js +++ b/tests/unit/amo/components/TestLandingPage.js @@ -68,6 +68,23 @@ describe('', () => { expect(rootNode.textContent).toContain('More featured extensions'); }); + it('renders a link to all categories', () => { + const fakeDispatch = sinon.stub(); + const fakeParams = { + visibleAddonType: visibleAddonType(ADDON_TYPE_EXTENSION), + }; + const root = shallow( + + ); + + expect(root.find('.LandingPage-button')) + .toHaveProp('children', 'Explore all categories'); + }); + it('sets the links in each footer for extensions', () => { const fakeDispatch = sinon.stub(); const fakeParams = { @@ -81,14 +98,14 @@ describe('', () => { /> ); - expect(root.childAt(1).prop('footerLink')).toEqual({ + expect(root.childAt(3)).toHaveProp('footerLink', { pathname: `/${visibleAddonType(ADDON_TYPE_EXTENSION)}/featured/`, }); - expect(root.childAt(2).prop('footerLink')).toEqual({ + expect(root.childAt(4)).toHaveProp('footerLink', { pathname: '/search/', query: { addonType: ADDON_TYPE_EXTENSION, sort: SEARCH_SORT_TOP_RATED }, }); - expect(root.childAt(3).prop('footerLink')).toEqual({ + expect(root.childAt(5)).toHaveProp('footerLink', { pathname: '/search/', query: { addonType: ADDON_TYPE_EXTENSION, sort: SEARCH_SORT_POPULAR }, }); @@ -107,14 +124,14 @@ describe('', () => { /> ); - expect(root.childAt(1).prop('footerLink')).toEqual({ + expect(root.childAt(3)).toHaveProp('footerLink', { pathname: `/${visibleAddonType(ADDON_TYPE_THEME)}/featured/`, }); - expect(root.childAt(2).prop('footerLink')).toEqual({ + expect(root.childAt(4)).toHaveProp('footerLink', { pathname: '/search/', query: { addonType: ADDON_TYPE_THEME, sort: SEARCH_SORT_TOP_RATED }, }); - expect(root.childAt(3).prop('footerLink')).toEqual({ + expect(root.childAt(5)).toHaveProp('footerLink', { pathname: '/search/', query: { addonType: ADDON_TYPE_THEME, sort: SEARCH_SORT_POPULAR }, });