forked from bkkhack/hackmap
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
still requires a lot of work, but the basic use cases are covered.
- Loading branch information
Showing
5 changed files
with
329 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<!DOCTYPE html> | ||
<html lang="en"> | ||
<head> | ||
<meta charset="UTF-8"> | ||
<title>Success</title> | ||
</head> | ||
<body> | ||
Good job! Redirecting... | ||
<script> | ||
// github.com will redirect to this page, after the user allows oauth access. | ||
// this page is in a popup window / new tab. We read the `code` query parameter | ||
// and send it to the main (opener) window using an event. | ||
var code = window.location.href.match(/\?code=(.*)/); | ||
if(code) { | ||
code = code[1]; | ||
window.opener.dispatchEvent(new CustomEvent( | ||
"oauth-code-received", | ||
{ detail: code } | ||
)); | ||
} | ||
</script> | ||
</body> | ||
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import axios from 'axios' // ajax library | ||
import serialization from './github-serialization.js' | ||
import auth from './github-oauth.js' | ||
|
||
/* | ||
* GitHub Issues API. Handles saving and polling of | ||
* projects, stored as github issue comments. | ||
* | ||
* Expects a configuration of: | ||
* { | ||
* organization: "bkkhack", | ||
* repository: "hackmap", | ||
* label: "BKKHack Main Thread", | ||
* pollIntervalSeconds: 60, | ||
* onAuthenticationRequired: () => Promise(token) | ||
* onProjectsUpdated: (projects) => () | ||
* } | ||
*/ | ||
export default class GitHubIssueService { | ||
constructor(config) { | ||
this.config = config; | ||
this.ajax = this.createGithubApiClient(config.organization, config.repository); | ||
|
||
// if the user has already logged in (e.g. in a previous session), use the | ||
// authenticated client for all operations. Authenticated calls have a | ||
// higher rate limit, even for calls that don't require authentication | ||
if(auth.isLoggedIn()) { | ||
this.ensureAuthenticatedClient(); | ||
} | ||
|
||
// issue ajax requests to load data from github | ||
// the model will be updated periodically from the callbacks | ||
this.ajax.get('issues', { params: { labels: this.config.label } }) | ||
.then(issueResponse => { | ||
if(!(issueResponse.data || issueResponse.data.length)) { | ||
throw "Could not find an open issue labeled: " + this.config.label; | ||
} | ||
this.issueNumber = issueResponse.data[0].number; | ||
}) | ||
.then(() => this.pollIssueForComments()) | ||
.catch(err => this.reportError(err)); | ||
} | ||
|
||
postNewProject(project) { | ||
return this.ensureAuthenticatedClient() | ||
.then(() => { | ||
let body = serialization.serializeProjectToComment(project); | ||
return this.ajax | ||
.post('issues/' + this.issueNumber + '/comments', { body: body }); | ||
}) | ||
.then((response) => { | ||
return serialization.deserializeCommentToProject(response.data); | ||
}); | ||
} | ||
|
||
updateProject(project) { | ||
return this.ensureAuthenticatedClient() | ||
.then(() => { | ||
let body = serialization.serializeProjectToComment(project); | ||
return this.ajax.patch('issues/comments/' + project.id, { body: body }); | ||
}) | ||
.then(response => { | ||
return serialization.deserializeCommentToProject(response.data); | ||
}); | ||
} | ||
|
||
pollIssueForComments() { | ||
var commentsUrl = "issues/" + this.issueNumber + "/comments"; | ||
|
||
var poll = () => { | ||
console.log("polling..."); | ||
return this.ajax.get(commentsUrl) | ||
.then((response) => { | ||
var projects = response.data | ||
.map(serialization.deserializeCommentToProject); | ||
this.config.onProjectsUpdated(projects); | ||
}) | ||
.catch((err) => this.reportError(err)); | ||
} | ||
|
||
window.setInterval(poll, 1000 * this.config.pollIntervalSeconds); | ||
return poll(); | ||
} | ||
|
||
ensureAuthenticatedClient() { | ||
return this.config.onAuthenticationRequired() | ||
.then(token => { | ||
this.ajax = this.createGithubApiClient( | ||
this.config.organization, | ||
this.config.repository, | ||
token); | ||
}); | ||
} | ||
|
||
createGithubApiClient(org, repo, token) { | ||
var ajaxConfig = { | ||
baseURL: 'https://api.github.com/repos/' + org + '/' + repo + '/', | ||
}; | ||
if(token) { | ||
ajaxConfig.headers = { | ||
'Authorization': 'token ' + token | ||
}; | ||
} | ||
return axios.create(ajaxConfig); | ||
} | ||
|
||
reportError(err) { | ||
console.log(err); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import axios from 'axios' // ajax library | ||
|
||
const clientId = "1b717d04ec7f3615bb18"; | ||
// the following url needs to match the app configuration on github.com | ||
const landingPage = window.location.origin + "/oauth_redirect.html"; | ||
const tokenKey = "token"; | ||
const oAuthUrl = `https://github.com/login/oauth/authorize?scope=public_repo&redirect_uri=${landingPage}&client_id=${clientId}`; | ||
const codeToTokenUrl = "https://bkkhackmap.herokuapp.com/authenticate/"; | ||
|
||
// ping to wake heroku instance. we don't care about the response. we just want the oauth login to quick. | ||
axios.options(codeToTokenUrl); | ||
|
||
export default { | ||
isLoggedIn: function() { | ||
return !!window.localStorage.getItem(tokenKey); | ||
}, | ||
// implements https://developer.github.com/v3/oauth/#web-application-flow | ||
getOAuthToken: function() { | ||
return new Promise((resolve, reject) => { | ||
// if we already have a token, just return it | ||
var token = window.localStorage.getItem(tokenKey); | ||
if(token) { | ||
resolve(token); | ||
return; | ||
} | ||
// otherwise, open a window to do the github login | ||
// github will redirect to landingPage, which will fire an | ||
// event with the github auth code | ||
var oauthWindow = window.open(oAuthUrl); // needs to be a new window. github can't be embedded in an iframe for security reasons | ||
window.addEventListener("oauth-code-received", function(event) { | ||
var code = event.detail; | ||
// we received the code; exchange it for an auth token. | ||
// this part needs to interact with a server because we need to hide the oAuth secret. | ||
// the server is just a heroku app running https://github.com/prose/gatekeeper | ||
axios.get(codeToTokenUrl + code) | ||
.then(response => { | ||
window.localStorage.setItem(tokenKey, response.data.token); | ||
oauthWindow.close(); | ||
resolve(response.data.token); | ||
}) | ||
.catch(error => reject(error)); | ||
}); | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
|
||
const commentExtractionRegex = /<!-- ([.\d]+),([.\d]+) -->/; | ||
|
||
function parseCoordinatesFromComment(commentLines) { | ||
var lastLine = commentLines.slice(-1)[0]; | ||
var matches = commentExtractionRegex.exec(lastLine); | ||
|
||
return matches ? {x: matches[1], y: matches[2]} | ||
: {x: 0, y: 0}; | ||
} | ||
|
||
export default { | ||
deserializeCommentToProject: function(comment) { | ||
var textLines = comment.body.split('\r\n'); | ||
var coords = parseCoordinatesFromComment(textLines); | ||
return { | ||
id: comment.id, | ||
title: textLines[0], | ||
description: textLines[1], | ||
username: comment.user.login, | ||
avatar_thumbnail: comment.user.avatar_url + "&s=" + 40, | ||
avatar: comment.user.avatar_url + "&s=" + 120, | ||
x: coords.x, | ||
y: coords.y, | ||
}; | ||
}, | ||
|
||
serializeProjectToComment: function(project) { | ||
let body = project.title + '\r\n' + | ||
(project.description || '') + '\r\n' + | ||
`<!-- ${project.x},${project.y} -->`; | ||
return body; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import Vue from 'vue'; | ||
import GitHubIssueService from './github-issues.js' | ||
import GitHubAuth from './github-oauth.js' | ||
|
||
const model = { | ||
projects: [], | ||
mapWidth:10, | ||
mapHeight:0, | ||
form: { | ||
isOpen: false, | ||
title: "", | ||
description: "" | ||
}, | ||
selectedProject: {}, | ||
authenticationUrl: "" | ||
} | ||
const githubIssue = new GitHubIssueService({ | ||
organization: "waf", | ||
repository: "hackmap", | ||
label: "BKKHack Main Thread", | ||
onAuthenticationRequired: GitHubAuth.getOAuthToken, | ||
pollIntervalSeconds: 60, | ||
onProjectsUpdated: projects => { | ||
model.projects = projects; | ||
} | ||
}); | ||
|
||
|
||
var projectsColumn = new Vue({ | ||
el: '.projects.side-column', | ||
data: model, | ||
methods: { | ||
toggleForm: function() { | ||
if(!this.form.isOpen) { | ||
this.form.isOpen = true; | ||
return; | ||
} | ||
this.form.title = this.form.title.trim(); | ||
this.form.description = this.form.description.trim(); | ||
|
||
if(!this.form.title && !this.form.description) { | ||
//empty form, close it. | ||
this.form.isOpen = false; | ||
return; | ||
} else if(!this.form.title) { | ||
//user filled in a description, but not a title | ||
alert("Please enter a topic for your project"); | ||
return; | ||
} else { | ||
// title and optional description provided, save the text | ||
githubIssue.postNewProject(this.form) | ||
.then(project => { | ||
// add the new project and blank the form. | ||
this.projects.push(project); | ||
this.form.title = ""; | ||
this.form.description = ""; | ||
}) | ||
.catch(err => console.error(err)); | ||
this.form.isOpen = false; | ||
} | ||
}, | ||
drag: function(event) { | ||
console.log(event); | ||
var avatar = event.target.querySelector("img"); | ||
event.dataTransfer.setDragImage(avatar, 20, 20); | ||
var projectId = event.target.dataset.id; | ||
event.dataTransfer.setData("projectId", projectId); | ||
} | ||
} | ||
}) | ||
|
||
var map = new Vue({ | ||
el: '.center-column', | ||
data: model, | ||
methods: { | ||
drop: function(event) { | ||
var projectId = event.dataTransfer.getData("projectId"); | ||
var project = model.projects.filter(p => p.id == projectId)[0]; | ||
this.updateMapDimensions(); | ||
var oldX = project.x; | ||
var oldY = project.y; | ||
project.x = event.offsetX / model.mapWidth; | ||
project.y = event.offsetY / model.mapHeight; | ||
githubIssue.updateProject(project) | ||
.catch(err => { | ||
console.log("update rejected"); | ||
project.x = oldX; | ||
project.y = oldY; | ||
}); | ||
}, | ||
dragover: function(event) { | ||
event.preventDefault(); // mark this element as drop target. | ||
event.dataTransfer.dropEffect = 'copy'; | ||
}, | ||
drag: function(event) { | ||
var projectId = event.target.dataset.id; | ||
event.dataTransfer.setData("projectId", projectId); | ||
}, | ||
updateMapDimensions: function() { | ||
var floorplan = this.$el.querySelector('.floorplan'); | ||
model.mapWidth = floorplan.clientWidth; | ||
model.mapHeight = floorplan.clientHeight; | ||
}, | ||
}, | ||
mounted: function () { | ||
window.addEventListener('resize', this.updateMapDimensions); | ||
this.updateMapDimensions(); | ||
}, | ||
beforeDestroy: function () { | ||
window.removeEventListener('resize', this.updateMapDimensions); | ||
} | ||
}); | ||
|
||
var projectDetails = new Vue({ | ||
el: '.details.side-column', | ||
data: model | ||
}) |