From 8542c355f8444875a3846cb84d1b70c94434911d Mon Sep 17 00:00:00 2001 From: Nathan Cahill Date: Mon, 9 Nov 2015 16:42:30 -0700 Subject: [PATCH 1/5] add submission app --- .babelrc | 3 + .eslintrc | 10 +++ README.md | 3 + package.json | 32 ++++++++++ src/github.js | 150 +++++++++++++++++++++++++++++++++++++++++++ src/index.js | 157 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils.js | 19 ++++++ webpack.config.js | 17 +++++ 8 files changed, 391 insertions(+) create mode 100644 .babelrc create mode 100644 .eslintrc create mode 100644 README.md create mode 100644 package.json create mode 100644 src/github.js create mode 100644 src/index.js create mode 100644 src/utils.js create mode 100644 webpack.config.js diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..c13c5f6 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "presets": ["es2015"] +} 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 new file mode 100644 index 0000000..0304964 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +## 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/package.json b/package.json new file mode 100644 index 0000000..32a6437 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "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": "webpack && uglifyjs dist/submission.js -c -o dist/submission.min.js" + }, + "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", + "eslint": "^1.9.0", + "uglify": "^0.1.5", + "webpack": "^1.12.3" + } +} diff --git a/src/github.js b/src/github.js new file mode 100644 index 0000000..7b2c6fc --- /dev/null +++ b/src/github.js @@ -0,0 +1,150 @@ + +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 + +/** + * 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 + */ +export const ajax = (options, cb) => { + options.headers = {'Authorization': 'token ' + token} + + nanoajax.ajax(options, (code, response) => cb(JSON.parse(response))) +} + +/** + * Get user. + * + * @param {function} cb callback + */ +export const getUser = cb => { + ajax({ url: API_BASE + '/user' }, cb) +} + +/** + * Get repo if exists. + * + * @param {string} repo repo to check. + * @param {function} cb callback + */ +export const getRepo = (repo, cb) => { + ajax({ url: API_BASE + '/repos/' + repo }, response => { + if (response.message && response.message === 'Not Found') { + cb(null) + } else { + cb(response) + } + }) +} + +/** + * Get latest commit SHA on master branch. + * + * @param {string} repo repo to get commit from. + * @param {function} cb callback + */ +export const getHead = (repo, cb) => { + ajax({ url: API_BASE + '/repos/' + repo + '/git/refs/heads/master' }, response => { + cb(response.object.sha) + }) +} + +/** + * Fork repo. + * + * @param {string} repo repo to fork, ie. 'trailbehind/OpenHuntingData' + * @param {function} cb callback + */ +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 + */ +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 + */ +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 + */ +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..f766b8e --- /dev/null +++ b/src/index.js @@ -0,0 +1,157 @@ + +import * as github from './github' +import * as utils from './utils' + +const BASE_REPO = 'trailbehind/OpenHuntingData' +const REPO_NAME = 'OpenHuntingData' +const TIMEOUT_SECS = 15 + + +let params = utils.getParams() + , form = window.document.forms['submission'] + , pr = window.document.getElementById('pr') + + +/** + * Sign in user, when loading the page or after authentication. + * + * @param {object} user Github user object. + */ +const signinUser = user => { + let button = window.document.getElementById('signin') + , blank = window.document.getElementById('unauthenticated') + + button.setAttribute('href', '#') + button.innerHTML = ` ${user.login}` + + blank.style.display = 'none' + form.style.display = 'block' +} + +/** + * Handle UI for start and done submitting events. + */ +const startSubmitting = () => { + pr.setAttribute('disabled', 'disabled') + pr.textContent = 'Submitting...' + +} + +const doneSubmitting = () => { + pr.removeAttribute('disabeld') + pr.textContent = 'Submit Pull Request' +} + +/** + * 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}` + , message = `add ${source.country}/${source.state}/${filename}.json` + , content = window.btoa(JSON.stringify(source, null, 3)) + + github.getHead(repo, sha => { + github.branchRepo(repo, branch, sha, () => { + github.createFile(repo, branch, path, content, message, () => { + github.pullRequest(BASE_REPO, username + ':' + branch, message, () => { + 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 + + e.preventDefault() + + if (!github.getToken()) return + + startSubmitting() + + source = { + url: form.url.value, + species: form.species.value.split(', '), + attribution: form.attribution.value, + country: form.country.value, + state: form.state.value, + filetype: form.filetype.value + } + + github.getUser(user => { + let username = user.login + , repo = username + '/' + REPO_NAME + + github.getRepo(repo, response => { + if (response) { + addSource(username, repo, source) + } else { + github.forkRepo(BASE_REPO, () => { + let count = 0 + , ping = window.setInterval(() => { + github.getRepo(repo, response => { + if (response) { + window.clearInterval(ping) + addSource(username, repo, source) + } else { + count += 1 + + if (count > TIMEOUT_SECS * 2) { + window.clearInterval(ping) + doneSubmitting() + } + } + }) + }, 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) 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' + } +}; From f466b805c4d64a6e298f5da03c3d1eefd47fc490 Mon Sep 17 00:00:00 2001 From: Nathan Cahill Date: Mon, 9 Nov 2015 16:43:52 -0700 Subject: [PATCH 2/5] add build for gh-pages --- dist/submission.min.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 dist/submission.min.js diff --git a/dist/submission.min.js b/dist/submission.min.js new file mode 100644 index 0000000..72a8cb9 --- /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 _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),BASE_REPO="trailbehind/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),signinUser=function(user){var button=window.document.getElementById("signin"),blank=window.document.getElementById("unauthenticated");button.setAttribute("href","#"),button.innerHTML=' '+user.login,blank.style.display="none",form.style.display="block"},startSubmitting=function(){pr.setAttribute("disabled","disabled"),pr.textContent="Submitting..."},doneSubmitting=function(){pr.removeAttribute("disabeld"),pr.textContent="Submit Pull Request"},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,message="add "+source.country+"/"+source.state+"/"+filename+".json",content=window.btoa(JSON.stringify(source,null,3));github.getHead(repo,function(sha){github.branchRepo(repo,branch,sha,function(){github.createFile(repo,branch,path,content,message,function(){github.pullRequest(BASE_REPO,username+":"+branch,message,function(){doneSubmitting()})})})})},submit=function(e){var source=void 0;e.preventDefault(),github.getToken()&&(startSubmitting(),source={url:form.url.value,species:form.species.value.split(", "),attribution:form.attribution.value,country:form.country.value,state:form.state.value,filetype:form.filetype.value},github.getUser(function(user){var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(response){response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(){var count=0,ping=window.setInterval(function(){github.getRepo(repo,function(response){response?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),doneSubmitting()))})},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)},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.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.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){return cb(JSON.parse(response))})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(response){cb(response.message&&"Not Found"===response.message?null:response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(response){cb(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}}]); \ No newline at end of file From 233a3ed2fe75aa8eeb48caa02ccdce64fd13ea8b Mon Sep 17 00:00:00 2001 From: Nathan Cahill Date: Fri, 13 Nov 2015 00:19:40 -0700 Subject: [PATCH 3/5] add signout --- .babelrc | 2 +- dist/submission.min.js | 2 +- package.json | 6 +++- src/css/dropdown.css | 74 ++++++++++++++++++++++++++++++++++++++++++ src/css/submission.css | 24 ++++++++++++++ src/dropdown.js | 39 ++++++++++++++++++++++ src/github.js | 4 +++ src/index.js | 13 ++++++++ 8 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 src/css/dropdown.css create mode 100644 src/css/submission.css create mode 100644 src/dropdown.js diff --git a/.babelrc b/.babelrc index c13c5f6..eaf3238 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,3 @@ { - "presets": ["es2015"] + "presets": ["es2015", "stage-0"] } diff --git a/dist/submission.min.js b/dist/submission.min.js index 72a8cb9..5ae3fba 100644 --- a/dist/submission.min.js +++ b/dist/submission.min.js @@ -1 +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 _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),BASE_REPO="trailbehind/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),signinUser=function(user){var button=window.document.getElementById("signin"),blank=window.document.getElementById("unauthenticated");button.setAttribute("href","#"),button.innerHTML=' '+user.login,blank.style.display="none",form.style.display="block"},startSubmitting=function(){pr.setAttribute("disabled","disabled"),pr.textContent="Submitting..."},doneSubmitting=function(){pr.removeAttribute("disabeld"),pr.textContent="Submit Pull Request"},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,message="add "+source.country+"/"+source.state+"/"+filename+".json",content=window.btoa(JSON.stringify(source,null,3));github.getHead(repo,function(sha){github.branchRepo(repo,branch,sha,function(){github.createFile(repo,branch,path,content,message,function(){github.pullRequest(BASE_REPO,username+":"+branch,message,function(){doneSubmitting()})})})})},submit=function(e){var source=void 0;e.preventDefault(),github.getToken()&&(startSubmitting(),source={url:form.url.value,species:form.species.value.split(", "),attribution:form.attribution.value,country:form.country.value,state:form.state.value,filetype:form.filetype.value},github.getUser(function(user){var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(response){response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(){var count=0,ping=window.setInterval(function(){github.getRepo(repo,function(response){response?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),doneSubmitting()))})},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)},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.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.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){return cb(JSON.parse(response))})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(response){cb(response.message&&"Not Found"===response.message?null:response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(response){cb(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}}]); \ No newline at end of file +!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="trailbehind/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),signinUser=function(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("disabeld"),pr.textContent="Submit Pull Request"},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,message="add "+source.country+"/"+source.state+"/"+filename+".json",content=window.btoa(JSON.stringify(source,null,3));github.getHead(repo,function(sha){github.branchRepo(repo,branch,sha,function(){github.createFile(repo,branch,path,content,message,function(){github.pullRequest(BASE_REPO,username+":"+branch,message,function(){doneSubmitting()})})})})},submit=function(e){var source=void 0;e.preventDefault(),github.getToken()&&(startSubmitting(),source={url:form.url.value,species:form.species.value.split(", "),attribution:form.attribution.value,country:form.country.value,state:form.state.value,filetype:form.filetype.value},github.getUser(function(user){var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(response){response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(){var count=0,ping=window.setInterval(function(){github.getRepo(repo,function(response){response?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),doneSubmitting()))})},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)},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){return cb(JSON.parse(response))})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(response){cb(response.message&&"Not Found"===response.message?null:response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(response){cb(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 index 32a6437..ba06e15 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ }, "scripts": { "lint": "eslint src/*.js", - "build": "webpack && uglifyjs dist/submission.js -c -o dist/submission.min.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", @@ -25,6 +27,8 @@ "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..e00b83b --- /dev/null +++ b/src/css/submission.css @@ -0,0 +1,24 @@ + +#main { + position: relative; + margin-top: 50px; +} + +.form.form-inline { + display: inline-block; + margin-right: 10px; +} + +dl.form > dd input[type="text"] { + width: 100%; +} + +h2 { + margin-top: 5px; + margin-bottom: 30px; +} + +.avatar { + width: 20px; + margin-right: 5px; +} 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 index 7b2c6fc..4ed5447 100644 --- a/src/github.js +++ b/src/github.js @@ -12,6 +12,10 @@ let token = window.localStorage.getItem('token') export const getToken = () => token +export const clearToken = () => { + window.localStorage.removeItem('token') +} + /** * Get access token from Github OAuth code. * diff --git a/src/index.js b/src/index.js index f766b8e..4ff3646 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import * as github from './github' import * as utils from './utils' +import Dropdown from './dropdown' const BASE_REPO = 'trailbehind/OpenHuntingData' const REPO_NAME = 'OpenHuntingData' @@ -19,6 +20,7 @@ let params = utils.getParams() */ const signinUser = user => { let button = window.document.getElementById('signin') + , signout = window.document.getElementById('signout') , blank = window.document.getElementById('unauthenticated') button.setAttribute('href', '#') @@ -26,6 +28,16 @@ const signinUser = user => { 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 } /** @@ -155,3 +167,4 @@ if (github.getToken()) { * Listen for the form submit event, and submit a pull request of the new source. */ form.addEventListener('submit', submit, false) + From c5b6fd2e93a2e914c5f5d1cf354b82911549102e Mon Sep 17 00:00:00 2001 From: Nathan Cahill Date: Fri, 13 Nov 2015 01:34:24 -0700 Subject: [PATCH 4/5] add feature properties mapping - closes #41 --- dist/submission.min.css | 1 + dist/submission.min.js | 2 +- src/css/submission.css | 30 ++++++++++++++++++++++++++++++ src/index.js | 5 +++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 dist/submission.min.css diff --git a/dist/submission.min.css b/dist/submission.min.css new file mode 100644 index 0000000..f465168 --- /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}.form.form-inline{display:inline-block;margin-right:10px}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 index 5ae3fba..5fb6407 100644 --- a/dist/submission.min.js +++ b/dist/submission.min.js @@ -1 +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="trailbehind/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),signinUser=function(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("disabeld"),pr.textContent="Submit Pull Request"},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,message="add "+source.country+"/"+source.state+"/"+filename+".json",content=window.btoa(JSON.stringify(source,null,3));github.getHead(repo,function(sha){github.branchRepo(repo,branch,sha,function(){github.createFile(repo,branch,path,content,message,function(){github.pullRequest(BASE_REPO,username+":"+branch,message,function(){doneSubmitting()})})})})},submit=function(e){var source=void 0;e.preventDefault(),github.getToken()&&(startSubmitting(),source={url:form.url.value,species:form.species.value.split(", "),attribution:form.attribution.value,country:form.country.value,state:form.state.value,filetype:form.filetype.value},github.getUser(function(user){var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(response){response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(){var count=0,ping=window.setInterval(function(){github.getRepo(repo,function(response){response?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),doneSubmitting()))})},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)},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){return cb(JSON.parse(response))})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(response){cb(response.message&&"Not Found"===response.message?null:response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(response){cb(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 +!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="trailbehind/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),signinUser=function(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("disabeld"),pr.textContent="Submit Pull Request"},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,message="add "+source.country+"/"+source.state+"/"+filename+".json",content=window.btoa(JSON.stringify(source,null,3));github.getHead(repo,function(sha){github.branchRepo(repo,branch,sha,function(){github.createFile(repo,branch,path,content,message,function(){github.pullRequest(BASE_REPO,username+":"+branch,message,function(){doneSubmitting()})})})})},submit=function(e){var source=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}github.getUser(function(user){var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(response){response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(){var count=0,ping=window.setInterval(function(){github.getRepo(repo,function(response){response?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),doneSubmitting()))})},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)},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){return cb(JSON.parse(response))})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(response){cb(response.message&&"Not Found"===response.message?null:response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(response){cb(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/src/css/submission.css b/src/css/submission.css index e00b83b..1fc0923 100644 --- a/src/css/submission.css +++ b/src/css/submission.css @@ -22,3 +22,33 @@ h2 { 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/index.js b/src/index.js index 4ff3646..92b1d18 100644 --- a/src/index.js +++ b/src/index.js @@ -107,11 +107,16 @@ const submit = e => { 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 + } + github.getUser(user => { let username = user.login , repo = username + '/' + REPO_NAME From 4ae8885ec61c60674f1b2022ba6d2768f1c03409 Mon Sep 17 00:00:00 2001 From: Nathan Cahill Date: Sat, 14 Nov 2015 14:59:02 -0700 Subject: [PATCH 5/5] add error handling --- dist/submission.min.css | 2 +- dist/submission.min.js | 2 +- src/css/submission.css | 13 +++++ src/github.js | 50 ++++++++++++------ src/index.js | 110 ++++++++++++++++++++++++++++++---------- 5 files changed, 131 insertions(+), 46 deletions(-) diff --git a/dist/submission.min.css b/dist/submission.min.css index f465168..8ea1186 100644 --- a/dist/submission.min.css +++ b/dist/submission.min.css @@ -1 +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}.form.form-inline{display:inline-block;margin-right:10px}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 +.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 index 5fb6407..1fb5c26 100644 --- a/dist/submission.min.js +++ b/dist/submission.min.js @@ -1 +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="trailbehind/OpenHuntingData",REPO_NAME="OpenHuntingData",TIMEOUT_SECS=15,params=utils.getParams(),form=window.document.forms.submission,pr=window.document.getElementById("pr"),signinUser=function(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("disabeld"),pr.textContent="Submit Pull Request"},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,message="add "+source.country+"/"+source.state+"/"+filename+".json",content=window.btoa(JSON.stringify(source,null,3));github.getHead(repo,function(sha){github.branchRepo(repo,branch,sha,function(){github.createFile(repo,branch,path,content,message,function(){github.pullRequest(BASE_REPO,username+":"+branch,message,function(){doneSubmitting()})})})})},submit=function(e){var source=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}github.getUser(function(user){var username=user.login,repo=username+"/"+REPO_NAME;github.getRepo(repo,function(response){response?addSource(username,repo,source):github.forkRepo(BASE_REPO,function(){var count=0,ping=window.setInterval(function(){github.getRepo(repo,function(response){response?(window.clearInterval(ping),addSource(username,repo,source)):(count+=1,count>2*TIMEOUT_SECS&&(window.clearInterval(ping),doneSubmitting()))})},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)},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){return cb(JSON.parse(response))})});exports.getUser=function(cb){ajax({url:API_BASE+"/user"},cb)},exports.getRepo=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo},function(response){cb(response.message&&"Not Found"===response.message?null:response)})},exports.getHead=function(repo,cb){ajax({url:API_BASE+"/repos/"+repo+"/git/refs/heads/master"},function(response){cb(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 +!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/src/css/submission.css b/src/css/submission.css index 1fc0923..2ac10d5 100644 --- a/src/css/submission.css +++ b/src/css/submission.css @@ -4,11 +4,24 @@ 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%; } diff --git a/src/github.js b/src/github.js index 4ed5447..0890ff9 100644 --- a/src/github.js +++ b/src/github.js @@ -36,18 +36,28 @@ export const accessToken = (code, cb) => { * AJAX call with Github Authorization header. * * @param {object} options nanoajax options - * @param {function} cb callback + * @param {function} cb callback(err, data) */ export const ajax = (options, cb) => { options.headers = {'Authorization': 'token ' + token} - nanoajax.ajax(options, (code, response) => cb(JSON.parse(response))) + 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 + * @param {function} cb callback(err, data) */ export const getUser = cb => { ajax({ url: API_BASE + '/user' }, cb) @@ -57,15 +67,17 @@ export const getUser = cb => { * Get repo if exists. * * @param {string} repo repo to check. - * @param {function} cb callback + * @param {function} cb callback(err, data) */ export const getRepo = (repo, cb) => { - ajax({ url: API_BASE + '/repos/' + repo }, response => { + ajax({ url: API_BASE + '/repos/' + repo }, (err, response) => { + if (err) return cb(err) + if (response.message && response.message === 'Not Found') { - cb(null) - } else { - cb(response) + return cb(null, null) } + + cb(null, response) }) } @@ -73,19 +85,25 @@ export const getRepo = (repo, cb) => { * Get latest commit SHA on master branch. * * @param {string} repo repo to get commit from. - * @param {function} cb callback + * @param {function} cb callback(err, data) */ export const getHead = (repo, cb) => { - ajax({ url: API_BASE + '/repos/' + repo + '/git/refs/heads/master' }, response => { - cb(response.object.sha) + 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. 'trailbehind/OpenHuntingData' - * @param {function} cb callback + * @param {string} repo repo to fork, ie. 'OpenBounds/OpenHuntingData' + * @param {function} cb callback(err, data) */ export const forkRepo = (repo, cb) => { ajax({ @@ -100,7 +118,7 @@ export const forkRepo = (repo, cb) => { * @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 + * @param {function} cb callback(err, data) */ export const branchRepo = (repo, branch, sha, cb) => { ajax({ @@ -120,7 +138,7 @@ export const branchRepo = (repo, branch, sha, cb) => { * @param {string} path file path. * @param {base64} content base64 encoded file content. * @param {string} message commit message. - * @param {function} cb callback + * @param {function} cb callback(err, data) */ export const createFile = (repo, branch, path, content, message, cb) => { ajax({ @@ -140,7 +158,7 @@ export const createFile = (repo, branch, path, content, message, cb) => { * @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 + * @param {function} cb callback(err, data) */ export const pullRequest = (repo, head, message, cb) => { ajax({ diff --git a/src/index.js b/src/index.js index 92b1d18..e9d9f31 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ import * as github from './github' import * as utils from './utils' import Dropdown from './dropdown' -const BASE_REPO = 'trailbehind/OpenHuntingData' +const BASE_REPO = 'OpenBounds/OpenHuntingData' const REPO_NAME = 'OpenHuntingData' const TIMEOUT_SECS = 15 @@ -11,6 +11,8 @@ 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') /** @@ -18,7 +20,7 @@ let params = utils.getParams() * * @param {object} user Github user object. */ -const signinUser = user => { +const signinUser = (err, user) => { let button = window.document.getElementById('signin') , signout = window.document.getElementById('signout') , blank = window.document.getElementById('unauthenticated') @@ -40,8 +42,8 @@ const signoutUser = () => { window.location.href = window.location.pathname } -/** - * Handle UI for start and done submitting events. +/* + * Handle UI changes for start, done and error submitting events. */ const startSubmitting = () => { pr.setAttribute('disabled', 'disabled') @@ -50,10 +52,28 @@ const startSubmitting = () => { } const doneSubmitting = () => { - pr.removeAttribute('disabeld') + 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. * @@ -70,13 +90,23 @@ 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}` - , message = `add ${source.country}/${source.state}/${filename}.json` - , content = window.btoa(JSON.stringify(source, null, 3)) + , 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) - github.getHead(repo, sha => { - github.branchRepo(repo, branch, sha, () => { - github.createFile(repo, branch, path, content, message, () => { - github.pullRequest(BASE_REPO, username + ':' + branch, message, () => { doneSubmitting() }) }) @@ -96,6 +126,10 @@ const addSource = (username, repo, source) => { */ const submit = e => { let source + , filename + , path + , errMsg + , raw e.preventDefault() @@ -117,31 +151,47 @@ const submit = e => { source.properties[property] = form[property].value } - github.getUser(user => { + 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, response => { + github.getRepo(repo, (err, response) => { + if (err) return errorSubmitting(errMsg, raw) + if (response) { addSource(username, repo, source) } else { - github.forkRepo(BASE_REPO, () => { - let count = 0 - , ping = window.setInterval(() => { - github.getRepo(repo, response => { - if (response) { - window.clearInterval(ping) - addSource(username, repo, source) - } else { - count += 1 - - if (count > TIMEOUT_SECS * 2) { + 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) - doneSubmitting() + addSource(username, repo, source) + } else { + count += 1 + + if (count > TIMEOUT_SECS * 2) { + window.clearInterval(ping) + + errorSubmitting(errMsg, raw) + } } - } - }) - }, 500) + }) + }, 500) + }) }) } }) @@ -173,3 +223,7 @@ if (github.getToken()) { */ form.addEventListener('submit', submit, false) +/* + * Clear error message when done button is clicked. + */ +window.document.getElementById('doneerror').addEventListener('click', doneError, false)