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 2, 2016
1 parent fc9f807 commit b86c20c
Show file tree
Hide file tree
Showing 49 changed files with 1,281 additions and 131 deletions.
Binary file removed .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .eslintrc
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 modified assets/.DS_Store
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Black.otf
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Bold.otf
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Book.otf
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Light.otf
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Medium.otf
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Regular.otf
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Thin.otf
Binary file not shown.
Binary file added assets/fonts/Tofino/Tofino Ultra.otf
Binary file not shown.
84 changes: 84 additions & 0 deletions src/amo/components/Categories.js
@@ -0,0 +1,84 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { asyncConnect } from 'redux-connect';
import { compose } from 'redux';

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

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-listItem">
<Link className="CategoryLink-link"
to={{ pathname: '/search/', query: queryParams }}>
{category.name}
</Link>
</li>
);
})}
</ul>
</div>
);
}
}

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

export default compose(
asyncConnect([{
deferred: true,
key: 'Categories',
promise: loadCategoriesIfNeeded,
}]),
connect(mapStateToProps),
translate({ withRef: true }),
)(CategoriesBase);
21 changes: 21 additions & 0 deletions src/amo/components/Categories.scss
@@ -0,0 +1,21 @@
.Categories-list {
margin: 0;
padding: 0;
}

.Categories-listItem {
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%;

a {
color: #000;
display: block;
padding: 1.1em 0 0.9em;
text-decoration: none;
}
}
64 changes: 64 additions & 0 deletions src/amo/components/CategoryInfo.js
@@ -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
@@ -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;
}
39 changes: 39 additions & 0 deletions src/amo/components/CategoryLink.js
@@ -0,0 +1,39 @@
// import React, { PropTypes } from 'react';
// import { connect } from 'react-redux';
// import { Link } from 'react-router'
// import { compose } from 'redux';
//
//
// export class CategoryLinkBase extends React.Component {
// static propTypes = {
// addonType: PropTypes.string,
// clientApp: PropTypes.string.isRequired,
// lang: PropTypes.string.isRequired,
// name: PropTypes.string.isRequired,
// slug: PropTypes.string.isRequired,
// }
//
// static defaultProps = {
// addonType: 'extension',
// }
//
// render() {
// const { addonType, clientApp, lang, name, slug } = this.props;
//
// return (
// <Link className="CategoryLink-link" ref={(ref) => { this.link = ref; }}
// to={`/${lang}/${clientApp}/search/?category=${slug}&type=${addonType}`}>
// {name}
// </Link>
// );
// }
// }
//
// export const mapStateToProps = (state) => ({
// clientApp: state.api.clientApp,
// lang: state.api.lang,
// });
//
// export default compose(
// connect(mapStateToProps),
// )(CategoryLinkBase);
35 changes: 20 additions & 15 deletions src/amo/components/SearchPage.js
@@ -1,43 +1,48 @@
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,
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, 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} 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
@@ -1,2 +1,3 @@
// Action types.
export const SET_REVIEW = 'SET_REVIEW';
export const SET_USER_RATING = 'SET_USER_RATING';
44 changes: 44 additions & 0 deletions src/amo/containers/CategoriesPage.js
@@ -0,0 +1,44 @@
import React, { PropTypes } from 'react';
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 class CategoriesPageBase extends React.Component {
static propTypes = {
addonType: PropTypes.string.isRequired,
CategoriesComponent: PropTypes.node.isRequired,
}

static defaultProps = {
CategoriesComponent: Categories,
}

render() {
const { CategoriesComponent, addonType } = this.props;
return (
<div className="Categories-Page" ref={(ref) => { this.container = ref; }}>
<CategoriesComponent addonType={addonType} />
</div>
);
}
}

function mapStateToProps(state, ownProps) {
const { addonType } = ownProps.params;
return {
addonType,
};
}

export default compose(
asyncConnect([{
deferred: true,
key: 'CategoriesPage',
promise: loadCategoriesIfNeeded,
}]),
connect(mapStateToProps),
)(CategoriesPageBase);
3 changes: 2 additions & 1 deletion src/amo/containers/Home.js
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="/categories/extension/">
{i18n.gettext('Browse all extensions')}
</Link>

Expand Down

0 comments on commit b86c20c

Please sign in to comment.