├── .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 | ![Booking.js Screenshot](misc/widget-screenshot.png) 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 | 10 | 16 | 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 |
13 |
14 |
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 |
52 | 53 | Haircut and dyeing 54 | 1 hour 55 | 56 | 57 | The full monty 58 | 2 hours 59 | 60 |
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 | 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 |
4 |

{{ chosenDate }}

5 |

{{ chosenTime }}

6 | {{#allocatedResource}} 7 | {{ allocatedResourcePrefix }} 8 |

{{ allocatedResource }}

9 | {{/allocatedResource}} 10 |
11 |
12 |
13 |
14 |
{{& successMessage }}
15 |
16 |
17 |
18 | 24 |
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 | 17 |
18 | -------------------------------------------------------------------------------- /src/booking/templates/fields/label.html: -------------------------------------------------------------------------------- 1 |
2 |

5 | {{& title }} 6 |

7 |
-------------------------------------------------------------------------------- /src/booking/templates/fields/multi-checkbox.html: -------------------------------------------------------------------------------- 1 |
2 | {{ title }} 3 | {{# enum }} 4 | 19 | {{/ enum }} 20 |
21 | -------------------------------------------------------------------------------- /src/booking/templates/fields/select.html: -------------------------------------------------------------------------------- 1 |
2 | 7 | 18 |
19 | {{& arrowDownIcon }} 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/booking/templates/fields/tel.html: -------------------------------------------------------------------------------- 1 |
2 | {{^ hidden }} 3 | 8 | {{/ hidden }} 9 | 21 |
22 | -------------------------------------------------------------------------------- /src/booking/templates/fields/text.html: -------------------------------------------------------------------------------- 1 |
2 | {{^ hidden }} 3 | 8 | {{/ hidden }} 9 | 22 |
23 | -------------------------------------------------------------------------------- /src/booking/templates/fields/textarea.html: -------------------------------------------------------------------------------- 1 |
2 | 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 |
4 |

Confirm Rescheduling

5 |

Former Booking:

6 |

Date: {{ oldDate }}

7 |

Time: {{ oldTime }}

8 |

Rescheduled Booking:

9 |

Date: {{ chosenDate }}

10 |

Time: {{ chosenTime }}

11 | {{#allocatedResource}} 12 | {{ allocatedResourcePrefix }} 13 |

{{ allocatedResource }}

14 | {{/allocatedResource}} 15 |
16 |
17 |
18 |
19 |
{{& successMessage }}
20 |
21 |
22 |
23 | 29 |
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 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
{{ serviceName }}, {{ locationName }}
19 |
20 | {{ selectorOptions.title }} 21 |
22 |
23 | {{ selectorOptions.description }} 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
-------------------------------------------------------------------------------- /src/services/templates/error.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | Ouch, we've encountered a problem 20 |
21 |
22 |
23 |
24 |
25 |
26 |
{{& message }}
27 | {{& context }} 28 |
29 |
30 |
31 |
32 |
-------------------------------------------------------------------------------- /src/services/templates/locations.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | {{#service}} 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{/service}} 21 | {{^service}} 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{/service}} 30 | 31 |
32 | {{ selectorOptions.title }} 33 |
34 | 35 |
36 | {{ selectorOptions.description }} 37 |
38 | 39 | {{#selectorOptions.search_bar.enabled}} 40 |
41 | 42 | 43 | Use my current location 44 |
45 | {{/selectorOptions.search_bar.enabled}} 46 |
47 |
48 |
-------------------------------------------------------------------------------- /src/services/templates/services.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | {{#location}} 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{/location}} 21 | {{^location}} 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {{/location}} 30 | 31 |
32 | {{ selectorOptions.title }} 33 |
34 |
35 | {{ selectorOptions.description }} 36 |
37 |
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 | }); --------------------------------------------------------------------------------