Skip to content
This repository has been archived by the owner on Sep 1, 2022. It is now read-only.

Sohail Choudhry's PR #27

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"presets": ["env", "react"]
"presets": ["@babel/preset-env", "@babel/preset-react"]
}
20 changes: 20 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"ecmaFeatures": {
"modules": true,
"spread": true,
"restParams": true
},
"env": {
"browser": true,
"node": true,
"es6": true
},
"rules": {
"no-unused-vars": 1,
"no-undef": 2
},
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2018
}
}
29 changes: 23 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,37 @@
"private": false,
"scripts": {
"start": "webpack-dev-server --mode development --open",
"build": "webpack --mode production"
"build": "webpack --mode production",
"lint": "eslint .",
"test": "jest",
"test:watch": "npm run test -- --watch"
},
"dependencies": {
"@babel/core": "^7.8.3",
"@babel/preset-env": "^7.8.3",
"@babel/preset-react": "^7.8.3",
"@material-ui/core": "^4.8.3",
"@material-ui/icons": "^4.5.1",
"babel-preset-airbnb": "^4.4.0",
"cross-fetch": "^3.0.4",
"react": "^16.3.1",
"react-dom": "^16.3.1"
"react-dom": "^16.3.1",
"react-redux": "^7.1.3",
"react-router": "^5.1.2",
"react-router-dom": "^5.1.2",
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"
},
"devDependencies": {
"babel-core": "6.26.*",
"babel-loader": "7.1.*",
"babel-preset-env": "1.7.0",
"babel-preset-react": "6.24.*",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"css-loader": "2.1.*",
"eslint": "^6.8.0",
"html-loader": "0.5.*",
"html-webpack-plugin": "3.2.*",
"jest": "^24.9.0",
"react-test-renderer": "^16.12.0",
"style-loader": "0.23.*",
"webpack": "4.29.*",
"webpack-cli": "3.2.*",
Expand Down
38 changes: 30 additions & 8 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
import React from 'react';
import ReactDOM from 'react-dom';
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import logger from "redux-logger";
import thunk from "redux-thunk";
import { createStore, applyMiddleware } from "redux";
import { HashRouter as Router, Route } from "react-router-dom";

import Header from './Header.jsx';
import Header from "./Header.jsx";
import ActivityFeed from "./activity-feed/ActivityFeedContainer";
import CallDetail from "./call-detail/CallDetailContainer";
import rootReducer from "./reducers";

const middleware = applyMiddleware(thunk, logger);
const store = createStore(rootReducer, middleware);

const App = () => {
return (
<div className='container'>
<Header/>
<div className="container-view">Some activities should be here</div>
</div>
<Provider store={store}>
<div className="container">
<Header />
<div className="container-view">
<Router>
<Route exact path="/" component={ActivityFeed} />
<Route
exact
path="/call-detail/:activityId"
component={CallDetail}
/>
</Router>
</div>
</div>
</Provider>
);
};

ReactDOM.render(<App/>, document.getElementById('app'));
ReactDOM.render(<App />, document.getElementById("app"));

export default App;
28 changes: 28 additions & 0 deletions src/activity-feed/ActivityFeedComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React, { useEffect } from "react";
import ActivityItem from "./ActivityItemComponent.jsx";

const ActivityFeedComponent = props => {
const { activities, loading, fetchActivities } = props;

useEffect(() => {
fetchActivities();
}, []);

if (loading) {
return <p>Loading...</p>;
}

return (
<div className="page activity-feed">
<p className="page-title">Activity Feed</p>
<div>
{activities &&
activities.map(activity => {
return <ActivityItem activity={activity} key={activity.id} />;
})}
</div>
</div>
);
};

export default ActivityFeedComponent;
27 changes: 27 additions & 0 deletions src/activity-feed/ActivityFeedContainer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { connect } from "react-redux";
import ActivityFeedComponent from "./ActivityFeedComponent.jsx";

import { activityFeedOperations } from "./duck";

const mapStateToProps = state => {
const { activities, loading } = state.activityFeed;
return {
activities,
loading
};
};

const mapDispatchToProps = dispatch => {
const fetchActivities = () => {
dispatch(activityFeedOperations.fetchActivities());
};

return { fetchActivities };
};

const ActivityFeedContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ActivityFeedComponent);

export default ActivityFeedContainer;
61 changes: 61 additions & 0 deletions src/activity-feed/ActivityItemComponent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from "react";
import { withRouter } from "react-router-dom";

import CallMissedOutgoingIcon from "@material-ui/icons/CallMissedOutgoing";
import CallMissedIcon from "@material-ui/icons/CallMissed";
import CallMadeIcon from "@material-ui/icons/CallMade";
import CallReceivedIcon from "@material-ui/icons/CallReceived";
import NavigateNextIcon from "@material-ui/icons/NavigateNext";

export const ActivityItemComponent = props => {
const { activity, history } = props;

return (
<div className="activity-feed-item">
<div className="item-left">{getIcon(activity)}</div>
<div className="item-center">
<div className="activity-feed-item-number">
{activity.direction === "inbound" ? activity.from : activity.to}
</div>
<div className="activity-feed-item-info">{getInfoText(activity)}</div>
</div>
<div className="item-right">
<a onClick={e => goToCallDetail(e, activity.id, history)}>
<NavigateNextIcon />
</a>
</div>
</div>
);
};

const getInfoText = activity => {
const direction = activity.direction === "inbound" ? "on" : "from";
const callStatus =
activity.call_type === "answered" ? "called" : "tried to call";

return `${callStatus} ${direction} ${activity.via}`;
};

const getIcon = activity => {
if (activity.direction === "inbound") {
return activity.call_type === "answered" ? (
<CallReceivedIcon />
) : (
<CallMissedIcon />
);
} else {
return activity.call_type === "answered" ? (
<CallMadeIcon />
) : (
<CallMissedOutgoingIcon />
);
}
};

const goToCallDetail = (e, activityId, history) => {
e.preventDefault();
const newLocation = `/call-detail/${activityId}`;
history.push(newLocation);
};

export default withRouter(ActivityItemComponent);
23 changes: 23 additions & 0 deletions src/activity-feed/ActivityItemComponent.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from "react";
import renderer from "react-test-renderer";
import { ActivityItemComponent } from "./ActivityItemComponent";
describe("ActivityItemComponent", () => {
test("snapshot renders", () => {
const activity = {
id: 7834,
created_at: "2018-04-19T09:38:41.000Z",
direction: "outbound",
from: "Pierre-Baptiste Béchu",
to: "06 46 62 12 33",
via: "NYC Office",
duration: "120",
is_archived: false,
call_type: "missed"
};
const component = renderer.create(
<ActivityItemComponent activity={activity} />
);
let tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`ActivityItemComponent snapshot renders 1`] = `
<div
className="activity-feed-item"
>
<div
className="item-left"
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<defs>
<path
d="M24 24H0V0h24v24z"
id="a"
/>
</defs>
<path
d="M3 8.41l9 9 7-7V15h2V7h-8v2h4.59L12 14.59 4.41 7 3 8.41z"
/>
</svg>
</div>
<div
className="item-center"
>
<div
className="activity-feed-item-number"
>
06 46 62 12 33
</div>
<div
className="activity-feed-item-info"
>
tried to call from NYC Office
</div>
</div>
<div
className="item-right"
>
<a
onClick={[Function]}
>
<svg
aria-hidden="true"
className="MuiSvgIcon-root"
focusable="false"
role="presentation"
viewBox="0 0 24 24"
>
<path
d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"
/>
</svg>
</a>
</div>
</div>
`;
19 changes: 19 additions & 0 deletions src/activity-feed/duck/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import types from "./types.js";

const requestActivities = () => {
return {
type: types.REQUEST_ACTIVITIES
};
};

const receiveActivities = data => {
return {
type: types.RECEIVE_ACTIVITIES,
activities: data
};
};

export default {
requestActivities,
receiveActivities
};
4 changes: 4 additions & 0 deletions src/activity-feed/duck/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import activityFeedReducer from "./reducers";
export { default as activityFeedOperations } from "./operations";
export { default as activityFeedTypes } from "./types";
export default activityFeedReducer;
23 changes: 23 additions & 0 deletions src/activity-feed/duck/operations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fetch from "cross-fetch";
import Creators from "./actions";

const requestActivitiesAction = Creators.requestActivities;
const receiveActivitiesAction = Creators.receiveActivities;

// 'fetchActivities()' will fetch the activities
const fetchActivities = () => {
return dispatch => {
// Dispatching this action will toggle the 'loading'
// flag in the store, so that the UI can show a loading icon.
dispatch(requestActivitiesAction());
return fetch("https://aircall-job.herokuapp.com/activities")
.then(response => response.json())
.then(json => {
dispatch(receiveActivitiesAction(json));
});
};
};

export default {
fetchActivities
};
Loading