Show Calendar and animate buble
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -66,7 +66,7 @@ input[name="hours"] {
|
||||
|
||||
textarea[name="description"] {
|
||||
resize: none;
|
||||
width: calc(100% - 1rem);
|
||||
width: calc(100% - 1rem - 2px);
|
||||
}
|
||||
|
||||
button {
|
||||
|
||||
@@ -60,8 +60,8 @@
|
||||
box-sizing: content-box;
|
||||
background-color: white;
|
||||
width: 600px;
|
||||
height: 400px;
|
||||
padding: 2rem;
|
||||
height: 540px;
|
||||
padding: 3rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
37
src/js/components/Calendar/Day.js
Normal file
37
src/js/components/Calendar/Day.js
Normal 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
|
||||
46
src/js/components/Calendar/index.js
Normal file
46
src/js/components/Calendar/index.js
Normal 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
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}`
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user