Woodshop
Search…
Create a dynamic calendar view for an event-booking use case
This example shows you how you can implement a calendar view with a custom workflow involving dynamic API calls.
In our example, we want to manage the bookings for a sports court where:
    We have a list of court opening dates. Each date can be subject to a price increase if the period is busy. These dates come from a collection called availableDates
    A list of available slots appears after selecting a date and duration. These available slots come from a smart collection called availableSlots
    The user can book a specific slot using a smart action calledbook.

How it works

Smart view definition

Learn more about smart views. File template.hbs
This file contains the HTML and CSS needed to build the view.
1
<style>
2
.calendar {
3
padding: 20px;
4
background: white;
5
height:100%;
6
width: 50%;
7
overflow: scroll;
8
color: #415574;
9
}
10
.wrapper-view {
11
display: flex;
12
height:800px;
13
width:100%;
14
background-color: white;
15
}
16
.right-hand-wrapper {
17
width: 40%;
18
padding: 3px 30px;
19
background-color: white;
20
margin-left:30px;
21
height: 80%;
22
overflow: scroll;
23
}
24
:root {
25
--fc-border-color:#e0e4e8;
26
--fc-button-text-color: #415574;
27
--fc-button-bg-color: #ffffff;
28
--fc-button-border-color: #c8ced7;
29
--fc-button-hover-bg-color: rgb(195, 195, 195);
30
--fc-button-hover-border-color: #c8ced7;
31
--fc-button-active-bg-color: #ffffff;
32
--fc-button-active-border-color: #c8ced7;
33
}
34
.slot-container {
35
display: flex;
36
margin: 10px 0;
37
vertical-align: center;
38
padding: 2px 0;
39
}
40
.slot-value {
41
margin: auto 0;
42
margin-right: 30px;
43
width: 40px;
44
}
45
.calendar .fc-toolbar.fc-header-toolbar .fc-left {
46
font-size: 14px;
47
font-weight: bold;
48
}
49
.calendar .fc-day-header {
50
padding: 10px 0;
51
background-color: #f7f7f7;
52
}
53
.calendar .fc-event {
54
background-color: #f7f7f7;
55
border: 1px solid #ddd;
56
color: #555;
57
font-size: 14px;
58
margin-top: 5px;
59
}
60
.calendar .fc-daygrid-event {
61
background-color: #a2c1f5;
62
color: white;
63
font-size: 14px;
64
border: none;
65
padding: 6px;
66
}
67
.c-beta-radio-button-duration {
68
display: flex;
69
margin-top: 4px;
70
flex-wrap: wrap;
71
margin-bottom: -4px;
72
}
73
</style>
74
75
<div class="wrapper-view">
76
<div id='{{calendarId}}' class="calendar"></div>
77
<div class="right-hand-wrapper">
78
<div class="c-beta-label c-beta-label--top ember-view l-dmb">
79
<div class= "c-beta-label__label">Selected date</div>
80
<div class = "c-row-value">
81
{{#if this.selectedDate}}
82
{{this.selectedDate}}
83
{{else}}
84
None
85
{{/if}}
86
</div>
87
</div>
88
89
<div class="c-beta-label c-beta-label--top ember-view l-dmb">
90
<div class= "c-beta-label__label">Duration</div>
91
<BetaRadioButton
92
@class='c-beta-radio-button-duration'
93
@namePath='label'
94
@options={{this.durations}}
95
@value={{this.selectedDuration}}
96
@valuePath='value'
97
/>
98
</div>
99
100
{{#if this.availableSlots}}
101
<div class="c-beta-label c-beta-label--top ember-view l-dmb">
102
<div class= "c-beta-label__label">Available slots</div>
103
{{#each this.availableSlots as |slot|}}
104
<div class="slot-container">
105
<div class="c-row-value slot-value">{{slot.forest-time}}</div>
106
<div class="c-beta-button c-beta-button--primary" onclick={{ action 'triggerSmartAction' this.availableSlotsCollection 'book' slot}}>book</div>
107
</div>
108
{{/each}}
109
</div>
110
{{else}}
111
{{#if (not this.selectedDate)}}
112
Please select a date to see slots available.
113
{{else}}
114
No slots available, please try another duration or another date.
115
{{/if}}
116
{{/if}}
117
</div>
118
</div>
119
Copied!
File template.js
This file contains all the logic needed to handle events and actions.
1
import Component from '@ember/component';
2
import { inject as service } from '@ember/service';
3
import { scheduleOnce } from '@ember/runloop';
4
import { observer } from '@ember/object';
5
import $ from 'jquery';
6
import SmartViewMixin from 'client/mixins/smart-view-mixin';
7
8
export default Component.extend(SmartViewMixin, {
9
store: service(),
10
conditionAfter: null,
11
conditionBefore: null,
12
loaded: false,
13
calendarId: null,
14
selectedAvailability: null,
15
selectedDate: null,
16
selectedDuration: 1,
17
availableSlots: null,
18
availableSlotsCollection: null,
19
_calendar: null,
20
21
init(...args) {
22
this._super(...args);
23
this.loadPlugin();
24
this.initConditions();
25
this.set('durations', [{
26
label: '1 hour',
27
value: 1,
28
}, {
29
label: '2 hours',
30
value: 2,
31
}, {
32
label: '3 hours',
33
value: 3,
34
}]);
35
},
36
37
didInsertElement() {
38
this.set('availableSlotsCollection', this.store.peekAll('collection').findBy('name', 'availableSlots'));
39
},
40
41
// update displayed events when new records are retrieved
42
onRecordsChange: observer('records.[]', function () {
43
this.setEvent();
44
}),
45
46
onConfigurationChange: observer('selectedDate', 'selectedDuration', function () {
47
this.searchAvailabilities();
48
}),
49
50
initConditions() {
51
if (this.filters) {
52
this.filters.forEach(condition => {
53
if (condition.operator === 'is after') {
54
this.set('conditionAfter', condition);
55
} else if (condition.operator === 'is before') {
56
this.set('conditionBefore', condition);
57
}
58
});
59
}
60
},
61
62
loadPlugin() {
63
scheduleOnce('afterRender', this, function () {
64
this.set('calendarId', `${this.elementId}-calendar`);
65
66
// retrieve fullCalendar script to build the calendar view
67
$.getScript('https://cdn.jsdelivr.net/npm/[email protected]/main.min.js', () => {
68
this.setEvent();
69
const calendarEl = document.getElementById(this.calendarId);
70
const calendar = new FullCalendar.Calendar(calendarEl, {
71
height: 600,
72
allDaySlot: true,
73
eventClick: (event, jsEvent, view) => {
74
// persist the selected event information when an event is clicked
75
this.set('selectedAvailability', event.event);
76
const eventStart = event.event.start;
77
const selectedDate = `${eventStart.getDate().toString()}/${(eventStart.getMonth() + 1).toString()}/${eventStart.getFullYear().toString()}`;
78
// persist the selected event's date to be displayed in the view
79
this.set('selectedDate', selectedDate);
80
},
81
// define logic to be triggered when the user navigates between date ranges
82
datesSet: (view) => {
83
// define params to query the relevant records from the database based on the date range
84
const params = {
85
filters: JSON.stringify({
86
aggregator: 'and',
87
conditions: [{
88
field: 'date',
89
operator: 'before',
90
value: view.end,
91
}, {
92
field: 'date',
93
operator: 'after',
94
value: view.start,
95
}],
96
}),
97
'page[number]': 1,
98
'page[size]': 31,
99
timezone: 'Europe/Paris',
100
};
101
102
// query the records from the availableDates collection
103
return this.store.query('forest-available-date', params)
104
.then((records) => {
105
this.set('records', records);
106
})
107
.catch((error) => {
108
this.set('records', null);
109
alert('We could not retrieve the available dates');
110
console.error(error);
111
});
112
},
113
});
114
115
this.set('_calendar', calendar);
116
calendar.render();
117
this.set('loaded', true);
118
});
119
120
const headElement = document.getElementsByTagName('head')[0];
121
const cssLink = document.createElement('link');
122
123
cssLink.type = 'text/css';
124
cssLink.rel = 'stylesheet';
125
cssLink.href = 'https://cdn.jsdelivr.net/npm/[email protected]/main.min.css';
126
headElement.appendChild(cssLink);
127
});
128
},
129
130
// create calendar event objects for each availableDates record
131
setEvent() {
132
if (!this.records || !this.loaded) { return; }
133
134
this._calendar.getEvents().forEach((event) => event.remove());
135
136
this.records.forEach((availability) => {
137
if (availability.get('forest-opened') === true) {
138
const event = {
139
id: availability.get('id'),
140
title: 'Available',
141
start: availability.get('forest-date'),
142
allDay: true,
143
};
144
145
if (availability.get('forest-pricingPremium') === 'high') {
146
event.textColor = 'white';
147
event.backgroundColor = '#FB6669';
148
event.title = 'Available';
149
}
150
this._calendar.addEvent(event);
151
}
152
});
153
},
154
155
// retrieve record from the availableSlots collection when an event has been selected
156
searchAvailabilities() {
157
if (this.selectedAvailability) {
158
return this.store.query('forest-available-slot', {
159
date: this.selectedAvailability.start,
160
duration: this.selectedDuration,
161
}).then((slots) => {
162
this.set('availableSlots', slots);
163
}).catch((error) => {
164
this.set('availableSlots', null);
165
alert('We could not retrieve the available slots');
166
console.error(error);
167
});
168
}
169
},
170
});
171
Copied!

Available dates model

File models/available-dates.js
This file contains the model definition for the collection availableDates. It is located in the models folder, at the root of the admin backend.
1
module.exports = (sequelize, DataTypes) => {
2
const { Sequelize } = sequelize;
3
const AvailableDates = sequelize.define('availableDates', {
4
date: {
5
type: DataTypes.DATE,
6
},
7
opened: {
8
type: DataTypes.BOOLEAN,
9
},
10
pricingPremium: {
11
type: DataTypes.STRING,
12
},
13
}, {
14
tableName: 'available_dates',
15
underscored: true,
16
timestamps: false,
17
schema: process.env.DATABASE_SCHEMA,
18
});
19
20
return AvailableDates;
21
};
22
Copied!

Available slots smart collection

To create a smart collection that returns records built from an API call, two files need to be created:
    a file available-slots.js inside the folder forest to declare the collection
    a file available-slots.js inside the routes folder to implement the GET logic for the collection
File forest/available-slots.js
This file includes the smart collection definition.
1
const { collection } = require('forest-express-sequelize');
2
3
collection('availableSlots', {
4
fields: [{
5
field: 'startDate',
6
type: 'Date',
7
}, {
8
field: 'endDate',
9
type: 'Date',
10
}, {
11
field: 'time',
12
type: 'String',
13
}, {
14
field: 'maxTimeSlot',
15
type: 'Number',
16
}],
17
segments: [],
18
});
19
Copied!
File routes/available-slots.js
This file includes the logic implemented to retrieve the available slots from an API call and return them serialized to the UI.
1
const express = require('express');
2
const { PermissionMiddlewareCreator, RecordSerializer } = require('forest-express-sequelize');
3
const { availableSlots } = require('../models');
4
5
const router = express.Router();
6
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('availableSlots');
7
const recordSerializer = new RecordSerializer({ name: 'availableSlots' });
8
9
// Get a list of Available slots
10
router.get('/availableSlots', permissionMiddlewareCreator.list(), (request, response, next) => {
11
const { date } = request.query;
12
const { duration } = request.query;
13
return fetch(`https://apicallplaceholder/slots/?date=${date}&duration=${duration}`)
14
.then((response) => JSON.parse(response))
15
.then((matchingSlots) => {
16
return recordSerializer.serialize(matchingSlots)
17
.then((recordsSerialized) => response.send(recordsSerialized));
18
})
19
.catch((error) => {
20
console.error(error);
21
});
22
});
23
24
25
module.exports = router;
Copied!

Book smart action

To create the action to book a slot, two files need to be updated:
    the file available-slots.js inside the folder forest to declare the action
    the file available-slots.js inside the routes folder to implement the logic for the action
File forest/available-slots.js
This file includes the smart action definition. The action form is pre-filled with the start and end date. The last step is to select the user associated with this booking.
1
const { collection } = require('forest-express-sequelize');
2
3
collection('availableSlots', {
4
actions: [
5
{
6
name: 'book',
7
type: 'single',
8
fields: [{
9
field: 'start date',
10
type: 'Date',
11
}, {
12
field: 'end date',
13
type: 'Date',
14
}, {
15
field: 'user',
16
reference: 'users.id',
17
}],
18
values: (context) => {
19
return {
20
'start date': context.startDate,
21
'end date': context.endDate,
22
};
23
},
24
},
25
],
26
...
27
});
28
Copied!
File routes/available-slots.js
This file includes the logic of the smart action. It basically creates a record from the bookings collection with the information passed on by the user input form.
1
const express = require('express');
2
const { PermissionMiddlewareCreator } = require('forest-express-sequelize');
3
const { bookings } = require('../models');
4
5
const router = express.Router();
6
const permissionMiddlewareCreator = new PermissionMiddlewareCreator('availableSlots');
7
8
...
9
router.post('/actions/book', permissionMiddlewareCreator.smartAction(), (request, response) => {
10
const attr = request.body.data.attributes.values;
11
const startDate = attr['start date'];
12
const endDate = attr['end date'];
13
bookings.create({
14
startDate,
15
endDate,
16
userIdKey: attr.user,
17
}).then(() => response.send({ success: 'successfully created booking' }));
18
});
19
20
21
module.exports = router;
Copied!
Last modified 11mo ago