summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcin Gębala <elwoodxblues@users.noreply.github.com>2017-10-06 16:13:09 +0200
committerGitHub <noreply@github.com>2017-10-06 16:13:09 +0200
commitd65b80f70e97c108cbc5cf8359266d5e0cee4766 (patch)
treee46c7f0c93bcfbb118fc609d6fc6c02851da1259
parent9e00c6572bc3e6df855f431ef4b3db2d59f11e0a (diff)
parentff205096360362eb48cad5a45ef67467e0569885 (diff)
downloadsaleor-frontend-d65b80f70e97c108cbc5cf8359266d5e0cee4766.tar.gz
saleor-frontend-d65b80f70e97c108cbc5cf8359266d5e0cee4766.tar.bz2
saleor-frontend-d65b80f70e97c108cbc5cf8359266d5e0cee4766.zip
Merge pull request #1135 from mirumee/static_category_view
Static category view
-rw-r--r--package.json7
-rw-r--r--requirements.in1
-rw-r--r--requirements.txt1
-rw-r--r--saleor/core/permissions.py1
-rw-r--r--saleor/core/templatetags/shop.py14
-rw-r--r--saleor/product/filters.py81
-rw-r--r--saleor/product/views.py26
-rw-r--r--saleor/settings.py1
-rw-r--r--saleor/static/js/category.js58
-rw-r--r--saleor/static/js/components/Loading.js16
-rw-r--r--saleor/static/js/components/categoryPage/AttributeInput.js30
-rw-r--r--saleor/static/js/components/categoryPage/CategoryFilter.js36
-rw-r--r--saleor/static/js/components/categoryPage/CategoryPage.js225
-rw-r--r--saleor/static/js/components/categoryPage/FilterHeader.js27
-rw-r--r--saleor/static/js/components/categoryPage/NoResults.js18
-rw-r--r--saleor/static/js/components/categoryPage/PriceFilter.js78
-rw-r--r--saleor/static/js/components/categoryPage/ProductFilters.js102
-rw-r--r--saleor/static/js/components/categoryPage/ProductItem.js72
-rw-r--r--saleor/static/js/components/categoryPage/ProductList.js53
-rw-r--r--saleor/static/js/components/categoryPage/ProductPrice.js40
-rw-r--r--saleor/static/js/components/categoryPage/SortBy.js115
-rw-r--r--saleor/static/js/components/categoryPage/utils.js35
-rw-r--r--saleor/static/js/components/utils.js12
-rw-r--r--saleor/static/js/storefront.js107
-rw-r--r--saleor/static/scss/components/_filters.scss3
-rw-r--r--saleor/static/scss/components/_header.scss2
-rw-r--r--templates/account/login.html2
-rw-r--r--templates/category/_items.html6
-rw-r--r--templates/category/index.html190
-rw-r--r--tests/test_product.py43
-rw-r--r--webpack.config.js4
-rw-r--r--yarn.lock80
32 files changed, 459 insertions, 1027 deletions
diff --git a/package.json b/package.json
index 03532d74..cce8900d 100644
--- a/package.json
+++ b/package.json
@@ -33,8 +33,6 @@
"query-string": "^5.0.0",
"react": "^15.4.2",
"react-dom": "^15.4.2",
- "react-inlinesvg": "^0.6.2",
- "react-relay": "^1.3.0",
"select2": "^4.0.3",
"sortablejs": "^1.5.0-rc1",
"svg-injector-2": "^2.0.35",
@@ -54,8 +52,6 @@
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.22.0",
"babel-preset-stage-0": "^6.22.0",
- "babel-relay-plugin": "^0.11.0",
- "babel-relay-plugin-loader": "^0.11.0",
"css-loader": "^0.28.7",
"eslint": "^4.6.1",
"eslint-config-standard": "^10.2.1",
@@ -81,8 +77,7 @@
"plugins": [
"transform-decorators-legacy",
"transform-class-properties",
- "transform-object-rest-spread",
- "babel-relay-plugin-loader"
+ "transform-object-rest-spread"
]
},
"metadata": {
diff --git a/requirements.in b/requirements.in
index 2e9851c3..45b9a72b 100644
--- a/requirements.in
+++ b/requirements.in
@@ -7,6 +7,7 @@ dj_email_url>=0.0.4
django-bootstrap3>=8.2.1
django-cache-url>=1.0.0
django-countries>=4.1
+django-filter==1.0.4
django-mptt>=0.7.1
django-payments>=0.11.0
django-prices>=0.4.11
diff --git a/requirements.txt b/requirements.txt
index f653c836..b849e94c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,6 +20,7 @@ dj-email-url==0.0.10
django-bootstrap3==9.0.0
django-cache-url==1.3.1
django-countries==4.6.1
+django-filter==1.0.4
django-mptt==0.8.7
django-payments==0.11.0.3
django-prices-openexchangerates==0.1.15
diff --git a/saleor/core/permissions.py b/saleor/core/permissions.py
index 941b79f8..b0ced75a 100644
--- a/saleor/core/permissions.py
+++ b/saleor/core/permissions.py
@@ -1,3 +1,4 @@
+from __future__ import unicode_literals
from django.contrib.auth.models import Permission
diff --git a/saleor/core/templatetags/shop.py b/saleor/core/templatetags/shop.py
index f5318703..598baa6e 100644
--- a/saleor/core/templatetags/shop.py
+++ b/saleor/core/templatetags/shop.py
@@ -1,9 +1,12 @@
+from __future__ import unicode_literals
try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
from django.template import Library
+from django.utils.http import urlencode
+
register = Library()
@@ -13,3 +16,14 @@ def slice(items, group_size=1):
args = [iter(items)] * group_size
return (filter(None, group)
for group in zip_longest(*args, fillvalue=None))
+
+
+@register.simple_tag(takes_context=True)
+def get_sort_by_url(context, field, descending=False):
+ request = context['request']
+ request_get = request.GET.dict()
+ if descending:
+ request_get['sort_by'] = '-' + field
+ else:
+ request_get['sort_by'] = field
+ return '%s?%s' % (request.path, urlencode(request_get))
diff --git a/saleor/product/filters.py b/saleor/product/filters.py
new file mode 100644
index 00000000..0773fca3
--- /dev/null
+++ b/saleor/product/filters.py
@@ -0,0 +1,81 @@
+from __future__ import unicode_literals
+from collections import OrderedDict
+
+from django_filters import (FilterSet, MultipleChoiceFilter, RangeFilter,
+ OrderingFilter)
+from django.forms import CheckboxSelectMultiple
+from django.utils.translation import pgettext_lazy
+
+from django_prices.models import PriceField
+
+from .models import Product, ProductAttribute
+
+
+DEFAULT_SORT = 'name'
+
+
+SORT_BY_FIELDS = [{'value': 'name',
+ 'label': pgettext_lazy('Sort by filter', 'name')},
+ {'value': 'price',
+ 'label': pgettext_lazy('Sort by filter', 'price')}]
+
+
+class ProductFilter(FilterSet):
+ def __init__(self, *args, **kwargs):
+ self.category = kwargs.pop('category')
+ super(ProductFilter, self).__init__(*args, **kwargs)
+ self.product_attributes, self.variant_attributes = (
+ self._get_attributes())
+ self.filters.update(self._get_product_attributes_filters())
+ self.filters.update(self._get_product_variants_attributes_filters())
+ self.filters = OrderedDict(sorted(self.filters.items()))
+
+ sort_by = OrderingFilter(
+ label='Sort by',
+ fields=[(field['value'], field['value']) for field in SORT_BY_FIELDS]
+ )
+
+ class Meta:
+ model = Product
+ fields = ['price']
+ filter_overrides = {
+ PriceField: {
+ 'filter_class': RangeFilter
+ }
+ }
+
+ def _get_attributes(self):
+ product_attributes = (
+ ProductAttribute.objects.all()
+ .prefetch_related('values')
+ .filter(products_class__products__categories=self.category)
+ .distinct())
+ variant_attributes = (
+ ProductAttribute.objects.all()
+ .prefetch_related('values')
+ .filter(product_variants_class__products__categories=self.category)
+ .distinct())
+ return product_attributes, variant_attributes
+
+ def _get_product_attributes_filters(self):
+ filters = {}
+ for attribute in self.product_attributes:
+ filters[attribute.slug] = MultipleChoiceFilter(
+ name='attributes__%s' % attribute.pk,
+ label=attribute.name,
+ widget=CheckboxSelectMultiple,
+ choices=self._get_attribute_choices(attribute))
+ return filters
+
+ def _get_product_variants_attributes_filters(self):
+ filters = {}
+ for attribute in self.variant_attributes:
+ filters[attribute.slug] = MultipleChoiceFilter(
+ name='variants__attributes__%s' % attribute.pk,
+ label=attribute.name,
+ widget=CheckboxSelectMultiple,
+ choices=self._get_attribute_choices(attribute))
+ return filters
+
+ def _get_attribute_choices(self, attribute):
+ return [(choice.pk, choice.name) for choice in attribute.values.all()]
diff --git a/saleor/product/views.py b/saleor/product/views.py
index 7bdc5b39..1a793226 100644
--- a/saleor/product/views.py
+++ b/saleor/product/views.py
@@ -9,12 +9,15 @@ from django.shortcuts import get_object_or_404, redirect
from django.template.response import TemplateResponse
from ..cart.utils import set_cart_cookie
-from ..core.utils import serialize_decimal
+from ..core.utils import get_paginator_items, serialize_decimal
+from ..settings import PAGINATE_BY
+from .filters import DEFAULT_SORT, ProductFilter, SORT_BY_FIELDS
from .models import Category
from .utils import (products_with_details, products_for_cart,
handle_cart_form, get_availability,
get_product_images, get_variant_picker_data,
- get_product_attributes_data, product_json_ld)
+ get_product_attributes_data,
+ product_json_ld, products_with_availability)
def product_details(request, slug, product_id, form=None):
@@ -115,5 +118,20 @@ def category_index(request, path, category_id):
if actual_path != path:
return redirect('product:category', permanent=True, path=actual_path,
category_id=category_id)
- return TemplateResponse(request, 'category/index.html',
- {'category': category})
+ products = (products_with_details(user=request.user)
+ .filter(categories__name=category)
+ .order_by(DEFAULT_SORT))
+ product_filter = ProductFilter(
+ request.GET, queryset=products, category=category)
+ products_paginated = get_paginator_items(
+ product_filter.qs, PAGINATE_BY, request.GET.get('page'))
+ products_and_availability = list(products_with_availability(
+ products_paginated, request.discounts, request.currency))
+ sort_by = request.GET.get('sort_by', DEFAULT_SORT)
+ ctx = {'category': category, 'filter': product_filter,
+ 'products': products_and_availability,
+ 'products_paginated': products_paginated,
+ 'sort_by_choices': SORT_BY_FIELDS,
+ 'sort_by_label': sort_by.strip('-'),
+ 'is_descending': sort_by.startswith('-')}
+ return TemplateResponse(request, 'category/index.html', ctx)
diff --git a/saleor/settings.py b/saleor/settings.py
index db4517ef..a72cf5a3 100644
--- a/saleor/settings.py
+++ b/saleor/settings.py
@@ -179,6 +179,7 @@ INSTALLED_APPS = [
'webpack_loader',
'social_django',
'django_countries',
+ 'django_filters',
]
LOGGING = {
diff --git a/saleor/static/js/category.js b/saleor/static/js/category.js
deleted file mode 100644
index 1d92c909..00000000
--- a/saleor/static/js/category.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { PropTypes } from 'react';
-import ReactDOM from 'react-dom';
-import Relay from 'react-relay/classic';
-
-import CategoryPage from './components/categoryPage/CategoryPage';
-import ProductFilters from './components/categoryPage/ProductFilters';
-import Loading from './components/Loading';
-
-const categoryPage = document.getElementById('category-page');
-const categoryData = JSON.parse(categoryPage.getAttribute('data-category'));
-
-class App extends React.Component {
-
- static propTypes = {
- root: PropTypes.object
- }
-
- render() {
- return <CategoryPage {...this.props.root} />;
- }
-}
-
-const RelayApp = Relay.createContainer(App, {
- initialVariables: {
- categoryId: categoryData.id
- },
- fragments: {
- root: () => Relay.QL`
- fragment on Query {
- category(pk: $categoryId) {
- ${CategoryPage.getFragment('category')}
- }
- attributes(categoryPk: $categoryId) {
- ${ProductFilters.getFragment('attributes')}
- }
- }
- `
- }
-});
-
-const AppRoute = {
- queries: {
- root: () => Relay.QL`
- query { root }
- `
- },
- params: {},
- name: 'Root'
-};
-
-ReactDOM.render(
- <Relay.RootContainer
- Component={RelayApp}
- route={AppRoute}
- renderLoading={() => <Loading />}
- />,
- categoryPage
-);
diff --git a/saleor/static/js/components/Loading.js b/saleor/static/js/components/Loading.js
deleted file mode 100644
index 17a7f4fc..00000000
--- a/saleor/static/js/components/Loading.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react';
-import InlineSVG from 'react-inlinesvg';
-
-import loader from '../../images/loader.svg';
-
-const Loading = () => {
- return (
- <div className="row loader">
- <div className="col-12">
- <InlineSVG src={loader} />
- </div>
- </div>
- );
-};
-
-export default Loading;
diff --git a/saleor/static/js/components/categoryPage/AttributeInput.js b/saleor/static/js/components/categoryPage/AttributeInput.js
deleted file mode 100644
index 367d2bf1..00000000
--- a/saleor/static/js/components/categoryPage/AttributeInput.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, { PropTypes } from 'react';
-
-const AttributeInput = ({attribute, checked, onClick, value}) => {
- const handleChange = (event) => {
- const { name, value } = event.target;
- onClick(name, value);
- };
-
- return (
- <label>
- <input
- checked={checked}
- name={attribute.slug}
- onChange={handleChange}
- type="checkbox"
- value={value.slug}
- />
- {value.name}
- </label>
- );
-};
-
-AttributeInput.propTypes = {
- checked: PropTypes.bool,
- attribute: PropTypes.object.isRequired,
- value: PropTypes.object.isRequired,
- onClick: PropTypes.func.isRequired
-};
-
-export default AttributeInput;
diff --git a/saleor/static/js/components/categoryPage/CategoryFilter.js b/saleor/static/js/components/categoryPage/CategoryFilter.js
deleted file mode 100644
index 825571bc..00000000
--- a/saleor/static/js/components/categoryPage/CategoryFilter.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import InlineSVG from 'react-inlinesvg';
-
-import arrowLeftIcon from '../../../images/arrow_left.svg';
-
-export default class CategoryFilter extends Component {
-
- static propTypes = {
- category: PropTypes.object.isRequired
- }
-
- render() {
- const { category } = this.props;
- const parent = category.ancestors ? category.ancestors[category.ancestors.length - 1] : null;
- return (
- <div className="product-filters__categories">
- <h2><strong>{category.name}</strong></h2>
- {parent && (
- <div className="product-filters__categories__parents">
- <InlineSVG src={arrowLeftIcon} />
- <a href={parent.url}>{`${pgettext('Category page filters', 'See all')} ${parent.name}`}</a>
- </div>
- )}
- <ul className={category.parent ? ('product-filters__categories__childs') : ('product-filters__categories__childs no-parent')}>
- {category.children && (category.children.map((child) => {
- return (
- <li key={child.pk} className="item">
- <a href={child.url}>{child.name}</a>
- </li>
- );
- }))}
- </ul>
- </div>
- );
- }
-}
diff --git a/saleor/static/js/components/categoryPage/CategoryPage.js b/saleor/static/js/components/categoryPage/CategoryPage.js
deleted file mode 100644
index ca82ca53..00000000
--- a/saleor/static/js/components/categoryPage/CategoryPage.js
+++ /dev/null
@@ -1,225 +0,0 @@
-import queryString from 'query-string';
-import React, { Component, PropTypes } from 'react';
-import Relay from 'react-relay/classic';
-
-import CategoryFilter from './CategoryFilter';
-import PriceFilter from './PriceFilter';
-import ProductFilters from './ProductFilters';
-import ProductList from './ProductList';
-import SortBy from './SortBy';
-import { ensureAllowedName, getAttributesFromQuery, getFromQuery } from './utils';
-import {isMobile} from '../utils';
-
-const PAGINATE_BY = 24;
-const SORT_BY_FIELDS = ['name', 'price'];
-
-class CategoryPage extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- filtersMenu: !isMobile()
- };
- }
-
- static propTypes = {
- attributes: PropTypes.array,
- category: PropTypes.object,
- relay: PropTypes.object
- }
-
- incrementProductsCount = () => {
- this.props.relay.setVariables({
- count: this.props.relay.variables.count + PAGINATE_BY
- });
- }
-
- setSorting = (value) => {
- this.props.relay.setVariables({
- sortBy: value
- });
- }
-
- toggleMenu = (target) => {
- this.setState({
- filtersMenu: !target
- });
- }
-
- clearFilters = () => {
- this.props.relay.setVariables({
- attributesFilter: [],
- minPrice: null,
- maxPrice: null
- });
- }
-
- updateAttributesFilter = (key) => {
- // Create a new attributesFilter array by cloning the current one to make
- // Relay refetch products with new attributes. Passing the same array (even
- // if it's modified) would not result in new query, but would return cached
- // results.
- const attributesFilter = this.props.relay.variables.attributesFilter.slice(0);
- const index = attributesFilter.indexOf(key);
- if (index < 0) {
- attributesFilter.push(key);
- } else {
- attributesFilter.splice(index, 1);
- }
- this.props.relay.setVariables({ attributesFilter });
- }
-
- updatePriceFilter = (minPrice, maxPrice) => {
- this.props.relay.setVariables({
- minPrice: parseInt(minPrice) || null,
- maxPrice: parseInt(maxPrice) || null
- });
- }
-
- persistStateInUrl() {
- const { attributesFilter, count, maxPrice, minPrice, sortBy } = this.props.relay.variables;
- let urlParams = {};
- if (minPrice) {
- urlParams['minPrice'] = minPrice;
- }
- if (maxPrice) {
- urlParams['maxPrice'] = maxPrice;
- }
- if (count > PAGINATE_BY) {
- urlParams['count'] = count;
- }
- if (sortBy) {
- urlParams['sortBy'] = sortBy;
- }
- attributesFilter.forEach(filter => {
- const [ attributeName, valueSlug ] = filter.split(':');
- if (attributeName in urlParams) {
- urlParams[attributeName].push(valueSlug);
- } else {
- urlParams[attributeName] = [valueSlug];
- }
- });
- const url = Object.keys(urlParams).length ? '?' + queryString.stringify(urlParams) : location.href.split('?')[0];
- history.pushState({}, null, url);
- }
-
- componentDidUpdate() {
- // Persist current state of relay variables as query string. Current
- // variables are available in props after component rerenders, so it has to
- // be called inside componentDidUpdate method.
- this.persistStateInUrl();
- }
-
- render() {
- const { attributes, category, relay: { variables }, relay } = this.props;
- const { pendingVariables } = relay;
- const { filtersMenu } = this.state;
- return (
- <div className="category-page">
- <div className="category-top">
- <div className="row">
- <div className="col-md-7">
- <ul className="breadcrumbs list-unstyled d-none d-md-block">
- <li><a href="/">{pgettext('Main navigation item', 'Home')}</a></li>
- {category.ancestors && (category.ancestors.map((ancestor) => {
- return (
- <li key={ancestor.pk}><a href={ancestor.url}>{ancestor.name}</a></li>
- );
- }))}
- <li><a href={category.url}>{category.name}</a></li>
- </ul>
- </div>
- <div className="col-md-5">
- <div className="row">
- <div className="col-6 col-md-2 col-lg-6 filters-menu">
- <span className="filters-menu__label d-sm-none" onClick={() => this.toggleMenu(filtersMenu)}>{pgettext('Category page filters', 'Filters')}</span>
- {(variables.attributesFilter.length || variables.minPrice || variables.maxPrice) && (
- <span className="filters-menu__icon d-sm-none"></span>
- )}
- </div>
- <div className="col-6 col-md-10 col-lg-6">
- <SortBy sortedValue={variables.sortBy} setSorting={this.setSorting} />
- </div>
- </div>
- </div>
- </div>
- </div>
- <div className="row">
- <div className="col-md-4 col-lg-3">
- <div className="product-filters">
- <CategoryFilter category={category} />
- </div>
- {filtersMenu && (
- <div>
- <h2>
- {pgettext('Category page filters', 'Filters')}
- <span className="clear-filters float-right" onClick={this.clearFilters}>{pgettext('Category page filters', 'Clear filters')}</span>
- </h2>
- <div className="product-filters">
- <ProductFilters
- attributes={attributes}
- checkedAttributes={variables.attributesFilter}
- onFilterChanged={this.updateAttributesFilter}
- />
- <PriceFilter
- onFilterChanged={this.updatePriceFilter}
- maxPrice={variables.maxPrice}
- minPrice={variables.minPrice}
- />
- </div>
- </div>
- )}
- </div>
- <div className="col-md-8 col-lg-9 category-list">
- <div>
- <ProductList
- onLoadMore={this.incrementProductsCount}
- products={category.products}
- updating={pendingVariables}
- />
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-export default Relay.createContainer(CategoryPage, {
- initialVariables: {
- attributesFilter: getAttributesFromQuery(['count', 'minPrice', 'maxPrice', 'sortBy']),
- count: parseInt(getFromQuery('count', PAGINATE_BY)) || PAGINATE_BY,
- minPrice: parseInt(getFromQuery('minPrice')) || null,
- maxPrice: parseInt(getFromQuery('maxPrice')) || null,
- sortBy: ensureAllowedName(getFromQuery('sortBy', 'name'), SORT_BY_FIELDS)
- },
- fragments: {
- category: () => Relay.QL`
- fragment on CategoryType {
- pk
- name
- url
- ancestors {
- name
- pk
- url
- }
- children {
- name
- pk
- url
- slug
- }
- products (
- first: $count,
- attributes: $attributesFilter,
- priceGte: $minPrice,
- priceLte: $maxPrice,
- orderBy: $sortBy
- ) {
- ${ProductList.getFragment('products')}
- }
- }
- `
- }
-});
diff --git a/saleor/static/js/components/categoryPage/FilterHeader.js b/saleor/static/js/components/categoryPage/FilterHeader.js
deleted file mode 100644
index 76f68267..00000000
--- a/saleor/static/js/components/categoryPage/FilterHeader.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import React, { PropTypes } from 'react';
-
-import InlineSVG from 'react-inlinesvg';
-
-import chevronUpIcon from '../../../images/chevron_up.svg';
-import chevronDownIcon from '../../../images/chevron_down.svg';
-
-const FilterHeader = ({ onClick, title, visibility }) => {
- const imageSrc = visibility ? (chevronUpIcon) : (chevronDownIcon);
- const key = visibility ? 'chevronUpIcon' : 'chevronDownIcon';
- return (
- <h3 onClick={onClick}>
- {title}
- <div className="collapse-filters-icon">
- <InlineSVG key={key} src={imageSrc} />
- </div>
- </h3>
- );
-};
-
-FilterHeader.propTypes = {
- onClick: PropTypes.func.isRequired,
- title: PropTypes.string.isRequired,
- visibility: PropTypes.bool
-};
-
-export default FilterHeader;
diff --git a/saleor/static/js/components/categoryPage/NoResults.js b/saleor/static/js/components/categoryPage/NoResults.js
deleted file mode 100644
index 09d39426..00000000
--- a/saleor/static/js/components/categoryPage/NoResults.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import InlineSVG from 'react-inlinesvg';
-
-import noResultsImg from '../../../images/pirate.svg';
-
-const NoResults = () => {
- return (
- <div className="no-results">
- <div className="col-12">
- <InlineSVG src={noResultsImg} />
- <p>{pgettext('Epty search results', 'Sorry, no matches found for your request.')}</p>
- <p>{pgettext('Epty search results', 'Try again or shop new arrivals.')}</p>
- </div>
- </div>
- );
-};
-
-export default NoResults;
diff --git a/saleor/static/js/components/categoryPage/PriceFilter.js b/saleor/static/js/components/categoryPage/PriceFilter.js
deleted file mode 100644
index c907c8b2..00000000
--- a/saleor/static/js/components/categoryPage/PriceFilter.js
+++ /dev/null
@@ -1,78 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import FilterHeader from './FilterHeader';
-import {isMobile} from '../utils';
-
-export default class PriceFilter extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- visibility: !isMobile()
- };
- }
-
- static propTypes = {
- minPrice: PropTypes.number,
- maxPrice: PropTypes.number,
- onFilterChanged: PropTypes.func.isRequired
- }
-
- checkKey = (event) => {
- if (event.key === 'Enter') {
- this.updateFilter();
- }
- }
-
- updateFilter = () => {
- const minPrice = this.minPriceInput.value;
- const maxPrice = this.maxPriceInput.value;
- this.props.onFilterChanged(minPrice, maxPrice);
- }
-
- changeVisibility = () => {
- const { minPrice, maxPrice } = this.props;
- if (!(minPrice || maxPrice)) {
- this.setState({
- visibility: !this.state.visibility
- });
- }
- }
-
- render() {
- const { maxPrice, minPrice } = this.props;
- const { visibility } = this.state;
- return (
- <div className="product-filters__price-range">
- <FilterHeader
- onClick={this.changeVisibility}
- title={pgettext('Price filter on category page', 'Price range')}
- visibility={visibility}
- />
- {(visibility || minPrice || maxPrice) && (
- <div>
- <input
- className="form-control"
- defaultValue={minPrice}
- min="0"
- onKeyUp={this.checkKey}
- placeholder={pgettext('Price filter on category page', 'from')}
- ref={input => (this.minPriceInput = input)}
- type="number"
- />
- <span>&#8212;</span>
- <input
- className="form-control"
- defaultValue={maxPrice}
- min="0"
- onKeyUp={this.checkKey}
- placeholder={pgettext('Price filter on category page', 'to')}
- ref={input => (this.maxPriceInput = input)}
- type="number"
- />
- <button className="btn primary" onClick={this.updateFilter}>{pgettext('Price filter on category page', 'Update')}</button>
- </div>
- )}
- </div>
- );
- }
-}
diff --git a/saleor/static/js/components/categoryPage/ProductFilters.js b/saleor/static/js/components/categoryPage/ProductFilters.js
deleted file mode 100644
index 71f7ffac..00000000
--- a/saleor/static/js/components/categoryPage/ProductFilters.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import Relay from 'react-relay/classic';
-
-import AttributeInput from './AttributeInput';
-import FilterHeader from './FilterHeader';
-import {isMobile} from '../utils';
-
-class ProductFilters extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- visibility: {}
- };
- }
-
- static propTypes = {
- attributes: PropTypes.array,
- checkedAttributes: PropTypes.array,
- onFilterChanged: PropTypes.func.isRequired
- }
-
- getFilterKey(attributeSlug, valueSlug) {
- return `${attributeSlug}:${valueSlug}`;
- }
-
- onClick = (attributeSlug, valueSlug) => {
- this.props.onFilterChanged(this.getFilterKey(attributeSlug, valueSlug));
- }
-
- changeVisibility = (target) => {
- this.setState({
- visibility: Object.assign(this.state.visibility, {[target]: !this.state.visibility[target]})
- });
- }
-
- componentWillMount() {
- this.props.attributes.map((attribute) => {
- const attrValue = `${attribute.slug}`;
- this.setState({
- visibility: Object.assign(this.state.visibility, {[attrValue]: !isMobile()})
- });
- });
- }
-
- render() {
- const { attributes, checkedAttributes } = this.props;
- const { visibility } = this.state;
- return (
- <div className="product-filters__attributes">
- {attributes && (attributes.map((attribute) => {
- return (
- <div key={attribute.id}>
- <FilterHeader
- onClick={() => this.changeVisibility(attribute.slug)}
- title={attribute.name}
- visibility={visibility[attribute.slug]}
- />
- <ul id={attribute.slug}>
- {attribute.values.map((value) => {
- const key = this.getFilterKey(attribute.slug, value.slug);
- const isKeyChecked = checkedAttributes.indexOf(key) > -1;
- if (visibility[attribute.slug] || isKeyChecked) {
- return (
- <li key={value.id} className="item">
- <AttributeInput
- attribute={attribute}
- checked={isKeyChecked}
- onClick={this.onClick}
- value={value}
- />
- </li>
- );
- }
- })}
- </ul>
- </div>
- );
- }))}
- </div>
- );
- }
-}
-
-export default Relay.createContainer(ProductFilters, {
- fragments: {
- attributes: () => Relay.QL`
- fragment on ProductAttributeType @relay(plural: true) {
- id
- pk
- name
- slug
- values {
- id
- name
- slug
- color
- }
- }
- `
- }
-});
diff --git a/saleor/static/js/components/categoryPage/ProductItem.js b/saleor/static/js/components/categoryPage/ProductItem.js
deleted file mode 100644
index 5be4101c..00000000
--- a/saleor/static/js/components/categoryPage/ProductItem.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import Relay from 'react-relay/classic';
-
-import ProductPrice from './ProductPrice';
-
-class ProductItem extends Component {
-
- static propTypes = {
- product: PropTypes.object
- };
-
- getSchema = () => {
- const { product } = this.props;
- let data = {
- "@context": "http://schema.org/",
- "@type": "Product",
- "name": product.name,
- "image": product.thumbnailUrl1x,
- "offers": {
- "@type": "Offer",
- "priceCurrency": product.price.currency,
- "price": product.price.net,
- }
- };
- return JSON.stringify(data);
- };
-
- render() {
- const { product } = this.props;
- let productSchema = this.getSchema();
- let srcset = product.thumbnailUrl1x + ' 1x, ' + product.thumbnailUrl2x + ' 2x';
- return (
- <div className="col-6 col-md-4 product-list">
- <script type="application/ld+json">{productSchema}</script>
- <a href={product.url} className="link--clean">
- <div className="text-center">
- <div>
- <img className="img-responsive" src={product.thumbnailUrl1x} srcSet={srcset} alt="" />
- <span className="product-list-item-name" title={product.name}>{product.name}</span>
- </div>
- <div className="panel-footer">
- <ProductPrice price={product.price} availability={product.availability} />
- </div>
- </div>
- </a>
- </div>
- );
- }
-}
-
-export default Relay.createContainer(ProductItem, {
- fragments: {
- product: () => Relay.QL`
- fragment on ProductType {
- id
- name
- price {
- currency
- gross
- grossLocalized
- net
- }
- availability {
- ${ProductPrice.getFragment('availability')}
- }
- thumbnailUrl1x: thumbnailUrl(size: "255x255")
- thumbnailUrl2x: thumbnailUrl(size: "510x510")
- url
- }
- `
- }
-});
diff --git a/saleor/static/js/components/categoryPage/ProductList.js b/saleor/static/js/components/categoryPage/ProductList.js
deleted file mode 100644
index 953e83e5..00000000
--- a/saleor/static/js/components/categoryPage/ProductList.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import Relay from 'react-relay/classic';
-
-import ProductItem from './ProductItem';
-import NoResults from './NoResults';
-
-class ProductList extends Component {
-
- static propTypes = {
- onLoadMore: PropTypes.func.isRequired,
- products: PropTypes.object,
- setSorting: PropTypes.object,
- updating: PropTypes.object
- };
-
- onLoadMore = () => this.props.onLoadMore();
- setSorting = (event) => this.props.setSorting(event);
-
- render() {
- const { edges, pageInfo: { hasNextPage } } = this.props.products;
- return (
- <div className={this.props.updating && 'category-list--loading'}>
- <div className="row">
- {edges.length > 0 ? (edges.map((edge, i) => (
- <ProductItem key={i} product={edge.node} />
- ))) : (<NoResults />)}
- </div>
- <div className="load-more">
- {hasNextPage && (
- <button className="btn gray" onClick={this.onLoadMore}>{pgettext('Load more products on category view', 'Load more')}</button>
- )}
- </div>
- </div>
- );
- }
-}
-
-export default Relay.createContainer(ProductList, {
- fragments: {
- products: () => Relay.QL`
- fragment on ProductTypeConnection {
- edges {
- node {
- ${ProductItem.getFragment('product')}
- }
- }
- pageInfo {
- hasNextPage
- }
- }
- `
- }
-});
diff --git a/saleor/static/js/components/categoryPage/ProductPrice.js b/saleor/static/js/components/categoryPage/ProductPrice.js
deleted file mode 100644
index d9ea060d..00000000
--- a/saleor/static/js/components/categoryPage/ProductPrice.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { PropTypes } from 'react';
-import Relay from 'react-relay/classic';
-import InlineSVG from 'react-inlinesvg';
-
-import SaleImg from '../../../images/sale_bg.svg';
-
-const ProductPrice = ({ availability, price }) => {
- const { discount, priceRange } = availability;
- const isPriceRange = priceRange && priceRange.minPrice.gross !== priceRange.maxPrice.gross;
- return (
- <div>
- <span itemProp="price">
- {isPriceRange && <span>{pgettext('product price range', 'from')} </span>} {priceRange.minPrice.grossLocalized}
- </span>
- {discount && (
- <div className="product-list__sale"><InlineSVG src={SaleImg} /><span className="product-list__sale__text">{pgettext('Sale (discount) label for item in product list', 'Sale')}</span></div>
- )}
- </div>
- );
-};
-
-ProductPrice.propTypes = {
- availability: PropTypes.object.isRequired,
- price: PropTypes.object.isRequired
-};
-
-export default Relay.createContainer(ProductPrice, {
- fragments: {
- availability: () => Relay.QL`
- fragment on ProductAvailabilityType {
- available,
- discount { gross },
- priceRange {
- maxPrice { gross, grossLocalized, currency },
- minPrice { gross, grossLocalized, currency }
- }
- }
- `
- }
-});
diff --git a/saleor/static/js/components/categoryPage/SortBy.js b/saleor/static/js/components/categoryPage/SortBy.js
deleted file mode 100644
index 9e000656..00000000
--- a/saleor/static/js/components/categoryPage/SortBy.js
+++ /dev/null
@@ -1,115 +0,0 @@
-import React, { Component, PropTypes } from 'react';
-import InlineSVG from 'react-inlinesvg';
-
-import arrowUpIcon from '../../../images/arrow_up.svg';
-import arrowDownIcon from '../../../images/arrow_down.svg';
-
-export default class sortBy extends Component {
-
- constructor(props) {
- super(props);
- this.state = {
- visibility: false
- };
- }
-
- static propTypes = {
- setSorting: PropTypes.func,
- sortedValue: PropTypes.string
- }
-
- setSorting = (event) => {
- const value = event.currentTarget.className;
- this.props.setSorting(value);
- this.changeVisibility();
- }
-
- changeVisibility = () => {
- this.setState({
- visibility: !this.state.visibility
- });
- }
-
- render() {
- const { sortedValue } = this.props;
- const { visibility } = this.state;
- return (
- <div className="sort-by">
- <div className={visibility ? ('click-area') : ('click-area hide')} onClick={this.changeVisibility}></div>
- <button className="btn btn-link" onClick={this.changeVisibility}>
- {sortedValue ? (
- sortedValue.search('-') ? (
- <div>
- <span>{pgettext('Category page filters','Sort by:')} <strong>{sortedValue}</strong></span>
- <div className="sort-order-icon">
- <InlineSVG key="arrowUpIcon" src={arrowUpIcon} />
- </div>
- </div>
- ) : (
- <div>
- <span>{pgettext('Category page filters', 'Sort by:')} <strong>{sortedValue.replace('-', '')}</strong></span>
- <div className="sort-order-icon">
- <InlineSVG key="arrowDownIcon" src={arrowDownIcon} />
- </div>
- </div>
- )
- ) : (
- <span>{pgettext('Category page filters', 'Sort by:')} <strong>{pgettext('Category page filters', 'default')}</strong></span>
- )}
- </button>
- {visibility && (
- <ul className="sort-list">
- <li className="name">
- <div className="row">
- <div className="col-6">{pgettext('Category page filters', 'Sort by:')} <strong>{gettext('Name')}</strong></div>
- <div className="col-6">
- <div className="name" onClick={this.setSorting}>
- <span>{pgettext('Category page filters', 'ascending')}</span>
- <div className="float-right sort-order-icon">
- <InlineSVG src={arrowUpIcon} />
- </div>
- </div>
- </div>
- </div>
- <div className="row">
- <div className="col-6"></div>
- <div className="col-6">
- <div className="-name" onClick={this.setSorting}>
- <span>{pgettext('Category page filters', 'descending')}</span>
- <div className="float-right sort-order-icon">
- <InlineSVG src={arrowDownIcon} />
- </div>
- </div>
- </div>
- </div>
- </li>
- <li className="price">
- <div className="row">
- <div className="col-6">{pgettext('Category page filters', 'Sort by:')} <strong>{pgettext('Category page filters', 'Price')}</strong></div>
- <div className="col-6">
- <div className="price" onClick={this.setSorting}>
- <span>{pgettext('Category page filters', 'ascending')}</span>
- <div className="float-right sort-order-icon">
- <InlineSVG src={arrowUpIcon} />
- </div>
- </div>
- </div>
- </div>
- <div className="row">
- <div className="col-6"></div>
- <div className="col-6">
- <div className="-price" onClick={this.setSorting}>
- <span>{pgettext('Category page filters', 'descending')}</span>
- <div className="float-right sort-order-icon">
- <InlineSVG src={arrowDownIcon} />
- </div>
- </div>
- </div>
- </div>
- </li>
- </ul>
- )}
- </div>
- );
- }
-}
diff --git a/saleor/static/js/components/categoryPage/utils.js b/saleor/static/js/components/categoryPage/utils.js
deleted file mode 100644
index fc4af715..00000000
--- a/saleor/static/js/components/categoryPage/utils.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import queryString from 'query-string';
-
-export const getFromQuery = (key, defaultValue = null) => {
- let value = queryString.parse(location.search)[key];
- return value || defaultValue;
-};
-
-export const getAttributesFromQuery = (exclude) => {
- // Exclude parameter is used to exclude other query string parameters than
- // product attribute filters.
- const urlParams = queryString.parse(location.search);
- let attributes = [];
- Object.keys(urlParams).forEach(key => {
- if (exclude.indexOf(key) === -1) {
- if (Array.isArray(urlParams[key])) {
- const values = urlParams[key];
- values.map((valueSlug) => {
- attributes.push(`${key}:${valueSlug}`);
- });
- } else {
- const valueSlug = urlParams[key];
- attributes.push(`${key}:${valueSlug}`);
- }
- }
- });
- return attributes;
-};
-
-export const ensureAllowedName = (name, allowed) => {
- let origName = name;
- if (name && name.startsWith('-')) {
- name = name.substr(1, name.length);
- }
- return allowed.indexOf(name) > -1 ? origName : null;
-};
diff --git a/saleor/static/js/components/utils.js b/saleor/static/js/components/utils.js
deleted file mode 100644
index 6b0a2d5f..00000000
--- a/saleor/static/js/components/utils.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export const xsBreakpoint = 576;
-export const smBreakpoint = 768;
-export const mdBreakpoint = 992;
-export const lgBreakpoint = 1200;
-
-export const isMobile = () => {
- return window.innerWidth < smBreakpoint;
-}
-
-export const isTablet = () => {
- return window.innerWidth >= smBreakpoint && window.innerWidth <= mdBreakpoint;
-} \ No newline at end of file
diff --git a/saleor/static/js/storefront.js b/saleor/static/js/storefront.js
index 5da7ca43..e2ab6416 100644
--- a/saleor/static/js/storefront.js
+++ b/saleor/static/js/storefront.js
@@ -2,7 +2,6 @@ import '../scss/storefront.scss';
import 'jquery.cookie';
import React from 'react';
import ReactDOM from 'react-dom';
-import Relay from 'react-relay/classic';
import SVGInjector from 'svg-injector-2';
import variantPickerStore from './stores/variantPicker';
@@ -16,7 +15,7 @@ import ProductSchema from './components/variantPicker/ProductSchema';
let csrftoken = $.cookie('csrftoken');
-function csrfSafeMethod (method) {
+function csrfSafeMethod(method) {
return /^(GET|HEAD|OPTIONS|TRACE)$/.test(method);
}
@@ -28,15 +27,6 @@ $.ajaxSetup({
}
});
-Relay.injectNetworkLayer(
- new Relay.DefaultNetworkLayer('/graphql/', {
- credentials: 'same-origin',
- headers: {
- 'X-CSRFToken': csrftoken
- }
- })
-);
-
new SVGInjector().inject(document.querySelectorAll('svg[data-src]'));
let getAjaxError = (response) => {
@@ -54,8 +44,8 @@ $(document).ready((e) => {
if (windowWidth < 767) {
$mobileNav.append('<ul class="nav navbar-nav navbar__menu__login"></ul>');
$('.navbar__login a').appendTo('.navbar__menu__login')
- .wrap('<li class="nav-item login-item"></li>')
- .addClass('nav-link');
+ .wrap('<li class="nav-item login-item"></li>')
+ .addClass('nav-link');
}
$toogleIcon.click((e) => {
@@ -106,7 +96,7 @@ if ($initialValue) {
// Smart address form
-$(function() {
+$(function () {
const $i18nAddresses = $('.i18n-address');
$i18nAddresses.each(function () {
const $form = $(this).closest('form');
@@ -119,18 +109,51 @@ $(function() {
});
});
+// Sorter dropdown
+
+$(document).ready((e) => {
+ $('.sort-by button').on('click', (e) => {
+ const t = $(e.currentTarget).parent();
+ const l = t.find('.sort-list');
+ if (l.hasClass('d-none')) {
+ l.removeClass('d-none');
+ t.find('.click-area').removeClass('d-none');
+ } else {
+ l.addClass('d-none');
+ t.find('.click-area').addClass('d-none');
+ }
+ });
+ $('.sort-by .click-area').on('click', (e) => {
+ $('.sort-by .sort-list').addClass('d-none');
+ $(e.currentTarget).addClass('d-none');
+ });
+});
+
+// Mobile filters menu
+
+$(document).ready((e) => {
+ $('.filters-menu').on('click', (e) => {
+ const t = $('.filters-menu__body');
+ if (t.hasClass('d-none')) {
+ t.removeClass('d-none');
+ } else {
+ t.addClass('d-none');
+ }
+ });
+});
+
// Input Passwords
let $inputPassword = $('input[type=password]');
-$("<img class='passIcon' src="+passwordIvisible+" />").insertAfter($inputPassword);
+$("<img class='passIcon' src=" + passwordIvisible + " />").insertAfter($inputPassword);
$inputPassword.parent().addClass('relative');
$('.passIcon').on('click', (e) => {
let $input = $(e.target).parent().find('input');
if ($input.attr('type') == 'password') {
- $input.attr('type','text');
+ $input.attr('type', 'text');
$(e.target).attr('src', passwordVisible);
} else {
- $input.attr('type','password');
+ $input.attr('type', 'password');
$(e.target).attr('src', passwordIvisible);
}
});
@@ -253,7 +276,7 @@ if (variantPickerContainer) {
// Product Schema
const productSchemaContainer = document.getElementById('product-schema-component');
if (productSchemaContainer) {
- let productSchema = JSON.parse(document.getElementById('product-schema-component').children[0].text)
+ let productSchema = JSON.parse(document.getElementById('product-schema-component').children[0].text);
ReactDOM.render(
<ProductSchema
variantStore={variantPickerStore}
@@ -270,20 +293,19 @@ let $deleteAdressIcons = $('.icons');
let $deleteAdressIcon = $('.delete-icon');
let $deleteAddress = $('.address-delete');
- $deleteAdressIcon.on('click', (e) => {
+$deleteAdressIcon.on('click', (e) => {
if ($deleteAddress.hasClass('none')) {
$deleteAddress.removeClass('none');
$deleteAdressIcons.addClass('none');
} else {
$deleteAddress.addClass('none');
}
- });
+});
- $deleteAddress.find('.cancel').on('click', (e) => {
+$deleteAddress.find('.cancel').on('click', (e) => {
$deleteAddress.addClass('none');
$deleteAdressIcons.removeClass('none');
- });
-
+});
// Cart quantity form
@@ -293,7 +315,7 @@ let $total = $('.cart-subtotal');
let $cartBadge = $('.navbar__brand__cart .badge');
let $removeProductSucces = $('.remove-product-alert');
let $closeMsg = $('.close-msg');
-$cartLine.each(function() {
+$cartLine.each(function () {
let $quantityInput = $(this).find('#id_quantity');
let cartFormUrl = $(this).find('.form-cart').attr('action');
let $qunatityError = $(this).find('.cart__line__quantity-error');
@@ -308,7 +330,7 @@ $cartLine.each(function() {
success: (response) => {
if (newQuantity == 0) {
if (response.cart.numLines == 0) {
- $.cookie('alert', 'true', { path: '/cart' });
+ $.cookie('alert', 'true', {path: '/cart'});
location.reload();
} else {
$removeProductSucces.removeClass('hidden-xs-up');
@@ -340,7 +362,7 @@ $cartLine.each(function() {
$cartDropdown.load(summaryLink);
$removeProductSucces.removeClass('hidden-xs-up');
} else {
- $.cookie('alert', 'true', { path: '/cart' });
+ $.cookie('alert', 'true', {path: '/cart'});
location.reload();
}
deliveryAjax();
@@ -351,7 +373,7 @@ $cartLine.each(function() {
// StyleGuide fixed menu
-$(document).ready(function() {
+$(document).ready(function () {
let styleGuideMenu = $('.styleguide__nav');
$(window).scroll(function () {
if ($(this).scrollTop() > 100) {
@@ -359,14 +381,41 @@ $(document).ready(function() {
} else {
styleGuideMenu.removeClass("fixed");
}
- })
+ });
});
if ($.cookie('alert') === 'true') {
$removeProductSucces.removeClass('hidden-xs-up');
- $.cookie('alert', 'false', { path: '/cart' });
+ $.cookie('alert', 'false', {path: '/cart'});
}
$closeMsg.on('click', (e) => {
$removeProductSucces.addClass('hidden-xs-up');
});
+
+
+$('.toggle-filter').each(function () {
+ let icon = $(this).find('.collapse-filters-icon');
+ let ele = $(this).find('.filter-form-field');
+
+ let filterArrowDown = $('.product-filters__attributes').data('icon-down');
+ let filterArrowUp = $('.product-filters__attributes').data('icon-up');
+
+ ele.attr('aria-expanded', '');
+
+ $(this).find('.filter-label').on('click', () => {
+ if (ele.attr('aria-expanded') !== undefined) {
+ ele.removeAttr('aria-expanded').find('input[type="checkbox"], input[type="number"]').each((i, el) => {
+ if (!el.checked) {
+ $(el.parentNode.parentNode).addClass('d-none');
+ }
+ });
+ icon.find('img').attr('src', filterArrowDown);
+ } else {
+ ele.attr('aria-expanded', '').find('input[type="checkbox"], input[type="number"]').each((i, el) => {
+ $(el.parentNode.parentNode).removeClass('d-none');
+ });
+ icon.find('img').attr('src', filterArrowUp);
+ }
+ });
+});
diff --git a/saleor/static/scss/components/_filters.scss b/saleor/static/scss/components/_filters.scss
index cafd80c0..9f614cb0 100644
--- a/saleor/static/scss/components/_filters.scss
+++ b/saleor/static/scss/components/_filters.scss
@@ -160,6 +160,9 @@
svg {
margin-top: $global-padding/3;
}
+ a {
+ color: inherit;
+ }
&:last-child {
border: none;
padding-bottom: 0;
diff --git a/saleor/static/scss/components/_header.scss b/saleor/static/scss/components/_header.scss
index 0df22375..d97e1fc1 100644
--- a/saleor/static/scss/components/_header.scss
+++ b/saleor/static/scss/components/_header.scss
@@ -202,7 +202,7 @@
background-color: $white;
padding-left: 0;
padding-right: $global-padding;
- padding-top: $global-padding * 2;
+ padding-top: $global-padding * 5;
transition: 0.3s ease-in-out;
ul {
text-align: left;
diff --git a/templates/account/login.html b/templates/account/login.html
index 328f9a55..bd6af86c 100644
--- a/templates/account/login.html
+++ b/templates/account/login.html
@@ -6,7 +6,7 @@
{% block content %}
- <div class="col-lg-10 offset-lg-1 col-sm-12">
+ <div class="col-lg-10 col-sm-12 m-auto">
<div class="row login">
<div class="col-md-6 login__register">
<div class="login__register-link">
diff --git a/templates/category/_items.html b/templates/category/_items.html
index 78b5da2f..ef276a33 100644
--- a/templates/category/_items.html
+++ b/templates/category/_items.html
@@ -13,13 +13,13 @@
<a href="{{ product.get_absolute_url }}" class="link--clean">
<div class="text-center">
<div>
- <img class="img-responsive"
- src="{% product_first_image product method="crop" size="255x255" %}"
+ <img class="img-responsive"
+ src="{% product_first_image product method="crop" size="255x255" %}"
srcset="{% product_first_image product method="crop" size="255x255" %} 1x, {% product_first_image product method="crop" size="510x510" %} 2x"
alt="">
<span class="product-list-item-name" title="{{ product }}">{{ product }}</span>
</div>
-
+
<div class="panel-footer">
{% if availability.available %}
{% price_range availability.price_range %}
diff --git a/templates/category/index.html b/templates/category/index.html
index fdae31ee..5d1018c3 100644
--- a/templates/category/index.html
+++ b/templates/category/index.html
@@ -1,30 +1,190 @@
{% extends "base.html" %}
-{% load slice from shop %}
-{% load staticfiles %}
{% load bootstrap_pagination from bootstrap3 %}
+{% load i18n %}
+{% load shop %}
+{% load staticfiles %}
{% load render_bundle from webpack_loader %}
+{% load prices_i18n %}
{% block footer_scripts %}
{{ block.super }}
- {% render_bundle 'category' 'js' %}
{% endblock footer_scripts %}
{% block title %}{{ category }} — {{ block.super }}{% endblock %}
{% block breadcrumb %}
- {{ block.super }}
- {% for breadcrumb in breadcrumbs %}
- <li{% if forloop.last %} class="active"{% endif %}>
- {% if not forloop.last %}
- <a href="{{ breadcrumb.get_absolute_url }}">{{ breadcrumb }}</a>
- {% else %}
- <span>{{ breadcrumb }}</span>
- {% endif %}
- </li>
- {% endfor %}
+ {{ block.super }}
+ {% for breadcrumb in breadcrumbs %}
+ <li{% if forloop.last %} class="active"{% endif %}>
+ {% if not forloop.last %}
+ <a href="{{ breadcrumb.get_absolute_url }}">{{ breadcrumb }}</a>
+ {% else %}
+ <span>{{ breadcrumb }}</span>
+ {% endif %}
+ </li>
+ {% endfor %}
{% endblock breadcrumb %}
{% block content %}
- {% csrf_token %}
- <div id="category-page" data-category='{"id":{{category.id}}}'></div>
+ <div id="category-page">
+ <div class="category-top">
+ <div class="row">
+ <div class="col-md-7">
+ <ul class="breadcrumbs list-unstyled d-none d-md-block">
+ <li><a href="/">Home</a></li>
+ {% for ancestor in category.get_ancestors %}
+ <li><a href='{{ ancestor.get_absolute_url }}'>{{ ancestor.name }}</a></li>
+ {% endfor %}
+ <li><a href='{{ category.get_absolute_url }}'>{{ category.name }}</a></li>
+ </ul>
+ </div>
+ <div class="col-md-5">
+ <div class="row">
+ <div class="col-6 col-md-2 col-lg-6 filters-menu">
+ <span class="filters-menu__label d-sm-none">Filters</span>
+ </div>
+ <div class="col-6 col-md-10 col-lg-6">
+ <div class="sort-by">
+ <div class="click-area d-none"></div>
+ <button class="btn btn-link">
+ <div>
+ <span>
+ Sort by:&nbsp;
+ <strong>
+ {{ sort_by_label }}
+ </strong>
+ </span>
+ <div class="sort-order-icon">
+ {% if is_descending %}
+ <svg data-src="{% static "assets/arrow_down.svg" %}">
+ {% else %}
+ <svg data-src="{% static "assets/arrow_up.svg" %}">
+ {% endif %}
+ </div>
+ </div>
+ </button>
+ <ul class="sort-list d-none">
+ {% for choice in sort_by_choices %}
+ <li>
+ <div class="row">
+ <div class="col-6">
+ Sort by: <strong>{{ choice.label }}</strong>
+ </div>
+ <div class="col-6">
+ <div>
+ <a href="{% get_sort_by_url choice.value %}">
+ <span>{% trans 'ascending' context 'sort by option' %}</span>
+ <div class="sort-order-icon float-right">
+ <img src="{% static "assets/arrow_up.svg" %}">
+ </div>
+ </a>
+ <a href="{% get_sort_by_url choice.value descending=True %}">
+ <span>{% trans 'descending' context 'sort by option' %}</span>
+ <div class="sort-order-icon float-right">
+ <img src="{% static "assets/arrow_down.svg" %}">
+ </div>
+ </a>
+ </div>
+ </div>
+ </div>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-md-4 col-lg-3">
+ <div class="product-filters">
+ <div class="product-filters__categories">
+ <h2>
+ <strong>{{ category.name }}</strong>
+ </h2>
+ <ul class="product-filters__categories__childs no-parent">
+ {% for child in category.get_children %}
+ <li><a href="{{ child.get_absolute_url }}">{{ child.name }}</a></li>
+ {% endfor %}
+ </ul>
+ </div>
+ </div>
+ <div class="filters-menu__body d-none d-md-block">
+ <h2>
+ {% trans 'Filters' context 'Filter heading title' %}
+ <a href="?">
+ <span class="clear-filters float-right">{% trans 'Clear filters' context 'Category page filters' %}</span>
+ </a>
+ </h2>
+ <div class="product-filters">
+ <div class="product-filters__attributes" data-icon-up="{% static "assets/chevron_up.svg" %}"
+ data-icon-down="{% static "assets/chevron_down.svg" %}">
+ <form method="get">
+ {% for field in filter.form %}
+ {% if field.name == 'sort_by' %}
+ <!--Field 'sort_by' is hidden because it is rendered in header.
+ This is required if you want to have sorting to be kept during further filtering.-->
+ <input type="hidden" name="sort_by"
+ value="{% if request.GET.sort_by %}{{ request.GET.sort_by }}{% endif %}">
+ {% elif field.name != 'price' %}
+ <div class="toggle-filter">
+ <h3 class="filter-label">
+ {{ field.label }}
+ <div class="collapse-filters-icon">
+ <img src="{% static "assets/chevron_up.svg" %}">
+ </div>
+ </h3>
+ <div class="filter-form-field" style="display:block">
+ {{ field }}
+ </div>
+ </div>
+ {% endif %}
+ {% endfor %}
+ {% for field in filter.form %}
+ {% if field.name == 'price' %}
+ <div class="toggle-filter product-filters__price-range">
+ <h3 class="filter-label">
+ {{ field.label }}
+ <div class="collapse-filters-icon">
+ <img src="{% static "assets/chevron_up.svg" %}">
+ </div>
+ </h3>
+ <div class="filter-form-field" style="display:block">
+ <div>
+ <input id="{{ field.auto_id }}_0" name="{{ field.name }}_0"
+ value="{% if field.value.0 %}{{ field.value.0 }}{% endif %}" type="number" min="0"
+ class="form-control d-inline"
+ placeholder="from"><span>-</span><input id="{{ field.auto_id }}_1" name="{{ field.name }}_1"
+ value="{% if field.value.1 %}{{ field.value.1 }}{% endif %}" type="number" min="0"
+ class="form-control d-inline" placeholder="to">
+ </div>
+ </div>
+ <button class="btn primary" type="submit">Update</button>
+ </div>
+ {% endif %}
+ {% endfor %}
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-md-8 col-lg-9 category-list">
+ <div>
+ <div>
+ <div class="row">
+ {% include "category/_items.html" with products=products %}
+ </div>
+ <div class="row">
+ <div class="m-auto">
+ {% if products_paginated.has_other_pages %}
+ {% bootstrap_pagination products_paginated extra=request.GET.urlencode %}
+ {% endif %}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
{% endblock content %}
diff --git a/tests/test_product.py b/tests/test_product.py
index 947c427d..e915f832 100644
--- a/tests/test_product.py
+++ b/tests/test_product.py
@@ -369,3 +369,46 @@ def test_variant_availability_status(unavailable_product):
stock.save()
status = get_variant_availability_status(variant)
assert status == VariantAvailabilityStatus.AVAILABLE
+
+
+def test_product_filter_before_filtering(
+ authorized_client, product_in_stock, default_category):
+ products = models.Product.objects.all()
+ url = reverse(
+ 'product:category', kwargs={'path': default_category.slug,
+ 'category_id': default_category.pk})
+ response = authorized_client.get(url)
+ assert list(products) == list(response.context['filter'].qs)
+
+
+def test_product_filter_product_exists(authorized_client, product_in_stock,
+ default_category):
+ products = models.Product.objects.all()
+ url = reverse(
+ 'product:category', kwargs={'path': default_category.slug,
+ 'category_id': default_category.pk})
+ data = {'price_0': [''], 'price_1': ['20']}
+ response = authorized_client.get(url, data)
+ assert list(response.context['filter'].qs) == list(products)
+
+
+def test_product_filter_product_does_not_exists(
+ authorized_client, product_in_stock, default_category):
+ url = reverse(
+ 'product:category', kwargs={'path': default_category.slug,
+ 'category_id': default_category.pk})
+ data = {'price_0': ['20'], 'price_1': ['']}
+ response = authorized_client.get(url, data)
+ assert list(response.context['filter'].qs) == []
+
+
+def test_product_filter_form(authorized_client, product_in_stock,
+ default_category):
+ products = models.Product.objects.all()
+ url = reverse(
+ 'product:category', kwargs={'path': default_category.slug,
+ 'category_id': default_category.pk})
+ response = authorized_client.get(url)
+ assert 'price' in response.context['filter'].form.fields.keys()
+ assert 'sort_by' in response.context['filter'].form.fields.keys()
+ assert list(response.context['filter'].qs) == list(products)
diff --git a/webpack.config.js b/webpack.config.js
index b34b3f5a..cab86784 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -63,7 +63,6 @@ var faviconsWebpackPlugin = new FaviconsWebpackPlugin({
var config = {
entry: {
- category: './saleor/static/js/category.js',
dashboard: './saleor/static/dashboard/js/dashboard.js',
storefront: './saleor/static/js/storefront.js',
vendor: [
@@ -71,8 +70,7 @@ var config = {
'bootstrap',
'jquery',
'jquery.cookie',
- 'react',
- 'react-relay'
+ 'react'
]
},
output: output,
diff --git a/yarn.lock b/yarn.lock
index 9530fd0a..ea467d6a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -343,8 +343,8 @@ babel-es6-polyfill@^1.1.0:
regenerator-babel "^0.8.13-2"
babel-eslint@^8.0.0:
- version "8.0.0"
- resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.0.tgz#ce06f385bdfb5b6d7e603f06222f891abd14c240"
+ version "8.0.1"
+ resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-8.0.1.tgz#5d718be7a328625d006022eb293ed3008cbd6346"
dependencies:
babel-code-frame "7.0.0-beta.0"
babel-traverse "7.0.0-beta.0"
@@ -1879,13 +1879,6 @@ deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
-default-gateway@^2.2.2:
- version "2.5.0"
- resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-2.5.0.tgz#78e24dbd2e1df7490c2b8050515b8e816bfa7da5"
- dependencies:
- execa "^0.7.0"
- ip-regex "^2.1.0"
-
define-properties@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94"
@@ -2242,8 +2235,8 @@ eslint-scope@^3.7.1:
estraverse "^4.1.1"
eslint@^4.6.1:
- version "4.7.2"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.7.2.tgz#ff6f5f5193848a27ee9b627be3e73fb9cb5e662e"
+ version "4.8.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-4.8.0.tgz#229ef0e354e0e61d837c7a80fdfba825e199815e"
dependencies:
ajv "^5.2.0"
babel-code-frame "^6.22.0"
@@ -2435,8 +2428,8 @@ extglob@^0.3.1:
is-extglob "^1.0.0"
extract-text-webpack-plugin@^3.0.0:
- version "3.0.0"
- resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.0.tgz#90caa7907bc449f335005e3ac7532b41b00de612"
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/extract-text-webpack-plugin/-/extract-text-webpack-plugin-3.0.1.tgz#605a8893faca1dd49bb0d2ca87493f33fd43d102"
dependencies:
async "^2.4.1"
loader-utils "^1.1.0"
@@ -2974,9 +2967,9 @@ hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
-hoist-non-react-statics@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
+hoist-non-react-statics@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
home-or-tmp@^2.0.0:
version "2.0.0"
@@ -3167,12 +3160,11 @@ inquirer@^3.0.6:
strip-ansi "^4.0.0"
through "^2.3.6"
-internal-ip@^2.0.2:
- version "2.0.3"
- resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-2.0.3.tgz#ed3cf9b671ac7ff23037bfacad42eb439cd9546c"
+internal-ip@1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-1.2.0.tgz#ae9fbf93b984878785d50a8de1b356956058cf5c"
dependencies:
- default-gateway "^2.2.2"
- ipaddr.js "^1.5.2"
+ meow "^3.3.0"
interpret@^0.6.4:
version "0.6.6"
@@ -3196,10 +3188,6 @@ ip-regex@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-1.0.3.tgz#dc589076f659f419c222039a33316f1c7387effd"
-ip-regex@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
-
ip@^1.1.0, ip@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
@@ -3208,10 +3196,6 @@ ipaddr.js@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.4.0.tgz#296aca878a821816e5b85d0a285a99bcff4582f0"
-ipaddr.js@^1.5.2:
- version "1.5.2"
- resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0"
-
is-absolute-url@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@@ -3809,7 +3793,7 @@ memory-fs@~0.3.0:
errno "^0.1.3"
readable-stream "^2.0.1"
-meow@^3.7.0:
+meow@^3.3.0, meow@^3.7.0:
version "3.7.0"
resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb"
dependencies:
@@ -3933,10 +3917,10 @@ mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdi
minimist "0.0.8"
mobx-react@^4.1.0:
- version "4.3.2"
- resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.3.2.tgz#1ecbffa5690cc6460db6bb16c0c11034f0b783da"
+ version "4.3.3"
+ resolved "https://registry.yarnpkg.com/mobx-react/-/mobx-react-4.3.3.tgz#4ad76c03d1e942b431e942f9ea18df0756771655"
dependencies:
- hoist-non-react-statics "^1.2.0"
+ hoist-non-react-statics "^2.3.1"
mobx@^3.0.2:
version "3.3.0"
@@ -4965,8 +4949,8 @@ rc@^1.1.7:
strip-json-comments "~2.0.1"
react-dom@^15.4.2:
- version "15.6.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.1.tgz#2cb0ed4191038e53c209eb3a79a23e2a4cf99470"
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.2.tgz#41cfadf693b757faf2708443a1d1fd5a02bef730"
dependencies:
fbjs "^0.8.9"
loose-envify "^1.1.0"
@@ -4981,17 +4965,17 @@ react-inlinesvg@^0.6.2:
once "^1.4"
react-relay@^1.3.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/react-relay/-/react-relay-1.4.0.tgz#69ab51917bef59359e0f9b229069239f9023c21b"
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/react-relay/-/react-relay-1.4.1.tgz#61f7b5802d70446e154490f9d0fceec9150ebda5"
dependencies:
babel-runtime "^6.23.0"
fbjs "^0.8.14"
prop-types "^15.5.8"
- relay-runtime "1.4.0"
+ relay-runtime "1.4.1"
react@^15.4.2:
- version "15.6.1"
- resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df"
+ version "15.6.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-15.6.2.tgz#dba0434ab439cfe82f108f0f511663908179aa72"
dependencies:
create-react-class "^15.6.0"
fbjs "^0.8.9"
@@ -5154,9 +5138,9 @@ relay-debugger-react-native-runtime@0.0.10:
version "0.0.10"
resolved "https://registry.yarnpkg.com/relay-debugger-react-native-runtime/-/relay-debugger-react-native-runtime-0.0.10.tgz#0ef36012a1fba928962205514b46f635c652f235"
-relay-runtime@1.4.0:
- version "1.4.0"
- resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-1.4.0.tgz#d91e4649324696060137b014aaaad08765018451"
+relay-runtime@1.4.1:
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/relay-runtime/-/relay-runtime-1.4.1.tgz#f88dcd0a422700a04563f291f570e4ce368e36d0"
dependencies:
babel-runtime "^6.23.0"
fbjs "^0.8.14"
@@ -5396,8 +5380,8 @@ select-hose@^2.0.0:
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
select2@^4.0.3:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/select2/-/select2-4.0.3.tgz#207733fe91eacb9cb1a13f12463401f472449e0f"
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/select2/-/select2-4.0.4.tgz#a18a628785f98d13999971ae95d8d7e57268076b"
dependencies:
almond "~0.3.1"
jquery-mousewheel "~3.1.13"
@@ -6179,8 +6163,8 @@ webpack-dev-middleware@^1.11.0:
time-stamp "^2.0.0"
webpack-dev-server@^2.7.1:
- version "2.8.2"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.8.2.tgz#abd61f410778cc4c843d7cebbf41465b1ab7734c"
+ version "2.9.1"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.9.1.tgz#7ac9320b61b00eb65b2109f15c82747fc5b93585"
dependencies:
ansi-html "0.0.7"
array-includes "^3.0.3"
@@ -6192,7 +6176,7 @@ webpack-dev-server@^2.7.1:
express "^4.13.3"
html-entities "^1.2.0"
http-proxy-middleware "~0.17.4"
- internal-ip "^2.0.2"
+ internal-ip "1.2.0"
ip "^1.1.5"
loglevel "^1.4.1"
opn "^5.1.0"