Show Calendar and animate buble

This commit is contained in:
Manuel Bouza
2019-02-21 16:12:18 +01:00
parent 859a27e4a1
commit 0c07baa598
12 changed files with 232 additions and 63 deletions

View File

@@ -24,6 +24,7 @@
"react": "^16.8.0",
"react-dom": "^16.8.0",
"react-select": "^2.3.0",
"react-spring": "^8.0.7",
"route-parser": "^0.0.5"
},
"devDependencies": {

View File

@@ -66,7 +66,7 @@ input[name="hours"] {
textarea[name="description"] {
resize: none;
width: calc(100% - 1rem);
width: calc(100% - 1rem - 2px);
}
button {

View File

@@ -60,8 +60,8 @@
box-sizing: content-box;
background-color: white;
width: 600px;
height: 400px;
padding: 2rem;
height: 540px;
padding: 3rem;
margin: 0 auto;
}
}

View File

@@ -5,6 +5,7 @@ html {
height: 100%;
body {
margin: 0;
height: 100%;
#moco-bx-root {
@@ -27,6 +28,54 @@ html {
margin: 0;
}
}
.moco-bx-calendar {
display: flex;
justify-content: space-between;
margin-bottom: 3rem;
.moco-bx-calendar__day {
display: flex;
flex-flow: column nowrap;
align-items: center;
flex: 0 0 48px;
width: 48px;
height: 60px;
cursor: pointer;
.moco-bx-calendar__hours {
display: flex;
justify-content: center;
align-items: center;
width: 48px;
height: 48px;
flex: 0 0 48px;
color: white;
background-color: #eee;
}
&.moco-bx-calendar__day--filled {
.moco-bx-calendar__hours {
background-color: #7dc332;
}
}
&.moco-bx-calendar__day--week-day-6,
&.moco-bx-calendar__day--week-day-0 {
.moco-bx-calendar__hours {
background-color: #bbb;
}
}
&.moco-bx-calendar__day--active {
.moco-bx-calendar__hours {
background-color: #38b5eb;
}
}
}
}
}
}
}

View File

@@ -1,4 +1,5 @@
import axios from "axios"
import { formatDate } from "utils"
export default class Client {
#client;
@@ -20,14 +21,17 @@ export default class Client {
})
}
login = () => this.#client.post("session", { api_key: this.#apiKey });
projects = () => this.#client.get("projects");
activities = (fromDate, toDate) =>
this.#client.get("activities", {
params: { date: `${formatDate(fromDate)}:${formatDate(toDate)}` }
})
bookedHours = service =>
this.#client.get("activities/tags", {
params: { selection: [service.id], remote_service: service.name }
});
})
createActivity = activity => this.#client.post("activities", { activity });
}

View File

@@ -2,17 +2,19 @@ import React, { Component } from "react"
import PropTypes from "prop-types"
import ApiClient from "api/Client"
import Form from "components/Form"
import Calendar from "components/Calendar"
import Spinner from "components/Spinner"
import { observable, computed, toJS } from "mobx"
import { observer, disposeOnUnmount } from "mobx-react"
import { observable, computed } from "mobx"
import { observer } from "mobx-react"
import {
findLastProject,
findLastTask,
groupedProjectOptions,
currentDate,
formatDate,
secondsFromHours
} from "utils"
import logoUrl from 'images/logo.png'
import { startOfWeek, endOfWeek } from "date-fns"
import logoUrl from "images/logo.png"
import { head } from "lodash"
@observer
@@ -31,13 +33,14 @@ class App extends Component {
apiKey: PropTypes.string,
version: PropTypes.string
})
};
}
@observable projects = []
@observable activities = []
@observable lastProjectId
@observable lastTaskId
@observable changeset = {};
@observable formErrors = {};
@observable changeset = {}
@observable formErrors = {}
@observable isLoading = true
@computed get changesetWithDefaults() {
@@ -55,7 +58,7 @@ class App extends Component {
remote_service: service.name,
remote_id: service.id,
remote_url: service.url,
date: currentDate(),
date: formatDate(new Date()),
assignment_id: project?.value,
task_id: task?.value,
billable: task?.billable,
@@ -78,7 +81,8 @@ class App extends Component {
}
componentDidMount() {
this.fetchProjects()
Promise.all([this.fetchProjects(), this.fetchActivities()])
.then(() => this.isLoading = false)
window.addEventListener("keydown", this.handleKeyDown)
}
@@ -90,23 +94,31 @@ class App extends Component {
this.#apiClient = new ApiClient(settings)
}
fetchProjects = () => {
this.isLoading = true
fromDate = () => startOfWeek(new Date(), { weekStartsOn: 1 })
toDate = () => endOfWeek(new Date(), { weekStartsOn: 1 })
return this.#apiClient
fetchProjects = () =>
this.#apiClient
.projects()
.then(({ data }) => {
this.projects = groupedProjectOptions(data.projects)
this.lastProjectId = data.last_project_id
this.lastTaskId = data.lastTaskId
})
.catch(error => {
this.sendMessage({ type: 'closeForm' })
.catch(() => {
this.sendMessage({ type: "closeForm" })
})
.finally(() => {
this.isLoading = false
fetchActivities = () => {
return this.#apiClient
.activities(this.fromDate(), this.toDate())
.then(({ data }) => {
this.activities = data
})
};
.catch(() => {
this.sendMessage({ type: "closeForm" })
})
}
createActivity = () => {
this.isLoading = true
@@ -116,9 +128,10 @@ class App extends Component {
.then(({ data }) => {
this.changeset = {}
this.formErrors = {}
this.sendMessage(
{ type: 'activityCreated', payload: { hours: data.hours} }
)
this.sendMessage({
type: "activityCreated",
payload: { hours: data.hours }
})
})
.catch(error => {
if (error.response?.status === 422) {
@@ -128,15 +141,15 @@ class App extends Component {
this.unauthorizedError = true
}
})
.finally(() => this.isLoading = false)
.finally(() => (this.isLoading = false))
}
handleKeyDown = event => {
event.stopPropagation()
if (event.keyCode === 27) {
this.sendMessage({ type: 'closeForm' })
this.sendMessage({ type: "closeForm" })
}
};
}
handleChange = event => {
const {
@@ -148,7 +161,11 @@ class App extends Component {
if (name === "assignment_id") {
this.changeset.task_id = null
}
};
}
handleSelectDate = date => {
this.changeset.date = formatDate(date)
}
handleSubmit = event => {
event.preventDefault()
@@ -156,13 +173,12 @@ class App extends Component {
}
handleCancel = () => {
this.sendMessage({ type: 'closeForm' })
this.sendMessage({ type: "closeForm" })
}
sendMessage = action =>
chrome.tabs.query(
{ active: true, currentWindow: true },
tabs => chrome.tabs.sendMessage(tabs[0].id, action)
chrome.tabs.query({ active: true, currentWindow: true }, tabs =>
chrome.tabs.sendMessage(tabs[0].id, action)
)
render() {
@@ -170,15 +186,23 @@ class App extends Component {
return <Spinner />
}
const { service } = this.props;
return (
<>
<div className="moco-bx-logo__container">
<img className="moco-bx-logo" src={chrome.extension.getURL(logoUrl)} />
<img
className="moco-bx-logo"
src={chrome.extension.getURL(logoUrl)}
/>
<h1>MOCO Zeiterfassung</h1>
</div>
<Calendar
fromDate={this.fromDate()}
toDate={this.toDate()}
activities={this.activities}
selectedDate={new Date(this.changesetWithDefaults.date)}
onChange={this.handleSelectDate}
/>
<Form
changeset={this.changesetWithDefaults}
projects={this.projects}

View File

@@ -1,21 +1,12 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import { Spring, config, animated } from 'react-spring/renderprops'
import ApiClient from "api/Client"
import Popup from "components/Popup"
import InvalidConfigurationError from "components/InvalidConfigurationError"
import Form from "components/Form"
import Spinner from "components/Spinner"
import { observable, computed, reaction } from "mobx"
import { observable, reaction } from "mobx"
import { observer, disposeOnUnmount } from "mobx-react"
import logoUrl from "images/logo.png"
import {
findLastProject,
findLastTask,
groupedProjectOptions,
currentDate,
secondsFromHours
} from "utils"
import { head } from "lodash"
@observer
class Bubble extends Component {
@@ -75,7 +66,7 @@ class Bubble extends Component {
initializeApiClient = settings => {
this.#apiClient = new ApiClient(settings)
}
open = event => {
if (event && event.target && event.target.classList.contains('moco-bx-popup')) {
return this.close()
@@ -139,20 +130,30 @@ class Bubble extends Component {
const { service, settings } = this.props
return (
<div className="moco-bx-bubble" onClick={this.open} style={service.position}>
<img className="moco-bx-logo" src={chrome.extension.getURL(logoUrl)} />
{this.bookedHours > 0
? <span className="moco-bx-badge">{this.bookedHours}h</span>
: null
}
<>
<Spring from={{ transform: 'scale(0.1)' }} to={{ transform: 'scale(1)' }} config={config.wobbly}>
{props => (
<animated.div
className="moco-bx-bubble"
style={{...service.position, ...props}}
onClick={this.open}
>
<img className="moco-bx-logo" src={chrome.extension.getURL(logoUrl)} />
{this.bookedHours > 0
? <span className="moco-bx-badge">{this.bookedHours}h</span>
: null
}
</animated.div>
)}
</Spring>
{this.isOpen && (
<Popup
service={service}
settings={settings}
unauthorizedError={this.unauthorizedError}
service={service}
settings={settings}
unauthorizedError={this.unauthorizedError}
/>
)}
</div>
</>
)
}
}

View File

@@ -0,0 +1,37 @@
import React, { useCallback } from 'react'
import PropTypes from 'prop-types'
import { format, getDay } from 'date-fns'
import deLocale from 'date-fns/locale/de'
import cn from 'classnames'
const Day = ({ date, hours, active, onClick }) => {
const handleClick = useCallback(() => onClick(date), [date])
return (
<div
className={cn(
'moco-bx-calendar__day',
`moco-bx-calendar__day--week-day-${getDay(date)}`,
{
'moco-bx-calendar__day--active': active,
'moco-bx-calendar__day--filled': hours > 0
}
)}
onClick={handleClick}
>
<span className='moxo-bx-calendar__day-of-week'>
{format(date, 'dd', { locale: deLocale })}
</span>
<span className='moco-bx-calendar__hours'>{hours > 0 ? hours : null}</span>
</div>
)
}
Day.propTypes = {
date: PropTypes.instanceOf(Date).isRequired,
hours: PropTypes.number.isRequired,
active: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired
}
export default Day

View File

@@ -0,0 +1,46 @@
import React from 'react'
import PropTypes from 'prop-types'
import Day from './Day'
import { formatDate, trace } from 'utils'
import { eachDay } from 'date-fns'
import { pathEq } from 'lodash/fp'
const hoursInDate = activities => date =>
activities
.filter(pathEq('date', formatDate(date)))
.reduce((acc, activity) => acc + activity.hours, 0)
const Calendar = ({ fromDate, toDate, selectedDate, activities, onChange }) => {
const getHours = hoursInDate(activities)
return (
<div className='moco-bx-calendar'>
{eachDay(fromDate, toDate).map(date => (
<Day
key={date}
date={date}
hours={getHours(date)}
onClick={onChange}
active={trace('dateL', formatDate(date)) === trace('dateR', formatDate(selectedDate))}
/>
))}
</div>
)
}
Calendar.propTypes = {
fromDate: PropTypes.instanceOf(Date).isRequired,
toDate: PropTypes.instanceOf(Date).isRequired,
selectedDate: PropTypes.instanceOf(Date).isRequired,
activities: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
date: PropTypes.string.isRequired,
hours: PropTypes.number.isRequired,
timer_started_at: PropTypes.string
}).isRequired
),
onChange: PropTypes.func.isRequired
}
export default Calendar

View File

@@ -9,7 +9,7 @@ const Popup = props => {
const styles = useMemo(() => ({
width: '536px',
height: props.unauthorizedError ? '890px' : '400px'
height: props.unauthorizedError ? '890px' : '470px'
}), [props.unauthorizedError])
return (

View File

@@ -73,8 +73,8 @@ export const trace = curry((tag, value) => {
return value
})
export const currentDate = (locale = 'de') =>
format(new Date(), 'YYYY-MM-DD', { locale })
export const formatDate = date =>
format(date, 'YYYY-MM-DD')
export const extensionSettingsUrl = () =>
`chrome://extensions/?id=${chrome.runtime.id}`

View File

@@ -667,7 +667,7 @@
"@babel/plugin-transform-react-jsx-self" "^7.0.0"
"@babel/plugin-transform-react-jsx-source" "^7.0.0"
"@babel/runtime@^7.1.2":
"@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1":
version "7.3.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a"
integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==
@@ -5983,6 +5983,13 @@ react-select@^2.3.0:
react-input-autosize "^2.2.1"
react-transition-group "^2.2.1"
react-spring@^8.0.7:
version "8.0.7"
resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.7.tgz#55b1d298be16b308388cb865ee707a1194112fe0"
integrity sha512-QHEjWLvEuRCGFRlR29o03CMc+uN1QMPt8Gxxp37szpnkJUq+HDIWNXNCacQ+kJxqzcu65JoeWV6D5q3/y5VK6Q==
dependencies:
"@babel/runtime" "^7.3.1"
react-transition-group@^2.2.1:
version "2.5.3"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.5.3.tgz#26de363cab19e5c88ae5dbae105c706cf953bb92"