├── .deployment ├── .gitignore ├── .nuxt ├── App.vue ├── client.js ├── components │ ├── no-ssr.js │ ├── nuxt-child.js │ ├── nuxt-error.vue │ ├── nuxt-link.js │ ├── nuxt-loading.vue │ └── nuxt.vue ├── empty.js ├── index.js ├── loading.html ├── middleware.js ├── router.js ├── server.js ├── store.js ├── utils.js └── views │ ├── app.template.html │ └── error.html ├── LICENSE ├── README.md ├── assets └── cda-data.json ├── components ├── MoreInfo.vue ├── SpeakingGlobe.vue └── SpeakingTable.vue ├── deploy.cmd ├── favicon.ico ├── geo-lookup.js ├── layouts └── default.vue ├── mixins └── createGlobe.js ├── nuxt.config.js ├── package.json ├── pages └── index.vue ├── static ├── favicon.ico └── world7.jpg ├── store └── index.js ├── web.config └── yarn.lock /.deployment: -------------------------------------------------------------------------------- 1 | [config] 2 | command = deploy.cmd 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .### OSX ### 2 | *.DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two 7 | Icon 8 | # Thumbnails 9 | ._* 10 | # Files that might appear in the root of a volume 11 | .DocumentRevisions-V100 12 | .fseventsd 13 | .Spotlight-V100 14 | .TemporaryItems 15 | .Trashes 16 | .VolumeIcon.icns 17 | .com.apple.timemachine.donotpresent 18 | # Directories potentially created on remote AFP share 19 | .AppleDB 20 | .AppleDesktop 21 | Network Trash Folder 22 | Temporary Items 23 | .apdisk 24 | 25 | ### Node ### 26 | # Logs 27 | logs 28 | *.log 29 | npm-debug.log* 30 | 31 | # Runtime data 32 | pids 33 | *.pid 34 | *.seed 35 | *.pid.lock 36 | 37 | # Directory for instrumented libs generated by jscoverage/JSCover 38 | lib-cov 39 | 40 | # Coverage directory used by tools like istanbul 41 | coverage 42 | 43 | # nyc test coverage 44 | .nyc_output 45 | 46 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 47 | .grunt 48 | 49 | # node-waf configuration 50 | .lock-wscript 51 | 52 | # Compiled binary addons (http://nodejs.org/api/addons.html) 53 | build/Release 54 | 55 | # Dependency directories 56 | node_modules 57 | jspm_packages 58 | package-lock.json 59 | 60 | # Optional npm cache directory 61 | .npm 62 | 63 | # Optional eslint cache 64 | .eslintcache 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity -------------------------------------------------------------------------------- /.nuxt/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 88 | 89 | -------------------------------------------------------------------------------- /.nuxt/client.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import middleware from './middleware' 3 | import { createApp, NuxtError } from './index' 4 | import { 5 | applyAsyncData, 6 | sanitizeComponent, 7 | getMatchedComponents, 8 | getMatchedComponentsInstances, 9 | flatMapComponents, 10 | getContext, 11 | middlewareSeries, 12 | promisify, 13 | getLocation, 14 | compile 15 | } from './utils' 16 | 17 | const noopData = () => { return {} } 18 | const noopFetch = () => {} 19 | 20 | // Global shared references 21 | let _lastPaths = [] 22 | let _lastComponentsFiles = [] 23 | let app 24 | let router 25 | let store 26 | 27 | // Try to rehydrate SSR data from window 28 | const NUXT = window.__NUXT__ || {} 29 | NUXT.components = window.__COMPONENTS__ || null 30 | 31 | 32 | // Setup global Vue error handler 33 | const defaultErrorHandler = Vue.config.errorHandler 34 | Vue.config.errorHandler = function (err, vm, info) { 35 | err.statusCode = err.statusCode || err.name || 'Whoops!' 36 | err.message = err.message || err.toString() 37 | 38 | // Show Nuxt Error Page 39 | if(vm && vm.$root && vm.$root.$nuxt && vm.$root.$nuxt.error && info !== 'render function') { 40 | vm.$root.$nuxt.error(err) 41 | } 42 | 43 | // Call other handler if exist 44 | if (typeof defaultErrorHandler === 'function') { 45 | return defaultErrorHandler(...arguments) 46 | } 47 | 48 | // Log to console 49 | if (process.env.NODE_ENV !== 'production') { 50 | console.error(err) 51 | } else { 52 | console.error(err.message) 53 | } 54 | } 55 | 56 | 57 | // Create and mount App 58 | createApp() 59 | .then(mountApp) 60 | .catch(err => { 61 | console.error('[nuxt] Error while initializing app', err) 62 | }) 63 | 64 | function componentOption(component, key, ...args) { 65 | if (!component || !component.options || !component.options[key]) { 66 | return {} 67 | } 68 | const option = component.options[key] 69 | if (typeof option === 'function') { 70 | return option(...args) 71 | } 72 | return option 73 | } 74 | 75 | function mapTransitions(Components, to, from) { 76 | const componentTransitions = component => { 77 | const transition = componentOption(component, 'transition', to, from) || {} 78 | return (typeof transition === 'string' ? { name: transition } : transition) 79 | } 80 | 81 | return Components.map(Component => { 82 | // Clone original object to prevent overrides 83 | const transitions = Object.assign({}, componentTransitions(Component)) 84 | 85 | // Combine transitions & prefer `leave` transitions of 'from' route 86 | if (from && from.matched.length && from.matched[0].components.default) { 87 | const from_transitions = componentTransitions(from.matched[0].components.default) 88 | Object.keys(from_transitions) 89 | .filter(key => from_transitions[key] && key.toLowerCase().indexOf('leave') !== -1) 90 | .forEach(key => { transitions[key] = from_transitions[key] }) 91 | } 92 | 93 | return transitions 94 | }) 95 | } 96 | 97 | async function loadAsyncComponents (to, from, next) { 98 | // Check if route hash changed 99 | const fromPath = from.fullPath.split('#')[0] 100 | const toPath = to.fullPath.split('#')[0] 101 | this._hashChanged = fromPath === toPath 102 | 103 | 104 | if (!this._hashChanged && this.$loading.start) { 105 | this.$loading.start() 106 | } 107 | 108 | 109 | try { 110 | await Promise.all(flatMapComponents(to, (Component, _, match, key) => { 111 | // If component already resolved 112 | if (typeof Component !== 'function' || Component.options) { 113 | const _Component = sanitizeComponent(Component) 114 | match.components[key] = _Component 115 | return _Component 116 | } 117 | 118 | // Resolve component 119 | return Component().then(Component => { 120 | const _Component = sanitizeComponent(Component) 121 | match.components[key] = _Component 122 | return _Component 123 | }) 124 | })) 125 | 126 | next() 127 | } catch (err) { 128 | if (!err) err = {} 129 | const statusCode = err.statusCode || err.status || (err.response && err.response.status) || 500 130 | this.error({ statusCode, message: err.message }) 131 | next(false) 132 | } 133 | } 134 | 135 | // Get matched components 136 | function resolveComponents(router) { 137 | const path = getLocation(router.options.base, router.options.mode) 138 | 139 | return flatMapComponents(router.match(path), (Component, _, match, key, index) => { 140 | // If component already resolved 141 | if (typeof Component !== 'function' || Component.options) { 142 | const _Component = sanitizeComponent(Component) 143 | match.components[key] = _Component 144 | return _Component 145 | } 146 | 147 | // Resolve component 148 | return Component().then(Component => { 149 | const _Component = sanitizeComponent(Component) 150 | if (NUXT.serverRendered) { 151 | applyAsyncData(_Component, NUXT.data[index]) 152 | if (NUXT.components) { 153 | Component.options.components = Object.assign(_Component.options.components, NUXT.components[index]) 154 | } 155 | _Component._Ctor = _Component 156 | } 157 | match.components[key] = _Component 158 | return _Component 159 | }) 160 | }) 161 | } 162 | 163 | function callMiddleware (Components, context, layout) { 164 | let midd = [] 165 | let unknownMiddleware = false 166 | 167 | // If layout is undefined, only call global middleware 168 | if (typeof layout !== 'undefined') { 169 | midd = [] // Exclude global middleware if layout defined (already called before) 170 | if (layout.middleware) { 171 | midd = midd.concat(layout.middleware) 172 | } 173 | Components.forEach(Component => { 174 | if (Component.options.middleware) { 175 | midd = midd.concat(Component.options.middleware) 176 | } 177 | }) 178 | } 179 | 180 | midd = midd.map(name => { 181 | if (typeof middleware[name] !== 'function') { 182 | unknownMiddleware = true 183 | this.error({ statusCode: 500, message: 'Unknown middleware ' + name }) 184 | } 185 | return middleware[name] 186 | }) 187 | 188 | if (unknownMiddleware) return 189 | return middlewareSeries(midd, context) 190 | } 191 | 192 | async function render (to, from, next) { 193 | if (this._hashChanged) return next() 194 | 195 | // nextCalled is true when redirected 196 | let nextCalled = false 197 | const _next = path => { 198 | if(this.$loading.finish) this.$loading.finish() 199 | if (nextCalled) return 200 | nextCalled = true 201 | next(path) 202 | } 203 | 204 | // Update context 205 | const context = getContext({ 206 | to, 207 | from, 208 | store, 209 | isClient: true, 210 | next: _next.bind(this), 211 | error: this.error.bind(this) 212 | }, app) 213 | this._context = context 214 | this._dateLastError = this.$options._nuxt.dateErr 215 | this._hadError = !!this.$options._nuxt.err 216 | 217 | // Get route's matched components 218 | const Components = getMatchedComponents(to) 219 | 220 | // If no Components matched, generate 404 221 | if (!Components.length) { 222 | // Default layout 223 | await callMiddleware.call(this, Components, context) 224 | if (context._redirected) return 225 | 226 | // Load layout for error page 227 | const layout = await this.loadLayout(typeof NuxtError.layout === 'function' ? NuxtError.layout(context) : NuxtError.layout) 228 | await callMiddleware.call(this, Components, context, layout) 229 | if (context._redirected) return 230 | 231 | this.error({ statusCode: 404, message: 'This page could not be found' }) 232 | return next() 233 | } 234 | 235 | // Update ._data and other properties if hot reloaded 236 | Components.forEach(Component => { 237 | if (Component._Ctor && Component._Ctor.options) { 238 | Component.options.asyncData = Component._Ctor.options.asyncData 239 | Component.options.fetch = Component._Ctor.options.fetch 240 | } 241 | }) 242 | 243 | // Apply transitions 244 | this.setTransitions(mapTransitions(Components, to, from)) 245 | 246 | try { 247 | // Call middleware 248 | await callMiddleware.call(this, Components, context) 249 | if (context._redirected) return 250 | 251 | // Set layout 252 | let layout = Components[0].options.layout 253 | if (typeof layout === 'function') { 254 | layout = layout(context) 255 | } 256 | layout = await this.loadLayout(layout) 257 | 258 | // Call middleware for layout 259 | await callMiddleware.call(this, Components, context, layout) 260 | if (context._redirected) return 261 | 262 | // Call .validate() 263 | let isValid = true 264 | Components.forEach(Component => { 265 | if (!isValid) return 266 | if (typeof Component.options.validate !== 'function') return 267 | isValid = Component.options.validate({ 268 | params: to.params || {}, 269 | query : to.query || {}, 270 | store: context.store 271 | }) 272 | }) 273 | // ...If .validate() returned false 274 | if (!isValid) { 275 | this.error({ statusCode: 404, message: 'This page could not be found' }) 276 | return next() 277 | } 278 | 279 | // Call asyncData & fetch hooks on components matched by the route. 280 | await Promise.all(Components.map((Component, i) => { 281 | // Check if only children route changed 282 | Component._path = compile(to.matched[i].path)(to.params) 283 | if (!this._hadError && this._isMounted && Component._path === _lastPaths[i] && (i + 1) !== Components.length) { 284 | return Promise.resolve() 285 | } 286 | 287 | let promises = [] 288 | 289 | const hasAsyncData = Component.options.asyncData && typeof Component.options.asyncData === 'function' 290 | const hasFetch = !!Component.options.fetch 291 | const loadingIncrease = (hasAsyncData && hasFetch) ? 30 : 45 292 | 293 | // Call asyncData(context) 294 | if (hasAsyncData) { 295 | const promise = promisify(Component.options.asyncData, context) 296 | .then(asyncDataResult => { 297 | applyAsyncData(Component, asyncDataResult) 298 | if(this.$loading.increase) this.$loading.increase(loadingIncrease) 299 | }) 300 | promises.push(promise) 301 | } 302 | 303 | // Call fetch(context) 304 | if (hasFetch) { 305 | let p = Component.options.fetch(context) 306 | if (!p || (!(p instanceof Promise) && (typeof p.then !== 'function'))) { 307 | p = Promise.resolve(p) 308 | } 309 | p.then(fetchResult => { 310 | if(this.$loading.increase) this.$loading.increase(loadingIncrease) 311 | }) 312 | promises.push(p) 313 | } 314 | 315 | return Promise.all(promises) 316 | })) 317 | 318 | _lastPaths = Components.map((Component, i) => compile(to.matched[i].path)(to.params)) 319 | 320 | if(this.$loading.finish) this.$loading.finish() 321 | 322 | // If not redirected 323 | if (!nextCalled) next() 324 | 325 | } catch (error) { 326 | if (!error) error = {} 327 | _lastPaths = [] 328 | error.statusCode = error.statusCode || error.status || (error.response && error.response.status) || 500 329 | 330 | // Load error layout 331 | let layout = NuxtError.layout 332 | if (typeof layout === 'function') { 333 | layout = layout(context) 334 | } 335 | await this.loadLayout(layout) 336 | 337 | this.error(error) 338 | next(false) 339 | } 340 | } 341 | 342 | // Fix components format in matched, it's due to code-splitting of vue-router 343 | function normalizeComponents (to, ___) { 344 | flatMapComponents(to, (Component, _, match, key) => { 345 | if (typeof Component === 'object' && !Component.options) { 346 | // Updated via vue-router resolveAsyncComponents() 347 | Component = Vue.extend(Component) 348 | Component._Ctor = Component 349 | match.components[key] = Component 350 | } 351 | return Component 352 | }) 353 | } 354 | 355 | // When navigating on a different route but the same component is used, Vue.js 356 | // Will not update the instance data, so we have to update $data ourselves 357 | function fixPrepatch (to, ___) { 358 | if (this._hashChanged) return 359 | 360 | Vue.nextTick(() => { 361 | const instances = getMatchedComponentsInstances(to) 362 | 363 | _lastComponentsFiles = instances.map((instance, i) => { 364 | if (!instance) return ''; 365 | 366 | if (_lastPaths[i] === instance.constructor._path && typeof instance.constructor.options.data === 'function') { 367 | const newData = instance.constructor.options.data.call(instance) 368 | for (let key in newData) { 369 | Vue.set(instance.$data, key, newData[key]) 370 | } 371 | } 372 | 373 | return instance.constructor.options.__file 374 | }) 375 | 376 | // Hide error component if no error 377 | if (this._hadError && this._dateLastError === this.$options._nuxt.dateErr) { 378 | this.error() 379 | } 380 | 381 | // Set layout 382 | let layout = this.$options._nuxt.err ? NuxtError.layout : to.matched[0].components.default.options.layout 383 | if (typeof layout === 'function') { 384 | layout = layout(this._context) 385 | } 386 | this.setLayout(layout) 387 | 388 | 389 | // Hot reloading 390 | setTimeout(() => hotReloadAPI(this), 100) 391 | 392 | }) 393 | } 394 | 395 | function nuxtReady (app) { 396 | window._nuxtReadyCbs.forEach((cb) => { 397 | if (typeof cb === 'function') { 398 | cb(app) 399 | } 400 | }) 401 | // Special JSDOM 402 | if (typeof window._onNuxtLoaded === 'function') { 403 | window._onNuxtLoaded(app) 404 | } 405 | // Add router hooks 406 | router.afterEach(function (to, from) { 407 | app.$nuxt.$emit('routeChanged', to, from) 408 | }) 409 | } 410 | 411 | 412 | // Special hot reload with asyncData(context) 413 | function hotReloadAPI (_app) { 414 | if (!module.hot) return 415 | 416 | let $components = [] 417 | let $nuxt = _app.$nuxt 418 | 419 | while ($nuxt && $nuxt.$children && $nuxt.$children.length) { 420 | $nuxt.$children.forEach((child, i) => { 421 | if (child.$vnode.data.nuxtChild) { 422 | let hasAlready = false 423 | $components.forEach(component => { 424 | if (component.$options.__file === child.$options.__file) { 425 | hasAlready = true 426 | } 427 | }) 428 | if (!hasAlready) { 429 | $components.push(child) 430 | } 431 | } 432 | $nuxt = child 433 | }) 434 | } 435 | 436 | $components.forEach(addHotReload.bind(_app)) 437 | } 438 | 439 | function addHotReload ($component, depth) { 440 | if ($component.$vnode.data._hasHotReload) return 441 | $component.$vnode.data._hasHotReload = true 442 | 443 | var _forceUpdate = $component.$forceUpdate.bind($component.$parent) 444 | 445 | $component.$vnode.context.$forceUpdate = () => { 446 | let Components = getMatchedComponents(router.currentRoute) 447 | let Component = Components[depth] 448 | if (!Component) return _forceUpdate() 449 | if (typeof Component === 'object' && !Component.options) { 450 | // Updated via vue-router resolveAsyncComponents() 451 | Component = Vue.extend(Component) 452 | Component._Ctor = Component 453 | } 454 | this.error() 455 | let promises = [] 456 | const next = function (path) { 457 | this.$loading.finish && this.$loading.finish() 458 | router.push(path) 459 | } 460 | let context = getContext({ route: router.currentRoute, store, isClient: true, isHMR: true, next: next.bind(this), error: this.error }, app) 461 | this.$loading.start && this.$loading.start() 462 | callMiddleware.call(this, Components, context) 463 | .then(() => { 464 | // If layout changed 465 | if (depth !== 0) return Promise.resolve() 466 | let layout = Component.options.layout || 'default' 467 | if (typeof layout === 'function') { 468 | layout = layout(context) 469 | } 470 | if (this.layoutName === layout) return Promise.resolve() 471 | let promise = this.loadLayout(layout) 472 | promise.then(() => { 473 | this.setLayout(layout) 474 | Vue.nextTick(() => hotReloadAPI(this)) 475 | }) 476 | return promise 477 | }) 478 | .then(() => { 479 | return callMiddleware.call(this, Components, context, this.layout) 480 | }) 481 | .then(() => { 482 | // Call asyncData(context) 483 | let pAsyncData = promisify(Component.options.asyncData || noopData, context) 484 | pAsyncData.then((asyncDataResult) => { 485 | applyAsyncData(Component, asyncDataResult) 486 | this.$loading.increase && this.$loading.increase(30) 487 | }) 488 | promises.push(pAsyncData) 489 | // Call fetch() 490 | Component.options.fetch = Component.options.fetch || noopFetch 491 | let pFetch = Component.options.fetch(context) 492 | if (!pFetch || (!(pFetch instanceof Promise) && (typeof pFetch.then !== 'function'))) { pFetch = Promise.resolve(pFetch) } 493 | pFetch.then(() => this.$loading.increase && this.$loading.increase(30)) 494 | promises.push(pFetch) 495 | return Promise.all(promises) 496 | }) 497 | .then(() => { 498 | this.$loading.finish && this.$loading.finish() 499 | _forceUpdate() 500 | setTimeout(() => hotReloadAPI(this), 100) 501 | }) 502 | } 503 | } 504 | 505 | 506 | async function mountApp(__app) { 507 | // Set global variables 508 | app = __app.app 509 | router = __app.router 510 | store = __app.store 511 | 512 | // Resolve route components 513 | const Components = await Promise.all(resolveComponents(router)) 514 | 515 | // Create Vue instance 516 | const _app = new Vue(app) 517 | 518 | // Load layout 519 | const layout = NUXT.layout || 'default' 520 | await _app.loadLayout(layout) 521 | _app.setLayout(layout) 522 | 523 | // Mounts Vue app to DOM element 524 | const mountApp = () => { 525 | _app.$mount('#__nuxt') 526 | 527 | // Listen for first Vue update 528 | Vue.nextTick(() => { 529 | // Call window.onNuxtReady callbacks 530 | nuxtReady(_app) 531 | 532 | // Enable hot reloading 533 | hotReloadAPI(_app) 534 | 535 | }) 536 | } 537 | 538 | // Enable transitions 539 | _app.setTransitions = _app.$options._nuxt.setTransitions.bind(_app) 540 | if (Components.length) { 541 | _app.setTransitions(mapTransitions(Components, router.currentRoute)) 542 | _lastPaths = router.currentRoute.matched.map(route => compile(route.path)(router.currentRoute.params)) 543 | _lastComponentsFiles = Components.map(Component => Component.options.__file) 544 | } 545 | 546 | // Initialize error handler 547 | _app.error = _app.$options._nuxt.error.bind(_app) 548 | _app.$loading = {} // To avoid error while _app.$nuxt does not exist 549 | if (NUXT.error) _app.error(NUXT.error) 550 | 551 | // Add router hooks 552 | router.beforeEach(loadAsyncComponents.bind(_app)) 553 | router.beforeEach(render.bind(_app)) 554 | router.afterEach(normalizeComponents) 555 | router.afterEach(fixPrepatch.bind(_app)) 556 | 557 | // If page already is server rendered 558 | if (NUXT.serverRendered) { 559 | mountApp() 560 | return 561 | } 562 | 563 | render.call(_app, router.currentRoute, router.currentRoute, path => { 564 | if (!path) { 565 | normalizeComponents(router.currentRoute, router.currentRoute) 566 | fixPrepatch.call(_app, router.currentRoute, router.currentRoute) 567 | mountApp() 568 | return 569 | } 570 | 571 | // Push the path and then mount app 572 | let mounted = false 573 | router.afterEach(() => { 574 | if (mounted) return 575 | mounted = true 576 | mountApp() 577 | }) 578 | router.push(path) 579 | }) 580 | } 581 | -------------------------------------------------------------------------------- /.nuxt/components/no-ssr.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** From https://github.com/egoist/vue-no-ssr 3 | ** With the authorization of @egoist 4 | */ 5 | export default { 6 | name: 'no-ssr', 7 | props: ['placeholder'], 8 | data () { 9 | return { 10 | canRender: false 11 | } 12 | }, 13 | mounted () { 14 | this.canRender = true 15 | }, 16 | render (h) { 17 | if (this.canRender) { 18 | if ( 19 | process.env.NODE_ENV === 'development' && 20 | this.$slots.default.length > 1 21 | ) { 22 | throw new Error(' You cannot use multiple child components') 23 | } 24 | return this.$slots.default[0] 25 | } 26 | return h('div', { class: { 'no-ssr-placeholder': true } }, this.placeholder) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.nuxt/components/nuxt-child.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const transitionsKeys = [ 4 | 'name', 5 | 'mode', 6 | 'appear', 7 | 'css', 8 | 'type', 9 | 'duration', 10 | 'enterClass', 11 | 'leaveClass', 12 | 'appearClass', 13 | 'enterActiveClass', 14 | 'enterActiveClass', 15 | 'leaveActiveClass', 16 | 'appearActiveClass', 17 | 'enterToClass', 18 | 'leaveToClass', 19 | 'appearToClass' 20 | ] 21 | const listenersKeys = [ 22 | 'beforeEnter', 23 | 'enter', 24 | 'afterEnter', 25 | 'enterCancelled', 26 | 'beforeLeave', 27 | 'leave', 28 | 'afterLeave', 29 | 'leaveCancelled', 30 | 'beforeAppear', 31 | 'appear', 32 | 'afterAppear', 33 | 'appearCancelled' 34 | ] 35 | 36 | export default { 37 | name: 'nuxt-child', 38 | functional: true, 39 | render (h, { parent, data }) { 40 | data.nuxtChild = true 41 | const _parent = parent 42 | const transitions = parent.$nuxt.nuxt.transitions 43 | const defaultTransition = parent.$nuxt.nuxt.defaultTransition 44 | let depth = 0 45 | while (parent) { 46 | if (parent.$vnode && parent.$vnode.data.nuxtChild) { 47 | depth++ 48 | } 49 | parent = parent.$parent 50 | } 51 | data.nuxtChildDepth = depth 52 | const transition = transitions[depth] || defaultTransition 53 | let transitionProps = {} 54 | transitionsKeys.forEach((key) => { 55 | if (typeof transition[key] !== 'undefined') { 56 | transitionProps[key] = transition[key] 57 | } 58 | }) 59 | let listeners = {} 60 | listenersKeys.forEach((key) => { 61 | if (typeof transition[key] === 'function') { 62 | listeners[key] = transition[key].bind(_parent) 63 | } 64 | }) 65 | return h('transition', { 66 | props: transitionProps, 67 | on: listeners 68 | }, [ 69 | h('router-view', data) 70 | ]) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.nuxt/components/nuxt-error.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 67 | 68 | 113 | -------------------------------------------------------------------------------- /.nuxt/components/nuxt-link.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | export default { 4 | name: 'nuxt-link', 5 | functional: true, 6 | render (h, { data, children }) { 7 | return h('router-link', data, children) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.nuxt/components/nuxt-loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 89 | 90 | 104 | -------------------------------------------------------------------------------- /.nuxt/components/nuxt.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 33 | -------------------------------------------------------------------------------- /.nuxt/empty.js: -------------------------------------------------------------------------------- 1 | // This file is intentionally left empty for noop aliases 2 | -------------------------------------------------------------------------------- /.nuxt/index.js: -------------------------------------------------------------------------------- 1 | import 'es6-promise/auto' 2 | import Vue from 'vue' 3 | import Meta from 'vue-meta' 4 | import { createRouter } from './router.js' 5 | import NoSSR from './components/no-ssr.js' 6 | import NuxtChild from './components/nuxt-child.js' 7 | import NuxtLink from './components/nuxt-link.js' 8 | import NuxtError from './components/nuxt-error.vue' 9 | import Nuxt from './components/nuxt.vue' 10 | import App from './App.vue' 11 | import { getContext, getLocation } from './utils' 12 | import { createStore } from './store.js' 13 | 14 | 15 | // Component: 16 | Vue.component(NoSSR.name, NoSSR) 17 | 18 | // Component: 19 | Vue.component(NuxtChild.name, NuxtChild) 20 | 21 | // Component: 22 | Vue.component(NuxtLink.name, NuxtLink) 23 | 24 | // Component: ` 25 | Vue.component(Nuxt.name, Nuxt) 26 | 27 | // vue-meta configuration 28 | Vue.use(Meta, { 29 | keyName: 'head', // the component option name that vue-meta looks for meta info on. 30 | attribute: 'data-n-head', // the attribute name vue-meta adds to the tags it observes 31 | ssrAttribute: 'data-n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered 32 | tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag 33 | }) 34 | 35 | const defaultTransition = {"name":"page","mode":"out-in","appear":false,"appearClass":"appear","appearActiveClass":"appear-active","appearToClass":"appear-to"} 36 | 37 | async function createApp (ssrContext) { 38 | const router = createRouter() 39 | 40 | const store = createStore() 41 | 42 | // Create Root instance 43 | // here we inject the router and store to all child components, 44 | // making them available everywhere as `this.$router` and `this.$store`. 45 | const app = { 46 | router, 47 | store, 48 | _nuxt: { 49 | defaultTransition, 50 | transitions: [ defaultTransition ], 51 | setTransitions (transitions) { 52 | if (!Array.isArray(transitions)) { 53 | transitions = [ transitions ] 54 | } 55 | transitions = transitions.map((transition) => { 56 | if (!transition) { 57 | transition = defaultTransition 58 | } else if (typeof transition === 'string') { 59 | transition = Object.assign({}, defaultTransition, { name: transition }) 60 | } else { 61 | transition = Object.assign({}, defaultTransition, transition) 62 | } 63 | return transition 64 | }) 65 | this.$options._nuxt.transitions = transitions 66 | return transitions 67 | }, 68 | err: null, 69 | dateErr: null, 70 | error (err) { 71 | err = err || null 72 | if (typeof err === 'string') { 73 | err = { statusCode: 500, message: err } 74 | } 75 | const _nuxt = this._nuxt || this.$options._nuxt 76 | _nuxt.dateErr = Date.now() 77 | _nuxt.err = err 78 | return err 79 | } 80 | }, 81 | ...App 82 | } 83 | 84 | // Make app available in store 85 | store.app = app 86 | 87 | const next = ssrContext ? ssrContext.next : location => app.router.push(location) 88 | let route 89 | if (ssrContext) { 90 | route = router.resolve(ssrContext.url).route 91 | } else { 92 | const path = getLocation(router.options.base) 93 | route = router.resolve(path).route 94 | } 95 | const ctx = getContext({ 96 | isServer: !!ssrContext, 97 | isClient: !ssrContext, 98 | route, 99 | next, 100 | error: app._nuxt.error.bind(app), 101 | store, 102 | req: ssrContext ? ssrContext.req : undefined, 103 | res: ssrContext ? ssrContext.res : undefined, 104 | beforeRenderFns: ssrContext ? ssrContext.beforeRenderFns : undefined 105 | }, app) 106 | 107 | const inject = function (key, value) { 108 | if (!key) throw new Error('inject(key, value) has no key provided') 109 | if (!value) throw new Error('inject(key, value) has no value provided') 110 | key = '$' + key 111 | // Add into app 112 | app[key] = value 113 | // Add into vm 114 | Vue.use(() => { 115 | const installKey = '__nuxt_' + key + '_installed__' 116 | if (Vue[installKey]) return 117 | Vue[installKey] = true 118 | if (!Vue.prototype.hasOwnProperty(key)) { 119 | Object.defineProperty(Vue.prototype, key, { 120 | get () { 121 | return this.$root.$options[key] 122 | } 123 | }) 124 | } 125 | }) 126 | 127 | // Add into store 128 | store[key] = app[key] 129 | 130 | } 131 | 132 | 133 | if (process.browser) { 134 | // Replace store state before plugins execution 135 | if (window.__NUXT__ && window.__NUXT__.state) { 136 | store.replaceState(window.__NUXT__.state) 137 | } 138 | } 139 | 140 | 141 | 142 | 143 | 144 | if (process.server && ssrContext && ssrContext.url) { 145 | await new Promise((resolve, reject) => { 146 | router.push(ssrContext.url, resolve, reject) 147 | }) 148 | } 149 | 150 | return { 151 | app, 152 | router, 153 | store 154 | } 155 | } 156 | 157 | export { createApp, NuxtError } 158 | -------------------------------------------------------------------------------- /.nuxt/loading.html: -------------------------------------------------------------------------------- 1 | 43 | 44 |
45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /.nuxt/middleware.js: -------------------------------------------------------------------------------- 1 | export default {} 2 | -------------------------------------------------------------------------------- /.nuxt/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | const _57d756df = () => import('../pages/index.vue' /* webpackChunkName: "pages/index" */).then(m => m.default || m) 7 | 8 | 9 | 10 | const scrollBehavior = (to, from, savedPosition) => { 11 | // SavedPosition is only available for popstate navigations. 12 | if (savedPosition) { 13 | return savedPosition 14 | } else { 15 | let position = {} 16 | // If no children detected 17 | if (to.matched.length < 2) { 18 | // Scroll to the top of the page 19 | position = { x: 0, y: 0 } 20 | } 21 | else if (to.matched.some((r) => r.components.default.options.scrollToTop)) { 22 | // If one of the children has scrollToTop option set to true 23 | position = { x: 0, y: 0 } 24 | } 25 | // If link has anchor, scroll to anchor by returning the selector 26 | if (to.hash) { 27 | position = { selector: to.hash } 28 | } 29 | return position 30 | } 31 | } 32 | 33 | 34 | export function createRouter () { 35 | return new Router({ 36 | mode: 'history', 37 | base: '/', 38 | linkActiveClass: 'nuxt-link-active', 39 | linkExactActiveClass: 'nuxt-link-exact-active', 40 | scrollBehavior, 41 | routes: [ 42 | { 43 | path: "/", 44 | component: _57d756df, 45 | name: "index" 46 | } 47 | ], 48 | fallback: false 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /.nuxt/server.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import clone from 'clone' 3 | import { stringify } from 'querystring' 4 | import { omit } from 'lodash' 5 | import middleware from './middleware' 6 | import { createApp, NuxtError } from './index' 7 | import { applyAsyncData, sanitizeComponent, getMatchedComponents, getContext, middlewareSeries, promisify, urlJoin } from './utils' 8 | 9 | const debug = require('debug')('nuxt:render') 10 | debug.color = 4 // force blue color 11 | 12 | const isDev = true 13 | 14 | const noopApp = () => new Vue({ render: (h) => h('div') }) 15 | 16 | const createNext = context => opts => { 17 | context.redirected = opts 18 | // If nuxt generate 19 | if (!context.res) { 20 | context.nuxt.serverRendered = false 21 | return 22 | } 23 | opts.query = stringify(opts.query) 24 | opts.path = opts.path + (opts.query ? '?' + opts.query : '') 25 | if (opts.path.indexOf('http') !== 0 && ('/' !== '/' && opts.path.indexOf('/') !== 0)) { 26 | opts.path = urlJoin('/', opts.path) 27 | } 28 | // Avoid loop redirect 29 | if (opts.path === context.url) { 30 | context.redirected = false 31 | return 32 | } 33 | context.res.writeHead(opts.status, { 34 | 'Location': opts.path 35 | }) 36 | context.res.end() 37 | } 38 | 39 | // This exported function will be called by `bundleRenderer`. 40 | // This is where we perform data-prefetching to determine the 41 | // state of our application before actually rendering it. 42 | // Since data fetching is async, this function is expected to 43 | // return a Promise that resolves to the app instance. 44 | export default async context => { 45 | // Create context.next for simulate next() of beforeEach() when wanted to redirect 46 | context.redirected = false 47 | context.next = createNext(context) 48 | context.beforeRenderFns = [] 49 | 50 | const { app, router, store } = await createApp(context) 51 | const _app = new Vue(app) 52 | 53 | 54 | // Add store to the context 55 | context.store = store 56 | 57 | 58 | // Add route to the context 59 | context.route = router.currentRoute 60 | 61 | // Nuxt object 62 | context.nuxt = { layout: 'default', data: [], error: null, state: null, serverRendered: true } 63 | 64 | // Add meta infos 65 | context.meta = _app.$meta() 66 | 67 | // Error function 68 | context.error = _app.$options._nuxt.error.bind(_app) 69 | 70 | // Keep asyncData for each matched component in context 71 | context.asyncData = {} 72 | 73 | // Create shared ctx 74 | const ctx = getContext(context, app) 75 | 76 | const s = isDev && Date.now() 77 | 78 | // Resolve components 79 | let Components = [] 80 | try { 81 | Components = await Promise.all(getMatchedComponents(router.match(context.url)).map(Component => { 82 | if (typeof Component !== 'function' || Component.cid) { 83 | return sanitizeComponent(Component) 84 | } 85 | return Component().then(Component => sanitizeComponent(Component)) 86 | })) 87 | } catch (err) { 88 | // Throw back error to renderRoute() 89 | throw err 90 | } 91 | 92 | 93 | // Dispatch store nuxtServerInit 94 | if (store._actions && store._actions.nuxtServerInit) { 95 | await store.dispatch('nuxtServerInit', ctx) 96 | } 97 | // ...If there is a redirect 98 | if (context.redirected) return noopApp() 99 | 100 | 101 | // Call global middleware (nuxt.config.js) 102 | let midd = [] 103 | midd = midd.map((name) => { 104 | if (typeof middleware[name] !== 'function') { 105 | context.nuxt.error = context.error({ statusCode: 500, message: 'Unknown middleware ' + name }) 106 | } 107 | return middleware[name] 108 | }) 109 | if (!context.nuxt.error) { 110 | await middlewareSeries(midd, ctx) 111 | } 112 | // ...If there is a redirect 113 | if (context.redirected) return noopApp() 114 | 115 | // Set layout 116 | let layout = Components.length ? Components[0].options.layout : NuxtError.layout 117 | if (typeof layout === 'function') layout = layout(ctx) 118 | await _app.loadLayout(layout) 119 | layout = _app.setLayout(layout) 120 | // ...Set layout to __NUXT__ 121 | context.nuxt.layout = _app.layoutName 122 | 123 | // Call middleware (layout + pages) 124 | if (!context.nuxt.error) { 125 | midd = [] 126 | if (layout.middleware) midd = midd.concat(layout.middleware) 127 | Components.forEach((Component) => { 128 | if (Component.options.middleware) { 129 | midd = midd.concat(Component.options.middleware) 130 | } 131 | }) 132 | midd = midd.map((name) => { 133 | if (typeof middleware[name] !== 'function') { 134 | context.nuxt.error = context.error({ statusCode: 500, message: 'Unknown middleware ' + name }) 135 | } 136 | return middleware[name] 137 | }) 138 | 139 | await middlewareSeries(midd, ctx) 140 | 141 | // If there is a redirect 142 | if (context.redirected) return noopApp() 143 | } 144 | 145 | // Call .validate() 146 | let isValid = true 147 | Components.forEach((Component) => { 148 | if (!isValid) return 149 | if (typeof Component.options.validate !== 'function') return 150 | isValid = Component.options.validate({ 151 | params: context.route.params || {}, 152 | query: context.route.query || {}, 153 | store: ctx.store 154 | }) 155 | }) 156 | // ...If .validate() returned false 157 | if (!isValid) { 158 | // Don't server-render the page in generate mode 159 | if (context._generate) { 160 | context.nuxt.serverRendered = false 161 | } 162 | // Call the 404 error by making the Components array empty 163 | Components = [] 164 | } 165 | 166 | // Call asyncData & fetch hooks on components matched by the route. 167 | let asyncDatas = await Promise.all(Components.map(Component => { 168 | let promises = [] 169 | 170 | // Call asyncData(context) 171 | if (Component.options.asyncData && typeof Component.options.asyncData === 'function') { 172 | let promise = promisify(Component.options.asyncData, ctx) 173 | promise.then(asyncDataResult => { 174 | context.asyncData[Component.cid] = asyncDataResult 175 | applyAsyncData(Component) 176 | return asyncDataResult 177 | }) 178 | promises.push(promise) 179 | } else { 180 | promises.push(null) 181 | } 182 | 183 | // Call fetch(context) 184 | if (Component.options.fetch) { 185 | promises.push(Component.options.fetch(ctx)) 186 | } 187 | else { 188 | promises.push(null) 189 | } 190 | 191 | return Promise.all(promises) 192 | })) 193 | 194 | // If no Components found, returns 404 195 | if (!Components.length) { 196 | context.nuxt.error = context.error({ statusCode: 404, message: 'This page could not be found' }) 197 | } 198 | 199 | if (asyncDatas.length) debug('Data fetching ' + context.url + ': ' + (Date.now() - s) + 'ms') 200 | 201 | // datas are the first row of each 202 | context.nuxt.data = asyncDatas.map(r => r[0] || {}) 203 | 204 | // If an error occured in the execution 205 | if (_app.$options._nuxt.err) { 206 | context.nuxt.error = _app.$options._nuxt.err 207 | } 208 | 209 | 210 | // Add the state from the vuex store 211 | context.nuxt.state = store.state 212 | 213 | 214 | await Promise.all(context.beforeRenderFns.map((fn) => promisify(fn, { Components, nuxtState: context.nuxt }))) 215 | 216 | // If no error, return main app 217 | if (!context.nuxt.error) { 218 | return _app 219 | } 220 | 221 | // Load layout for error page 222 | layout = (typeof NuxtError.layout === 'function' ? NuxtError.layout(ctx) : NuxtError.layout) 223 | context.nuxt.layout = layout || '' 224 | await _app.loadLayout(layout) 225 | _app.setLayout(layout) 226 | 227 | return _app 228 | } 229 | -------------------------------------------------------------------------------- /.nuxt/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | // Recursive find files in {srcDir}/store 7 | const files = require.context('@/store', true, /^\.\/.*\.(js|ts)$/) 8 | const filenames = files.keys() 9 | 10 | // Store 11 | let storeData = {} 12 | 13 | // Check if store/index.js exists 14 | let indexFilename 15 | filenames.forEach((filename) => { 16 | if (filename.indexOf('./index.') !== -1) { 17 | indexFilename = filename 18 | } 19 | }) 20 | if (indexFilename) { 21 | storeData = getModule(indexFilename) 22 | } 23 | 24 | // If store is not an exported method = modules store 25 | if (typeof storeData !== 'function') { 26 | 27 | // Store modules 28 | if (!storeData.modules) { 29 | storeData.modules = {} 30 | } 31 | 32 | for (let filename of filenames) { 33 | let name = filename.replace(/^\.\//, '').replace(/\.(js|ts)$/, '') 34 | if (name === 'index') continue 35 | 36 | let namePath = name.split(/\//) 37 | let module = getModuleNamespace(storeData, namePath) 38 | 39 | name = namePath.pop() 40 | module[name] = getModule(filename) 41 | module[name].namespaced = true 42 | } 43 | 44 | } 45 | 46 | // createStore 47 | export const createStore = storeData instanceof Function ? storeData : () => { 48 | return new Vuex.Store(Object.assign({ 49 | strict: (process.env.NODE_ENV !== 'production'), 50 | }, storeData, { 51 | state: storeData.state instanceof Function ? storeData.state() : {} 52 | })) 53 | } 54 | 55 | // Dynamically require module 56 | function getModule (filename) { 57 | const file = files(filename) 58 | const module = file.default || file 59 | if (module.commit) { 60 | throw new Error('[nuxt] store/' + filename.replace('./', '') + ' should export a method which returns a Vuex instance.') 61 | } 62 | if (module.state && typeof module.state !== 'function') { 63 | throw new Error('[nuxt] state should be a function in store/' + filename.replace('./', '')) 64 | } 65 | return module 66 | } 67 | 68 | function getModuleNamespace (storeData, namePath) { 69 | if (namePath.length === 1) { 70 | return storeData.modules 71 | } 72 | let namespace = namePath.shift() 73 | storeData.modules[namespace] = storeData.modules[namespace] || {} 74 | storeData.modules[namespace].namespaced = true 75 | storeData.modules[namespace].modules = storeData.modules[namespace].modules || {} 76 | return getModuleNamespace(storeData.modules[namespace], namePath) 77 | } 78 | -------------------------------------------------------------------------------- /.nuxt/utils.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const noopData = () => ({}) 4 | 5 | // window.onNuxtReady(() => console.log('Ready')) hook 6 | // Useful for jsdom testing or plugins (https://github.com/tmpvar/jsdom#dealing-with-asynchronous-script-loading) 7 | if (process.browser) { 8 | window._nuxtReadyCbs = [] 9 | window.onNuxtReady = function (cb) { 10 | window._nuxtReadyCbs.push(cb) 11 | } 12 | } 13 | 14 | export function applyAsyncData (Component, asyncData) { 15 | const ComponentData = Component.options.data || noopData 16 | // Prevent calling this method for each request on SSR context 17 | if (!asyncData && Component.options.hasAsyncData) { 18 | return 19 | } 20 | Component.options.hasAsyncData = true 21 | Component.options.data = function () { 22 | const data = ComponentData.call(this) 23 | if (this.$ssrContext) { 24 | asyncData = this.$ssrContext.asyncData[Component.cid] 25 | } 26 | return { ...data, ...asyncData } 27 | } 28 | if (Component._Ctor && Component._Ctor.options) { 29 | Component._Ctor.options.data = Component.options.data 30 | } 31 | } 32 | 33 | export function sanitizeComponent (Component) { 34 | if (!Component.options) { 35 | Component = Vue.extend(Component) // fix issue #6 36 | Component._Ctor = Component 37 | } else { 38 | Component._Ctor = Component 39 | Component.extendOptions = Component.options 40 | } 41 | // For debugging purpose 42 | if (!Component.options.name && Component.options.__file) { 43 | Component.options.name = Component.options.__file 44 | } 45 | return Component 46 | } 47 | 48 | export function getMatchedComponents (route) { 49 | return [].concat.apply([], route.matched.map(function (m) { 50 | return Object.keys(m.components).map(function (key) { 51 | return m.components[key] 52 | }) 53 | })) 54 | } 55 | 56 | export function getMatchedComponentsInstances (route) { 57 | return [].concat.apply([], route.matched.map(function (m) { 58 | return Object.keys(m.instances).map(function (key) { 59 | return m.instances[key] 60 | }) 61 | })) 62 | } 63 | 64 | export function flatMapComponents (route, fn) { 65 | return Array.prototype.concat.apply([], route.matched.map(function (m, index) { 66 | return Object.keys(m.components).map(function (key) { 67 | return fn(m.components[key], m.instances[key], m, key, index) 68 | }) 69 | })) 70 | } 71 | 72 | export function getContext (context, app) { 73 | let ctx = { 74 | isServer: !!context.isServer, 75 | isClient: !!context.isClient, 76 | isStatic: process.static, 77 | isDev: true, 78 | isHMR: context.isHMR || false, 79 | app: app, 80 | store: context.store, 81 | route: (context.to ? context.to : context.route), 82 | payload: context.payload, 83 | error: context.error, 84 | base: '/', 85 | env: {} 86 | } 87 | const next = context.next 88 | ctx.params = ctx.route.params || {} 89 | ctx.query = ctx.route.query || {} 90 | ctx.redirect = function (status, path, query) { 91 | if (!status) return 92 | ctx._redirected = true // Used in middleware 93 | // if only 1 or 2 arguments: redirect('/') or redirect('/', { foo: 'bar' }) 94 | if (typeof status === 'string' && (typeof path === 'undefined' || typeof path === 'object')) { 95 | query = path || {} 96 | path = status 97 | status = 302 98 | } 99 | next({ 100 | path: path, 101 | query: query, 102 | status: status 103 | }) 104 | } 105 | if (context.req) ctx.req = context.req 106 | if (context.res) ctx.res = context.res 107 | if (context.from) ctx.from = context.from 108 | if (ctx.isServer && context.beforeRenderFns) { 109 | ctx.beforeNuxtRender = (fn) => context.beforeRenderFns.push(fn) 110 | } 111 | if (ctx.isClient && window.__NUXT__) { 112 | ctx.serverState = window.__NUXT__ 113 | } 114 | return ctx 115 | } 116 | 117 | export function middlewareSeries (promises, context) { 118 | if (!promises.length || context._redirected) { 119 | return Promise.resolve() 120 | } 121 | return promisify(promises[0], context) 122 | .then(() => { 123 | return middlewareSeries(promises.slice(1), context) 124 | }) 125 | } 126 | 127 | export function promisify (fn, context) { 128 | let promise 129 | if (fn.length === 2) { 130 | // fn(context, callback) 131 | promise = new Promise((resolve) => { 132 | fn(context, function (err, data) { 133 | if (err) { 134 | context.error(err) 135 | } 136 | data = data || {} 137 | resolve(data) 138 | }) 139 | }) 140 | } else { 141 | promise = fn(context) 142 | } 143 | if (!promise || (!(promise instanceof Promise) && (typeof promise.then !== 'function'))) { 144 | promise = Promise.resolve(promise) 145 | } 146 | return promise 147 | } 148 | 149 | // Imported from vue-router 150 | export function getLocation (base, mode) { 151 | var path = window.location.pathname 152 | if (mode === 'hash') { 153 | return window.location.hash.replace(/^#\//, '') 154 | } 155 | if (base && path.indexOf(base) === 0) { 156 | path = path.slice(base.length) 157 | } 158 | return (path || '/') + window.location.search + window.location.hash 159 | } 160 | 161 | export function urlJoin () { 162 | return [].slice.call(arguments).join('/').replace(/\/+/g, '/') 163 | } 164 | 165 | // Imported from path-to-regexp 166 | 167 | /** 168 | * Compile a string to a template function for the path. 169 | * 170 | * @param {string} str 171 | * @param {Object=} options 172 | * @return {!function(Object=, Object=)} 173 | */ 174 | export function compile (str, options) { 175 | return tokensToFunction(parse(str, options)) 176 | } 177 | 178 | /** 179 | * The main path matching regexp utility. 180 | * 181 | * @type {RegExp} 182 | */ 183 | const PATH_REGEXP = new RegExp([ 184 | // Match escaped characters that would otherwise appear in future matches. 185 | // This allows the user to escape special characters that won't transform. 186 | '(\\\\.)', 187 | // Match Express-style parameters and un-named parameters with a prefix 188 | // and optional suffixes. Matches appear as: 189 | // 190 | // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined] 191 | // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined] 192 | // "/*" => ["/", undefined, undefined, undefined, undefined, "*"] 193 | '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))' 194 | ].join('|'), 'g') 195 | 196 | /** 197 | * Parse a string for the raw tokens. 198 | * 199 | * @param {string} str 200 | * @param {Object=} options 201 | * @return {!Array} 202 | */ 203 | function parse (str, options) { 204 | var tokens = [] 205 | var key = 0 206 | var index = 0 207 | var path = '' 208 | var defaultDelimiter = options && options.delimiter || '/' 209 | var res 210 | 211 | while ((res = PATH_REGEXP.exec(str)) != null) { 212 | var m = res[0] 213 | var escaped = res[1] 214 | var offset = res.index 215 | path += str.slice(index, offset) 216 | index = offset + m.length 217 | 218 | // Ignore already escaped sequences. 219 | if (escaped) { 220 | path += escaped[1] 221 | continue 222 | } 223 | 224 | var next = str[index] 225 | var prefix = res[2] 226 | var name = res[3] 227 | var capture = res[4] 228 | var group = res[5] 229 | var modifier = res[6] 230 | var asterisk = res[7] 231 | 232 | // Push the current path onto the tokens. 233 | if (path) { 234 | tokens.push(path) 235 | path = '' 236 | } 237 | 238 | var partial = prefix != null && next != null && next !== prefix 239 | var repeat = modifier === '+' || modifier === '*' 240 | var optional = modifier === '?' || modifier === '*' 241 | var delimiter = res[2] || defaultDelimiter 242 | var pattern = capture || group 243 | 244 | tokens.push({ 245 | name: name || key++, 246 | prefix: prefix || '', 247 | delimiter: delimiter, 248 | optional: optional, 249 | repeat: repeat, 250 | partial: partial, 251 | asterisk: !!asterisk, 252 | pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?') 253 | }) 254 | } 255 | 256 | // Match any characters still remaining. 257 | if (index < str.length) { 258 | path += str.substr(index) 259 | } 260 | 261 | // If the path exists, push it onto the end. 262 | if (path) { 263 | tokens.push(path) 264 | } 265 | 266 | return tokens 267 | } 268 | 269 | /** 270 | * Prettier encoding of URI path segments. 271 | * 272 | * @param {string} 273 | * @return {string} 274 | */ 275 | function encodeURIComponentPretty (str) { 276 | return encodeURI(str).replace(/[\/?#]/g, function (c) { 277 | return '%' + c.charCodeAt(0).toString(16).toUpperCase() 278 | }) 279 | } 280 | 281 | /** 282 | * Encode the asterisk parameter. Similar to `pretty`, but allows slashes. 283 | * 284 | * @param {string} 285 | * @return {string} 286 | */ 287 | function encodeAsterisk (str) { 288 | return encodeURI(str).replace(/[?#]/g, function (c) { 289 | return '%' + c.charCodeAt(0).toString(16).toUpperCase() 290 | }) 291 | } 292 | 293 | /** 294 | * Expose a method for transforming tokens into the path function. 295 | */ 296 | function tokensToFunction (tokens) { 297 | // Compile all the tokens into regexps. 298 | var matches = new Array(tokens.length) 299 | 300 | // Compile all the patterns before compilation. 301 | for (var i = 0; i < tokens.length; i++) { 302 | if (typeof tokens[i] === 'object') { 303 | matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$') 304 | } 305 | } 306 | 307 | return function (obj, opts) { 308 | var path = '' 309 | var data = obj || {} 310 | var options = opts || {} 311 | var encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent 312 | 313 | for (var i = 0; i < tokens.length; i++) { 314 | var token = tokens[i] 315 | 316 | if (typeof token === 'string') { 317 | path += token 318 | 319 | continue 320 | } 321 | 322 | var value = data[token.name] 323 | var segment 324 | 325 | if (value == null) { 326 | if (token.optional) { 327 | // Prepend partial segment prefixes. 328 | if (token.partial) { 329 | path += token.prefix 330 | } 331 | 332 | continue 333 | } else { 334 | throw new TypeError('Expected "' + token.name + '" to be defined') 335 | } 336 | } 337 | 338 | if (Array.isArray(value)) { 339 | if (!token.repeat) { 340 | throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`') 341 | } 342 | 343 | if (value.length === 0) { 344 | if (token.optional) { 345 | continue 346 | } else { 347 | throw new TypeError('Expected "' + token.name + '" to not be empty') 348 | } 349 | } 350 | 351 | for (var j = 0; j < value.length; j++) { 352 | segment = encode(value[j]) 353 | 354 | if (!matches[i].test(segment)) { 355 | throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`') 356 | } 357 | 358 | path += (j === 0 ? token.prefix : token.delimiter) + segment 359 | } 360 | 361 | continue 362 | } 363 | 364 | segment = token.asterisk ? encodeAsterisk(value) : encode(value) 365 | 366 | if (!matches[i].test(segment)) { 367 | throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"') 368 | } 369 | 370 | path += token.prefix + segment 371 | } 372 | 373 | return path 374 | } 375 | } 376 | 377 | /** 378 | * Escape a regular expression string. 379 | * 380 | * @param {string} str 381 | * @return {string} 382 | */ 383 | function escapeString (str) { 384 | return str.replace(/([.+*?=^!:()[\]|\/\\])/g, '\\$1') 385 | } 386 | 387 | /** 388 | * Escape the capturing group by escaping special characters and meaning. 389 | * 390 | * @param {string} group 391 | * @return {string} 392 | */ 393 | function escapeGroup (group) { 394 | return group.replace(/([=!:$\/()])/g, '\\$1') 395 | } 396 | -------------------------------------------------------------------------------- /.nuxt/views/app.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ HEAD }} 5 | 6 | 7 | {{ APP }} 8 | 9 | 10 | -------------------------------------------------------------------------------- /.nuxt/views/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Server error 5 | 6 | 7 | 10 | 11 | 12 |
13 |
14 | 15 |
Server error
16 |
{{ message }}
17 |
18 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sarah Drasner 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft CDA Speaking Info and Locations 2 | 3 | Small demo showing where and when all of the Microsoft Cloud Developer Advocates are speaking. Using Serverless functions to update geolocation info, Nuxt for server-side rendering, and Vue.js computed properties for really fast list filtering. 4 | 5 | Live demo at [http://cda-locale.azurewebsites.net/](http://cda-locale.azurewebsites.net/) 6 | 7 | Article series explaining the demo at [https://css-tricks.com/exploring-data-with-serverless-and-vue-part-i](https://css-tricks.com/exploring-data-with-serverless-and-vue-part-i) (this is a two part series) 8 | 9 | ![Microsoft CDA locations transitions screenshot](https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/cda-shot.jpg "Microsoft CDA Locations") 10 | 11 | ## Build Setup 12 | 13 | ``` bash 14 | # install dependencies 15 | $ yarn 16 | 17 | # serve with hot reload at localhost:3000 18 | $ npm run dev 19 | 20 | # build for production and launch server 21 | $ npm run build 22 | $ npm start 23 | 24 | # generate static project 25 | $ npm run generate 26 | ``` 27 | 28 | 29 | -------------------------------------------------------------------------------- /assets/cda-data.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "Abel Wang", 4 | "Conference": "Visual Studio Live", 5 | "From": "3/13/2017", 6 | "To": "3/17/2017", 7 | "Location": "Las Vegas", 8 | "Link": "https://vslive.com/Home.aspx", 9 | "Latitude": 36.1699412, 10 | "Longitude": -115.1398296 11 | }, 12 | { 13 | "Name": "Abel Wang", 14 | "Conference": "Tech Summit 2017 Amsterdam", 15 | "From": "3/20/2017", 16 | "To": "3/21/2017", 17 | "Location": "Amsterdam", 18 | "Link": "https://www.microsoft.com/en-us/techsummit/default.aspx", 19 | "Latitude": 52.3702157, 20 | "Longitude": 4.895167900000001 21 | }, 22 | { 23 | "Name": "Abel Wang", 24 | "Conference": "Tech Summit 2017 Milan", 25 | "From": "3/20/2017", 26 | "To": "3/21/2017", 27 | "Location": "Milan", 28 | "Link": "https://www.microsoft.com/en-us/techsummit/default.aspx", 29 | "Latitude": 45.4642035, 30 | "Longitude": 9.189982 31 | }, 32 | { 33 | "Name": "Abel Wang", 34 | "Conference": "TechSummit 2017 Seoul", 35 | "From": "4/26/2017", 36 | "To": "4/27/2017", 37 | "Location": "Seoul", 38 | "Link": "https://www.microsoft.com/en-us/techsummit/default.aspx", 39 | "Latitude": 37.566535, 40 | "Longitude": 126.9779692 41 | }, 42 | { 43 | "Name": "Abel Wang", 44 | "Conference": "Velocity", 45 | "From": "6/19/2017", 46 | "To": "6/21/2017", 47 | "Location": "San Jose", 48 | "Link": "https://conferences.oreilly.com/velocity/vl-ca", 49 | "Latitude": 37.3382082, 50 | "Longitude": -121.8863286 51 | }, 52 | { 53 | "Name": "Abel Wang", 54 | "Conference": "VSLive Redmond", 55 | "From": "8/15/2017", 56 | "To": "8/16/2017", 57 | "Location": "Redmond", 58 | "Link": "https://vslive.com/Home.aspx", 59 | "Latitude": 47.6739881, 60 | "Longitude": -122.121512 61 | }, 62 | { 63 | "Name": "Abel Wang", 64 | "Conference": "Jenkins World", 65 | "From": "8/29/2017", 66 | "To": "8/31/2017", 67 | "Location": "San Francisco", 68 | "Link": "https://www.cloudbees.com/jenkinsworld/home", 69 | "Latitude": 37.7749295, 70 | "Longitude": -122.4194155 71 | }, 72 | { 73 | "Name": "Abel Wang", 74 | "Conference": "Minneapolis Azure Conference", 75 | "From": "9/7/2017", 76 | "To": "9/7/2017", 77 | "Location": "Minneapolis", 78 | "Link": "", 79 | "Latitude": 44.977753, 80 | "Longitude": -93.2650108 81 | }, 82 | { 83 | "Name": "Abel Wang", 84 | "Conference": "Visual Studio Partner Summit", 85 | "From": "9/20/2017", 86 | "To": "9/21/2017", 87 | "Location": "Redmond", 88 | "Link": "https://vspartner.com/external/summit2017", 89 | "Latitude": 47.6739881, 90 | "Longitude": -122.121512 91 | }, 92 | { 93 | "Name": "Abel Wang", 94 | "Conference": "MSIgnite 2017", 95 | "From": "9/24/2017", 96 | "To": "9/29/2017", 97 | "Location": "Orlando, FL", 98 | "Link": "https://www.microsoft.com/en-us/ignite/default.aspx", 99 | "Latitude": 28.5383355, 100 | "Longitude": -81.3792365 101 | }, 102 | { 103 | "Name": "Abel Wang", 104 | "Conference": "Minnesota Developer Conference", 105 | "From": "10/2/2017", 106 | "To": "10/2/2017", 107 | "Location": "Minneapolis", 108 | "Link": "http://mdc.ilmservice.com/", 109 | "Latitude": 44.977753, 110 | "Longitude": -93.2650108 111 | }, 112 | { 113 | "Name": "Abel Wang", 114 | "Conference": "AppConn - Budapest", 115 | "From": "10/10/2017", 116 | "To": "10/11/2017", 117 | "Location": "Budapest", 118 | "Link": "", 119 | "Latitude": 47.497912, 120 | "Longitude": 19.040235 121 | }, 122 | { 123 | "Name": "Abel Wang", 124 | "Conference": "VS Live -Anaheim", 125 | "From": "10/16/2017", 126 | "To": "10/17/2017", 127 | "Location": "Anaheim", 128 | "Link": "https://vslive.com/Home.aspx", 129 | "Latitude": 33.8365932, 130 | "Longitude": -117.9143012 131 | }, 132 | { 133 | "Name": "Abel Wang", 134 | "Conference": "AppConn - Houston", 135 | "From": "11/1/2017", 136 | "To": "11/2/2017", 137 | "Location": "Houston, TX", 138 | "Link": "", 139 | "Latitude": 29.7604267, 140 | "Longitude": -95.3698028 141 | }, 142 | { 143 | "Name": "Abel Wang", 144 | "Conference": "AppConn - Thailand", 145 | "From": "11/9/2017", 146 | "To": "11/10/2017", 147 | "Location": "Bangkok", 148 | "Link": "", 149 | "Latitude": 13.7563309, 150 | "Longitude": 100.5017651 151 | }, 152 | { 153 | "Name": "Abel Wang", 154 | "Conference": "MS Connect", 155 | "From": "11/15/2017", 156 | "To": "11/17/2017", 157 | "Location": "NYC", 158 | "Link": "https://www.microsoft.com/en-us/connectevent", 159 | "Latitude": 40.7127753, 160 | "Longitude": -74.0059728 161 | }, 162 | { 163 | "Name": "Abel Wang", 164 | "Conference": "Tech Summit 2017 Amsterdam", 165 | "From": "3/20/2017", 166 | "To": "3/21/2017", 167 | "Location": "Amsterdam", 168 | "Link": "https://www.microsoft.com/en-us/techsummit/default.aspx", 169 | "Latitude": 52.3702157, 170 | "Longitude": 4.895167900000001 171 | }, 172 | { 173 | "Name": "Abel Wang", 174 | "Conference": "Tech Summit 2017 Milan", 175 | "From": "3/20/2017", 176 | "To": "3/21/2017", 177 | "Location": "Milan", 178 | "Link": "https://www.microsoft.com/en-us/techsummit/default.aspx", 179 | "Latitude": 45.4642035, 180 | "Longitude": 9.189982 181 | }, 182 | { 183 | "Name": "Abel Wang", 184 | "Conference": "TechSummit 2017 Seoul", 185 | "From": "4/26/2017", 186 | "To": "4/27/2017", 187 | "Location": "Seoul", 188 | "Link": "https://www.microsoft.com/en-us/techsummit/default.aspx", 189 | "Latitude": 37.566535, 190 | "Longitude": 126.9779692 191 | }, 192 | { 193 | "Name": "Abel Wang", 194 | "Conference": "MSIgnite 2017", 195 | "From": "9/24/2017", 196 | "To": "9/29/2017", 197 | "Location": "Orlando, FL", 198 | "Link": "https://www.microsoft.com/en-us/ignite/default.aspx", 199 | "Latitude": 28.5383355, 200 | "Longitude": -81.3792365 201 | }, 202 | { 203 | "Name": "Alena Hall", 204 | "Conference": "OSCON", 205 | "From": "5/8/2017", 206 | "To": "5/11/2017", 207 | "Location": "Austin, TX", 208 | "Link": "https://conferences.oreilly.com/oscon/oscon-tx/public/schedule/detail/56877", 209 | "Latitude": 30.267153, 210 | "Longitude": -97.7430608 211 | }, 212 | { 213 | "Name": "Alena Hall", 214 | "Conference": "Seattle Scalability", 215 | "From": "6/21/2017", 216 | "To": "6/21/2017", 217 | "Location": "Seattle, WA", 218 | "Link": "https://www.meetup.com/Seattle-Scalability-Meetup/events/239225314/", 219 | "Latitude": 47.6062095, 220 | "Longitude": -122.3320708 221 | }, 222 | { 223 | "Name": "Alena Hall", 224 | "Conference": "Open FSharp", 225 | "From": "9/28/2017", 226 | "To": "9/30/2017", 227 | "Location": "San Francisco, CA", 228 | "Link": "http://www.openfsharp.org/schedule.html", 229 | "Latitude": 37.7749295, 230 | "Longitude": -122.4194155 231 | }, 232 | { 233 | "Name": "Anthony Chu", 234 | "Conference": "SQL Saturday Vancouver", 235 | "From": "8/25/2017", 236 | "To": "8/26/2017", 237 | "Location": "Vancouver", 238 | "Link": "http://www.sqlsaturday.com/635/", 239 | "Latitude": 49.2827291, 240 | "Longitude": -123.1207375 241 | }, 242 | { 243 | "Name": "Anthony Chu", 244 | "Conference": "Seattle Code Camp", 245 | "From": "9/8/2017", 246 | "To": "9/9/2017", 247 | "Location": "Seattle", 248 | "Link": "https://seattle.codecamp.us/", 249 | "Latitude": 47.6062095, 250 | "Longitude": -122.3320708 251 | }, 252 | { 253 | "Name": "Ashley McNamara", 254 | "Conference": "CraftConf", 255 | "From": "4/27/2017", 256 | "To": "3/31/2017", 257 | "Location": "Budapest", 258 | "Link": "", 259 | "Latitude": 47.497912, 260 | "Longitude": 19.040235 261 | }, 262 | { 263 | "Name": "Ashley McNamara", 264 | "Conference": "OSCON", 265 | "From": "5/8/2017", 266 | "To": "5/11/2017", 267 | "Location": "Austin", 268 | "Link": "", 269 | "Latitude": 30.267153, 270 | "Longitude": -97.7430608 271 | }, 272 | { 273 | "Name": "Ashley McNamara", 274 | "Conference": "SDxE", 275 | "From": "9/25/2017", 276 | "To": "9/27/2017", 277 | "Location": "Austin", 278 | "Link": "", 279 | "Latitude": 30.267153, 280 | "Longitude": -97.7430608 281 | }, 282 | { 283 | "Name": "Ashley McNamara", 284 | "Conference": "Dockercon", 285 | "From": "10/16/2017", 286 | "To": "10/19/2017", 287 | "Location": "Copenhagen", 288 | "Link": "", 289 | "Latitude": 55.6760968, 290 | "Longitude": 12.5683372 291 | }, 292 | { 293 | "Name": "Ashley McNamara", 294 | "Conference": "GALS CH9", 295 | "From": "10/24/2017", 296 | "To": "10/24/2017", 297 | "Location": "Seattle", 298 | "Link": "", 299 | "Latitude": 47.6062095, 300 | "Longitude": -122.3320708 301 | }, 302 | { 303 | "Name": "Ashley McNamara", 304 | "Conference": "OpenDev", 305 | "From": "10/25/2017", 306 | "To": "10/25/2017", 307 | "Location": "Seattle", 308 | "Link": "", 309 | "Latitude": 47.6062095, 310 | "Longitude": -122.3320708 311 | }, 312 | { 313 | "Name": "Ashley McNamara", 314 | "Conference": "Gophercon Brazil", 315 | "From": "11/13/2017", 316 | "To": "11/17/2017", 317 | "Location": "Brazil", 318 | "Link": "", 319 | "Latitude": -14.235004, 320 | "Longitude": -51.92528 321 | }, 322 | { 323 | "Name": "Ashley McNamara", 324 | "Conference": "Gophercon", 325 | "From": "7/13/2017", 326 | "To": "7/15/2017", 327 | "Location": "Denver", 328 | "Link": "", 329 | "Latitude": 39.7392358, 330 | "Longitude": -104.990251 331 | }, 332 | { 333 | "Name": "Ashley McNamara", 334 | "Conference": "Gophercon UK", 335 | "From": "8/16/2017", 336 | "To": "8/18/2017", 337 | "Location": "London", 338 | "Link": "", 339 | "Latitude": 51.5073509, 340 | "Longitude": -0.1277583 341 | }, 342 | { 343 | "Name": "Ashley McNamara", 344 | "Conference": "DevOps Days Raleigh", 345 | "From": "9/7/2017", 346 | "To": "9/8/2017", 347 | "Location": "Raleigh", 348 | "Link": "", 349 | "Latitude": 35.7795897, 350 | "Longitude": -78.6381787 351 | }, 352 | { 353 | "Name": "Ashley McNamara", 354 | "Conference": "vJUG24", 355 | "From": "10/25/2017", 356 | "To": "10/25/2017", 357 | "Location": "Austin", 358 | "Link": "", 359 | "Latitude": 30.267153, 360 | "Longitude": -97.7430608 361 | }, 362 | { 363 | "Name": "Asim Hussain", 364 | "Conference": "NG-NL", 365 | "From": "3/16/2017", 366 | "To": "3/16/2017", 367 | "Location": "Amsterdam", 368 | "Link": "https://ng-nl.org/", 369 | "Latitude": 52.3702157, 370 | "Longitude": 4.895167900000001 371 | }, 372 | { 373 | "Name": "Asim Hussain", 374 | "Conference": "Ng Conf", 375 | "From": "4/18/2017", 376 | "To": "4/20/2017", 377 | "Location": "Salt Lake City", 378 | "Link": "https://www.ng-conf.org/", 379 | "Latitude": 40.7607793, 380 | "Longitude": -111.8910474 381 | }, 382 | { 383 | "Name": "Asim Hussain", 384 | "Conference": "Angular Camp", 385 | "From": "6/7/2017", 386 | "To": "7/7/2017", 387 | "Location": "Barcelona", 388 | "Link": "https://2017.angularcamp.org/", 389 | "Latitude": 41.3850639, 390 | "Longitude": 2.1734035 391 | }, 392 | { 393 | "Name": "Asim Hussain", 394 | "Conference": "Angular UP", 395 | "From": "7/25/2017", 396 | "To": "7/25/2017", 397 | "Location": "Tel Aviv", 398 | "Link": "http://angular-up.com/", 399 | "Latitude": 32.0852999, 400 | "Longitude": 34.78176759999999 401 | }, 402 | { 403 | "Name": "Asim Hussain", 404 | "Conference": "Lviv JS", 405 | "From": "8/26/2017", 406 | "To": "8/26/2017", 407 | "Location": "Lviv", 408 | "Link": "http://lvivjs.org.ua/", 409 | "Latitude": 49.839683, 410 | "Longitude": 24.029717 411 | }, 412 | { 413 | "Name": "Asim Hussain", 414 | "Conference": "FrontEnd Con", 415 | "From": "9/20/2017", 416 | "To": "9/22/2017", 417 | "Location": "Warsaw", 418 | "Link": "http://frontend-con.io/", 419 | "Latitude": 52.2296756, 420 | "Longitude": 21.0122287 421 | }, 422 | { 423 | "Name": "Asim Hussain", 424 | "Conference": "DefFest Nantes", 425 | "From": "10/18/2017", 426 | "To": "10/20/2017", 427 | "Location": "Nantes", 428 | "Link": "https://devfest.gdgnantes.com/", 429 | "Latitude": 47.218371, 430 | "Longitude": -1.553621 431 | }, 432 | { 433 | "Name": "Asim Hussain", 434 | "Conference": "KharkivJS", 435 | "From": "10/28/2017", 436 | "To": "10/29/2017", 437 | "Location": "Kharkiv", 438 | "Link": "http://kharkivjs.org/", 439 | "Latitude": 49.9935, 440 | "Longitude": 36.230383 441 | }, 442 | { 443 | "Name": "Asim Hussain", 444 | "Conference": "Angular Connect", 445 | "From": "11/6/2017", 446 | "To": "11/8/2017", 447 | "Location": "London", 448 | "Link": "https://www.angularconnect.com/", 449 | "Latitude": 51.5073509, 450 | "Longitude": -0.1277583 451 | }, 452 | { 453 | "Name": "Asim Hussain", 454 | "Conference": "PyCon Canada", 455 | "From": "11/18/2017", 456 | "To": "11/19/2017", 457 | "Location": "Montreal", 458 | "Link": "https://2017.pycon.ca/", 459 | "Latitude": 45.5016889, 460 | "Longitude": -73.567256 461 | }, 462 | { 463 | "Name": "Asim Hussain", 464 | "Conference": "FinJS", 465 | "From": "11/27/2017", 466 | "To": "11/28/2017", 467 | "Location": "London", 468 | "Link": "http://finjs.io/", 469 | "Latitude": 51.5073509, 470 | "Longitude": -0.1277583 471 | }, 472 | { 473 | "Name": "Asim Hussain", 474 | "Conference": "Code Europe", 475 | "From": "12/4/2017", 476 | "To": "12/6/2017", 477 | "Location": "Krakow", 478 | "Link": "https://www.codeeurope.pl/", 479 | "Latitude": 50.06465009999999, 480 | "Longitude": 19.9449799 481 | }, 482 | { 483 | "Name": "Asim Hussain", 484 | "Conference": "ng Belgium", 485 | "From": "12/8/2017", 486 | "To": "12/8/2017", 487 | "Location": "Belgium", 488 | "Link": "https://ng-be.org", 489 | "Latitude": 50.503887, 490 | "Longitude": 4.469936 491 | }, 492 | { 493 | "Name": "Brian Benz", 494 | "Conference": "JavaOne San Francisco", 495 | "From": "9/28/2017", 496 | "To": "10/5/2017", 497 | "Location": "San Francisco", 498 | "Link": "https://www.pluralsight.com/event-details/2017/live-2017", 499 | "Latitude": 37.7749295, 500 | "Longitude": -122.4194155 501 | }, 502 | { 503 | "Name": "Brian Benz", 504 | "Conference": "JavaOne Partner evening event", 505 | "From": "10/1/2017", 506 | "To": "10/3/2017", 507 | "Location": "San Francisco", 508 | "Link": "https://vslive.com/events/redmond-2017", 509 | "Latitude": 37.7749295, 510 | "Longitude": -122.4194155 511 | }, 512 | { 513 | "Name": "Brian Benz", 514 | "Conference": "EclipseCon EU", 515 | "From": "10/23/2017", 516 | "To": "10/26/2017", 517 | "Location": "Ludwigdburg", 518 | "Link": "https://www.golanguk.com/", 519 | "Latitude": 48.8940624, 520 | "Longitude": 9.195464 521 | }, 522 | { 523 | "Name": "Brian Benz", 524 | "Conference": "Belfast Java User Group", 525 | "From": "10/29/2017", 526 | "To": "10/30/2017", 527 | "Location": "Belfast", 528 | "Link": "https://uptime.events/", 529 | "Latitude": 54.59728500000001, 530 | "Longitude": -5.93012 531 | }, 532 | { 533 | "Name": "Brian Benz", 534 | "Conference": "Dublin Java User Group", 535 | "From": "10/30/2017", 536 | "To": "10/31/2017", 537 | "Location": "Dublin", 538 | "Link": "https://onlinexperiences.com/scripts/Server.nxp?LASCmd=AI:4;F:QS!10100&ShowKey=42620", 539 | "Latitude": 53.3498053, 540 | "Longitude": -6.2603097 541 | }, 542 | { 543 | "Name": "Brian Benz", 544 | "Conference": "JFall Utrecht", 545 | "From": "10/31/2017", 546 | "To": "11/2/2017", 547 | "Location": "Utrecht", 548 | "Link": "https://ng-atl.org", 549 | "Latitude": 52.09073739999999, 550 | "Longitude": 5.1214201 551 | }, 552 | { 553 | "Name": "Brian Benz", 554 | "Conference": "Oredev", 555 | "From": "11/5/2017", 556 | "To": "11/10/2017", 557 | "Location": "Malmo", 558 | "Link": "lvivjs.org.ua", 559 | "Latitude": 55.604981, 560 | "Longitude": 13.003822 561 | }, 562 | { 563 | "Name": "Brian Benz", 564 | "Conference": "Devoxx Morocco", 565 | "From": "11/13/2017", 566 | "To": "11/16/2017", 567 | "Location": "Morocco", 568 | "Link": "", 569 | "Latitude": 31.791702, 570 | "Longitude": -7.092619999999999 571 | }, 572 | { 573 | "Name": "Brian Benz", 574 | "Conference": "SpringOne San Francisco", 575 | "From": "11/27/2017", 576 | "To": "12/7/2017", 577 | "Location": "San Francisco", 578 | "Link": "https://yougottalovefrontend.com/", 579 | "Latitude": 37.7749295, 580 | "Longitude": -122.4194155 581 | }, 582 | { 583 | "Name": "Brian Clark", 584 | "Conference": "ngAtl", 585 | "From": "1/30/2018", 586 | "To": "2/2/2018", 587 | "Location": "Atlanta", 588 | "Link": "https://www.cloudbees.com/jenkinsworld/home", 589 | "Latitude": 33.7489954, 590 | "Longitude": -84.3879824 591 | }, 592 | { 593 | "Name": "Brian Clark", 594 | "Conference": "Ignite", 595 | "From": "9/22/2017", 596 | "To": "9/29/2017", 597 | "Location": "Orlando", 598 | "Link": "http://angularconnect.com/", 599 | "Latitude": 28.5383355, 600 | "Longitude": -81.3792365 601 | }, 602 | { 603 | "Name": "Brian Clark", 604 | "Conference": "Node.js Interactive", 605 | "From": "10/3/2017", 606 | "To": "10/6/2017", 607 | "Location": "Vancouver", 608 | "Link": "https://www.microsoft.com/en-us/ignite/", 609 | "Latitude": 49.2827291, 610 | "Longitude": -123.1207375 611 | }, 612 | { 613 | "Name": "Brian Clark", 614 | "Conference": "AngularMix", 615 | "From": "10/9/2017", 616 | "To": "10/12/2017", 617 | "Location": "Orlando", 618 | "Link": "\"Serverless tour\"", 619 | "Latitude": 28.5383355, 620 | "Longitude": -81.3792365 621 | }, 622 | { 623 | "Name": "Brian Clark", 624 | "Conference": "AngleBrackets", 625 | "From": "4/18/2017", 626 | "To": "5/24/2017", 627 | "Location": "Orlando", 628 | "Link": "", 629 | "Latitude": 28.5383355, 630 | "Longitude": -81.3792365 631 | }, 632 | { 633 | "Name": "Brian Clark", 634 | "Conference": "AngleBrackets", 635 | "From": "10/28/2017", 636 | "To": "10/31/2017", 637 | "Location": "Las Vegas", 638 | "Link": "https://2017.fullstackfest.com/", 639 | "Latitude": 36.1699412, 640 | "Longitude": -115.1398296 641 | }, 642 | { 643 | "Name": "Brian Clark", 644 | "Conference": "Nodevember", 645 | "From": "11/17/2017", 646 | "To": "11/28/2017", 647 | "Location": "Nashville", 648 | "Link": "", 649 | "Latitude": 36.1626638, 650 | "Longitude": -86.7816016 651 | }, 652 | { 653 | "Name": "Brian Clark", 654 | "Conference": "360 Live", 655 | "From": "11/12/2017", 656 | "To": "11/17/2017", 657 | "Location": "Orlando", 658 | "Link": "", 659 | "Latitude": 28.5383355, 660 | "Longitude": -81.3792365 661 | }, 662 | { 663 | "Name": "Brian Ketelsen", 664 | "Conference": "dotGo", 665 | "From": "11/6/2017", 666 | "To": "11/6/2017", 667 | "Location": "London", 668 | "Link": "https://eventloom.com/event/home/summit2017", 669 | "Latitude": 51.5073509, 670 | "Longitude": -0.1277583 671 | }, 672 | { 673 | "Name": "Brian Ketelsen", 674 | "Conference": "CodeMotion", 675 | "From": "11/10/2017", 676 | "To": "11/11/2017", 677 | "Location": "Milan", 678 | "Link": "", 679 | "Latitude": 45.4642035, 680 | "Longitude": 9.189982 681 | }, 682 | { 683 | "Name": "Bridget Kromhout", 684 | "Conference": "SREcon Americas", 685 | "From": "3/13/2017", 686 | "To": "3/14/2017", 687 | "Location": "San Francisco", 688 | "Link": "http://devupconf.org/", 689 | "Latitude": 37.7749295, 690 | "Longitude": -122.4194155 691 | }, 692 | { 693 | "Name": "Bridget Kromhout", 694 | "Conference": "Agile Technical Conference", 695 | "From": "4/19/2017", 696 | "To": "4/21/2017", 697 | "Location": "Boston, MA", 698 | "Link": "http://visfest.com/d3unconf-2017/", 699 | "Latitude": 42.3600825, 700 | "Longitude": -71.0588801 701 | }, 702 | { 703 | "Name": "Bridget Kromhout", 704 | "Conference": "GOTO Chicago", 705 | "From": "5/1/2017", 706 | "To": "5/4/2017", 707 | "Location": "Chicago", 708 | "Link": "https://www.devopsdays.org/events/2017-raleigh/welcome/", 709 | "Latitude": 41.8781136, 710 | "Longitude": -87.6297982 711 | }, 712 | { 713 | "Name": "Bridget Kromhout", 714 | "Conference": "Open South North", 715 | "From": "6/8/2017", 716 | "To": "6/8/2017", 717 | "Location": "Minneapolis", 718 | "Link": "", 719 | "Latitude": 44.977753, 720 | "Longitude": -93.2650108 721 | }, 722 | { 723 | "Name": "Bridget Kromhout", 724 | "Conference": "Cloud Foundry Summit", 725 | "From": "6/13/2017", 726 | "To": "6/15/2017", 727 | "Location": "Santa Clara", 728 | "Link": "", 729 | "Latitude": 37.3541079, 730 | "Longitude": -121.9552356 731 | }, 732 | { 733 | "Name": "Bridget Kromhout", 734 | "Conference": "Velocity San Jose", 735 | "From": "6/19/2017", 736 | "To": "6/22/2017", 737 | "Location": "San Jose", 738 | "Link": "", 739 | "Latitude": 37.3382082, 740 | "Longitude": -121.8863286 741 | }, 742 | { 743 | "Name": "Bridget Kromhout", 744 | "Conference": "Uptime - Ops and Systems Programming Conference", 745 | "From": "8/24/2017", 746 | "To": "8/25/2017", 747 | "Location": "Pittsburgh", 748 | "Link": "http://events.linuxfoundation.org/events/open-source-summit-north-america", 749 | "Latitude": 40.44062479999999, 750 | "Longitude": -79.9958864 751 | }, 752 | { 753 | "Name": "Bridget Kromhout", 754 | "Conference": "devopsdays Philadelphia", 755 | "From": "10/24/2017", 756 | "To": "10/25/2017", 757 | "Location": "Philadelphia", 758 | "Link": "https://blog.jeremylikness.com/atlanta-code-camp-2017-serverless-net-da640edd59e9", 759 | "Latitude": 39.9525839, 760 | "Longitude": -75.1652215 761 | }, 762 | { 763 | "Name": "Bridget Kromhout", 764 | "Conference": "O'Reilly Software Architecture", 765 | "From": "4/2/2017", 766 | "To": "4/5/2017", 767 | "Location": "New York City, NY", 768 | "Link": "http://finjs.io/", 769 | "Latitude": 40.7127753, 770 | "Longitude": -74.0059728 771 | }, 772 | { 773 | "Name": "Bridget Kromhout", 774 | "Conference": "All Things Open", 775 | "From": "10/23/2017", 776 | "To": "10/24/2017", 777 | "Location": "Raleigh", 778 | "Link": "https://allthingsopen.org/", 779 | "Latitude": 35.7795897, 780 | "Longitude": -78.6381787 781 | }, 782 | { 783 | "Name": "Burke Holland", 784 | "Conference": "ConnectJS", 785 | "From": "9/19/2017", 786 | "To": "9/22/2017", 787 | "Location": "Atlanta", 788 | "Link": "http://connect.tech", 789 | "Latitude": 33.7489954, 790 | "Longitude": -84.3879824 791 | }, 792 | { 793 | "Name": "Burke Holland", 794 | "Conference": "The Web Unleashed", 795 | "From": "9/24/2017", 796 | "To": "9/26/2017", 797 | "Location": "Toronto, Canada", 798 | "Link": "http://fitc.ca/event/webu17/", 799 | "Latitude": 43.653226, 800 | "Longitude": -79.3831843 801 | }, 802 | { 803 | "Name": "Burke Holland", 804 | "Conference": "DevUp", 805 | "From": "10/13/2017", 806 | "To": "10/16/2017", 807 | "Location": "St. Louis", 808 | "Link": "http://devupconf.org/", 809 | "Latitude": 38.6270025, 810 | "Longitude": -90.19940419999999 811 | }, 812 | { 813 | "Name": "Burke Holland", 814 | "Conference": "Certified Fresh", 815 | "From": "10/19/2017", 816 | "To": "10/20/2017", 817 | "Location": "Online", 818 | "Link": "https://certifiedfreshevents.com/", 819 | "Latitude": 36.146636, 820 | "Longitude": -95.90194 821 | }, 822 | { 823 | "Name": "Burke Holland", 824 | "Conference": "Dev Intersection", 825 | "From": "10/31/2017", 826 | "To": "11/2/2017", 827 | "Location": "Las Vegas", 828 | "Link": "https://www.devintersection.com/#!/", 829 | "Latitude": 36.1699412, 830 | "Longitude": -115.1398296 831 | }, 832 | { 833 | "Name": "Burke Holland", 834 | "Conference": "DevReach", 835 | "From": "11/12/2017", 836 | "To": "11/14/2017", 837 | "Location": "Sofia Bulgaria", 838 | "Link": "http://devreach.com/", 839 | "Latitude": 42.6977082, 840 | "Longitude": 23.3218675 841 | }, 842 | { 843 | "Name": "Damian Brady", 844 | "Conference": "Ignite", 845 | "From": "9/25/2017", 846 | "To": "9/29/2017", 847 | "Location": "Orlando", 848 | "Link": "https://www.microsoft.com/en-us/ignite/default.aspx", 849 | "Latitude": 28.5383355, 850 | "Longitude": -81.3792365 851 | }, 852 | { 853 | "Name": "Damian Brady", 854 | "Conference": "TechBash", 855 | "From": "10/4/2017", 856 | "To": "10/6/2017", 857 | "Location": "Pennsylvania", 858 | "Link": "https://techbash.com/", 859 | "Latitude": 41.2033216, 860 | "Longitude": -77.1945247 861 | }, 862 | { 863 | "Name": "Damian Brady", 864 | "Conference": "Prairie Dev Con Deliver", 865 | "From": "10/10/2017", 866 | "To": "10/12/2017", 867 | "Location": "Winnipeg", 868 | "Link": "http://www.prdcdeliver.com/", 869 | "Latitude": 49.895136, 870 | "Longitude": -97.13837439999999 871 | }, 872 | { 873 | "Name": "Damian Brady", 874 | "Conference": "TechDays NL", 875 | "From": "10/11/2017", 876 | "To": "10/13/2017", 877 | "Location": "Amsterdam", 878 | "Link": "https://www.techdays.nl/", 879 | "Latitude": 52.3702157, 880 | "Longitude": 4.895167900000001 881 | }, 882 | { 883 | "Name": "Damian Brady", 884 | "Conference": "Visual Studio Live 360", 885 | "From": "11/7/2017", 886 | "To": "11/17/2017", 887 | "Location": "Orlando", 888 | "Link": "https://vslive.com/ECG/live360events/Events/Orlando-2017/VSLive.aspx", 889 | "Latitude": 28.5383355, 890 | "Longitude": -81.3792365 891 | }, 892 | { 893 | "Name": "Damian Brady", 894 | "Conference": "ConFoo", 895 | "From": "12/4/2017", 896 | "To": "12/6/2017", 897 | "Location": "Vancouver", 898 | "Link": "https://confoo.ca/en/yvr2017", 899 | "Latitude": 49.2827291, 900 | "Longitude": -123.1207375 901 | }, 902 | { 903 | "Name": "Donovan Brown", 904 | "Conference": "DEVintersection", 905 | "From": "5/21/2017", 906 | "To": "5/24/2017", 907 | "Location": "Orlando", 908 | "Link": "https://devintersection.com/#!/Visual-Studio-ASP-Azure-Conference/", 909 | "Latitude": 28.5383355, 910 | "Longitude": -81.3792365 911 | }, 912 | { 913 | "Name": "Donovan Brown", 914 | "Conference": "Techorama", 915 | "From": "5/22/2017", 916 | "To": "5/24/2017", 917 | "Location": "", 918 | "Link": "https://techorama.be/" 919 | }, 920 | { 921 | "Name": "Donovan Brown", 922 | "Conference": "Dutch MS ALM Group", 923 | "From": "5/29/2017", 924 | "To": "5/29/2017", 925 | "Location": "", 926 | "Link": "https://www.meetup.com/Dutch-Microsoft-ALM-Group/events/236353322/" 927 | }, 928 | { 929 | "Name": "Donovan Brown", 930 | "Conference": "Scottish Developers", 931 | "From": "6/6/2017", 932 | "To": "6/6/2017", 933 | "Location": "Glasgow", 934 | "Link": "https://scottishdevelopers.com/", 935 | "Latitude": 55.864237, 936 | "Longitude": -4.251806 937 | }, 938 | { 939 | "Name": "Donovan Brown", 940 | "Conference": "Scottish Developers", 941 | "From": "6/7/2017", 942 | "To": "6/7/2017", 943 | "Location": "Edinburgh", 944 | "Link": "https://scottishdevelopers.com/", 945 | "Latitude": 55.953252, 946 | "Longitude": -3.188267 947 | }, 948 | { 949 | "Name": "Donovan Brown", 950 | "Conference": "Scottish Developers", 951 | "From": "6/8/2017", 952 | "To": "6/8/2017", 953 | "Location": "Aberdeen", 954 | "Link": "https://scottishdevelopers.com/", 955 | "Latitude": 57.149717, 956 | "Longitude": -2.094278 957 | }, 958 | { 959 | "Name": "Donovan Brown", 960 | "Conference": "Cambridge .NET User Group", 961 | "From": "6/12/2017", 962 | "To": "6/12/2017", 963 | "Location": "Cambridge", 964 | "Link": "https://www.meetup.com/Cambridge-NET-User-Group/events/237509818/?utm_content=bufferaede2&utm_medium=social&utm_source=twitter.com&utm_campaign=buffer", 965 | "Latitude": 52.205337, 966 | "Longitude": 0.121817 967 | }, 968 | { 969 | "Name": "Donovan Brown", 970 | "Conference": "Swedish Microsoft ALM and DevOps", 971 | "From": "6/14/2017", 972 | "To": "6/14/2017", 973 | "Location": "Stockholm", 974 | "Link": "https://www.meetup.com/swedish-ms-alm-devops/events/237449414/", 975 | "Latitude": 59.32932349999999, 976 | "Longitude": 18.0685808 977 | }, 978 | { 979 | "Name": "Donovan Brown", 980 | "Conference": ".NET Aarhus User Group", 981 | "From": "6/19/2017", 982 | "To": "6/19/2017", 983 | "Location": "Aarhus", 984 | "Link": "https://www.facebook.com/events/252955858443355/", 985 | "Latitude": 56.162939, 986 | "Longitude": 10.203921 987 | }, 988 | { 989 | "Name": "Donovan Brown", 990 | "Conference": ".NET Copenhagen User Group", 991 | "From": "6/22/2017", 992 | "To": "6/22/2017", 993 | "Location": "Copenhagen", 994 | "Link": "", 995 | "Latitude": 55.6760968, 996 | "Longitude": 12.5683372 997 | }, 998 | { 999 | "Name": "Donovan Brown", 1000 | "Conference": "Azure Saturday", 1001 | "From": "6/24/2017", 1002 | "To": "6/24/2017", 1003 | "Location": "Munich", 1004 | "Link": "https://azuresaturday.de/", 1005 | "Latitude": 48.1351253, 1006 | "Longitude": 11.5819805 1007 | }, 1008 | { 1009 | "Name": "Donovan Brown", 1010 | "Conference": "Developer Week", 1011 | "From": "6/26/2017", 1012 | "To": "6/29/2017", 1013 | "Location": "Nurmberg", 1014 | "Link": "http://www.developer-week.de/", 1015 | "Latitude": 49.4521018, 1016 | "Longitude": 11.0766654 1017 | }, 1018 | { 1019 | "Name": "Jeremy Likness", 1020 | "Conference": "DevNexus 2017: Angular and TypeScript Workshop", 1021 | "From": "2/22/2017", 1022 | "To": "2/22/2017", 1023 | "Location": "Atlanta, GA", 1024 | "Link": "https://github.com/JeremyLikness/ng2ts-workshop-v2", 1025 | "Latitude": 33.7489954, 1026 | "Longitude": -84.3879824 1027 | }, 1028 | { 1029 | "Name": "Jeremy Likness", 1030 | "Conference": "Code Career Academy: TypeScript, JavaScript's Safety Harness", 1031 | "From": "3/23/2017", 1032 | "To": "3/23/2017", 1033 | "Location": "Lawrenceville, GA", 1034 | "Link": "https://www.meetup.com/Code-Career-Academy-Meetups-Gwinnett/events/237645432/", 1035 | "Latitude": 33.9562149, 1036 | "Longitude": -83.9879625 1037 | }, 1038 | { 1039 | "Name": "Jeremy Likness", 1040 | "Conference": "Music City Code 2017: Rapid Dev with Angular and TypeScript and Contain Your Excitement (Docker)", 1041 | "From": "6/1/2017", 1042 | "To": "6/3/2017", 1043 | "Location": "Nashville, TN", 1044 | "Link": "https://blog.jeremylikness.com/music-city-code-2017-e78e6279a60b", 1045 | "Latitude": 36.1626638, 1046 | "Longitude": -86.7816016 1047 | }, 1048 | { 1049 | "Name": "Jeremy Likness", 1050 | "Conference": "We Rise Women in Tech Conference 2017: Contain Your Excitement (Docker)", 1051 | "From": "6/23/2017", 1052 | "To": "6/24/2017", 1053 | "Location": "Atlanta, GA", 1054 | "Link": "https://werise.tech/sessions/2017/4/16/contain-your-excitement-hands-on-docker", 1055 | "Latitude": 33.7489954, 1056 | "Longitude": -84.3879824 1057 | }, 1058 | { 1059 | "Name": "Jeremy Likness", 1060 | "Conference": "Atlanta Docker Meetup: Herding Cattle with Azure ACS", 1061 | "From": "8/14/2017", 1062 | "To": "8/14/2017", 1063 | "Location": "Atlanta, GA", 1064 | "Link": "https://blog.jeremylikness.com/herding-cattle-with-the-azure-container-service-acs-e329e7def93e", 1065 | "Latitude": 33.7489954, 1066 | "Longitude": -84.3879824 1067 | }, 1068 | { 1069 | "Name": "Jeremy Likness", 1070 | "Conference": "Atlanta .NET User Group: Explore the Cosmos (DB) with .NET Core 2.0", 1071 | "From": "8/28/2017", 1072 | "To": "8/28/2017", 1073 | "Location": "Alpharetta, GA", 1074 | "Link": "https://blog.jeremylikness.com/explore-the-cosmos-db-with-net-core-2-0-aab48423dcdc", 1075 | "Latitude": 34.0753762, 1076 | "Longitude": -84.2940899 1077 | }, 1078 | { 1079 | "Name": "Jeremy Likness", 1080 | "Conference": "Atlanta Code Camp 2017: Serverless .NET", 1081 | "From": "9/16/2017", 1082 | "To": "9/16/2017", 1083 | "Location": "Marietta, GA", 1084 | "Link": "https://blog.jeremylikness.com/atlanta-code-camp-2017-serverless-net-da640edd59e9", 1085 | "Latitude": 33.95260200000001, 1086 | "Longitude": -84.5499327 1087 | }, 1088 | { 1089 | "Name": "Jeremy Likness", 1090 | "Conference": "Google Developer Group Atlanta Meetup: Going Serverless", 1091 | "From": "9/19/2017", 1092 | "To": "9/19/2017", 1093 | "Location": "Atlanta, GA", 1094 | "Link": "https://blog.jeremylikness.com/google-developer-group-atlanta-serverless-node-js-functions-2ec8d987a4b1", 1095 | "Latitude": 33.7489954, 1096 | "Longitude": -84.3879824 1097 | }, 1098 | { 1099 | "Name": "Jeremy Likness", 1100 | "Conference": "Connect.TECH 2017: All Day Node Camp and TypeScript for Node.js Presentation", 1101 | "From": "9/20/2017", 1102 | "To": "9/22/2017", 1103 | "Location": "Atlanta, GA", 1104 | "Link": "https://blog.jeremylikness.com/connect-tech-2017-node-js-and-typescript-8419b0e4d689", 1105 | "Latitude": 33.7489954, 1106 | "Longitude": -84.3879824 1107 | }, 1108 | { 1109 | "Name": "Jeremy Likness", 1110 | "Conference": "Ignite", 1111 | "From": "9/25/2017", 1112 | "To": "9/29/2017", 1113 | "Location": "Orlando, FL", 1114 | "Link": "https://microsoft.com/ignite", 1115 | "Latitude": 28.5383355, 1116 | "Longitude": -81.3792365 1117 | }, 1118 | { 1119 | "Name": "Jeremy Likness", 1120 | "Conference": "Microsoft Community Connections Charlotte", 1121 | "From": "10/6/2017", 1122 | "To": "10/7/2017", 1123 | "Location": "Charlotte, NC", 1124 | "Link": " ", 1125 | "Latitude": 35.2270869, 1126 | "Longitude": -80.8431267 1127 | }, 1128 | { 1129 | "Name": "Jeremy Likness", 1130 | "Conference": "Cloud Storage and Bay Area Lambda Meetup: Code First, Going Serverless", 1131 | "From": "10/17/2017", 1132 | "To": "10/17/2017", 1133 | "Location": "San Francisco, CA", 1134 | "Link": "https://www.meetup.com/Cloud-Storage-Bay-Area/events/243386936/", 1135 | "Latitude": 37.7749295, 1136 | "Longitude": -122.4194155 1137 | }, 1138 | { 1139 | "Name": "Jeremy Likness", 1140 | "Conference": "SF Microservices Meetup: Code First, Going Serverless", 1141 | "From": "10/18/2017", 1142 | "To": "10/18/2017", 1143 | "Location": "Bay Area, CA", 1144 | "Link": "https://www.meetup.com/SF-Microservices/events/243921541/", 1145 | "Latitude": 37.8271784, 1146 | "Longitude": -122.2913078 1147 | }, 1148 | { 1149 | "Name": "Jeremy Likness", 1150 | "Conference": "SF Bay Area Meetup: Code First, Going Serverless", 1151 | "From": "10/19/2017", 1152 | "To": "10/19/2017", 1153 | "Location": "Mountain View, CA", 1154 | "Link": "https://www.meetup.com/microservices/events/243498016/", 1155 | "Latitude": 37.3860517, 1156 | "Longitude": -122.0838511 1157 | }, 1158 | { 1159 | "Name": "Jeremy Likness", 1160 | "Conference": "PASS Summit 2017: Web API Design Workshop and Swagger Presentation", 1161 | "From": "10/30/2017", 1162 | "To": "11/3/2017", 1163 | "Location": "Seattle, WA", 1164 | "Link": "http://www.pass.org/Summit/2017/Speakers/Details.aspx?spid=5787", 1165 | "Latitude": 47.6062095, 1166 | "Longitude": -122.3320708 1167 | }, 1168 | { 1169 | "Name": "Jeremy Likness", 1170 | "Conference": "Azure in the ATL: Event Grid, Glue for the Internet", 1171 | "From": "2/20/2018", 1172 | "To": "2/20/2018", 1173 | "Location": "Atlanta, GA", 1174 | "Link": "https://www.meetup.com/Azure-in-the-ATL/events/243464450/", 1175 | "Latitude": 33.7489954, 1176 | "Longitude": -84.3879824 1177 | }, 1178 | { 1179 | "Name": "Jeremy Likness", 1180 | "Conference": "Ignite", 1181 | "From": "9/25/2017", 1182 | "To": "9/29/2017", 1183 | "Location": "Orlando", 1184 | "Link": "https://www.microsoft.com/en-us/ignite/", 1185 | "Latitude": 28.5383355, 1186 | "Longitude": -81.3792365 1187 | }, 1188 | { 1189 | "Name": "John Papa", 1190 | "Conference": "ngAtl", 1191 | "From": "1/30/2017", 1192 | "To": "2/2/2017", 1193 | "Location": "Atlanta, GA", 1194 | "Link": "http://ng-atl.org/", 1195 | "Latitude": 33.7489954, 1196 | "Longitude": -84.3879824 1197 | }, 1198 | { 1199 | "Name": "John Papa", 1200 | "Conference": "ngConf", 1201 | "From": "4/18/2018", 1202 | "To": "4/29/2018", 1203 | "Location": "Salt Lake City", 1204 | "Link": "https://www.ng-conf.org/", 1205 | "Latitude": 40.7607793, 1206 | "Longitude": -111.8910474 1207 | }, 1208 | { 1209 | "Name": "John Papa", 1210 | "Conference": "Pluralsight Live", 1211 | "From": "9/18/2017", 1212 | "To": "9/21/2017", 1213 | "Location": "Salt Lake City", 1214 | "Link": "https://www.pluralsight.com/event-details/2017", 1215 | "Latitude": 40.7607793, 1216 | "Longitude": -111.8910474 1217 | }, 1218 | { 1219 | "Name": "John Papa", 1220 | "Conference": "Ignite", 1221 | "From": "9/24/2017", 1222 | "To": "9/24/2017", 1223 | "Location": "Orlando", 1224 | "Link": "https://www.microsoft.com/en-us/ignite/default.aspx", 1225 | "Latitude": 28.5383355, 1226 | "Longitude": -81.3792365 1227 | }, 1228 | { 1229 | "Name": "John Papa", 1230 | "Conference": "AngularMix", 1231 | "From": "10/10/2017", 1232 | "To": "10/12/2017", 1233 | "Location": "Orlando", 1234 | "Link": "http://angularmix.com", 1235 | "Latitude": 28.5383355, 1236 | "Longitude": -81.3792365 1237 | }, 1238 | { 1239 | "Name": "John Papa", 1240 | "Conference": "DevUp", 1241 | "From": "10/14/2017", 1242 | "To": "10/16/2017", 1243 | "Location": "St. Louis", 1244 | "Link": "http://devupconf.org/", 1245 | "Latitude": 38.6270025, 1246 | "Longitude": -90.19940419999999 1247 | }, 1248 | { 1249 | "Name": "John Papa", 1250 | "Conference": "AngleBrackets", 1251 | "From": "10/29/2017", 1252 | "To": "10/31/2017", 1253 | "Location": "Las Vegas", 1254 | "Link": "htto://anglebrackets.org", 1255 | "Latitude": 36.1699412, 1256 | "Longitude": -115.1398296 1257 | }, 1258 | { 1259 | "Name": "John Papa", 1260 | "Conference": "360 Live", 1261 | "From": "11/15/2017", 1262 | "To": "11/17/2017", 1263 | "Location": "Orlando", 1264 | "Link": "https://live360events.com/events/orlando-2017/home.aspx", 1265 | "Latitude": 28.5383355, 1266 | "Longitude": -81.3792365 1267 | }, 1268 | { 1269 | "Name": "John Papa", 1270 | "Conference": "All Things Open", 1271 | "From": "10/23/2017", 1272 | "To": "10/24/2017", 1273 | "Location": "Raleigh", 1274 | "Link": "https://allthingsopen.org/", 1275 | "Latitude": 35.7795897, 1276 | "Longitude": -78.6381787 1277 | }, 1278 | { 1279 | "Name": "Laurent Bugnion", 1280 | "Conference": "MVVM Cross Hackfest", 1281 | "From": "8/31/2017", 1282 | "To": "9/2/2017", 1283 | "Location": "Amsterdam", 1284 | "Link": "", 1285 | "Latitude": 52.3702157, 1286 | "Longitude": 4.895167900000001 1287 | }, 1288 | { 1289 | "Name": "Laurent Bugnion", 1290 | "Conference": "MonkeyFest", 1291 | "From": "9/21/2017", 1292 | "To": "9/23/2017", 1293 | "Location": "Singapore", 1294 | "Link": "", 1295 | "Latitude": 1.352083, 1296 | "Longitude": 103.819836 1297 | }, 1298 | { 1299 | "Name": "Matt Soucoup", 1300 | "Conference": "Indy.Code()", 1301 | "From": "3/23/2017", 1302 | "To": "3/31/2017", 1303 | "Location": "Indianapolis", 1304 | "Link": "https://indycode.amegala.com/", 1305 | "Latitude": 39.768403, 1306 | "Longitude": -86.158068 1307 | }, 1308 | { 1309 | "Name": "Matt Soucoup", 1310 | "Conference": "That Conference", 1311 | "From": "8/3/2017", 1312 | "To": "8/9/2017", 1313 | "Location": "Wisconsin Dells", 1314 | "Link": "https://www.thatconference.com", 1315 | "Latitude": 43.6274794, 1316 | "Longitude": -89.7709579 1317 | }, 1318 | { 1319 | "Name": "Matt Soucoup", 1320 | "Conference": "MKE DOT NET", 1321 | "From": "9/7/2017", 1322 | "To": "9/8/2017", 1323 | "Location": "Milwaukee", 1324 | "Link": "http://www.mkedotnet.com/", 1325 | "Latitude": 43.0379848, 1326 | "Longitude": -87.91082949999999 1327 | }, 1328 | { 1329 | "Name": "Matt Soucoup", 1330 | "Conference": "VSLive", 1331 | "From": "9/19/2017", 1332 | "To": "9/21/2017", 1333 | "Location": "Chicago", 1334 | "Link": "https://vslive.com/Events/Chicago-2017/Home.aspx", 1335 | "Latitude": 41.8781136, 1336 | "Longitude": -87.6297982 1337 | }, 1338 | { 1339 | "Name": "Matt Soucoup", 1340 | "Conference": "Prairie.Code()", 1341 | "From": "9/27/2017", 1342 | "To": "9/29/2017", 1343 | "Location": "Des Moines", 1344 | "Link": "https://prairiecode.amegala.com", 1345 | "Latitude": 41.6005448, 1346 | "Longitude": -93.6091064 1347 | }, 1348 | { 1349 | "Name": "Matt Soucoup", 1350 | "Conference": "Mobile Era", 1351 | "From": "10/4/2017", 1352 | "To": "10/6/2017", 1353 | "Location": "Oslo", 1354 | "Link": "https://mobileera.rocks", 1355 | "Latitude": 59.9138688, 1356 | "Longitude": 10.7522454 1357 | }, 1358 | { 1359 | "Name": "Matt Soucoup", 1360 | "Conference": "CodeMash", 1361 | "From": "1/8/2018", 1362 | "To": "1/12/2018", 1363 | "Location": "Sandusky", 1364 | "Link": "https://codemash.org", 1365 | "Latitude": 41.4489396, 1366 | "Longitude": -82.7079605 1367 | }, 1368 | { 1369 | "Name": "Maxime Rouiller", 1370 | "Conference": "Defi Montreal", 1371 | "From": "9/4/2017", 1372 | "To": "9/5/2017", 1373 | "Location": "Montreal", 1374 | "Link": "https://www.meetup.com/BelfastJUG/", 1375 | "Latitude": 45.5016889, 1376 | "Longitude": -73.567256 1377 | }, 1378 | { 1379 | "Name": "Maxime Rouiller", 1380 | "Conference": "Instrumenting .NET Core Applications with Azure Application Insights", 1381 | "From": "10/17/2017", 1382 | "To": "10/17/2017", 1383 | "Location": "Atlanta, GA", 1384 | "Link": "http://2017.cssdevconf.com/", 1385 | "Latitude": 33.7489954, 1386 | "Longitude": -84.3879824 1387 | }, 1388 | { 1389 | "Name": "Paige Bailey", 1390 | "Conference": "Houston Data Science Meetup", 1391 | "From": "7/6/2017", 1392 | "To": "7/6/2017", 1393 | "Location": "Houston, TX", 1394 | "Link": "https://www.meetup.com/Houston-Data-Science/events/240566684/", 1395 | "Latitude": 29.7604267, 1396 | "Longitude": -95.3698028 1397 | }, 1398 | { 1399 | "Name": "Paige Bailey", 1400 | "Conference": "Houston Data Science Meetup", 1401 | "From": "7/22/2017", 1402 | "To": "7/22/2017", 1403 | "Location": "Houston, TX", 1404 | "Link": "https://www.meetup.com/Houston-Data-Science/events/240417376/?_cookie-check=rmfKwWUeuVIzJ7Sk", 1405 | "Latitude": 29.7604267, 1406 | "Longitude": -95.3698028 1407 | }, 1408 | { 1409 | "Name": "Paige Bailey", 1410 | "Conference": "Data & AI Summit", 1411 | "From": "8/25/2017", 1412 | "To": "8/25/2017", 1413 | "Location": "Online", 1414 | "Link": "https://onlinexperiences.com/scripts/Server.nxp?LASCmd=AI:4;F:QS!10100&ShowKey=42620", 1415 | "Latitude": 36.146636, 1416 | "Longitude": -95.90194 1417 | }, 1418 | { 1419 | "Name": "Paige Bailey", 1420 | "Conference": "Quicken Loan Technology Conference", 1421 | "From": "9/19/2017", 1422 | "To": "9/20/2017", 1423 | "Location": "Detroit, MI", 1424 | "Link": "http://qltechcon.com/index.html", 1425 | "Latitude": 42.331427, 1426 | "Longitude": -83.0457538 1427 | }, 1428 | { 1429 | "Name": "Paige Bailey", 1430 | "Conference": "Florida PyCon", 1431 | "From": "10/7/2017", 1432 | "To": "10/7/2017", 1433 | "Location": "Miami, FL", 1434 | "Link": "http://flpy.org/", 1435 | "Latitude": 25.7616798, 1436 | "Longitude": -80.1917902 1437 | }, 1438 | { 1439 | "Name": "Paige Bailey", 1440 | "Conference": "CodeCamp NYC 2017", 1441 | "From": "10/14/2017", 1442 | "To": "10/15/2017", 1443 | "Location": "New York City, NY", 1444 | "Link": "http://codecampnyc.org/selected-sessions/", 1445 | "Latitude": 40.7127753, 1446 | "Longitude": -74.0059728 1447 | }, 1448 | { 1449 | "Name": "Paige Bailey", 1450 | "Conference": "Jazoon Tech Days", 1451 | "From": "10/26/2017", 1452 | "To": "10/27/2017", 1453 | "Location": "Zurich", 1454 | "Link": "http://jazoon.com/", 1455 | "Latitude": 47.3768866, 1456 | "Longitude": 8.541694 1457 | }, 1458 | { 1459 | "Name": "Paige Bailey", 1460 | "Conference": "HackTX", 1461 | "From": "10/28/2017", 1462 | "To": "10/29/2017", 1463 | "Location": "Austin, TX", 1464 | "Link": "https://hacktx.com/", 1465 | "Latitude": 30.267153, 1466 | "Longitude": -97.7430608 1467 | }, 1468 | { 1469 | "Name": "Paige Bailey", 1470 | "Conference": "Invent It. Build It. 2017", 1471 | "From": "10/28/2017", 1472 | "To": "10/28/2017", 1473 | "Location": "Austin, TX", 1474 | "Link": "http://societyofwomenengineers.swe.org/invent-it-build-it", 1475 | "Latitude": 30.267153, 1476 | "Longitude": -97.7430608 1477 | }, 1478 | { 1479 | "Name": "Paige Bailey", 1480 | "Conference": "Developer Week - Austin", 1481 | "From": "11/7/2017", 1482 | "To": "11/9/2017", 1483 | "Location": "Austin, TX", 1484 | "Link": "http://www.developerweek.com/Austin/", 1485 | "Latitude": 30.267153, 1486 | "Longitude": -97.7430608 1487 | }, 1488 | { 1489 | "Name": "Paige Bailey", 1490 | "Conference": "DevReach Bulgaria", 1491 | "From": "11/13/2017", 1492 | "To": "11/15/2017", 1493 | "Location": "Bulgaria", 1494 | "Link": "http://devreach.com/", 1495 | "Latitude": 42.733883, 1496 | "Longitude": 25.48583 1497 | }, 1498 | { 1499 | "Name": "Paige Bailey", 1500 | "Conference": "CodeMotion: Madrid", 1501 | "From": "11/24/2017", 1502 | "To": "11/25/2017", 1503 | "Location": "Madrid", 1504 | "Link": "https://2017.codemotion.es/en/", 1505 | "Latitude": 40.4167754, 1506 | "Longitude": -3.7037902 1507 | }, 1508 | { 1509 | "Name": "Paige Bailey", 1510 | "Conference": "PyData NYC", 1511 | "From": "11/27/2017", 1512 | "To": "11/30/2017", 1513 | "Location": "New York City, NY", 1514 | "Link": "https://pydata.org/nyc2017/", 1515 | "Latitude": 40.7127753, 1516 | "Longitude": -74.0059728 1517 | }, 1518 | { 1519 | "Name": "Ruediger Schickhaus", 1520 | "Conference": "Red Hat Forum", 1521 | "From": "10/24/2017", 1522 | "To": "10/24/2017", 1523 | "Location": "Vienna", 1524 | "Link": "https://www.meetup.com/Code-Career-Academy-Meetups-Gwinnett/events/237645432/", 1525 | "Latitude": 48.2081743, 1526 | "Longitude": 16.3738189 1527 | }, 1528 | { 1529 | "Name": "Sarah Drasner", 1530 | "Conference": "SVG Summit", 1531 | "From": "2/15/2017", 1532 | "To": "2/15/2017", 1533 | "Location": "Online", 1534 | "Link": "http://environmentsforhumans.com/2017/svg-summit/", 1535 | "Latitude": 36.146636, 1536 | "Longitude": -95.90194 1537 | }, 1538 | { 1539 | "Name": "Sarah Drasner", 1540 | "Conference": "Web Animation Workshops", 1541 | "From": "2/20/2017", 1542 | "To": "2/20/2017", 1543 | "Location": "San Francisco", 1544 | "Link": "https://webanimationworkshops.com/", 1545 | "Latitude": 37.7749295, 1546 | "Longitude": -122.4194155 1547 | }, 1548 | { 1549 | "Name": "Sarah Drasner", 1550 | "Conference": "ForwardJS", 1551 | "From": "3/4/2017", 1552 | "To": "3/4/2017", 1553 | "Location": "San Francisco", 1554 | "Link": "https://forwardjs.com/", 1555 | "Latitude": 37.7749295, 1556 | "Longitude": -122.4194155 1557 | }, 1558 | { 1559 | "Name": "Sarah Drasner", 1560 | "Conference": "SFHTML5", 1561 | "From": "4/7/2017", 1562 | "To": "4/7/2017", 1563 | "Location": "San Francisco", 1564 | "Link": "https://www.meetup.com/sfhtml5/?_cookie-check=SZNcxB3cqM9_CCux", 1565 | "Latitude": 37.7749295, 1566 | "Longitude": -122.4194155 1567 | }, 1568 | { 1569 | "Name": "Sarah Drasner", 1570 | "Conference": "Smashing Conf", 1571 | "From": "4/4/2017", 1572 | "To": "4/6/2017", 1573 | "Location": "San Francisco", 1574 | "Link": "https://smashingconf.com/workshops/sarah-drasner", 1575 | "Latitude": 37.7749295, 1576 | "Longitude": -122.4194155 1577 | }, 1578 | { 1579 | "Name": "Sarah Drasner", 1580 | "Conference": "FITC Toronto", 1581 | "From": "4/23/2017", 1582 | "To": "4/25/2017", 1583 | "Location": "Toronto, Canada", 1584 | "Link": "http://fitc.ca/event/to17/", 1585 | "Latitude": 43.653226, 1586 | "Longitude": -79.3831843 1587 | }, 1588 | { 1589 | "Name": "Sarah Drasner", 1590 | "Conference": "Beyond Tellerand", 1591 | "From": "5/15/2017", 1592 | "To": "5/17/2017", 1593 | "Location": "Dusseldorf, Germany", 1594 | "Link": "https://beyondtellerrand.com/", 1595 | "Latitude": 51.2277411, 1596 | "Longitude": 6.7734556 1597 | }, 1598 | { 1599 | "Name": "Sarah Drasner", 1600 | "Conference": "Beyond Tellerand Workshop Days", 1601 | "From": "5/18/2017", 1602 | "To": "5/18/2017", 1603 | "Location": "Dusseldorf, Germany", 1604 | "Link": "https://beyondtellerrand.com/", 1605 | "Latitude": 51.2277411, 1606 | "Longitude": 6.7734556 1607 | }, 1608 | { 1609 | "Name": "Sarah Drasner", 1610 | "Conference": "React Europe", 1611 | "From": "5/18/2017", 1612 | "To": "5/19/2017", 1613 | "Location": "Paris", 1614 | "Link": "https://www.react-europe.org/", 1615 | "Latitude": 48.856614, 1616 | "Longitude": 2.3522219 1617 | }, 1618 | { 1619 | "Name": "Sarah Drasner", 1620 | "Conference": "Awwwards Conference", 1621 | "From": "6/1/2017", 1622 | "To": "6/2/2017", 1623 | "Location": "Los Angeles", 1624 | "Link": "https://conference.awwwards.com/", 1625 | "Latitude": 34.0522342, 1626 | "Longitude": -118.2436849 1627 | }, 1628 | { 1629 | "Name": "Sarah Drasner", 1630 | "Conference": "Vue Conf", 1631 | "From": "6/22/2017", 1632 | "To": "6/23/2017", 1633 | "Location": "Wroclaw, Poland", 1634 | "Link": "https://conf.vuejs.org/", 1635 | "Latitude": 51.1078852, 1636 | "Longitude": 17.0385376 1637 | }, 1638 | { 1639 | "Name": "Sarah Drasner", 1640 | "Conference": "Animating Vue Workshop", 1641 | "From": "6/21/2017", 1642 | "To": "6/21/2017", 1643 | "Location": "Wroclaw, Poland", 1644 | "Link": "https://conf.vuejs.org/workshops", 1645 | "Latitude": 51.1078852, 1646 | "Longitude": 17.0385376 1647 | }, 1648 | { 1649 | "Name": "Sarah Drasner", 1650 | "Conference": "JS Channel", 1651 | "From": "7/28/2017", 1652 | "To": "7/29/2017", 1653 | "Location": "Bangalore, India", 1654 | "Link": "http://jschannel.com/#/", 1655 | "Latitude": 12.9715987, 1656 | "Longitude": 77.5945627 1657 | }, 1658 | { 1659 | "Name": "Sarah Drasner", 1660 | "Conference": "Develop Denver", 1661 | "From": "8/10/2017", 1662 | "To": "8/11/2017", 1663 | "Location": "Denver, Colorado", 1664 | "Link": "https://developdenver.org/", 1665 | "Latitude": 39.7392358, 1666 | "Longitude": -104.990251 1667 | }, 1668 | { 1669 | "Name": "Sarah Drasner", 1670 | "Conference": "Frontend Conference", 1671 | "From": "8/28/2017", 1672 | "To": "9/1/2017", 1673 | "Location": "Zurich", 1674 | "Link": "https://www.frontendconf.ch/", 1675 | "Latitude": 47.3768866, 1676 | "Longitude": 8.541694 1677 | }, 1678 | { 1679 | "Name": "Sarah Drasner", 1680 | "Conference": "Frontend Conference Workshops", 1681 | "From": "9/2/2017", 1682 | "To": "9/2/2017", 1683 | "Location": "Zurich", 1684 | "Link": "https://www.frontendconf.ch/workshops/from-animations-to-data-visualisation", 1685 | "Latitude": 47.3768866, 1686 | "Longitude": 8.541694 1687 | }, 1688 | { 1689 | "Name": "Sarah Drasner", 1690 | "Conference": "Web Animation Workshops", 1691 | "From": "9/2/2017", 1692 | "To": "9/5/2017", 1693 | "Location": "Paris", 1694 | "Link": "webanimationworkshops.com", 1695 | "Latitude": 48.856614, 1696 | "Longitude": 2.3522219 1697 | }, 1698 | { 1699 | "Name": "Sarah Drasner", 1700 | "Conference": "Fullstack Fest", 1701 | "From": "9/6/2017", 1702 | "To": "9/8/2017", 1703 | "Location": "Barcelona, Spain", 1704 | "Link": "https://2017.fullstackfest.com/", 1705 | "Latitude": 41.3850639, 1706 | "Longitude": 2.1734035 1707 | }, 1708 | { 1709 | "Name": "Sarah Drasner", 1710 | "Conference": "Web Animation Workshops", 1711 | "From": "9/16/2017", 1712 | "To": "9/19/2017", 1713 | "Location": "Portland", 1714 | "Link": "webanimationworkshops.com", 1715 | "Latitude": 45.5230622, 1716 | "Longitude": -122.6764815 1717 | }, 1718 | { 1719 | "Name": "Sarah Drasner", 1720 | "Conference": "D3 Unconf - Keynote", 1721 | "From": "9/21/2017", 1722 | "To": "9/22/2017", 1723 | "Location": "San Francisco", 1724 | "Link": "http://visfest.com/d3unconf-2017/", 1725 | "Latitude": 37.7749295, 1726 | "Longitude": -122.4194155 1727 | }, 1728 | { 1729 | "Name": "Sarah Drasner", 1730 | "Conference": "The Web Unleashed Workshop", 1731 | "From": "9/24/2017", 1732 | "To": "9/24/2017", 1733 | "Location": "Toronto, Canada", 1734 | "Link": "http://fitc.ca/presentation/workshop-building-animating-svgs/", 1735 | "Latitude": 43.653226, 1736 | "Longitude": -79.3831843 1737 | }, 1738 | { 1739 | "Name": "Sarah Drasner", 1740 | "Conference": "CSS Dev Conf- featured speaker", 1741 | "From": "10/6/2017", 1742 | "To": "10/10/2017", 1743 | "Location": "New Orleans", 1744 | "Link": "http://2017.cssdevconf.com/", 1745 | "Latitude": 29.95106579999999, 1746 | "Longitude": -90.0715323 1747 | }, 1748 | { 1749 | "Name": "Sarah Drasner", 1750 | "Conference": "CSS Dev Conf Workshops", 1751 | "From": "10/11/2017", 1752 | "To": "10/11/2017", 1753 | "Location": "New Orleans", 1754 | "Link": "http://2017.cssdevconf.com/", 1755 | "Latitude": 29.95106579999999, 1756 | "Longitude": -90.0715323 1757 | }, 1758 | { 1759 | "Name": "Sarah Drasner", 1760 | "Conference": "Web Animation Workshops", 1761 | "From": "10/12/2017", 1762 | "To": "10/13/2017", 1763 | "Location": "Pittsburgh", 1764 | "Link": "webanimationworkshops.com", 1765 | "Latitude": 40.44062479999999, 1766 | "Longitude": -79.9958864 1767 | }, 1768 | { 1769 | "Name": "Sarah Drasner", 1770 | "Conference": "Smashing Conf", 1771 | "From": "10/17/2017", 1772 | "To": "10/18/2017", 1773 | "Location": "Barcelona, Spain", 1774 | "Link": "https://smashingconf.com/barcelona-2017/workshops", 1775 | "Latitude": 41.3850639, 1776 | "Longitude": 2.1734035 1777 | }, 1778 | { 1779 | "Name": "Sarah Drasner", 1780 | "Conference": "Smashing Conf Workshops", 1781 | "From": "10/19/2017", 1782 | "To": "10/19/2017", 1783 | "Location": "Barcelona, Spain", 1784 | "Link": "https://smashingconf.com/barcelona-2017/workshops", 1785 | "Latitude": 41.3850639, 1786 | "Longitude": 2.1734035 1787 | }, 1788 | { 1789 | "Name": "Sarah Drasner", 1790 | "Conference": "Reactive Conf", 1791 | "From": "10/25/2017", 1792 | "To": "10/28/2017", 1793 | "Location": "Bratislava", 1794 | "Link": "https://reactiveconf.com/", 1795 | "Latitude": 48.1485965, 1796 | "Longitude": 17.1077478 1797 | }, 1798 | { 1799 | "Name": "Sarah Drasner", 1800 | "Conference": "You Gotta Love Frontend", 1801 | "From": "10/30/2017", 1802 | "To": "10/31/2017", 1803 | "Location": "Tel Aviv", 1804 | "Link": "https://yougottalovefrontend.com/", 1805 | "Latitude": 32.0852999, 1806 | "Longitude": 34.78176759999999 1807 | }, 1808 | { 1809 | "Name": "Sarah Drasner", 1810 | "Conference": "Vue Conf Amsterdam", 1811 | "From": "2/14/2018", 1812 | "To": "2/16/2018", 1813 | "Location": "Amsterdam", 1814 | "Link": "TBD", 1815 | "Latitude": 52.3702157, 1816 | "Longitude": 4.895167900000001 1817 | }, 1818 | { 1819 | "Name": "Sarah Drasner", 1820 | "Conference": "Fullstack 2018 - Keynote", 1821 | "From": "7/11/2018", 1822 | "To": "7/13/2018", 1823 | "Location": "London, UK", 1824 | "Link": "https://skillsmatter.com/conferences/9815-fullstack-2018-the-conference-on-javascript-node-and-internet-of-things", 1825 | "Latitude": 51.5073509, 1826 | "Longitude": -0.1277583 1827 | }, 1828 | { 1829 | "Name": "Sarah Drasner", 1830 | "Conference": "Frontend United", 1831 | "From": "5/31/2018", 1832 | "To": "6/1/2018", 1833 | "Location": "Utrecht", 1834 | "Link": "http://frontendunited.org/", 1835 | "Latitude": 52.09073739999999, 1836 | "Longitude": 5.1214201 1837 | }, 1838 | { 1839 | "Name": "Scott Cate", 1840 | "Conference": "DLD 2017 Tel Aviv Israel", 1841 | "From": "9/4/2017", 1842 | "To": "9/7/2017", 1843 | "Location": "Tel Aviv", 1844 | "Link": "", 1845 | "Latitude": 32.0852999, 1846 | "Longitude": 34.78176759999999 1847 | }, 1848 | { 1849 | "Name": "Shayne Boyer", 1850 | "Conference": "DevIntersection Europe", 1851 | "From": "9/18/2017", 1852 | "To": "9/19/2017", 1853 | "Location": "Stockholm", 1854 | "Link": "http://devintersectioneurope.com", 1855 | "Latitude": 59.32932349999999, 1856 | "Longitude": 18.0685808 1857 | }, 1858 | { 1859 | "Name": "Shayne Boyer", 1860 | "Conference": "AngularMix", 1861 | "From": "10/10/2017", 1862 | "To": "10/12/2017", 1863 | "Location": "Orlando", 1864 | "Link": "https://angularmix.com/", 1865 | "Latitude": 28.5383355, 1866 | "Longitude": -81.3792365 1867 | }, 1868 | { 1869 | "Name": "Shayne Boyer", 1870 | "Conference": "DevIntersection", 1871 | "From": "10/29/2017", 1872 | "To": "10/31/2017", 1873 | "Location": "Las Vegas", 1874 | "Link": "http://devintersection.com", 1875 | "Latitude": 36.1699412, 1876 | "Longitude": -115.1398296 1877 | }, 1878 | { 1879 | "Name": "Shayne Boyer", 1880 | "Conference": "Dockercon Europe", 1881 | "From": "6/28/2018", 1882 | "To": "10/19/2018", 1883 | "Location": "Copenhagen", 1884 | "Link": "https://europe-2017.dockercon.com/", 1885 | "Latitude": 55.6760968, 1886 | "Longitude": 12.5683372 1887 | }, 1888 | { 1889 | "Name": "Shayne Boyer", 1890 | "Conference": "Ignite", 1891 | "From": "9/25/2017", 1892 | "To": "9/29/2017", 1893 | "Location": "Orlando", 1894 | "Link": "https://www.microsoft.com/en-us/ignite/", 1895 | "Latitude": 28.5383355, 1896 | "Longitude": -81.3792365 1897 | }, 1898 | { 1899 | "Name": "Simona Cotin", 1900 | "Conference": "DLD 2017 Tel Aviv Israel", 1901 | "From": "9/4/2017", 1902 | "To": "9/7/2017", 1903 | "Location": "Tel Aviv", 1904 | "Link": "", 1905 | "Latitude": 32.0852999, 1906 | "Longitude": 34.78176759999999 1907 | }, 1908 | { 1909 | "Name": "Simona Cotin", 1910 | "Conference": "DLD Tel Aviv", 1911 | "From": "9/5/2017", 1912 | "To": "9/8/2017", 1913 | "Location": "Tel Aviv", 1914 | "Link": "", 1915 | "Latitude": 32.0852999, 1916 | "Longitude": 34.78176759999999 1917 | }, 1918 | { 1919 | "Name": "Simona Cotin", 1920 | "Conference": "PortoTechHub", 1921 | "From": "9/20/2017", 1922 | "To": "9/20/2017", 1923 | "Location": "Porto, Portugal", 1924 | "Link": "", 1925 | "Latitude": 41.1579438, 1926 | "Longitude": -8.629105299999999 1927 | }, 1928 | { 1929 | "Name": "Simona Cotin", 1930 | "Conference": "Frontend Connect", 1931 | "From": "9/22/2017", 1932 | "To": "9/23/2017", 1933 | "Location": "Warsaw, Poland", 1934 | "Link": "", 1935 | "Latitude": 52.2296756, 1936 | "Longitude": 21.0122287 1937 | }, 1938 | { 1939 | "Name": "Simona Cotin", 1940 | "Conference": "AngularMix", 1941 | "From": "10/10/2017", 1942 | "To": "10/12/2017", 1943 | "Location": "Orlando", 1944 | "Link": "", 1945 | "Latitude": 28.5383355, 1946 | "Longitude": -81.3792365 1947 | }, 1948 | { 1949 | "Name": "Simona Cotin", 1950 | "Conference": "Plone", 1951 | "From": "10/18/2017", 1952 | "To": "10/20/2017", 1953 | "Location": "Barcelona, Spain", 1954 | "Link": "", 1955 | "Latitude": 41.3850639, 1956 | "Longitude": 2.1734035 1957 | }, 1958 | { 1959 | "Name": "Simona Cotin", 1960 | "Conference": "Gdg Nantes", 1961 | "From": "10/19/2017", 1962 | "To": "10/20/2017", 1963 | "Location": "Nantes, France", 1964 | "Link": "", 1965 | "Latitude": 47.218371, 1966 | "Longitude": -1.553621 1967 | }, 1968 | { 1969 | "Name": "Simona Cotin", 1970 | "Conference": "Nodeconf", 1971 | "From": "11/2/2017", 1972 | "To": "11/8/2017", 1973 | "Location": "Kilkenny, Ireland", 1974 | "Link": "", 1975 | "Latitude": 52.6541454, 1976 | "Longitude": -7.2447879 1977 | }, 1978 | { 1979 | "Name": "Simona Cotin", 1980 | "Conference": "Angular Connect", 1981 | "From": "11/7/2017", 1982 | "To": "11/8/2017", 1983 | "Location": "London, UK", 1984 | "Link": "", 1985 | "Latitude": 51.5073509, 1986 | "Longitude": -0.1277583 1987 | }, 1988 | { 1989 | "Name": "Simona Cotin", 1990 | "Conference": "ngAtl", 1991 | "From": "1/19/2018", 1992 | "To": "2/2/2018", 1993 | "Location": "Atlanta", 1994 | "Link": "", 1995 | "Latitude": 33.7489954, 1996 | "Longitude": -84.3879824 1997 | }, 1998 | { 1999 | "Name": "Steven Murawski", 2000 | "Conference": "PowerShell + DevOps Global Summit", 2001 | "From": "4/9/2017", 2002 | "To": "4/12/2017", 2003 | "Location": "Bellevue, WA", 2004 | "Link": "https://powershell.org/summit/", 2005 | "Latitude": 47.6101497, 2006 | "Longitude": -122.2015159 2007 | }, 2008 | { 2009 | "Name": "Steven Murawski", 2010 | "Conference": "DevIntersection", 2011 | "From": "5/20/2017", 2012 | "To": "5/24/2017", 2013 | "Location": "Orlando, FL", 2014 | "Link": "http://devintersection.com", 2015 | "Latitude": 28.5383355, 2016 | "Longitude": -81.3792365 2017 | }, 2018 | { 2019 | "Name": "Steven Murawski", 2020 | "Conference": "ChefConf", 2021 | "From": "5/21/2017", 2022 | "To": "5/24/2017", 2023 | "Location": "Austin, TX", 2024 | "Link": "https://chefconf.chef.io/2017/", 2025 | "Latitude": 30.267153, 2026 | "Longitude": -97.7430608 2027 | }, 2028 | { 2029 | "Name": "Steven Murawski", 2030 | "Conference": "DevOps (DSC) Camp", 2031 | "From": "7/27/2017", 2032 | "To": "7/30/2017", 2033 | "Location": "Las Vegas, NV", 2034 | "Link": "https://www.eventbrite.com/e/devopsdsc-camp-2017-tickets-31137067808#", 2035 | "Latitude": 36.1699412, 2036 | "Longitude": -115.1398296 2037 | }, 2038 | { 2039 | "Name": "Steven Murawski", 2040 | "Conference": "Wisconsin VMWare Users Group (WI VMUG)", 2041 | "From": "8/3/2017", 2042 | "To": "8/3/2017", 2043 | "Location": "Appleton, WI", 2044 | "Link": "http://www.wivmug.org/2017/07/thursday-august-3rd-2017-appleton-meeting/", 2045 | "Latitude": 44.2619309, 2046 | "Longitude": -88.41538469999999 2047 | }, 2048 | { 2049 | "Name": "Steven Murawski", 2050 | "Conference": "WinOps", 2051 | "From": "9/20/2017", 2052 | "To": "9/21/2017", 2053 | "Location": "London", 2054 | "Link": "https://www.winops.org/london/", 2055 | "Latitude": 51.5073509, 2056 | "Longitude": -0.1277583 2057 | }, 2058 | { 2059 | "Name": "Steven Murawski", 2060 | "Conference": "PSDay UK", 2061 | "From": "9/22/2017", 2062 | "To": "9/22/2017", 2063 | "Location": "London", 2064 | "Link": "https://psday.uk", 2065 | "Latitude": 51.5073509, 2066 | "Longitude": -0.1277583 2067 | }, 2068 | { 2069 | "Name": "Steven Murawski", 2070 | "Conference": "London PowerShell Meetup", 2071 | "From": "10/12/2017", 2072 | "To": "10/12/2017", 2073 | "Location": "London", 2074 | "Link": "https://www.meetup.com/PowerShell-London-UK/events/243680102/", 2075 | "Latitude": 51.5073509, 2076 | "Longitude": -0.1277583 2077 | }, 2078 | { 2079 | "Name": "Steven Murawski", 2080 | "Conference": "DevOpsDays - Singapore", 2081 | "From": "10/24/2017", 2082 | "To": "10/26/2017", 2083 | "Location": "Singapore", 2084 | "Link": "https://www.devopsdays.org/events/2017-singapore/", 2085 | "Latitude": 1.352083, 2086 | "Longitude": 103.819836 2087 | }, 2088 | { 2089 | "Name": "Steven Murawski", 2090 | "Conference": "PowerShell Conference Asia", 2091 | "From": "10/26/2017", 2092 | "To": "10/28/2017", 2093 | "Location": "Singapore", 2094 | "Link": "http://psconf.asia/", 2095 | "Latitude": 1.352083, 2096 | "Longitude": 103.819836 2097 | }, 2098 | { 2099 | "Name": "Steven Murawski", 2100 | "Conference": "Chef Summit", 2101 | "From": "10/10/2017", 2102 | "To": "10/11/2017", 2103 | "Location": "London", 2104 | "Link": "https://www.chef.io/summits/", 2105 | "Latitude": 51.5073509, 2106 | "Longitude": -0.1277583 2107 | } 2108 | ] -------------------------------------------------------------------------------- /components/MoreInfo.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | 29 | -------------------------------------------------------------------------------- /components/SpeakingGlobe.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 66 | 67 | -------------------------------------------------------------------------------- /components/SpeakingTable.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 41 | 42 | -------------------------------------------------------------------------------- /deploy.cmd: -------------------------------------------------------------------------------- 1 | @if "%SCM_TRACE_LEVEL%" NEQ "4" @echo off 2 | 3 | :: ---------------------- 4 | :: KUDU Deployment Script 5 | :: Version: 1.0.8 6 | :: ---------------------- 7 | 8 | :: Prerequisites 9 | :: ------------- 10 | 11 | :: Verify node.js installed 12 | where node 2>nul >nul 13 | IF %ERRORLEVEL% NEQ 0 ( 14 | echo Missing node.js executable, please install node.js, if already installed make sure it can be reached from current environment. 15 | goto error 16 | ) 17 | 18 | :: Setup 19 | :: ----- 20 | 21 | setlocal enabledelayedexpansion 22 | 23 | SET ARTIFACTS=%~dp0%..\artifacts 24 | 25 | IF NOT DEFINED DEPLOYMENT_SOURCE ( 26 | SET DEPLOYMENT_SOURCE=%~dp0%. 27 | ) 28 | 29 | IF NOT DEFINED DEPLOYMENT_TARGET ( 30 | SET DEPLOYMENT_TARGET=%ARTIFACTS%\wwwroot 31 | ) 32 | 33 | IF NOT DEFINED NEXT_MANIFEST_PATH ( 34 | SET NEXT_MANIFEST_PATH=%ARTIFACTS%\manifest 35 | 36 | IF NOT DEFINED PREVIOUS_MANIFEST_PATH ( 37 | SET PREVIOUS_MANIFEST_PATH=%ARTIFACTS%\manifest 38 | ) 39 | ) 40 | 41 | IF NOT DEFINED KUDU_SYNC_CMD ( 42 | :: Install kudu sync 43 | echo Installing Kudu Sync 44 | call npm install kudusync -g --silent 45 | IF !ERRORLEVEL! NEQ 0 goto error 46 | 47 | :: Locally just running "kuduSync" would also work 48 | SET KUDU_SYNC_CMD=%appdata%\npm\kuduSync.cmd 49 | ) 50 | goto Deployment 51 | 52 | :: Utility Functions 53 | :: ----------------- 54 | 55 | :SelectNodeVersion 56 | 57 | IF DEFINED KUDU_SELECT_NODE_VERSION_CMD ( 58 | :: The following are done only on Windows Azure Websites environment 59 | call %KUDU_SELECT_NODE_VERSION_CMD% "%DEPLOYMENT_SOURCE%" "%DEPLOYMENT_TARGET%" "%DEPLOYMENT_TEMP%" 60 | IF !ERRORLEVEL! NEQ 0 goto error 61 | 62 | IF EXIST "%DEPLOYMENT_TEMP%\__nodeVersion.tmp" ( 63 | SET /p NODE_EXE=<"%DEPLOYMENT_TEMP%\__nodeVersion.tmp" 64 | IF !ERRORLEVEL! NEQ 0 goto error 65 | ) 66 | 67 | IF EXIST "%DEPLOYMENT_TEMP%\__npmVersion.tmp" ( 68 | SET /p NPM_JS_PATH=<"%DEPLOYMENT_TEMP%\__npmVersion.tmp" 69 | IF !ERRORLEVEL! NEQ 0 goto error 70 | ) 71 | 72 | IF NOT DEFINED NODE_EXE ( 73 | SET NODE_EXE=node 74 | ) 75 | 76 | SET NPM_CMD="!NODE_EXE!" "!NPM_JS_PATH!" 77 | ) ELSE ( 78 | SET NPM_CMD=npm 79 | SET NODE_EXE=node 80 | ) 81 | 82 | goto :EOF 83 | 84 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 85 | :: Deployment 86 | :: ---------- 87 | 88 | :Deployment 89 | echo Handling node.js deployment. 90 | 91 | :: 1. KuduSync 92 | IF /I "%IN_PLACE_DEPLOYMENT%" NEQ "1" ( 93 | call :ExecuteCmd "%KUDU_SYNC_CMD%" -v 50 -f "%DEPLOYMENT_SOURCE%" -t "%DEPLOYMENT_TARGET%" -n "%NEXT_MANIFEST_PATH%" -p "%PREVIOUS_MANIFEST_PATH%" -i ".git;.hg;.deployment;deploy.cmd" 94 | IF !ERRORLEVEL! NEQ 0 goto error 95 | ) 96 | 97 | :: 2. Select node version 98 | call :SelectNodeVersion 99 | 100 | :: 3. Install npm packages 101 | IF EXIST "%DEPLOYMENT_TARGET%\package.json" ( 102 | pushd "%DEPLOYMENT_TARGET%" 103 | call :ExecuteCmd !NPM_CMD! install 104 | IF !ERRORLEVEL! NEQ 0 goto error 105 | call :ExecuteCmd !NPM_CMD! run generate 106 | IF !ERRORLEVEL! NEQ 0 goto error 107 | popd 108 | ) 109 | 110 | :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 111 | goto end 112 | 113 | :: Execute command routine that will echo out when error 114 | :ExecuteCmd 115 | setlocal 116 | set _CMD_=%* 117 | call %_CMD_% 118 | if "%ERRORLEVEL%" NEQ "0" echo Failed exitCode=%ERRORLEVEL%, command=%_CMD_% 119 | exit /b %ERRORLEVEL% 120 | 121 | :error 122 | endlocal 123 | echo An error has occurred during web site deployment. 124 | call :exitSetErrorLevel 125 | call :exitFromFunction 2>nul 126 | 127 | :exitSetErrorLevel 128 | exit /b 1 129 | 130 | :exitFromFunction 131 | () 132 | 133 | :end 134 | endlocal 135 | echo Finished successfully. 136 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/cda-locale/5f93013356238170b2ecb911790adeb135f1f0e6/favicon.ico -------------------------------------------------------------------------------- /geo-lookup.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | 3 | let gContext; 4 | 5 | // Github configuration is read from process.env 6 | let GH_USER = process.env.GH_USER; 7 | let GH_KEY = process.env.GH_KEY; 8 | let GH_REPO = process.env.GH_REPO; 9 | let GH_FILE = process.env.GH_FILE; 10 | 11 | // Per-run cache of geographical locations, to avoid hitting Maps API unnecessarily 12 | let GEO_CACHE = {}; 13 | 14 | // ENTRY POINT - retrieve data file from Github and decode/parse it before passing 15 | // into getGeo (which iterates over each entry) 16 | module.exports = function(context, data) { 17 | // Make the context available globally 18 | gContext = context; 19 | 20 | // Do the work! 21 | getGithubJson(githubFilename(), (data, err) => { 22 | if (!err) { 23 | // No error; base64 decode and JSON parse the data from the Github response 24 | // (see https://developer.github.com/v3/repos/contents/#get-contents) 25 | let content = JSON.parse( 26 | new Buffer(data.content, 'base64').toString('ascii') 27 | ); 28 | 29 | // Retrieve the geo information for each item in the original data 30 | getGeo(makeIterator(content), (updatedContent, err) => { 31 | if (!err) { 32 | // Everything went well- go ahead and push back up to Github. Note that 33 | // we need to base64 encode the JSON to embed it into the PUT (dear god, why) 34 | let updatedContentB64 = new Buffer( 35 | JSON.stringify(updatedContent, null, 2) 36 | ).toString('base64'); 37 | let pushData = { 38 | path: GH_FILE, 39 | message: 'Looked up locations, beep boop.', 40 | content: updatedContentB64, 41 | sha: data.sha 42 | }; 43 | putGithubJson(githubFilename(), pushData, err => { 44 | context.log('All done!'); 45 | context.done(); 46 | }); 47 | } else { 48 | gContext.log('All done with get Geo error: ' + err); 49 | context.done(); 50 | } 51 | }); 52 | } else { 53 | gContext.log('All done with error: ' + err); 54 | context.done(); 55 | } 56 | }); 57 | }; 58 | 59 | // Given an array of entries (wrapped in an iterator), walk over each of them and populate 60 | // the lat/long, using Google Maps API. Note that we also cache locations to try and save 61 | // some API calls 62 | function getGeo(itr, cb) { 63 | let curr = itr.next(); 64 | if (curr.done) { 65 | // All done processing- pass the (now-populated) entries to the next callback 66 | cb(curr.data); 67 | return; 68 | } 69 | 70 | let location = curr.value.Location; 71 | 72 | // Check the cache to see if we've already looked up this location 73 | if (location in GEO_CACHE) { 74 | gContext.log( 75 | 'Cached ' + 76 | location + 77 | ' -> ' + 78 | GEO_CACHE[location].lat + 79 | ' ' + 80 | GEO_CACHE[location].long 81 | ); 82 | curr.value.Latitude = GEO_CACHE[location].lat; 83 | curr.value.Longitude = GEO_CACHE[location].long; 84 | getGeo(itr, cb); 85 | return; 86 | } 87 | 88 | // Nothing found in cache; do a lookup and cache the result 89 | getGoogleJson(location, (data, err) => { 90 | if (err) { 91 | gContext.log('Error on ' + location + ' :' + err); 92 | } else { 93 | if (data.results.length > 0) { 94 | let info = { 95 | lat: data.results[0].geometry.location.lat, 96 | long: data.results[0].geometry.location.lng 97 | }; 98 | GEO_CACHE[location] = info; 99 | curr.value.Latitude = info.lat; 100 | curr.value.Longitude = info.long; 101 | gContext.log(location + ' -> ' + info.lat + ' ' + info.long); 102 | } else { 103 | gContext.log( 104 | "Didn't find anything for " + location + ' ::' + JSON.stringify(data) 105 | ); 106 | } 107 | } 108 | setTimeout(() => getGeo(itr, cb), 1000); 109 | }); 110 | } 111 | 112 | function makeIterator(data) { 113 | var i = 0; 114 | return { 115 | next: function() { 116 | return i < data.length 117 | ? { 118 | value: data[i++], 119 | done: false 120 | } 121 | : { 122 | data: data, 123 | done: true 124 | }; 125 | } 126 | }; 127 | } 128 | 129 | function getGoogleJson(location, cb) { 130 | makeJsonReq( 131 | { 132 | hostname: 'maps.googleapis.com', 133 | path: '/maps/api/geocode/json?address=' + encodeURIComponent(location), 134 | method: 'GET' 135 | }, 136 | undefined, 137 | cb 138 | ); 139 | } 140 | 141 | function getGithubJson(path, cb) { 142 | makeJsonReq( 143 | { 144 | hostname: 'api.github.com', 145 | path: path, 146 | method: 'GET', 147 | auth: GH_USER + ':' + GH_KEY, 148 | headers: { 149 | 'User-Agent': +GH_USER 150 | } 151 | }, 152 | undefined, 153 | cb 154 | ); 155 | } 156 | 157 | function putGithubJson(path, putObj, cb) { 158 | makeJsonReq( 159 | { 160 | hostname: 'api.github.com', 161 | path: path, 162 | method: 'PUT', 163 | auth: GH_USER + ':' + GH_KEY, 164 | headers: { 165 | 'User-Agent': +GH_USER 166 | } 167 | }, 168 | JSON.stringify(putObj), 169 | cb 170 | ); 171 | } 172 | 173 | function makeJsonReq(opts, body, cb) { 174 | let req = https.request(opts, res => { 175 | if (res.statusCode !== 200) { 176 | let err = new Error( 177 | 'Request ' + 178 | opts.hostname + 179 | ' ' + 180 | opts.path + 181 | ' failed:' + 182 | res.statusCode + 183 | '\n\t:' + 184 | JSON.stringify(res.headers) 185 | ); 186 | res.resume(); 187 | cb(undefined, err); 188 | return; 189 | } 190 | 191 | res.setEncoding('utf8'); 192 | 193 | let data = ''; 194 | res.on('data', chunk => (data += chunk)); 195 | res.on('end', () => { 196 | cb(JSON.parse(data)); 197 | }); 198 | }); 199 | 200 | req.on('error', e => { 201 | gContext.log('Error in makeJsonReq: ' + e); 202 | }); 203 | 204 | // If a body is provided, include it 205 | if (body != undefined) { 206 | req.write(body); 207 | } 208 | req.end(); 209 | } 210 | 211 | function githubFilename() { 212 | return '/repos/' + GH_USER + '/' + GH_REPO + '/contents/' + GH_FILE; 213 | } 214 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 55 | -------------------------------------------------------------------------------- /mixins/createGlobe.js: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | 3 | export const createGlobe = { 4 | methods: { 5 | initGlobe(imageLoad) { 6 | const DAT = DAT || {}; 7 | 8 | DAT.Globe = function(container, opts) { 9 | opts = opts || {}; 10 | 11 | const colorFn = 12 | opts.colorFn || 13 | function(x) { 14 | let c = new THREE.Color(); 15 | c.setHSL(0.1 - x * 0.19, 1.0, 0.6); 16 | return c; 17 | }; 18 | 19 | const Shaders = { 20 | earth: { 21 | uniforms: { 22 | texture: { type: 't', value: null } 23 | }, 24 | vertexShader: [ 25 | 'varying vec3 vNormal;', 26 | 'varying vec2 vUv;', 27 | 'void main() {', 28 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 29 | 'vNormal = normalize( normalMatrix * normal );', 30 | 'vUv = uv;', 31 | '}' 32 | ].join('\n'), 33 | fragmentShader: [ 34 | 'uniform sampler2D texture;', 35 | 'varying vec3 vNormal;', 36 | 'varying vec2 vUv;', 37 | 'void main() {', 38 | 'vec3 diffuse = texture2D( texture, vUv ).xyz;', 39 | 'float intensity = 1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) );', 40 | 'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * pow( intensity, 3.0 );', 41 | 'gl_FragColor = vec4( diffuse + atmosphere, 1.0 );', 42 | '}' 43 | ].join('\n') 44 | }, 45 | atmosphere: { 46 | uniforms: {}, 47 | vertexShader: [ 48 | 'varying vec3 vNormal;', 49 | 'void main() {', 50 | 'vNormal = normalize( normalMatrix * normal );', 51 | 'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );', 52 | '}' 53 | ].join('\n'), 54 | fragmentShader: [ 55 | 'varying vec3 vNormal;', 56 | 'void main() {', 57 | 'float intensity = pow( 0.8 - dot( vNormal, vec3( 0, 0, 1.0 ) ), 12.0 );', 58 | 'gl_FragColor = vec4( 1.0, 1.0, 1.0, 1.0 ) * intensity;', 59 | '}' 60 | ].join('\n') 61 | } 62 | }; 63 | 64 | let camera, 65 | scene, 66 | renderer, 67 | w, 68 | h, 69 | mesh, 70 | atmosphere, 71 | point, 72 | overRenderer; 73 | 74 | let curZoomSpeed = 0, 75 | zoomSpeed = 50, 76 | mouse = { x: 0, y: 0 }, 77 | mouseOnDown = { x: 0, y: 0 }, 78 | rotation = { x: 0, y: 0 }, 79 | target = { x: Math.PI * 3 / 2, y: Math.PI / 6.0 }, 80 | targetOnDown = { x: 0, y: 0 }, 81 | distance = 100000, 82 | distanceTarget = 100000, 83 | padding = 40, 84 | PI_HALF = Math.PI / 2; 85 | 86 | function init() { 87 | var shader, uniforms, material; 88 | w = container.offsetWidth || window.innerWidth / 2; 89 | h = container.offsetHeight || window.innerHeight / 1.5; 90 | 91 | camera = new THREE.PerspectiveCamera(30, w / h, 1, 10000); 92 | camera.position.z = distance; 93 | 94 | scene = new THREE.Scene(); 95 | 96 | var geometry = new THREE.SphereGeometry(200, 40, 30); 97 | 98 | shader = Shaders['earth']; 99 | uniforms = THREE.UniformsUtils.clone(shader.uniforms); 100 | uniforms['texture'].value = imageLoad; 101 | 102 | material = new THREE.ShaderMaterial({ 103 | uniforms: uniforms, 104 | vertexShader: shader.vertexShader, 105 | fragmentShader: shader.fragmentShader 106 | }); 107 | 108 | mesh = new THREE.Mesh(geometry, material); 109 | mesh.rotation.y = Math.PI; 110 | scene.add(mesh); 111 | 112 | shader = Shaders['atmosphere']; 113 | uniforms = THREE.UniformsUtils.clone(shader.uniforms); 114 | 115 | material = new THREE.ShaderMaterial({ 116 | uniforms: uniforms, 117 | vertexShader: shader.vertexShader, 118 | fragmentShader: shader.fragmentShader, 119 | side: THREE.BackSide, 120 | blending: THREE.AdditiveBlending, 121 | transparent: true 122 | }); 123 | 124 | mesh = new THREE.Mesh(geometry, material); 125 | mesh.scale.set(1.1, 1.1, 1.1); 126 | scene.add(mesh); 127 | 128 | geometry = new THREE.BoxGeometry(0.75, 0.75, 1); 129 | geometry.applyMatrix(new THREE.Matrix4().makeTranslation(0, 0, -0.5)); 130 | 131 | point = new THREE.Mesh(geometry); 132 | 133 | renderer = new THREE.WebGLRenderer({ antialias: true }); 134 | renderer.setSize(w, h); 135 | 136 | renderer.domElement.style.position = 'absolute'; 137 | container.appendChild(renderer.domElement); 138 | container.addEventListener('mousedown', onMouseDown, false); 139 | container.addEventListener('mousewheel', onMouseWheel, false); 140 | document.addEventListener('keydown', onDocumentKeyDown, false); 141 | window.addEventListener('resize', onWindowResize, false); 142 | 143 | container.addEventListener( 144 | 'mouseover', 145 | function() { 146 | overRenderer = true; 147 | }, 148 | false 149 | ); 150 | 151 | container.addEventListener( 152 | 'mouseout', 153 | function() { 154 | overRenderer = false; 155 | }, 156 | false 157 | ); 158 | } 159 | 160 | function addData(data, opts) { 161 | var lat, lng, size, color, i, step, colorFnWrapper; 162 | 163 | opts.animated = opts.animated || false; 164 | this.is_animated = opts.animated; 165 | opts.format = opts.format || 'magnitude'; // other option is 'legend' 166 | if (opts.format === 'magnitude') { 167 | step = 3; 168 | colorFnWrapper = function(data, i) { 169 | return colorFn(data[i + 2]); 170 | }; 171 | } else if (opts.format === 'legend') { 172 | step = 4; 173 | colorFnWrapper = function(data, i) { 174 | return colorFn(data[i + 3]); 175 | }; 176 | } else { 177 | throw 'error: format not supported: ' + opts.format; 178 | } 179 | 180 | if (opts.animated) { 181 | if (this._baseGeometry === undefined) { 182 | this._baseGeometry = new THREE.Geometry(); 183 | for (i = 0; i < data.length; i += step) { 184 | lat = data[i]; 185 | lng = data[i + 1]; 186 | color = colorFnWrapper(data, i); 187 | size = 0; 188 | addPoint(lat, lng, size, color, this._baseGeometry); 189 | } 190 | } 191 | if (this._morphTargetId === undefined) { 192 | this._morphTargetId = 0; 193 | } else { 194 | this._morphTargetId += 1; 195 | } 196 | opts.name = opts.name || 'morphTarget' + this._morphTargetId; 197 | } 198 | var subgeo = new THREE.Geometry(); 199 | for (i = 0; i < data.length; i += step) { 200 | lat = data[i]; 201 | lng = data[i + 1]; 202 | color = colorFnWrapper(data, i); 203 | size = data[i + 2]; 204 | size = size * 200; 205 | addPoint(lat, lng, size, color, subgeo); 206 | } 207 | if (opts.animated) { 208 | this._baseGeometry.morphTargets.push({ 209 | name: opts.name, 210 | vertices: subgeo.vertices 211 | }); 212 | } else { 213 | this._baseGeometry = subgeo; 214 | } 215 | } 216 | 217 | function createPoints() { 218 | if (this._baseGeometry !== undefined) { 219 | if (this.is_animated === false) { 220 | this.points = new THREE.Mesh( 221 | this._baseGeometry, 222 | new THREE.MeshBasicMaterial({ 223 | color: 0xffffff, 224 | vertexColors: THREE.FaceColors, 225 | morphTargets: false 226 | }) 227 | ); 228 | } else { 229 | if (this._baseGeometry.morphTargets.length < 8) { 230 | var padding = 8 - this._baseGeometry.morphTargets.length; 231 | for (var i = 0; i <= padding; i++) { 232 | this._baseGeometry.morphTargets.push({ 233 | name: 'morphPadding' + i, 234 | vertices: this._baseGeometry.vertices 235 | }); 236 | } 237 | } 238 | this.points = new THREE.Mesh( 239 | this._baseGeometry, 240 | new THREE.MeshBasicMaterial({ 241 | color: 0xffffff, 242 | vertexColors: THREE.FaceColors, 243 | morphTargets: true 244 | }) 245 | ); 246 | } 247 | scene.add(this.points); 248 | } 249 | } 250 | 251 | function addPoint(lat, lng, size, color, subgeo) { 252 | let phi = (90 - lat) * Math.PI / 180; 253 | let theta = (180 - lng) * Math.PI / 180; 254 | 255 | point.position.x = 200 * Math.sin(phi) * Math.cos(theta); 256 | point.position.y = 200 * Math.cos(phi); 257 | point.position.z = 200 * Math.sin(phi) * Math.sin(theta); 258 | 259 | point.lookAt(mesh.position); 260 | 261 | point.scale.z = Math.max(size, 0.1); // avoid non-invertible matrix 262 | point.updateMatrix(); 263 | 264 | for (let i = 0; i < point.geometry.faces.length; i++) { 265 | point.geometry.faces[i].color = color; 266 | } 267 | if (point.matrixAutoUpdate) { 268 | point.updateMatrix(); 269 | } 270 | subgeo.merge(point.geometry, point.matrix); 271 | } 272 | 273 | function onMouseDown(event) { 274 | event.preventDefault(); 275 | 276 | container.addEventListener('mousemove', onMouseMove, false); 277 | container.addEventListener('mouseup', onMouseUp, false); 278 | container.addEventListener('mouseout', onMouseOut, false); 279 | 280 | mouseOnDown.x = -event.clientX; 281 | mouseOnDown.y = event.clientY; 282 | 283 | targetOnDown.x = target.x; 284 | targetOnDown.y = target.y; 285 | 286 | container.style.cursor = 'move'; 287 | } 288 | 289 | function onMouseMove(event) { 290 | mouse.x = -event.clientX; 291 | mouse.y = event.clientY; 292 | 293 | var zoomDamp = distance / 1000; 294 | 295 | target.x = 296 | targetOnDown.x + (mouse.x - mouseOnDown.x) * 0.005 * zoomDamp; 297 | target.y = 298 | targetOnDown.y + (mouse.y - mouseOnDown.y) * 0.005 * zoomDamp; 299 | 300 | target.y = target.y > PI_HALF ? PI_HALF : target.y; 301 | target.y = target.y < -PI_HALF ? -PI_HALF : target.y; 302 | } 303 | 304 | function onMouseUp(event) { 305 | container.removeEventListener('mousemove', onMouseMove, false); 306 | container.removeEventListener('mouseup', onMouseUp, false); 307 | container.removeEventListener('mouseout', onMouseOut, false); 308 | container.style.cursor = 'auto'; 309 | } 310 | 311 | function onMouseOut(event) { 312 | container.removeEventListener('mousemove', onMouseMove, false); 313 | container.removeEventListener('mouseup', onMouseUp, false); 314 | container.removeEventListener('mouseout', onMouseOut, false); 315 | } 316 | 317 | function onMouseWheel(event) { 318 | event.preventDefault(); 319 | if (overRenderer) { 320 | zoom(event.wheelDeltaY * 0.3); 321 | } 322 | return false; 323 | } 324 | 325 | function onDocumentKeyDown(event) { 326 | switch (event.keyCode) { 327 | case 38: 328 | zoom(100); 329 | event.preventDefault(); 330 | break; 331 | case 40: 332 | zoom(-100); 333 | event.preventDefault(); 334 | break; 335 | } 336 | } 337 | 338 | function onWindowResize(event) { 339 | camera.aspect = container.offsetWidth / container.offsetHeight; 340 | camera.updateProjectionMatrix(); 341 | renderer.setSize(container.offsetWidth, container.offsetHeight); 342 | } 343 | 344 | function zoom(delta) { 345 | distanceTarget -= delta; 346 | distanceTarget = distanceTarget > 1000 ? 1000 : distanceTarget; 347 | distanceTarget = distanceTarget < 350 ? 350 : distanceTarget; 348 | } 349 | 350 | function animate() { 351 | requestAnimationFrame(animate); 352 | render(); 353 | } 354 | 355 | function render() { 356 | zoom(curZoomSpeed); 357 | 358 | rotation.x += (target.x - rotation.x) * 0.1; 359 | rotation.y += (target.y - rotation.y) * 0.1; 360 | distance += (distanceTarget - distance) * 0.3; 361 | 362 | camera.position.x = 363 | distance * Math.sin(rotation.x) * Math.cos(rotation.y); 364 | camera.position.y = distance * Math.sin(rotation.y); 365 | camera.position.z = 366 | distance * Math.cos(rotation.x) * Math.cos(rotation.y); 367 | 368 | camera.lookAt(mesh.position); 369 | renderer.render(scene, camera); 370 | } 371 | 372 | init(); 373 | this.animate = animate; 374 | 375 | this.__defineGetter__('time', function() { 376 | return this._time || 0; 377 | }); 378 | 379 | this.__defineSetter__('time', function(t) { 380 | var validMorphs = []; 381 | var morphDict = this.points.morphTargetDictionary; 382 | for (var k in morphDict) { 383 | if (k.indexOf('morphPadding') < 0) { 384 | validMorphs.push(morphDict[k]); 385 | } 386 | } 387 | validMorphs.sort(); 388 | var l = validMorphs.length - 1; 389 | var scaledt = t * l + 1; 390 | var index = Math.floor(scaledt); 391 | for (i = 0; i < validMorphs.length; i++) { 392 | this.points.morphTargetInfluences[validMorphs[i]] = 0; 393 | } 394 | var lastIndex = index - 1; 395 | var leftover = scaledt - index; 396 | if (lastIndex >= 0) { 397 | this.points.morphTargetInfluences[lastIndex] = 1 - leftover; 398 | } 399 | this.points.morphTargetInfluences[index] = leftover; 400 | this._time = t; 401 | }); 402 | 403 | this.addData = addData; 404 | this.createPoints = createPoints; 405 | this.renderer = renderer; 406 | this.scene = scene; 407 | 408 | return this; 409 | }; 410 | 411 | // this particular implementation 412 | const container = document.getElementById('container'), 413 | globe = new DAT.Globe(container); 414 | 415 | let i, 416 | tweens = []; 417 | 418 | let data = this.teamsArr; 419 | window.data = data; 420 | 421 | for (i = 0; i < data[0].length; i++) { 422 | globe.addData(data[0][i], { format: 'magnitude', name: data[0][i] }); 423 | } 424 | 425 | // Create the geometry 426 | globe.createPoints(); 427 | 428 | // Begin animation 429 | globe.animate(); 430 | document.body.style.backgroundImage = 'none'; // remove loading 431 | } 432 | }, 433 | mounted() { 434 | //we have to load the texture when it's mounted and pass it in 435 | let earthmap = THREE.ImageUtils.loadTexture('/world7.jpg'); 436 | this.initGlobe(earthmap); 437 | } 438 | }; 439 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 3 | ** Headers of the page 4 | */ 5 | head: { 6 | title: 'cda-locale', 7 | meta: [ 8 | { charset: 'utf-8' }, 9 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 10 | { 11 | hid: 'description', 12 | name: 'description', 13 | content: 14 | 'Showing where all of the Microsoft Cloud Developer Advocates will be speaking' 15 | } 16 | ], 17 | link: [ 18 | { 19 | rel: 'icon', 20 | type: 'image/x-icon', 21 | href: '/favicon.ico' 22 | } 23 | ], 24 | link: [ 25 | { 26 | rel: 'stylesheet', 27 | href: 'https://fonts.googleapis.com/css?family=Open+Sans:300,400' 28 | } 29 | ] 30 | }, 31 | /* 32 | ** Customize the progress-bar color 33 | */ 34 | loading: { color: '#3B8070' }, 35 | /* 36 | ** Build configuration 37 | */ 38 | build: {} 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cda-locale", 3 | "version": "1.0.0", 4 | "description": 5 | "Showing where all of the MS Cloud Developer Advocates will be speaking", 6 | "author": "sdras ", 7 | "private": true, 8 | "scripts": { 9 | "dev": "nuxt", 10 | "build": "nuxt build", 11 | "generate": "nuxt generate", 12 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 13 | "precommit": "npm run lint" 14 | }, 15 | "dependencies": { 16 | "autoprefixer": "^7.1.4", 17 | "babel-preset-env": "^1.6.0", 18 | "nuxt": "^1.0.0-rc8", 19 | "pug": "^2.0.0-rc.4", 20 | "three": "^0.87.1", 21 | "vuex": "^2.4.0" 22 | }, 23 | "devDependencies": { 24 | "babel-eslint": "^7.2.3", 25 | "eslint": "^4.3.0", 26 | "eslint-config-standard": "^10.2.1", 27 | "eslint-loader": "^1.9.0", 28 | "eslint-plugin-html": "^3.1.1", 29 | "eslint-plugin-import": "^2.7.0", 30 | "eslint-plugin-node": "^5.1.1", 31 | "eslint-plugin-promise": "^3.5.0", 32 | "eslint-plugin-standard": "^3.0.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 59 | 60 | 147 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/cda-locale/5f93013356238170b2ecb911790adeb135f1f0e6/static/favicon.ico -------------------------------------------------------------------------------- /static/world7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sdras/cda-locale/5f93013356238170b2ecb911790adeb135f1f0e6/static/world7.jpg -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import Vuex from 'vuex'; 2 | import speakerData from './../assets/cda-data.json'; 3 | 4 | const createStore = () => { 5 | return new Vuex.Store({ 6 | state: { 7 | speakingColumns: ['Name', 'Conference', 'From', 'To', 'Location'], 8 | speakerData 9 | } 10 | }); 11 | }; 12 | 13 | export default createStore; 14 | -------------------------------------------------------------------------------- /web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 42 | 43 | 44 | 45 | --------------------------------------------------------------------------------