diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..94700a4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+node_modules
+.idea
+private
diff --git a/.meteor/.finished-upgraders b/.meteor/.finished-upgraders
new file mode 100644
index 0000000..910574c
--- /dev/null
+++ b/.meteor/.finished-upgraders
@@ -0,0 +1,17 @@
+# This file contains information which helps Meteor properly upgrade your
+# app when you run 'meteor update'. You should check it into version control
+# with your project.
+
+notices-for-0.9.0
+notices-for-0.9.1
+0.9.4-platform-file
+notices-for-facebook-graph-api-2
+1.2.0-standard-minifiers-package
+1.2.0-meteor-platform-split
+1.2.0-cordova-changes
+1.2.0-breaking-changes
+1.3.0-split-minifiers-package
+1.4.0-remove-old-dev-bundle-link
+1.4.1-add-shell-server-package
+1.4.3-split-account-service-packages
+1.5-add-dynamic-import-package
diff --git a/.meteor/.gitignore b/.meteor/.gitignore
new file mode 100644
index 0000000..4083037
--- /dev/null
+++ b/.meteor/.gitignore
@@ -0,0 +1 @@
+local
diff --git a/.meteor/.id b/.meteor/.id
new file mode 100644
index 0000000..9a3be4f
--- /dev/null
+++ b/.meteor/.id
@@ -0,0 +1,7 @@
+# This file contains a token that is unique to your project.
+# Check it into your repository along with the rest of this directory.
+# It can be used for purposes such as:
+# - ensuring you don't accidentally deploy one app on top of another
+# - providing package authors with aggregated statistics
+
+1v2pn7n1jbklfu1hg4tnm
diff --git a/.meteor/packages b/.meteor/packages
new file mode 100644
index 0000000..1f49c34
--- /dev/null
+++ b/.meteor/packages
@@ -0,0 +1,67 @@
+# Meteor packages used by this project, one per line.
+# Check this file (and the other files in this directory) into your repository.
+#
+# 'meteor add' and 'meteor remove' will edit this file for you,
+# but you can also edit it by hand.
+
+meteor-base@1.3.0 # Packages every Meteor app needs to have
+mobile-experience@1.0.5 # Packages for a great mobile UX
+mongo@1.4.2 # The database Meteor supports right now
+blaze-html-templates@1.0.4 # Compile .html files into Meteor Blaze views
+reactive-var@1.0.11 # Reactive variable for tracker
+reactive-dict@1.2.0 # ???
+tracker@1.1.3 # Meteor's client-side reactive programming library
+tomwasd:history-polyfill # Adds IE 8/9 support for HTML5 history.
+email@1.2.3 # Adds the Meteor/Email package for sending lost password emails
+
+standard-minifier-css@1.4.0 # CSS minifier run for production mode
+standard-minifier-js@2.3.1 # JS minifier run for production mode
+es5-shim@4.7.0 # ECMAScript 5 compatibility for older browsers.
+poorvavyas:es6-shim
+ecmascript@0.10.0 # Enable ECMAScript2015+ syntax in app code
+
+#accounts-ui
+#accounts-base
+accounts-password@1.5.0
+useraccounts:core
+useraccounts:unstyled
+useraccounts:flow-routing # Configures email flows. Used for AccountsTemplates class.
+alanning:roles # Adds roles to the user mix. https://atmospherejs.com/alanning/roles && https://github.com/alanning/meteor-roles/blob/master/examples/flow-router/
+
+kadira:flow-router
+arillo:flow-router-helpers # Provides various template helpers such as {{pathFor 'templateName'}}
+#tomwasd:flow-router-seo
+kadira:blaze-layout
+
+shell-server@0.3.1 # ???
+meteortoys:allthings
+session@1.1.7
+##browser-policy # Adds support for specifying browser level security rules related to content and what's allowed to laod on the page.
+check@1.3.0 # Allows for checking the structure and types of arguments passed to Meteor methods and publications.
+#audit-argument-checks # Used in combination with the Check package for checking the structure and types of arguments passed to Meteor methods and publications. Automatically alerts when a method or publication does not use a check() call.
+
+aldeed:simple-schema@1.5.3
+aldeed:collection2
+#matb33:collection-hooks # Allows the collections to register handlers that run before or after database interactions.
+#zimme:collection-softremovable
+
+#aldeed:autoform@5.8.1
+#aldeed:collection2-core@2.0.1
+#aldeed:schema-deny # Addon for Collection2-core to use denyInsert or denyUpdate options.
+#aldeed:schema-index # Addon for Collection2-core to use index or unique options.
+#skehoe1989:autoform-relations # Adds relations to autoform
+
+#twbs:bootstrap # Requires jquery 1.9-2.x, not 3+
+fortawesome:fontawesome
+momentjs:moment
+mizzao:bootboxjs # ???
+aldeed:template-extension
+juliancwirko:s-alert # Client error/alert handling
+jcbernack:reactive-aggregate # Allows us to create a new client collection (from the server) with the contents being an aggregate of server data. Note that aggregation can only be done on the server currently as mini-mongo does not support it.
+ostrio:logger
+ostrio:loggermongo
+dynamic-import@0.3.0
+markdown@1.0.12
+wcrisman:jquery-custom-scrollbar
+
+stylus@=2.513.14 # This package is no longer supported, but it still works and is available. It provides support for reading .styl files on the client and converting them to CSS on the fly. The alternative would be to compile the styl files to CSS on the server ahead of time.
diff --git a/.meteor/platforms b/.meteor/platforms
new file mode 100644
index 0000000..efeba1b
--- /dev/null
+++ b/.meteor/platforms
@@ -0,0 +1,2 @@
+server
+browser
diff --git a/.meteor/release b/.meteor/release
new file mode 100644
index 0000000..d502dc0
--- /dev/null
+++ b/.meteor/release
@@ -0,0 +1 @@
+METEOR@1.6.1
diff --git a/.meteor/versions b/.meteor/versions
new file mode 100644
index 0000000..0d16b67
--- /dev/null
+++ b/.meteor/versions
@@ -0,0 +1,140 @@
+accounts-base@1.4.2
+accounts-password@1.5.0
+alanning:roles@1.2.16
+aldeed:collection2@2.10.0
+aldeed:collection2-core@1.2.0
+aldeed:schema-deny@1.1.0
+aldeed:schema-index@1.1.1
+aldeed:simple-schema@1.5.3
+aldeed:template-extension@4.1.0
+allow-deny@1.1.0
+arillo:flow-router-helpers@0.5.2
+autoupdate@1.4.0
+babel-compiler@7.0.0
+babel-runtime@1.2.2
+base64@1.0.10
+binary-heap@1.0.10
+blaze@2.3.2
+blaze-html-templates@1.1.2
+blaze-tools@1.0.10
+boilerplate-generator@1.4.0
+caching-compiler@1.1.11
+caching-html-compiler@1.1.2
+callback-hook@1.1.0
+check@1.3.0
+coffeescript@1.0.17
+ddp@1.4.0
+ddp-client@2.3.1
+ddp-common@1.4.0
+ddp-rate-limiter@1.0.7
+ddp-server@2.1.2
+deps@1.0.12
+diff-sequence@1.1.0
+dynamic-import@0.3.0
+ecmascript@0.10.0
+ecmascript-runtime@0.5.0
+ecmascript-runtime-client@0.6.0
+ecmascript-runtime-server@0.5.0
+ejson@1.1.0
+email@1.2.3
+es5-shim@4.7.3
+fortawesome:fontawesome@4.7.0
+geojson-utils@1.0.10
+hot-code-push@1.0.4
+html-tools@1.0.11
+htmljs@1.0.11
+http@1.4.0
+id-map@1.1.0
+jcbernack:reactive-aggregate@0.7.0
+jquery@1.11.10
+juliancwirko:s-alert@3.2.0
+kadira:blaze-layout@2.3.0
+kadira:flow-router@2.12.1
+launch-screen@1.1.1
+livedata@1.0.18
+localstorage@1.2.0
+logging@1.1.19
+markdown@1.0.12
+mdg:validation-error@0.2.0
+meteor@1.8.2
+meteor-base@1.3.0
+meteorhacks:aggregate@1.3.0
+meteorhacks:collection-utils@1.2.0
+meteortoys:allthings@4.0.0
+meteortoys:authenticate@4.0.0
+meteortoys:autopub@4.0.0
+meteortoys:blueprint@4.0.0
+meteortoys:email@4.0.0
+meteortoys:hotreload@4.0.0
+meteortoys:listen@4.0.0
+meteortoys:method@4.0.0
+meteortoys:mobile@4.0.0
+meteortoys:pub@4.0.0
+meteortoys:result@4.0.0
+meteortoys:shell@4.0.0
+meteortoys:status@4.0.0
+meteortoys:sub@4.0.0
+meteortoys:throttle@4.0.0
+meteortoys:toggle@4.0.0
+meteortoys:toykit@4.0.1
+minifier-css@1.3.0
+minifier-js@2.3.1
+minimongo@1.4.3
+mizzao:bootboxjs@4.4.0
+mobile-experience@1.0.5
+mobile-status-bar@1.0.14
+modules@0.11.3
+modules-runtime@0.9.2
+momentjs:moment@2.20.1
+mongo@1.4.2
+mongo-dev-server@1.1.0
+mongo-id@1.0.6
+mongo-livedata@1.0.12
+msavin:jetsetter@4.0.0
+msavin:mongol@4.0.1
+npm-bcrypt@0.9.3
+npm-mongo@2.2.34
+observe-sequence@1.0.16
+ordered-dict@1.1.0
+ostrio:logger@2.0.6
+ostrio:loggermongo@2.0.3
+poorvavyas:es6-shim@0.21.1
+promise@0.10.1
+raix:eventemitter@0.1.3
+random@1.1.0
+rate-limit@1.0.8
+reactive-dict@1.2.0
+reactive-var@1.0.11
+reload@1.2.0
+retry@1.1.0
+routepolicy@1.0.12
+server-render@0.3.0
+service-configuration@1.0.11
+session@1.1.7
+sha@1.0.9
+shell-server@0.3.1
+shim-common@0.1.0
+socket-stream-client@0.1.0
+softwarerero:accounts-t9n@1.3.11
+spacebars@1.0.15
+spacebars-compiler@1.1.3
+srp@1.0.10
+standard-minifier-css@1.4.0
+standard-minifier-js@2.3.1
+stylus@2.513.14
+templating@1.3.2
+templating-compiler@1.3.3
+templating-runtime@1.3.2
+templating-tools@1.1.2
+tomwasd:history-polyfill@0.0.1
+tracker@1.1.3
+ui@1.0.13
+underscore@1.0.10
+url@1.2.0
+useraccounts:core@1.14.2
+useraccounts:flow-routing@1.14.2
+useraccounts:unstyled@1.14.2
+wcrisman:jquery-custom-scrollbar@3.0.0
+webapp@1.5.0
+webapp-hashing@1.0.9
+zimme:active-route@2.3.2
diff --git a/README IMPORT.md b/README IMPORT.md
new file mode 100644
index 0000000..05fc23e
--- /dev/null
+++ b/README IMPORT.md
@@ -0,0 +1,19 @@
+To import data, place the data in the /private folder, then write a script in the /server folder (javascript).
+
+The script file should define Meteor methods:
+```
+
+Meteor.methods({
+ "importSomethingOfMine": function() {
+ //Your script here.
+ }
+});
+```
+
+Then run meteor.
+
+Then at the terminal (in your WebStorm UI, or at a command line (at the project root path), run `meteor shell` to open a server side shell.
+
+Once in the shell, you can write javascript code that will be executed.
+
+Type `Meteor.call('importSomethingOfMine')` and hit the enter key and it will execute the method you defined earlier.
\ No newline at end of file
diff --git a/README.md b/README.md
index 1420b2b..56f1282 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,9 @@
-AVEF
+Clone this project as a separate project and rename it. In WebStorm, right click the project and rename the project.
+
+Next run "meteor" from the command line within WebStorm (or at the command line in the project outside a development environment). This will install what is needed and run the server.
+
+Finally, run a browser and direct it at localhost:3000 (this is linked in the console after starting the server).
+
+Run `meteor update` to get the latest version of meteor installed for this application. This can take a fair bit of time to finish.
+
+Run `meteor npm install --save xxxxx` to install npm packages.
\ No newline at end of file
diff --git a/client/bootstrap.styl b/client/bootstrap.styl
new file mode 100644
index 0000000..2fe409b
--- /dev/null
+++ b/client/bootstrap.styl
@@ -0,0 +1,61 @@
+textarea:focus,
+input[type="text"]:focus,
+input[type="password"]:focus,
+input[type="datetime"]:focus,
+input[type="datetime-local"]:focus,
+input[type="date"]:focus,
+input[type="month"]:focus,
+input[type="time"]:focus,
+input[type="week"]:focus,
+input[type="number"]:focus,
+input[type="email"]:focus,
+input[type="url"]:focus,
+input[type="search"]:focus,
+input[type="tel"]:focus,
+input[type="color"]:focus,
+.uneditable-input:focus,
+.list-group:focus
+ border-color: rgba(82, 168, 236, 0.8)
+ outline: 0
+ outline: thin dotted \9
+ /* IE6-9 */
+
+ /*
+ -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
+ -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
+ box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
+ */
+ -webkit-box-shadow: 0px 0px 46px -13px rgba(230,28,230,1) !important
+ -moz-box-shadow: 0px 0px 46px -13px rgba(230,28,230,1) !important
+ box-shadow: 0px 0px 46px -13px rgba(230,28,230,1) !important
+
+.form-control
+ font-size: 14px
+ margin-bottom: 0px
+
+.input-group
+ margin-bottom: 15px
+
+.select2 .select2-selection
+ border-color: #ccc
+
+.select2-container--default.select2-container--focus .select2-selection--multiple
+ border-color: rgba(101, 174, 231, 0.823529)
+ outline: 0
+ outline: thin dotted \9
+ -webkit-box-shadow: 0px 0px 46px -11px rgba(230,28,230,1) !important
+ -moz-box-shadow: 0px 0px 46px -11px rgba(230,28,230,1) !important
+ box-shadow: 0px 0px 46px -11px rgba(230,28,230,1) !important
+
+.has-error
+ border-color: #a94442 !important
+ /*
+ -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075) !important;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075) !important;
+ */
+
+ /*
+ -webkit-box-shadow: 0px 0px 46px -13px rgba(255,28,230,1) !important;
+ -moz-box-shadow: 0px 0px 46px -13px rgba(255,28,230,1) !important;
+ box-shadow: 0px 0px 46px -13px rgba(255,28,230,1) !important;
+ */
\ No newline at end of file
diff --git a/client/client.js b/client/client.js
new file mode 100644
index 0000000..0ebc439
--- /dev/null
+++ b/client/client.js
@@ -0,0 +1,64 @@
+import {Meteor} from 'meteor/meteor';
+import '/imports/startup/client';
+import '/imports/startup/both';
+import '/imports/api';
+import '/imports/ui/helpers.js';
+// import '/imports/util/normalize.css';
+import '/imports/util/validator.js';
+import '/imports/util/polyfills/blaze.js';
+import '/imports/util/polyfills/regex.js';
+import '/imports/util/polyfills/date.js';
+import '/imports/util/polyfills/array.js';
+import '/imports/util/de.combo.js';
+import '/imports/util/resize/ResizeSensor.js';
+import '/imports/util/resize/ElementQueries.js';
+import '/imports/ui/layouts/Public.js';
+import '/imports/ui/layouts/Admin.js';
+import '/imports/ui/layouts/Login.js';
+import '/imports/ui/accounts/accounts.js';
+import '/imports/util/select2/select2.css';
+import '/imports/util/select2/select2.full.js';
+//The SweetAlert2 NPM package is where this is being pulled from. The js file that actually wants to use it should import it (see Sales.js).
+import 'sweetalert2/dist/sweetalert2.min.css';
+import '/imports/util/simplegrid.css';
+import 'dragula/dist/dragula.css';
+//import 'malihu-custom-scrollbar-plugin/jquery.mCustomScrollbar.css';
+
+Blaze._allowJavascriptUrls();
+
+Meteor.subscribe("measures");
+Meteor.subscribe("venues");
+Meteor.subscribe("categories");
+Meteor.subscribe("subcategories");
+Meteor.subscribe("items");
+
+Meteor.startup(function () {
+ sAlert.config({
+ effect: '',
+ position: 'bottom-right',
+ timeout: 5000,
+ html: false,
+ onRouteClose: true,
+ stack: true,
+ // or you can pass an object:
+ // stack: {
+ // spacing: 10 // in px
+ // limit: 3 // when fourth alert appears all previous ones are cleared
+ // }
+ offset: 0, // in px - will be added to first alert (bottom or top - depends of the position in config)
+ beep: false,
+ // examples:
+ // beep: '/beep.mp3' // or you can pass an object:
+ // beep: {
+ // info: '/beep-info.mp3',
+ // error: '/beep-error.mp3',
+ // success: '/beep-success.mp3',
+ // warning: '/beep-warning.mp3'
+ // }
+ onClose: _.noop //
+ // examples:
+ // onClose: function() {
+ // /* Code here will be executed once the alert closes. */
+ // }
+ });
+});
\ No newline at end of file
diff --git a/client/head.html b/client/head.html
new file mode 100644
index 0000000..cc91107
--- /dev/null
+++ b/client/head.html
@@ -0,0 +1,6 @@
+
+
\ No newline at end of file
diff --git a/imports/ui/AdminHome.import.styl b/imports/ui/AdminHome.import.styl
new file mode 100644
index 0000000..2c4c1dc
--- /dev/null
+++ b/imports/ui/AdminHome.import.styl
@@ -0,0 +1,2 @@
+#adminHome
+ margin: 20px 40px
\ No newline at end of file
diff --git a/imports/ui/AdminHome.js b/imports/ui/AdminHome.js
new file mode 100644
index 0000000..4020bc1
--- /dev/null
+++ b/imports/ui/AdminHome.js
@@ -0,0 +1,2 @@
+
+import './AdminHome.html';
\ No newline at end of file
diff --git a/imports/ui/Home.html b/imports/ui/Home.html
new file mode 100644
index 0000000..00a4f22
--- /dev/null
+++ b/imports/ui/Home.html
@@ -0,0 +1,5 @@
+
+
+ {{#markdown}}This is a test of __markdown__.{{/markdown}}
+
+
\ No newline at end of file
diff --git a/imports/ui/Home.import.styl b/imports/ui/Home.import.styl
new file mode 100644
index 0000000..0e9a5ec
--- /dev/null
+++ b/imports/ui/Home.import.styl
@@ -0,0 +1,3 @@
+#homePage
+ display: block
+ min-height: 300px
\ No newline at end of file
diff --git a/imports/ui/Home.js b/imports/ui/Home.js
new file mode 100644
index 0000000..ba3b14c
--- /dev/null
+++ b/imports/ui/Home.js
@@ -0,0 +1,2 @@
+
+import './Home.html';
\ No newline at end of file
diff --git a/imports/ui/UserManagement.html b/imports/ui/UserManagement.html
new file mode 100644
index 0000000..403205d
--- /dev/null
+++ b/imports/ui/UserManagement.html
@@ -0,0 +1,76 @@
+
+
+
diff --git a/imports/ui/accounts/accounts.js b/imports/ui/accounts/accounts.js
new file mode 100644
index 0000000..6ee7f20
--- /dev/null
+++ b/imports/ui/accounts/accounts.js
@@ -0,0 +1,18 @@
+import { Template } from 'meteor/templating';
+
+import './accounts.html';
+
+// Simply 'inherits' helpers from AccountsTemplates
+Template.OverrideAtForm.helpers(AccountsTemplates.atFormHelpers);
+
+// Simply 'inherits' helpers and events from AccountsTemplates
+Template.OverrideAtPwdForm.helpers(AccountsTemplates.atPwdFormHelpers);
+Template.OverrideAtPwdForm.events(AccountsTemplates.atPwdFormEvents);
+
+// We identified the templates that need to be overridden by looking at the available templates
+// here: https://github.com/meteor-useraccounts/unstyled/tree/master/lib
+// Template['override-atPwdFormBtn'].replaces('atPwdFormBtn');
+// Template['override-atPwdForm'].replaces('atPwdForm');
+// Template['override-atTextInput'].replaces('atTextInput');
+// Template['override-atTitle'].replaces('atTitle');
+// Template['override-atError'].replaces('atError');
\ No newline at end of file
diff --git a/imports/ui/helpers.js b/imports/ui/helpers.js
new file mode 100644
index 0000000..40f6bfc
--- /dev/null
+++ b/imports/ui/helpers.js
@@ -0,0 +1,8 @@
+
+// General use helpers - available to all views.
+
+UI.registerHelper('currentUserName', function() {
+ if(Meteor.user()){
+ return Meteor.user().emails[0].address;
+ }
+});
\ No newline at end of file
diff --git a/imports/ui/layouts/Admin.html b/imports/ui/layouts/Admin.html
new file mode 100644
index 0000000..0f4e6f8
--- /dev/null
+++ b/imports/ui/layouts/Admin.html
@@ -0,0 +1,32 @@
+
+ {{> sAlert}}
+
', {role: 'menu', class: this.options.listClass});
+
+ //Ensure that if the hidden field exists and changes, that the hidden field's id matches the text in the input field. If not then the hidden id field was changed manually and externally and the text field should be updated.
+ if(this.$hidden) {
+ this.$hidden.on('change', hiddenInputChanged);
+ }
+
+ function hiddenInputChanged() {
+ let id = _this.$hidden.val();
+ let $li = _this.$list.children("[role!='node']");
+
+ for(let i = 0; i < $li.length; i++) {
+ let $next = $($li[i]);
+
+ if($next.data('model').id == id) {
+ if(_this.$input.val() != $next.text())
+ _this.$input.val($next.text());
+ }
+ }
+ }
+
+ //this.$list.appendTo($input.parent());
+ this.$list.appendTo(this.$listContainer);
+ //this.$listContainer.appendTo($input.parent());
+ this.$listContainer.prependTo(document.body); //Place the container at the top of the page with no height.
+
+ //Setup the list to highlight the item the user is hovering over, to select the item the user clicks, and to remove the hover styling when the list closes due to a selection being made.
+ this.$list
+ .on('mousemove', 'li', function() {
+ // _this.$list.find(_this.options.selectionClass).removeClass(_this.options.selectionClass);
+ let $this = $(this);
+
+ //Skip nodes.
+ while($this && $this.attr('role') == 'node') {
+ $this = $this.next();
+ }
+
+ //If we could find a non-node element then highlight it.
+ if($this) $this.addClass(_this.options.selectionClass).siblings().removeClass(_this.options.selectionClass);
+ })
+ .on('mousedown', 'li', function() {
+ let $this = $(this);
+
+ //Skip nodes.
+ while($this && $this.attr('role') == 'node') {
+ $this = $this.next();
+ }
+
+ //If we could find a non-node element then highlight it.
+ if($this) _this.select($this);
+ })
+ .on('mouseup', 'li', function() {
+ //Remove the selection highlighting.
+ _this.$list.children().removeClass(_this.options.selectionClass);
+ //Hide the list.
+ _this.hide();
+ });
+ //Setup the input field to handle opening the list when it receives focus, and close it when it loses focus.
+ this.$input
+ .on('focus', $.proxy(_this.focus, _this))
+ .on('blur', $.proxy(_this.blur, _this));
+ // this.$listContainer
+ // .on('focus', $.proxy(_this.focus, _this, "list container"))
+ // .on('blur', $.proxy(_this.blur, _this, "list container"));
+ // this.$list
+ // .on('focus', $.proxy(_this.focus, _this, "list"))
+ // .on('blur', $.proxy(_this.blur, _this, "list"));
+ //Handle key events on the input field. Up/down arrows should change the selection in the list. Enter should select an item and close the list. Tab and escape should hide the list before moving to the next focusable element on the page.
+ this.$input.on('input keydown', function(event) {
+ switch(event.keyCode) {
+ case 38: { //Up
+ let visibles = _this.$list.find('li.visible[role!="node"]');
+ let selected = visibles.index(visibles.filter('.' + _this.options.selectionClass)) || 0;
+ _this.highlight(selected - 1);
+ event.preventDefault();
+ break;
+ }
+ case 40: { //Down
+ let visibles = _this.$list.find('li.visible[role!="node"]');
+ let selected = visibles.index(visibles.filter('li.selected')) || 0;
+ _this.highlight(selected + 1);
+ event.preventDefault();
+ break;
+ }
+ case 13: //Enter
+ if(_this.$list.is(':visible')) {
+ _this.select(_this.$list.find('li.selected'));
+ event.preventDefault();
+ }
+ break;
+ case 9: //Tab
+ _this.select(_this.$list.find('li.selected'));
+ break;
+ case 27: //Esc
+ if(_this.$input.hasClass('open')) {
+ _this.hide();
+ //Try to stop any default behavior from occurring.
+ if(event.stopPropagation) event.stopPropagation();
+ else event.cancelBubble = true; //IE 6-8
+ event.preventDefault();
+ return false;
+ }
+ else {
+ return true;
+ }
+ default:
+ _this.filter();
+ _this.highlight(0);
+ break;
+ }
+ });
+
+ //
+ // Adds one or more elements to the control.
+ // data: The item or array of items to add. This will be the root tree elements if groupFunctions is provided.
+ function add(data) {
+ let groupFunctions = _this.options.groupFunctions;
+ let getClasses = _this.options.getClasses;
+
+ let addOne = function(data, parent) {
+ let text = _this.options.textAttr ? ($.isFunction(_this.options.textAttr) ? _this.options.textAttr(data) : data[_this.options.textAttr]) : data;
+ let li = $("
" + text + "
");
+
+ li.appendTo(_this.$list);
+ li.data('model', data);
+ if(parent) li.data('parent-li', parent);
+ };
+ let addOneGroup = function(data, text, children) {
+ let li = $("
" + text + "
");
+
+ li.appendTo(_this.$list);
+ li.data('model', data);
+
+ for(let childIndex = 0; childIndex < children.length; childIndex++) {
+ addOne(children[childIndex], li);
+ }
+ };
+ let addOneBranch = function(data) {
+ let parents = $.isFunction(groupFunctions.groupParents) ? groupFunctions.groupParents(data) : data;
+
+ //Since there may be one or more parents identified for each data element passed to us...
+ if(Array.isArray(parents)) {
+ for(let parentIndex = 0; parentIndex < parents.length; parentIndex++) {
+ addOneGroup(parents[parentIndex], groupFunctions.parentText(parents[parentIndex]), groupFunctions.children(parents[parentIndex]));
+ }
+ }
+ else {
+ addOneGroup(parents, groupFunctions.parentText(parents), groupFunctions.children(parents));
+ }
+ };
+
+ if(groupFunctions instanceof Object && $.isFunction(groupFunctions.children) && $.isFunction(groupFunctions.parentText)) {
+ if(Array.isArray(data)) {
+ for(let dataIndex = 0; dataIndex < data.length; dataIndex++) {
+ addOneBranch(data[dataIndex]);
+ }
+ }
+ else {
+ addOneBranch(data);
+ }
+ }
+ else {
+ if(Array.isArray(data)) {
+ for(let dataIndex = 0; dataIndex < data.length; dataIndex++) {
+ addOne(data[dataIndex]);
+ }
+ }
+ else {
+ addOne(data);
+ }
+ }
+
+ //Filter the set of elements so that only those matching the text in the input field are marked as visible.
+ _this.filter();
+ }
+
+ Tracker.autorun(function() {
+ this.$list.empty();
+ if(options.cursor) {
+ //Add the initial set of data.
+ add(options.cursor.fetch());
+ }
+ else if(options.set) {
+ for(let i = 0; i < options.set.length; i++) {
+ add(options.set[i]);
+ }
+ }
+ }.bind(this));
+
+ //Check the hidden input field for an ID, and setup the selection based in it if there is one.
+ if(this.$hidden && _this.$hidden.val()) {
+ hiddenInputChanged();
+ }
+
+ //TODO: Should probably check to ensure comparator is a function? Not that it will help much if the function is not written correctly. Stupid Javascript!
+ if(this.options.selection && this.options.comparator) {
+ Tracker.autorun(function() {
+ let selectedData = this.options.selection.get();
+
+ if(selectedData) {
+ let listItems = this.$list.children();
+ let found = false;
+
+ for(let i = 0; !found && i < listItems.length; i++) {
+ if(this.options.comparator($(listItems[i]).data('model'), selectedData)) {
+ found = true;
+ this.select($(listItems[i]));
+ }
+ }
+ }
+ }.bind(this));
+ }
+ };
+
+ Combo.DEFAULTS = {
+ cursor: undefined, //A meteor Cursor used to populate the values displayed in the combo.
+ set: [], //An array of values displayed in the combo. This must be specified if cursor is not specified.
+ selection: undefined, //A meteor ReactiveVar whose value will be set to the current selection.
+ comparator: function(a, b) {return a === b;}, //A function that takes two collection objects and compares them for equality. If the combo shows users for example, this comparator would compare one user id to another. Required for the combo to set the selection if the view changes it externally relative to this combo.
+ textAttr: undefined, //The attribute of the data elements to use for the name. This can also be a function that takes the data object and returns the text.
+ idAttr: 'id', //The attribute of the data elements to use for the ID. This can also be a function that takes the data obejct and returns the ID.
+ // groupFunctions: The object containing three functions: 'groupParent', 'parentText', 'children'.
+ // groupParents(data) will take a data element and return the objects that best represents the parents of the children (for a multi layer tree, this would be the node just before the leaf nodes).
+ // parentText(parent) will be passed the group parent and the data object that generated it, and will return the text that represents the path to that parent.
+ // children(parent) will be passed the group parent (returned by groupParents()), and will return an array of children or leaf nodes for the tree.
+ groupFunctions: undefined,
+ filter: true, //Whether to filter the list as the user types.
+ effects: 'fade',
+ duration: '200',
+ listClass: 'de.combo-list',
+ selectionClass: 'selected', //The class to use for the selected element in the dropdown list.
+ getClasses: undefined //An optional function that will return a string to use in the list item's class attribute to style the list item for a given model data. The function will be passed the data object for the list item.
+ };
+
+ Combo.prototype.select = function($li) {
+ if($li.length == 0) {
+ if(this.$input.val() != '') {
+ this.$input.val("");
+ if(this.$hidden) this.$hidden.val(undefined).change();
+ this.filter();
+ //Note: Don't trigger the select event - for some reason it causes the dropdown to reopen and the control to retain focus when clicking out of the widget.
+ }
+ }
+ else {
+ if(!this.$list.has($li) || !$li.is('li.visible')) return;
+
+ //No need to change selection if the selection has not changed.
+ if(this.$input.val() != $li.text()) {
+ this.$input.val($li.text()); //Save the selected text into the text input.
+ if(this.$hidden) {
+ this.$hidden.val($li.data('model')[this.options.idAttr]);
+ this.$hidden.change();
+ } //Save the ID into the hidden form input if it exists.
+ this.hide();
+ this.filter();
+ //this.trigger('select', $li);
+
+ //Set the reactive var for the selection if one is provided and the selection has changed relative to the model.
+ if(this.options.selection && this.options.selection.get() != $li.data('model')) {
+ this.options.selection.set($li.data('model'));
+ }
+ }
+ }
+ };
+
+ Combo.prototype.escapeRegex = function(s) {
+ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+ };
+
+ // Removes all filtering. This is used to clear the filtering when first opening the combo when there is a value in the field. This is desirable because when we have an exact match, but are opening the combo, we most often want to select a new value.
+ Combo.prototype.clearFilter = function() {
+ console.log("CLearing Filter");
+ //Show all list elements.
+ this.$list.find('li').addClass('visible').show();
+ //Hide any node list elements.
+ this.$list.find('li[role="node"]').removeClass('visible').hide();
+ };
+
+ //Filters the list items by marking those that match the text in the text field as having the class 'visible'.
+ Combo.prototype.filter = function() {
+ try {
+ let search = this.$input.val();
+ let _this = this;
+
+ search = search ? search : "";
+ search = search.toLowerCase().trim();
+
+ //Show all list elements.
+ this.$list.find('li').addClass('visible').show();
+ //Hide any node list elements.
+ this.$list.find('li[role="node"]').removeClass('visible').hide();
+
+ if(this.options.filter) {
+ //Hide non-node elements (leaf nodes) that don't match.
+ let li = this.$list.children();
+
+ let searches = search && search.length > 0 ? search.split(/\s+/) : undefined;
+ let regexs = searches ? [] : undefined;
+
+ if(searches) {
+ for(let i = 0; i < searches.length; i++) {
+ regexs.push(new RegExp("\\b" + this.escapeRegex(searches[i])));
+ }
+ }
+
+ //Iterate over the list elements:
+ // hide all list items that are nodes;
+ // show all list items that are not nodes and whose text matches the input value;
+ // show all node list items associated with visible child list items (they occur after the parent, so the parent will be hidden first, then made visible).
+ for(let i = 0; i < li.length; i++) {
+ let $next = $(li[i]);
+ let node = $next.attr('role') == 'node';
+ //let hidden = node || $next.text().toLowerCase().indexOf(search) < 0;
+ let text = $next.text().toLowerCase();
+ let match = true;
+
+ if(!node && searches) {
+ for(let i = 0; match && i < regexs.length; i++) {
+ match = regexs[i].test(text)
+ }
+ }
+
+ //let match = text.match(/\bxxx/gi);
+ let hidden = node || !match;
+
+ if(hidden) $next.removeClass('visible').hide();
+
+ //If this isn't hidden and we have a tree with grouping, then turn on the group (parent) associated with this option.
+ if(!hidden && _this.options.groupFunctions) {
+ let parent = $next.data('parent-li');
+
+ if(!parent.hasClass('visible')) parent.addClass('visible').show();
+ }
+ }
+
+ //If we hid all elements then hide the whole list.
+ if(this.$list.find('li.visible').length == 0) this.hide();
+ }
+ } catch(e) {
+ console.log(e);
+ }
+ };
+
+ Combo.prototype.focus = function() {
+ this.show();
+ this.$input.select();
+ };
+
+ Combo.prototype.blur = function() {
+ this.hide();
+ this.select(this.$list.find('li.selected'));
+ };
+
+ Combo.prototype.show = function() {
+ // Make sure we don't repeatedly try to show the combo.
+ if(!this.isShowing) {
+ let position = this.$input.offset();
+
+ this.isShowing = true;
+ // Position the list relative to the field. Note that we place the combo at the top of the page (in the body tag) to avoid overflow not showing and to ensure the page scrolls if needed.
+ this.$list.css({position: 'absolute', top: position.top + this.$input.outerHeight(), left: position.left, width: this.$input.outerWidth()});
+ this.clearFilter();
+
+ if(!this.$list.is(':visible') && this.$list.find('li.visible').length > 0) {
+ let fns = {default: 'show', fade: 'fadeIn', slide: 'slideDown'};
+ let fn = fns[this.options.effects];
+
+ this.trigger('show');
+ this.$input.addClass('open');
+ this.$list[fn](this.options.duration, $.proxy(this.trigger, this, 'shown'));
+ }
+ }
+ };
+
+ Combo.prototype.hide = function() {
+ if(this.isShowing) {
+ let fns = {default: 'hide', fade: 'fadeOut', slide: 'slideUp'};
+ let fn = fns[this.options.effects];
+
+ this.isShowing = false;
+ this.trigger('hide');
+ this.$input.removeClass('open');
+ this.$list[fn](this.options.duration, $.proxy(this.trigger, this, 'hidden'));
+ }
+ };
+
+ // goDown: true/false - defaults to true - indicating whether the highlighting should go up or down if the requested item is a node. Nodes cannot be highlighted or selected.
+ Combo.prototype.highlight = function(index) {
+ let _this = this;
+
+ this.show();
+
+ setTimeout(function() {
+ let visibles = _this.$list.find('li.visible[role!="node"]');
+ let oldSelected = _this.$list.find('li.' + _this.options.selectionClass).removeClass(_this.options.selectionClass);
+ let oldSelectedIndex = visibles.index(oldSelected);
+
+ if(visibles.length > 0) {
+ let selectedIndex = (visibles.length + index) % visibles.length;
+ let selected = visibles.eq(selectedIndex);
+ let top = selected.position().top;
+
+ if(selected.attr('role') != 'node') selected.addClass(_this.options.selectionClass);
+
+ if(selectedIndex < oldSelectedIndex && top < 0)
+ _this.$list.scrollTop(_this.$list.scrollTop() + top);
+ if(selectedIndex > oldSelectedIndex && top + selected.outerHeight() > _this.$list.outerHeight())
+ _this.$list.scrollTop(_this.$list.scrollTop() + selected.outerHeight() + 2 * (top - _this.$list.outerHeight()));
+ }
+ });
+ };
+
+ Combo.prototype.trigger = function(event) {
+ let params = Array.prototype.slice.call(arguments, 1);
+ let args = [event + '.de.combo'];
+
+ args.push(params);
+
+ if(this.$select) this.$select.trigger.apply(this.$select, args);
+ this.$input.trigger.apply(this.$input, args);
+ };
+
+ $.fn.buildCombo = function(options) {
+ for(let index = 0; index < this.length; index++) {
+ let $next = $(this[index]);
+ let nextCombo = new Combo($next, $next.siblings('input[type=hidden]').first(), options);
+
+ $next.data("de.combo", nextCombo);
+ }
+ };
+ $.fn.getCombo = function() {
+ if(this.length > 0) {
+ return $(this[0]).data('de.combo');
+ }
+ };
+})(jQuery);
diff --git a/imports/util/normalize.css b/imports/util/normalize.css
new file mode 100644
index 0000000..cfbaeff
--- /dev/null
+++ b/imports/util/normalize.css
@@ -0,0 +1,508 @@
+/*! normalize.css 2012-03-11T12:53 UTC - http://github.com/necolas/normalize.css */
+
+/* =============================================================================
+ HTML5 display definitions
+ ========================================================================== */
+
+/*
+ * Corrects block display not defined in IE6/7/8/9 & FF3
+ */
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section,
+summary {
+ display: block;
+}
+
+/*
+ * Corrects inline-block display not defined in IE6/7/8/9 & FF3
+ */
+
+audio,
+canvas,
+video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+
+/*
+ * Prevents modern browsers from displaying 'audio' without controls
+ * Remove excess height in iOS5 devices
+ */
+
+audio:not([controls]) {
+ display: none;
+ height: 0;
+}
+
+/*
+ * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4
+ * Known issue: no IE6 support
+ */
+
+[hidden] {
+ display: none;
+}
+
+
+/* =============================================================================
+ Base
+ ========================================================================== */
+
+/*
+ * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units
+ * http://clagnut.com/blog/348/#c790
+ * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom
+ * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/
+ */
+
+html {
+ font-size: 100%; /* 1 */
+ -webkit-text-size-adjust: 100%; /* 2 */
+ -ms-text-size-adjust: 100%; /* 2 */
+}
+
+/*
+ * Addresses font-family inconsistency between 'textarea' and other form elements.
+ */
+
+html,
+button,
+input,
+select,
+textarea {
+ font-family: sans-serif;
+}
+
+/*
+ * Addresses margins handled incorrectly in IE6/7
+ */
+
+body {
+ margin: 0;
+}
+
+
+/* =============================================================================
+ Links
+ ========================================================================== */
+
+/*
+ * Addresses outline displayed oddly in Chrome
+ */
+
+a:focus {
+ outline: thin dotted;
+}
+
+/*
+ * Improves readability when focused and also mouse hovered in all browsers
+ * people.opera.com/patrickl/experiments/keyboard/test
+ */
+
+a:hover,
+a:active {
+ outline: 0;
+}
+
+
+/* =============================================================================
+ Typography
+ ========================================================================== */
+
+/*
+ * Addresses font sizes and margins set differently in IE6/7
+ * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5
+ */
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0;
+}
+
+h2 {
+ font-size: 1.5em;
+ margin: 0.83em 0;
+}
+
+h3 {
+ font-size: 1.17em;
+ margin: 1em 0;
+}
+
+h4 {
+ font-size: 1em;
+ margin: 1.33em 0;
+}
+
+h5 {
+ font-size: 0.83em;
+ margin: 1.67em 0;
+}
+
+h6 {
+ font-size: 0.75em;
+ margin: 2.33em 0;
+}
+
+/*
+ * Addresses styling not present in IE7/8/9, S5, Chrome
+ */
+
+abbr[title] {
+ border-bottom: 1px dotted;
+}
+
+/*
+ * Addresses style set to 'bolder' in FF3+, S4/5, Chrome
+*/
+
+b,
+strong {
+ font-weight: bold;
+}
+
+blockquote {
+ margin: 1em 40px;
+}
+
+/*
+ * Addresses styling not present in S5, Chrome
+ */
+
+dfn {
+ font-style: italic;
+}
+
+/*
+ * Addresses styling not present in IE6/7/8/9
+ */
+
+mark {
+ background: #ff0;
+ color: #000;
+}
+
+/*
+ * Addresses margins set differently in IE6/7
+ */
+
+p,
+pre {
+ margin: 1em 0;
+}
+
+/*
+ * Corrects font family set oddly in IE6, S4/5, Chrome
+ * en.wikipedia.org/wiki/User:Davidgothberg/Test59
+ */
+
+pre,
+code,
+kbd,
+samp {
+ font-family: monospace, serif;
+ _font-family: 'courier new', monospace;
+ font-size: 1em;
+}
+
+/*
+ * Improves readability of pre-formatted text in all browsers
+ */
+
+pre {
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+
+/*
+ * 1. Addresses CSS quotes not supported in IE6/7
+ * 2. Addresses quote property not supported in S4
+ */
+
+/* 1 */
+
+q {
+ quotes: none;
+}
+
+/* 2 */
+
+q:before,
+q:after {
+ content: '';
+ content: none;
+}
+
+small {
+ font-size: 75%;
+}
+
+/*
+ * Prevents sub and sup affecting line-height in all browsers
+ * gist.github.com/413930
+ */
+
+sub,
+sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+
+sup {
+ top: -0.5em;
+}
+
+sub {
+ bottom: -0.25em;
+}
+
+
+/* =============================================================================
+ Lists
+ ========================================================================== */
+
+/*
+ * Addresses margins set differently in IE6/7
+ */
+
+dl,
+menu,
+ol,
+ul {
+ margin: 1em 0;
+}
+
+dd {
+ margin: 0 0 0 40px;
+}
+
+/*
+ * Addresses paddings set differently in IE6/7
+ */
+
+menu,
+ol,
+ul {
+ padding: 0 0 0 40px;
+}
+
+/*
+ * Corrects list images handled incorrectly in IE7
+ */
+
+nav ul,
+nav ol {
+ list-style: none;
+ list-style-image: none;
+}
+
+
+/* =============================================================================
+ Embedded content
+ ========================================================================== */
+
+/*
+ * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3
+ * 2. Improves image quality when scaled in IE7
+ * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/
+ */
+
+img {
+ border: 0; /* 1 */
+ -ms-interpolation-mode: bicubic; /* 2 */
+}
+
+/*
+ * Corrects overflow displayed oddly in IE9
+ */
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+
+/* =============================================================================
+ Figures
+ ========================================================================== */
+
+/*
+ * Addresses margin not present in IE6/7/8/9, S5, O11
+ */
+
+figure {
+ margin: 0;
+ -webkit-margin-before: 0;
+ -webkit-margin-after: 0;
+ -webkit-margin-start: 0;
+ -webkit-margin-end: 0;
+}
+
+
+/* =============================================================================
+ Forms
+ ========================================================================== */
+
+/*
+ * Corrects margin displayed oddly in IE6/7
+ */
+
+form {
+ margin: 0;
+}
+
+/*
+ * Define consistent border, margin, and padding
+ */
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em;
+}
+
+/*
+ * 1. Corrects color not being inherited in IE6/7/8/9
+ * 2. Corrects text not wrapping in FF3
+ * 3. Corrects alignment displayed oddly in IE6/7
+ */
+
+legend {
+ border: 0; /* 1 */
+ padding: 0;
+ white-space: normal; /* 2 */
+ *margin-left: -7px; /* 3 */
+}
+
+/*
+ * 1. Corrects font size not being inherited in all browsers
+ * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome
+ * 3. Improves appearance and consistency in all browsers
+ */
+
+button,
+input,
+select,
+textarea {
+ font-size: 100%; /* 1 */
+ margin: 0; /* 2 */
+ vertical-align: baseline; /* 3 */
+ *vertical-align: middle; /* 3 */
+}
+
+/*
+ * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet
+ */
+
+button,
+input {
+ line-height: normal; /* 1 */
+}
+
+/*
+ * 1. Improves usability and consistency of cursor style between image-type 'input' and others
+ * 2. Corrects inability to style clickable 'input' types in iOS
+ * 3. Removes inner spacing in IE7 without affecting normal text inputs
+ * Known issue: inner spacing remains in IE6
+ */
+
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+ cursor: pointer; /* 1 */
+ -webkit-appearance: button; /* 2 */
+ *overflow: visible; /* 3 */
+}
+
+/*
+ * Re-set default cursor for disabled elements
+ */
+
+button[disabled],
+input[disabled] {
+ cursor: default;
+}
+
+/*
+ * 1. Addresses box sizing set to content-box in IE8/9
+ * 2. Removes excess padding in IE8/9
+ * 3. Removes excess padding in IE7
+ Known issue: excess padding remains in IE6
+ */
+
+input[type="checkbox"],
+input[type="radio"] {
+ box-sizing: border-box; /* 1 */
+ padding: 0; /* 2 */
+ *height: 13px; /* 3 */
+ *width: 13px; /* 3 */
+}
+
+/*
+ * 1. Addresses appearance set to searchfield in S5, Chrome
+ * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof)
+ */
+
+input[type="search"] {
+ -webkit-appearance: textfield; /* 1 */
+ -moz-box-sizing: content-box;
+ -webkit-box-sizing: content-box; /* 2 */
+ box-sizing: content-box;
+}
+
+/*
+ * Removes inner padding and search cancel button in S5, Chrome on OS X
+ */
+
+input[type="search"]::-webkit-search-decoration,
+input[type="search"]::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+}
+
+/*
+ * Removes inner padding and border in FF3+
+ * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/
+ */
+
+button::-moz-focus-inner,
+input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+
+/*
+ * 1. Removes default vertical scrollbar in IE6/7/8/9
+ * 2. Improves readability and alignment in all browsers
+ */
+
+textarea {
+ overflow: auto; /* 1 */
+ vertical-align: top; /* 2 */
+}
+
+
+/* =============================================================================
+ Tables
+ ========================================================================== */
+
+/*
+ * Remove most spacing between table cells
+ */
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
diff --git a/imports/util/polyfills/array.js b/imports/util/polyfills/array.js
new file mode 100644
index 0000000..26d7702
--- /dev/null
+++ b/imports/util/polyfills/array.js
@@ -0,0 +1,108 @@
+// https://tc39.github.io/ecma262/#sec-array.prototype.includes
+if(!Array.prototype.includes) {
+ Object.defineProperty(Array.prototype, 'includes', {
+ value: function(searchElement, fromIndex) {
+
+ // 1. Let O be ? ToObject(this value).
+ if (this == null) {
+ throw new TypeError('"this" is null or not defined');
+ }
+
+ let o = Object(this);
+
+ // 2. Let len be ? ToLength(? Get(O, "length")).
+ let len = o.length >>> 0;
+
+ // 3. If len is 0, return false.
+ if (len === 0) {
+ return false;
+ }
+
+ // 4. Let n be ? ToInteger(fromIndex).
+ // (If fromIndex is undefined, this step produces the value 0.)
+ let n = fromIndex | 0;
+
+ // 5. If n ≥ 0, then
+ // a. Let k be n.
+ // 6. Else n < 0,
+ // a. Let k be len + n.
+ // b. If k < 0, let k be 0.
+ let k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
+
+ // 7. Repeat, while k < len
+ while (k < len) {
+ // a. Let elementK be the result of ? Get(O, ! ToString(k)).
+ // b. If SameValueZero(searchElement, elementK) is true, return true.
+ // c. Increase k by 1.
+ // NOTE: === provides the correct "SameValueZero" comparison needed here.
+ if (o[k] === searchElement) {
+ return true;
+ }
+ k++;
+ }
+
+ // 8. Return false
+ return false;
+ }
+ });
+}
+
+//http://stackoverflow.com/questions/5306680/move-an-array-element-from-one-array-position-to-another
+if(!Array.prototype.move) {
+ Array.prototype.move = function (old_index, new_index) {
+ if (new_index >= this.length) {
+ let k = new_index - this.length;
+ while ((k--) + 1) {
+ this.push(undefined);
+ }
+ }
+ this.splice(new_index, 0, this.splice(old_index, 1)[0]);
+ return this; // for testing purposes
+ };
+}
+
+//My own implementation to work around Javascript's shitty naming and support for collection operations.
+if(!Array.prototype.remove) {
+ Array.prototype.remove = function(item) {
+ let index = this.indexOf(item);
+
+ if(index != -1) {
+ this.splice(index, 1);
+ return true;
+ }
+ return false;
+ }
+}
+
+//My own implementation to work around Javascript's shitty naming and support for collection operations.
+// index is optional
+if(!Array.prototype.add) {
+ Array.prototype.add = function(item, index) {
+ if(index == undefined || isNaN(index) || index >= this.length) return this.push(item);
+ else return this.splice(index, 0, item);
+ }
+}
+
+//My own implementation to work around Javascript's shitty naming and support for collection operations.
+// Sorts a contiguous section of the array.
+// Index is the index of the first element to be sorted (inclusive).
+// Length is the number of elements of the array to be sorted (must be >= 2). If the length + index is greater than the array length then it will be adjusted to the end of the array.
+// All other invalid inputs will result in no sorting action taken and no error.
+if(!Array.prototype.partialSort) {
+ Array.prototype.partialSort = function(index, length, compareFunction) {
+ if(index >= 0 && length >= 2 && index <= (this.length - 2)) {
+ //Adjust the length so it doesn't over-run the array. This is the only error correction we will perform.
+ if(index + length > this.length) length = this.length - index;
+
+ //Shallow copy of the data in the segment to be sorted.
+ let sorted = this.slice(index, length + index);
+
+ sorted.sort(compareFunction);
+
+ //Put the sorted array elements back into the array.
+ for(let i = index, j = 0; j < length; i++, j++) {
+ this[i] = sorted[j];
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/imports/util/polyfills/blaze.js b/imports/util/polyfills/blaze.js
new file mode 100644
index 0000000..c22bbe2
--- /dev/null
+++ b/imports/util/polyfills/blaze.js
@@ -0,0 +1,19 @@
+/**
+ * Get the parent template instance.
+ * @param {Number} [levels] How many levels to go up. Default is 1
+ * @returns {Blaze.TemplateInstance}
+ */
+Blaze.TemplateInstance.prototype.parentTemplate = Blaze.TemplateInstance.prototype.parentInstance = function(levels) {
+ let view = this.view;
+
+ levels = (typeof levels === "undefined") ? 1 : levels;
+
+ while(view) {
+ //if(view.name.substring(0, 9) === "Template." && !(levels--)) {
+ if(view.template && !(levels--)) {
+ //return view.templateInstance();
+ return view.templateInstance();
+ }
+ view = view.parentView;
+ }
+};
\ No newline at end of file
diff --git a/imports/util/polyfills/date.js b/imports/util/polyfills/date.js
new file mode 100644
index 0000000..d8d6019
--- /dev/null
+++ b/imports/util/polyfills/date.js
@@ -0,0 +1,71 @@
+//
+// Add a method to get a timezone adjusted date for an input field that is a date picker.
+// Use $('input[name="date"]').val(new Date().toDateInputValue()) to set the date of the input field.
+//
+Date.prototype.toDateInputValue = (function() {
+ let local = new Date(this);
+ local.setMinutes(this.getMinutes() - this.getTimezoneOffset());
+ return local.toJSON().slice(0,10);
+});
+
+Date.prototype.getWeek = function() {
+ let dowOffset = 1; // I am fixing this to indicate that the first day of the week is always Monday (weeks end on Sunday), for this application. This was a parameter in the original code.
+
+ //dowOffset = typeof(dowOffset) == 'number' ? dowOffset : 0; //default dowOffset to zero - This should check to ensure that dowOffset is between 0..6
+ let newYear = new Date(this.getFullYear(),0,1);
+ let day = newYear.getDay() - dowOffset; //the day of week the year begins on
+ day = (day >= 0 ? day : day + 7);
+ // The number of days from the beginning of the year to this.day
+ let daynum = Math.floor((this.getTime() - newYear.getTime() - (this.getTimezoneOffset()-newYear.getTimezoneOffset())*60000)/86400000) + 1;
+ let weeknum;
+
+ // I have removed the mid-week starting cutoff detection because in this app we always want to start with week #1 (never have a week zero).
+ //if(day < 4) { //if the year starts before the middle of a week
+ weeknum = Math.floor((daynum + day - 1) / 7) + 1;
+
+ // I have turned off the detection of whether the last days of the year belong to this year's last week or next year's first week. This gets too confusing and does not result in any additional usefulness.
+ //if(weeknum > 52) {
+ // nYear = new Date(this.getFullYear() + 1, 0, 1);
+ // nday = nYear.getDay() - dowOffset;
+ // nday = nday >= 0 ? nday : nday + 7;
+ // // if the next year starts before the middle of the week, it is week #1 of that year
+ // weeknum = nday < 4 ? 1 : 53;
+ //}
+ //}
+ //else {
+ // weeknum = Math.floor((daynum+day-1)/7);
+ //}
+
+ return weeknum;
+};
+
+Date.prototype.getUTCWeek = function() {
+ let dowOffset = 1; // I am fixing this to indicate that the first day of the week is always Monday (weeks end on Sunday), for this application. This was a parameter in the original code.
+
+ //dowOffset = typeof(dowOffset) == 'number' ? dowOffset : 0; //default dowOffset to zero - This should check to ensure that dowOffset is between 0..6
+ let newYear = new Date(this.getUTCFullYear(),0,1);
+ let day = newYear.getDay() - dowOffset; //the day of week the year begins on
+ day = (day >= 0 ? day : day + 7);
+ // The number of days from the beginning of the year to this.day
+ let daynum = Math.floor((this.getTime() - newYear.getTime() - (this.getTimezoneOffset() - newYear.getTimezoneOffset())*60000)/86400000) + 1;
+ let weeknum;
+
+ // I have removed the mid-week starting cutoff detection because in this app we always want to start with week #1 (never have a week zero).
+ //if(day < 4) { //if the year starts before the middle of a week
+ weeknum = Math.floor((daynum + day - 1) / 7) + 1;
+
+ // I have turned off the detection of whether the last days of the year belong to this year's last week or next year's first week. This gets too confusing and does not result in any additional usefulness.
+ //if(weeknum > 52) {
+ // nYear = new Date(this.getFullYear() + 1, 0, 1);
+ // nday = nYear.getDay() - dowOffset;
+ // nday = nday >= 0 ? nday : nday + 7;
+ // // if the next year starts before the middle of the week, it is week #1 of that year
+ // weeknum = nday < 4 ? 1 : 53;
+ //}
+ //}
+ //else {
+ // weeknum = Math.floor((daynum+day-1)/7);
+ //}
+
+ return weeknum;
+};
\ No newline at end of file
diff --git a/imports/util/polyfills/regex.js b/imports/util/polyfills/regex.js
new file mode 100644
index 0000000..44259c9
--- /dev/null
+++ b/imports/util/polyfills/regex.js
@@ -0,0 +1,3 @@
+RegExp.escape = function(s) {
+ return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
+};
\ No newline at end of file
diff --git a/imports/util/resize/ElementQueries.js b/imports/util/resize/ElementQueries.js
new file mode 100644
index 0000000..b71d6c9
--- /dev/null
+++ b/imports/util/resize/ElementQueries.js
@@ -0,0 +1,515 @@
+/**
+ * Copyright Marc J. Schmidt. See the LICENSE file at the top-level
+ * directory of this distribution and at
+ * https://github.com/marcj/css-element-queries/blob/master/LICENSE.
+ */
+;
+(function (root, factory) {
+ if (typeof define === "function" && define.amd) {
+ define(['./ResizeSensor.js'], factory);
+ } else if (typeof exports === "object") {
+ module.exports = factory(require('./ResizeSensor.js'));
+ } else {
+ root.ElementQueries = factory(root.ResizeSensor);
+ }
+}(this, function (ResizeSensor) {
+
+ /**
+ *
+ * @type {Function}
+ * @constructor
+ */
+ var ElementQueries = function() {
+
+ var trackingActive = false;
+ var elements = [];
+
+ /**
+ *
+ * @param element
+ * @returns {Number}
+ */
+ function getEmSize(element) {
+ if (!element) {
+ element = document.documentElement;
+ }
+ var fontSize = window.getComputedStyle(element, null).fontSize;
+ return parseFloat(fontSize) || 16;
+ }
+
+ /**
+ *
+ * @copyright https://github.com/Mr0grog/element-query/blob/master/LICENSE
+ *
+ * @param {HTMLElement} element
+ * @param {*} value
+ * @returns {*}
+ */
+ function convertToPx(element, value) {
+ var numbers = value.split(/\d/);
+ var units = numbers[numbers.length-1];
+ value = parseFloat(value);
+ switch (units) {
+ case "px":
+ return value;
+ case "em":
+ return value * getEmSize(element);
+ case "rem":
+ return value * getEmSize();
+ // Viewport units!
+ // According to http://quirksmode.org/mobile/tableViewport.html
+ // documentElement.clientWidth/Height gets us the most reliable info
+ case "vw":
+ return value * document.documentElement.clientWidth / 100;
+ case "vh":
+ return value * document.documentElement.clientHeight / 100;
+ case "vmin":
+ case "vmax":
+ var vw = document.documentElement.clientWidth / 100;
+ var vh = document.documentElement.clientHeight / 100;
+ var chooser = Math[units === "vmin" ? "min" : "max"];
+ return value * chooser(vw, vh);
+ default:
+ return value;
+ // for now, not supporting physical units (since they are just a set number of px)
+ // or ex/ch (getting accurate measurements is hard)
+ }
+ }
+
+ /**
+ *
+ * @param {HTMLElement} element
+ * @constructor
+ */
+ function SetupInformation(element) {
+ this.element = element;
+ this.options = {};
+ var key, option, width = 0, height = 0, value, actualValue, attrValues, attrValue, attrName;
+
+ /**
+ * @param {Object} option {mode: 'min|max', property: 'width|height', value: '123px'}
+ */
+ this.addOption = function(option) {
+ var idx = [option.mode, option.property, option.value].join(',');
+ this.options[idx] = option;
+ };
+
+ var attributes = ['min-width', 'min-height', 'max-width', 'max-height'];
+
+ /**
+ * Extracts the computed width/height and sets to min/max- attribute.
+ */
+ this.call = function() {
+ // extract current dimensions
+ width = this.element.offsetWidth;
+ height = this.element.offsetHeight;
+
+ attrValues = {};
+
+ for (key in this.options) {
+ if (!this.options.hasOwnProperty(key)){
+ continue;
+ }
+ option = this.options[key];
+
+ value = convertToPx(this.element, option.value);
+
+ actualValue = option.property == 'width' ? width : height;
+ attrName = option.mode + '-' + option.property;
+ attrValue = '';
+
+ if (option.mode == 'min' && actualValue >= value) {
+ attrValue += option.value;
+ }
+
+ if (option.mode == 'max' && actualValue <= value) {
+ attrValue += option.value;
+ }
+
+ if (!attrValues[attrName]) attrValues[attrName] = '';
+ if (attrValue && -1 === (' '+attrValues[attrName]+' ').indexOf(' ' + attrValue + ' ')) {
+ attrValues[attrName] += ' ' + attrValue;
+ }
+ }
+
+ for (var k in attributes) {
+ if(!attributes.hasOwnProperty(k)) continue;
+
+ if (attrValues[attributes[k]]) {
+ this.element.setAttribute(attributes[k], attrValues[attributes[k]].substr(1));
+ } else {
+ this.element.removeAttribute(attributes[k]);
+ }
+ }
+ };
+ }
+
+ /**
+ * @param {HTMLElement} element
+ * @param {Object} options
+ */
+ function setupElement(element, options) {
+ if (element.elementQueriesSetupInformation) {
+ element.elementQueriesSetupInformation.addOption(options);
+ } else {
+ element.elementQueriesSetupInformation = new SetupInformation(element);
+ element.elementQueriesSetupInformation.addOption(options);
+ element.elementQueriesSensor = new ResizeSensor(element, function() {
+ element.elementQueriesSetupInformation.call();
+ });
+ }
+ element.elementQueriesSetupInformation.call();
+
+ if (trackingActive && elements.indexOf(element) < 0) {
+ elements.push(element);
+ }
+ }
+
+ /**
+ * @param {String} selector
+ * @param {String} mode min|max
+ * @param {String} property width|height
+ * @param {String} value
+ */
+ var allQueries = {};
+ function queueQuery(selector, mode, property, value) {
+ if (typeof(allQueries[mode]) == 'undefined') allQueries[mode] = {};
+ if (typeof(allQueries[mode][property]) == 'undefined') allQueries[mode][property] = {};
+ if (typeof(allQueries[mode][property][value]) == 'undefined') allQueries[mode][property][value] = selector;
+ else allQueries[mode][property][value] += ','+selector;
+ }
+
+ function getQuery() {
+ var query;
+ if (document.querySelectorAll) query = document.querySelectorAll.bind(document);
+ if (!query && 'undefined' !== typeof $$) query = $$;
+ if (!query && 'undefined' !== typeof jQuery) query = jQuery;
+
+ if (!query) {
+ throw 'No document.querySelectorAll, jQuery or Mootools\'s $$ found.';
+ }
+
+ return query;
+ }
+
+ /**
+ * Start the magic. Go through all collected rules (readRules()) and attach the resize-listener.
+ */
+ function findElementQueriesElements() {
+ var query = getQuery();
+
+ for (var mode in allQueries) if (allQueries.hasOwnProperty(mode)) {
+
+ for (var property in allQueries[mode]) if (allQueries[mode].hasOwnProperty(property)) {
+ for (var value in allQueries[mode][property]) if (allQueries[mode][property].hasOwnProperty(value)) {
+ var elements = query(allQueries[mode][property][value]);
+ for (var i = 0, j = elements.length; i < j; i++) {
+ setupElement(elements[i], {
+ mode: mode,
+ property: property,
+ value: value
+ });
+ }
+ }
+ }
+
+ }
+ }
+
+ /**
+ *
+ * @param {HTMLElement} element
+ */
+ function attachResponsiveImage(element) {
+ var children = [];
+ var rules = [];
+ var sources = [];
+ var defaultImageId = 0;
+ var lastActiveImage = -1;
+ var loadedImages = [];
+
+ for (var i in element.children) {
+ if(!element.children.hasOwnProperty(i)) continue;
+
+ if (element.children[i].tagName && element.children[i].tagName.toLowerCase() === 'img') {
+ children.push(element.children[i]);
+
+ var minWidth = element.children[i].getAttribute('min-width') || element.children[i].getAttribute('data-min-width');
+ //var minHeight = element.children[i].getAttribute('min-height') || element.children[i].getAttribute('data-min-height');
+ var src = element.children[i].getAttribute('data-src') || element.children[i].getAttribute('url');
+
+ sources.push(src);
+
+ var rule = {
+ minWidth: minWidth
+ };
+
+ rules.push(rule);
+
+ if (!minWidth) {
+ defaultImageId = children.length - 1;
+ element.children[i].style.display = 'block';
+ } else {
+ element.children[i].style.display = 'none';
+ }
+ }
+ }
+
+ lastActiveImage = defaultImageId;
+
+ function check() {
+ var imageToDisplay = false, i;
+
+ for (i in children){
+ if(!children.hasOwnProperty(i)) continue;
+
+ if (rules[i].minWidth) {
+ if (element.offsetWidth > rules[i].minWidth) {
+ imageToDisplay = i;
+ }
+ }
+ }
+
+ if (!imageToDisplay) {
+ //no rule matched, show default
+ imageToDisplay = defaultImageId;
+ }
+
+ if (lastActiveImage != imageToDisplay) {
+ //image change
+
+ if (!loadedImages[imageToDisplay]){
+ //image has not been loaded yet, we need to load the image first in memory to prevent flash of
+ //no content
+
+ var image = new Image();
+ image.onload = function() {
+ children[imageToDisplay].src = sources[imageToDisplay];
+
+ children[lastActiveImage].style.display = 'none';
+ children[imageToDisplay].style.display = 'block';
+
+ loadedImages[imageToDisplay] = true;
+
+ lastActiveImage = imageToDisplay;
+ };
+
+ image.src = sources[imageToDisplay];
+ } else {
+ children[lastActiveImage].style.display = 'none';
+ children[imageToDisplay].style.display = 'block';
+ lastActiveImage = imageToDisplay;
+ }
+ } else {
+ //make sure for initial check call the .src is set correctly
+ children[imageToDisplay].src = sources[imageToDisplay];
+ }
+ }
+
+ element.resizeSensor = new ResizeSensor(element, check);
+ check();
+
+ if (trackingActive) {
+ elements.push(element);
+ }
+ }
+
+ function findResponsiveImages(){
+ var query = getQuery();
+
+ var elements = query('[data-responsive-image],[responsive-image]');
+ for (var i = 0, j = elements.length; i < j; i++) {
+ attachResponsiveImage(elements[i]);
+ }
+ }
+
+ var regex = /,?[\s\t]*([^,\n]*?)((?:\[[\s\t]*?(?:min|max)-(?:width|height)[\s\t]*?[~$\^]?=[\s\t]*?"[^"]*?"[\s\t]*?])+)([^,\n\s\{]*)/mgi;
+ var attrRegex = /\[[\s\t]*?(min|max)-(width|height)[\s\t]*?[~$\^]?=[\s\t]*?"([^"]*?)"[\s\t]*?]/mgi;
+ /**
+ * @param {String} css
+ */
+ function extractQuery(css) {
+ var match;
+ var smatch;
+ css = css.replace(/'/g, '"');
+ while (null !== (match = regex.exec(css))) {
+ smatch = match[1] + match[3];
+ attrs = match[2];
+
+ while (null !== (attrMatch = attrRegex.exec(attrs))) {
+ queueQuery(smatch, attrMatch[1], attrMatch[2], attrMatch[3]);
+ }
+ }
+ }
+
+ /**
+ * @param {CssRule[]|String} rules
+ */
+ function readRules(rules) {
+ var selector = '';
+ if (!rules) {
+ return;
+ }
+ if ('string' === typeof rules) {
+ rules = rules.toLowerCase();
+ if (-1 !== rules.indexOf('min-width') || -1 !== rules.indexOf('max-width')) {
+ extractQuery(rules);
+ }
+ } else {
+ for (var i = 0, j = rules.length; i < j; i++) {
+ if (1 === rules[i].type) {
+ selector = rules[i].selectorText || rules[i].cssText;
+ if (-1 !== selector.indexOf('min-height') || -1 !== selector.indexOf('max-height')) {
+ extractQuery(selector);
+ }else if(-1 !== selector.indexOf('min-width') || -1 !== selector.indexOf('max-width')) {
+ extractQuery(selector);
+ }
+ } else if (4 === rules[i].type) {
+ readRules(rules[i].cssRules || rules[i].rules);
+ }
+ }
+ }
+ }
+
+ var defaultCssInjected = false;
+
+ /**
+ * Searches all css rules and setups the event listener to all elements with element query rules..
+ *
+ * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
+ * (no garbage collection possible if you don not call .detach() first)
+ */
+ this.init = function(withTracking) {
+ trackingActive = typeof withTracking === 'undefined' ? false : withTracking;
+
+ for (var i = 0, j = document.styleSheets.length; i < j; i++) {
+ try {
+ readRules(document.styleSheets[i].cssRules || document.styleSheets[i].rules || document.styleSheets[i].cssText);
+ } catch(e) {
+ if (e.name !== 'SecurityError') {
+ throw e;
+ }
+ }
+ }
+
+ if (!defaultCssInjected) {
+ var style = document.createElement('style');
+ style.type = 'text/css';
+ style.innerHTML = '[responsive-image] > img, [data-responsive-image] {overflow: hidden; padding: 0; } [responsive-image] > img, [data-responsive-image] > img { width: 100%;}';
+ document.getElementsByTagName('head')[0].appendChild(style);
+ defaultCssInjected = true;
+ }
+
+ findElementQueriesElements();
+ findResponsiveImages();
+ };
+
+ /**
+ *
+ * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
+ * (no garbage collection possible if you don not call .detach() first)
+ */
+ this.update = function(withTracking) {
+ this.init(withTracking);
+ };
+
+ this.detach = function() {
+ if (!this.withTracking) {
+ throw 'withTracking is not enabled. We can not detach elements since we don not store it.' +
+ 'Use ElementQueries.withTracking = true; before domready or call ElementQueryes.update(true).';
+ }
+
+ var element;
+ while (element = elements.pop()) {
+ ElementQueries.detach(element);
+ }
+
+ elements = [];
+ };
+ };
+
+ /**
+ *
+ * @param {Boolean} withTracking allows and requires you to use detach, since we store internally all used elements
+ * (no garbage collection possible if you don not call .detach() first)
+ */
+ ElementQueries.update = function(withTracking) {
+ ElementQueries.instance.update(withTracking);
+ };
+
+ /**
+ * Removes all sensor and elementquery information from the element.
+ *
+ * @param {HTMLElement} element
+ */
+ ElementQueries.detach = function(element) {
+ if (element.elementQueriesSetupInformation) {
+ //element queries
+ element.elementQueriesSensor.detach();
+ delete element.elementQueriesSetupInformation;
+ delete element.elementQueriesSensor;
+
+ } else if (element.resizeSensor) {
+ //responsive image
+
+ element.resizeSensor.detach();
+ delete element.resizeSensor;
+ } else {
+ //console.log('detached already', element);
+ }
+ };
+
+ ElementQueries.withTracking = false;
+
+ ElementQueries.init = function() {
+ if (!ElementQueries.instance) {
+ ElementQueries.instance = new ElementQueries();
+ }
+
+ ElementQueries.instance.init(ElementQueries.withTracking);
+ };
+
+ var domLoaded = function (callback) {
+ /* Internet Explorer */
+ /*@cc_on
+ @if (@_win32 || @_win64)
+ document.write('
+
\ No newline at end of file
diff --git a/public/barChartData.csv b/public/barChartData.csv
new file mode 100644
index 0000000..ac84883
--- /dev/null
+++ b/public/barChartData.csv
@@ -0,0 +1,7 @@
+State,Under 5 Years,5 to 13 Years,14 to 17 Years,18 to 24 Years,25 to 44 Years,45 to 64 Years,65 Years and Over
+CA,2704659,4499890,2159981,3853788,10604510,8819342,4114496
+TX,2027307,3277946,1420518,2454721,7017731,5656528,2472223
+NY,1208495,2141490,1058031,1999120,5355235,5120254,2607672
+FL,1140516,1938695,925060,1607297,4782119,4746856,3187797
+IL,894368,1558919,725973,1311479,3596343,3239173,1575308
+PA,737462,1345341,679201,1203944,3157759,3414001,1910571
\ No newline at end of file
diff --git a/public/images/Header_v1.jpg b/public/images/Header_v1.jpg
new file mode 100644
index 0000000..2f3226e
Binary files /dev/null and b/public/images/Header_v1.jpg differ
diff --git a/public/images/Logo_v1.png b/public/images/Logo_v1.png
new file mode 100644
index 0000000..a0da11f
Binary files /dev/null and b/public/images/Logo_v1.png differ
diff --git a/public/images/loading.gif b/public/images/loading.gif
new file mode 100644
index 0000000..18abf9a
Binary files /dev/null and b/public/images/loading.gif differ
diff --git a/server/server.js b/server/server.js
new file mode 100644
index 0000000..9817534
--- /dev/null
+++ b/server/server.js
@@ -0,0 +1,21 @@
+import '/imports/util/polyfills/regex.js';
+import '/imports/startup/server';
+import '/imports/startup/both';
+import '/imports/api';
+import '/imports/startup/server/postStartup/version.js'; //Run this right after the api - relies on the API to upgrade the app database & data to the current version.
+
+let PropertiesReader = require('properties-reader');
+let props = PropertiesReader('./assets/app/release.properties');
+
+if(props.get('email-settings')) {
+ process.env.MAIL_URL = props.get('email-settings');
+ // console.log("Loaded email settings from properties file.");
+}
+
+if (!process.env.MAIL_URL) {
+ process.env.MAIL_URL = Meteor.settings.MAIL_URL;
+}
+
+// console.log("Mail settings: " + process.env.MAIL_URL);
+
+if(Meteor.log) Meteor.log.info("Server Started");