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

Romain Billard application #19

Open
wants to merge 15 commits 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
8 changes: 7 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
{
"presets": ["env", "react"]
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
["@babel/transform-runtime"]
]
}
10 changes: 10 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"parser": "babel-eslint",
"extends": [
"standard",
"standard-react"
],
"env": {
"browser": true
}
}
29 changes: 23 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,32 @@
"build": "webpack --mode production"
},
"dependencies": {
"react": "^16.3.1",
"react-dom": "^16.3.1"
"date-fns": "^2.7.0",
"prop-types": "^15.7.2",
"react": ">=15",
"react-dom": "^16.11.0",
"react-redux": "7.1.3",
"react-router-dom": "5.1.2",
"redux": "4.0.0",
"redux-saga": "^1.1.3",
"styled-components": "^4.4.1"
},
"devDependencies": {
"babel-core": "6.26.*",
"babel-loader": "7.1.*",
"babel-preset-env": "1.7.0",
"babel-preset-react": "6.24.*",
"@babel/core": "^7.7.2",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.7.1",
"@babel/preset-react": "^7.7.0",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"css-loader": "2.1.*",
"eslint": "^6.6.0",
"eslint-config-standard": "^14.1.0",
"eslint-config-standard-react": "^9.2.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-node": "^10.0.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-react": "^7.16.0",
"eslint-plugin-standard": "^4.0.1",
"html-loader": "0.5.*",
"html-webpack-plugin": "3.2.*",
"style-loader": "0.23.*",
Expand Down
35 changes: 23 additions & 12 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,28 @@
import React from 'react';
import ReactDOM from 'react-dom';

import Header from './Header.jsx';
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'
import store from './store'
import Header from './components/Header/index.jsx'
import Activities from './pages/Activities/index.jsx'
import Activity from './pages/Activity/index.jsx'

const App = () => {
return (
<div className='container'>
<Header/>
<div className="container-view">Some activities should be here</div>
</div>
);
};
<Provider store={store}>
<Router>
<div className='container'>
<Header />
<Switch>
<Route path='/:id' component={Activity} />
<Route path='/' component={Activities} />
</Switch>
</div>
</Router>
</Provider>
)
}

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

export default App;
export default App
25 changes: 0 additions & 25 deletions src/Header.jsx

This file was deleted.

13 changes: 13 additions & 0 deletions src/actions/activities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ACTIONS from './index'

export const getActivitiesAction = { type: ACTIONS.GET_ACTIVITIES }

export const setActivitiesAction = list => ({ type: ACTIONS.SET_ACTIVITIES, list })

export const getActivityDetailAction = id => ({ type: ACTIONS.GET_ACTIVITY_DETAIL, id })

export const setActivityDetailAction = detail => ({ type: ACTIONS.SET_ACTIVITY_DETAIL, detail })

export const archiveActivityAction = id => ({ type: ACTIONS.ARCHIVE_ACTIVITY, id })

export const resetArchivesAction = { type: ACTIONS.RESET_ACTIVITIES_ARCHIVES }
8 changes: 8 additions & 0 deletions src/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
GET_ACTIVITIES: 'GET_ACTIVITIES',
SET_ACTIVITIES: 'SET_ACTIVITIES',
GET_ACTIVITY_DETAIL: 'GET_ACTIVITY_DETAIL',
SET_ACTIVITY_DETAIL: 'SET_ACTIVITY_DETAIL',
ARCHIVE_ACTIVITY: 'ARCHIVE_ACTIVITY',
RESET_ACTIVITIES_ARCHIVES: 'RESET_ACTIVITIES_ARCHIVES'
}
6 changes: 6 additions & 0 deletions src/api/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { get, post } from '../helpers/api'

export const fetchActivities = async () => get('activities')
export const fetchActivityDetail = async id => get(`activities/${id}`)
export const archiveActivity = async (id) => post(`activities/${id}`, JSON.stringify({ is_archived: true }))
export const resetActivitiesArchives = async () => get('reset')
57 changes: 57 additions & 0 deletions src/components/ActivityDetail/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import format from 'date-fns/format'
import { archiveActivityAction } from '../../actions/activities'
import ActivityStatusIcon from '../ActivityStatusIcon/index.jsx'
import Button from '../Button/index.jsx'
import * as S from './styles'

/* eslint-disable camelcase */
const ActivityDetail = ({ activity: { id, direction, from, created_at, duration, call_type, via, to, is_archived }, archiveActivity }) => {
const handleArchiveClick = () => {
archiveActivity(id)
}

return (
<S.Activity direction={direction}>
<S.Header>
<S.Status>
<ActivityStatusIcon status={call_type} />
<b>{call_type}</b>
</S.Status>

<b>{direction}</b>
</S.Header>

<S.Details>
<b>{from}</b>
{to && <S.To>{to}</S.To>}

{via && <em>Via {via}</em>}
<em>{format(new Date(created_at), 'P p')}</em>
<em>{duration} seconds</em>
</S.Details>

<S.Archive>
{!is_archived &&
<Button onClick={handleArchiveClick}>Archive</Button>}

{is_archived && <b>ARCHIVED</b>}
</S.Archive>
</S.Activity>
)
}

ActivityDetail.propTypes = {
activity: PropTypes.object.isRequired,
archiveActivity: PropTypes.func.isRequired
}

const mstp = ({ activities: { detail } }) => ({ activity: detail })

const mdtp = (dispatch) => ({
archiveActivity: (id) => dispatch(archiveActivityAction(id))
})

export default connect(mstp, mdtp)(ActivityDetail)
51 changes: 51 additions & 0 deletions src/components/ActivityDetail/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import styled from 'styled-components'

export const To = styled.span`
font-size: 1.2em;
`

export const Status = styled.div`
display: flex;
align-items: center;
`

export const Header = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0 12px;
margin: 0 0 12px;
border-bottom: solid 1px rgba(0, 0, 0, .1);

b {
display: inline-block;
margin-left: 12px;
text-transform: uppercase;
}
`

export const Details = styled.div`
b, em, span {
display: block;
margin-bottom: 0.3em;
}

b {
font-size: 1.3em;
}

em:last-child {
margin-top: 12px;
}
`

export const Archive = styled.div`
margin-top: 24px;
`

export const Activity = styled.section`
margin-top: 24px;
padding: 10px 12px;
border-radius: 8px;
background: ${props => props.direction === 'inbound' ? 'lightgreen' : 'lightgray'};
`
42 changes: 42 additions & 0 deletions src/components/ActivityItem/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
import format from 'date-fns/format'
import * as S from './styles'

/* eslint-disable camelcase */
const ActivityItem = ({ activity: { id, direction, from, created_at, is_archived } }) => {
const callDate = new Date(created_at)

return (
<S.Activity to={`/${id}`}>
<S.CallDirection direction={direction} />

<S.CallDetails>
<S.Details>
<b>{from}</b>
<span>{format(callDate, 'P')}</span>
</S.Details>
<S.Time>
<span>{format(callDate, 'p')}</span>
{is_archived && <span>Archived</span>}
</S.Time>
</S.CallDetails>
</S.Activity>
)
}

ActivityItem.propTypes = {
activity: PropTypes.shape({
id: PropTypes.number.isRequired,
created_at: PropTypes.string.isRequired,
direction: PropTypes.string.isRequired,
from: PropTypes.string.isRequired,
to: PropTypes.string,
via: PropTypes.string.isRequired,
duration: PropTypes.string.isRequired,
is_archived: PropTypes.bool.isRequired,
call_type: PropTypes.string.isRequired
})
}

export default ActivityItem
49 changes: 49 additions & 0 deletions src/components/ActivityItem/styles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import styled from 'styled-components'
import { Link } from 'react-router-dom'

export const CallDirection = styled.div`
width: 12px;
height: 12px;
background: ${props => props.direction === 'outbound' ? 'gray' : 'lightgreen'};
border-radius: 6px;
`

export const CallDetails = styled.div`
display: flex;
justify-content: space-between;
flex: 1;
align-items: center;
`

export const Details = styled.div`
display: flex;
flex-direction: column;

b {
margin-bottom: 3px;
}
`

export const Time = styled.div`
display: flex;
flex-direction: column;

span:not(:last-child) {
display: inline-block;
margin-bottom: 4px;
}
`

export const Activity = styled(Link)`
display: flex;
align-items: center;
padding: 10px 12px;
border-radius: 8px;
background: rgba(0, 0, 0, .04);
text-decoration: none;
color: #000;

${CallDirection} {
margin-right: 12px;
}
`
20 changes: 20 additions & 0 deletions src/components/ActivityStatusIcon/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react'

const ActivityStatusIcon = ({ status }) => {
return (
<div>
{status === 'outgoing' &&
<svg fill='none' height='24' stroke='#000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M23 7V1h-6M16 8l7-7M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z' /></svg>}

{status === 'missed' && <svg fill='none' height='24' stroke='#000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M23 1l-6 6M17 1l6 6M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z' /></svg>}

{status === 'incoming' && <svg fill='none' height='24' stroke='#000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M16 2v6h6M23 1l-7 7M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z' /></svg>}

{status === 'answered' && <svg fill='none' height='24' stroke='#000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M15.05 5A5 5 0 0 1 19 8.95M15.05 1A9 9 0 0 1 23 8.94m-1 7.98v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z' /></svg>}

{status === 'voicemail' && <svg fill='none' height='24' stroke='#000' strokeLinecap='round' strokeLinejoin='round' strokeWidth='2' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M19 1l4 4-4 4M15 5h8M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z' /></svg>}
</div>
)
}

export default ActivityStatusIcon
10 changes: 10 additions & 0 deletions src/components/Button/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react'
import * as S from './styles'

const Button = (props) => {
return (
<S.Button {...props} />
)
}

export default Button
Loading