Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

Commit

Permalink
feat(calendar): add calendar component
Browse files Browse the repository at this point in the history
  • Loading branch information
jackcmeyer committed Dec 30, 2019
1 parent e51204f commit 820a3a0
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 1 deletion.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@
"@fortawesome/fontawesome-svg-core": "~1.2.25",
"@fortawesome/free-solid-svg-icons": "~5.12.0",
"@fortawesome/react-fontawesome": "~0.1.4",
"@fullcalendar/core": "^4.3.1",
"@fullcalendar/daygrid": "^4.3.0",
"@fullcalendar/interaction": "~4.3.0",
"@fullcalendar/react": "^4.3.0",
"@fullcalendar/timegrid": "^4.3.0",
"@tinymce/tinymce-react": "~3.3.2",
"@types/react-datepicker": "~2.10.0",
"chart.js": "~2.9.3",
Expand Down
76 changes: 75 additions & 1 deletion src/components/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,79 @@
import React from 'react'
import FullCalendar from '@fullcalendar/react'
import dayGridPlugin from '@fullcalendar/daygrid'
import interactionPlugin from '@fullcalendar/interaction'
import timeGridPlugin from '@fullcalendar/timegrid'
import { EventApi } from '@fullcalendar/core'
import Event from './interfaces'

const Calendar = () => <h1>Calendar</h1>
import '@fullcalendar/core/main.css'
import '@fullcalendar/daygrid/main.css'
import '@fullcalendar/timegrid/main.css'

interface Props {
view: string
events: Event[]
disabled?: boolean
onDateClick?: (date: Date, allDay: boolean) => void
onDateRangeSelected?: (startDate: Date, endDate: Date, allDay: boolean) => void
onEventClick?: (event: Event) => void
}

const viewToCalendarViewMap = {
month: 'dayGridMonth',
week: 'timeGridWeek',
day: 'timeGridDay',
}

const getEventFromFullCalendarEventApi = (e: EventApi): Event => ({
id: e.id,
start: e.start,
end: e.end,
title: e.title,
allDay: e.allDay,
})

const getCalendarViewFromViewProp = (view: string) => (viewToCalendarViewMap as any)[view]

const Calendar = (props: Props) => {
const { view, events, disabled, onDateClick, onDateRangeSelected, onEventClick } = props
const fullCalendarRef = React.createRef<FullCalendar>()
return (
<FullCalendar
events={events}
ref={fullCalendarRef}
selectable={!disabled}
header={{
left: 'prev,next today',
center: 'title',
right: 'dayGridMonth,timeGridWeek,timeGridDay',
}}
defaultView={getCalendarViewFromViewProp(view)}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
themeSystem="bootstrap"
dateClick={(arg) => {
console.log('date click')
if (onDateClick) {
onDateClick(arg.date, arg.allDay)
}
}}
select={(arg) => {
if (onDateRangeSelected) {
onDateRangeSelected(arg.start, arg.end, arg.allDay)
}
}}
eventClick={(arg) => {
if (onEventClick) {
onEventClick(getEventFromFullCalendarEventApi(arg.event))
}
}}
/>
)
}

Calendar.defaultProps = {
view: 'week',
events: [],
}

export { Calendar }
7 changes: 7 additions & 0 deletions src/components/Calendar/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default interface Event {
id: string
allDay: boolean
start: Date | null
end: Date | null
title: string
}
48 changes: 48 additions & 0 deletions stories/calendar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import moment from 'moment'
import { Calendar } from '../src/components/Calendar'
import { Toast, Toaster } from '../src/components/Toaster'

storiesOf('Calendar', module)
.addParameters({
info: {
inline: true,
},
})
.addDecorator((storyFn) => <div style={{ textAlign: 'center' }}>{storyFn()}</div>)
.add('Calendar', () => {
const start = moment()
const end = start.add(1, 'hours')
return (
<div>
<Calendar
onDateClick={(date, allDay) => {
console.log('from story')
Toast('success', 'Date Click', `${date.toISOString()} all day is ${allDay}`)
}}
onEventClick={(event) => {
Toast('success', 'Event Click', event.title)
}}
onDateRangeSelected={(startDate: Date, endDate: Date) => {
Toast(
'success',
'Range Selected',
`${startDate.toISOString()} to ${endDate.toISOString()}`,
)
}}
events={[
{
start: start.toDate(),
end: end.toDate(),
title: 'Some Title',
id: 'Some Id',
allDay: false,
},
]}
/>

<Toaster autoClose={800} hideProgressBar draggable />
</div>
)
})
119 changes: 119 additions & 0 deletions test/calendar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React from 'react'
import FullCalendar from '@fullcalendar/react'
import { mount, shallow } from 'enzyme'
import { act } from 'react-dom/test-utils'
import { TimeGridView } from '@fullcalendar/timegrid'
import moment from 'moment'
import { Calendar } from '../src/components/Calendar'

describe('Calendar', () => {
it('should render a full calendar component with the proper default props', () => {
const wrapper = shallow(<Calendar />)

const fullCalendar = wrapper.find(FullCalendar)
expect(fullCalendar).toHaveLength(1)
expect(fullCalendar.prop('defaultView')).toEqual('timeGridWeek')
expect(fullCalendar.prop('selectable')).toBeTruthy()
})

it('should render a disabled calendar', () => {
const wrapper = shallow(<Calendar disabled />)

const fullCalendar = wrapper.find(FullCalendar)
expect(fullCalendar.prop('selectable')).toBeFalsy()
})

it('should pass events to full calendar', () => {
const start = moment()
const end = start.add(1, 'hours').toDate()
const events = [
{
id: 'id123',
start: start.toDate(),
end,
allDay: true,
title: 'Title',
},
]
const wrapper = shallow(<Calendar events={events} />)

const fullCalendar = wrapper.find(FullCalendar)
expect(fullCalendar.prop('events')).toEqual(events)
})

it('should call the onDateClick callback when a date is selected', () => {
const onDateClickSpy = jest.fn()
const wrapper = mount(<Calendar onDateClick={onDateClickSpy} />)

const fullCalendar = wrapper.find(FullCalendar)

const date = new Date()
const allDay = true

act(() => {
;(fullCalendar.prop('dateClick') as any)({
date,
allDay,
dateStr: new Date().toISOString(),
resource: expect.anything(),
dayEl: expect.any(HTMLElement),
jsEvent: expect.any(MouseEvent),
view: expect.any(TimeGridView),
})
})

expect(onDateClickSpy).toHaveBeenCalledTimes(1)
expect(onDateClickSpy).toHaveBeenCalledWith(date, allDay)
})

it('should call the onDateRangeSelected callback when a date range is selected', () => {
const onDateRangeSelectedSpy = jest.fn()
const wrapper = mount(<Calendar onDateRangeSelected={onDateRangeSelectedSpy} />)

const fullCalendar = wrapper.find(FullCalendar)

const start = new Date()
const end = new Date()
const allDay = true
act(() => {
;(fullCalendar.prop('select') as any)({
start,
end,
allDay,
startStr: new Date().toISOString(),
endStr: new Date().toISOString(),
jsEvent: expect.any(MouseEvent),
view: expect.any(TimeGridView),
})
})

expect(onDateRangeSelectedSpy).toHaveBeenCalledTimes(1)
expect(onDateRangeSelectedSpy).toHaveBeenCalledWith(start, end, allDay)
})

it('should call the onEventClick callback when an event is selected', () => {
const onEventClickSpy = jest.fn()
const wrapper = mount(<Calendar onEventClick={onEventClickSpy} />)

const fullCalendar = wrapper.find(FullCalendar)

const event = {
start: new Date(),
end: new Date(),
allDay: true,
title: 'Some Title',
id: 'someid',
}
act(() => {
;(fullCalendar.prop('eventClick') as any)({
event,
el: expect.any(HTMLElement),
jsEvent: expect.any(MouseEvent),
view: expect.any(TimeGridView),
})
})

expect(onEventClickSpy).toHaveBeenCalledTimes(1)
expect(onEventClickSpy).toHaveBeenCalledWith(event)
})
})

0 comments on commit 820a3a0

Please sign in to comment.