Commit 5b7c989a authored by Maxym Borodenko's avatar Maxym Borodenko
Browse files

Merge branch '2-shopping-cart' into 'master'

Shopping cart

Closes #2

See merge request !5
parents 2bfde444 afdf495f
......@@ -22,6 +22,9 @@
<li class="nav-item">
<a class="nav-link" href="#/api-info">API Info</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#/cart">Cart</a>
</li>
<li class="nav-item">
<a class="nav-link" href=".">EN</a>
</li>
......
......@@ -102,5 +102,11 @@
"62": "Field margin",
"99": "Other"
}
},
"cart": {
"title": "Shopping cart",
"addToCart": "Add to cart",
"removeFromCart": "Remove from cart",
"isEmpty": "Shopping cart is empty"
}
}
......@@ -9,10 +9,13 @@ import FilteredPage, { IPageRequest } from '@genesys/client/model/FilteredPage';
import Pagination from './Pagination';
import { AccessionFilters } from './AccessionFilters';
import { withTranslation, WithTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities';
interface IAccessionListPageState {
filter: AccessionFilter;
accessions: FilteredPage<Accession>;
selected: number[];
isAllSelected: boolean;
}
interface IAccessionListPageProps {
......@@ -27,6 +30,8 @@ class AccessionListPage extends React.Component<IAccessionListPageProps & WithTr
this.state = {
filter: this.props.filter,
accessions: null,
selected: [],
isAllSelected: false,
}
}
......@@ -72,9 +77,34 @@ class AccessionListPage extends React.Component<IAccessionListPageProps & WithTr
this.loadData({ ...this.state.filter, ...filter }, {});
};
private toggleRowSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const { selected } = this.state;
const { id } = (e.currentTarget as HTMLElement).dataset;
const updated = selected.filter((selectedId) => selectedId !== +id);
if (updated.length === selected.length) {
updated.push(+id);
}
this.setState({ selected: updated });
};
private addSelectedToCart = () => {
LocalStorageCart.addToCart(this.state.selected);
this.setState({ selected: [], isAllSelected: false });
};
private onToggleAll = () => {
this.setState((prevState) => ({
isAllSelected: !prevState.isAllSelected,
selected: !prevState.isAllSelected ? prevState.accessions.content.map((acc) => acc.id) : [],
}));
};
public render() {
const { accessions } = this.state;
const { accessions, selected, isAllSelected } = this.state;
const { t } = this.props;
const selectedIds = new Set();
selected.forEach((id) => selectedIds.add(+id));
if (accessions === null) {
return (
<div>{ t('loading') }</div>
......@@ -82,31 +112,57 @@ class AccessionListPage extends React.Component<IAccessionListPageProps & WithTr
} else {
return (
<>
<h1>{ t('estimatedNumberOfItems', { count: accessions.totalElements, what: t('accession.model', { count: accessions.totalElements }) }) }</h1>
<h1 className="d-flex justify-content-between align-items-center">
{ t('estimatedNumberOfItems', { count: accessions.totalElements, what: t('accession.model', { count: accessions.totalElements }) }) }
{ selected.length !== 0 &&
<button type="button" className="btn btn-primary" onClick={ this.addSelectedToCart }>
{ t('cart.addToCart') }
</button>
}
</h1>
<AccessionFilters filter={ this.state.filter } applyFilter={ this.applyFilter } key={ `filters-${accessions.filterCode}` }/>
<Pagination loadData={ this.loadData } paged={ accessions }>
<table className="table table-striped">
<thead className="thead-dark">
<tr>
<th>{ t('accession.crop') }</th>
<th>{ t('accession.acceNumb') }</th>
<th>{ t('accession.accessionName') }</th>
<th>{ t('accession.taxonomy') }</th>
<th>{ t('accession.countryOfOrigin') }</th>
<th>{ t('accession.sampStat') }</th>
</tr>
<tr>
<th>
<input
type="checkbox"
name="select-all"
checked={ isAllSelected }
onChange={ this.onToggleAll }
className="align-middle"
/>
</th>
<th>{ t('accession.crop') }</th>
<th>{ t('accession.acceNumb') }</th>
<th>{ t('accession.accessionName') }</th>
<th>{ t('accession.taxonomy') }</th>
<th>{ t('accession.countryOfOrigin') }</th>
<th>{ t('accession.sampStat') }</th>
</tr>
</thead>
<tbody>
{ accessions.content.map((a) => (
<tr key={ a.id }>
<td>{ a.cropName }</td>
<td><Link to={ `/a/${a.uuid}` }>{ a.accessionNumber }</Link></td>
<td>{ a.accessionName }</td>
<td><span dangerouslySetInnerHTML={ { __html: a.taxonomy.taxonNameHtml } } /></td>
<td>{ a.countryOfOrigin && a.countryOfOrigin.name }</td>
<td>{ a.sampStat && t(`accession.sampleStatus.${a.sampStat}`) }</td>
</tr>
)) }
{ accessions.content.map((a, i) => (
<tr key={ a.id } className={ selectedIds.has(a.id) ? 'table-primary' : '' }>
<td>
<input
type="checkbox"
name={ `checkbox-${a.id}-${i}` }
data-id={ a.id }
checked={ selectedIds.has(a.id) }
onChange={ this.toggleRowSelect }
className="align-middle"
/>
</td>
<td>{ a.cropName }</td>
<td><Link to={ `/a/${a.uuid}` }>{ a.accessionNumber }</Link></td>
<td>{ a.accessionName }</td>
<td><span dangerouslySetInnerHTML={ { __html: a.taxonomy.taxonNameHtml } } /></td>
<td>{ a.countryOfOrigin && a.countryOfOrigin.name }</td>
<td>{ a.sampStat && t(`accession.sampleStatus.${a.sampStat}`) }</td>
</tr>
)) }
</tbody>
</table>
</Pagination>
......
......@@ -5,6 +5,7 @@ import ApiInfoPage from './ApiInfoPage';
import AccessionList from './AccessionListPage';
import AccessionDetails from './AccessionDetailsPage';
import { createHashHistory } from 'history';
import CartPage from './CartPage';
const hashHistory = createHashHistory({});
......@@ -25,6 +26,7 @@ export default class App extends React.Component<IAppProps, any> {
<Route path="/a/:uuid" exact render={ (props) => <AccessionDetails { ...props } apiUrl={ apiUrl } /> } />
<Route path="/" exact render={ (props) => <AccessionList { ...props } filter={ this.props.filter }/> }/>
<Route path="/api-info" exact component={ ApiInfoPage }/>
<Route path="/cart/" exact component={ CartPage }/>
<Route component={ NotFound }/>
</Switch>
......
import React from 'react';
import { AccessionService } from '@genesys/client/service';
import AccessionFilter from '@genesys/client/model/accession/AccessionFilter';
import Accession from '@genesys/client/model/accession/Accession';
import FilteredPage, { IPageRequest } from '@genesys/client/model/FilteredPage';
import { withTranslation, WithTranslation } from 'react-i18next';
import { LocalStorageCart } from 'utilities';
import { Link } from 'react-router-dom';
interface IAccessionListPageState {
accessions: FilteredPage<Accession & Record<string, any>>;
isEmpty: boolean;
selected: number[];
isAllSelected: boolean;
}
interface IAccessionListPageProps extends WithTranslation {
filter?: AccessionFilter;
}
export class AccessionListPage extends React.Component<IAccessionListPageProps, IAccessionListPageState> {
public constructor(props) {
super(props);
this.state = {
accessions: null,
isEmpty: false,
selected: [],
isAllSelected: false,
}
}
public componentDidMount(): void {
if (typeof document !== 'undefined') {
window.addEventListener('storage', this.handleLocalStorageUpdate);
this.receiveData();
}
}
public componentWillUnmount() {
if (typeof document !== 'undefined') {
window.removeEventListener('storage', this.handleLocalStorageUpdate);
}
}
private handleLocalStorageUpdate = (e: StorageEvent) => {
if (e.key === LocalStorageCart.LS_KEY) {
this.receiveData();
}
};
private receiveData = () => {
const accessionsIds = LocalStorageCart.getCartItemsIds();
if (accessionsIds.length === 0) {
this.setState({ isEmpty: true, accessions: null });
} else {
this.loadData({ id: accessionsIds }, {});
}
};
private loadData = (filter: AccessionFilter, pageR: IPageRequest): Promise<void> => {
const cartItems = LocalStorageCart.getCartItemsLS();
const map = new Map();
cartItems.forEach((item) => map.set(+item.id, item.distributionFormCode));
return AccessionService
.list(filter, pageR)
.then((data: FilteredPage<Accession & any>) => {
console.log('accessions: ', data);
data.content.forEach((accession) => accession.distributionFormCode = map.get(accession.id));
this.setState({ accessions: data, isEmpty: false });
})
.catch((e) => {
console.log('Api call failed: ', e);
});
};
private toggleRowSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const { selected } = this.state;
const { id } = (e.currentTarget as HTMLElement).dataset;
const updated = selected.filter((selectedId) => selectedId !== +id);
if (updated.length === selected.length) {
updated.push(+id);
}
this.setState({ selected: updated });
};
private removeItems = () => {
const { selected, accessions } = this.state;
LocalStorageCart.removeFromCart(selected);
const accessionsUpdated = { ...accessions, content: accessions.content.filter((acc) => !selected.some((id) => id === acc.id)) };
const isEmpty = accessionsUpdated.content.length === 0;
this.setState({ selected: [], accessions: isEmpty ? null : accessionsUpdated, isEmpty, isAllSelected: false });
};
private onToggleAll = () => {
this.setState((prevState) => ({
isAllSelected: !prevState.isAllSelected,
selected: !prevState.isAllSelected ? prevState.accessions.content.map((acc) => acc.id) : [],
}));
};
public render() {
const { accessions, selected, isEmpty, isAllSelected } = this.state;
const { t } = this.props;
const selectedIds = new Set();
selected.forEach((id) => selectedIds.add(id));
return (
<>
<h1 className="d-flex justify-content-between align-items-center">
{ t('cart.title') }
{ selected.length !== 0 &&
<button type="button" className="btn btn-primary" onClick={ this.removeItems }>
{ t('cart.removeFromCart') }
</button>
}
</h1>
{ isEmpty && <div>{ t('cart.isEmpty') }</div> }
{ accessions && (
<table className="table table-striped">
<thead className="thead-dark">
<tr>
<th>
<input
type="checkbox"
name="select-all"
checked={ isAllSelected }
onChange={ this.onToggleAll }
className="align-middle"
/>
</th>
<th>{ t('accession.model') }</th>
<th>{ t('accession.accessionName') }</th>
<th>{ t('accession.countryOfOrigin') }</th>
</tr>
</thead>
<tbody>
{ accessions.content.map((a, i) => (
<tr key={ a.id } className={ selectedIds.has(a.id) ? 'table-primary' : '' }>
<td>
<input
type="checkbox"
name={ `checkbox-${a.id}-${i}` }
data-id={ a.id }
checked={ selectedIds.has(a.id) }
onChange={ this.toggleRowSelect }
className="align-middle"
/>
</td>
<td><Link to={ `/a/${a.id}` }>{ a.accessionNumber }</Link></td>
<td>{ a.accessionName }</td>
<td>{ a.countryOfOrigin && a.countryOfOrigin.name }</td>
</tr>
)) }
</tbody>
</table>
) }
{ !isEmpty && !accessions && <div>{t('common.loading')}...</div> }
</>
);
};
}
export default withTranslation()(AccessionListPage)
......@@ -2,8 +2,9 @@ import React from 'react';
import { Link } from 'react-router-dom';
import {WithTranslation, withTranslation} from "react-i18next";
// interface INavigation extends React.ClassAttributes<any> {}
class Navigation extends React.Component<WithTranslation, any> {
interface INavigation extends React.ClassAttributes<any>, WithTranslation {}
class Navigation extends React.Component<INavigation, any> {
public render() {
const { t } = this.props;
......@@ -14,6 +15,7 @@ class Navigation extends React.Component<WithTranslation, any> {
<ul style={ { display: 'flex', listStyle: 'none' } }>
<li style={ { marginRight: '20px' } }><Link to="/">{t("nav.home")}</Link></li>
<li style={ { marginRight: '20px' } }><Link to="/api-info">{t("nav.apiInfo")}</Link></li>
<li style={ { marginRight: '20px' } }><Link to="/cart/">{t('nav.cart')}</Link></li>
</ul>
</nav>
</header>
......
......@@ -31,3 +31,39 @@ export function checkAccessTokens(apiUrl: string, clientId: string, clientSecret
return applicationLogin();
}
export class LocalStorageCart {
public static LS_KEY = 'ui-embedded-cart';
public static getCartItemsLS = () => JSON.parse(localStorage.getItem(LocalStorageCart.LS_KEY));
public static getCartItemsIds = () => {
const localStorageCart = LocalStorageCart.getCartItemsLS();
return localStorageCart && localStorageCart.length ? localStorageCart.map((id) => +id) : [];
};
public static addToCart = (items: number[]) => {
// console.log('add to cart: ', items);
const cartIds = LocalStorageCart.getCartItemsIds();
if (cartIds.length === 0) {
return localStorage.setItem(LocalStorageCart.LS_KEY, JSON.stringify(items));
}
const idsSet = new Set(cartIds);
items.forEach((id) => {
if (idsSet.has(id)) {
return;
}
idsSet.add(id);
});
const cart = [];
idsSet.forEach((id) => cart.push(id));
localStorage.setItem(LocalStorageCart.LS_KEY, JSON.stringify(cart));
};
public static removeFromCart = (ids: number[]) => {
const cartIds = LocalStorageCart.getCartItemsIds();
const updatedCartItems = cartIds.filter((cartId) => !ids.some((idToRemove) => cartId === idToRemove));
localStorage.setItem(LocalStorageCart.LS_KEY, JSON.stringify(updatedCartItems));
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment