diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..eaf3238
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["es2015", "stage-0"]
+}
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..b435e4f
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,10 @@
+{
+ "parser": "babel-eslint",
+ "rules": {
+ "strict": 0
+ },
+ "extends": "eslint:recommended",
+ "env": {
+ "browser": true
+ }
+}
diff --git a/README.md b/README.md
index 10757f1..0304964 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,3 @@
-# SubmissionForm
-Web form for contributing data to OpenBounds projects
+## OpenBounds Data Submission
+
+A Javascript app running on Github Pages for data submission via pull request.
\ No newline at end of file
diff --git a/dist/submission.min.css b/dist/submission.min.css
new file mode 100644
index 0000000..8ea1186
--- /dev/null
+++ b/dist/submission.min.css
@@ -0,0 +1 @@
+.dropdown-menu::after,.dropdown-menu::before{position:absolute;content:"";left:auto;display:inline-block}.dropdown-btn.selected:focus,.dropdown-btn:focus,.dropdown-btn:focus:hover{border-color:#b5b5b5}.dropdown-btn:focus,.dropdown-btn:focus:hover{box-shadow:none}.dropdown-btn.selected,.dropdown-btn.selected:focus{background-color:#dcdcdc;box-shadow:inset 0 2px 4px rgba(0,0,0,.15)}.dropdown-btn::after{display:inline-block;width:0;height:0;content:"";vertical-align:-2px;margin-left:5px;border:4px solid;border-right-color:transparent;border-left-color:transparent;border-bottom-color:transparent}.dropdown-menu{width:180px;position:absolute;right:10px;top:45px;padding-top:5px;padding-bottom:5px;background-clip:padding-box;box-shadow:0 3px 12px rgba(0,0,0,.15)}.dropdown-menu::before{border:8px solid transparent;border-bottom-color:rgba(0,0,0,.15);top:-16px;right:9px}.dropdown-menu::after{border:7px solid transparent;border-bottom-color:#fff;top:-14px;right:10px}.dropdown-item{padding:4px 10px 4px 15px;color:#333}.dropdown-item:hover{color:#fff;text-decoration:none;text-shadow:none;background-color:#4078c0}#main{position:relative;margin-top:50px}#manual textarea{width:100%;font-family:monospace}#doneerror{float:right}.form.form-inline{display:inline-block;margin-right:10px}.form.form-inline:last-child{margin-right:0}dl.form>dd input[type=text]{width:100%}h2{margin-top:5px;margin-bottom:30px}.avatar{width:20px;margin-right:5px}table,td input{width:100%}table{margin:15px 0}td,th{text-align:left;padding:6px 15px;border-bottom:1px solid #E1E1E1}tr:last-child td{border-bottom:none}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}
\ No newline at end of file
diff --git a/dist/submission.min.js b/dist/submission.min.js
new file mode 100644
index 0000000..1fb5c26
--- /dev/null
+++ b/dist/submission.min.js
@@ -0,0 +1 @@
+!function(modules){function __webpack_require__(moduleId){if(installedModules[moduleId])return installedModules[moduleId].exports;var module=installedModules[moduleId]={exports:{},id:moduleId,loaded:!1};return modules[moduleId].call(module.exports,module,module.exports,__webpack_require__),module.loaded=!0,module.exports}var installedModules={};return __webpack_require__.m=modules,__webpack_require__.c=installedModules,__webpack_require__.p="",__webpack_require__(0)}([function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}function _interopRequireWildcard(obj){if(obj&&obj.__esModule)return obj;var newObj={};if(null!=obj)for(var key in obj)Object.prototype.hasOwnProperty.call(obj,key)&&(newObj[key]=obj[key]);return newObj["default"]=obj,newObj}var _github=__webpack_require__(1),github=_interopRequireWildcard(_github),_utils=__webpack_require__(3),utils=_interopRequireWildcard(_utils),_dropdown=__webpack_require__(4),_dropdown2=_interopRequireDefault(_dropdown),BASE_REPO="OpenBounds/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),alert=window.document.getElementById("alert"),manual=window.document.getElementById("manual"),signinUser=function(err,user){var button=window.document.getElementById("signin"),signout=window.document.getElementById("signout"),blank=window.document.getElementById("unauthenticated");button.setAttribute("href","#"),button.innerHTML='
'+user.login,blank.style.display="none",form.style.display="block",signout.addEventListener("click",signoutUser),new _dropdown2["default"](button)},signoutUser=function(){github.clearToken(),window.location.href=window.location.pathname},startSubmitting=function(){pr.setAttribute("disabled","disabled"),pr.textContent="Submitting..."},doneSubmitting=function(){pr.removeAttribute("disabled"),pr.textContent="Submit Pull Request"},errorSubmitting=function(msg,content){alert.innerHTML=msg,manual.getElementsByTagName("textarea")[0].textContent=content,alert.style.display="block",manual.style.display="block"},doneError=function(){alert.innerHTML="",manual.getElementsByTagName("textarea")[0].textContent="",alert.style.display="none",manual.style.display="none",doneSubmitting()},addSource=function(username,repo,source){var filename=source.species.join("-").replace(/[\s]/g,"").toLowerCase(),path="sources/"+source.country+"/"+source.state+"/"+filename+".json",branch="add-"+source.country+"-"+source.state+"-"+filename,msg="add "+source.country+"/"+source.state+"/"+filename+".json",errMsg="Error submitting pull request. Create the file "+path+" with the JSON below.",raw=JSON.stringify(source,null,3),content=window.btoa(raw);github.getHead(repo,function(err,sha){return err?errorSubmitting(errMsg,raw):void github.branchRepo(repo,branch,sha,function(err){return err?errorSubmitting(errMsg,raw):void github.createFile(repo,branch,path,content,msg,function(err){return err?errorSubmitting(errMsg,raw):void github.pullRequest(BASE_REPO,username+":"+branch,msg,function(err){return err?errorSubmitting(errMsg,raw):void doneSubmitting()})})})})},submit=function(e){var source=void 0,filename=void 0,path=void 0,errMsg=void 0,raw=void 0;if(e.preventDefault(),github.getToken()){startSubmitting(),source={url:form.url.value,species:form.species.value.split(", "),attribution:form.attribution.value,properties:{},country:form.country.value,state:form.state.value,filetype:form.filetype.value};for(var _arr=["id","name"],_i=0;_i<_arr.length;_i++){var property=_arr[_i];source.properties[property]=form[property].value}filename=source.species.join("-").replace(/[\s]/g,"").toLowerCase(),path="sources/"+source.country+"/"+source.state+"/"+filename+".json",errMsg="Error submitting pull request. Create the file "+path+" with the JSON below.",raw=JSON.stringify(source,null,3),github.getUser(function(err,user){if(err)return errorSubmitting(errMsg,raw);var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(err,response){return err?errorSubmitting(errMsg,raw):void(response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(err){return err?errorSubmitting(errMsg,raw):void github.getRepo(repo,function(err){if(err)return errorSubmitting(errMsg,raw);var count=0,ping=window.setInterval(function(){github.getHead(repo,function(err,sha){sha?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),errorSubmitting(errMsg,raw)))})},500)})}))})})}};github.getToken()?github.getUser(signinUser):params.code&&github.accessToken(params.code,function(){window.history.replaceState({},window.document.title,window.location.pathname),github.getUser(signinUser)}),form.addEventListener("submit",submit,!1),window.document.getElementById("doneerror").addEventListener("click",doneError,!1)},function(module,exports,__webpack_require__){"use strict";function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{"default":obj}}Object.defineProperty(exports,"__esModule",{value:!0}),exports.pullRequest=exports.createFile=exports.branchRepo=exports.forkRepo=exports.getHead=exports.getRepo=exports.getUser=exports.ajax=exports.accessToken=exports.clearToken=exports.getToken=void 0;var _nanoajax=__webpack_require__(2),_nanoajax2=_interopRequireDefault(_nanoajax),API_BASE="https://api.github.com",token=window.localStorage.getItem("token"),ajax=(exports.getToken=function(){return token},exports.clearToken=function(){window.localStorage.removeItem("token")},exports.accessToken=function(code,cb){_nanoajax2["default"].ajax({url:"http://github-gatekeeper.aws.gaiagps.com/authenticate/"+code},function(code,response){token=JSON.parse(response).token,window.localStorage.setItem("token",token),cb(token)})},exports.ajax=function(options,cb){options.headers={Authorization:"token "+token},_nanoajax2["default"].ajax(options,function(code,response){var parsed=void 0;try{parsed=JSON.parse(response)}catch(e){return cb(e)}return cb(null,parsed)})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(err,response){return err?cb(err):response.message&&"Not Found"===response.message?cb(null,null):void cb(null,response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(err,response){return err?cb(err):response.message&&"Git Repository is empty."===response.message?cb(null,null):void cb(null,response.object.sha)})},exports.forkRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/forks",method:"POST"},cb)},exports.branchRepo=function(repo,branch,sha,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs",body:JSON.stringify({ref:"refs/heads/"+branch,sha:sha})},cb)},exports.createFile=function(repo,branch,path,content,message,cb){ajax({url:API_BASE+"/repos/"+repo+"/contents/"+path,method:"PUT",body:JSON.stringify({message:message,content:content,branch:branch})},cb)},exports.pullRequest=function(repo,head,message,cb){ajax({url:API_BASE+"/repos/"+repo+"/pulls",body:JSON.stringify({title:message,head:head,base:"master"})},cb)}},function(module,exports){(function(global){function getRequest(cors){return cors&&global.XDomainRequest&&!/MSIE 1/.test(navigator.userAgent)?new XDomainRequest:global.XMLHttpRequest?new XMLHttpRequest:void 0}function setDefault(obj,key,value){obj[key]=obj[key]||value}var reqfields=["responseType","withCredentials","timeout","onprogress"];exports.ajax=function(params,callback){function cb(statusCode,responseText){return function(){called||callback(req.status||statusCode,req.response||req.responseText||responseText,req),called=!0}}var headers=params.headers||{},body=params.body,method=params.method||(body?"POST":"GET"),called=!1,req=getRequest(params.cors);req.open(method,params.url,!0);var success=req.onload=cb(200);req.onreadystatechange=function(){4===req.readyState&&success()},req.onerror=cb(null,"Error"),req.ontimeout=cb(null,"Timeout"),req.onabort=cb(null,"Abort"),body&&(setDefault(headers,"X-Requested-With","XMLHttpRequest"),setDefault(headers,"Content-Type","application/x-www-form-urlencoded"));for(var field,i=0,len=reqfields.length;len>i;i++)field=reqfields[i],void 0!==params[field]&&(req[field]=params[field]);for(var field in headers)req.setRequestHeader(field,headers[field]);return req.send(body),req}}).call(exports,function(){return this}())},function(module,exports){"use strict";Object.defineProperty(exports,"__esModule",{value:!0});exports.getParams=function(){var params={},_iteratorNormalCompletion=!0,_didIteratorError=!1,_iteratorError=void 0;try{for(var _step,_iterator=window.location.search.substring(1).split("&")[Symbol.iterator]();!(_iteratorNormalCompletion=(_step=_iterator.next()).done);_iteratorNormalCompletion=!0){var param=_step.value,nv=param.split("=");nv[0]&&(params[nv[0]]=nv[1]||!0)}}catch(err){_didIteratorError=!0,_iteratorError=err}finally{try{!_iteratorNormalCompletion&&_iterator["return"]&&_iterator["return"]()}finally{if(_didIteratorError)throw _iteratorError}}return params}},function(module,exports){"use strict";function _classCallCheck(instance,Constructor){if(!(instance instanceof Constructor))throw new TypeError("Cannot call a class as a function")}Object.defineProperty(exports,"__esModule",{value:!0});var Dropdown=function Dropdown(button){var _this=this;_classCallCheck(this,Dropdown),this.open=function(e){e.preventDefault(),_this.closed?(_this.closed=!1,_this.button.nextElementSibling.style.display="block",_this.button.classList.add("selected"),window.setTimeout(function(){window.document.addEventListener("click",_this.close)},50)):_this.close()},this.close=function(e){e.preventDefault(),window.document.removeEventListener("click",_this.close),_this.button.classList.remove("selected"),_this.button.nextElementSibling.style.display="none",_this.closed=!0},this.closed=!0,this.button=button,this.button.classList.add("dropdown-btn"),this.button.addEventListener("click",this.open,!0)};exports["default"]=Dropdown}]);
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..ba06e15
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "openboundsdata",
+ "version": "1.0.0",
+ "description": "OpenBounds Data Submission",
+ "main": "src/index.js",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/OpenBounds/OpenHuntingData.git"
+ },
+ "scripts": {
+ "lint": "eslint src/*.js",
+ "build-css": "cleancss --output dist/submission.min.css src/css/*.css",
+ "build-js": "webpack && uglifyjs dist/submission.js -c -o dist/submission.min.js",
+ "build": "npm run build-js && npm run build-css"
+ },
+ "author": "OpenBounds",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/OpenBounds/OpenHuntingData/issues"
+ },
+ "homepage": "https://github.com/OpenBounds/OpenHuntingData",
+ "dependencies": {
+ "nanoajax": "^0.4.0"
+ },
+ "devDependencies": {
+ "babel-core": "^6.1.2",
+ "babel-eslint": "^4.1.4",
+ "babel-loader": "^6.0.1",
+ "babel-preset-es2015": "^6.1.2",
+ "babel-preset-stage-0": "^6.1.18",
+ "clean-css": "^3.4.7",
+ "eslint": "^1.9.0",
+ "uglify": "^0.1.5",
+ "webpack": "^1.12.3"
+ }
+}
diff --git a/src/css/dropdown.css b/src/css/dropdown.css
new file mode 100644
index 0000000..ee5dae7
--- /dev/null
+++ b/src/css/dropdown.css
@@ -0,0 +1,74 @@
+
+.dropdown-btn:focus,
+.dropdown-btn:focus:hover,
+.dropdown-btn.selected:focus {
+ border-color: #b5b5b5;
+}
+
+.dropdown-btn:focus,
+.dropdown-btn:focus:hover {
+ box-shadow: none;
+}
+
+.dropdown-btn.selected, .dropdown-btn.selected:focus {
+ background-color: #dcdcdc;
+ box-shadow: inset 0 2px 4px rgba(0,0,0,0.15);
+}
+
+.dropdown-btn::after {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ content: "";
+ vertical-align: -2px;
+ margin-left: 5px;
+ border: 4px solid;
+ border-right-color: transparent;
+ border-left-color: transparent;
+ border-bottom-color: transparent;
+}
+
+.dropdown-menu {
+ width: 180px;
+ position: absolute;
+ right: 10px;
+ top: 45px;
+ padding-top: 5px;
+ padding-bottom: 5px;
+ background-clip: padding-box;
+ box-shadow: 0 3px 12px rgba(0,0,0,0.15);
+}
+
+.dropdown-menu::before {
+ position: absolute;
+ display: inline-block;
+ content: "";
+ border: 8px solid transparent;
+ border-bottom-color: rgba(0,0,0,0.15);
+ top: -16px;
+ left: auto;
+ right: 9px;
+}
+
+.dropdown-menu::after {
+ position: absolute;
+ display: inline-block;
+ content: "";
+ border: 7px solid transparent;
+ border-bottom-color: #fff;
+ top: -14px;
+ left: auto;
+ right: 10px;
+}
+
+.dropdown-item {
+ padding: 4px 10px 4px 15px;
+ color: #333;
+}
+
+.dropdown-item:hover {
+ color: #fff;
+ text-decoration: none;
+ text-shadow: none;
+ background-color: #4078c0;
+}
diff --git a/src/css/submission.css b/src/css/submission.css
new file mode 100644
index 0000000..2ac10d5
--- /dev/null
+++ b/src/css/submission.css
@@ -0,0 +1,67 @@
+
+#main {
+ position: relative;
+ margin-top: 50px;
+}
+
+#manual textarea {
+ width: 100%;
+ font-family: monospace;
+}
+
+#doneerror {
+ float: right;
+}
+
+.form.form-inline {
+ display: inline-block;
+ margin-right: 10px;
+}
+
+.form.form-inline:last-child {
+ margin-right: 0;
+}
+
+dl.form > dd input[type="text"] {
+ width: 100%;
+}
+
+h2 {
+ margin-top: 5px;
+ margin-bottom: 30px;
+}
+
+.avatar {
+ width: 20px;
+ margin-right: 5px;
+}
+
+table {
+ width: 100%;
+ margin: 15px 0;
+}
+
+th,
+td {
+ text-align: left;
+ padding: 6px 15px;
+ border-bottom: 1px solid #E1E1E1;
+}
+
+tr:last-child td {
+ border-bottom: none;
+}
+
+td input {
+ width: 100%;
+}
+
+th:first-child,
+td:first-child {
+ padding-left: 0;
+}
+
+th:last-child,
+td:last-child {
+ padding-right: 0;
+}
diff --git a/src/dropdown.js b/src/dropdown.js
new file mode 100644
index 0000000..3d227db
--- /dev/null
+++ b/src/dropdown.js
@@ -0,0 +1,39 @@
+
+/**
+ * Create dropdown with button
+ *
+ */
+export default class Dropdown {
+ constructor (button) {
+ this.closed = true
+ this.button = button
+ this.button.classList.add('dropdown-btn')
+ this.button.addEventListener('click', this.open, true)
+ }
+
+ open = e => {
+ e.preventDefault()
+
+ if (this.closed) {
+ this.closed = false
+ this.button.nextElementSibling.style.display = 'block'
+ this.button.classList.add('selected')
+
+ window.setTimeout(() => {
+ window.document.addEventListener('click', this.close)
+ }, 50)
+ } else {
+ this.close()
+ }
+ }
+
+ close = e => {
+ e.preventDefault()
+
+ window.document.removeEventListener('click', this.close)
+
+ this.button.classList.remove('selected')
+ this.button.nextElementSibling.style.display = 'none'
+ this.closed = true
+ }
+}
diff --git a/src/github.js b/src/github.js
new file mode 100644
index 0000000..0890ff9
--- /dev/null
+++ b/src/github.js
@@ -0,0 +1,172 @@
+
+import nanoajax from 'nanoajax'
+
+const API_BASE = 'https://api.github.com'
+
+/*
+ * Github is restrictive on cookie usage on Github Pages. Use localStorage
+ * to store the OAuth token.
+ */
+let token = window.localStorage.getItem('token')
+
+
+export const getToken = () => token
+
+export const clearToken = () => {
+ window.localStorage.removeItem('token')
+}
+
+/**
+ * Get access token from Github OAuth code.
+ *
+ * @param {string} code OAuth code from Github API
+ */
+export const accessToken = (code, cb) => {
+ nanoajax.ajax({
+ url: 'http://github-gatekeeper.aws.gaiagps.com/authenticate/' + code
+ }, (code, response) => {
+ token = JSON.parse(response).token
+ window.localStorage.setItem('token', token)
+
+ cb(token)
+ })
+}
+
+/**
+ * AJAX call with Github Authorization header.
+ *
+ * @param {object} options nanoajax options
+ * @param {function} cb callback(err, data)
+ */
+export const ajax = (options, cb) => {
+ options.headers = {'Authorization': 'token ' + token}
+
+ nanoajax.ajax(options, (code, response) => {
+ let parsed
+
+ try {
+ parsed = JSON.parse(response)
+ } catch (e) {
+ return cb(e)
+ }
+
+ return cb(null, parsed)
+ })
+}
+
+/**
+ * Get user.
+ *
+ * @param {function} cb callback(err, data)
+ */
+export const getUser = cb => {
+ ajax({ url: API_BASE + '/user' }, cb)
+}
+
+/**
+ * Get repo if exists.
+ *
+ * @param {string} repo repo to check.
+ * @param {function} cb callback(err, data)
+ */
+export const getRepo = (repo, cb) => {
+ ajax({ url: API_BASE + '/repos/' + repo }, (err, response) => {
+ if (err) return cb(err)
+
+ if (response.message && response.message === 'Not Found') {
+ return cb(null, null)
+ }
+
+ cb(null, response)
+ })
+}
+
+/**
+ * Get latest commit SHA on master branch.
+ *
+ * @param {string} repo repo to get commit from.
+ * @param {function} cb callback(err, data)
+ */
+export const getHead = (repo, cb) => {
+ ajax({ url: API_BASE + '/repos/' + repo + '/git/refs/heads/master' }, (err, response) => {
+ if (err) return cb(err)
+
+ if (response.message && response.message === 'Git Repository is empty.') {
+ return cb(null, null)
+ }
+
+ cb(null, response.object.sha)
+ })
+}
+
+/**
+ * Fork repo.
+ *
+ * @param {string} repo repo to fork, ie. 'OpenBounds/OpenHuntingData'
+ * @param {function} cb callback(err, data)
+ */
+export const forkRepo = (repo, cb) => {
+ ajax({
+ url: API_BASE + '/repos/' + repo + '/forks',
+ method: 'POST'
+ }, cb)
+}
+
+/**
+ * Create branch in repo.
+ *
+ * @param {string} repo repo to create the branch in.
+ * @param {string} branch branch name.
+ * @param {string} sha SHA1 to set the branch to.
+ * @param {function} cb callback(err, data)
+ */
+export const branchRepo = (repo, branch, sha, cb) => {
+ ajax({
+ url: API_BASE + '/repos/' + repo + '/git/refs',
+ body: JSON.stringify({
+ ref: 'refs/heads/' + branch,
+ sha: sha
+ })
+ }, cb)
+}
+
+/**
+ * Create file in repo.
+ *
+ * @param {string} repo repo to create the file in.
+ * @param {string} branch branch to create the file in.
+ * @param {string} path file path.
+ * @param {base64} content base64 encoded file content.
+ * @param {string} message commit message.
+ * @param {function} cb callback(err, data)
+ */
+export const createFile = (repo, branch, path, content, message, cb) => {
+ ajax({
+ url: API_BASE + '/repos/' + repo + '/contents/' + path,
+ method: 'PUT',
+ body: JSON.stringify({
+ message: message,
+ content: content,
+ branch: branch
+ })
+ }, cb)
+}
+
+/**
+ * Create a pull request
+ *
+ * @param {string} repo repo to create the pull request in.
+ * @param {string} head branch to pull request, ie. user:add-source
+ * @param {string} message pull request title.
+ * @param {function} cb callback(err, data)
+ */
+export const pullRequest = (repo, head, message, cb) => {
+ ajax({
+ url: API_BASE + '/repos/' + repo + '/pulls',
+ body: JSON.stringify({
+ title: message,
+ head: head,
+ base: 'master'
+ })
+ }, cb)
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..e9d9f31
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,229 @@
+
+import * as github from './github'
+import * as utils from './utils'
+import Dropdown from './dropdown'
+
+const BASE_REPO = 'OpenBounds/OpenHuntingData'
+const REPO_NAME = 'OpenHuntingData'
+const TIMEOUT_SECS = 15
+
+
+let params = utils.getParams()
+ , form = window.document.forms['submission']
+ , pr = window.document.getElementById('pr')
+ , alert = window.document.getElementById('alert')
+ , manual = window.document.getElementById('manual')
+
+
+/**
+ * Sign in user, when loading the page or after authentication.
+ *
+ * @param {object} user Github user object.
+ */
+const signinUser = (err, user) => {
+ let button = window.document.getElementById('signin')
+ , signout = window.document.getElementById('signout')
+ , blank = window.document.getElementById('unauthenticated')
+
+ button.setAttribute('href', '#')
+ button.innerHTML = `
${user.login}`
+
+ blank.style.display = 'none'
+ form.style.display = 'block'
+
+ signout.addEventListener('click', signoutUser)
+
+ new Dropdown(button)
+}
+
+const signoutUser = () => {
+ github.clearToken()
+
+ window.location.href = window.location.pathname
+}
+
+/*
+ * Handle UI changes for start, done and error submitting events.
+ */
+const startSubmitting = () => {
+ pr.setAttribute('disabled', 'disabled')
+ pr.textContent = 'Submitting...'
+
+}
+
+const doneSubmitting = () => {
+ pr.removeAttribute('disabled')
+ pr.textContent = 'Submit Pull Request'
+}
+
+const errorSubmitting = (msg, content) => {
+ alert.innerHTML = msg
+ manual.getElementsByTagName('textarea')[0].textContent = content
+
+ alert.style.display = 'block'
+ manual.style.display = 'block'
+}
+
+const doneError = () => {
+ alert.innerHTML = ''
+ manual.getElementsByTagName('textarea')[0].textContent = ''
+
+ alert.style.display = 'none'
+ manual.style.display = 'none'
+
+ doneSubmitting()
+}
+
+/**
+ * Create a pull request to add a source file.
+ *
+ * Get the head sha of the master branch. Create a feature branch at that sha
+ * named after the file being submitted. In the branch, create the source file
+ * with Base64 encoded JSON pretty-printed content. Then submit a pull request
+ * of the feature branch to the base repo.
+ *
+ * @param {string} username Github user's username.
+ * @param {string} repo Repo to create the file in, ie. user/OpenHuntingData
+ * @param {object} source Source object.
+ */
+const addSource = (username, repo, source) => {
+ let filename = source.species.join('-').replace(/[\s]/g, '').toLowerCase()
+ , path = `sources/${source.country}/${source.state}/${filename}.json`
+ , branch = `add-${source.country}-${source.state}-${filename}`
+ , msg = `add ${source.country}/${source.state}/${filename}.json`
+ , errMsg = `Error submitting pull request. Create the file ${path} with the JSON below.`
+ , raw = JSON.stringify(source, null, 3)
+ , content = window.btoa(raw)
+
+ github.getHead(repo, (err, sha) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ github.branchRepo(repo, branch, sha, (err) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ github.createFile(repo, branch, path, content, msg, (err) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ github.pullRequest(BASE_REPO, username + ':' + branch, msg, (err) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ doneSubmitting()
+ })
+ })
+ })
+ })
+}
+
+/*
+ * Submit source form to Github pull request.
+ *
+ * Create a source object from the source form. Get the authenticated user and
+ * username from Github, then check if the user has already forked the repo.
+ *
+ * If the repo is found, add the source to the repo. Otherwise, create a fork
+ * of the repo, and wait until it becomes available (async call). If fork
+ * does not become available within TIMEOUT_SEC, fail.
+ */
+const submit = e => {
+ let source
+ , filename
+ , path
+ , errMsg
+ , raw
+
+ e.preventDefault()
+
+ if (!github.getToken()) return
+
+ startSubmitting()
+
+ source = {
+ url: form.url.value,
+ species: form.species.value.split(', '),
+ attribution: form.attribution.value,
+ properties: {},
+ country: form.country.value,
+ state: form.state.value,
+ filetype: form.filetype.value
+ }
+
+ for (let property of ['id', 'name']) {
+ source.properties[property] = form[property].value
+ }
+
+ filename = source.species.join('-').replace(/[\s]/g, '').toLowerCase()
+ path = `sources/${source.country}/${source.state}/${filename}.json`
+ errMsg = `Error submitting pull request. Create the file ${path} with the JSON below.`
+ raw = JSON.stringify(source, null, 3)
+
+ github.getUser((err, user) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ let username = user.login
+ , repo = username + '/' + REPO_NAME
+
+ github.getRepo(repo, (err, response) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ if (response) {
+ addSource(username, repo, source)
+ } else {
+ github.forkRepo(BASE_REPO, (err) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ github.getRepo(repo, (err) => {
+ if (err) return errorSubmitting(errMsg, raw)
+
+ let count = 0
+ , ping = window.setInterval(() => {
+ github.getHead(repo, (err, sha) => {
+ if (sha) {
+ window.clearInterval(ping)
+ addSource(username, repo, source)
+ } else {
+ count += 1
+
+ if (count > TIMEOUT_SECS * 2) {
+ window.clearInterval(ping)
+
+ errorSubmitting(errMsg, raw)
+ }
+ }
+ })
+ }, 500)
+ })
+ })
+ }
+ })
+ })
+}
+
+/*
+ * Handle user authentication and OAuth response. If the user token is present
+ * when the page loads, retrieve the user object from Github and update the
+ * UI. Otherwise, if the URL parameter `code` is set (a resposne from Github's
+ * OAuth API), exchange it for a token with Gatekeeper.
+ *
+ * Replace the window history state to prevent multiple tokens being created,
+ * then update the UI.
+ */
+if (github.getToken()) {
+ github.getUser(signinUser)
+} else {
+ if (params.code) {
+ github.accessToken(params.code, () => {
+ window.history.replaceState({}, window.document.title, window.location.pathname)
+ github.getUser(signinUser)
+ })
+ }
+}
+
+/*
+ * Listen for the form submit event, and submit a pull request of the new source.
+ */
+form.addEventListener('submit', submit, false)
+
+/*
+ * Clear error message when done button is clicked.
+ */
+window.document.getElementById('doneerror').addEventListener('click', doneError, false)
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..633aa03
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,19 @@
+
+/**
+ * Get params from URL.
+ *
+ * Modified from http://stackoverflow.com/a/979996/1377021
+ */
+export const getParams = () => {
+ let params = {}
+
+ for (let param of window.location.search.substring(1).split('&')) {
+ let nv = param.split('=')
+
+ if (!nv[0]) continue;
+
+ params[nv[0]] = nv[1] || true
+ }
+
+ return params
+}
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..8494ecb
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,17 @@
+
+webpack = require('webpack')
+
+module.exports = {
+ entry: "./src/index.js",
+ module: {
+ loaders: [{
+ test: /\.js$/,
+ exclude: /node_modules/,
+ loader: "babel-loader"
+ }]
+ },
+ output: {
+ path: './dist/',
+ filename: 'submission.js'
+ }
+};