Skip to content

Commit 99b24f6

Browse files
save progress
1 parent adcaa20 commit 99b24f6

File tree

13 files changed

+207
-46
lines changed

13 files changed

+207
-46
lines changed

frontend/src/__tests__/components/UsersDataGrid.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { fireEvent, render, waitFor } from '@testing-library/react';
22
import { UsersDataGrid } from 'components/UsersDataGrid';
3+
import moment from 'moment';
34

45
describe('UsersDataGrid', () => {
6+
const USERS_DATES = [
7+
moment('1990-01-01', 'YYYY-MM-DD').toDate(),
8+
moment('1970-11-18', 'YYYY-MM-DD').toDate(),
9+
];
510
const DEFAULT_PROPS = {
611
users: {
712
data: [
@@ -10,14 +15,14 @@ describe('UsersDataGrid', () => {
1015
first_name: 'John',
1116
last_name: 'Doe',
1217
country_name: 'USA',
13-
age: 25,
18+
date_of_birth: USERS_DATES[0].getTime(),
1419
},
1520
{
1621
id: 2,
1722
first_name: 'Jane',
1823
last_name: 'Doe',
1924
country_name: 'Canada',
20-
age: 23,
25+
date_of_birth: USERS_DATES[1].getTime(),
2126
},
2227
],
2328
loaded: true,
@@ -28,7 +33,7 @@ describe('UsersDataGrid', () => {
2833
totalPages: 1,
2934
perPage: 10,
3035
},
31-
fetchUsers: async () => { },
36+
usersUpdates: async () => { },
3237
setPage: i => { },
3338
setTotalPages: i => { },
3439
setPerPage: i => { },
@@ -177,4 +182,12 @@ describe('UsersDataGrid', () => {
177182
});
178183
});
179184
});
185+
186+
it('should show users ages in the table', () => {
187+
const { container } = render(<UsersDataGrid {...DEFAULT_PROPS} />);
188+
const table = container.querySelector('table');
189+
USERS_DATES.forEach(birthDate => {
190+
expect(table).toHaveTextContent(moment().diff(birthDate, 'years'));
191+
});
192+
});
180193
});

frontend/src/__tests__/services/UsersService.js

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,37 +12,50 @@ describe('Users Service', () => {
1212
});
1313
afterAll(() => server.close());
1414

15+
function makeDate(date) {
16+
const dateOfBirth = moment(date, 'DD-MM-YYYY').toDate().getTime(); // JS timestamp not rounded
17+
const PHPTimestamp = Math.floor(dateOfBirth / 1000); // PHP timestamp rounded
18+
return {
19+
PHPTimestamp,
20+
JSTimestamp: PHPTimestamp * 1000,
21+
JSDate: new Date(PHPTimestamp * 1000),
22+
};
23+
}
24+
1525
it('should fetch users', async () => {
16-
const users = [
17-
{ id: 1, first_name: 'John', last_name: 'Doe', country_id: 1, country_name: 'USA' },
18-
{ id: 2, first_name: 'Jane', last_name: 'Doe', country_id: 1, country_name: 'USA' },
19-
];
26+
const dateOfBirth = makeDate('01-02-2000');
27+
function users(timestamp) { // populate users with the same date of birth
28+
return [
29+
{ id: 1, first_name: 'John', last_name: 'Doe', country_id: 1, country_name: 'USA', date_of_birth: timestamp },
30+
{ id: 2, first_name: 'Jane', last_name: 'Doe', country_id: 1, country_name: 'USA', date_of_birth: timestamp },
31+
];
32+
}
2033
const totalPages = 2;
2134
server.use(
22-
rest.get('/api/users', (req, res, ctx) => res(ctx.json({ users, totalPages })))
35+
rest.get('/api/users', (req, res, ctx) => res(ctx.json({ users: users(dateOfBirth.PHPTimestamp), totalPages })))
2336
);
2437
const response = await UsersService.getUsers();
25-
expect(response).toEqual({ users, totalPages });
38+
expect(response).toEqual({ users: users(dateOfBirth.JSTimestamp), totalPages });
2639
});
2740

2841
it('should fetch a user data and convert to the correct format', async () => {
29-
const dateOfBirth = moment('01-02-2000', 'DD-MM-YYYY').toDate();
42+
const dateOfBirth = makeDate('01-02-2000');
3043
server.use(
3144
rest.get('/api/users/13', (req, res, ctx) => {
3245
return res(ctx.json({
33-
id: 13, first_name: 'John', last_name: 'Doe', country_id: 1, country_name: 'USA', date_of_birth: dateOfBirth.getTime(),
46+
id: 13, first_name: 'John', last_name: 'Doe', country_id: 1, country_name: 'USA', date_of_birth: dateOfBirth.PHPTimestamp,
3447
}));
3548
})
3649
);
3750

3851
const response = await UsersService.getUser(13);
3952
expect(response).toEqual({
40-
id: 13, first_name: 'John', last_name: 'Doe', country_id: 1, country_name: 'USA', date_of_birth: dateOfBirth,
53+
id: 13, first_name: 'John', last_name: 'Doe', country_id: 1, country_name: 'USA', date_of_birth: dateOfBirth.JSDate,
4154
});
4255
});
4356

4457
it('should send a request to create a user in the correct format', async () => {
45-
const dateOfBirth = moment('01-02-2000', 'DD-MM-YYYY').toDate();
58+
const dateOfBirth = makeDate('21-02-2000');
4659
let request;
4760
server.use(
4861
rest.post('/api/users', async (req, res, ctx) => {
@@ -51,16 +64,16 @@ describe('Users Service', () => {
5164
})
5265
);
5366
await UsersService.createUser({
54-
firstName: 'John', lastName: 'Doe', country: 'USA', dateOfBirth: dateOfBirth.getTime(),
67+
firstName: 'John', lastName: 'Doe', country: 'USA', dateOfBirth: dateOfBirth.JSTimestamp,
5568
});
5669

5770
expect(request).toEqual({
58-
first_name: 'John', last_name: 'Doe', country_name: 'USA', date_of_birth: dateOfBirth.getTime(),
71+
first_name: 'John', last_name: 'Doe', country_name: 'USA', date_of_birth: dateOfBirth.PHPTimestamp,
5972
});
6073
});
6174

6275
it('should send a request to edit a user in the correct format', async () => {
63-
const dateOfBirth = moment('01-02-2000', 'DD-MM-YYYY').toDate();
76+
const dateOfBirth = makeDate('21-02-2000');
6477
let request;
6578
server.use(
6679
rest.patch('/api/users/13', async (req, res, ctx) => {
@@ -69,11 +82,25 @@ describe('Users Service', () => {
6982
})
7083
);
7184
await UsersService.updateUser(13, {
72-
firstName: 'John', lastName: 'Doe', country: 'USA', dateOfBirth: dateOfBirth.getTime(),
85+
firstName: 'John', lastName: 'Doe', country: 'USA', dateOfBirth: dateOfBirth.JSTimestamp,
7386
});
7487

7588
expect(request).toEqual({
76-
first_name: 'John', last_name: 'Doe', country_name: 'USA', date_of_birth: dateOfBirth.getTime(),
89+
first_name: 'John', last_name: 'Doe', country_name: 'USA', date_of_birth: dateOfBirth.PHPTimestamp,
7790
});
7891
});
92+
93+
it('should receive a timestamp in PHP format but return it in JS format', async () => {
94+
const dateOfBirth = makeDate('16-02-2000');
95+
server.use(
96+
rest.get('/api/users/13', (req, res, ctx) => {
97+
return res(ctx.json({
98+
id: 13, first_name: 'John', last_name: 'Doe', country_id: 1, country_name: 'USA', date_of_birth: dateOfBirth.PHPTimestamp,
99+
}));
100+
})
101+
);
102+
103+
const response = await UsersService.getUser(13);
104+
expect(response.date_of_birth).toEqual(dateOfBirth.JSDate);
105+
});
79106
});

frontend/src/components/UsersChart.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { Component } from 'react';
22
import { connect } from 'react-redux';
3-
import { fetchCountries } from 'store/countriesSlice';
3+
import { countriesUpdates } from 'store/events';
44
import ReactEcharts from 'echarts-for-react';
55

66
function getPieChartOption(countries) {
@@ -33,10 +33,14 @@ function getPieChartOption(countries) {
3333
}
3434

3535
class UsersChart extends Component {
36-
async componentDidMount() {
37-
await this.props.fetchCountries();
36+
componentDidMount() {
37+
this.props.countriesUpdates(true);
3838
}
39-
39+
40+
componentWillUnmount() {
41+
this.props.countriesUpdates(false);
42+
}
43+
4044
render() {
4145
const { countries } = this.props;
4246
if (!countries.loaded) {
@@ -57,7 +61,7 @@ export default connect(
5761
state => ({
5862
countries: state.countries
5963
}),
60-
{ fetchCountries }
64+
{ countriesUpdates }
6165
)(UsersChart);
6266

6367
// for testing

frontend/src/components/UsersDataGrid.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@ import storeActions from 'store/actions';
55
import UsersTable from './UsersTable';
66

77
export class UsersDataGrid extends Component {
8-
async componentDidMount() {
9-
await this.props.fetchUsers();
8+
componentDidMount() {
9+
this.props.usersUpdates(true);
10+
}
11+
12+
componentWillUnmount() {
13+
this.props.usersUpdates(false);
1014
}
1115

1216
paginationArea() {
@@ -102,9 +106,9 @@ export class UsersDataGrid extends Component {
102106
}
103107

104108
function requiredActions() {
105-
const { users: { fetchUsers }, UI: { setPage, setTotalPages, setPerPage }, actions: { AddUserDialog, RemoveUserDialog, ViewUserDialog } } = storeActions;
109+
const { events: { usersUpdates }, UI: { setPage, setPerPage }, actions: { AddUserDialog, RemoveUserDialog, ViewUserDialog } } = storeActions;
106110
return {
107-
fetchUsers, setPage, setTotalPages, setPerPage,
111+
usersUpdates, setPage, setPerPage,
108112
addUser: AddUserDialog, removeUser: RemoveUserDialog, viewUser: ViewUserDialog,
109113
};
110114
}

frontend/src/components/UsersTable.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import React from "react";
22
import { useTable } from "react-table";
33

4+
function calculateAge(dateOfBirth) {
5+
const today = new Date();
6+
const birthDate = new Date(dateOfBirth);
7+
let age = today.getFullYear() - birthDate.getFullYear();
8+
const month = today.getMonth() - birthDate.getMonth();
9+
if (month < 0 || (month === 0 && today.getDate() < birthDate.getDate())) {
10+
age--;
11+
}
12+
age = age ?? 0;
13+
// force cast to string
14+
return age + "";
15+
}
16+
417
export default function UsersTable({ users, viewUser, removeUser }) {
518
function makeHandler(f) {
619
return (id) => {
@@ -26,7 +39,7 @@ export default function UsersTable({ users, viewUser, removeUser }) {
2639
},
2740
{
2841
Header: 'Age',
29-
accessor: 'age',
42+
Cell: ({ row: { original: { date_of_birth } } }) => <span>{calculateAge(date_of_birth)}</span>
3043
},
3144
{
3245
Header: () => <span className='float-end'>Actions</span>,

frontend/src/services/UsersService.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,18 @@ import axios from 'axios';
33
import { API_URL } from 'config';
44

55
export async function getUsers(params) {
6-
const response = await axios.get(`${API_URL}/users`, { params: params });
7-
return response.data;
6+
const { data: { users, ...rest } } = await axios.get(`${API_URL}/users`, { params: params });
7+
return {
8+
// convert PHP timestamp to JS timestamp, which is in milliseconds
9+
users: users.map(user => {
10+
const { date_of_birth, ...rest } = user;
11+
return {
12+
...rest,
13+
date_of_birth: date_of_birth * 1000
14+
};
15+
}),
16+
...rest
17+
};
818
};
919

1020
function _prepareUser(user) {
@@ -28,7 +38,8 @@ function _prepareUser(user) {
2838
first_name: firstName,
2939
last_name: lastName,
3040
country_name: country,
31-
date_of_birth: dateOfBirth
41+
// convert date JS timestamp to PHP timestamp, which is in seconds
42+
date_of_birth: Math.floor(dateOfBirth / 1000)
3243
};
3344
}
3445

@@ -53,7 +64,7 @@ export async function getUser(userId) {
5364
const { date_of_birth, ...user } = response.data;
5465
return {
5566
...user,
56-
date_of_birth: new Date(date_of_birth)
67+
date_of_birth: new Date(date_of_birth * 1000)
5768
};
5869
};
5970

@@ -66,4 +77,5 @@ export async function updateUser(userId, user) {
6677
return response.data;
6778
};
6879

69-
export default { getUsers, createUser, removeUser, getUser, updateUser };
80+
const UsersService = { getUsers, createUser, removeUser, getUser, updateUser };
81+
export default UsersService;

frontend/src/store/actions.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { actions as countriesActions } from 'store/countriesSlice';
44
import { actions as UIActions } from 'store/UISlice';
55
import { actions as notificationsActions } from 'store/notificationsSlice';
66
import { actions as appActions } from 'store/AppActions';
7+
import { actions as eventsActions } from 'store/events';
78
import { push } from 'redux-first-history';
89

910
const actions = {
@@ -15,5 +16,6 @@ const actions = {
1516
navigateTo: push
1617
},
1718
actions: appActions,
19+
events: eventsActions,
1820
};
1921
export default actions;

frontend/src/store/events.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// events slice that stores how many listeners are subscribed to each event (usersUpdates, countriesUpdates)
2+
import { createAction, createSlice } from "@reduxjs/toolkit";
3+
4+
export const eventsSlice = createSlice({
5+
name: 'events',
6+
initialState: {
7+
usersUpdates: 0,
8+
_usersVersion: null,
9+
countriesUpdates: 0
10+
},
11+
reducers: {
12+
// takes object and a flag (true/false).
13+
usersUpdates: (state, action) => {
14+
const { payload: { subscribe = true } = {} } = action;
15+
state.usersUpdates += subscribe ? 1 : -1;
16+
},
17+
// takes object and a flag (true/false).
18+
countriesUpdates: (state, action) => {
19+
const { payload: { subscribe = true } = {} } = action;
20+
state.countriesUpdates += subscribe ? 1 : -1;
21+
}
22+
}
23+
});
24+
25+
export const actions = {
26+
...eventsSlice.actions,
27+
invalidateAll: createAction("events/invalidateAll")
28+
};
29+
export const { usersUpdates, countriesUpdates, invalidateAll } = actions;

frontend/src/store/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { countriesSlice } from "./countriesSlice";
99
import { notificationsSlice } from "./notificationsSlice";
1010
import { UISlice } from "./UISlice";
1111
import { usersSlice } from "./usersSlice";
12+
import { eventsSlice } from "./events";
13+
14+
// listeners
15+
import { eventsListener } from "./listeners";
1216

1317
function storeWithHistory(reducers, initialState) {
1418
const { routerReducer, routerMiddleware, createReduxHistory } = createReduxHistoryContext({
@@ -21,7 +25,7 @@ function storeWithHistory(reducers, initialState) {
2125
router: routerReducer,
2226
},
2327
preloadedState: initialState,
24-
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(routerMiddleware),
28+
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(routerMiddleware).prepend(eventsListener.middleware),
2529
});
2630
const history = createReduxHistory(store);
2731

@@ -33,7 +37,8 @@ function buildAppStore(preloadedState) {
3337
users: usersSlice,
3438
countries: countriesSlice,
3539
notifications: notificationsSlice,
36-
UI: UISlice
40+
UI: UISlice,
41+
events: eventsSlice,
3742
};
3843

3944
const reducers = {};

0 commit comments

Comments
 (0)