Commit ebbcc440 authored by Benjamin Bouvier's avatar Benjamin Bouvier
Browse files

Project Minefield: use Redux in the client;

parent 8bbe4c1c
......@@ -180,7 +180,7 @@
- 2
# disallow use of labeled statements
no-labels:
- 2
- 0
# disallow unnecessary nested blocks
no-lone-blocks:
- 2
......@@ -425,7 +425,7 @@
# specify the maximum depth callbacks can be nested
max-nested-callbacks:
- 2
- 1
- 2
# limits the number of parameters that can be used in the function declaration.
max-params:
- 0
......
......@@ -25,6 +25,13 @@
jsx-quotes:
- 2
- 'prefer-double'
# Deactives arrow body style; it's stupid when returning a single
# object.
arrow-body-style:
- 0
# Can't enforce when enforcing a functions ordering.
no-use-before-define:
- 0
# Prevent missing displayName in a React component definition
# TODO: set to 2 when one module by file
'react/display-name':
......@@ -158,9 +165,10 @@
# Ensure named imports correspond to a named export in the remote file.
'import/named':
- 2
# Ensure a default export is present, given a default import.
# Ensure a default export is present, given a default import:
# doesn't work when importing everything: `import * from module`
'import/default':
- 2
- 0
# Ensure imported namespaces contain dereferenced properties as they are dereferenced.
'import/namespace':
- 2
......
import React from 'react';
import { connect } from 'react-redux';
import { Actions, store, State } from '../../store';
import { translate as $t, NONE_CATEGORY_ID } from '../../helpers';
import { get, actions } from '../../store';
import { translate as $t } from '../../helpers';
import CategoryListItem from './item';
import CreateForm from './create-form';
export default class CategoryList extends React.Component {
class CategoryList extends React.Component {
constructor(props) {
super(props);
this.state = {
showForm: false,
categories: store.getCategories()
};
this.listener = this.listener.bind(this);
this.handleSave = this.handleSave.bind(this);
this.handleShowForm = this.handleShowForm.bind(this);
}
listener() {
this.setState({
categories: store.getCategories()
});
}
componentDidMount() {
store.on(State.categories, this.listener);
}
componentWillUnmount() {
store.removeListener(State.categories, this.listener);
}
handleShowForm(e) {
e.preventDefault();
this.setState({
......@@ -53,7 +40,7 @@ export default class CategoryList extends React.Component {
color
};
Actions.createCategory(category);
this.props.createCategory(category);
this.refs.createform.clearLabel();
this.setState({
......@@ -63,9 +50,15 @@ export default class CategoryList extends React.Component {
}
render() {
let items = this.state.categories
.filter(cat => cat.id !== NONE_CATEGORY_ID)
.map(cat => <CategoryListItem cat={ cat } key={ cat.id } />);
let items = this.props.categories.map(cat =>
<CategoryListItem
cat={ cat }
categories={ this.props.categories }
updateCategory={ this.props.updateCategory }
deleteCategory={ this.props.deleteCategory }
key={ cat.id }
/>);
let maybeForm = (
this.state.showForm ?
......@@ -118,3 +111,23 @@ export default class CategoryList extends React.Component {
);
}
}
const Export = connect(state => {
return {
categories: get.categoriesButNone(state)
};
}, dispatch => {
return {
createCategory(category) {
actions.createCategory(dispatch, category);
},
updateCategory(former, newer) {
actions.updateCategory(dispatch, former, newer);
},
deleteCategory(former, replaceById) {
actions.deleteCategory(dispatch, former, replaceById);
}
};
})(CategoryList);
export default Export;
import React from 'react';
import { Actions, store } from '../../store';
import { translate as $t, NONE_CATEGORY_ID } from '../../helpers';
import CreateForm from './create-form';
......@@ -26,7 +25,7 @@ export default class CategoryListItem extends React.Component {
color
};
Actions.updateCategory(this.props.cat, category);
this.props.updateCategory(this.props.cat, category);
this.setState({
editMode: false
......@@ -53,7 +52,7 @@ export default class CategoryListItem extends React.Component {
handleDelete() {
let replaceCategory = this.refs.replacement.value;
Actions.deleteCategory(this.props.cat, replaceCategory);
this.props.deleteCategory(this.props.cat, replaceCategory);
}
render() {
......@@ -70,8 +69,8 @@ export default class CategoryListItem extends React.Component {
/>);
}
let replacementOptions = store.getCategories()
.filter(cat => (cat.id !== c.id && cat.id !== NONE_CATEGORY_ID))
let replacementOptions = this.props.categories
.filter(cat => cat.id !== c.id)
.map(cat =>
<option
key={ cat.id }
......
/* globals Dygraph: false */
import React from 'react';
import { debug } from '../../helpers';
import ChartComponent from './chart-base';
import { round2 } from './helpers';
function createChartBalance(chartId, account, operations) {
if (account === null) {
debug('ChartComponent: no account');
return;
}
let ops = operations.slice().sort((a, b) => +a.date - +b.date);
function makeKey(date) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
let opmap = new Map;
// Fill all dates.
const DAY = 1000 * 60 * 60 * 24;
let firstDate = ops.length ? +ops[0].date : Date.now();
firstDate = (firstDate / DAY | 0) * DAY;
let today = (Date.now() / DAY | 0) * DAY;
for (; firstDate <= today; firstDate += DAY) {
opmap.set(makeKey(new Date(firstDate)), 0);
}
// Date (day) -> cumulated sum of amounts for this day (scalar).
for (let o of ops) {
let key = makeKey(o.date);
if (opmap.has(key))
opmap.set(key, opmap.get(key) + o.amount);
}
let balance = account.initialAmount;
let csv = 'Date,Balance\n';
for (let [date, amount] of opmap) {
balance += amount;
csv += `${date},${round2(balance)}\n`;
}
/* eslint-disable no-new */
// Create the chart
new Dygraph(document.querySelector(chartId), csv);
/* eslint-enable no-new */
}
export default class BalanceChart extends ChartComponent {
redraw() {
createChartBalance('#barchart', this.props.account, this.props.operations);
}
render() {
return <div id="barchart" style={ { width: '100%' } }></div>;
}
}
......@@ -13,5 +13,4 @@ export default class ChartComponent extends React.Component {
componentDidMount() {
this.redraw();
}
}
export function round2(x) {
return Math.round(x * 100) / 100;
}
/* globals c3: false */
import React from 'react';
import { translate as $t } from '../../helpers';
import ChartComponent from './chart-base';
import { round2 } from './helpers';
function createChartPositiveNegative(chartId, operations) {
function datekey(op) {
let d = op.date;
return `${d.getFullYear()} - ${d.getMonth()}`;
}
const POS = 0, NEG = 1, BAL = 2;
// Month -> [Positive amount, Negative amount, Diff]
let map = new Map;
// Datekey -> Date
let dateset = new Map;
for (let i = 0; i < operations.length; i++) {
let op = operations[i];
let dk = datekey(op);
map.set(dk, map.get(dk) || [0, 0, 0]);
let triplet = map.get(dk);
triplet[POS] += op.amount > 0 ? op.amount : 0;
triplet[NEG] += op.amount < 0 ? -op.amount : 0;
triplet[BAL] += op.amount;
dateset.set(dk, +op.date);
}
// Sort date in ascending order: push all pairs of (datekey, date) in an
// array and sort that array by the second element. Then read that array in
// ascending order.
let dates = Array.from(dateset);
dates.sort((a, b) => a[1] - b[1]);
let series = [];
function addSerie(name, mapIndex) {
let data = [];
for (let j = 0; j < dates.length; j++) {
let dk = dates[j][0];
data.push(round2(map.get(dk)[mapIndex]));
}
let serie = [name].concat(data);
series.push(serie);
}
addSerie($t('client.charts.received'), POS);
addSerie($t('client.charts.spent'), NEG);
addSerie($t('client.charts.saved'), BAL);
let categories = [];
for (let i = 0; i < dates.length; i++) {
let date = new Date(dates[i][1]);
// Undefined means the default locale
let defaultLocale;
let str = date.toLocaleDateString(defaultLocale, {
year: 'numeric',
month: 'long'
});
categories.push(str);
}
let yAxisLegend = $t('client.charts.amount');
c3.generate({
bindto: chartId,
data: {
columns: series,
type: 'bar'
},
bar: {
width: {
ratio: .5
}
},
axis: {
x: {
type: 'category',
categories
},
y: {
label: yAxisLegend
}
},
grid: {
x: {
show: true
},
y: {
show: true,
lines: [{ value: 0 }]
}
}
});
}
export default class InOutChart extends ChartComponent {
redraw() {
createChartPositiveNegative('#barchart', this.props.operations);
}
render() {
return <div id="barchart" style={ { width: '100%' } }></div>;
}
}
/* globals c3: false, Dygraph: false */
import React from 'react';
import { connect } from 'react-redux';
import { store, State } from '../../store';
import { assert, debug, translate as $t } from '../../helpers';
import { get } from '../../store';
import { assert, translate as $t } from '../../helpers';
import ChartComponent from './chart-base';
import InOutChart from './in-out-chart';
import BalanceChart from './balance-chart';
import OperationsByCategoryChart from './operations-by-category-chart';
function round2(x) {
return Math.round(x * 100) / 100;
}
// Charts
export function createBarChartAll(operations, barchartId) {
function datekey(op) {
let d = op.date;
return `${d.getFullYear()}-${d.getMonth()}`;
}
// Category -> {Month -> [Amounts]}
let map = new Map;
// Category -> color
let colorMap = {};
// Datekey -> Date
let dateset = new Map;
for (let i = 0, size = operations.length; i < size; i++) {
let op = operations[i];
let c = store.getCategoryFromId(op.categoryId);
map.set(c.title, map.get(c.title) || {});
let categoryDates = map.get(c.title);
let dk = datekey(op);
(categoryDates[dk] = categoryDates[dk] || []).push(op.amount);
dateset.set(dk, +op.date);
colorMap[c.title] = colorMap[c.title] || c.color;
}
// Sort date in ascending order: push all pairs of (datekey, date) in an
// array and sort that array by the second element. Then read that array in
// ascending order.
let dates = Array.from(dateset);
dates.sort((a, b) => a[1] - b[1]);
let series = [];
for (let c of map.keys()) {
let data = [];
for (let j = 0; j < dates.length; j++) {
let dk = dates[j][0];
let values = map.get(c)[dk] = map.get(c)[dk] || [];
data.push(round2(values.reduce((a, b) => a + b, 0)));
}
data = [c].concat(data);
series.push(data);
}
let categories = [];
for (let i = 0; i < dates.length; i++) {
let date = new Date(dates[i][1]);
// Undefined means the default locale
let defaultLocale;
let str = date.toLocaleDateString(defaultLocale, {
year: 'numeric',
month: 'long'
});
categories.push(str);
}
let yAxisLegend = $t('client.charts.amount');
return c3.generate({
bindto: barchartId,
data: {
columns: series,
type: 'bar',
colors: colorMap
},
bar: {
width: {
ratio: .5
}
},
axis: {
x: {
type: 'category',
categories
},
y: {
label: yAxisLegend
}
},
grid: {
x: {
show: true
},
y: {
show: true,
lines: [{ value: 0 }]
}
}
});
}
export function createPieChartAll(operations, chartId) {
let catMap = new Map;
// categoryId -> [val1, val2, val3]
for (let op of operations) {
let catId = op.categoryId;
let arr = catMap.has(catId) ? catMap.get(catId) : [];
arr.push(op.amount);
catMap.set(catId, arr);
}
// [ [categoryName, val1, val2], [anotherCategoryName, val3, val4] ]
let series = [];
// {label -> color}
let colorMap = {};
for (let [catId, valueArr] of catMap) {
let c = store.getCategoryFromId(catId);
series.push([c.title].concat(valueArr));
colorMap[c.title] = c.color;
}
return c3.generate({
bindto: chartId,
data: {
columns: series,
type: 'pie',
colors: colorMap
},
tooltip: {
format: {
value(value, ratio) {
return `${round2(ratio * 100)}% (${Math.abs(round2(value))})`;
}
}
}
});
}
export function createChartBalance(chartId, account, operations) {
if (account === null) {
debug('ChartComponent: no account');
return;
}
let ops = operations.slice().sort((a, b) => +a.date - +b.date);
function makeKey(date) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`;
}
let opmap = new Map;
// Fill all dates
const DAY = 1000 * 60 * 60 * 24;
let firstDate = ops.length ? +ops[0].date : Date.now();
firstDate = (firstDate / DAY | 0) * DAY;
let today = (Date.now() / DAY | 0) * DAY;
for (; firstDate <= today; firstDate += DAY) {
opmap.set(makeKey(new Date(firstDate)), 0);
}
// Date (day) -> cumulated sum of amounts for this day (scalar)
for (let o of ops) {
let key = makeKey(o.date);
opmap.set(key, opmap.get(key) + o.amount);
}
let balance = account.initialAmount;
let csv = 'Date,Balance\n';
for (let [date, amount] of opmap) {
balance += amount;
csv += `${date},${round2(balance)}\n`;
}
/* eslint-disable no-new */
// Create the chart
new Dygraph(document.querySelector(chartId), csv);
/* eslint-enable no-new */
}
export function createChartPositiveNegative(chartId, operations) {
function datekey(op) {
let d = op.date;
return `${d.getFullYear()} - ${d.getMonth()}`;
}
const POS = 0, NEG = 1, BAL = 2;
// Month -> [Positive amount, Negative amount, Diff]
let map = new Map;
// Datekey -> Date