Skip to content

Commit

Permalink
Implement categories (fixes #1183)
Browse files Browse the repository at this point in the history
This lacks some style bits which I want to address later. This commit
has become quite massive.

Implements category component, category page container, and adds
category support to search.
  • Loading branch information
tofumatt committed Nov 8, 2016
1 parent 0e1912f commit a76ff28
Show file tree
Hide file tree
Showing 40 changed files with 1,223 additions and 163 deletions.
Binary file removed .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
},
"parser": "babel-eslint",
"rules": {
"arrow-body-style": ["off", "always"],
"arrow-parens": ["error", "always"],
// Beware about turning this rule back on. The rule encourages you to create
// static class methods on React components when they don't use `this`.
Expand Down
Binary file removed assets/.DS_Store
Binary file not shown.
71 changes: 71 additions & 0 deletions src/amo/components/Categories.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';

import Link from 'amo/components/Link';
import translate from 'core/i18n/translate';

import './Categories.scss';


export function filterAndSortCategories(categories, addonType, clientApp) {
return categories.filter((category) => {
return category.type === addonType && category.application === clientApp;
}).sort((a, b) => {
return a.name > b.name;
});
}

export class CategoriesBase extends React.Component {
static propTypes = {
addonType: PropTypes.string.isRequired,
categories: PropTypes.arrayOf(PropTypes.object),
clientApp: PropTypes.string.isRequired,
error: PropTypes.bool,
loading: PropTypes.bool.isRequired,
i18n: PropTypes.object.isRequired,
}

render() {
const {
addonType, categories, clientApp, error, loading, i18n,
} = this.props;

if (loading && !categories.length) {
return <div>{i18n.gettext('Loading...')}</div>;
}

if (error) {
return <div>{i18n.gettext('Failed to load categories.')}</div>;
}

const categoriesToShow = filterAndSortCategories(
categories, addonType, clientApp);

if (!loading && !categoriesToShow.length) {
return <div>{i18n.gettext('No categories found.')}</div>;
}

return (
<div className="Categories">
<ul className="Categories-list"
ref={(ref) => { this.categories = ref; }}>
{categoriesToShow.map((category) => {
const queryParams = { category: category.slug, type: addonType };
return (
<li className="Categories-list-item">
<Link className="Categories-link"
to={{ pathname: '/search/', query: queryParams }}>
{category.name}
</Link>
</li>
);
})}
</ul>
</div>
);
}
}

export default compose(
translate({ withRef: true }),
)(CategoriesBase);
21 changes: 21 additions & 0 deletions src/amo/components/Categories.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.Categories-list {
margin: 0;
padding: 0;
}

.Categories-list-item {
background: url('../img/icons/arrow.svg') 100% 50% no-repeat;
border-bottom: 2px solid #000;
display: block;
list-style: none;
margin: 0 auto;
padding: 0;
width: 95%;
}

.Categories-link {
color: #000;
display: block;
padding: 1.1em 0 0.9em;
text-decoration: none;
}
64 changes: 64 additions & 0 deletions src/amo/components/CategoryInfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import classNames from 'classnames';
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-connect';
import { compose } from 'redux';

import translate from 'core/i18n/translate';
import { loadCategoriesIfNeeded } from 'core/utils';

import './CategoryInfo.scss';


export class CategoryInfoBase extends React.Component {
static propTypes = {
addonType: PropTypes.string.isRequired,
categories: PropTypes.arrayOf(PropTypes.object),
clientApp: PropTypes.string.isRequired,
slug: PropTypes.string.isRequired,
}

render() {
const { addonType, categories, clientApp, slug } = this.props;

if (!categories) {
return null;
}

const categoryMatch = categories.filter((category) => {
return (category.application === clientApp && category.slug === slug &&
category.type === addonType);
});
const category = categoryMatch.length ? categoryMatch[0] : false;

if (!category) {
return null;
}

return (
<div className={classNames('CategoryInfo', category.slug)}>
<h2 className="CategoryInfo-header"
ref={(ref) => { this.header = ref; }}>
{category.name}
</h2>
</div>
);
}
}

export function mapStateToProps(state) {
return {
clientApp: state.api.clientApp,
...state.categories,
};
}

export default compose(
asyncConnect([{
deferred: true,
key: 'CategoryInfo',
promise: loadCategoriesIfNeeded,
}]),
connect(mapStateToProps),
translate({ withRef: true }),
)(CategoryInfoBase);
27 changes: 27 additions & 0 deletions src/amo/components/CategoryInfo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
@import "~core/css/inc/vars";

$padding-icon: 20px;

.CategoryInfo {
background: $padding-icon 50% no-repeat;
margin: 0 auto;
min-height: 72px;
padding: 0 0 0 ($padding-icon * 6);

&.alerts-updates {
background-image: url('../img/categories/alerts-updates.svg');
}
}

.CategoryInfo-header {
font-size: $font-size-default;
margin: $padding-icon 0 0;
padding: 0;
}

.CategoryInfo-description {
line-height: 1.2;
font-size: $font-size-s;
margin: 0;
max-width: 275px;
}
38 changes: 23 additions & 15 deletions src/amo/components/SearchPage.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,51 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';

import Link from 'amo/components/Link';
import Paginate from 'core/components/Paginate';
import SearchResults from 'core/components/Search/SearchResults';

import CategoryInfo from './CategoryInfo';
import SearchResult from './SearchResult';


export class SearchPageBase extends React.Component {
export default class SearchPage extends React.Component {
static propTypes = {
CategoryInfoComponent: PropTypes.node.isRequired,
LinkComponent: PropTypes.node.isRequired,
ResultComponent: PropTypes.node.isRequired,
addonType: PropTypes.string.isRequired,
category: PropTypes.string,
count: PropTypes.number,
hasSearchParams: PropTypes.bool.isRequired,
loading: PropTypes.bool.isRequired,
page: PropTypes.number,
results: PropTypes.array,
query: PropTypes.string,
}

static defaultProps = {
CategoryInfoComponent: CategoryInfo,
LinkComponent: Link,
ResultComponent: SearchResult,
}

render() {
const { count, loading, page, query, results } = this.props;
const { CategoryInfoComponent, LinkComponent, ResultComponent,
addonType, category, count, hasSearchParams, loading, page, query,
results,
} = this.props;
const paginator = query && count > 0 ?
<Paginate LinkComponent={Link} count={count} currentPage={page}
<Paginate LinkComponent={LinkComponent} count={count} currentPage={page}
pathname="/search/" query={{ q: query }} showPages={0} /> : [];

return (
<div className="search-page">
<SearchResults results={results} query={query} loading={loading}
count={count} ResultComponent={SearchResult} />
<SearchResults CategoryInfoComponent={CategoryInfoComponent}
ResultComponent={ResultComponent} addonType={addonType}
category={category} count={count} hasSearchParams={hasSearchParams}
loading={loading} query={query} results={results} />
{paginator}
</div>
);
}
}

export function mapStateToProps(state) {
return { clientApp: state.api.clientApp, lang: state.api.lang };
}

export default compose(
connect(mapStateToProps),
)(SearchPageBase);
1 change: 1 addition & 0 deletions src/amo/constants.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
// Action types.
export const SET_REVIEW = 'SET_REVIEW';
export const SET_USER_RATING = 'SET_USER_RATING';
24 changes: 24 additions & 0 deletions src/amo/containers/CategoriesPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { compose } from 'redux';
import { asyncConnect } from 'redux-connect';
import { connect } from 'react-redux';

import Categories from 'amo/components/Categories';
import { loadCategoriesIfNeeded } from 'core/utils';


export function mapStateToProps(state, ownProps) {
return {
addonType: ownProps.params.addonType.replace(/s$/, ''),
clientApp: state.api.clientApp,
...state.categories,
};
}

export default compose(
asyncConnect([{
deferred: true,
key: 'CategoriesPage',
promise: loadCategoriesIfNeeded,
}]),
connect(mapStateToProps),
)(Categories);
5 changes: 3 additions & 2 deletions src/amo/containers/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import translate from 'core/i18n/translate';

import 'amo/css/Home.scss';


export class HomePageBase extends React.Component {
static propTypes = {
i18n: PropTypes.object.isRequired,
Expand All @@ -24,7 +25,7 @@ export class HomePageBase extends React.Component {
<li className="HomePage-be-social"><Link to="#share-stuff"><span>{i18n.gettext('Be social')}</span></Link></li>
<li className="HomePage-share-stuff"><Link to="#share-stuff"><span>{i18n.gettext('Share stuff')}</span></Link></li>
</ul>
<Link className="HomePage-extensions-link" to="#extensions">
<Link className="HomePage-extensions-link" to="/search/?type=extension">
{i18n.gettext('Browse all extensions')}
</Link>

Expand All @@ -37,7 +38,7 @@ export class HomePageBase extends React.Component {
<li className="HomePage-sporty"><Link to="#sporty"><span>{i18n.gettext('Sporty')}</span></Link></li>
<li className="HomePage-mystical"><Link to="#mystical"><span>{i18n.gettext('Mystical')}</span></Link></li>
</ul>
<Link className="HomePage-themes-link" to="#themes">
<Link className="HomePage-themes-link" to="/search/?type=theme">
{i18n.gettext('Browse all themes')}
</Link>
</div>
Expand Down
24 changes: 22 additions & 2 deletions src/amo/containers/SearchPage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-connect';
import { compose } from 'redux';

import SearchPage from 'amo/components/SearchPage';
import createSearchPage from 'core/containers/SearchPage';
import {
loadSearchResultsIfNeeded,
mapStateToProps,
} from 'core/containers/SearchPage';
import { loadCategoriesIfNeeded } from 'core/utils';


export default createSearchPage(SearchPage);
export default compose(
asyncConnect([
{
deferred: true,
promise: loadSearchResultsIfNeeded,
},
{
deferred: true,
promise: loadCategoriesIfNeeded,
},
]),
connect(mapStateToProps)
)(SearchPage);
13 changes: 13 additions & 0 deletions src/amo/img/categories/alerts-updates.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/amo/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import HandleLogin from 'core/containers/HandleLogin';

import AddonReview from './components/AddonReview';
import App from './containers/App';
import CategoriesPage from './containers/CategoriesPage';
import Home from './containers/Home';
import DetailPage from './containers/DetailPage';
import SearchPage from './containers/SearchPage';
Expand All @@ -14,6 +15,7 @@ export default (
<IndexRoute component={Home} />
<Route path="addon/:slug/" component={DetailPage} />
<Route path="addon/:slug/review/:reviewId/" component={AddonReview} />
<Route path="categories/:addonType/" component={CategoriesPage} />
<Route path="fxa-authenticate" component={HandleLogin} />
<Route path="search/" component={SearchPage} />
</Route>
Expand Down
5 changes: 4 additions & 1 deletion src/amo/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ import { middleware } from 'core/store';
import addons from 'core/reducers/addons';
import api from 'core/reducers/api';
import auth from 'core/reducers/authentication';
import categories from 'core/reducers/categories';
import reviews from 'amo/reducers/reviews';
import search from 'core/reducers/search';

export default function createStore(initialState = {}) {
return _createStore(
combineReducers({ addons, api, auth, search, reviews, reduxAsyncConnect }),
combineReducers({
addons, api, auth, categories, search, reviews, reduxAsyncConnect,
}),
initialState,
middleware(),
);
Expand Down

0 comments on commit a76ff28

Please sign in to comment.