├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── lib ├── fixer-service.js └── free-currency-service.js ├── package-lock.json ├── package.json ├── public ├── index.html └── js │ └── app.js └── server.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "rules": { 4 | "linebreak-style": 0 5 | }, 6 | "env":{ 7 | "browser": true, 8 | "jquery": true 9 | }, 10 | "globals": { 11 | "Router": false, 12 | "axios": false, 13 | "Handlebars": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michael Wanyoike 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Single Page Application Demo 2 | 3 | This is a demo project for beginners showing them how to build a Single Page Application without using a popular framework such as React, Angular, Vue, Ember or Backbone.js. 4 | 5 | The main libraries used here are: 6 | 7 | - [jQuery](https://jquery.com/) : DOM Handler 8 | - [Handlebars](https://handlebarsjs.com/) : Templates Library 9 | - [Vanilla Router](https://github.com/Graidenix/vanilla-router) - Clint-side routing 10 | 11 | You can find the tutorial this demo comes with on [Sitepoint](https://www.sitepoint.com). 12 | This application consumes data provided by [Fixer.io](https://fixer.io). 13 | 14 | ## Requirements 15 | 16 | - [Node.js](http://nodejs.org/) 17 | 18 | ## Installation Steps 19 | 20 | You'll need to register an [account](https://fixer.io/signup/free) with fixer.io in order to access their **Free API Key**. After you have cloned the repository, create a new file called `.env` at the root of the project. Provide your api key inside the file like this: 21 | 22 | ```env 23 | API_KEY= 24 | PORT=3000 25 | TIMEOUT=5000 26 | ``` 27 | 28 | 1. Clone repository 29 | 2. Run `npm install` 30 | 3. Start server with `npm start` or `node server` 31 | 4. Visit [http://localhost:3000/](http://localhost:3000/). 32 | 33 | ## License 34 | 35 | The MIT License (MIT) Copyright (c) 36 | 37 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 38 | 39 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 40 | 41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 42 | -------------------------------------------------------------------------------- /lib/fixer-service.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const axios = require('axios'); 3 | 4 | const symbols = process.env.SYMBOLS || 'EUR,USD,GBP'; 5 | 6 | const api = axios.create({ 7 | baseURL: 'http://data.fixer.io/api', 8 | params: { 9 | access_key: process.env.API_KEY, 10 | }, 11 | timeout: process.env.TIMEOUT || 5000, 12 | }); 13 | 14 | const get = async (url) => { 15 | const response = await api.get(url); 16 | const { data } = response; 17 | if (data.success) { 18 | return data; 19 | } 20 | throw new Error(data.error.type); 21 | }; 22 | 23 | module.exports = { 24 | getRates: () => get(`/latest&symbols=${symbols}&base=EUR`), 25 | getSymbols: () => get('/symbols'), 26 | getHistoricalRate: date => get(`/${date}&symbols=${symbols}&base=EUR`), 27 | }; 28 | -------------------------------------------------------------------------------- /lib/free-currency-service.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const axios = require('axios'); 3 | 4 | const api = axios.create({ 5 | baseURL: 'https://free.currencyconverterapi.com/api/v5', 6 | timeout: process.env.TIMEOUT || 5000, 7 | }); 8 | 9 | module.exports = { 10 | convertCurrency: async (from, to) => { 11 | const response = await api.get(`/convert?q=${from}_${to}&compact=y`); 12 | const key = Object.keys(response.data)[0]; 13 | const { val } = response.data[key]; 14 | return { rate: val }; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single-page-application", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "align-text": { 17 | "version": "0.1.4", 18 | "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", 19 | "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", 20 | "requires": { 21 | "kind-of": "3.2.2", 22 | "longest": "1.0.1", 23 | "repeat-string": "1.6.1" 24 | } 25 | }, 26 | "amdefine": { 27 | "version": "1.0.1", 28 | "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", 29 | "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" 30 | }, 31 | "array-flatten": { 32 | "version": "1.1.1", 33 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 34 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 35 | }, 36 | "async": { 37 | "version": "1.5.2", 38 | "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", 39 | "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" 40 | }, 41 | "axios": { 42 | "version": "0.18.0", 43 | "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", 44 | "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", 45 | "requires": { 46 | "follow-redirects": "1.4.1", 47 | "is-buffer": "1.1.6" 48 | } 49 | }, 50 | "body-parser": { 51 | "version": "1.18.2", 52 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz", 53 | "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=", 54 | "requires": { 55 | "bytes": "3.0.0", 56 | "content-type": "1.0.4", 57 | "debug": "2.6.9", 58 | "depd": "1.1.2", 59 | "http-errors": "1.6.3", 60 | "iconv-lite": "0.4.19", 61 | "on-finished": "2.3.0", 62 | "qs": "6.5.1", 63 | "raw-body": "2.3.2", 64 | "type-is": "1.6.16" 65 | }, 66 | "dependencies": { 67 | "debug": { 68 | "version": "2.6.9", 69 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 70 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 71 | "requires": { 72 | "ms": "2.0.0" 73 | } 74 | } 75 | } 76 | }, 77 | "bytes": { 78 | "version": "3.0.0", 79 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 80 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 81 | }, 82 | "camelcase": { 83 | "version": "1.2.1", 84 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", 85 | "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", 86 | "optional": true 87 | }, 88 | "center-align": { 89 | "version": "0.1.3", 90 | "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", 91 | "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", 92 | "optional": true, 93 | "requires": { 94 | "align-text": "0.1.4", 95 | "lazy-cache": "1.0.4" 96 | } 97 | }, 98 | "cliui": { 99 | "version": "2.1.0", 100 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", 101 | "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", 102 | "optional": true, 103 | "requires": { 104 | "center-align": "0.1.3", 105 | "right-align": "0.1.3", 106 | "wordwrap": "0.0.2" 107 | }, 108 | "dependencies": { 109 | "wordwrap": { 110 | "version": "0.0.2", 111 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", 112 | "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", 113 | "optional": true 114 | } 115 | } 116 | }, 117 | "content-disposition": { 118 | "version": "0.5.2", 119 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 120 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 121 | }, 122 | "content-type": { 123 | "version": "1.0.4", 124 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 125 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 126 | }, 127 | "cookie": { 128 | "version": "0.3.1", 129 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", 130 | "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" 131 | }, 132 | "cookie-signature": { 133 | "version": "1.0.6", 134 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 135 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 136 | }, 137 | "debug": { 138 | "version": "3.1.0", 139 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 140 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 141 | "requires": { 142 | "ms": "2.0.0" 143 | } 144 | }, 145 | "decamelize": { 146 | "version": "1.2.0", 147 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 148 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", 149 | "optional": true 150 | }, 151 | "depd": { 152 | "version": "1.1.2", 153 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 154 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 155 | }, 156 | "destroy": { 157 | "version": "1.0.4", 158 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 159 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 160 | }, 161 | "dotenv": { 162 | "version": "5.0.1", 163 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.1.tgz", 164 | "integrity": "sha512-4As8uPrjfwb7VXC+WnLCbXK7y+Ueb2B3zgNCePYfhxS1PYeaO1YTeplffTEcbfLhvFNGLAz90VvJs9yomG7bow==" 165 | }, 166 | "ee-first": { 167 | "version": "1.1.1", 168 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 169 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 170 | }, 171 | "encodeurl": { 172 | "version": "1.0.2", 173 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 174 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 175 | }, 176 | "escape-html": { 177 | "version": "1.0.3", 178 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 179 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 180 | }, 181 | "etag": { 182 | "version": "1.8.1", 183 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 184 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 185 | }, 186 | "express": { 187 | "version": "4.16.3", 188 | "resolved": "https://registry.npmjs.org/express/-/express-4.16.3.tgz", 189 | "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=", 190 | "requires": { 191 | "accepts": "1.3.5", 192 | "array-flatten": "1.1.1", 193 | "body-parser": "1.18.2", 194 | "content-disposition": "0.5.2", 195 | "content-type": "1.0.4", 196 | "cookie": "0.3.1", 197 | "cookie-signature": "1.0.6", 198 | "debug": "2.6.9", 199 | "depd": "1.1.2", 200 | "encodeurl": "1.0.2", 201 | "escape-html": "1.0.3", 202 | "etag": "1.8.1", 203 | "finalhandler": "1.1.1", 204 | "fresh": "0.5.2", 205 | "merge-descriptors": "1.0.1", 206 | "methods": "1.1.2", 207 | "on-finished": "2.3.0", 208 | "parseurl": "1.3.2", 209 | "path-to-regexp": "0.1.7", 210 | "proxy-addr": "2.0.3", 211 | "qs": "6.5.1", 212 | "range-parser": "1.2.0", 213 | "safe-buffer": "5.1.1", 214 | "send": "0.16.2", 215 | "serve-static": "1.13.2", 216 | "setprototypeof": "1.1.0", 217 | "statuses": "1.4.0", 218 | "type-is": "1.6.16", 219 | "utils-merge": "1.0.1", 220 | "vary": "1.1.2" 221 | }, 222 | "dependencies": { 223 | "debug": { 224 | "version": "2.6.9", 225 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 226 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 227 | "requires": { 228 | "ms": "2.0.0" 229 | } 230 | } 231 | } 232 | }, 233 | "finalhandler": { 234 | "version": "1.1.1", 235 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", 236 | "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", 237 | "requires": { 238 | "debug": "2.6.9", 239 | "encodeurl": "1.0.2", 240 | "escape-html": "1.0.3", 241 | "on-finished": "2.3.0", 242 | "parseurl": "1.3.2", 243 | "statuses": "1.4.0", 244 | "unpipe": "1.0.0" 245 | }, 246 | "dependencies": { 247 | "debug": { 248 | "version": "2.6.9", 249 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 250 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 251 | "requires": { 252 | "ms": "2.0.0" 253 | } 254 | } 255 | } 256 | }, 257 | "follow-redirects": { 258 | "version": "1.4.1", 259 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.4.1.tgz", 260 | "integrity": "sha512-uxYePVPogtya1ktGnAAXOacnbIuRMB4dkvqeNz2qTtTQsuzSfbDolV+wMMKxAmCx0bLgAKLbBOkjItMbbkR1vg==", 261 | "requires": { 262 | "debug": "3.1.0" 263 | } 264 | }, 265 | "forwarded": { 266 | "version": "0.1.2", 267 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 268 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 269 | }, 270 | "fresh": { 271 | "version": "0.5.2", 272 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 273 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 274 | }, 275 | "handlebars": { 276 | "version": "4.0.11", 277 | "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", 278 | "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", 279 | "requires": { 280 | "async": "1.5.2", 281 | "optimist": "0.6.1", 282 | "source-map": "0.4.4", 283 | "uglify-js": "2.8.29" 284 | } 285 | }, 286 | "http-errors": { 287 | "version": "1.6.3", 288 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 289 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 290 | "requires": { 291 | "depd": "1.1.2", 292 | "inherits": "2.0.3", 293 | "setprototypeof": "1.1.0", 294 | "statuses": "1.4.0" 295 | } 296 | }, 297 | "iconv-lite": { 298 | "version": "0.4.19", 299 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 300 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 301 | }, 302 | "inherits": { 303 | "version": "2.0.3", 304 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 305 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 306 | }, 307 | "ipaddr.js": { 308 | "version": "1.6.0", 309 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.6.0.tgz", 310 | "integrity": "sha1-4/o1e3c9phnybpXwSdBVxyeW+Gs=" 311 | }, 312 | "is-buffer": { 313 | "version": "1.1.6", 314 | "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", 315 | "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" 316 | }, 317 | "jquery": { 318 | "version": "3.3.1", 319 | "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.3.1.tgz", 320 | "integrity": "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg==" 321 | }, 322 | "kind-of": { 323 | "version": "3.2.2", 324 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", 325 | "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", 326 | "requires": { 327 | "is-buffer": "1.1.6" 328 | } 329 | }, 330 | "lazy-cache": { 331 | "version": "1.0.4", 332 | "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", 333 | "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", 334 | "optional": true 335 | }, 336 | "longest": { 337 | "version": "1.0.1", 338 | "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", 339 | "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" 340 | }, 341 | "media-typer": { 342 | "version": "0.3.0", 343 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 344 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 345 | }, 346 | "merge-descriptors": { 347 | "version": "1.0.1", 348 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 349 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 350 | }, 351 | "methods": { 352 | "version": "1.1.2", 353 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 354 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 355 | }, 356 | "mime": { 357 | "version": "1.4.1", 358 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz", 359 | "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==" 360 | }, 361 | "mime-db": { 362 | "version": "1.33.0", 363 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 364 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 365 | }, 366 | "mime-types": { 367 | "version": "2.1.18", 368 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 369 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 370 | "requires": { 371 | "mime-db": "1.33.0" 372 | } 373 | }, 374 | "minimist": { 375 | "version": "0.0.10", 376 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", 377 | "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" 378 | }, 379 | "ms": { 380 | "version": "2.0.0", 381 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 382 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 383 | }, 384 | "negotiator": { 385 | "version": "0.6.1", 386 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 387 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 388 | }, 389 | "on-finished": { 390 | "version": "2.3.0", 391 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 392 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 393 | "requires": { 394 | "ee-first": "1.1.1" 395 | } 396 | }, 397 | "optimist": { 398 | "version": "0.6.1", 399 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", 400 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", 401 | "requires": { 402 | "minimist": "0.0.10", 403 | "wordwrap": "0.0.3" 404 | } 405 | }, 406 | "parseurl": { 407 | "version": "1.3.2", 408 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 409 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 410 | }, 411 | "path-to-regexp": { 412 | "version": "0.1.7", 413 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 414 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 415 | }, 416 | "proxy-addr": { 417 | "version": "2.0.3", 418 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.3.tgz", 419 | "integrity": "sha512-jQTChiCJteusULxjBp8+jftSQE5Obdl3k4cnmLA6WXtK6XFuWRnvVL7aCiBqaLPM8c4ph0S4tKna8XvmIwEnXQ==", 420 | "requires": { 421 | "forwarded": "0.1.2", 422 | "ipaddr.js": "1.6.0" 423 | } 424 | }, 425 | "qs": { 426 | "version": "6.5.1", 427 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", 428 | "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" 429 | }, 430 | "range-parser": { 431 | "version": "1.2.0", 432 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 433 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 434 | }, 435 | "raw-body": { 436 | "version": "2.3.2", 437 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz", 438 | "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=", 439 | "requires": { 440 | "bytes": "3.0.0", 441 | "http-errors": "1.6.2", 442 | "iconv-lite": "0.4.19", 443 | "unpipe": "1.0.0" 444 | }, 445 | "dependencies": { 446 | "depd": { 447 | "version": "1.1.1", 448 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz", 449 | "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k=" 450 | }, 451 | "http-errors": { 452 | "version": "1.6.2", 453 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", 454 | "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=", 455 | "requires": { 456 | "depd": "1.1.1", 457 | "inherits": "2.0.3", 458 | "setprototypeof": "1.0.3", 459 | "statuses": "1.4.0" 460 | } 461 | }, 462 | "setprototypeof": { 463 | "version": "1.0.3", 464 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz", 465 | "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ=" 466 | } 467 | } 468 | }, 469 | "repeat-string": { 470 | "version": "1.6.1", 471 | "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", 472 | "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" 473 | }, 474 | "right-align": { 475 | "version": "0.1.3", 476 | "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", 477 | "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", 478 | "optional": true, 479 | "requires": { 480 | "align-text": "0.1.4" 481 | } 482 | }, 483 | "safe-buffer": { 484 | "version": "5.1.1", 485 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 486 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" 487 | }, 488 | "semantic-ui-calendar": { 489 | "version": "0.0.8", 490 | "resolved": "https://registry.npmjs.org/semantic-ui-calendar/-/semantic-ui-calendar-0.0.8.tgz", 491 | "integrity": "sha1-rhZQTk2AFsrFupoArYzNSkMN4+8=" 492 | }, 493 | "semantic-ui-css": { 494 | "version": "2.3.1", 495 | "resolved": "https://registry.npmjs.org/semantic-ui-css/-/semantic-ui-css-2.3.1.tgz", 496 | "integrity": "sha512-8M2OkoKZHfEnNUYB4Ha8q+tTAWN/g17X2l6HUg6n3DP4QDJLQl1xyhnRvM9UhJpsRvkRqgWgQLbRA6cl7Ep2dw==", 497 | "requires": { 498 | "jquery": "3.3.1" 499 | } 500 | }, 501 | "send": { 502 | "version": "0.16.2", 503 | "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", 504 | "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", 505 | "requires": { 506 | "debug": "2.6.9", 507 | "depd": "1.1.2", 508 | "destroy": "1.0.4", 509 | "encodeurl": "1.0.2", 510 | "escape-html": "1.0.3", 511 | "etag": "1.8.1", 512 | "fresh": "0.5.2", 513 | "http-errors": "1.6.3", 514 | "mime": "1.4.1", 515 | "ms": "2.0.0", 516 | "on-finished": "2.3.0", 517 | "range-parser": "1.2.0", 518 | "statuses": "1.4.0" 519 | }, 520 | "dependencies": { 521 | "debug": { 522 | "version": "2.6.9", 523 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 524 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 525 | "requires": { 526 | "ms": "2.0.0" 527 | } 528 | } 529 | } 530 | }, 531 | "serve-static": { 532 | "version": "1.13.2", 533 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz", 534 | "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==", 535 | "requires": { 536 | "encodeurl": "1.0.2", 537 | "escape-html": "1.0.3", 538 | "parseurl": "1.3.2", 539 | "send": "0.16.2" 540 | } 541 | }, 542 | "setprototypeof": { 543 | "version": "1.1.0", 544 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 545 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 546 | }, 547 | "source-map": { 548 | "version": "0.4.4", 549 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", 550 | "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", 551 | "requires": { 552 | "amdefine": "1.0.1" 553 | } 554 | }, 555 | "statuses": { 556 | "version": "1.4.0", 557 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", 558 | "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" 559 | }, 560 | "type-is": { 561 | "version": "1.6.16", 562 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 563 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 564 | "requires": { 565 | "media-typer": "0.3.0", 566 | "mime-types": "2.1.18" 567 | } 568 | }, 569 | "uglify-js": { 570 | "version": "2.8.29", 571 | "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", 572 | "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", 573 | "optional": true, 574 | "requires": { 575 | "source-map": "0.5.7", 576 | "uglify-to-browserify": "1.0.2", 577 | "yargs": "3.10.0" 578 | }, 579 | "dependencies": { 580 | "source-map": { 581 | "version": "0.5.7", 582 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", 583 | "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", 584 | "optional": true 585 | } 586 | } 587 | }, 588 | "uglify-to-browserify": { 589 | "version": "1.0.2", 590 | "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", 591 | "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", 592 | "optional": true 593 | }, 594 | "unpipe": { 595 | "version": "1.0.0", 596 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 597 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 598 | }, 599 | "utils-merge": { 600 | "version": "1.0.1", 601 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 602 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 603 | }, 604 | "vanilla-router": { 605 | "version": "1.2.6", 606 | "resolved": "https://registry.npmjs.org/vanilla-router/-/vanilla-router-1.2.6.tgz", 607 | "integrity": "sha512-PtfRKEhUVDbr8SsqR8ZVLGvDVsWd57fXYV1dtABMudT3ZHzKgQNifkn1zI/N6bSNhBKKnJ+yR61BXVdzP4mi9w==" 608 | }, 609 | "vary": { 610 | "version": "1.1.2", 611 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 612 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 613 | }, 614 | "window-size": { 615 | "version": "0.1.0", 616 | "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", 617 | "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", 618 | "optional": true 619 | }, 620 | "wordwrap": { 621 | "version": "0.0.3", 622 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", 623 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" 624 | }, 625 | "yargs": { 626 | "version": "3.10.0", 627 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", 628 | "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", 629 | "optional": true, 630 | "requires": { 631 | "camelcase": "1.2.1", 632 | "cliui": "2.1.0", 633 | "decamelize": "1.2.0", 634 | "window-size": "0.1.0" 635 | } 636 | } 637 | } 638 | } 639 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "single-page-application", 3 | "version": "1.0.0", 4 | "description": "This is a demo project for beginners showing them how to build a Single Page Application without using a popular framework such as React, Angular, Vue, Ember or Backbone.js.", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "keywords": [], 11 | "author": "Michael Wanyoike", 12 | "license": "MIT", 13 | "dependencies": { 14 | "axios": "^0.18.0", 15 | "body-parser": "^1.18.2", 16 | "dotenv": "^5.0.1", 17 | "express": "^4.16.3", 18 | "handlebars": "^4.0.11", 19 | "jquery": "^3.3.1", 20 | "semantic-ui-calendar": "0.0.8", 21 | "semantic-ui-css": "^2.3.1", 22 | "vanilla-router": "^1.2.6" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | SPA Demo 10 | 11 | 12 |
13 | 14 | 29 | 30 | 42 | 43 | 44 | 83 | 84 | 124 | 125 | 146 | 147 | 148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 |
159 | 160 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', () => { 2 | const el = $('#app'); 3 | 4 | // Compile Handlebar Templates 5 | const errorTemplate = Handlebars.compile($('#error-template').html()); 6 | const ratesTemplate = Handlebars.compile($('#rates-template').html()); 7 | const exchangeTemplate = Handlebars.compile($('#exchange-template').html()); 8 | const historicalTemplate = Handlebars.compile($('#historical-template').html()); 9 | 10 | // Instantiate api handler 11 | const api = axios.create({ 12 | baseURL: 'http://localhost:3000/api', 13 | timeout: 5000, 14 | }); 15 | 16 | const router = new Router({ 17 | mode: 'history', 18 | page404: (path) => { 19 | const html = errorTemplate({ 20 | color: 'yellow', 21 | title: 'Error 404 - Page NOT Found!', 22 | message: `The path '/${path}' does not exist on this site`, 23 | }); 24 | el.html(html); 25 | }, 26 | }); 27 | 28 | // Display Error Banner 29 | const showError = (error) => { 30 | const { title, message } = error.response.data; 31 | const html = errorTemplate({ color: 'red', title, message }); 32 | el.html(html); 33 | }; 34 | 35 | // Display Latest Currency Rates 36 | router.add('/', async () => { 37 | // Display loader first 38 | let html = ratesTemplate(); 39 | el.html(html); 40 | try { 41 | // Load Currency Rates 42 | const response = await api.get('/rates'); 43 | const { base, date, rates } = response.data; 44 | // Display Rates Table 45 | html = ratesTemplate({ base, date, rates }); 46 | el.html(html); 47 | $('.loading').removeClass('loading'); 48 | } catch (error) { 49 | showError(error); 50 | } 51 | }); 52 | 53 | // Perform POST request, calculate and display conversion results 54 | const getConversionResults = async () => { 55 | // Extract form data 56 | const from = $('#from').val(); 57 | const to = $('#to').val(); 58 | const amount = $('#amount').val(); 59 | // Send post data to express(proxy) server 60 | try { 61 | const response = await api.post('/convert', { from, to }); 62 | const { rate } = response.data; 63 | const result = rate * amount; 64 | $('#result').html(`${to} ${result}`); 65 | } catch (error) { 66 | showError(error); 67 | } finally { 68 | $('#result-segment').removeClass('loading'); 69 | } 70 | }; 71 | 72 | // Handle Convert Button Click Event 73 | const convertRatesHandler = () => { 74 | if ($('.ui.form').form('is valid')) { 75 | // hide error message 76 | $('.ui.error.message').hide(); 77 | // Post to express server 78 | $('#result-segment').addClass('loading'); 79 | getConversionResults(); 80 | // Prevent page from submitting to server 81 | return false; 82 | } 83 | return true; 84 | }; 85 | 86 | router.add('/exchange', async () => { 87 | // Display loader first 88 | let html = exchangeTemplate(); 89 | el.html(html); 90 | try { 91 | // Load Symbols 92 | const response = await api.get('/symbols'); 93 | const { symbols } = response.data; 94 | html = exchangeTemplate({ symbols }); 95 | el.html(html); 96 | $('.loading').removeClass('loading'); 97 | // Specify Form Validation Rules 98 | $('.ui.form').form({ 99 | fields: { 100 | from: 'empty', 101 | to: 'empty', 102 | amount: 'decimal', 103 | }, 104 | }); 105 | // Specify Submit Handler 106 | $('.submit').click(convertRatesHandler); 107 | } catch (error) { 108 | showError(error); 109 | } 110 | }); 111 | 112 | const getHistoricalRates = async () => { 113 | const date = $('#date').val(); 114 | try { 115 | const response = await api.post('/historical', { date }); 116 | const { base, rates } = response.data; 117 | const html = ratesTemplate({ base, date, rates }); 118 | $('#historical-table').html(html); 119 | } catch (error) { 120 | showError(error); 121 | } finally { 122 | $('.segment').removeClass('loading'); 123 | } 124 | }; 125 | 126 | const historicalRatesHandler = () => { 127 | if ($('.ui.form').form('is valid')) { 128 | // hide error message 129 | $('.ui.error.message').hide(); 130 | // Indicate loading status 131 | $('.segment').addClass('loading'); 132 | getHistoricalRates(); 133 | // Prevent page from submitting to server 134 | return false; 135 | } 136 | return true; 137 | }; 138 | 139 | router.add('/historical', () => { 140 | const html = historicalTemplate(); 141 | el.html(html); 142 | // Activate Date Picker 143 | $('#calendar').calendar({ 144 | type: 'date', 145 | formatter: { 146 | date: date => new Date(date).toISOString().split('T')[0], 147 | }, 148 | }); 149 | $('.ui.form').form({ 150 | fields: { 151 | date: 'empty', 152 | }, 153 | }); 154 | $('.submit').click(historicalRatesHandler); 155 | }); 156 | 157 | router.navigateTo(window.location.pathname); 158 | 159 | // Highlight Active Menu on Load 160 | const link = $(`a[href$='${window.location.pathname}']`); 161 | link.addClass('active'); 162 | 163 | $('a').on('click', (event) => { 164 | // Block page load 165 | event.preventDefault(); 166 | 167 | // Highlight Active Menu on Click 168 | const target = $(event.target); 169 | $('.item').removeClass('active'); 170 | target.addClass('active'); 171 | 172 | // Navigate to clicked url 173 | const href = target.attr('href'); 174 | const path = href.substr(href.lastIndexOf('/')); 175 | router.navigateTo(path); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const { getRates, getSymbols, getHistoricalRate } = require('./lib/fixer-service'); 5 | const { convertCurrency } = require('./lib/free-currency-service'); 6 | 7 | const app = express(); 8 | const port = process.env.PORT || 3000; 9 | 10 | // Set public folder as root 11 | app.use(express.static('public')); 12 | 13 | // Parse POST data as URL encoded data 14 | app.use(bodyParser.urlencoded({ 15 | extended: true, 16 | })); 17 | 18 | // Parse POST data as JSON 19 | app.use(bodyParser.json()); 20 | 21 | // Provide access to node_modules folder 22 | app.use('/scripts', express.static(`${__dirname}/node_modules/`)); 23 | 24 | 25 | const errorHandler = (err, req, res) => { 26 | if (err.response) { 27 | // The request was made and the server responded with a status code 28 | // that falls out of the range of 2xx 29 | res.status(403).send({ title: 'Server responded with an error', message: err.message }); 30 | } else if (err.request) { 31 | // The request was made but no response was received 32 | res.status(503).send({ title: 'Unable to communicate with server', message: err.message }); 33 | } else { 34 | // Something happened in setting up the request that triggered an Error 35 | res.status(500).send({ title: 'An unexpected error occurred', message: err.message }); 36 | } 37 | }; 38 | 39 | // Fetch Latest Currency Rates 40 | app.get('/api/rates', async (req, res) => { 41 | try { 42 | const data = await getRates(); 43 | res.setHeader('Content-Type', 'application/json'); 44 | res.send(data); 45 | } catch (error) { 46 | errorHandler(error, req, res); 47 | } 48 | }); 49 | 50 | // Fetch Symbols 51 | app.get('/api/symbols', async (req, res) => { 52 | try { 53 | const data = await getSymbols(); 54 | res.setHeader('Content-Type', 'application/json'); 55 | res.send(data); 56 | } catch (error) { 57 | errorHandler(error, req, res); 58 | } 59 | }); 60 | 61 | // Convert Currency 62 | app.post('/api/convert', async (req, res) => { 63 | try { 64 | const { from, to } = req.body; 65 | const data = await convertCurrency(from, to); 66 | res.setHeader('Content-Type', 'application/json'); 67 | res.send(data); 68 | } catch (error) { 69 | errorHandler(error, req, res); 70 | } 71 | }); 72 | 73 | // Fetch Currency Rates by date 74 | app.post('/api/historical', async (req, res) => { 75 | try { 76 | const { date } = req.body; 77 | const data = await getHistoricalRate(date); 78 | res.setHeader('Content-Type', 'application/json'); 79 | res.send(data); 80 | } catch (error) { 81 | errorHandler(error, req, res); 82 | } 83 | }); 84 | 85 | // Redirect all traffic to index.html 86 | app.use((req, res) => res.sendFile(`${__dirname}/public/index.html`)); 87 | 88 | app.listen(port, () => { 89 | // eslint-disable-next-line no-console 90 | console.log('listening on %d', port); 91 | }); 92 | --------------------------------------------------------------------------------