├── .circleci
└── config.yml
├── .env
├── .gitignore
├── README.md
├── flux.json
├── karma.conf.js
├── misc
├── avatar-biff.jpg
├── avatar-doc.jpg
├── avatar-mcfly.png
├── widget-screenshot-gray.png
├── widget-screenshot-old.png
└── widget-screenshot.png
├── package.json
├── public
├── appointments.html
├── assets
│ ├── favicon.png
│ ├── main.css
│ ├── main.js
│ └── tk-logo.svg
├── autoload.htm
├── booking-engine.htm
├── build
│ ├── 094c3d7379e4d082568e277096065e4c.svg
│ ├── 202fc69fb2f60dddd2c47a3af2f32ad1.svg
│ ├── 332836a9633ba75d29ec.svg
│ ├── 35d9aa74a5c9697bc38c.woff2
│ ├── 3bc4108fa8f9a72e6ea5.woff
│ ├── 4d140983c7c8c4a8c722.woff2
│ ├── 51986e8b3d5bf43c231139147b1e75c7.png
│ ├── 608be94da46d7cd7a003.woff2
│ ├── 6517a5545bd31eeb4a61.woff2
│ ├── 6e872659616d9b8fb28c.woff2
│ ├── 8deeb3884c8ce11a3537.woff2
│ ├── a8d9e1dfd4e09be454f54a7f20a317b0.svg
│ ├── accce9b81f55c6f4779c.woff2
│ ├── acfea1d2352591b09c46.woff2
│ ├── appointments.min.css
│ ├── appointments.min.js
│ ├── appointments.min.js.LICENSE.txt
│ ├── booking.min.css
│ ├── booking.min.js
│ └── booking.min.js.LICENSE.txt
├── callbacks.htm
├── fields.htm
├── group-bookings.htm
├── index.html
├── list-view.htm
├── local-language.htm
├── local-preset.htm
├── local-strings.htm
├── multiple.htm
├── remote-project.htm
├── selectable-length.htm
├── single.htm
└── team-availability.htm
├── src
├── booking
│ ├── assets
│ │ ├── arrow-down-icon.svg
│ │ ├── checkmark-icon.svg
│ │ ├── close-icon.svg
│ │ ├── error-icon.svg
│ │ ├── error-warning-icon.svg
│ │ ├── loading-spinner.svg
│ │ ├── timekit-logo.svg
│ │ └── timezone-icon.svg
│ ├── configs.js
│ ├── helpers
│ │ ├── base.js
│ │ ├── config.js
│ │ ├── template.js
│ │ └── util.js
│ ├── index.js
│ ├── pages
│ │ ├── booking.js
│ │ └── reschedule.js
│ ├── services
│ │ └── timezones.js
│ ├── styles
│ │ ├── fullcalendar.scss
│ │ ├── main.scss
│ │ ├── testmoderibbon.scss
│ │ ├── utils.scss
│ │ └── variables.scss
│ ├── templates
│ │ ├── booking-page.html
│ │ ├── error.html
│ │ ├── fields
│ │ │ ├── checkbox.html
│ │ │ ├── label.html
│ │ │ ├── multi-checkbox.html
│ │ │ ├── select.html
│ │ │ ├── tel.html
│ │ │ ├── text.html
│ │ │ └── textarea.html
│ │ ├── footer.html
│ │ ├── loading.html
│ │ ├── reschedule-page.html
│ │ ├── testmoderibbon.html
│ │ ├── user-avatar.html
│ │ └── user-displayname.html
│ └── widget.js
└── services
│ ├── assets
│ ├── close-icon.svg
│ ├── error-warning-icon.svg
│ ├── icon_back.svg
│ ├── icon_clear.svg
│ ├── icon_close.svg
│ ├── icon_geolocation.svg
│ ├── icon_search.svg
│ └── logo.png
│ ├── classes
│ ├── base.js
│ ├── config.js
│ ├── template.js
│ └── util.js
│ ├── configs.js
│ ├── index.js
│ ├── pages
│ ├── calendar.js
│ ├── locations.js
│ └── services.js
│ ├── styles
│ └── base.scss
│ ├── templates
│ ├── calendar.html
│ ├── error.html
│ ├── locations.html
│ ├── services.html
│ └── slots
│ │ └── locations.html
│ └── widget.js
├── test
├── advancedConfiguration.spec.js
├── availabilityListView.spec.js
├── basicConfiguration.spec.js
├── basicInteractions.spec.js
├── bookingConfiguration.spec.js
├── bookingFields.spec.js
├── configLoading.spec.js
├── disableBookingPage.spec.js
├── errorHandling.spec.js
├── fixtures
│ ├── hour-compatibility.html
│ ├── main.html
│ └── minified.html
├── groupBookings.spec.js
├── initialization.spec.js
├── mobileResponsive.spec.js
├── multipleInstances.spec.js
├── prefillFromUrl.spec.js
├── teamAvailability.spec.js
├── timezoneHelper.spec.js
└── utils
│ ├── commonInteractions.js
│ ├── createWidget.js
│ ├── defaultConfig.js
│ ├── mockAjax.js
│ └── teamAvailabilityConfig.js
├── version
├── webpack.common.js
├── webpack.dev.js
├── webpack.prod.js
└── yarn.lock
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | jobs:
3 | build:
4 | working_directory: ~/timekit-io/booking-js
5 | docker:
6 | - image: circleci/node:latest-browsers
7 | environment:
8 | - NODE_ENV: development
9 | steps:
10 | - checkout
11 | - run: node --version
12 | - restore_cache:
13 | keys:
14 | # This branch if available
15 | - v1-dep-{{ .Branch }}-{{ .Revision }}
16 | # Default branch if not
17 | - v1-dep-master-
18 | - run:
19 | name: Install dependencies
20 | command: |
21 | git config --global url."https://".insteadOf git://
22 | - run:
23 | name: Install dependencies
24 | command: |
25 | node -v
26 | yarn -v
27 | yarn install --pure-lockfile
28 | yarn add sass
29 | yarn add puppeteer
30 | # Save dependency cache
31 | - save_cache:
32 | key: v1-dep-{{ .Branch }}-{{ .Revision }}
33 | paths:
34 | - ~/.cache/yarn
35 | - ./node_modules
36 | # Run frontend tests
37 | - run:
38 | name: Test unit suite and build prod
39 | command: |
40 | yarn run test
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/.env
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .git/
3 | dist/dist
4 | node_modules/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Booking.js by Timekit
2 |
3 | > Make a beautiful embeddable booking widget in minutes running on the Timekit API.
4 |
5 | ⚠️ This is **version 3** of booking.js that supports the new projects model and uses App Widget Key for authentication.
6 |
7 | - Version 1/2 will not be supported anymore
8 |
9 | 
10 |
11 | ## Documentation
12 |
13 | All documentation, guides and examples can be found on [our developer portal](https://developers.timekit.io/v2/docs/booking-widget-v2).
14 |
15 | *This repo is mainly for community contributions, and the curious soul that would like to customize the widget.*
16 |
17 | ## Roadmap/todos
18 |
19 | See [Issues](https://github.com/timekit-io/booking-js/issues) for feature requests, bugs etc.
20 |
21 | ## License attributions
22 |
23 | The `json-schema` v0.2.3 package is used pursuant to the BSD-3-Clause license
24 |
25 | ### Setting up locally
26 |
27 | Checkout this new project locally using git command showed below:
28 | ```
29 | # go to timekit workspace
30 | cd ~/timekit-io
31 |
32 | # clone this new project loacally
33 | git clone git@github.com:timekit-io/bookingjs.git
34 |
35 | # install depedencies
36 | yarn install
37 |
38 | # run bookingjs project
39 | # for local testing use: http://localhost:8081
40 | yarn dev
41 | ```
42 |
43 | ## How to publish changes to npm
44 | make sure you upgrade version number in package.json file follow sementic versioning
45 | - run ```yarn test``
46 | - test manually to make sure widget works fine
47 | - merge your changes to master and pull master branch locally
48 | - make sure your changes are uptodate and clean
49 | - run following commands
50 |
51 | ```
52 | # login as tulipnpm user
53 | yarn login
54 |
55 | # commit and publish new changes
56 | yarn publish
57 | ```
--------------------------------------------------------------------------------
/flux.json:
--------------------------------------------------------------------------------
1 | {
2 | "release": {
3 | "check-git-master": true,
4 | "check-circle-master": true,
5 | "install-npm-deps": true,
6 | "bump-version-file": true,
7 | "bump-package-version": true,
8 | "build-dist-bundle": "yarn run build",
9 | "release-on-github": true
10 | },
11 | "deploy": {
12 | "prod": {
13 | "check-git-master": true,
14 | "install-npm-deps": true,
15 | "source-dir": "public/build",
16 | "build-dist-bundle": "yarn run build",
17 | "aws-cdn-deploy": "timekit-cdn/booking-js/v3"
18 | },
19 | "staging": {
20 | "source-dir": "public/build",
21 | "build-dist-bundle": "yarn run build",
22 | "aws-cdn-deploy": "timekit-cdn-staging/booking-js/v3"
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function (config) {
2 | process.env.CHROME_BIN = require('puppeteer').executablePath();
3 | config.set({
4 | // base path that will be used to resolve all patterns (eg. files, exclude)
5 | basePath: './',
6 |
7 | // web server port
8 | port: 9876,
9 |
10 | // frameworks to use
11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
12 | frameworks: ['jasmine-ajax', 'jasmine', 'browserify', 'viewport'],
13 |
14 | preprocessors: {
15 | 'test/*.spec.js': [ 'browserify' ]
16 | },
17 |
18 | // list of files / patterns to load in the browser
19 | files: [
20 | 'node_modules/jquery/dist/jquery.js',
21 | 'node_modules/jasmine-jquery/lib/jasmine-jquery.js',
22 | { pattern: 'misc/**/*.*', included: false, served: true, watched: false },
23 | { pattern: 'public/build/**/*', included: false, served: true, watched: true },
24 | {
25 | pattern: 'test/*.spec.js',
26 | included: true,
27 | watched: true,
28 | served: true,
29 | },
30 | {
31 | pattern: 'test/fixtures/**/*.html',
32 | included: false,
33 | watched: true,
34 | served: true,
35 | }
36 | ],
37 |
38 | // start these browsers
39 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
40 | browsers: ['ChromeHeadless'],
41 |
42 | // enable / disable colors in the output (reporters and logs)
43 | colors: true,
44 |
45 | // Continuous Integration mode
46 | // if true, Karma captures browsers, runs the tests and exits
47 | singleRun: true,
48 |
49 | // enable / disable watching file and executing tests whenever any file changes
50 | autoWatch: true,
51 |
52 | // level of logging
53 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
54 | logLevel: config.LOG_INFO,
55 |
56 | // test results reporter to use
57 | // possible values: 'dots', 'progress'
58 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter
59 | // reporters: ['coverage'],
60 | reporters: ['spec']
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/misc/avatar-biff.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/misc/avatar-biff.jpg
--------------------------------------------------------------------------------
/misc/avatar-doc.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/misc/avatar-doc.jpg
--------------------------------------------------------------------------------
/misc/avatar-mcfly.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/misc/avatar-mcfly.png
--------------------------------------------------------------------------------
/misc/widget-screenshot-gray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/misc/widget-screenshot-gray.png
--------------------------------------------------------------------------------
/misc/widget-screenshot-old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/misc/widget-screenshot-old.png
--------------------------------------------------------------------------------
/misc/widget-screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/misc/widget-screenshot.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "license": "MIT",
3 | "version": "3.1.1",
4 | "author": "Timekit Inc.",
5 | "name": "timekit-booking",
6 | "main": "public/build/booking.min.js",
7 | "description": "Make a beautiful embeddable booking widget in minutes",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/timekit-io/booking-js.git"
11 | },
12 | "scripts": {
13 | "test": "karma start",
14 | "test:watch": "karma start --watch",
15 | "build": "webpack --config webpack.prod.js",
16 | "dev": "webpack serve --config webpack.dev.js",
17 | "deploy:prod": "../flux/flux timekit:deploy prod booking-js",
18 | "deploy:hosted": "../flux/flux timekit:deploy hosted booking-js",
19 | "release:patch": "../flux/flux timekit:release patch booking-js",
20 | "release:minor": "../flux/flux timekit:release minor booking-js",
21 | "release:major": "../flux/flux timekit:release major booking-js"
22 | },
23 | "devDependencies": {
24 | "browserify": "^17.0.0",
25 | "css-loader": "^6.7.1",
26 | "file-loader": "^6.2.0",
27 | "html-webpack-plugin": "^5.5.0",
28 | "jasmine": "^4.2.1",
29 | "jasmine-jquery": "^2.1.1",
30 | "jquery": "^3.6.0",
31 | "karma": "^6.4.0",
32 | "karma-browserify": "^8.1.0",
33 | "karma-chrome-launcher": "^3.1.1",
34 | "karma-jasmine": "^5.1.0",
35 | "karma-jasmine-ajax": "^0.1.13",
36 | "karma-spec-reporter": "^0.0.34",
37 | "karma-viewport": "^1.0.9",
38 | "mini-css-extract-plugin": "^2.6.1",
39 | "mustache-loader": "^1.4.3",
40 | "postcss-loader": "^7.0.0",
41 | "puppeteer": "^15.3.0",
42 | "sass": "^1.53.0",
43 | "sass-loader": "^13.0.2",
44 | "terser-webpack-plugin": "^5.3.3",
45 | "watchify": "^4.0.0",
46 | "webpack": "^5.73.0",
47 | "webpack-bundle-analyzer": "^4.5.0",
48 | "webpack-cli": "^4.10.0",
49 | "webpack-dev-server": "^4.9.2",
50 | "webpack-merge": "^5.8.0"
51 | },
52 | "dependencies": {
53 | "@fontsource/open-sans": "^4.5.11",
54 | "@fullcalendar/core": "^5.11.0",
55 | "@fullcalendar/daygrid": "^5.11.0",
56 | "@fullcalendar/interaction": "^5.11.0",
57 | "@fullcalendar/list": "^5.11.0",
58 | "@fullcalendar/moment": "^5.11.0",
59 | "@fullcalendar/timegrid": "^5.11.0",
60 | "json-stringify-safe": "^5.0.1",
61 | "lodash": "^4.17.21",
62 | "moment": "^2.29.3",
63 | "moment-timezone": "^0.5.34",
64 | "querystringify": "^2.2.0",
65 | "sprintf-js": "^1.1.2",
66 | "svg-inline-loader": "^0.8.2",
67 | "timekit-sdk": "^1.19.3"
68 | },
69 | "resolutions": {
70 | "timekit-sdk/axios": "0.27.2"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/public/appointments.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Timekit Appointments
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
21 |
22 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/public/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/assets/favicon.png
--------------------------------------------------------------------------------
/public/assets/main.css:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: #F9FAFB;
3 | font-family: 'Open Sans', 'Helvetica Neue', helvetica, arial, sans-serif;
4 | margin: 0;
5 | }
6 |
7 | .main {
8 | max-width: 700px;
9 | margin: 30px auto;
10 | padding: 0 15px;
11 | }
12 |
13 | .intro {
14 | display: none;
15 | text-align: center;
16 | font-size: 17px;
17 | margin: 0 auto;
18 | color: #425e78;
19 | max-width: 700px;
20 | padding: 100px 30px;
21 | }
22 |
23 | .intro-logo {
24 | display: inline-block;
25 | margin-bottom: 45px;
26 | }
27 |
28 | .intro-text {
29 | display: inline-block;
30 | margin-bottom: 10px;
31 | line-height: 1.6em;
32 | }
33 |
34 | .intro-signup {
35 | margin: 15px;
36 | display: inline-block;
37 | background-color: #2e5bec;
38 | box-shadow: 0 2px 6px 0 rgba(0,0,0,.15);
39 | padding: 10px 25px 9px;
40 | border-radius: 4px;
41 | color: #fff;
42 | font-size: 13px;
43 | font-weight: 700;
44 | text-align: center;
45 | letter-spacing: 1px;
46 | text-decoration: none;
47 | text-transform: uppercase;
48 | }
49 |
50 | .intro-signup:hover {
51 | box-shadow: 0 3px 15px 0 rgba(0,0,0,.15);
52 | background-color: #224edd;
53 | }
54 |
55 | .footer {
56 | display: block;
57 | position: relative;
58 | text-align: center;
59 | margin-bottom: 30px;
60 | }
61 |
62 | .powered-by {
63 | display: none;
64 | padding: 0;
65 | text-decoration: none;
66 | overflow: hidden;
67 | }
68 |
69 | .powered-by .logo {
70 | display: inline-block;
71 | float: left;
72 | width: 23px;
73 | height: 23px;
74 | margin-right: 15px;
75 | }
76 |
77 | .powered-by .text {
78 | display: inline-block;
79 | float: left;
80 | font-weight: 700;
81 | letter-spacing: 1px;
82 | color: #2e5bec;
83 | text-decoration: none;
84 | text-transform: uppercase;
85 | font-size: 13px;
86 | line-height: 23px;
87 | }
88 |
--------------------------------------------------------------------------------
/public/assets/main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function init() {
4 |
5 | const urlPath = location.pathname;
6 | const projectSlug = urlPath.replace('/', '');
7 | const urlParams = new URLSearchParams(window.location.search);
8 | const host = location.host.includes('localhost') ? '-localhost' : '';
9 |
10 | const uuid = urlParams.get('uuid');
11 | const action = urlParams.get('action');
12 | const eventEnd = urlParams.get('event.end');
13 | const eventStart = urlParams.get('event.start');
14 |
15 | const poweredByLink = document.getElementById('powered-by');
16 | poweredByLink.href = poweredByLink.href + '&utm_term=' + projectSlug;
17 |
18 | if (!projectSlug) {
19 | var intro = document.getElementById('intro');
20 | intro.style.display = 'block';
21 | return;
22 | }
23 |
24 | poweredByLink.style.display = 'inline-block';
25 |
26 | new TimekitBooking().init({
27 | project_slug: projectSlug,
28 | api_base_url: 'https://api' + host + '.timekit.io/',
29 | reschedule: {
30 | uuid,
31 | action,
32 | eventEnd,
33 | eventStart
34 | }
35 | });
36 |
37 | }
38 |
39 | init();
--------------------------------------------------------------------------------
/public/assets/tk-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Full vector path
5 | Created with Sketch.
6 |
7 |
8 |
9 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/public/autoload.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js autoload example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/public/booking-engine.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js bookings engine example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/public/build/094c3d7379e4d082568e277096065e4c.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/build/202fc69fb2f60dddd2c47a3af2f32ad1.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/build/332836a9633ba75d29ec.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/build/35d9aa74a5c9697bc38c.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/35d9aa74a5c9697bc38c.woff2
--------------------------------------------------------------------------------
/public/build/3bc4108fa8f9a72e6ea5.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/3bc4108fa8f9a72e6ea5.woff
--------------------------------------------------------------------------------
/public/build/4d140983c7c8c4a8c722.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/4d140983c7c8c4a8c722.woff2
--------------------------------------------------------------------------------
/public/build/51986e8b3d5bf43c231139147b1e75c7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/51986e8b3d5bf43c231139147b1e75c7.png
--------------------------------------------------------------------------------
/public/build/608be94da46d7cd7a003.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/608be94da46d7cd7a003.woff2
--------------------------------------------------------------------------------
/public/build/6517a5545bd31eeb4a61.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/6517a5545bd31eeb4a61.woff2
--------------------------------------------------------------------------------
/public/build/6e872659616d9b8fb28c.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/6e872659616d9b8fb28c.woff2
--------------------------------------------------------------------------------
/public/build/8deeb3884c8ce11a3537.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/8deeb3884c8ce11a3537.woff2
--------------------------------------------------------------------------------
/public/build/a8d9e1dfd4e09be454f54a7f20a317b0.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/build/accce9b81f55c6f4779c.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/accce9b81f55c6f4779c.woff2
--------------------------------------------------------------------------------
/public/build/acfea1d2352591b09c46.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/public/build/acfea1d2352591b09c46.woff2
--------------------------------------------------------------------------------
/public/build/appointments.min.css:
--------------------------------------------------------------------------------
1 | .tk-appt-window{font-size:16px}.tk-appt-window__wrapper{opacity:1;position:fixed;bottom:100px;right:20px;width:376px;display:flex;min-height:250px;max-height:704px;border-radius:8px;overflow:hidden;z-index:1000000;background:#fff;padding:24px 24px 4px;flex-direction:column;height:calc(100% - 120px);box-shadow:rgba(0,0,0,.16) 0px 5px 40px;transition:background-color .25s ease-out,box-shadow .25s ease-out}.tk-appt-window__wrapper.hide{display:none}.tk-appt-window__wrapper-body{height:90%;overflow:hidden;margin-top:16px}.tk-appt-window__wrapper-body__scroll{height:90%;width:100%;scrollbar-width:none;-ms-overflow-style:none;overflow-y:auto}.tk-appt-window__wrapper-body__scroll::-webkit-scrollbar{display:none}.tk-appt-window__wrapper-body__scroll .card-title{margin-top:8px;margin-right:auto;font-family:"Open Sans";font-size:14px;font-weight:600;text-rendering:geometricPrecision;font-stretch:normal;font-style:normal;line-height:normal;letter-spacing:normal;text-align:left;color:#14141a;display:flex;justify-content:space-between;align-items:center;width:100%}.tk-appt-window__wrapper-body__scroll .card-body{margin-top:8px;margin-right:auto;font-family:"Open Sans";font-size:14px;font-weight:normal;font-stretch:normal;font-style:normal;line-height:normal;letter-spacing:normal;text-align:left;color:#8e8e93}.tk-appt-window__wrapper-body__scroll .card-footer{margin-top:8px;margin-bottom:8px;font-family:"Open Sans";font-size:14px;font-weight:normal;font-stretch:normal;font-style:normal;line-height:normal;letter-spacing:normal;text-align:left;color:#8e8e93}.tk-appt-window__wrapper-body__scroll .card-container{width:95%;border-radius:8px;background-color:#fff;box-shadow:0 4px 20px 0 rgba(0,0,0,.05);margin-top:10px;padding:8px;display:flex;transition:.3s;flex-direction:column}.tk-appt-window__wrapper-body__scroll .card-container:hover{box-shadow:0px 4px 20px rgba(0,0,0,.2);cursor:pointer}.tk-appt-window__wrapper-body__scroll .card-container:hover .card-title{color:#214de6}.tk-appt-window__wrapper-body__scroll .card-wrapper{display:grid;cursor:pointer;position:relative;grid-template-columns:repeat(auto-fit, minmax(220px, 1fr))}.tk-appt-window__wrapper-body__scroll .card-wrapper .card-container-image{flex-basis:100%;background-color:#fff;border-radius:8px;box-shadow:0 4px 20px 0 rgba(0,0,0,.05);margin:5px;padding:8px;display:flex;flex-direction:column;transition:.3s}.tk-appt-window__wrapper-body__scroll .card-wrapper .card-container-image .card-image-container{height:auto;text-align:center}.tk-appt-window__wrapper-body__scroll .card-wrapper .card-container-image .card-image-container .card-image{width:100%;overflow:hidden;object-fit:cover;border-radius:4px;background-color:#ebebee}.tk-appt-window__wrapper-actions{display:flex;flex-direction:row;justify-content:space-between}.tk-appt-window__wrapper-actions i.back-icon,.tk-appt-window__wrapper-actions i.close-icon{width:28px;height:28px;display:block;font-weight:normal}.tk-appt-window__wrapper-actions__left{margin:8px 0px;transition:.3s}.tk-appt-window__wrapper-actions__left i.back-icon{width:0;height:0}.tk-appt-window__wrapper-actions__left i.back-icon.show{width:28px;height:28px}.tk-appt-window__wrapper-actions__right{width:28px;height:28px;margin:8px 0px;transition:.3s}.tk-appt-window__wrapper-header{display:flex;padding:10px;position:relative;flex-direction:column}.tk-appt-window__wrapper-header .search-bar-wrapper{position:relative}.tk-appt-window__wrapper-header .search-bar-wrapper #search-bar{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-top:4px;padding:10px 30px 11px 8px;border-radius:8px;border:none;box-shadow:0 4px 20px 0 rgba(0,0,0,.05);background-color:#fff;font-size:14px;font-family:"Open Sans";width:-webkit-fill-available;padding-left:30px;transition:.3s}.tk-appt-window__wrapper-header .search-bar-wrapper #search-bar:hover{box-shadow:0px 4px 20px rgba(0,0,0,.2)}.tk-appt-window__wrapper-header .search-bar-wrapper #search-bar::placeholder{font-size:14px;color:#c7c7cc;font-family:"Open Sans"}.tk-appt-window__wrapper-header .search-bar-wrapper .geolocation-button{top:2px;border:0;display:flex;cursor:pointer;font-size:14px;text-align:left;font-weight:600;padding:9px 30px;position:relative;border-radius:3px;font-family:"Open Sans";box-shadow:0px 0px 4px rgba(0,0,0,.25);background:url(/build/332836a9633ba75d29ec.svg) no-repeat scroll 6px 6px,#fff}.tk-appt-window__wrapper-header .search-bar-wrapper .geolocation-button.hide{display:none}.tk-appt-window__wrapper-header .search-bar-wrapper.hide{display:none}.tk-appt-window__wrapper-header__nav{display:flex;margin:16px 0px;flex-direction:row;justify-content:flex-start}.tk-appt-window__wrapper-header__nav-point{width:9px;height:9px;border-radius:50%;background-color:#c7c7cc}.tk-appt-window__wrapper-header__nav-point.active{background-color:blue}.tk-appt-window__wrapper-header__nav-line{flex:2;height:1px;margin-top:auto;margin-bottom:auto;background-color:#c7c7cc}.tk-appt-window__wrapper-header__nav-line.active{background-color:blue}.tk-appt-window__wrapper-header__selected{font-family:"Open Sans";font-weight:normal;font-stretch:normal;font-style:normal;line-height:normal;letter-spacing:normal;text-align:left;text-align:left;color:#8e8e93;margin:16px 0px;font-size:12px}.tk-appt-window__wrapper-header__title{font-family:"Open Sans";font-size:18px;text-rendering:geometricPrecision;font-weight:bold;font-stretch:normal;font-style:normal;line-height:normal;letter-spacing:normal;text-align:left;color:#14141a}.tk-appt-window__wrapper-header__summary{width:100%;margin:8px 0px;font-family:"Open Sans";font-size:14px;font-weight:normal;font-stretch:normal;font-style:normal;line-height:1.5;letter-spacing:normal;text-align:left;color:#8e8e93;overflow:hidden}.tk-appt-window #tk-bot-btn{width:65px;height:65px;font-size:40px;border-radius:50%;color:#fff;display:flex;position:fixed;align-items:center;text-decoration:none;justify-content:center;right:20px;bottom:20px;border:none;outline:none;cursor:pointer;background-size:cover;background-position:center;transition:background-color .25s ease-out,box-shadow .25s ease-out;box-shadow:0 1px 3px 0 rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 2px 1px -1px rgba(0,0,0,.12)}.tk-appt-window #tk-bot-btn:hover{box-shadow:0 4px 5px -2px rgba(0,0,0,.2),0 7px 10px 1px rgba(0,0,0,.14),0 2px 16px 1px rgba(0,0,0,.12)}
2 | @font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/acfea1d2352591b09c46.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+0460-052F,U+1C80-1C88,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/35d9aa74a5c9697bc38c.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/6e872659616d9b8fb28c.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+1F00-1FFF}@font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/accce9b81f55c6f4779c.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+0370-03FF}@font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/608be94da46d7cd7a003.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+0590-05FF,U+200C-2010,U+20AA,U+25CC,U+FB1D-FB4F}@font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/6517a5545bd31eeb4a61.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+1EA0-1EF9,U+20AB}@font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/8deeb3884c8ce11a3537.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+0100-024F,U+0259,U+1E00-1EFF,U+2020,U+20A0-20AB,U+20AD-20CF,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:"Open Sans";font-style:normal;font-display:swap;font-weight:400;src:url(/build/4d140983c7c8c4a8c722.woff2) format("woff2"),url(/build/3bc4108fa8f9a72e6ea5.woff) format("woff");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
3 |
--------------------------------------------------------------------------------
/public/build/appointments.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * Timekit JavaScript SDK
3 | * http://timekit.io
4 | *
5 | * Copyright 2015 Timekit, Inc.
6 | * The Timekit JavaScript SDK is freely distributable under the MIT license.
7 | *
8 | */
9 |
10 | /*! https://mths.be/base64 v1.0.0 by @mathias | MIT license */
11 |
--------------------------------------------------------------------------------
/public/build/booking.min.js.LICENSE.txt:
--------------------------------------------------------------------------------
1 | /*!
2 | * Timekit JavaScript SDK
3 | * http://timekit.io
4 | *
5 | * Copyright 2015 Timekit, Inc.
6 | * The Timekit JavaScript SDK is freely distributable under the MIT license.
7 | *
8 | */
9 |
10 | /*! https://mths.be/base64 v1.0.0 by @mathias | MIT license */
11 |
12 | //! Copyright (c) JS Foundation and other contributors
13 |
14 | //! github.com/moment/moment-timezone
15 |
16 | //! license : MIT
17 |
18 | //! moment-timezone.js
19 |
20 | //! moment.js
21 |
22 | //! moment.js locale configuration
23 |
24 | //! version : 0.5.36
25 |
26 | //! version : 0.5.37
27 |
--------------------------------------------------------------------------------
/public/callbacks.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js callbacks example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/public/fields.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js customer fields example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/public/group-bookings.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js group bookings example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Timekit
6 |
7 |
8 |
9 |
10 |
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 | Timekit makes it easy for customers to schedule appointments with ease.
21 | Setup your own booking page in a matter of minutes!
22 |
23 |
24 |
25 | Signup now
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/public/list-view.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js list view example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/public/local-language.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js FullCalendar language support
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/public/local-preset.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js localization preset example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/public/local-strings.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js localization strings example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/multiple.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js multiple instances example
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
29 |
30 |
31 |
32 |
45 |
46 |
47 |
48 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/public/remote-project.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js remote project example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/public/selectable-length.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js single instance example
6 |
7 |
8 |
49 |
50 |
51 |
61 |
62 |
63 |
64 |
65 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/public/single.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js single instance example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/public/team-availability.htm:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Booking.js team availability example
6 |
7 |
8 |
11 |
12 |
13 |
14 |
15 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/booking/assets/arrow-down-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shape
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/booking/assets/checkmark-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/booking/assets/close-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | close-icon
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/booking/assets/error-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | error-icon
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/booking/assets/error-warning-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | error-warning-icon
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/booking/assets/loading-spinner.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/booking/assets/timekit-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | timekit-logo
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/booking/assets/timezone-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Shape
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/booking/configs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const listPlugin = require("@fullcalendar/list").default;
4 | const momentPlugin = require("@fullcalendar/moment").default;
5 | const dayGridPlugin = require("@fullcalendar/daygrid").default;
6 | const timeGridPlugin = require("@fullcalendar/timegrid").default;
7 |
8 | /*
9 | * Default configuration
10 | */
11 | const primary = {
12 | name: '',
13 | debug: false,
14 | autoload: true,
15 | el: '#bookingjs',
16 | disable_remote_load: false,
17 | disable_confirm_page: false,
18 | create_booking_response_include: ['attributes', 'event', 'user'],
19 | ui: {
20 | display_name: '',
21 | show_timezone_helper: true,
22 | availability_view: "agendaWeek",
23 | localization: {
24 | reschedule_submit_button: 'Confirm Reschedule',
25 | reschedule_success_message: 'We have reschduled your booking successfully',
26 | }
27 | },
28 | booking: {},
29 | callbacks: {},
30 | reschedule: {},
31 | availability: {},
32 | customer_fields: {},
33 | sdk: {
34 | headers: {
35 | 'Timekit-Context': 'widget'
36 | }
37 | },
38 | fullcalendar: {
39 | allDaySlot: false,
40 | nowIndicator: true,
41 | scrollTime: '08:00:00',
42 | plugins: [timeGridPlugin, listPlugin, momentPlugin, dayGridPlugin],
43 | views: {
44 | agenda: {
45 | displayEventEnd: false,
46 | },
47 | listing: {
48 | type: 'list',
49 | listDayAltFormat: 'dddd',
50 | duration: { days: 365 / 2 },
51 | noEventsMessage: 'No timeslots available'
52 | }
53 | },
54 | }
55 | };
56 |
57 | const primaryWithoutProject = {
58 | ui: {
59 | avatar: '',
60 | display_name: '',
61 | show_credits: true,
62 | show_timezone_helper: true,
63 | time_date_format: '12h-mdy-sun',
64 | availability_view: 'timeGridWeek',
65 | localization: {
66 | submit_button: 'Book it',
67 | allocated_resource_prefix: 'with',
68 | reschedule_submit_button: 'Reschedule',
69 | reschedule_success_message: 'We have reschduled your booking successfully',
70 | success_message: 'We have received your booking and sent a confirmation to %s'
71 | }
72 | },
73 | reschedule: {},
74 | availability: {
75 | mode: 'roundrobin_random'
76 | },
77 | booking: {
78 | graph: 'instant'
79 | },
80 | customer_fields: {
81 | name: {
82 | title: 'Name',
83 | required: true,
84 | split_name: false
85 | },
86 | email: {
87 | title: 'E-mail',
88 | format: 'email',
89 | required: true
90 | }
91 | }
92 | }
93 |
94 | const customerFieldsNativeFormats = {
95 | name: {
96 | format: 'string'
97 | },
98 | email: {
99 | format: 'email'
100 | },
101 | comment: {
102 | format: 'textarea'
103 | },
104 | phone: {
105 | format: 'tel'
106 | }
107 | }
108 |
109 | // Preset: timeDateFormat = '24h-dmy-mon'
110 | const timeDateFormat24hdmymon = {
111 | ui: {
112 | display_name: '',
113 | booking_time_format: 'HH:mm',
114 | availability_view: "agendaWeek",
115 | booking_date_format: 'D. MMMM YYYY'
116 | },
117 | fullcalendar: {
118 | firstDay: 1,
119 | eventTimeFormat: {
120 | hour12: false,
121 | meridiem: false,
122 | hour: '2-digit',
123 | minute: '2-digit',
124 | },
125 | views: {
126 | week: {
127 | dayHeaderFormat: {
128 | weekday: 'long',
129 | month: 'numeric',
130 | day: 'numeric',
131 | omitCommas: true
132 | }
133 | },
134 | timeGrid: {
135 | dayHeaderFormat: {
136 | weekday: 'short',
137 | month: 'numeric',
138 | day: 'numeric',
139 | omitCommas: true
140 | },
141 | slotLabelFormat: {
142 | hour12: false,
143 | hour: '2-digit',
144 | minute: '2-digit',
145 | }
146 | }
147 | }
148 | }
149 | };
150 |
151 | // Preset: timeDateFormat = '12h-mdy-sun'
152 | const timeDateFormat12hmdysun = {
153 | ui: {
154 | display_name: '',
155 | booking_time_format: 'hh:mma',
156 | availability_view: "agendaWeek",
157 | booking_date_format: 'MMMM D, YYYY'
158 | },
159 | fullcalendar: {
160 | firstDay: 0,
161 | eventTimeFormat: { // like '14:30'
162 | hour: '2-digit',
163 | minute: '2-digit',
164 | meridiem: 'short'
165 | },
166 | views: {
167 | week: {
168 | dayHeaderFormat: {
169 | weekday: 'long',
170 | month: 'numeric',
171 | day: 'numeric',
172 | omitCommas: true
173 | }
174 | },
175 | timeGrid: {
176 | dayHeaderFormat: {
177 | weekday: 'short',
178 | month: 'numeric',
179 | day: 'numeric',
180 | omitCommas: true
181 | },
182 | },
183 | dayGridDay: {
184 | dayHeaderFormat: {
185 | weekday: 'short',
186 | month: 'numeric',
187 | day: 'numeric',
188 | omitCommas: true
189 | },
190 | }
191 | }
192 | }
193 | };
194 |
195 | // Preset: availabilityView = 'timeGridWeek'
196 | const availabilityViewAgendaWeek = {
197 | fullcalendar: {
198 | initialView: 'timeGridWeek',
199 | headerToolbar: {
200 | left: '',
201 | center: '',
202 | right: 'today, prev, next'
203 | },
204 | }
205 | }
206 |
207 | // Preset: availabilityView = 'listing'
208 | const availabilityViewListing = {
209 | fullcalendar: {
210 | initialView: 'listWeek',
211 | headerToolbar: {
212 | left: '',
213 | center: '',
214 | right: 'today prev,next'
215 | },
216 | }
217 | }
218 |
219 | // Export objects
220 | module.exports = {
221 | primary: primary,
222 | primaryWithoutProject: primaryWithoutProject,
223 | customerFieldsNativeFormats: customerFieldsNativeFormats,
224 | presets: {
225 | timeDateFormat: {
226 | '24h-dmy-mon': timeDateFormat24hdmymon,
227 | '12h-mdy-sun': timeDateFormat12hmdysun
228 | },
229 | availabilityView: {
230 | 'listing': availabilityViewListing,
231 | 'agendaWeek': availabilityViewAgendaWeek
232 | }
233 | }
234 | };
235 |
--------------------------------------------------------------------------------
/src/booking/helpers/base.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment-timezone');
2 |
3 | class BaseTemplate {
4 |
5 | parseHtmlTags(field) {
6 | if (field.format === 'label') {
7 | field.title = field.title.replace(/\(\((.*?)\)\)/g, function(match, token) {
8 | var linkTag = token.split(',');
9 | return '' + linkTag[0].trim() + ' ';
10 | });
11 | }
12 | return field;
13 | }
14 |
15 | htmlToElement(html) {
16 | const template = document.createElement('template');
17 | html = html.trim();
18 | template.innerHTML = html;
19 | return template.content.firstChild;
20 | }
21 |
22 | formatTimestamp(start, format = '') {
23 | return moment.utc(start).utcOffset(start.substring(19)).format(format);
24 | }
25 |
26 | initCloseButton(page) {
27 | const form = page.querySelector('.bookingjs-form');
28 | const closeButton = page.querySelector('.bookingjs-bookpage-close');
29 |
30 | closeButton.addEventListener('click', (e) => {
31 | e.preventDefault();
32 | if (form.classList.contains('success')) {
33 | this.template.getAvailability();
34 | }
35 | this.hidePageModel(page);
36 | });
37 | }
38 |
39 | initFormValidation(form) {
40 | const formElements = form.querySelectorAll('.bookingjs-form-input');
41 | const selectElements = form.querySelectorAll('.bookingjs-form-input--select');
42 | const formCheckBoxElements = form.querySelectorAll('.bookingjs-form-field--checkbox-multi');
43 |
44 | for (let i = 0; i < formElements.length; i++) {
45 | if (formElements[i].value && formElements[i].value.trim()) {
46 | formElements[i].classList.remove('field-required');
47 | }
48 | formElements[i].addEventListener("input", function(e) {
49 | const field = e.target.closest('.bookingjs-form-field');
50 | if (e.target.value) {
51 | e.target.classList.remove('field-required');
52 | field.classList.add('bookingjs-form-field--dirty');
53 | } else {
54 | e.target.classList.add('field-required');
55 | field.classList.remove('bookingjs-form-field--dirty')
56 | };
57 | e.preventDefault();
58 | });
59 | }
60 |
61 | for (let j = 0; j < formCheckBoxElements.length; j++) {
62 | if (formCheckBoxElements[j].value && formCheckBoxElements[j].value.trim()) {
63 | formCheckBoxElements[j].classList.remove('field-required');
64 | }
65 | formCheckBoxElements[j].addEventListener("change", function(e) {
66 | if (e.target.checked) {
67 | e.target.classList.remove('field-required');
68 | formCheckBoxElements[j].removeAttribute('required');
69 | } else {
70 | e.target.classList.add('field-required');
71 | formCheckBoxElements[j].setAttribute('required', 'required');
72 | }
73 | e.preventDefault();
74 | });
75 | }
76 |
77 | for (let k = 0; k < selectElements.length; k++) {
78 | if (selectElements[k].value && selectElements[k].value.trim()) {
79 | selectElements[k].classList.remove('field-required');
80 | }
81 | selectElements[k].addEventListener("change", function(e) {
82 | if (e.target.value) {
83 | e.target.classList.remove('field-required');
84 | } else {
85 | e.target.classList.add('field-required');
86 | }
87 | e.preventDefault();
88 | });
89 | }
90 | }
91 |
92 | showBookingFailed(form, error) {
93 | const submitButton = form.querySelector('.bookingjs-form-button');
94 |
95 | submitButton.classList.add('button-shake');
96 | setTimeout(() => submitButton.classList.remove('button-shake'), 500);
97 |
98 | form.classList.add('loading');
99 | form.classList.add('error');
100 |
101 | setTimeout(() => form.classList.remove('error'), 2000);
102 |
103 | this.utils.logDebug(['Booking Error:', error]);
104 | }
105 |
106 | prepareFormFields(form) {
107 | const formData = {};
108 | const formElements = Array.from(form.elements);
109 |
110 | for(let i=0; i page.remove(), 200);
144 | }
145 | }
146 |
147 | module.exports = BaseTemplate;
--------------------------------------------------------------------------------
/src/booking/helpers/config.js:
--------------------------------------------------------------------------------
1 | const get = require("lodash/get");
2 | const qs = require('querystringify');
3 | const merge = require("lodash/merge");
4 |
5 | class Config {
6 | constructor() {
7 | this.config = {};
8 | this.global = null;
9 | this.defaultConfigs = require('../configs');
10 | }
11 |
12 | all() {
13 | return this.config;
14 | }
15 |
16 | get(key) {
17 | return get(this.config, key);
18 | }
19 |
20 | getGlobal(key) {
21 | return get(this.global, key);
22 | }
23 |
24 | set(configs) {
25 | return (this.config = this.setDefaults(configs));
26 | }
27 |
28 | setGlobal(value) {
29 | this.global = value;
30 | return this;
31 | }
32 |
33 | parseAndUpdate(suppliedConfig) {
34 |
35 | // Extend the default config with supplied settings
36 | let newConfig = this.setDefaults(suppliedConfig);
37 |
38 | // Apply presets
39 | newConfig = this.applyConfigPreset(newConfig, 'timeDateFormat', newConfig.ui.time_date_format)
40 | newConfig = this.applyConfigPreset(newConfig, 'availabilityView', newConfig.ui.availability_view)
41 |
42 | // Set default formats for native fields
43 | newConfig = this.setCustomerFieldsNativeFormats(newConfig);
44 |
45 | // Check for required settings
46 | if (!newConfig.app_key) throw 'A required config setting ("app_key") was missing';
47 |
48 | // Prefill fields based on query string
49 | const urlParams = this.getGlobal("location") && this.getGlobal("location.search");
50 | if (urlParams) newConfig = this.applyPrefillFromUrlGetParams(newConfig, qs.parse(urlParams));
51 |
52 | return this.set(newConfig);
53 | }
54 |
55 | setDefaultsWithoutProject(suppliedConfig) {
56 | return merge({}, this.defaultConfigs.primaryWithoutProject, suppliedConfig);
57 | }
58 |
59 | applyPrefillFromUrlGetParams(suppliedConfig, urlParams) {
60 | const customerFields = suppliedConfig.customer_fields;
61 | const customerFieldsKeys = Object.keys(customerFields);
62 |
63 | for(let i=0; i < customerFieldsKeys.length; i++) {
64 | const key = customerFieldsKeys[i];
65 |
66 | if (!urlParams['customer.' + key]) continue;
67 | suppliedConfig.customer_fields[key].prefilled = urlParams['customer.' + key];
68 | }
69 |
70 | return suppliedConfig;
71 | }
72 |
73 | setCustomerFieldsNativeFormats(config) {
74 | const customerFields = config.customer_fields;
75 | const customerFieldsKeys = Object.keys(customerFields);
76 |
77 | for(let i=0; i < customerFieldsKeys.length; i++) {
78 | const key = customerFieldsKeys[i];
79 | const field = customerFields[customerFieldsKeys[i]];
80 |
81 | if (!this.defaultConfigs.customerFieldsNativeFormats[key]) continue;
82 | config.customer_fields[key] = merge(this.defaultConfigs.customerFieldsNativeFormats[key], field);
83 | }
84 |
85 | return config;
86 | }
87 |
88 | applyConfigPreset(localConfig, propertyName, propertyObject) {
89 | const presetCheck = this.defaultConfigs.presets[propertyName][propertyObject];
90 | if (presetCheck) return merge({}, presetCheck, localConfig);
91 | return localConfig;
92 | }
93 |
94 | setDefaults(suppliedConfig) {
95 | suppliedConfig.sdk = this.prepareSdkConfig(suppliedConfig);
96 | return merge({}, this.defaultConfigs.primary, suppliedConfig);
97 | }
98 |
99 | prepareSdkConfig(suppliedConfig) {
100 | if (typeof suppliedConfig.sdk === 'undefined') {
101 | suppliedConfig.sdk = {};
102 | }
103 | if (suppliedConfig.app_key) {
104 | suppliedConfig.sdk.appKey = suppliedConfig.app_key;
105 | }
106 | if (suppliedConfig.api_base_url) {
107 | suppliedConfig.sdk.apiBaseUrl = suppliedConfig.api_base_url;
108 | }
109 | return merge({}, this.defaultConfigs.primary.sdk, suppliedConfig.sdk);
110 | }
111 | }
112 |
113 | module.exports = Config;
--------------------------------------------------------------------------------
/src/booking/helpers/util.js:
--------------------------------------------------------------------------------
1 | const isEmpty = require("lodash/isEmpty");
2 |
3 | class Util {
4 | constructor(config) {
5 | this.config = config;
6 | }
7 |
8 | isFunction(object) {
9 | return !!(object && object.constructor && object.call && object.apply);
10 | }
11 |
12 | isArray(object) {
13 | return object && object.constructor === Array;
14 | }
15 |
16 | logError(message) {
17 | console.warn('TimekitBooking Error: ', message);
18 | }
19 |
20 | logDeprecated(message) {
21 | console.warn('TimekitBooking Deprecated: ', message);
22 | }
23 |
24 | logDebug(message) {
25 | if (this.config.get('debug')) {
26 | console.log('TimekitBooking Debug: ', message);
27 | }
28 | }
29 |
30 | logDeprecatedfunction(message) {
31 | console.warn('TimekitBooking Deprecated: ', message);
32 | }
33 |
34 | isEmbeddedProject(suppliedConfig) {
35 | return typeof suppliedConfig.project_id !== 'undefined'
36 | }
37 |
38 | isHostedProject(suppliedConfig) {
39 | return typeof suppliedConfig.project_slug !== 'undefined'
40 | }
41 |
42 | isRemoteProject(suppliedConfig) {
43 | return (this.isEmbeddedProject(suppliedConfig) || this.isHostedProject(suppliedConfig))
44 | }
45 |
46 | doesConfigExist(suppliedConfig) {
47 | return (suppliedConfig !== undefined && typeof suppliedConfig === 'object' && !isEmpty(suppliedConfig));
48 | }
49 |
50 | doCallback(hook, arg, deprecated) {
51 | if(this.config.get('callbacks') && this.isFunction(this.config.get('callbacks.' + hook))) {
52 | if (deprecated) {
53 | this.logDeprecated(hook + ' callback has been replaced, please see docs');
54 | }
55 | this.config.get('callbacks.' + hook)(arg);
56 | }
57 | this.logDebug(['Trigger callback "' + hook + '" with arguments:', arg]);
58 | }
59 | }
60 |
61 | module.exports = Util;
--------------------------------------------------------------------------------
/src/booking/index.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | const BookingWidget = require('./widget');
5 | const globalLibraryConfig = window.timekitBookingConfig;
6 |
7 | // Autoload if config is available on window, else export function
8 | if (window && globalLibraryConfig && globalLibraryConfig.autoload !== false) {
9 | const instance = new BookingWidget();
10 | module.exports = instance.init(globalLibraryConfig);
11 | } else {
12 | module.exports = BookingWidget;
13 | }
--------------------------------------------------------------------------------
/src/booking/pages/reschedule.js:
--------------------------------------------------------------------------------
1 | const moment = require('moment-timezone');
2 | const interpolate = require('sprintf-js');
3 | const BaseTemplate = require('../helpers/base');
4 |
5 | class BookingReschdulePage extends BaseTemplate {
6 | constructor(template) {
7 | super();
8 | this.template = template;
9 | this.utils = template.utils;
10 | this.config = template.config;
11 | this.bookingReschdulePageTarget = null;
12 | }
13 |
14 | initBookingAndRender(eventData) {
15 | const dateFormat = this.config.get('ui.booking_date_format') || moment.localeData().longDateFormat('LL');
16 | const timeFormat = this.config.get('ui.booking_time_format') || moment.localeData().longDateFormat('LT');
17 |
18 | eventData.oldDate = this.formatTimestamp(this.config.get('reschedule.eventStart'), dateFormat);
19 | eventData.oldTime = this.formatTimestamp(this.config.get('reschedule.eventStart'), timeFormat) + ' - ' + this.formatTimestamp(this.config.get('reschedule.eventEnd'), timeFormat);
20 |
21 | return this.render(eventData);
22 | }
23 |
24 | render(eventData) {
25 | this.utils.doCallback('showBookingReschdulePage', eventData);
26 |
27 | const template = require('../templates/reschedule-page.html');
28 | const successMessage = this.config.get('ui.localization.reschedule_success_message');
29 | const dateFormat = this.config.get('ui.booking_date_format') || moment.localeData().longDateFormat('LL');
30 | const timeFormat = this.config.get('ui.booking_time_format') || moment.localeData().longDateFormat('LT');
31 | const allocatedResource = eventData.extendedProps.resources ? eventData.extendedProps.resources[0].name : false;
32 |
33 | this.bookingReschdulePageTarget = this.htmlToElement(
34 | template({
35 | oldTime: eventData.oldTime,
36 | oldDate: eventData.oldDate,
37 | allocatedResource: allocatedResource,
38 | chosenDate: this.formatTimestamp(eventData.startStr, dateFormat),
39 | closeIcon: require('!svg-inline-loader!../assets/close-icon.svg'),
40 | errorIcon: require('!svg-inline-loader!../assets/error-icon.svg'),
41 | submitText: this.config.get('ui.localization.reschedule_submit_button'),
42 | loadingIcon: require('!svg-inline-loader!../assets/loading-spinner.svg'),
43 | checkmarkIcon: require('!svg-inline-loader!../assets/checkmark-icon.svg'),
44 | allocatedResourcePrefix: this.config.get('ui.localization.allocated_resource_prefix'),
45 | chosenTime: this.formatTimestamp(eventData.startStr, timeFormat) + ' - ' + this.formatTimestamp(eventData.endStr, timeFormat),
46 | successMessage: interpolate.sprintf(
47 | successMessage.indexOf('%s') !== -1 ? successMessage : successMessage + ' %s', ' '
48 | )
49 | })
50 | );
51 |
52 | this.renderCustomerFields(eventData);
53 | this.initCloseButton(this.bookingReschdulePageTarget);
54 | this.template.rootTarget.append(this.bookingReschdulePageTarget);
55 |
56 | this.template.rootTarget.addEventListener("customer-timezone-changed", (e) => {
57 | e.preventDefault();
58 | if (!this.bookingReschdulePageTarget) return;
59 |
60 | const formerBookingDate = this.bookingReschdulePageTarget.querySelector('.former-booking-date');
61 | const currentBookingDate = this.bookingReschdulePageTarget.querySelector('.current-booking-date');
62 |
63 | currentBookingDate.innerHTML = this.formatTimestamp(eventData.startStr, dateFormat);
64 | formerBookingDate.innerHTML = this.formatTimestamp(this.config.get('reschedule.eventStart'), dateFormat);
65 |
66 | const formerBookingTime = this.bookingReschdulePageTarget.querySelector('.former-booking-time');
67 | const currentBookingTime = this.bookingReschdulePageTarget.querySelector('.current-booking-time');
68 |
69 | currentBookingTime.innerHTML = this.formatTimestamp(eventData.startStr, timeFormat) + ' - ' + this.formatTimestamp(eventData.endStr, timeFormat);
70 | formerBookingTime.innerHTML = this.formatTimestamp(this.config.get('reschedule.eventStart'), timeFormat) + ' - ' + this.formatTimestamp(this.config.get('reschedule.eventEnd'), timeFormat);
71 | });
72 |
73 | setTimeout(() => this.bookingReschdulePageTarget.classList.add('show'), 100);
74 |
75 | return this;
76 | }
77 |
78 | renderCustomerFields(eventData) {
79 | const textareaTemplate = require('../templates/fields/textarea.html');
80 | const form = this.bookingReschdulePageTarget.querySelector('.bookingjs-form');
81 | const formFieldsEle = this.bookingReschdulePageTarget.querySelector('.bookingjs-form-fields');
82 |
83 | formFieldsEle.append(this.htmlToElement(textareaTemplate({
84 | key: 'message',
85 | format: 'text',
86 | prefilled: '',
87 | required: true,
88 | readonly: false,
89 | title: 'Message to the host',
90 | arrowDownIcon: require('!svg-inline-loader!../assets/arrow-down-icon.svg'),
91 | })));
92 |
93 | this.initFormValidation(form);
94 | form.addEventListener("submit", e => this.submitForm(e, eventData));
95 | }
96 |
97 | submitForm(e, eventData) {
98 | e.preventDefault();
99 |
100 | const form = e.target;
101 | const formData = this.prepareFormFields(form);
102 |
103 | // close the form if submitted
104 | if (form.classList.contains('success') || !this.config.get('reschedule.uuid')) {
105 | this.template.getAvailability();
106 | this.initCloseButton(this.bookingReschdulePageTarget);
107 | return;
108 | }
109 |
110 | form.classList.add('loading');
111 | this.utils.doCallback('submitBookingReschduleForm', formData);
112 |
113 | // Call create event endpoint
114 | this.timekitRescheduleBooking(formData, eventData)
115 | .then(() => {
116 | form.classList.remove('loading');
117 | form.classList.add('success');
118 | setTimeout(() => (window.location.href = window.location.href.split('?')[0]), 500);
119 | })
120 | .catch((error) => this.showBookingFailed(form, error));
121 | }
122 |
123 | timekitRescheduleBooking(formData, eventData) {
124 | const extendedProps = eventData.extendedProps;
125 | const end = moment(eventData.endStr).tz(this.template.customerTimezoneSelected);
126 | const start = moment(eventData.startStr).tz(this.template.customerTimezoneSelected);
127 |
128 | const payLoad = {
129 | end: end.format(),
130 | start: start.format(),
131 | message: formData.message,
132 | resource_id: extendedProps.resources[0].id
133 | };
134 |
135 | this.utils.doCallback('submitReschduleBookingStarted', payLoad);
136 |
137 | const request = this.template.sdk
138 | .include(this.config.get('create_booking_response_include'))
139 | .makeRequest({
140 | data: payLoad,
141 | method: 'post',
142 | url: '/bookings/' + this.config.get('reschedule.uuid') + '/reschedule',
143 | });
144 |
145 | request
146 | .then((response) => this.utils.doCallback('submitReschduleBookingSuccessful', response))
147 | .catch((response) => {
148 | this.utils.doCallback('submitReschduleBookingFailed', response);
149 | this.template.triggerError([
150 | 'An error with Timekit Reschdule Booking occured',
151 | response,
152 | ]);
153 | });
154 |
155 | return request;
156 | }
157 | }
158 |
159 | module.exports = BookingReschdulePage;
--------------------------------------------------------------------------------
/src/booking/styles/fullcalendar.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 |
3 | // Classes
4 | .fc-theme-standard .fc-scrollgrid {
5 | border-top: 0;
6 | }
7 |
8 | .fc-view-container {
9 | background-color: $bgColor;
10 | color: $textColor;
11 | }
12 |
13 | .fc-col-header {
14 | border-bottom: 1px solid $borderColor;
15 | .fc-day {
16 | font-size: 12px;
17 | font-weight: 600;
18 | color: $subtleColor;
19 | }
20 | }
21 |
22 | .fc-timegrid-slot-label-cushion {
23 | color: $subtleColor;
24 | font-size: 0.9em;
25 | }
26 |
27 | .fc-state-default {
28 | text-shadow: none;
29 | box-shadow: none;
30 | background-image: none;
31 | background-color: white;
32 | border-color: white;
33 | }
34 |
35 | .fc-button {
36 | text-transform: uppercase;
37 | font-weight: 600;
38 | font-size: 1.1em;
39 | border: 0px;
40 | outline: none;
41 | &:hover,
42 | &:visited,
43 | &:active,
44 | &:focus {
45 | outline: none;
46 | border: 0px;
47 | background-color: transparent;
48 | }
49 | }
50 |
51 | .fc-content-skeleton {
52 | border-top: 1px solid #DDD;
53 | }
54 |
55 | .fc .fc-toolbar.fc-header-toolbar {
56 | padding: 0px;
57 | margin-bottom: 0;
58 | min-height: 48px;
59 | border-bottom: 1px solid $borderColor;
60 |
61 | .fc--button {
62 | display: none;
63 | }
64 |
65 | .fc-button {
66 | border: 0;
67 | opacity: 0.3;
68 | height: auto;
69 | outline: none;
70 | color: #333;
71 | margin-left: 0;
72 | font-size: 1em;
73 | font-weight: bold;
74 | padding: 15px 17px;
75 | background-color: white;
76 | text-transform: uppercase;
77 | transition: opacity 0.2s ease;
78 |
79 | &.fc-today-button {
80 | padding: 16px 5px;
81 | }
82 |
83 | &:hover {
84 | opacity: 1;
85 | }
86 |
87 | &.fc-state-disabled {
88 | transition: opacity 0s;
89 | opacity: 0;
90 | }
91 |
92 | &.fc-prev-button {
93 | padding-right: 8px;
94 | }
95 |
96 | &.fc-next-button {
97 | padding-left: 8px;
98 | }
99 |
100 | .fc-icon {
101 | font-size: 150%;
102 | top: -7%;
103 | }
104 | }
105 |
106 | > .fc-right > button.fc-today-button {
107 | padding: 16px 5px;
108 | }
109 |
110 | > .fc-right h2 {
111 | font-size: 13px;
112 | padding: 15px 0px 15px 20px;
113 | color: $textColor;
114 | font-weight: 600;
115 | }
116 |
117 | }
118 |
119 | .fc-theme-standard td.fc-today {
120 | background: white;
121 | }
122 |
123 | .fc-body > tr > .fc-widget-content,
124 | .fc-head > tr > .fc-widget-header {
125 | border: 0 !important;
126 | }
127 |
128 | .fc th {
129 | border-color: white;
130 | }
131 |
132 | .fc-theme-standard .fc-divider,
133 | .fc-theme-standard .fc-popover .fc-header {
134 | background-color: transparent;
135 | }
136 |
137 | .empty-calendar .fc-event {
138 | opacity: 0;
139 | }
140 |
141 | .fc-event {
142 | transition: color .2s ease, border-color .2s ease, opacity .6s ease, box-shadow .2s ease;
143 | border: none;
144 | border-left: 2px solid darken($borderColor, 35%);
145 | padding: 3px;
146 | background-color: white;
147 | border-radius: 3px;
148 | margin: 1px 0;
149 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.07);
150 | cursor: pointer;
151 | margin-bottom: 2px;
152 | opacity: 1;
153 |
154 | &.fc-v-event {
155 | .fc-event-main {
156 | color: $textColor;
157 | }
158 | }
159 |
160 | &:hover,
161 | &-clicked {
162 | box-shadow: 0 2px 4px rgba(0,0,0,.12);
163 | border-left: 3px solid $primaryColor;
164 | color: $primaryColor;
165 | font-weight: 600;
166 | padding-left: 2px;
167 | }
168 |
169 | .fc-content {
170 | transform: translateX(0);
171 | transition: transform .2s ease;
172 | }
173 |
174 | &:hover .fc-content {
175 | transform: translateX(2px);
176 | }
177 |
178 | .fc-bg {
179 | opacity: 0;
180 | }
181 | }
182 |
183 | .fc-daygrid-event {
184 | margin: 1px 0 3px;
185 | padding: 10px 15px;
186 |
187 | .fc-daygrid-event-dot {
188 | display: none;
189 | }
190 |
191 | &:hover,
192 | &-clicked {
193 | padding-left: 14px;
194 | }
195 |
196 | .fc-event-time {
197 | font-size: 12px;
198 | font-weight: 500;
199 | }
200 |
201 | .fc-event-title {
202 | padding: 0 5px 5px;
203 | font-size: 12px;
204 | font-weight: 500;
205 | }
206 |
207 | &:hover .fc-event-time,
208 | &-clicked .fc-event-time,
209 | &:hover .fc-event-title,
210 | &-clicked .fc-event-title {
211 | font-weight: 600;
212 | }
213 | }
214 |
215 | .fc-timegrid-body .fc-timegrid-slots {
216 | td.fc-timegrid-slot-minor {
217 | border-top-style: none;
218 | }
219 | td {
220 | border-top-color: $bgColor;
221 | &.fc-axis {
222 | border-top-color: $borderColor;
223 | }
224 | }
225 | }
226 |
227 | .fc-time-grid-event {
228 |
229 | &.fc-short {
230 |
231 | .fc-content {
232 | font-size: 0.7em;
233 | line-height: 0.2em;
234 | }
235 |
236 | .fc-time:after {
237 | content: '';
238 | }
239 | }
240 |
241 | .fc-time {
242 | font-size: 1.1em;
243 | padding: 5px;
244 | }
245 |
246 | .fc-title {
247 | padding: 0 5px 5px;
248 | font-weight: bold;
249 | }
250 | }
251 |
252 | .fc-theme-standard th, .fc-theme-standard td, .fc-theme-standard thead, .fc-theme-standard tbody, .fc-theme-standard .fc-divider, .fc-theme-standard .fc-row, .fc-theme-standard .fc-popover {
253 | border-color: $borderColor;
254 | }
255 |
256 | .fc-agendaMonthly-view .fc-event {
257 | color: white;
258 | }
259 |
260 | .fc-now-indicator {
261 | border-color: rgba(255, 0, 0, 0.5);
262 | }
263 |
264 | .fc-theme-standard .fc-daygrid {
265 | .fc-daygrid-day {
266 | background: white;
267 | }
268 | .fc-scroller {
269 | padding: 5px 15px;
270 | }
271 | .fc-content-skeleton {
272 | border-top: 0px;
273 | }
274 | }
275 |
276 |
277 | // List view
278 |
279 | .fc-theme-standard .fc-list-view .fc-scroller {
280 | padding: 0px 15px;
281 | }
282 |
283 | .fc-list-view {
284 | border-width: 0px;
285 | }
286 |
287 | .fc-list-table {
288 | width: 80%;
289 | max-width: 400px;
290 | margin: 0 auto 30px auto;
291 | .fc-list-day th {
292 | z-index: 5;
293 | font-size: 1.3em;
294 | line-height: 1em;
295 | font-weight: 500;
296 | color: $primaryColor;
297 | padding: 20px 19px 15px 0;
298 | .fc-list-day-text {
299 | color: $subtleColor;
300 | }
301 | }
302 | }
303 |
304 | .is-small .fc-theme-standard .fc-list-heading td {
305 | font-size: 1.1em;
306 | }
307 |
308 | .fc-theme-standard .fc-list-event:hover td {
309 | background-color: transparent;
310 | }
311 |
312 | .fc-list-event {
313 | display: block;
314 | transition: color .2s ease, border-color .2s ease, opacity .6s ease, box-shadow .2s ease;
315 | border: none;
316 | border-left: 2px solid darken($borderColor, 35%);
317 | background-color: #fff;
318 | border-radius: 3px;
319 | color: #333;
320 | margin: 1px 0;
321 | box-shadow: 0 1px 2px rgba(0,0,0,.07);
322 | cursor: pointer;
323 | margin-bottom: 3px;
324 | font-weight: 500;
325 | font-size: 12px;
326 |
327 | &:hover {
328 | box-shadow: 0 2px 4px rgba(0,0,0,.12);
329 | border-left: 3px solid $primaryColor;
330 | color: $primaryColor;
331 | font-weight: 600;
332 | padding-left: 2px;
333 | }
334 |
335 | td {
336 | background: transparent;
337 | border-color: transparent;
338 | transform: translateX(0);
339 | transition: transform .2s ease;
340 | }
341 |
342 | &:hover td {
343 | background: transparent;
344 | transform: translateX(2px);
345 | }
346 |
347 | .fc-list-event-dot {
348 | display: none;
349 | }
350 |
351 | .fc-list-event-time {
352 | padding-right: 0px;
353 | min-width: 110px;
354 | }
355 |
356 | .fc-list-event-title a {
357 | font-weight: 600;
358 | }
359 | }
360 |
361 | .fc-theme-standard .fc-list-empty {
362 | background-color: transparent;
363 | }
--------------------------------------------------------------------------------
/src/booking/styles/testmoderibbon.scss:
--------------------------------------------------------------------------------
1 | // Imports
2 |
3 | @import 'variables';
4 |
5 | // Test Mode Ribbon
6 |
7 | .bookingjs-ribbon-wrapper {
8 | background: transparent;
9 | height: 140px;
10 | width: 35px;
11 | position: absolute;
12 | bottom: -34px;
13 | right: 19px;
14 | transform: rotate(45deg);
15 | overflow: hidden;
16 | z-index: 42;
17 | -webkit-backface-visibility: hidden;
18 |
19 | .bookingjs-ribbon-container {
20 | background : transparent;
21 | height: 110px;
22 | width: 110px;
23 | position: absolute;
24 | left: -54px;
25 | top: 15px;
26 | overflow: hidden;
27 | transform: rotate(45deg);
28 |
29 | &:before { /* Top Curl */
30 | content: "";
31 | display: block;
32 | position: absolute;
33 | right: 94px;
34 | top: 0px;
35 | width: 0;
36 | height: 0;
37 | border-left: 6px solid transparent;
38 | border-right: 6px solid transparent;
39 | border-bottom: 6px solid $ribbonDarkColor;
40 | }
41 |
42 | &:after { /* Bottom Curl */
43 | content: "";
44 | display: block;
45 | position: absolute;
46 | right: 0;
47 | top: 92px;
48 | width: 0;
49 | height: 0;
50 | border-top: 6px solid transparent;
51 | border-bottom: 6px solid transparent;
52 | border-left: 6px solid $ribbonDarkColor;
53 | }
54 |
55 | .bookingjs-ribbon {
56 | width: 140px;
57 | height: 21px;
58 | position: relative;
59 | top: 32px;
60 | right: 3px;
61 | z-index: 1;
62 | overflow: hidden;
63 | transform:rotate(45deg);
64 | background: $ribbonColor;
65 |
66 | > span {
67 | position: relative;
68 | text-align: center;
69 | display: block;
70 | position: relative;
71 | bottom: -6px;
72 | transform: rotate(180deg);
73 | font-size: 10px;
74 | color: $bgColor;
75 | text-transform: uppercase;
76 | font-weight: 700;
77 | letter-spacing: 1px;
78 | line-height: 1;
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/booking/styles/utils.scss:
--------------------------------------------------------------------------------
1 | // Rotate spin animation
2 |
3 | @keyframes spin {
4 | to { transform: rotate(360deg); }
5 | }
6 |
7 | // Shake animation
8 |
9 | @keyframes shake {
10 | 0% { transform: translateX(0px); }
11 | 25% { transform: translateX(5px); }
12 | 50% { transform: translateX(-5px); }
13 | 75% { transform: translateX(5px); }
14 | 100% { transform: translateX(0px); }
15 | }
16 |
17 | // Form input common styles
18 | @mixin formInput {
19 | transition: box-shadow 0.2s ease;
20 | width: 100%;
21 | padding: 15px 25px;
22 | margin: 0;
23 | border: 0px solid $borderColor;
24 | font-size: 1em;
25 | box-shadow: inset 0px 0px 1px 1px rgba(255,255,255, 0);
26 | text-align: left;
27 | box-sizing: border-box;
28 | line-height: 1.5em;
29 | font-family: $fontFamily;
30 | color: $textColor;
31 | overflow: auto;
32 | border-bottom: 1px solid $borderColor;
33 |
34 | &:focus {
35 | outline: 0;
36 | &:not(.field-required) {
37 | box-shadow: inset 0px 0px 1px 1px $primaryColor;
38 | }
39 | }
40 |
41 | &.hidden {
42 | display: none;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/booking/styles/variables.scss:
--------------------------------------------------------------------------------
1 | // Colors
2 | $textColor: #333;
3 | $textLightColor: #787878;
4 | $textLighterColor: #AEAEAE;
5 | $bgColor: #FBFBFB;
6 | $borderColor: #ececec;
7 | $subtleColor: darken($borderColor, 25%);
8 | $primaryColor: #2e5bec;
9 | $primaryDarkColor: darken($primaryColor, 10%);
10 | $errorColor: #D83B46;
11 | $successColor: #46CE92;
12 | $ribbonColor: #ffb46e;
13 | $ribbonDarkColor: darken($ribbonColor, 15%);
14 |
15 | // Fonts
16 | @import url('https://fonts.googleapis.com/css?family=Open+Sans:400,600');
17 | $fontFamily: 'Open Sans', Helvetica, Tahoma, Arial, sans-serif;
--------------------------------------------------------------------------------
/src/booking/templates/booking-page.html:
--------------------------------------------------------------------------------
1 |
2 |
{{& closeIcon }}
3 |
11 |
25 |
26 |
--------------------------------------------------------------------------------
/src/booking/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{& errorWarningIcon }}
5 |
6 |
7 | Ouch, we've encountered a problem
8 |
9 |
10 | {{& message }}
11 | {{& context }}
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/booking/templates/fields/checkbox.html:
--------------------------------------------------------------------------------
1 |
2 |
5 | {{ title }}
6 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/booking/templates/fields/label.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/booking/templates/fields/multi-checkbox.html:
--------------------------------------------------------------------------------
1 |
2 | {{ title }}
3 | {{# enum }}
4 |
7 |
17 | {{ . }}
18 |
19 | {{/ enum }}
20 |
21 |
--------------------------------------------------------------------------------
/src/booking/templates/fields/select.html:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/src/booking/templates/fields/tel.html:
--------------------------------------------------------------------------------
1 |
2 | {{^ hidden }}
3 |
6 | {{ title }}
7 |
8 | {{/ hidden }}
9 |
21 |
22 |
--------------------------------------------------------------------------------
/src/booking/templates/fields/text.html:
--------------------------------------------------------------------------------
1 |
2 | {{^ hidden }}
3 |
6 | {{ title }}
7 |
8 | {{/ hidden }}
9 |
22 |
23 |
--------------------------------------------------------------------------------
/src/booking/templates/fields/textarea.html:
--------------------------------------------------------------------------------
1 |
2 |
5 | {{ title }}
6 |
7 |
15 |
16 |
--------------------------------------------------------------------------------
/src/booking/templates/footer.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/booking/templates/loading.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{& loadingIcon }}
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/booking/templates/reschedule-page.html:
--------------------------------------------------------------------------------
1 |
2 |
{{& closeIcon }}
3 |
16 |
30 |
--------------------------------------------------------------------------------
/src/booking/templates/testmoderibbon.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ ribbonText }}
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/booking/templates/user-avatar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/booking/templates/user-displayname.html:
--------------------------------------------------------------------------------
1 |
2 | {{ name }}
3 |
4 |
--------------------------------------------------------------------------------
/src/booking/widget.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const merge = require("lodash/merge");
4 | const timekitSdk = require('timekit-sdk');
5 |
6 | const Util = require('./helpers/util');
7 | const Config = require('./helpers/config');
8 | const Template = require('./helpers/template');
9 |
10 | class BookingWidget {
11 | constructor() {
12 | this.config = new Config();
13 | this.utils = new Util(this.config);
14 | this.sdk = timekitSdk.newInstance();
15 | this.template = new Template(this.config, this.utils, this.sdk);
16 | }
17 |
18 | getVersion() {
19 | return VERSION;
20 | }
21 |
22 | getSdk() {
23 | return this.sdk;
24 | }
25 |
26 | getConfig() {
27 | return this.config;
28 | }
29 |
30 | getCalendar() {
31 | return this.template.getCalendar();
32 | }
33 |
34 | destroy() {
35 | this.template.destroy();
36 | }
37 |
38 | timekitCreateBooking(formData, eventData) {
39 | return this.template.bookingPage.timekitCreateBooking(formData, eventData);
40 | }
41 |
42 | render() {
43 | this.utils.doCallback('renderStarted');
44 |
45 | // Setup the Timekit SDK with correct config
46 | this.sdk.configure(this.config.get('sdk'));
47 |
48 | const timezone = this.config.get("ui.timezone");
49 |
50 | // Start by guessing customer timezone
51 | if (timezone) {
52 | this.template.setCustomerTimezone(timezone);
53 | } else {
54 | this.template.guessCustomerTimezone();
55 | }
56 |
57 | // Initialize FullCalendar
58 | this.template.initializeCalendar()
59 | .getAvailability()
60 | .renderAvatarImage()
61 | .renderDisplayName()
62 | .renderFooter();
63 |
64 | this.utils.doCallback('renderCompleted');
65 |
66 | return this;
67 | }
68 |
69 | init(suppliedConfig, global) {
70 |
71 | // Allows mokcing the window object if passed
72 | global = global || window;
73 | this.config.setGlobal(global);
74 |
75 | // Make sure that SDK is ready and debug flag is checked early
76 | this.config.set(suppliedConfig || {});
77 |
78 | this.utils.logDebug(['Version:', this.getVersion()]);
79 | this.utils.logDebug(['Supplied config:', suppliedConfig]);
80 |
81 | try {
82 | this.template.render(suppliedConfig || {});
83 | } catch (e) {
84 | this.utils.logError(e);
85 | return this;
86 | }
87 |
88 | // Check whether a config is supplied
89 | if (!this.utils.doesConfigExist(suppliedConfig)) {
90 | this.template.triggerError('No configuration was supplied. Please supply a config object upon library initialization');
91 | return this;
92 | }
93 |
94 | // Start from local config
95 | if (!this.utils.isRemoteProject(suppliedConfig) || suppliedConfig.disable_remote_load) {
96 | return this.startWithConfig(this.config.setDefaultsWithoutProject(suppliedConfig));
97 | }
98 |
99 | // Load remote embedded config
100 | if (this.utils.isEmbeddedProject(suppliedConfig)) {
101 | this.loadRemoteEmbeddedProject(suppliedConfig);
102 | }
103 |
104 | // Load remote hosted config
105 | if (this.utils.isHostedProject(suppliedConfig)) {
106 | this.loadRemoteHostedProject(suppliedConfig);
107 | }
108 |
109 | return this;
110 | }
111 |
112 | startWithConfig(suppliedConfig) {
113 | try {
114 | // Handle config and defaults
115 | const configs = this.config.parseAndUpdate(suppliedConfig);
116 | this.utils.logDebug(['Final config:', configs]);
117 | this.render();
118 | } catch (e) {
119 | this.template.triggerError(e);
120 | return this;
121 | }
122 | }
123 |
124 | loadRemoteHostedProject(suppliedConfig) {
125 | this.sdk.configure(this.config.get('sdk'));
126 | this.sdk.makeRequest({
127 | method: 'get',
128 | url: '/projects/hosted/' + suppliedConfig.project_slug
129 | })
130 | .then((response) => this.remoteProjectLoaded(response, suppliedConfig))
131 | .catch(e => this.template.triggerError(['The project could not be found, please double-check your "project_slug"', e]));
132 | }
133 |
134 | loadRemoteEmbeddedProject(suppliedConfig) {
135 | // App key is required when fetching an embedded project, bail if not fund
136 | if (!suppliedConfig.app_key) {
137 | this.template.triggerError('Missing "app_key" in conjunction with "project_id", please provide your "app_key" for authentication');
138 | return this;
139 | }
140 |
141 | this.sdk.configure(this.config.get('sdk'));
142 | this.sdk.makeRequest({
143 | method: 'get',
144 | url: '/projects/embed/' + suppliedConfig.project_id
145 | })
146 | .then((response) => this.remoteProjectLoaded(response, suppliedConfig))
147 | .catch(e => this.template.triggerError(['The project could not be found, please double-check your "project_id" and "app_key"', e]));
148 | }
149 |
150 | remoteProjectLoaded(response, suppliedConfig) {
151 | const remoteConfig = response.data;
152 |
153 | if (remoteConfig.id) {
154 | remoteConfig.project_id = remoteConfig.id;
155 | delete remoteConfig.id;
156 | }
157 | if (remoteConfig.slug) {
158 | remoteConfig.project_slug = remoteConfig.slug;
159 | delete remoteConfig.slug;
160 | }
161 |
162 | this.utils.logDebug(['Remote config:', remoteConfig]);
163 | return this.startWithConfig(merge({}, remoteConfig, suppliedConfig));
164 | }
165 | }
166 |
167 | module.exports = BookingWidget;
--------------------------------------------------------------------------------
/src/services/assets/close-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/services/assets/error-warning-icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | error-warning-icon
5 | Created with Sketch.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/services/assets/icon_back.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/services/assets/icon_clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/services/assets/icon_close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/services/assets/icon_geolocation.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/src/services/assets/icon_search.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/services/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timekit-io/booking-js/981bc56720c12e235e8051f007aeff4cde03a684/src/services/assets/logo.png
--------------------------------------------------------------------------------
/src/services/classes/base.js:
--------------------------------------------------------------------------------
1 | class BaseTemplate {
2 | constructor(template) {
3 | if (template && template.pageTarget) {
4 | if (template.rootTarget.contains(template.errorTarget)) {
5 | template.rootTarget.removeChild(template.errorTarget);
6 | }
7 | if (template.rootTarget.contains(template.pageTarget)) {
8 | template.rootTarget.removeChild(template.pageTarget);
9 | }
10 | }
11 | }
12 |
13 | htmlToElement(html) {
14 | const template = document.createElement('template');
15 | template.innerHTML = html.trim();
16 | return template.content.firstChild;
17 | }
18 |
19 | renderAndInitActions(pageTarget, isTemplate = false) {
20 | const templateObj = isTemplate ? this : this.template;
21 |
22 | templateObj.rootTarget.append(pageTarget);
23 |
24 | const defaultOpt = this.config.get('stratergy');
25 | const stratergy = this.config.getSession('stratergy');
26 |
27 | const backIcon = pageTarget.querySelector('i.back-icon');
28 | const closeIcon = pageTarget.querySelector('i.close-icon');
29 |
30 | if (defaultOpt === stratergy && this.config.noSessions()) {
31 | pageTarget.classList.add("hide");
32 | }
33 |
34 | if (closeIcon) {
35 | const aTag = closeIcon.closest('a');
36 | aTag && aTag.addEventListener('click', (e) => {
37 | e.preventDefault();
38 | pageTarget.classList.toggle("hide");
39 | });
40 | }
41 |
42 | if (backIcon) {
43 | const aTag = backIcon.closest('a');
44 | if (defaultOpt === stratergy) {
45 | backIcon.classList.remove("show");
46 | }
47 | aTag && aTag.addEventListener('click', (e) => {
48 | e.preventDefault();
49 |
50 | const defaultOpt = this.config.get('stratergy');
51 | const stratergy = this.config.getSession('stratergy');
52 |
53 | if (stratergy === 'service') {
54 | this.config.setSession('stratergy', 'location');
55 | } else if (stratergy === 'location') {
56 | this.config.setSession('stratergy', 'service');
57 | } else if(stratergy === 'calendar') {
58 | this.config
59 | .destroySessions()
60 | .setSession('stratergy', defaultOpt);
61 | }
62 |
63 | templateObj.initPage();
64 | });
65 | }
66 | }
67 | }
68 |
69 | module.exports = BaseTemplate;
--------------------------------------------------------------------------------
/src/services/classes/config.js:
--------------------------------------------------------------------------------
1 | const get = require("lodash/get");
2 | const merge = require("lodash/merge");
3 |
4 | class Config {
5 | constructor() {
6 | this.config = {};
7 | this.session = {};
8 | this.defaultConfigs = require('../configs');
9 | }
10 |
11 | getSession(key, defaultValue) {
12 | return get(this.session, key) || defaultValue;
13 | }
14 |
15 | setSession(key, data) {
16 | this.session[key] = data;
17 | }
18 |
19 | destroySessions() {
20 | this.session = {};
21 | return this;
22 | }
23 |
24 | noSessions() {
25 | return !this.getSession('service') && !this.getSession('location');
26 | }
27 |
28 | all() {
29 | return this.config;
30 | }
31 |
32 | get(key) {
33 | return get(this.config, key);
34 | }
35 |
36 | set(configs) {
37 | return (this.config = this.setDefaults(configs));
38 | }
39 |
40 | parseAndUpdate(configs) {
41 |
42 | // Extend the default config with supplied settings
43 | let newConfig = this.setDefaults(configs);
44 |
45 | // Check for required settings
46 | if (!newConfig.app_key) throw 'A required config setting ("app_key") was missing';
47 |
48 | return this.set(newConfig);
49 | }
50 |
51 | setDefaults(configs) {
52 | configs = this.prepareSessionConfigs(configs);
53 | configs.sdk = this.prepareSdkConfig(configs);
54 | return merge({}, this.defaultConfigs, configs);
55 | }
56 |
57 | prepareSessionConfigs(configs) {
58 | if (configs.stratergy) {
59 | this.setSession("stratergy", configs.stratergy);
60 | }
61 | return configs;
62 | }
63 |
64 | prepareSdkConfig(configs) {
65 | if (typeof configs.sdk === 'undefined') {
66 | configs.sdk = {};
67 | }
68 | if (configs.app_key) {
69 | configs.sdk.appKey = configs.app_key;
70 | }
71 | if (configs.api_base_url) {
72 | configs.sdk.apiBaseUrl = configs.api_base_url;
73 | }
74 | return merge({}, this.defaultConfigs.sdk, configs.sdk);
75 | }
76 | }
77 |
78 | module.exports = Config;
--------------------------------------------------------------------------------
/src/services/classes/template.js:
--------------------------------------------------------------------------------
1 | const BaseTemplate = require('./base');
2 |
3 | const get = require("lodash/get");
4 | const stringify = require('json-stringify-safe');
5 |
6 | const ServicesPage = require('../pages/services');
7 | const LocationsPage = require('../pages/locations');
8 | const CalendarWidgetPage = require('../pages/calendar');
9 |
10 | const LogoIcon = require('!file-loader!../assets/logo.png').default;
11 | const BackIcon = require('!file-loader!../assets/icon_back.svg').default;
12 | const CloseIcon = require('!file-loader!../assets/icon_close.svg').default;
13 |
14 | require('../styles/base.scss');
15 | require('@fontsource/open-sans');
16 |
17 | class Template extends BaseTemplate {
18 | constructor(config, utils, sdk) {
19 | super();
20 |
21 | // config and utils
22 | this.sdk = sdk;
23 | this.utils = utils;
24 | this.config = config;
25 |
26 | // dom nodes
27 | this.rootTarget = null;
28 | this.errorTarget = null;
29 | this.buttonTarget = null;
30 |
31 | // page target
32 | this.pageTarget = null;
33 | }
34 |
35 | destroy() {
36 | this.clearRootElem();
37 | this.rootTarget.remove();
38 | }
39 |
40 | clearRootElem() {
41 | let child = this.rootTarget.lastElementChild;
42 | while (child) {
43 | this.rootTarget.removeChild(child);
44 | child = this.rootTarget.lastElementChild;
45 | }
46 | }
47 |
48 | init(configs) {
49 | const targetElement = configs.el || this.config.get('el');
50 |
51 | this.rootTarget = document.createElement('div');
52 | this.rootTarget.classList.add("tk-appt-window");
53 | this.rootTarget.id = targetElement.replace('#', '');
54 |
55 | document.body.appendChild(this.rootTarget);
56 | this.rootTarget = document.getElementById(targetElement.replace('#', ''));
57 |
58 | if (!this.rootTarget) {
59 | throw this.triggerError(
60 | 'No target DOM element was found (' + targetElement + ')'
61 | );
62 | }
63 |
64 | this.clearRootElem();
65 | }
66 |
67 | initButton() {
68 | const defaultUI = this.config.get('defaultUI');
69 | if (defaultUI) {
70 | const targetElement = this.config.get('elBtn');
71 |
72 | this.buttonTarget = document.createElement('a');
73 | this.buttonTarget.id = targetElement.replace('#', '');
74 | this.buttonTarget.style.backgroundImage = `url(${LogoIcon})`;
75 |
76 | this.buttonTarget.addEventListener('click', (e) => {
77 | e.preventDefault();
78 | this.pageTarget && this.pageTarget.classList.toggle("hide");
79 | });
80 |
81 | this.rootTarget.append(this.buttonTarget);
82 | }
83 | return this;
84 | }
85 |
86 | initPage() {
87 | const defaultUI = this.config.get('defaultUI');
88 | const stratergy = this.config.getSession('stratergy', 'service');
89 |
90 | if (defaultUI) {
91 | if (stratergy === 'service') {
92 | this.initServices();
93 | } else if (stratergy === 'location') {
94 | this.initLocations();
95 | }
96 | }
97 | }
98 |
99 | initServices() {
100 | return new ServicesPage(this).render();
101 | }
102 |
103 | initLocations() {
104 | return new LocationsPage(this).render();
105 | }
106 |
107 | initCalendar(serviceId, locationId) {
108 | return new CalendarWidgetPage(this).render(serviceId, locationId);
109 | }
110 |
111 | triggerError(message) {
112 | // If an error already has been thrown, exit
113 | // If no target DOM element exists, only do the logging
114 | if (this.errorTarget) return message;
115 | if (!this.rootTarget) return message;
116 |
117 | this.utils.logError(message);
118 | this.utils.doCallback('errorTriggered', message);
119 |
120 | let contextProcessed = null;
121 | let messageProcessed = message;
122 |
123 | if (this.utils.isArray(message)) {
124 | messageProcessed = message[0];
125 | if (message[1]?.data) {
126 | contextProcessed = stringify(
127 | message[1].data.errors || message[1].data.error || message[1].data
128 | );
129 | } else {
130 | contextProcessed = stringify(message[1]);
131 | }
132 | }
133 |
134 | const template = require('../templates/error.html');
135 | this.errorTarget = this.htmlToElement(
136 | template({
137 | backIcon: BackIcon,
138 | closeIcon: CloseIcon,
139 | message: messageProcessed,
140 | context: contextProcessed,
141 | })
142 | );
143 |
144 | this.renderAndInitActions(this.errorTarget, true);
145 |
146 | return message;
147 | }
148 | }
149 |
150 | module.exports = Template;
--------------------------------------------------------------------------------
/src/services/classes/util.js:
--------------------------------------------------------------------------------
1 | const isEmpty = require("lodash/isEmpty");
2 |
3 | class Util {
4 | constructor(config) {
5 | this.config = config;
6 | }
7 |
8 | isFunction(object) {
9 | return !!(object && object.constructor && object.call && object.apply);
10 | }
11 |
12 | isArray(object) {
13 | return object && object.constructor === Array;
14 | }
15 |
16 | logError(message) {
17 | console.warn('TimekitBooking Error: ', message);
18 | }
19 |
20 | logDebug(message) {
21 | if (this.config.get('debug')) {
22 | console.log('TimekitBooking Debug: ', message);
23 | }
24 | }
25 |
26 | doesConfigExist(suppliedConfig) {
27 | return (suppliedConfig !== undefined && typeof suppliedConfig === 'object' && !isEmpty(suppliedConfig));
28 | }
29 |
30 | doCallback(hook, arg) {
31 | if(this.config.get('callbacks') && this.isFunction(this.config.get('callbacks.' + hook))) {
32 | this.config.get('callbacks.' + hook)(arg);
33 | }
34 | this.logDebug(['Trigger callback "' + hook + '" with arguments:', arg]);
35 | }
36 | }
37 |
38 | module.exports = Util;
--------------------------------------------------------------------------------
/src/services/configs.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | debug: false,
3 | autoload: true,
4 | defaultUI: true,
5 | elBtn: '#tk-bot-btn',
6 | stratergy: 'location',
7 | el: '#tk-appointments',
8 | sdk: {
9 | headers: {
10 | 'Timekit-Context': 'widget'
11 | }
12 | },
13 | selectorOptions: {
14 | service: {
15 | title: 'Select an Appointment Type',
16 | description: 'Which appointment type would you like?',
17 | },
18 | location: {
19 | title: 'Select a Location',
20 | description: 'Choose a location from the following that you will be visiting for your appointment.',
21 | search_bar: {
22 | enabled: true,
23 | placeholder: 'Search for a city or postal code.'
24 | },
25 | },
26 | booking: {
27 | title: 'Complete Booking Details',
28 | description: 'Please select an appointment time and fill out the booking form.',
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/services/index.js:
--------------------------------------------------------------------------------
1 |
2 | 'use strict';
3 |
4 | module.exports = require('./widget');
--------------------------------------------------------------------------------
/src/services/pages/calendar.js:
--------------------------------------------------------------------------------
1 | const get = require("lodash/get");
2 |
3 | const BaseTemplate = require('../classes/base');
4 | const BackIcon = require('!file-loader!../assets/icon_back.svg').default;
5 | const CloseIcon = require('!file-loader!../assets/icon_close.svg').default;
6 |
7 | class CalendarWidgetPage extends BaseTemplate {
8 | constructor(template) {
9 | super(template);
10 | this.sdk = template.sdk;
11 | this.template = template;
12 | this.utils = template.utils;
13 | this.config = template.config;
14 | }
15 |
16 | render(serviceId, locationId) {
17 | this.config.setSession('stratergy', 'calendar');
18 | this.sdk.makeRequest({
19 | method: 'get',
20 | url: '/projects?search=locations.uuid:' + locationId + ';services.uuid:' + serviceId
21 | })
22 | .then(({ data: projects }) => {
23 |
24 | const project = get(projects, '0');
25 | const template = require('../templates/calendar.html');
26 |
27 | const service = this.config.getSession('selectedService');
28 | const location = this.config.getSession('selectedLocation');
29 |
30 | this.template.pageTarget = this.htmlToElement(template({
31 | backIcon: BackIcon,
32 | closeIcon: CloseIcon,
33 | serviceName: get(service, 'name'),
34 | locationName: get(location, 'name'),
35 | selectorOptions: this.config.get('selectorOptions.booking')
36 | }));
37 |
38 | this.renderAndInitActions(this.template.pageTarget);
39 |
40 | new TimekitBooking().init({
41 | project_id: project.id,
42 | app_key: this.config.get('sdk.appKey'),
43 | api_base_url: this.config.get('sdk.apiBaseUrl'),
44 | });
45 | })
46 | .catch((response) => {
47 | this.utils.doCallback('initCalendarFailed', response);
48 | this.template.triggerError([
49 | 'For given service and location calendar does not exists',
50 | response,
51 | ]);
52 | });
53 | }
54 | }
55 |
56 | module.exports = CalendarWidgetPage;
--------------------------------------------------------------------------------
/src/services/pages/locations.js:
--------------------------------------------------------------------------------
1 | const get = require("lodash/get");
2 | const find = require("lodash/find");
3 | const filter = require("lodash/filter");
4 |
5 | const BaseTemplate = require('../classes/base');
6 | const BackIcon = require('!file-loader!../assets/icon_back.svg').default;
7 | const CloseIcon = require('!file-loader!../assets/icon_close.svg').default;
8 | const SearchIcon = require('!file-loader!../assets/icon_search.svg').default;
9 |
10 | class LocationsPage extends BaseTemplate {
11 | constructor(template) {
12 | super(template);
13 | this.latitude = 0;
14 | this.longitude = 0;
15 | this.sdk = template.sdk;
16 | this.template = template;
17 | this.utils = template.utils;
18 | this.locationsTarget = null;
19 | this.config = template.config;
20 | }
21 |
22 | getDegreesToRadians(degrees) {
23 | return degrees * Math.PI / 180;
24 | }
25 |
26 | getDistance(project) {
27 | return this.getDistanceInKmBetweenEarthCoordinates(get(project, 'meta.t_latitude') || 0, get(project, 'meta.t_longitude') || 0);
28 | }
29 |
30 | getDistanceInKmBetweenEarthCoordinates(latitude, longitude) {
31 | const earthRadiusKm = 6371;
32 |
33 | let destLatitudeInRadians = this.getDegreesToRadians(this.latitude - latitude);
34 | let destLongitudeInRadians = this.getDegreesToRadians(this.longitude - longitude);
35 |
36 | latitude = this.getDegreesToRadians(latitude);
37 | let latitudeInRadiants = this.getDegreesToRadians(this.latitude);
38 |
39 | let a = Math.sin(destLatitudeInRadians/2) * Math.sin(destLatitudeInRadians/2) + Math.sin(destLongitudeInRadians/2) * Math.sin(destLongitudeInRadians/2) * Math.cos(latitude) * Math.cos(latitudeInRadiants);
40 | let c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
41 |
42 | return parseFloat((earthRadiusKm * c).toFixed(1));
43 | }
44 |
45 | getLocationsTemplate(locations) {
46 | const template = require('../templates/slots/locations.html');
47 | return this.htmlToElement(template({
48 | selectorOptions: this.config.get('selectorOptions.location'),
49 | locations: locations.map((projectData) => ({
50 | ...projectData,
51 | distance: this.getDistance(projectData)
52 | })).sort((prev, next) => prev.distance - next.distance)
53 | }));
54 | }
55 |
56 | initLocationClick(locations) {
57 | const locationLinks = this.template.pageTarget.querySelectorAll('.card-container');
58 | for (let i=0; i < locationLinks.length; i++) {
59 | locationLinks[i].addEventListener("click", (e) => {
60 | e.preventDefault();
61 | const wrapper = e.target.closest(".card-container");
62 | const selectedService = this.config.getSession('selectedService');
63 | if (wrapper.id) {
64 | this.config.setSession('selectedLocation', find(locations, {
65 | id: wrapper.id
66 | }));
67 | if (selectedService && selectedService.id) {
68 | this.template.initCalendar(selectedService.id, wrapper.id);
69 | } else {
70 | this.template.initServices();
71 | }
72 | }
73 | });
74 | }
75 | }
76 |
77 | renderElement(locations) {
78 | this.config.setSession('locations', locations);
79 |
80 | const template = require('../templates/locations.html');
81 | const selectedService = this.config.getSession('selectedService');
82 |
83 | this.template.pageTarget = this.htmlToElement(template({
84 | backIcon: BackIcon,
85 | locations: locations,
86 | closeIcon: CloseIcon,
87 | searchIcon: SearchIcon,
88 | service: selectedService,
89 | selectorOptions: this.config.get('selectorOptions.location'),
90 | }));
91 |
92 | this.locationsTarget = this.getLocationsTemplate(locations);
93 |
94 | const searchInput = this.template.pageTarget.querySelector('#search-bar');
95 | const locationsEle = this.template.pageTarget.querySelector('#location-list');
96 | const searchGeoLocBtn = this.template.pageTarget.querySelector('#geolocation-search');
97 |
98 | locationsEle.append(this.locationsTarget);
99 |
100 | searchInput && searchInput.addEventListener("focus", (e) => {
101 | if (this.config.get('selectorOptions.location.search_bar.enabled')) {
102 | searchGeoLocBtn && searchGeoLocBtn.classList.remove('hide');
103 | }
104 | e.preventDefault();
105 | });
106 |
107 | searchGeoLocBtn && searchGeoLocBtn.addEventListener("focusout", (e) => {
108 | e.target.classList.add('hide');
109 | e.preventDefault();
110 | });
111 |
112 | searchInput && searchInput.addEventListener("input", (e) => {
113 | const filteredLocations = filter(locations, function({ meta }) {
114 | const searchText = e.target.value.toLowerCase();
115 | return meta.t_store_city.toLowerCase().includes(searchText) ||
116 | (meta.t_store_name && meta.t_store_name.toLowerCase().includes(searchText)) ||
117 | (meta.t_store_postal_code && meta.t_store_postal_code.toLowerCase().includes(searchText));
118 | });
119 |
120 | locationsEle.removeChild(this.locationsTarget);
121 | this.locationsTarget = this.getLocationsTemplate(filteredLocations);
122 |
123 | locationsEle.append(this.locationsTarget);
124 | this.initLocationClick(filteredLocations);
125 |
126 | e.preventDefault();
127 | });
128 |
129 | searchGeoLocBtn && searchGeoLocBtn.addEventListener("click", async (e) => {
130 | e.preventDefault();
131 | e.target.classList.add('hide');
132 | return await new Promise(async (resolve) => {
133 | const options = {
134 | timeout: 5000,
135 | enableHighAccuracy: false,
136 | maximumAge: 600000 // Keep cached data for 10 mins
137 | };
138 | return await window.navigator.geolocation.getCurrentPosition((position) => {
139 | this.latitude = position.coords.latitude;
140 | this.longitude = position.coords.longitude;
141 | return resolve({});
142 | }, () => {
143 | this.latitude = 0;
144 | this.longitude = 0;
145 | return resolve({});
146 | }, options);
147 | }).then(() => {
148 |
149 | locationsEle.removeChild(this.locationsTarget);
150 | this.locationsTarget = this.getLocationsTemplate(locations);
151 |
152 | locationsEle.append(this.locationsTarget);
153 | this.initLocationClick(locations);
154 | })
155 | });
156 |
157 | this.initLocationClick(locations);
158 | this.renderAndInitActions(this.template.pageTarget);
159 | }
160 |
161 | render() {
162 | this.config.setSession('stratergy', 'location');
163 |
164 | const selectedService = this.config.getSession('selectedService');
165 | const selectedServiceLocations = get(selectedService, 'locations', []);
166 |
167 | // when service is already selected in step-1
168 | if (selectedService?.id) {
169 | if (selectedServiceLocations.length > 0) {
170 | this.renderElement(selectedServiceLocations);
171 | } else {
172 | this.template.triggerError([
173 | 'No location found for service: ' + selectedService.name,
174 | ]);
175 | }
176 | }
177 |
178 | // when service is not selected and location is step-1
179 | else {
180 | this.sdk.makeRequest({
181 | method: 'get',
182 | url: '/locations?include=services'
183 | })
184 | .then(({ data: locations }) => this.renderElement(locations))
185 | .catch((response) => {
186 | this.utils.doCallback('initLocationsFailed', response);
187 | this.template.triggerError([
188 | 'An error occurred fetching locations',
189 | response,
190 | ]);
191 | });
192 | }
193 |
194 | return this.template;
195 | }
196 | }
197 |
198 | module.exports = LocationsPage;
--------------------------------------------------------------------------------
/src/services/pages/services.js:
--------------------------------------------------------------------------------
1 | const get = require("lodash/get");
2 | const find = require("lodash/find");
3 |
4 | const BaseTemplate = require('../classes/base');
5 | const BackIcon = require('!file-loader!../assets/icon_back.svg').default;
6 | const CloseIcon = require('!file-loader!../assets/icon_close.svg').default;
7 |
8 | class ServicesPage extends BaseTemplate {
9 | constructor(template) {
10 | super(template);
11 | this.sdk = template.sdk;
12 | this.template = template;
13 | this.utils = template.utils;
14 | this.config = template.config;
15 | }
16 |
17 | renderElement(services) {
18 | this.config.setSession('services', services);
19 |
20 | const template = require('../templates/services.html');
21 | const selectedLocation = this.config.getSession('selectedLocation');
22 |
23 | this.template.pageTarget = this.htmlToElement(template({
24 | services,
25 | backIcon: BackIcon,
26 | closeIcon: CloseIcon,
27 | location: selectedLocation,
28 | selectorOptions: this.config.get('selectorOptions.service')
29 | }));
30 |
31 | const serviceLinks = this.template.pageTarget.querySelectorAll('.card-wrapper');
32 |
33 | for (let i=0; i < serviceLinks.length; i++) {
34 | serviceLinks[i].addEventListener("click", (e) => {
35 | e.preventDefault();
36 | const wrapper = e.target.closest(".card-wrapper");
37 | const selectedLocation = this.config.getSession('selectedLocation');
38 | if (wrapper.id) {
39 | this.config.setSession('selectedService', find(services, {
40 | id: wrapper.id
41 | }));
42 | if (selectedLocation && selectedLocation.id) {
43 | this.template.initCalendar(wrapper.id, selectedLocation.id);
44 | } else {
45 | this.template.initLocations();
46 | }
47 | }
48 | });
49 | }
50 |
51 | this.renderAndInitActions(this.template.pageTarget);
52 | }
53 |
54 | render() {
55 | this.config.setSession('stratergy', 'service');
56 |
57 | const selectedLocation = this.config.getSession('selectedLocation');
58 | const selectedLocationServices = get(selectedLocation, 'services', []);
59 |
60 | // when location is already selected in step-1
61 | if (selectedLocation?.id) {
62 | if (selectedLocationServices.length > 0) {
63 | this.renderElement(selectedLocationServices);
64 | } else {
65 | this.template.triggerError([
66 | 'No services found for location: ' + selectedLocation.name,
67 | ]);
68 | }
69 | }
70 |
71 | // when location is not selected and service is step-1
72 | else {
73 | this.sdk.makeRequest({
74 | method: 'get',
75 | url: '/location/services?include=locations'
76 | })
77 | .then(({ data: services }) => this.renderElement(services))
78 | .catch((response) => {
79 | this.utils.doCallback('initServiceFailed', response);
80 | this.template.triggerError([
81 | 'An error occurred fetching services',
82 | response,
83 | ]);
84 | });
85 | }
86 |
87 | return this.template;
88 | }
89 | }
90 |
91 | module.exports = ServicesPage;
--------------------------------------------------------------------------------
/src/services/templates/calendar.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/services/templates/error.html:
--------------------------------------------------------------------------------
1 |
2 |
22 |
23 |
24 |
25 |
26 |
{{& message }}
27 |
{{& context }}
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/services/templates/locations.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/services/templates/services.html:
--------------------------------------------------------------------------------
1 |
2 |
38 |
39 |
40 | {{#services}}
41 |
42 |
43 |
44 |
45 |
46 |
{{ name }}
47 |
{{ description }}
48 |
49 |
50 |
51 | {{/services}}
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/services/templates/slots/locations.html:
--------------------------------------------------------------------------------
1 |
2 | {{#locations}}
3 |
4 |
5 | {{ name }}
6 | {{#selectorOptions.search_bar.enabled}}
7 |
{{distance}} km
8 | {{/selectorOptions.search_bar.enabled}}
9 |
10 |
{{ meta.t_store_address }}, {{ meta.t_store_city }}
11 |
12 |
13 | {{/locations}}
14 |
--------------------------------------------------------------------------------
/src/services/widget.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Util = require('./classes/util');
4 | const timekitSdk = require('timekit-sdk');
5 | const Config = require('./classes/config');
6 | const Template = require('./classes/template');
7 |
8 | class BookingWidget {
9 | constructor() {
10 | this.config = new Config();
11 | this.utils = new Util(this.config);
12 | this.sdk = timekitSdk.newInstance();
13 | this.template = new Template(this.config, this.utils, this.sdk);
14 | }
15 |
16 | init(configs) {
17 | return new Promise(async (resolve, reject) => {
18 | try {
19 | this.config.parseAndUpdate(configs);
20 | this.sdk.configure(this.config.get('sdk'));
21 | this.template.init(this.config.all());
22 | } catch (e) {
23 | this.utils.logError(e);
24 | return this;
25 | }
26 |
27 | // Check whether a config is supplied
28 | if (!this.utils.doesConfigExist(configs)) {
29 | this.template.triggerError('No configuration was supplied. Please supply a config object upon library initialization');
30 | return this;
31 | }
32 |
33 | try {
34 | this.render();
35 | } catch (e) {
36 | this.utils.logError(e);
37 | return reject(e.message);
38 | }
39 | resolve(true);
40 | });
41 | }
42 |
43 | destroy() {
44 | this.template.destroy();
45 | }
46 |
47 | render() {
48 | this.utils.doCallback('renderStarted');
49 | this.template.initButton().initPage();
50 | this.utils.doCallback('renderCompleted');
51 | return this;
52 | }
53 | }
54 |
55 | module.exports = BookingWidget;
--------------------------------------------------------------------------------
/test/advancedConfiguration.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | const moment = require('moment');
6 | const mockAjax = require('./utils/mockAjax');
7 | const createWidget = require('./utils/createWidget');
8 | var interact = require('./utils/commonInteractions');
9 |
10 | describe('spec', () => {
11 |
12 | beforeEach(function(){
13 | loadFixtures('main.html');
14 | jasmine.Ajax.install();
15 | mockAjax.all();
16 | });
17 |
18 | afterEach(function() {
19 | jasmine.Ajax.uninstall();
20 | });
21 |
22 | it('should go to the first upcoming event automatically', function(done) {
23 |
24 | mockAjax.findTimeWithDateInFuture();
25 | const widget = createWidget();
26 |
27 | setTimeout(function() {
28 |
29 | const calendarDate = moment(widget.getCalendar().getDate());
30 | const future = moment().add(1, 'month').startOf('day');
31 |
32 | expect(calendarDate.format('YYYY-MM-DD')).toBe(future.format('YYYY-MM-DD'));
33 |
34 | setTimeout(function() {
35 | const scrollable = $('.bookingjs-calendar').find('.fc-scroller');
36 | const scrollTop = scrollable.scrollTop();
37 | expect(scrollTop).not.toBe(321);
38 | done();
39 | }, 300);
40 | }, 100);
41 | });
42 |
43 | it('should be able to load even though no timeslots are available', function(done) {
44 | mockAjax.findTimeWithNoTimeslots();
45 | createWidget();
46 | setTimeout(function() {
47 | expect($('.fc-time-grid-event').length).toBe(0)
48 | done();
49 | }, 300);
50 | });
51 |
52 | it('should be able override config settings fetched remotely, but before render', function(done) {
53 |
54 | mockAjax.all();
55 |
56 | function updateConfig () {
57 | let configObj = widget.getConfig();
58 | let widgetConfig = configObj.all();
59 |
60 | expect(widgetConfig.name).toBe('Marty McFly');
61 |
62 | widgetConfig.name = 'Marty McFly 2';
63 | configObj.set(widgetConfig);
64 | }
65 |
66 | var widget = new TimekitBooking();
67 | var config = {
68 | app_key: '12345',
69 | project_id: '12345',
70 | callbacks: {
71 | renderStarted: updateConfig
72 | }
73 | };
74 |
75 | spyOn(config.callbacks, 'renderStarted').and.callThrough();
76 |
77 | widget.init(config);
78 |
79 | setTimeout(function() {
80 | expect(config.callbacks.renderStarted).toHaveBeenCalled();
81 |
82 | var request = jasmine.Ajax.requests.first();
83 | expect(request.url).toBe('https://api.timekit.io/v2/projects/embed/12345');
84 |
85 | let configObj = widget.getConfig();
86 | let widgetConfig = configObj.all();
87 | expect(widgetConfig.name).toBe('Marty McFly 2');
88 |
89 | done();
90 | }, 100)
91 |
92 | });
93 |
94 | it('should be able to inject custom fullcalendar settings and register callbacks', function(done) {
95 |
96 | mockAjax.all();
97 |
98 | var config = {
99 | fullcalendar: {
100 | buttonText: {
101 | today: 'idag'
102 | }
103 | },
104 | callbacks: {
105 | fullCalendarInitialized: function () {}
106 | }
107 | }
108 |
109 | spyOn(config.callbacks, 'fullCalendarInitialized').and.callThrough();
110 |
111 | createWidget(config);
112 |
113 | setTimeout(function() {
114 | expect(config.callbacks.fullCalendarInitialized).toHaveBeenCalled();
115 |
116 | var todayButton = $('.fc-today-button')
117 | expect(todayButton.text()).toBe('idag')
118 |
119 | done();
120 | }, 600);
121 |
122 | });
123 |
124 | it('should be able to set which dynamic includes that CreateBooking request returns in response', function(done) {
125 |
126 | mockAjax.createBookingWithCustomIncludes();
127 |
128 | var config = {
129 | create_booking_response_include: ['provider_event', 'attributes', 'event', 'user']
130 | }
131 |
132 | createWidget(config);
133 | setTimeout(function() {
134 |
135 | interact.clickEvent();
136 |
137 | setTimeout(function() {
138 |
139 | interact.fillSubmit();
140 | setTimeout(function() {
141 |
142 | var request = jasmine.Ajax.requests.mostRecent();
143 | expect(request.url).toBe('https://api.timekit.io/v2/bookings?include=provider_event,attributes,event,user');
144 | done();
145 |
146 | }, 200);
147 | }, 500);
148 | }, 500);
149 |
150 | });
151 |
152 | })
--------------------------------------------------------------------------------
/test/availabilityListView.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var createWidget = require('./utils/createWidget');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Availability view', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | mockAjax.all();
15 | });
16 |
17 | afterEach(function() {
18 | jasmine.Ajax.uninstall();
19 | });
20 |
21 | it('should be able to render list view', function(done) {
22 |
23 | createWidget({
24 | ui: {
25 | availability_view: 'listing'
26 | }
27 | });
28 |
29 | setTimeout(function() {
30 | expect($('.fc-list-table')).toBeInDOM();
31 | interact.clickListEvent();
32 | setTimeout(function() {
33 | expect($('.bookingjs-bookpage')).toBeInDOM();
34 | expect($('.bookingjs-bookpage')).toBeVisible();
35 | done();
36 | }, 500);
37 | }, 500);
38 |
39 | });
40 |
41 | });
42 |
--------------------------------------------------------------------------------
/test/basicConfiguration.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var createWidget = require('./utils/createWidget');
7 |
8 | describe('Basic configuration', function() {
9 |
10 | beforeEach(function(){
11 | loadFixtures('main.html');
12 | jasmine.Ajax.install();
13 | mockAjax.all();
14 | });
15 |
16 | afterEach(function() {
17 | jasmine.Ajax.uninstall();
18 | });
19 |
20 | it('should be able to set the name', function(done) {
21 | var config = {
22 | ui: {
23 | display_name: 'Demo Name'
24 | }
25 | }
26 |
27 | createWidget(config);
28 | expect($('.bookingjs-displayname')).toBeInDOM();
29 | expect($('.bookingjs-displayname')).toBeVisible();
30 | expect($('.bookingjs-displayname')).toContainElement('span');
31 | expect($('.bookingjs-displayname span')).toContainText(config.ui.display_name);
32 |
33 | done()
34 |
35 | });
36 |
37 | it('should be able to set an avatar image', function(done) {
38 |
39 | var config = {
40 | ui: {
41 | avatar: '/base/misc/avatar-doc.jpg'
42 | }
43 | }
44 |
45 | createWidget(config);
46 | expect($('.bookingjs-avatar')).toBeInDOM();
47 | expect($('.bookingjs-avatar')).toBeVisible();
48 | expect($('.bookingjs-avatar')).toContainElement('img');
49 |
50 | var source = $('.bookingjs-avatar img').prop('src');
51 | var contains = source.indexOf(config.ui.avatar) > -1;
52 |
53 | expect(contains).toBe(true);
54 | done()
55 |
56 | });
57 |
58 | it('should be able to set app key in the root-level config key', function(done) {
59 |
60 | var appKey = '123';
61 |
62 | var config = {
63 | app_key: appKey
64 | }
65 |
66 | var widget = createWidget(config);
67 |
68 | var widgetSDK = widget.getSdk();
69 | var widgetConfig = widget.getConfig();
70 |
71 | expect(widgetConfig.get('app_key')).toBe(appKey)
72 | expect(widgetSDK.getConfig().appKey).toBe(appKey)
73 |
74 | done()
75 | });
76 |
77 | it('should be able expose current library version', function(done) {
78 |
79 | var widget = createWidget();
80 | var widgetVersion = widget.getVersion()
81 |
82 | expect(widgetVersion).toBeDefined()
83 | expect(typeof widgetVersion.charAt(1)).toBe('string')
84 |
85 | done()
86 | });
87 |
88 | it('should not have test mode ribbon by default', function(done) {
89 | createWidget();
90 | expect($('.bookingjs-ribbon-wrapper')).not.toBeInDOM();
91 | expect($('.bookingjs-ribbon-wrapper')).not.toBeVisible();
92 | done();
93 | });
94 |
95 | it('should have test mode ribbon when set', function(done) {
96 | mockAjax.findTimeOnTestModeApp();
97 | createWidget();
98 |
99 | setTimeout(function() {
100 | expect($('.bookingjs-ribbon-wrapper')).toBeInDOM();
101 | expect($('.bookingjs-ribbon-wrapper')).toBeVisible();
102 | done();
103 | }, 200);
104 | });
105 |
106 | it('should not render footer if TZ helper and credits are disabled', function(done) {
107 | createWidget({
108 | ui: {
109 | show_credits: false,
110 | show_timezone_helper: false
111 | }
112 | });
113 |
114 | setTimeout(function() {
115 | expect($('.bookingjs-footer')).not.toBeInDOM();
116 | done();
117 | }, 200);
118 | });
119 |
120 | });
121 |
--------------------------------------------------------------------------------
/test/basicInteractions.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var createWidget = require('./utils/createWidget');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Basic interaction', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | mockAjax.all();
15 | });
16 |
17 | afterEach(function() {
18 | jasmine.Ajax.uninstall();
19 | });
20 |
21 | it('should be able to click on an event', function(done) {
22 |
23 | createWidget();
24 |
25 | expect($('.bookingjs-calendar')).toBeInDOM();
26 | expect($('.bookingjs-calendar')).toBeVisible();
27 |
28 | setTimeout(function() {
29 |
30 | var calEventStart = interact.clickEvent();
31 |
32 | setTimeout(function() {
33 |
34 | expect($('.bookingjs-bookpage')).toBeInDOM();
35 | expect($('.bookingjs-bookpage')).toBeVisible();
36 |
37 | var pageTime = $('.bookingjs-bookpage-time').text();
38 | var contains = pageTime.indexOf(calEventStart) > -1;
39 |
40 | expect(contains).toBe(true);
41 |
42 | done();
43 |
44 | }, 500);
45 | }, 500);
46 |
47 | });
48 |
49 | it('should be able to close the booking page', function(done) {
50 |
51 | createWidget();
52 |
53 | setTimeout(function() {
54 |
55 | interact.clickEvent();
56 | setTimeout(function() {
57 |
58 | expect($('.bookingjs-bookpage')).toBeInDOM();
59 | expect($('.bookingjs-bookpage')).toBeVisible();
60 |
61 | document.querySelector('.bookingjs-bookpage-close').click();
62 |
63 | setTimeout(function() {
64 | expect($('.bookingjs-bookpage').length).toBe(0);
65 | done();
66 | }, 500);
67 | }, 500);
68 | }, 500);
69 |
70 | });
71 |
72 | it('should be able to book an event', function(done) {
73 |
74 | createWidget();
75 | setTimeout(function() {
76 |
77 | interact.clickEvent();
78 |
79 | setTimeout(function() {
80 |
81 | var inputs = interact.fillSubmit();
82 | expect($('.bookingjs-form').hasClass('loading')).toBe(true);
83 |
84 | setTimeout(function() {
85 |
86 | expect($('.bookingjs-form').hasClass('success')).toBe(true);
87 | expect($('.bookingjs-form-success-message')).toBeVisible();
88 |
89 | var successMessage = $('.bookingjs-form-success-message').html();
90 | var contains = successMessage.indexOf(inputs.email) > -1;
91 | expect(contains).toBe(true);
92 |
93 | done();
94 |
95 | }, 200);
96 | }, 500);
97 | }, 500);
98 |
99 | });
100 |
101 | it('should be able to book an event and pass widget ID', function(done) {
102 |
103 | createWidget({
104 | project_slug: 'my-widget-slug'
105 | });
106 |
107 | setTimeout(function() {
108 |
109 | interact.clickEvent();
110 | setTimeout(function() {
111 |
112 | interact.fillSubmit();
113 | expect($('.bookingjs-form').hasClass('loading')).toBe(true);
114 |
115 | setTimeout(function() {
116 |
117 | expect($('.bookingjs-form').hasClass('success')).toBe(true);
118 |
119 | var request = jasmine.Ajax.requests.mostRecent();
120 | expect(JSON.parse(request.params).project_id).toBeDefined()
121 |
122 | done();
123 |
124 | }, 200);
125 | }, 500);
126 | }, 500);
127 |
128 | });
129 |
130 | it('should be able to book an event, close page and refresh availability', function(done) {
131 |
132 | createWidget();
133 |
134 | setTimeout(function() {
135 |
136 | interact.clickEvent();
137 | setTimeout(function() {
138 |
139 | interact.fillSubmit();
140 | setTimeout(function() {
141 |
142 | expect($('.bookingjs-form').hasClass('success')).toBe(true);
143 | var request = jasmine.Ajax.requests.mostRecent();
144 | expect(request.url).toBe('https://api.timekit.io/v2/bookings?include=attributes,event,user');
145 |
146 | document.querySelector('.bookingjs-bookpage-close').click();
147 | setTimeout(function() {
148 |
149 | expect($('.bookingjs-bookpage').length).toBe(0);
150 | var request = jasmine.Ajax.requests.mostRecent();
151 | expect(request.url).toBe('https://api.timekit.io/v2/availability');
152 |
153 | done();
154 | }, 200);
155 | }, 200);
156 | }, 500);
157 | }, 500);
158 |
159 | });
160 |
161 | it('should be able to book an event and close page by clicking the submit button', function(done) {
162 |
163 | createWidget();
164 |
165 | setTimeout(function() {
166 |
167 | interact.clickEvent();
168 | setTimeout(function() {
169 |
170 | interact.fillSubmit();
171 | setTimeout(function() {
172 |
173 | expect($('.bookingjs-form').hasClass('success')).toBe(true);
174 |
175 | var request = jasmine.Ajax.requests.mostRecent();
176 | expect(request.url).toBe('https://api.timekit.io/v2/bookings?include=attributes,event,user');
177 |
178 | document.querySelector('.bookingjs-form-button').click();
179 | setTimeout(function() {
180 |
181 | expect($('.bookingjs-bookpage').length).toBe(0);
182 | var request = jasmine.Ajax.requests.mostRecent();
183 | expect(request.url).toBe('https://api.timekit.io/v2/availability');
184 |
185 | done();
186 | }, 200);
187 | }, 200);
188 | }, 500);
189 | }, 500);
190 |
191 | });
192 |
193 | });
194 |
--------------------------------------------------------------------------------
/test/bookingConfiguration.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var createWidget = require('./utils/createWidget');
6 | var mockAjax = require('./utils/mockAjax');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Booking configuration', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | });
15 |
16 | afterEach(function() {
17 | jasmine.Ajax.uninstall();
18 | });
19 |
20 | it('should be able override default configuration for booking and book', function(done) {
21 |
22 | mockAjax.all();
23 |
24 | var config = {
25 | booking: {
26 | event: {
27 | invite: false
28 | }
29 | }
30 | }
31 | createWidget(config);
32 |
33 | setTimeout(function() {
34 |
35 | interact.clickEvent();
36 | setTimeout(function() {
37 |
38 | interact.fillSubmit();
39 | setTimeout(function() {
40 |
41 | var request = jasmine.Ajax.requests.mostRecent();
42 | var requestData = JSON.parse(request.params);
43 |
44 | expect(request.url).toBe('https://api.timekit.io/v2/bookings?include=attributes,event,user');
45 | expect(requestData.event.invite).toBe(false);
46 |
47 | done();
48 |
49 | }, 200);
50 | }, 500);
51 | }, 500);
52 |
53 | });
54 |
55 | it('should be able override default configuration (extended) for booking and book', function(done) {
56 |
57 | mockAjax.getEmbedWidgetExtended();
58 | mockAjax.findTime();
59 | mockAjax.createBooking();
60 |
61 | var config = {
62 | project_id: '12345',
63 | app_key: '123',
64 | booking: {
65 | notify_customer_by_email: {
66 | enabled: false
67 | }
68 | }
69 | }
70 | createWidget(config);
71 |
72 | setTimeout(function() {
73 |
74 | expect($('.bookingjs-displayname span')).toContainText('McFlys Widget');
75 | interact.clickEvent();
76 |
77 | setTimeout(function() {
78 |
79 | var submitButton = $('.bookingjs-form-button .inactive-text')
80 | expect(submitButton.text()).toBe('Book McFly')
81 |
82 | var phoneNumberLabel = $('.bookingjs-form-label.label-phone')
83 | expect(phoneNumberLabel).toContainText('Phone Number')
84 |
85 | interact.fillSubmit();
86 | setTimeout(function() {
87 |
88 | var request = jasmine.Ajax.requests.mostRecent();
89 | var requestData = JSON.parse(request.params);
90 |
91 | expect(request.url).toBe('https://api.timekit.io/v2/bookings?include=attributes,event,user');
92 | expect(requestData.notify_customer_by_email.enabled).toBe(false);
93 |
94 | done();
95 |
96 | }, 200);
97 | }, 500);
98 | }, 500);
99 |
100 | });
101 |
102 | });
103 |
--------------------------------------------------------------------------------
/test/bookingFields.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var createWidget = require('./utils/createWidget');
6 | var mockAjax = require('./utils/mockAjax');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Booking fields', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | mockAjax.all();
15 | });
16 |
17 | afterEach(function() {
18 | jasmine.Ajax.uninstall();
19 | });
20 |
21 | it('should be able to add custom fields', function(done) {
22 |
23 | var config = {
24 | customer_fields: {
25 | comment: {
26 | title: 'Comment'
27 | },
28 | phone: {
29 | title: 'Phone',
30 | required: true
31 | },
32 | custom_field_1: {
33 | title: 'Custom 1',
34 | format: 'textarea'
35 | },
36 | custom_field_2: {
37 | title: 'Custom 2',
38 | format: 'checkbox',
39 | required: true
40 | },
41 | custom_field_3: {
42 | title: 'Custom 3',
43 | format: 'select',
44 | enum: ['One', 'Of', 'Many']
45 | },
46 | }
47 | }
48 |
49 | createWidget(config);
50 |
51 | setTimeout(function() {
52 |
53 | interact.clickEvent();
54 |
55 | setTimeout(function() {
56 |
57 | var commentInput = $('.input-comment');
58 | expect(commentInput).toBeInDOM();
59 | expect(commentInput).toBeVisible();
60 | expect(commentInput.attr('placeholder')).toBe('Comment');
61 | expect(commentInput.attr('required')).toBe(undefined);
62 |
63 | var phoneInput = $('.input-phone');
64 | expect(phoneInput).toBeInDOM();
65 | expect(phoneInput).toBeVisible();
66 | expect(phoneInput.attr('placeholder')).toBe('Phone');
67 | expect(phoneInput.attr('required')).toBe('required');
68 | expect(phoneInput.val()).toBe('');
69 |
70 | var custom1Input = $('.input-custom_field_1');
71 | expect(custom1Input).toBeInDOM();
72 | expect(custom1Input).toBeVisible();
73 | expect(custom1Input.is('textarea')).toBeTruthy();
74 |
75 | var custom2Input = $('.input-custom_field_2');
76 | expect(custom2Input).toBeInDOM();
77 | expect(custom2Input).toBeVisible();
78 | expect(custom2Input.attr('type')).toBe('checkbox');
79 | expect(custom2Input.attr('required')).toBe('required');
80 | expect(custom2Input.prop('checked')).toBeFalsy();
81 |
82 | var custom3Input = $('.input-custom_field_3');
83 | expect(custom3Input).toBeInDOM();
84 | expect(custom3Input).toBeVisible();
85 | expect(custom3Input.is('select')).toBeTruthy();
86 | expect(custom3Input.children().length).toBe(3);
87 | expect($(custom3Input.children()[0]).is('option')).toBeTruthy();
88 | expect($(custom3Input.children()[0]).val()).toBe('One');
89 |
90 | done();
91 |
92 | }, 500);
93 | }, 500);
94 |
95 | });
96 |
97 | it('should be able to add the phone field, prefilled and required', function(done) {
98 |
99 | var config = {
100 | customer_fields: {
101 | phone: {
102 | title: 'My custom placeholder',
103 | prefilled: '12345678',
104 | required: true
105 | }
106 | }
107 | }
108 |
109 | createWidget(config);
110 |
111 | setTimeout(function() {
112 |
113 | interact.clickEvent();
114 | setTimeout(function() {
115 |
116 | var phoneInput = $('.input-phone');
117 | expect(phoneInput).toBeInDOM();
118 | expect(phoneInput).toBeVisible();
119 | expect(phoneInput.attr('placeholder')).toBe(config.customer_fields.phone.title);
120 | expect(phoneInput.attr('required')).toBe('required');
121 | expect(phoneInput.attr('type')).toBe('tel');
122 | expect(phoneInput.val()).toBe(config.customer_fields.phone.prefilled);
123 |
124 | done();
125 |
126 | }, 500);
127 | }, 500);
128 |
129 | });
130 |
131 | it('should not output comment field by default', function(done) {
132 |
133 | var config = {
134 | customer_fields: {}
135 | }
136 |
137 | createWidget(config);
138 | setTimeout(function() {
139 |
140 | interact.clickEvent();
141 | setTimeout(function() {
142 |
143 | var commentInput = $('.input-comment');
144 | expect(commentInput.length).toBe(0);
145 | done();
146 |
147 | }, 500);
148 | }, 500);
149 |
150 | });
151 |
152 | it('should be able to lock fields for user input', function(done) {
153 |
154 | var config = {
155 | customer_fields: {
156 | name: {
157 | readonly: true,
158 | prefilled: 'My Test Name'
159 | },
160 | email: {
161 | readonly: false
162 | },
163 | comment: {
164 | title: 'Comment',
165 | readonly: true,
166 | prefilled: 'This should be submitted'
167 | }
168 | }
169 | }
170 |
171 | createWidget(config);
172 |
173 | setTimeout(function() {
174 |
175 | interact.clickEvent();
176 | setTimeout(function() {
177 |
178 | var nameInput = $('#input-name');
179 | expect(nameInput.prop('readonly')).toBe(true);
180 | expect(nameInput.is('[readonly]')).toBe(true);
181 |
182 | var emailInput = $('#input-email');
183 | expect(emailInput.prop('readonly')).toBe(false);
184 | expect(emailInput.is('[readonly]')).toBe(false);
185 |
186 | emailInput.val('someemail@timekit.io');
187 |
188 | var commentInput = $('#input-comment');
189 | expect(commentInput.prop('readonly')).toBe(true);
190 | expect(commentInput.is('[readonly]')).toBe(true);
191 | expect(commentInput.val()).toBe('This should be submitted');
192 |
193 | document.querySelector('.bookingjs-form-button').click();
194 | expect($('.bookingjs-form').hasClass('loading')).toBe(true);
195 |
196 | setTimeout(function() {
197 |
198 | expect($('.bookingjs-form').hasClass('success')).toBe(true);
199 |
200 | var request = jasmine.Ajax.requests.mostRecent();
201 | let expectedDescription = 'Name: ' + config.customer_fields.name.prefilled + '\nEmail: someemail@timekit.io\nComment: ' + config.customer_fields.comment.prefilled + '\n';
202 |
203 | var requestDescription = JSON.parse(request.params).description
204 | expect(requestDescription).toBe(expectedDescription);
205 |
206 | done();
207 |
208 | }, 200);
209 | }, 500);
210 | }, 500);
211 |
212 | });
213 |
214 | });
215 |
--------------------------------------------------------------------------------
/test/configLoading.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var createWidget = require('./utils/createWidget');
7 |
8 | describe('Config loading', function() {
9 |
10 | beforeEach(function(){
11 | loadFixtures('main.html');
12 | jasmine.Ajax.install();
13 | mockAjax.all();
14 | });
15 |
16 | afterEach(function() {
17 | jasmine.Ajax.uninstall();
18 | });
19 |
20 | it('should be able to load remote config with slug', function(done) {
21 |
22 | var widget = new TimekitBooking();
23 | var config = {
24 | project_slug: 'my-widget-slug'
25 | };
26 |
27 | widget.init(config);
28 | expect(widget).toBeDefined();
29 |
30 | setTimeout(function() {
31 |
32 | var request = jasmine.Ajax.requests.first();
33 | expect(request.url).toBe('https://api.timekit.io/v2/projects/hosted/my-widget-slug');
34 | expect(widget.getConfig().get('app_key')).toBeDefined();
35 | expect($('.bookingjs-calendar')).toBeInDOM();
36 | done();
37 |
38 | }, 50)
39 | });
40 |
41 | it('should be able to load remote config with slug and set widget ID', function(done) {
42 |
43 | var widget = new TimekitBooking();
44 | var config = {
45 | project_slug: 'my-widget-slug'
46 | };
47 |
48 | widget.init(config);
49 | expect(widget).toBeDefined();
50 |
51 | setTimeout(function() {
52 |
53 | var request = jasmine.Ajax.requests.first();
54 | expect(request.url).toBe('https://api.timekit.io/v2/projects/hosted/my-widget-slug');
55 | expect(widget.getConfig().get('project_id')).toBeDefined();
56 | expect($('.bookingjs-calendar')).toBeInDOM();
57 | done();
58 |
59 | }, 50)
60 | });
61 |
62 | it('should be able to load remote config with id', function(done) {
63 |
64 | var widget = new TimekitBooking();
65 | var config = {
66 | app_key: '12345',
67 | project_id: '12345'
68 | };
69 |
70 | widget.init(config);
71 | expect(widget).toBeDefined();
72 |
73 | setTimeout(function() {
74 |
75 | var request = jasmine.Ajax.requests.first();
76 | expect(request.url).toBe('https://api.timekit.io/v2/projects/embed/12345');
77 | expect(widget.getConfig().get('project_slug')).toBeDefined();
78 | expect($('.bookingjs-calendar')).toBeInDOM();
79 | done();
80 |
81 | }, 50)
82 | });
83 |
84 | it('should be able to load local config with widget ID set by disabling remote load', function(done) {
85 |
86 | var config = {
87 | project_id: '12345',
88 | disable_remote_load: true
89 | }
90 |
91 | var widget = createWidget(config);
92 | setTimeout(function() {
93 |
94 | var request = jasmine.Ajax.requests.first();
95 | expect(request.url).toBe('https://api.timekit.io/v2/availability');
96 | expect(widget.getConfig().get('project_id')).toBe('12345');
97 | done();
98 |
99 | }, 50)
100 | });
101 |
102 | });
103 |
--------------------------------------------------------------------------------
/test/disableBookingPage.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var createWidget = require('./utils/createWidget');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Disable booking page', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | mockAjax.all();
15 | });
16 |
17 | afterEach(function() {
18 | jasmine.Ajax.uninstall();
19 | });
20 |
21 | it('should be able disable booking page, create booking externally and register callback', function(done) {
22 |
23 | var successResponse;
24 | var clickedTimeslot;
25 |
26 | var config = {
27 | disable_confirm_page: true,
28 | callbacks: {
29 | clickTimeslot: function (response) {
30 | clickedTimeslot = response;
31 | },
32 | createBookingSuccessful: function (response) {
33 | successResponse = response;
34 | }
35 | }
36 | }
37 |
38 | spyOn(config.callbacks, 'clickTimeslot').and.callThrough();
39 | spyOn(config.callbacks, 'createBookingSuccessful').and.callThrough();
40 |
41 | var widget = createWidget(config);
42 |
43 | setTimeout(function() {
44 |
45 | interact.clickEvent();
46 | setTimeout(function() {
47 |
48 | expect(config.callbacks.clickTimeslot).toHaveBeenCalled();
49 | expect(clickedTimeslot.start).toBeDefined();
50 |
51 | var request = widget.timekitCreateBooking({
52 | name: 'John Doe',
53 | email: 'test@timekit.io'
54 | }, clickedTimeslot);
55 |
56 | expect(request.then).toBeDefined();
57 | spyOn(request, 'then').and.callThrough();
58 |
59 | request.then(function(){});
60 | setTimeout(function() {
61 |
62 | expect(request.then).toHaveBeenCalled();
63 | expect(config.callbacks.createBookingSuccessful).toHaveBeenCalled();
64 | expect(successResponse.data).toBeDefined();
65 | done();
66 |
67 | }, 200);
68 | }, 500);
69 | }, 500);
70 |
71 | });
72 |
73 | });
74 |
--------------------------------------------------------------------------------
/test/errorHandling.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var createWidget = require('./utils/createWidget');
6 | var mockAjax = require('./utils/mockAjax');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Error handling', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | mockAjax.all();
15 | window.console.warn = function() {}
16 | });
17 |
18 | afterEach(function() {
19 | jasmine.Ajax.uninstall();
20 | });
21 |
22 | it('should show error if no config is supplied at all', function(done) {
23 |
24 | var widget = new TimekitBooking();
25 | widget.init();
26 |
27 | expect($('.bookingjs-error')).toBeInDOM();
28 | expect($('.bookingjs-error-text-message')).toContainText('No configuration was supplied. Please supply a config object upon library initialization');
29 |
30 | done()
31 |
32 | });
33 |
34 | it('should show error if remote project ID is not found', function(done) {
35 |
36 | mockAjax.getNonExistingEmbedWidget();
37 |
38 | var widget = new TimekitBooking();
39 | widget.init({
40 | app_key: '12345',
41 | project_id: '54321'
42 | });
43 |
44 | setTimeout(function() {
45 |
46 | expect($('.bookingjs-error')).toBeInDOM();
47 | expect($('.bookingjs-error-text-message')).toContainText('The project could not be found, please double-check your "project_id" and "app_key"');
48 |
49 | done()
50 |
51 | }, 100);
52 |
53 | });
54 |
55 | it('should show error if project ID is supplied but no app key', function(done) {
56 |
57 | var widget = new TimekitBooking();
58 | widget.init({
59 | project_id: '54321'
60 | });
61 |
62 | setTimeout(function() {
63 |
64 | expect($('.bookingjs-error')).toBeInDOM();
65 | expect($('.bookingjs-error-text-message')).toContainText('Missing "app_key" in conjunction with "project_id", please provide your "app_key" for authentication');
66 |
67 | done()
68 |
69 | }, 100);
70 |
71 | });
72 |
73 | it('should show error if an invalid Fetch Availability parameter is sent', function(done) {
74 |
75 | mockAjax.findTimeWithError()
76 |
77 | var widget = new TimekitBooking();
78 | widget.init({
79 | app_key: '12345',
80 | project_id: '12345',
81 | availability: {
82 | future: 'wrong'
83 | }
84 | });
85 |
86 | setTimeout(function() {
87 |
88 | expect($('.bookingjs-error')).toBeInDOM();
89 | expect($('.bookingjs-error-text-message')).toContainText('An error with Timekit Fetch Availability occured');
90 |
91 | done()
92 |
93 | }, 100);
94 |
95 | });
96 |
97 | it('should show error if booking could not be created', function(done) {
98 |
99 | mockAjax.createBookingWithError()
100 |
101 | var widget = new TimekitBooking();
102 | widget.init({
103 | app_key: '12345',
104 | booking: {
105 | event: {
106 | calendar_id: 'doesnt exist'
107 | }
108 | }
109 | });
110 |
111 | setTimeout(function() {
112 |
113 | interact.clickEvent();
114 |
115 | setTimeout(function() {
116 |
117 | var inputs = interact.fillSubmit();
118 |
119 | expect($('.bookingjs-form').hasClass('loading')).toBe(true);
120 |
121 | setTimeout(function() {
122 |
123 | expect($('.bookingjs-error')).toBeInDOM();
124 | expect($('.bookingjs-error-text-message')).toContainText('An error with Timekit Create Booking occured');
125 |
126 | done();
127 |
128 | }, 200);
129 | }, 500);
130 | }, 500);
131 |
132 | });
133 |
134 | it('should show error if passed timezone is invalid', function(done) {
135 |
136 | var widget = new TimekitBooking();
137 | widget.init({
138 | app_key: '12345',
139 | ui: {
140 | timezone: 'this-is-invalid'
141 | }
142 | });
143 |
144 | setTimeout(function() {
145 |
146 | expect($('.bookingjs-error')).toBeInDOM();
147 | expect($('.bookingjs-error-text-message')).toContainText('Trying to set invalid or unknown timezone');
148 |
149 | done()
150 |
151 | }, 100);
152 |
153 | });
154 |
155 | });
156 |
--------------------------------------------------------------------------------
/test/fixtures/hour-compatibility.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Booking.js Hour compatibility fixture
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/fixtures/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Booking.js main fixture
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/fixtures/minified.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Booking.js main fixture
5 |
6 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/groupBookings.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var createWidget = require('./utils/createWidget');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Group bookings', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | mockAjax.all();
15 | });
16 |
17 | afterEach(function() {
18 | jasmine.Ajax.uninstall();
19 | });
20 |
21 | it('should be able to book a seat', function(done) {
22 |
23 | createWidget({
24 | booking: {
25 | graph: 'group_customer'
26 | }
27 | });
28 |
29 | setTimeout(function() {
30 |
31 | var request = jasmine.Ajax.requests.mostRecent();
32 | expect(request.url).toBe('https://api.timekit.io/v2/bookings/groups');
33 |
34 | interact.clickEvent();
35 | setTimeout(function() {
36 |
37 | interact.fillSubmit();
38 | expect($('.bookingjs-form').hasClass('loading')).toBe(true);
39 |
40 | setTimeout(function() {
41 |
42 | expect($('.bookingjs-form').hasClass('success')).toBe(true);
43 | expect($('.bookingjs-form-success-message')).toBeVisible();
44 |
45 | var request = jasmine.Ajax.requests.mostRecent();
46 | var requestData = JSON.parse(request.params)
47 |
48 | expect(request.url).toBe('https://api.timekit.io/v2/bookings?include=attributes,event,user');
49 | expect(requestData.graph).toBe('group_customer')
50 | expect(requestData.related.owner_booking_id).toBe('87623db3-cb5f-41e8-b85b-23b5efd04e07')
51 |
52 | done();
53 |
54 | }, 200);
55 | }, 500);
56 | }, 500);
57 |
58 | });
59 |
60 | });
61 |
--------------------------------------------------------------------------------
/test/initialization.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var baseConfig = require('./utils/defaultConfig');
7 |
8 | describe('Initialization regular', function() {
9 |
10 | beforeEach(function(){
11 | loadFixtures('main.html');
12 | jasmine.Ajax.install();
13 | mockAjax.all();
14 | });
15 |
16 | afterEach(function() {
17 | jasmine.Ajax.uninstall();
18 | });
19 |
20 | it('should be able to load the fixture page', function() {
21 | expect(window).toBeDefined();
22 | expect($('#bookingjs')).toBeInDOM();
23 | });
24 |
25 | it('should be able init and display the widget with instance pattern', function() {
26 | var widget = new TimekitBooking();
27 | widget.init(baseConfig);
28 |
29 | expect(widget).toBeDefined();
30 | expect(widget.getConfig().all()).toBeDefined();
31 | expect($('.bookingjs-calendar')).toBeInDOM();
32 | });
33 |
34 | it('should be able init and display the widget with singleton pattern', function() {
35 | new TimekitBooking().init(baseConfig);
36 | expect($('.bookingjs-calendar')).toBeInDOM();
37 | });
38 |
39 | });
40 |
41 | describe('Initialization minified', function() {
42 |
43 | beforeEach(function(){
44 | loadFixtures('minified.html');
45 | jasmine.Ajax.install();
46 | mockAjax.all();
47 | });
48 |
49 | afterEach(function() {
50 | jasmine.Ajax.uninstall();
51 | });
52 |
53 | it('should be able to load the fixture page', function() {
54 | expect(window).toBeDefined();
55 | expect($).toBeDefined();
56 | expect($('#bookingjs')).toBeInDOM();
57 | });
58 |
59 | it('should be able init and display the widget with instance pattern', function() {
60 | var widget = new TimekitBooking();
61 | widget.init(baseConfig);
62 |
63 | expect(widget).toBeDefined();
64 | expect(widget.getConfig().all()).toBeDefined();
65 | expect($('.bookingjs-calendar')).toBeInDOM();
66 | });
67 |
68 | it('should be destroy and cleanup itself', function() {
69 | var widget = new TimekitBooking();
70 | widget.init(baseConfig);
71 |
72 | expect(widget).toBeDefined();
73 | expect(widget.getConfig().all()).toBeDefined();
74 | expect($('.bookingjs-calendar')).toBeInDOM();
75 |
76 | widget.destroy();
77 | expect($('.bookingjs-calendar')).not.toBeInDOM();
78 | });
79 |
80 | });
81 |
--------------------------------------------------------------------------------
/test/mobileResponsive.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var mockAjax = require('./utils/mockAjax');
6 | var createWidget = require('./utils/createWidget');
7 | var interact = require('./utils/commonInteractions');
8 |
9 | describe('Mobile & responsive', function() {
10 |
11 | beforeEach(function(){
12 | loadFixtures('main.html');
13 | jasmine.Ajax.install();
14 | mockAjax.all();
15 | viewport.set(360, 740);
16 | });
17 |
18 | afterEach(function() {
19 | jasmine.Ajax.uninstall();
20 | viewport.reset()
21 | });
22 |
23 | it('should be able change day in mobile mode by clicking arrows', function(done) {
24 |
25 | createWidget({
26 | ui: {
27 | display_name: 'John Doe',
28 | availability_view: 'agendaWeek'
29 | }
30 | });
31 |
32 | expect($('.fc-dayGridDay-view')).toBeInDOM()
33 | var currentDay = $('.fc-col-header-cell-cushion')[0].textContent;
34 |
35 | var displayNameRect = $('.bookingjs-displayname')[0].getBoundingClientRect();
36 | var clickableArrowRect = $('.fc-next-button')[0].getBoundingClientRect();
37 |
38 | var overlap = !(displayNameRect.right < clickableArrowRect.left ||
39 | displayNameRect.left > clickableArrowRect.right ||
40 | displayNameRect.bottom < clickableArrowRect.top ||
41 | displayNameRect.top > clickableArrowRect.bottom)
42 | expect(overlap).toBe(false);
43 |
44 | setTimeout(function() {
45 |
46 | interact.clickNextArrow();
47 | setTimeout(function() {
48 |
49 | var nextDay = $('.fc-col-header-cell-cushion')[0].textContent;
50 | expect(currentDay).not.toBe(nextDay);
51 | done();
52 |
53 | }, 100);
54 | }, 200);
55 |
56 | });
57 |
58 | });
59 |
--------------------------------------------------------------------------------
/test/multipleInstances.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var baseConfig = require('./utils/defaultConfig');
6 | var mockAjax = require('./utils/mockAjax');
7 |
8 | describe('Multiple instances', function() {
9 |
10 | beforeEach(function(){
11 | loadFixtures('main.html');
12 | jasmine.Ajax.install();
13 | mockAjax.all();
14 | });
15 |
16 | afterEach(function() {
17 | jasmine.Ajax.uninstall();
18 | });
19 |
20 | it('should be able to create load multiple instances with isolated SDK configs', function() {
21 |
22 | var widget1 = new TimekitBooking();
23 | widget1.init({
24 | app_key: '12345'
25 | });
26 |
27 | var widget2 = new TimekitBooking();
28 | widget2.init({
29 | app_key: '67890'
30 | });
31 |
32 | var widget1SdkAppKey = widget1.getSdk().getConfig().appKey;
33 | var widget2SdkAppKey = widget2.getSdk().getConfig().appKey;
34 |
35 | expect(widget1SdkAppKey).not.toEqual(widget2SdkAppKey);
36 |
37 | var widget1ConfigAppKey = widget1.getConfig().get('app_key');
38 | var widget2ConfigAppKey = widget2.getConfig().get('app_key');
39 |
40 | expect(widget1ConfigAppKey).not.toEqual(widget2ConfigAppKey);
41 |
42 | });
43 |
44 | });
45 |
--------------------------------------------------------------------------------
/test/prefillFromUrl.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var qs = require('querystringify');
6 | var mockAjax = require('./utils/mockAjax');
7 | var createWidget = require('./utils/createWidget');
8 | var interact = require('./utils/commonInteractions');
9 |
10 | describe('Prefill fields from URL', function() {
11 |
12 | beforeEach(function(){
13 | loadFixtures('main.html');
14 | jasmine.Ajax.install();
15 | mockAjax.all();
16 | });
17 |
18 | afterEach(function() {
19 | jasmine.Ajax.uninstall();
20 | });
21 |
22 | it('should be able to prefill fields from URL query string', function(done) {
23 |
24 | var prefill = {
25 | 'customer.name': 'Marty',
26 | 'customer.email': 'marty.mcfly@timekit.io',
27 | 'customer.phone': '12345',
28 | 'customer.custom': 'foo'
29 | }
30 |
31 | var global = {
32 | location: {
33 | search: qs.stringify(prefill)
34 | }
35 | }
36 |
37 | var config = {
38 | customer_fields: {
39 | name: {
40 | title: 'Your name'
41 | },
42 | email: {
43 | title: 'Your email'
44 | },
45 | phone: {
46 | title: 'Your phone'
47 | }
48 | }
49 | }
50 |
51 | createWidget(config, global);
52 | setTimeout(function() {
53 |
54 | interact.clickEvent();
55 | setTimeout(function() {
56 |
57 | var nameInput = $('.input-name');
58 | expect(nameInput).toBeInDOM();
59 | expect(nameInput).toBeVisible();
60 | expect(nameInput.val()).toBe(prefill['customer.name']);
61 |
62 | var emailInput = $('.input-email');
63 | expect(emailInput).toBeInDOM();
64 | expect(emailInput).toBeVisible();
65 | expect(emailInput.val()).toBe(prefill['customer.email']);
66 |
67 | var phoneInput = $('.input-phone');
68 | expect(phoneInput).toBeInDOM();
69 | expect(phoneInput).toBeVisible();
70 | expect(phoneInput.val()).toBe(prefill['customer.phone']);
71 |
72 | var customInput = $('.input-custom');
73 | expect(customInput.length).toBe(0);
74 |
75 | done();
76 |
77 | }, 500);
78 | }, 500);
79 |
80 | });
81 |
82 | });
83 |
--------------------------------------------------------------------------------
/test/teamAvailability.spec.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | jasmine.getFixtures().fixturesPath = 'base/test/fixtures';
4 |
5 | var createWidget = require('./utils/createWidget');
6 | var teamAvailabilityConfig = require('./utils/teamAvailabilityConfig');
7 | var mockAjax = require('./utils/mockAjax');
8 | var interact = require('./utils/commonInteractions');
9 |
10 | describe('Team availability', function() {
11 |
12 | beforeEach(function(){
13 | loadFixtures('main.html');
14 | jasmine.Ajax.install();
15 | mockAjax.all();
16 | });
17 |
18 | afterEach(function() {
19 | jasmine.Ajax.uninstall();
20 | });
21 |
22 | it('should be able to book a timeslot', function(done) {
23 |
24 | createWidget(teamAvailabilityConfig);
25 | setTimeout(function() {
26 |
27 | interact.clickEvent();
28 | setTimeout(function() {
29 |
30 | interact.fillSubmit();
31 | expect($('.bookingjs-form').hasClass('loading')).toBe(true);
32 |
33 | setTimeout(function() {
34 | expect($('.bookingjs-form').hasClass('success')).toBe(true);
35 | expect($('.bookingjs-form-success-message')).toBeVisible();
36 |
37 | var successMessage = $('.bookingjs-form-success-message').html();
38 | var contains = successMessage.indexOf('We have received your booking and sent a confirmation to') > -1;
39 |
40 | // TODO:
41 | expect(contains).toBe(true);
42 | done();
43 |
44 | }, 200);
45 | }, 500);
46 | }, 500);
47 |
48 | });
49 |
50 | it('should show bookable resource name', function(done) {
51 |
52 | mockAjax.findTimeTeam();
53 | createWidget(teamAvailabilityConfig);
54 |
55 | setTimeout(function() {
56 |
57 | interact.clickEvent();
58 | setTimeout(function() {
59 |
60 | var resourceHeader = $('.bookingjs-bookpage-resource').html();
61 | expect(resourceHeader).toBe(undefined);
62 |
63 | done();
64 |
65 | }, 500);
66 | }, 500);
67 |
68 | });
69 |
70 | });
71 |
--------------------------------------------------------------------------------
/test/utils/commonInteractions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Set of shorthands for common interactions
4 | module.exports = {
5 |
6 | clickNextArrow: function () {
7 | document.querySelector(".fc-next-button").click();
8 | },
9 |
10 | clickEvent: function() {
11 | var calEvent = document.querySelector(".fc-timegrid-event");
12 | calEvent.click();
13 | return calEvent.querySelector(".fc-event-time").innerHTML;
14 | },
15 |
16 | clickListEvent: function() {
17 | var calEvent = document.querySelector('.fc-list-event');
18 | calEvent.click();
19 | return calEvent.querySelector('.fc-list-event-time').innerHTML;
20 | },
21 |
22 | fillSubmit: function() {
23 | const data = {
24 | name: 'Joe Test',
25 | email: 'test@timekit.io',
26 | comment: 'This is a test'
27 | };
28 |
29 | let nameInput = document.getElementById('input-name');
30 | let emailInput = document.getElementById('input-email');
31 | let commentInput = document.getElementById('input-comment');
32 |
33 | nameInput && (nameInput.value = data.name);
34 | emailInput && (emailInput.value = data.email);
35 | commentInput && (commentInput.value = data.comment);
36 |
37 | document.querySelector('.bookingjs-form-button').click();
38 |
39 | return data;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/test/utils/createWidget.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var baseConfig = require('./defaultConfig.js');
4 |
5 | // Creates a new widget instance with supplied config, inits it and returns it
6 | module.exports = function(config, global) {
7 |
8 | var newConfig = {};
9 | $.extend(true, newConfig, baseConfig, config);
10 |
11 | var widget = new TimekitBooking();
12 | widget.init(newConfig, global);
13 |
14 | return widget;
15 | }
16 |
--------------------------------------------------------------------------------
/test/utils/defaultConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Default config used across tests
4 | module.exports = {
5 | app_key: '12345'
6 | }
7 |
--------------------------------------------------------------------------------
/test/utils/teamAvailabilityConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Default config used across tests
4 | module.exports = {
5 | app_key: '12345',
6 | availability: {
7 | length: '2 hours',
8 | resources: [
9 | '123',
10 | '456'
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/version:
--------------------------------------------------------------------------------
1 | v3.1.1
--------------------------------------------------------------------------------
/webpack.common.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const packageJson = require('./package.json');
4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
5 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
6 |
7 | module.exports = {
8 | entry: {
9 | booking: {
10 | import: './src/booking/index.js',
11 | library: {
12 | type: 'umd',
13 | umdNamedDefine: true,
14 | name: 'TimekitBooking',
15 | },
16 | },
17 | appointments: {
18 | import: './src/services/index.js',
19 | library: {
20 | type: 'umd',
21 | umdNamedDefine: true,
22 | name: 'TimekitAppointments',
23 | },
24 | }
25 | },
26 | resolve: {
27 | extensions: ['.js']
28 | },
29 | output: {
30 | clean: true,
31 | publicPath: '/build/',
32 | filename: '[name].min.js',
33 | path: path.resolve(__dirname, 'public/build'),
34 | },
35 | module: {
36 | rules: [
37 | {
38 | test: /\.(sa|sc|c)ss$/,
39 | use: [
40 | MiniCssExtractPlugin.loader,
41 | "css-loader",
42 | "postcss-loader",
43 | "sass-loader",
44 | ],
45 | },
46 | { test: /\.html$/, loader: 'mustache-loader' },
47 | { test: /\.svg$/, loader: 'svg-inline-loader' },
48 | { test: /\.(png|jpg)$/, loader: 'file-loader' }
49 | ]
50 | },
51 | plugins: [
52 | // new BundleAnalyzerPlugin(),
53 | new webpack.DefinePlugin({
54 | VERSION: JSON.stringify(packageJson.version),
55 | }),
56 | new MiniCssExtractPlugin({
57 | filename: '[name].min.css'
58 | })
59 | ]
60 | };
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { merge } = require('webpack-merge');
3 | const common = require('./webpack.common.js');
4 | const HtmlWebpackPlugin = require('html-webpack-plugin');
5 |
6 | module.exports = merge(common, {
7 | mode: 'development',
8 | devtool: 'inline-source-map',
9 | devServer: {
10 | compress: true,
11 | allowedHosts: 'all',
12 | historyApiFallback: true,
13 | static: {
14 | publicPath: '/',
15 | directory: path.resolve(__dirname, 'public'),
16 | },
17 | historyApiFallback: true,
18 | },
19 | plugins: [
20 | new HtmlWebpackPlugin({
21 | filename: 'index.html',
22 | template: path.resolve(__dirname, 'public/index.html'),
23 | }),
24 | ],
25 | });
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const { merge } = require('webpack-merge');
2 | const common = require('./webpack.common.js');
3 | const TerserPlugin = require("terser-webpack-plugin");
4 |
5 | module.exports = merge(common, {
6 | mode: 'production',
7 | optimization: {
8 | minimize: true,
9 | minimizer: [new TerserPlugin()]
10 | },
11 | });
--------------------------------------------------------------------------------