commit
6ad85d68f7
@ -1,2 +1,11 @@
|
||||
# ictgj-voting
|
||||
The ICT GameJam Voting Application
|
||||
|
||||
pass in the `-dev` flag to enable development mode (load assets from file system instead of embedded).
|
||||
|
||||
Uses 'esc' for embedding assets:
|
||||
https://github.com/mjibson/esc
|
||||
|
||||
TODO: Build Instructions
|
||||
|
||||
TODO: Contribution Instructions
|
||||
|
@ -55,3 +55,31 @@ func handleAdminClients(w http.ResponseWriter, req *http.Request, page *pageData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func clientIsAuthenticated(cid string, req *http.Request) bool {
|
||||
return clientIsServer(req) || dbClientIsAuth(cid)
|
||||
}
|
||||
|
||||
func clientIsServer(req *http.Request) bool {
|
||||
clientIp, _, _ := net.SplitHostPort(req.RemoteAddr)
|
||||
ifaces, err := net.Interfaces()
|
||||
if err == nil {
|
||||
for _, i := range ifaces {
|
||||
if addrs, err := i.Addrs(); err == nil {
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if clientIp == ip.String() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@ -40,8 +41,25 @@ func handleAdmin(w http.ResponseWriter, req *http.Request) {
|
||||
handleAdminGames(w, req, page)
|
||||
case "clients":
|
||||
handleAdminClients(w, req, page)
|
||||
case "votes":
|
||||
handleAdminVotes(w, req, page)
|
||||
case "mode":
|
||||
handleAdminSetMode(w, req, page)
|
||||
default:
|
||||
page.TemplateData = getCondorcetResult()
|
||||
page.show("admin-main.html", w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleAdminSetMode(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||
vars := mux.Vars(req)
|
||||
newMode, err := strconv.Atoi(vars["id"])
|
||||
if err != nil {
|
||||
page.session.setFlashMessage("Invalid Mode: "+vars["id"], "error")
|
||||
}
|
||||
if dbSetPublicSiteMode(newMode) != nil {
|
||||
page.session.setFlashMessage("Invalid Mode: "+vars["id"], "error")
|
||||
}
|
||||
redirect("/admin", w, req)
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@ -10,23 +12,51 @@ import (
|
||||
func handleAdminGames(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||
vars := mux.Vars(req)
|
||||
page.SubTitle = "Games"
|
||||
gameId := vars["id"]
|
||||
teamId := req.FormValue("teamid")
|
||||
if strings.TrimSpace(teamId) != "" {
|
||||
page.session.setStringValue("teamid", teamId)
|
||||
page.TeamID = teamId
|
||||
teamId := vars["id"]
|
||||
if teamId == "" {
|
||||
// Games List
|
||||
type gamesPageData struct {
|
||||
Teams []Team
|
||||
}
|
||||
if gameId == "new" {
|
||||
gpd := new(gamesPageData)
|
||||
gpd.Teams = dbGetAllTeams()
|
||||
page.TemplateData = gpd
|
||||
page.SubTitle = "Games"
|
||||
page.show("admin-games.html", w)
|
||||
} else {
|
||||
switch vars["function"] {
|
||||
case "save":
|
||||
name := req.FormValue("gamename")
|
||||
desc := req.FormValue("gamedesc")
|
||||
if dbIsValidTeam(teamId) {
|
||||
if dbEditTeamGame(teamId, name) != nil {
|
||||
if err := dbUpdateTeamGame(teamId, name, desc); err != nil {
|
||||
page.session.setFlashMessage("Error updating game: "+err.Error(), "error")
|
||||
} else {
|
||||
page.session.setFlashMessage("Team game updated", "success")
|
||||
}
|
||||
redirect("/admin/teams/"+teamId, w, req)
|
||||
}
|
||||
default:
|
||||
page.SubTitle = "Add New Game"
|
||||
page.show("admin-addgame.html", w)
|
||||
case "screenshotupload":
|
||||
if err := saveScreenshots(teamId, req); err != nil {
|
||||
page.session.setFlashMessage("Error updating game: "+err.Error(), "error")
|
||||
}
|
||||
redirect("/admin/teams/"+teamId, w, req)
|
||||
case "screenshotdelete":
|
||||
ssid := vars["subid"]
|
||||
if err := dbDeleteTeamGameScreenshot(teamId, ssid); err != nil {
|
||||
page.session.setFlashMessage("Error deleting screenshot: "+err.Error(), "error")
|
||||
}
|
||||
redirect("/admin/teams/"+teamId, w, req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveScreenshots(teamId string, req *http.Request) error {
|
||||
file, _, err := req.FormFile("newssfile")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := ioutil.ReadAll(file)
|
||||
str := base64.StdEncoding.EncodeToString(data)
|
||||
return dbSaveTeamGameScreenshot(teamId, &Screenshot{Image: str})
|
||||
}
|
||||
|
@ -6,11 +6,16 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func refreshTeamsInMemory() {
|
||||
site.Teams = dbGetAllTeams()
|
||||
}
|
||||
|
||||
func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||
vars := mux.Vars(req)
|
||||
page.SubTitle = "Teams"
|
||||
teamId := vars["id"]
|
||||
if teamId == "new" {
|
||||
// Add a new team
|
||||
switch vars["function"] {
|
||||
case "save":
|
||||
name := req.FormValue("teamname")
|
||||
@ -24,12 +29,14 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
|
||||
page.session.setFlashMessage("Team "+name+" created!", "success")
|
||||
}
|
||||
}
|
||||
refreshTeamsInMemory()
|
||||
redirect("/admin/teams", w, req)
|
||||
default:
|
||||
page.SubTitle = "Add New Team"
|
||||
page.show("admin-addteam.html", w)
|
||||
}
|
||||
} else if teamId != "" {
|
||||
// Functions for existing team
|
||||
if dbIsValidTeam(teamId) {
|
||||
switch vars["function"] {
|
||||
case "save":
|
||||
@ -41,12 +48,17 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
|
||||
} else {
|
||||
page.session.setFlashMessage("Team Updated!", "success")
|
||||
}
|
||||
refreshTeamsInMemory()
|
||||
redirect("/admin/teams", w, req)
|
||||
case "delete":
|
||||
var err error
|
||||
t := dbGetTeam(teamId)
|
||||
if err = dbDeleteTeam(teamId); err != nil {
|
||||
page.session.setFlashMessage("Error deleting team: "+err.Error(), "error")
|
||||
} else {
|
||||
page.session.setFlashMessage("Team "+t.Name+" Deleted", "success")
|
||||
}
|
||||
refreshTeamsInMemory()
|
||||
redirect("/admin/teams", w, req)
|
||||
case "savemember":
|
||||
mbrName := req.FormValue("newmembername")
|
||||
@ -58,14 +70,17 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
|
||||
} else {
|
||||
page.session.setFlashMessage(mbrName+" added to team!", "success")
|
||||
}
|
||||
refreshTeamsInMemory()
|
||||
redirect("/admin/teams/"+teamId, w, req)
|
||||
case "deletemember":
|
||||
mbrId := req.FormValue("memberid")
|
||||
m, _ := dbGetTeamMember(teamId, mbrId)
|
||||
if err := dbDeleteTeamMember(teamId, mbrId); err != nil {
|
||||
page.session.setFlashMessage("Error deleting team member: "+err.Error(), "error")
|
||||
} else {
|
||||
page.session.setFlashMessage("Member deleted from team", "success")
|
||||
page.session.setFlashMessage(m.Name+" deleted from team", "success")
|
||||
}
|
||||
refreshTeamsInMemory()
|
||||
redirect("/admin/teams/"+teamId, w, req)
|
||||
default:
|
||||
page.SubTitle = "Edit Team"
|
||||
@ -78,10 +93,10 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
|
||||
redirect("/admin/teams", w, req)
|
||||
}
|
||||
} else {
|
||||
// Team List
|
||||
type teamsPageData struct {
|
||||
Teams []Team
|
||||
}
|
||||
|
||||
page.TemplateData = teamsPageData{Teams: dbGetAllTeams()}
|
||||
page.SubTitle = "Teams"
|
||||
page.show("admin-teams.html", w)
|
||||
|
148
admin_votes.go
Normal file
148
admin_votes.go
Normal file
@ -0,0 +1,148 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// getCondorcetResult returns the ranking of teams based on the condorcet method
|
||||
// https://en.wikipedia.org/wiki/Condorcet_method
|
||||
func getCondorcetResult() []Team {
|
||||
type teamPair struct {
|
||||
winner *Team
|
||||
loser *Team
|
||||
majority float32
|
||||
}
|
||||
var allPairs []teamPair
|
||||
var ret []Team
|
||||
for i := 0; i < len(site.Teams); i++ {
|
||||
for j := i + 1; j < len(site.Teams); j++ {
|
||||
// For each pairing find a winner
|
||||
winner, pct, _ := findWinnerBetweenTeams(&site.Teams[i], &site.Teams[j])
|
||||
if winner != nil {
|
||||
newPair := new(teamPair)
|
||||
newPair.winner = winner
|
||||
if winner.UUID == site.Teams[i].UUID {
|
||||
newPair.loser = &site.Teams[j]
|
||||
} else {
|
||||
newPair.loser = &site.Teams[i]
|
||||
}
|
||||
newPair.majority = pct
|
||||
allPairs = append(allPairs, *newPair)
|
||||
}
|
||||
}
|
||||
}
|
||||
teamWins := make(map[string]int)
|
||||
for i := range site.Teams {
|
||||
teamWins[site.Teams[i].UUID] = 0
|
||||
}
|
||||
for i := range allPairs {
|
||||
teamWins[allPairs[i].winner.UUID]++
|
||||
}
|
||||
for len(teamWins) > 0 { //len(ret) <= len(site.Teams) {
|
||||
topWins := 0
|
||||
var topTeam string
|
||||
for k, v := range teamWins {
|
||||
// If this team is already in ret, carry on
|
||||
if uuidIsInTeamSlice(k, ret) {
|
||||
continue
|
||||
}
|
||||
// If this is the last key in teamWins, just add it
|
||||
if len(teamWins) == 1 || v > topWins {
|
||||
topWins = v
|
||||
topTeam = k
|
||||
}
|
||||
}
|
||||
// Remove topTeam from map
|
||||
delete(teamWins, topTeam)
|
||||
// Now add topTeam to ret
|
||||
addTeam := site.getTeamByUUID(topTeam)
|
||||
if addTeam != nil {
|
||||
ret = append(ret, *addTeam)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// This is a helper function for calculating results
|
||||
func uuidIsInTeamSlice(uuid string, sl []Team) bool {
|
||||
for _, v := range sl {
|
||||
if v.UUID == uuid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findWinnerBetweenTeams returns the team that got the most votes
|
||||
// and the percentage of votes they received
|
||||
// or an error if a winner couldn't be determined.
|
||||
func findWinnerBetweenTeams(tm1, tm2 *Team) (*Team, float32, error) {
|
||||
// tally gets incremented for a tm1 win, decremented for a tm2 win
|
||||
var tm1votes, tm2votes float32
|
||||
for _, v := range site.Votes {
|
||||
for _, chc := range v.Choices {
|
||||
if chc.Team == tm1.UUID {
|
||||
tm1votes++
|
||||
break
|
||||
} else if chc.Team == tm2.UUID {
|
||||
tm2votes++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
ttlVotes := tm1votes + tm2votes
|
||||
if tm1votes > tm2votes {
|
||||
return tm1, 100 * (tm1votes / ttlVotes), nil
|
||||
} else if tm1votes < tm2votes {
|
||||
return tm2, 100 * (tm2votes / ttlVotes), nil
|
||||
}
|
||||
return nil, 50, errors.New("Unable to determine a winner")
|
||||
}
|
||||
|
||||
func getInstantRunoffResult() []Team {
|
||||
var ret []Team
|
||||
return ret
|
||||
}
|
||||
|
||||
func handleAdminVotes(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||
vars := mux.Vars(req)
|
||||
page.SubTitle = "Votes"
|
||||
|
||||
type vpdVote struct {
|
||||
Timestamp string
|
||||
ClientId string
|
||||
Choices []Team
|
||||
}
|
||||
type votePageData struct {
|
||||
AllVotes []vpdVote
|
||||
Results []Team
|
||||
}
|
||||
vpd := new(votePageData)
|
||||
for i := range site.Votes {
|
||||
v := new(vpdVote)
|
||||
v.Timestamp = site.Votes[i].Timestamp.Format(time.RFC3339)
|
||||
v.ClientId = site.Votes[i].ClientId
|
||||
for _, choice := range site.Votes[i].Choices {
|
||||
for _, fndTm := range site.Teams {
|
||||
if fndTm.UUID == choice.Team {
|
||||
v.Choices = append(v.Choices, fndTm)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
vpd.AllVotes = append(vpd.AllVotes, *v)
|
||||
}
|
||||
vpd.Results = getCondorcetResult()
|
||||
page.TemplateData = vpd
|
||||
|
||||
switch vars["function"] {
|
||||
default:
|
||||
page.show("admin-votes.html", w)
|
||||
}
|
||||
}
|
@ -5,3 +5,7 @@ div.content {
|
||||
div.bottom-space {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
@ -4,7 +4,11 @@ html {
|
||||
|
||||
body {
|
||||
color: #777;
|
||||
min-ehgiht: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.content {
|
||||
@ -13,10 +17,44 @@ div.content {
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.half {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.primary {
|
||||
color: #0078e7;
|
||||
}
|
||||
|
||||
.primary-bg {
|
||||
background-color: #0078e7;
|
||||
}
|
||||
|
||||
input.file {
|
||||
padding: .5em .6em;
|
||||
display: inline-block;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: inset 0 1px 3px #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
input.ranking-input {
|
||||
width: 50px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #CCC;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.submit-vote {
|
||||
}
|
||||
|
||||
i.move-icon {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
#menu {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
@ -29,6 +67,14 @@ div.content {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#menu .pure-menu-nonlink {
|
||||
color: #777;
|
||||
padding: .5em 1em;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#menu .menu-button {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
@ -57,8 +103,117 @@ div.content {
|
||||
background-color: #191818;
|
||||
}
|
||||
|
||||
input.uuid-field {
|
||||
width: 360px;
|
||||
div.horizontal-scroll {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
img.thumbnail {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.thumbnail-container {
|
||||
display: block;
|
||||
height: 120px;
|
||||
background-color: #EEE;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.padding {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.space {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.space-sides {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.space-vertical {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.big-space {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.left {
|
||||
text-align: left;
|
||||
}
|
||||
.right {
|
||||
text-align: right;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.center-all {
|
||||
text-align: center;
|
||||
vertical-align: center;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
table.center tbody>td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table.padding td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
table tfoot {
|
||||
border-top: 1px solid #CCC;
|
||||
}
|
||||
|
||||
.pure-button-error {
|
||||
background-color: #DD0000;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.pure-button-toggle-first {
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
.pure-button-toggle-middle {
|
||||
border-radius: 0px;
|
||||
margin-left: -1px;
|
||||
border-left: 1px solid #CCC;
|
||||
}
|
||||
.pure-button-toggle-last {
|
||||
border-top-left-radius: 0px;
|
||||
border-bottom-left-radius: 0px;
|
||||
margin-left: -1px;
|
||||
border-left: 1px solid #CCC;
|
||||
}
|
||||
|
||||
.pure-button:disabled {
|
||||
background-color: #CCC;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
#modal-overlay {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
z-index: 1000;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#modal-overlay>div {
|
||||
width: 500px;
|
||||
margin: 100px auto;
|
||||
background-color: #FFF;
|
||||
border: 1px solid #000;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 40em) {
|
||||
@ -92,6 +247,16 @@ input.uuid-field {
|
||||
margin-left: 150px;
|
||||
}
|
||||
|
||||
aside.flash.error {
|
||||
background-color: #DD0000;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
aside.flash.success {
|
||||
background-color: #229af9;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 150px;
|
||||
}
|
||||
|
@ -15,3 +15,52 @@ document.onkeydown = function(evt) {
|
||||
toggleAdminPanel();
|
||||
}
|
||||
}
|
||||
|
||||
function showModal(options) {
|
||||
var modal = document.getElementById('modal-overlay');
|
||||
document.getElementById('modal-title').innerText = (options.title)?options.title:"";
|
||||
document.getElementById('modal-subtitle').innerText = (options.subtitle)?options.subtitle:"";
|
||||
if(options.body) {
|
||||
document.getElementById('modal-body').innerText = options.body;
|
||||
} else if(options.bodyNode) {
|
||||
document.getElementById('modal-body').appendChild(options.bodyNode);
|
||||
}
|
||||
if(options.buttons) {
|
||||
for(var i = 0; i < options.buttons.length; i++) {
|
||||
var btn;
|
||||
if(options.buttons[i].isSubmit) {
|
||||
btn = document.createElement('submit');
|
||||
} else {
|
||||
btn = document.createElement('a');
|
||||
}
|
||||
options.buttons[i].title = (options.buttons[i].title==undefined)?'':options.buttons[i].title;
|
||||
options.buttons[i].href = (options.buttons[i].href==undefined)?'#':options.buttons[i].href;
|
||||
options.buttons[i].click = (options.buttons[i].click==undefined)?function(){}:options.buttons[i].click;
|
||||
options.buttons[i].class = (options.buttons[i].class==undefined)?'':options.buttons[i].class;
|
||||
options.buttons[i].position = (options.buttons[i].position==undefined)?'right':options.buttons[i].position;
|
||||
|
||||
btn.innerHTML = options.buttons[i].title;
|
||||
btn.title = options.buttons[i].title;
|
||||
btn.href = options.buttons[i].href;
|
||||
btn.className = 'space pure-button '+options.buttons[i].class+' '+options.buttons[i].position;
|
||||
snack.listener(
|
||||
{node:btn, event:'click'},
|
||||
options.buttons[i].click
|
||||
);
|
||||
document.getElementById('modal-buttons').appendChild(btn);
|
||||
}
|
||||
}
|
||||
modal.style.visibility = 'visible';
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
var modal = document.getElementById('modal-overlay');
|
||||
modal.style.visibility = 'hidden';
|
||||
document.getElementById('modal-title').innerHTML = '';
|
||||
document.getElementById('modal-body').innerHTML = '';
|
||||
var buttonsDiv = document.getElementById('modal-buttons')
|
||||
while(buttonsDiv.firstChild) {
|
||||
buttonsDiv.removeChild(buttonsDiv.firstChild);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 434 KiB After Width: | Height: | Size: 434 KiB |
@ -1 +0,0 @@
|
||||
{"title":"ICT GameJam Voting","port":8080,"session":"ict-gamejam","dir":"./","devmode":true,"db":"gjvote.db","CurrentJam":""}
|
184
main.go
184
main.go
@ -1,11 +1,11 @@
|
||||
package main
|
||||
|
||||
//go:generate esc -o assets.go assets templates
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@ -21,17 +21,29 @@ import (
|
||||
)
|
||||
|
||||
const AppName = "gjvote"
|
||||
const DbName = AppName + ".db"
|
||||
|
||||
// SiteData is stuff that stays the same
|
||||
type siteData struct {
|
||||
Title string `json:"title"`
|
||||
Port int `json:"port"`
|
||||
SessionName string `json:"session"`
|
||||
ServerDir string `json:"dir"`
|
||||
DevMode bool `json:"devmode"`
|
||||
DB string `json:"db"`
|
||||
Title string
|
||||
Port int
|
||||
SessionName string
|
||||
ServerDir string
|
||||
DevMode bool
|
||||
|
||||
CurrentJam string
|
||||
|
||||
Teams []Team
|
||||
Votes []Vote
|
||||
}
|
||||
|
||||
func (s *siteData) getTeamByUUID(uuid string) *Team {
|
||||
for i := range s.Teams {
|
||||
if s.Teams[i].UUID == uuid {
|
||||
return &s.Teams[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pageData is stuff that changes per request
|
||||
@ -51,10 +63,12 @@ type pageData struct {
|
||||
HideAdminMenu bool
|
||||
session *pageSession
|
||||
CurrentJam string
|
||||
ClientID string
|
||||
ClientId string
|
||||
ClientIsAuth bool
|
||||
ClientIsServer bool
|
||||
TeamID string
|
||||
|
||||
PublicMode int
|
||||
TemplateData interface{}
|
||||
}
|
||||
|
||||
@ -69,23 +83,27 @@ var sessionSecret = "JCOP5e8ohkTcOzcSMe74"
|
||||
var sessionStore = sessions.NewCookieStore([]byte(sessionSecret))
|
||||
var site *siteData
|
||||
var r *mux.Router
|
||||
var configFile string
|
||||
|
||||
func main() {
|
||||
configFile = "./config.json"
|
||||
loadConfig()
|
||||
saveConfig()
|
||||
dbSaveSiteConfig(site)
|
||||
initialize()
|
||||
|
||||
r = mux.NewRouter()
|
||||
r.StrictSlash(true)
|
||||
|
||||
s := http.StripPrefix("/assets/", http.FileServer(http.Dir(site.ServerDir+"assets/")))
|
||||
r.PathPrefix("/assets/").Handler(s)
|
||||
if site.DevMode {
|
||||
fmt.Println("Operating in Development Mode")
|
||||
}
|
||||
//s := http.StripPrefix("/assets/", http.FileServer(FS(site.DevMode)))
|
||||
//http.Dir(site.ServerDir+"assets/")))
|
||||
//r.PathPrefix("/assets/").Handler(s)
|
||||
r.PathPrefix("/assets/").Handler(http.FileServer(FS(site.DevMode)))
|
||||
|
||||
// Public Subrouter
|
||||
pub := r.PathPrefix("/").Subrouter()
|
||||
pub.HandleFunc("/", handleMain)
|
||||
pub.HandleFunc("/vote", handlePublicSaveVote)
|
||||
|
||||
// API Subrouter
|
||||
//api := r.PathPrefix("/api").Subtrouter()
|
||||
@ -98,6 +116,7 @@ func main() {
|
||||
admin.HandleFunc("/{category}", handleAdmin)
|
||||
admin.HandleFunc("/{category}/{id}", handleAdmin)
|
||||
admin.HandleFunc("/{category}/{id}/{function}", handleAdmin)
|
||||
admin.HandleFunc("/{category}/{id}/{function}/{subid}", handleAdmin)
|
||||
|
||||
http.Handle("/", r)
|
||||
|
||||
@ -108,25 +127,47 @@ func main() {
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
site = new(siteData)
|
||||
// Defaults:
|
||||
site.Title = "ICT GameJam"
|
||||
site.Port = 8080
|
||||
site.SessionName = "ict-gamejam"
|
||||
site.ServerDir = "./"
|
||||
site.DevMode = false
|
||||
site.DB = AppName + ".db"
|
||||
site = dbGetSiteConfig()
|
||||
|
||||
jsonInp, err := ioutil.ReadFile(configFile)
|
||||
if err == nil {
|
||||
assertError(json.Unmarshal(jsonInp, &site))
|
||||
if len(os.Args) > 1 {
|
||||
for _, v := range os.Args {
|
||||
key := v
|
||||
val := ""
|
||||
eqInd := strings.Index(v, "=")
|
||||
if eqInd > 0 {
|
||||
// It's a key/val argument
|
||||
key = v[:eqInd]
|
||||
val = v[eqInd+1:]
|
||||
}
|
||||
switch key {
|
||||
case "-title":
|
||||
site.Title = val
|
||||
fmt.Print("Set site title: ", site.Title, "\n")
|
||||
case "-port":
|
||||
var tryPort int
|
||||
var err error
|
||||
if tryPort, err = strconv.Atoi(val); err != nil {
|
||||
fmt.Print("Invalid port given: ", val, " (Must be an integer)\n")
|
||||
tryPort = site.Port
|
||||
}
|
||||
// TODO: Make sure a valid port number is given
|
||||
site.Port = tryPort
|
||||
case "-session-name":
|
||||
site.SessionName = val
|
||||
case "-server-dir":
|
||||
// TODO: Probably check if the given directory is valid
|
||||
site.ServerDir = val
|
||||
case "-help", "-h", "-?":
|
||||
printHelp()
|
||||
done()
|
||||
case "-dev":
|
||||
site.DevMode = true
|
||||
case "-reset-defaults":
|
||||
resetToDefaults()
|
||||
done()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveConfig() {
|
||||
var jsonInp []byte
|
||||
jsonInp, _ = json.Marshal(site)
|
||||
assertError(ioutil.WriteFile(configFile, jsonInp, 0644))
|
||||
}
|
||||
|
||||
func initialize() {
|
||||
@ -170,6 +211,10 @@ func initialize() {
|
||||
} else {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
|
||||
// Load all votes into memory
|
||||
site.Votes = dbGetAllVotes()
|
||||
site.Teams = dbGetAllTeams()
|
||||
}
|
||||
|
||||
func loggingHandler(h http.Handler) http.Handler {
|
||||
@ -201,13 +246,13 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
|
||||
p.Site = site
|
||||
p.SubTitle = "GameJam Voting"
|
||||
p.Stylesheets = make([]string, 0, 0)
|
||||
p.Stylesheets = append(p.Stylesheets, "/assets/css/pure-min.css")
|
||||
p.Stylesheets = append(p.Stylesheets, "/assets/css/grids-responsive-min.css")
|
||||
p.Stylesheets = append(p.Stylesheets, "/assets/font-awesome/css/font-awesome.min.css")
|
||||
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css")
|
||||
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/grids-responsive-min.css")
|
||||
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/font-awesome/css/font-awesome.min.css")
|
||||
p.Stylesheets = append(p.Stylesheets, "/assets/css/gjvote.css")
|
||||
|
||||
p.HeaderScripts = make([]string, 0, 0)
|
||||
p.HeaderScripts = append(p.HeaderScripts, "/assets/js/snack-min.js")
|
||||
p.HeaderScripts = append(p.HeaderScripts, "/assets/vendor/js/snack-min.js")
|
||||
|
||||
p.Scripts = make([]string, 0, 0)
|
||||
p.Scripts = append(p.Scripts, "/assets/js/gjvote.js")
|
||||
@ -220,9 +265,10 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
|
||||
// Build the menu
|
||||
if p.LoggedIn {
|
||||
p.Menu = append(p.Menu, menuItem{"Admin", "/admin", "fa-key"})
|
||||
p.Menu = append(p.Menu, menuItem{"Votes", "/admin/votes", "fa-sticky-note"})
|
||||
p.Menu = append(p.Menu, menuItem{"Teams", "/admin/teams", "fa-users"})
|
||||
p.Menu = append(p.Menu, menuItem{"Games", "/admin/games", "fa-gamepad"})
|
||||
p.Menu = append(p.Menu, menuItem{"Votes", "/admin/votes", "fa-sticky-note"})
|
||||
p.Menu = append(p.Menu, menuItem{"Clients", "/admin/clients", "fa-desktop"})
|
||||
|
||||
p.BottomMenu = append(p.BottomMenu, menuItem{"Users", "/admin/users", "fa-user"})
|
||||
p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/admin/dologout", "fa-sign-out"})
|
||||
@ -236,10 +282,15 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
|
||||
p.FlashClass = "error"
|
||||
}
|
||||
|
||||
p.ClientID = p.session.getClientID()
|
||||
p.ClientIsAuth = dbClientIsAuth(p.ClientID)
|
||||
p.ClientId = p.session.getClientId()
|
||||
p.ClientIsAuth = clientIsAuthenticated(p.ClientId, req)
|
||||
p.ClientIsServer = clientIsServer(req)
|
||||
// TeamID is for team self-administration
|
||||
p.TeamID, _ = p.session.getStringValue("teamid")
|
||||
|
||||
// Public Mode
|
||||
p.PublicMode = dbGetPublicSiteMode()
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
@ -263,24 +314,65 @@ func (p *pageData) show(tmplName string, w http.ResponseWriter) error {
|
||||
// outputTemplate
|
||||
// Spit out a template
|
||||
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
||||
_, err := os.Stat("templates/" + tmplName)
|
||||
if err == nil {
|
||||
t := template.New(tmplName)
|
||||
t, _ = t.ParseFiles("templates/" + tmplName)
|
||||
// TODO: Use embedded files for these... Hopefully?
|
||||
n := "/templates/" + tmplName
|
||||
l := template.Must(template.New("layout").Parse(FSMustString(site.DevMode, n)))
|
||||
t := template.Must(l.Parse(FSMustString(site.DevMode, n)))
|
||||
return t.Execute(w, tmplData)
|
||||
}
|
||||
return fmt.Errorf("WebServer: Cannot load template (templates/%s): File not found", tmplName)
|
||||
}
|
||||
|
||||
// redirect can be used only for GET redirects
|
||||
func redirect(url string, w http.ResponseWriter, req *http.Request) {
|
||||
http.Redirect(w, req, url, 303)
|
||||
}
|
||||
|
||||
func printOutput(out string) {
|
||||
if site.DevMode {
|
||||
fmt.Print(out)
|
||||
func resetToDefaults() {
|
||||
def := GetDefaultSiteConfig()
|
||||
fmt.Println("Reset settings to defaults?")
|
||||
fmt.Print(site.Title, " -> ", def.Title, "\n")
|
||||
fmt.Print(site.Port, " -> ", def.Port, "\n")
|
||||
fmt.Print(site.SessionName, " -> ", def.SessionName, "\n")
|
||||
fmt.Print(site.ServerDir, " -> ", def.ServerDir, "\n")
|
||||
fmt.Println("Are you sure? (y/N): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
conf, _ := reader.ReadString('\n')
|
||||
conf = strings.ToUpper(strings.TrimSpace(conf))
|
||||
if strings.HasPrefix(conf, "Y") {
|
||||
if dbSaveSiteConfig(def) != nil {
|
||||
errorExit("Error resetting to defaults")
|
||||
}
|
||||
fmt.Println("Reset to defaults")
|
||||
}
|
||||
}
|
||||
|
||||
func printHelp() {
|
||||
help := []string{
|
||||
"Game Jam Voting Help",
|
||||
" -help, -h, -? Print this message",
|
||||
" -dev Development mode, load assets from file system",
|
||||
" -port=<port num> Set the site port",
|
||||
" -session-name=<session> Set the name of the session to be used",
|
||||
" -server-dir=<directory> Set the server directory",
|
||||
" This designates where the database will be saved",
|
||||
" and where the app will look for files if you're",
|
||||
" operating in 'development' mode (-dev)",
|
||||
" -title=<title> Set the site title",
|
||||
" -current-jam=<name> Change the name of the current jam",
|
||||
" -reset-defaults Reset all configuration options to defaults",
|
||||
"",
|
||||
}
|
||||
for _, v := range help {
|
||||
fmt.Println(v)
|
||||
}
|
||||
}
|
||||
|
||||
func done() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func errorExit(msg string) {
|
||||
fmt.Println(msg)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func assertError(err error) {
|
||||
|
78
model.go
78
model.go
@ -10,11 +10,26 @@ import (
|
||||
var db *boltease.DB
|
||||
var dbOpened int
|
||||
|
||||
const (
|
||||
SiteModeWaiting = iota
|
||||
SiteModeVoting
|
||||
SiteModeError
|
||||
)
|
||||
|
||||
func GetDefaultSiteConfig() *siteData {
|
||||
ret := new(siteData)
|
||||
ret.Title = "ICT GameJam"
|
||||
ret.Port = 8080
|
||||
ret.SessionName = "ict-gamejam"
|
||||
ret.ServerDir = "./"
|
||||
return ret
|
||||
}
|
||||
|
||||
func openDatabase() error {
|
||||
dbOpened += 1
|
||||
if dbOpened == 1 {
|
||||
var err error
|
||||
db, err = boltease.Create(site.DB, 0600, nil)
|
||||
db, err = boltease.Create(DbName, 0600, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -82,3 +97,64 @@ func dbGetCurrentJam() (string, error) {
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func dbGetSiteConfig() *siteData {
|
||||
var ret *siteData
|
||||
def := GetDefaultSiteConfig()
|
||||
var err error
|
||||
if err = openDatabase(); err != nil {
|
||||
return def
|
||||
}
|
||||
defer closeDatabase()
|
||||
|
||||
ret = new(siteData)
|
||||
siteConf := []string{"site"}
|
||||
if ret.Title, err = db.GetValue(siteConf, "title"); err != nil {
|
||||
ret.Title = def.Title
|
||||
}
|
||||
if ret.Port, err = db.GetInt(siteConf, "port"); err != nil {
|
||||
ret.Port = def.Port
|
||||
}
|
||||
if ret.SessionName, err = db.GetValue(siteConf, "session-name"); err != nil {
|
||||
ret.SessionName = def.SessionName
|
||||
}
|
||||
if ret.ServerDir, err = db.GetValue(siteConf, "server-dir"); err != nil {
|
||||
ret.ServerDir = def.ServerDir
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func dbSaveSiteConfig(dat *siteData) error {
|
||||
var err error
|
||||
if err = openDatabase(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeDatabase()
|
||||
|
||||
siteConf := []string{"site"}
|
||||
if err = db.SetValue(siteConf, "title", dat.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.SetInt(siteConf, "port", dat.Port); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.SetValue(siteConf, "session-name", dat.SessionName); err != nil {
|
||||
return err
|
||||
}
|
||||
return db.SetValue(siteConf, "server-dir", dat.ServerDir)
|
||||
}
|
||||
|
||||
func dbGetPublicSiteMode() int {
|
||||
if ret, err := db.GetInt([]string{"site"}, "public-mode"); err != nil {
|
||||
return SiteModeWaiting
|
||||
} else {
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
func dbSetPublicSiteMode(mode int) error {
|
||||
if mode < 0 || mode >= SiteModeError {
|
||||
return errors.New("Invalid site mode")
|
||||
}
|
||||
return db.SetInt([]string{"site"}, "public-mode", mode)
|
||||
}
|
||||
|
@ -3,6 +3,8 @@ package main
|
||||
type Client struct {
|
||||
UUID string
|
||||
Auth bool
|
||||
Name string
|
||||
IP string
|
||||
}
|
||||
|
||||
func dbGetAllClients() []Client {
|
||||
@ -35,9 +37,32 @@ func dbGetClient(id string) *Client {
|
||||
cl := new(Client)
|
||||
cl.UUID = id
|
||||
cl.Auth = dbClientIsAuth(id)
|
||||
cl.Name, _ = db.GetValue([]string{"clients", id}, "name")
|
||||
cl.IP, _ = db.GetValue([]string{"clients", id}, "ip")
|
||||
return cl
|
||||
}
|
||||
|
||||
func dbSetClientName(cid, name string) error {
|
||||
var err error
|
||||
if err = db.OpenDB(); err != nil {
|
||||
return nil
|
||||
}
|
||||
defer db.CloseDB()
|
||||
|
||||
err = db.SetValue([]string{"clients", cid}, "name", name)
|
||||
return err
|
||||
}
|
||||
|
||||
func dbGetClientName(cid string) string {
|
||||
if err := db.OpenDB(); err != nil {
|
||||
return ""
|
||||
}
|
||||
defer db.CloseDB()
|
||||
|
||||
name, _ := db.GetValue([]string{"clients", cid}, "name")
|
||||
return name
|
||||
}
|
||||
|
||||
func dbAddDeauthClient(cid, ip string) error {
|
||||
var err error
|
||||
if err = db.OpenDB(); err != nil {
|
||||
|
143
model_games.go
143
model_games.go
@ -1,12 +1,147 @@
|
||||
package main
|
||||
|
||||
import "github.com/pborman/uuid"
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/pborman/uuid"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
UUID *uuid.UUID
|
||||
Name string
|
||||
TeamId string
|
||||
Description string
|
||||
Screenshots []Screenshot
|
||||
}
|
||||
|
||||
func dbIsValidGame(id string) bool {
|
||||
return true
|
||||
type Screenshot struct {
|
||||
Description string
|
||||
Image string
|
||||
UUID string
|
||||
}
|
||||
|
||||
func dbUpdateTeamGame(teamId, name, desc string) error {
|
||||
var err error
|
||||
if err = openDatabase(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeDatabase()
|
||||
|
||||
// Make sure the team is valid
|
||||
tm := dbGetTeam(teamId)
|
||||
if tm == nil {
|
||||
return errors.New("Invalid team")
|
||||
}
|
||||
gamePath := []string{"teams", teamId, "game"}
|
||||
|
||||
if err := db.MkBucketPath(gamePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if name == "" {
|
||||
name = tm.Name + "'s Game"
|
||||
}
|
||||
if err := db.SetValue(gamePath, "name", name); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.SetValue(gamePath, "description", desc); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.MkBucketPath(append(gamePath, "screenshots")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func dbGetAllGames() []Game {
|
||||
var ret []Game
|
||||
tms := dbGetAllTeams()
|
||||
for i := range tms {
|
||||
ret = append(ret, *dbGetTeamGame(tms[i].UUID))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func dbGetTeamGame(teamId string) *Game {
|
||||
var err error
|
||||
if err = openDatabase(); err != nil {
|
||||
return nil
|
||||
}
|
||||
defer closeDatabase()
|
||||
|
||||
gamePath := []string{"teams", teamId, "game"}
|
||||
gm := new(Game)
|
||||
if gm.Name, err = db.GetValue(gamePath, "name"); err != nil {
|
||||
gm.Name = ""
|
||||
}
|
||||
gm.TeamId = teamId
|
||||
if gm.Description, err = db.GetValue(gamePath, "description"); err != nil {
|
||||
gm.Description = ""
|
||||
}
|
||||
gm.Screenshots = dbGetTeamGameScreenshots(teamId)
|
||||
return gm
|
||||
}
|
||||
|
||||
// Screenshots are saved as base64 encoded pngs
|
||||
func dbGetTeamGameScreenshots(teamId string) []Screenshot {
|
||||
var ret []Screenshot
|
||||
var err error
|
||||
ssPath := []string{"teams", teamId, "game", "screenshots"}
|
||||
var ssIds []string
|
||||
if ssIds, err = db.GetBucketList(ssPath); err != nil {
|
||||
return ret
|
||||
}
|
||||
for _, v := range ssIds {
|
||||
if ss := dbGetTeamGameScreenshot(teamId, v); ss != nil {
|
||||
ret = append(ret, *ss)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func dbGetTeamGameScreenshot(teamId, ssId string) *Screenshot {
|
||||
var err error
|
||||
ssPath := []string{"teams", teamId, "game", "screenshots", ssId}
|
||||
ret := new(Screenshot)
|
||||
ret.UUID = ssId
|
||||
if ret.Description, err = db.GetValue(ssPath, "description"); err != nil {
|
||||
return nil
|
||||
}
|
||||
if ret.Image, err = db.GetValue(ssPath, "image"); err != nil {
|
||||
return nil
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func dbSaveTeamGameScreenshot(teamId string, ss *Screenshot) error {
|
||||
var err error
|
||||
if err = openDatabase(); err != nil {
|
||||
return nil
|
||||
}
|
||||
defer closeDatabase()
|
||||
|
||||
ssPath := []string{"teams", teamId, "game", "screenshots"}
|
||||
// Generate a UUID for this screenshot
|
||||
uuid := uuid.New()
|
||||
ssPath = append(ssPath, uuid)
|
||||
if err := db.MkBucketPath(ssPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.SetValue(ssPath, "description", ss.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := db.SetValue(ssPath, "image", ss.Image); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dbDeleteTeamGameScreenshot(teamId, ssId string) error {
|
||||
var err error
|
||||
if err = openDatabase(); err != nil {
|
||||
return nil
|
||||
}
|
||||
defer closeDatabase()
|
||||
|
||||
ssPath := []string{"teams", teamId, "game", "screenshots"}
|
||||
return db.DeleteBucket(ssPath, ssId)
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/pborman/uuid"
|
||||
)
|
||||
@ -105,6 +104,7 @@ func dbGetTeam(id string) *Team {
|
||||
return nil
|
||||
}
|
||||
tm.Members, _ = dbGetTeamMembers(id)
|
||||
tm.Game = dbGetTeamGame(id)
|
||||
return tm
|
||||
}
|
||||
|
||||
@ -213,14 +213,11 @@ func dbGetTeamMembers(teamid string) ([]TeamMember, error) {
|
||||
for _, v := range memberUuids {
|
||||
var mbr *TeamMember
|
||||
if mbr, err = dbGetTeamMember(teamid, v); err == nil {
|
||||
fmt.Println("Finding Team Members", teamid, mbr.Name)
|
||||
ret = append(ret, *mbr)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
return ret, nil
|
||||
return ret, err
|
||||
}
|
||||
|
||||
func dbGetTeamMember(teamid, mbrid string) (*TeamMember, error) {
|
||||
|
110
model_votes.go
Normal file
110
model_votes.go
Normal file
@ -0,0 +1,110 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A Choice is a ranking of a game in a vote
|
||||
type GameChoice struct {
|
||||
Team string // UUID of team
|
||||
Rank int
|
||||
}
|
||||
|
||||
// A Vote is a collection of game rankings
|
||||
type Vote struct {
|
||||
Timestamp time.Time
|
||||
ClientId string // UUID of client
|
||||
Choices []GameChoice
|
||||
}
|
||||
|
||||
func dbGetAllVotes() []Vote {
|
||||
var ret []Vote
|
||||
var err error
|
||||
if err = db.OpenDB(); err != nil {
|
||||
return ret
|
||||
}
|
||||
defer db.CloseDB()
|
||||
|
||||
votesBkt := []string{"votes"}
|
||||
var clients []string
|
||||
if clients, err = db.GetBucketList(votesBkt); err != nil {
|
||||
// Couldn't get the list of clients
|
||||
return ret
|
||||
}
|
||||
for _, clid := range clients {
|
||||
ret = append(ret, dbGetClientVotes(clid)...)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func dbGetClientVotes(clientId string) []Vote {
|
||||
var ret []Vote
|
||||
var err error
|
||||
if err = db.OpenDB(); err != nil {
|
||||
return ret
|
||||
}
|
||||
defer db.CloseDB()
|
||||
|
||||
var times []string
|
||||
votesBkt := []string{"votes", clientId}
|
||||
if times, err = db.GetBucketList(votesBkt); err != nil {
|
||||
return ret
|
||||
}
|
||||
for _, t := range times {
|
||||
var tm time.Time
|
||||
if tm, err = time.Parse(time.RFC3339, t); err == nil {
|
||||
var vt *Vote
|
||||
if vt, err = dbGetVote(clientId, tm); err == nil {
|
||||
ret = append(ret, *vt)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func dbGetVote(clientId string, timestamp time.Time) (*Vote, error) {
|
||||
var err error
|
||||
if err = db.OpenDB(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.CloseDB()
|
||||
|
||||
vt := new(Vote)
|
||||
vt.Timestamp = timestamp
|
||||
vt.ClientId = clientId
|
||||
votesBkt := []string{"votes", clientId, timestamp.Format(time.RFC3339)}
|
||||
var choices []string
|
||||
if choices, err = db.GetKeyList(votesBkt); err != nil {
|
||||
// Couldn't find the vote...
|
||||
return nil, err
|
||||
}
|
||||
for _, v := range choices {
|
||||
ch := new(GameChoice)
|
||||
var rank int
|
||||
|
||||
if rank, err = strconv.Atoi(v); err == nil {
|
||||
ch.Rank = rank
|
||||
ch.Team, err = db.GetValue(votesBkt, v)
|
||||
vt.Choices = append(vt.Choices, *ch)
|
||||
}
|
||||
}
|
||||
return vt, nil
|
||||
}
|
||||
|
||||
func dbSaveVote(clientId string, timestamp time.Time, votes []string) error {
|
||||
var err error
|
||||
if err = db.OpenDB(); err != nil {
|
||||
return nil
|
||||
}
|
||||
defer db.CloseDB()
|
||||
// Make sure we don't clobber a duplicate vote
|
||||
votesBkt := []string{"votes", clientId, timestamp.Format(time.RFC3339)}
|
||||
for i := range votes {
|
||||
if strings.TrimSpace(votes[i]) != "" {
|
||||
db.SetValue(votesBkt, strconv.Itoa(i), votes[i])
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
@ -31,7 +31,7 @@ func (p *pageSession) setStringValue(key, val string) {
|
||||
p.session.Save(p.req, p.w)
|
||||
}
|
||||
|
||||
func (p *pageSession) getClientID() string {
|
||||
func (p *pageSession) getClientId() string {
|
||||
var clientId string
|
||||
var err error
|
||||
if clientId, err = p.getStringValue("client_id"); err != nil {
|
||||
|
@ -1,30 +1,64 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func initPublicPage(w http.ResponseWriter, req *http.Request) *pageData {
|
||||
p := InitPageData(w, req)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func handleMain(w http.ResponseWriter, req *http.Request) {
|
||||
page := initPublicPage(w, req)
|
||||
page.SubTitle = ""
|
||||
switch dbGetPublicSiteMode() {
|
||||
case SiteModeWaiting:
|
||||
page.show("public-waiting.html", w)
|
||||
case SiteModeVoting:
|
||||
loadVotingPage(w, req)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tmpl := range []string{
|
||||
"htmlheader.html",
|
||||
"admin-menu.html",
|
||||
"header.html",
|
||||
"main.html",
|
||||
"footer.html",
|
||||
"htmlfooter.html",
|
||||
} {
|
||||
if err := outputTemplate(tmpl, page, w); err != nil {
|
||||
fmt.Printf("%s\n", err)
|
||||
func loadVotingPage(w http.ResponseWriter, req *http.Request) {
|
||||
page := initPublicPage(w, req)
|
||||
type votingPageData struct {
|
||||
Teams []Team
|
||||
Timestamp string
|
||||
}
|
||||
vpd := new(votingPageData)
|
||||
vpd.Teams = dbGetAllTeams()
|
||||
vpd.Timestamp = time.Now().Format(time.RFC3339)
|
||||
page.TemplateData = vpd
|
||||
page.show("public-voting.html", w)
|
||||
}
|
||||
|
||||
func handlePublicSaveVote(w http.ResponseWriter, req *http.Request) {
|
||||
page := initPublicPage(w, req)
|
||||
page.SubTitle = ""
|
||||
|
||||
// Check if we already have a vote for this client id/timestamp
|
||||
ts := req.FormValue("timestamp")
|
||||
timestamp, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
page.session.setFlashMessage("Error parsing timestamp: "+ts, "error")
|
||||
redirect("/", w, req)
|
||||
}
|
||||
if _, err := dbGetVote(page.ClientId, timestamp); err == nil {
|
||||
// Duplicate vote... Cancel it.
|
||||
page.session.setFlashMessage("Duplicate vote!", "error")
|
||||
redirect("/", w, req)
|
||||
}
|
||||
// voteSlice is an ordered string slice of the voters preferences
|
||||
voteCSV := req.FormValue("uservote")
|
||||
voteSlice := strings.Split(voteCSV, ",")
|
||||
if err := dbSaveVote(page.ClientId, timestamp, voteSlice); err != nil {
|
||||
page.session.setFlashMessage("Error Saving Vote: "+err.Error(), "error")
|
||||
}
|
||||
if newVote, err := dbGetVote(page.ClientId, timestamp); err == nil {
|
||||
site.Votes = append(site.Votes, *newVote)
|
||||
}
|
||||
redirect("/", w, req)
|
||||
}
|
||||
|
@ -5,45 +5,6 @@
|
||||
<label for="teamname">Team Name</label>
|
||||
<input id="teamname" name="teamname" type="text" placeholder="Team Name" value="" autofocus>
|
||||
</div>
|
||||
|
||||
<h2>Members</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Slack ID</th>
|
||||
<th>Twitter</th>
|
||||
<th>Email</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="team-member">
|
||||
<div class="pure-control-group">
|
||||
<label for="membername">Name</label>
|
||||
<input id="membername" name="membername" type="text" placeholder="Name" value="">
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="memberslackid">Slack ID</label>
|
||||
<input id="memberslackid" name="memberslackid" type="text" placeholder="@SlackID" value="">
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="membertwitter">Twitter</label>
|
||||
<input id="membertwitter" name="membertwitter" type="text" placeholder="@TwitterID" value="">
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label for="memberemail">Email</label>
|
||||
<input id="memberemail" name="memberemail" type="text" placeholder="user@email.com" value="">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="pure-button pure-button-primary">Add Team</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
@ -1,3 +1,7 @@
|
||||
<div>The hosting server is always an authenticated client</div>
|
||||
{{ if not .TemplateData.Clients }}
|
||||
<div>No additional clients have been authenticated</div>
|
||||
{{ else }}
|
||||
<table id="clients-table" class="hidden sortable pure-table pure-table-bordered center">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -17,12 +21,4 @@
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
snack.ready(function() {
|
||||
var tableBody = document.querySelector("#clients-table>tbody");
|
||||
if(tableBody.children.length>0) {
|
||||
// Show the table
|
||||
document.getElementById('clients-table').classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
|
@ -1,26 +1,67 @@
|
||||
{{ $uuid := .TemplateData.UUID }}
|
||||
<div class="center">
|
||||
<form class="pure-form pure-form-aligned" action="/admin/teams/{{ .TemplateData.UUID }}/save" method="POST">
|
||||
<div class="left">
|
||||
<form class="pure-form pure-form-aligned" action="/admin/teams/{{ $uuid }}/save" method="POST">
|
||||
<h3>Team Details</h3>
|
||||
<fieldset>
|
||||
<div class="pure-control-group">
|
||||
<span>{{ .TemplateData.Name }}</span>
|
||||
</div>
|
||||
|
||||
<div class="left big-space">
|
||||
<div class="pure-control-group">
|
||||
<label class="control-label" for="teamname">Team Name</label>
|
||||
<input id="teamname" name="teamname" value="{{ .TemplateData.Name }}" placeholder="Team Name">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="pure-control-group reset-pull">
|
||||
<a href="/admin/teams" class="pull-left space pure-button pure-button-plain">Cancel</a>
|
||||
<button type="submit" class="pull-right space pure-button pure-button-primary">Update</button>
|
||||
<button type="button" id="btnDeleteUser" class="pull-right space pure-button pure-button-error">Delete</button>
|
||||
<button type="submit" class="pull-right space pure-button pure-button-primary">Update Team</button>
|
||||
<button type="button" id="btnDeleteTeam" class="pull-right space pure-button pure-button-error">Delete Team</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<form class="pure-form pure-form-aligned" action="/admin/games/{{ $uuid }}/save" method="POST">
|
||||
<fieldset>
|
||||
<div class="left big-space">
|
||||
<a name="game" />
|
||||
<h3>Team Game</h3>
|
||||
<div class="pure-control-group">
|
||||
<label class="control-label" for="gamename">Game Name</label>
|
||||
<input id="gamename" name="gamename" value="{{ .TemplateData.Game.Name }}" placeholder="Game Name">
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label class="control-label" for="gamedesc">Description</label>
|
||||
<textarea id="gamedesc" name="gamedesc" placeholder="Description...">{{ .TemplateData.Game.Description }}</textarea>
|
||||
</div>
|
||||
<div class="pure-control-group">
|
||||
<label class="control-label">Screenshots</label>
|
||||
<div class="center-all horizontal-scroll thumbnail-container" id="thumbnail-container">
|
||||
{{ if not .TemplateData.Game.Screenshots }}
|
||||
<a style="margin-top:40px;" class="center-all pure-button pure-button-primary" href="javascript:toggleUploadSSForm();">Upload Screenshot</a>
|
||||
{{ else }}
|
||||
{{ range $i, $v := .TemplateData.Game.Screenshots }}
|
||||
<img data-teamid="{{ $uuid }}" data-ssid="{{ $v.UUID }}" class="thumbnail" alt="{{ $v.Description }}" src="data:image/png;base64,{{ $v.Image }}" />
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if .TemplateData.Game.Screenshots }}
|
||||
<div class="right">
|
||||
<a id="toggleUploadSSFormBtn" class="pure-button pure-button-primary" href="javascript:toggleUploadSSForm();">Upload Screenshot</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-control-group reset-pull">
|
||||
<a href="/admin/teams/{{ $uuid }}" class="pull-left space pure-button pure-button-plain">Cancel</a>
|
||||
<button type="submit" class="pull-right space pure-button pure-button-primary">Update Game</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
<h2>Members</h2>
|
||||
<table>
|
||||
<hr />
|
||||
<div class="left">
|
||||
<h3>Team Members</h3>
|
||||
<table class="center padding hide">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
@ -44,35 +85,55 @@
|
||||
<td>
|
||||
<form action="/admin/teams/{{ $uuid }}/deletemember" method="POST">
|
||||
<input type="hidden" name="memberid" value="{{ $v.UUID }}"/>
|
||||
<button type="submit" class="pure-button pure-button-plain"><i class="fa fa-trash"></i></button>
|
||||
<button type="submit" class="pure-button pure-button-error"><i class="fa fa-trash"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<td colspan="6" class="center">Add a new member</td>
|
||||
<td colspan="6">Add a new member</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="6">
|
||||
<form action="/admin/teams/{{ $uuid }}/savemember" method="POST">
|
||||
<input id="newmembername" name="newmembername" value="" placeholder="Member Name" />
|
||||
<td colspan="6" class="padding">
|
||||
<form class="pure-form" action="/admin/teams/{{ $uuid }}/savemember" method="POST">
|
||||
<div class="pure-control-group">
|
||||
<input id="newmembername" name="newmembername" value="" placeholder="Member Name" autofocus />
|
||||
<input id="newmemberslackid" name="newmemberslackid" value="" placeholder="@SlackID" />
|
||||
<input id="newmembertwitter" name="newmembertwitter" value="" placeholder="@Twitter" />
|
||||
<input id="newmemberemail" name="newmemberemail" value="" placeholder="user@email.com" />
|
||||
<button type="submit" class="pull-right space pure-button pure-button-primary">Add</button>
|
||||
<button type="submit" class="pull-right space-sides pure-button pure-button-primary">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pure-control-group reset-pull">
|
||||
<a href="/admin/teams" class="pull-left space pure-button pure-button-plain">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="uploadscreenshotform" style="display:none;">
|
||||
<h3>Upload Screenshot</h3>
|
||||
<form class="pure-form pure-form-aligned" action="/admin/games/{{ $uuid }}/screenshotupload" method="POST" enctype="multipart/form-data">
|
||||
<div class="pure-control-group" style="margin-bottom:50px;">
|
||||
<input class="file" type="file" name="newssfile" multiple>
|
||||
</div>
|
||||
<a href="javascript:hideModal();" class="pull-left space-sides pure-button">Cancel</a>
|
||||
<button type="submit" class="pull-right space-sides pure-button pure-button-primary">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="editscreenshotform" style="display:none;">
|
||||
<div id="editss-container" class="pure-control-group" style="margin-bottom:50px;">
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
snack.listener(
|
||||
{node:document.getElementById('btnDeleteTeam'),event:'click'},
|
||||
function() {
|
||||
showModal({
|
||||
title: 'Delete Team',
|
||||
subtitle: '({{ .TemplateData.Name }} - {{ $uuid}})',
|
||||
subtitle: '({{ .TemplateData.Name }})',
|
||||
body: 'Are you sure? This cannot be undone.',
|
||||
buttons: [{
|
||||
title:'Cancel',
|
||||
@ -87,4 +148,51 @@
|
||||
});
|
||||
}
|
||||
);
|
||||
snack.listener(
|
||||
{
|
||||
node:document.getElementById('thumbnail-container'),
|
||||
event:'click',
|
||||
delegate: function(node) {
|
||||
return node.getElementsByTagName('img');
|
||||
}
|
||||
},
|
||||
function() {
|
||||
showEditScreenShotModal(snack.wrap(this)[0]);
|
||||
}
|
||||
);
|
||||
|
||||
function showEditScreenShotModal(img) {
|
||||
var newImg = img.cloneNode();
|
||||
var editSSForm = document.getElementById('editscreenshotform');
|
||||
var cont = document.getElementById('editss-container');
|
||||
while(cont.hasChildNodes()) {
|
||||
cont.removeChild(cont.lastChild);
|
||||
}
|
||||
cont.appendChild(newImg);
|
||||
showModal({
|
||||
title: 'Edit Screenshot',
|
||||
bodyNode: editSSForm,
|
||||
buttons: [
|
||||
{ title: 'Delete', class: 'pure-button-error', position: 'right',
|
||||
click: function() {
|
||||
window.location = "/admin/games/{{ $uuid }}/screenshotdelete/"+img.dataset.ssid;
|
||||
}
|
||||
},
|
||||
{ title: 'Cancel', class: 'pure-button', position: 'right', click: hideModal }
|
||||
]
|
||||
});
|
||||
editSSForm.style.display="block";
|
||||
editSSForm.style.height="200px";
|
||||
}
|
||||
|
||||
function toggleUploadSSForm() {
|
||||
var uploadForm = document.getElementById('uploadscreenshotform');
|
||||
showModal({
|
||||
title: 'Upload Screenshot',
|
||||
subtitle: '({{ .TemplateData.Name }})',
|
||||
bodyNode: uploadForm
|
||||
});
|
||||
uploadForm.style.display="block";
|
||||
document.getElementById('modal-body').style.height='165px';
|
||||
}
|
||||
</script>
|
||||
|
22
templates/admin-games.html
Normal file
22
templates/admin-games.html
Normal file
@ -0,0 +1,22 @@
|
||||
{{ if not .TemplateData.Teams }}
|
||||
<div>No games have been created</div>
|
||||
{{ else }}
|
||||
<table id="games-table" class="sortable pure-table pure-table-bordered center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Game Name</th>
|
||||
<th>Team Name</th>
|
||||
<th>Screenshots</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $i, $v := .TemplateData.Teams }}
|
||||
<tr>
|
||||
<td>{{ $v.Game.Name }}</td>
|
||||
<td>{{ $v.Name }}</td>
|
||||
<td>{{ len $v.Game.Screenshots }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
@ -1,6 +1,24 @@
|
||||
<div class="content">
|
||||
<button onclick="window.location.href='/admin/votes'">Votes</button>
|
||||
<button onclick="window.location.href='/admin/teams'">Teams</button>
|
||||
<button onclick="window.location.href='/admin/games'">Games</button>
|
||||
<button onclick="window.location.href='/admin/users'">Users</button>
|
||||
<div>
|
||||
<h3>Public Mode</h3>
|
||||
<button onclick="window.location.href='/admin/mode/0'" class="pure-button-toggle-first pure-button {{ if eq .PublicMode 0 }}pure-button-primary{{ end }}">Waiting</button>
|
||||
<button onclick="window.location.href='/admin/mode/1'" class="pure-button-toggle-last pure-button {{ if eq .PublicMode 1 }}pure-button-primary{{ end }}">Voting</button>
|
||||
</div>
|
||||
{{ if eq .PublicMode 1 }}
|
||||
<div>
|
||||
<h3>Current Results</h3>
|
||||
<ol>
|
||||
{{ range $i, $v := .TemplateData }}
|
||||
<li>{{ $v.Name }}</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div>
|
||||
<h3>Admin Sections</h3>
|
||||
<button class="pure-button" onclick="window.location.href='/admin/votes'">Votes</button>
|
||||
<button class="pure-button" onclick="window.location.href='/admin/teams'">Teams</button>
|
||||
<button class="pure-button" onclick="window.location.href='/admin/games'">Games</button>
|
||||
<button class="pure-button" onclick="window.location.href='/admin/users'">Users</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,6 +1,5 @@
|
||||
<div id="menu" class="{{if .HideAdminMenu}}hidden{{end}}">
|
||||
<div class="pure-menu">
|
||||
<a class="pure-menu-heading" href="/admin/">Admin</a>
|
||||
<a href="#menu" class="menu-button">
|
||||
<i class="fa fa-bars"></i>
|
||||
</a>
|
||||
@ -13,9 +12,11 @@
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if .ClientIsAuth }}
|
||||
<a href="/admin/clients/{{.ClientID}}/remove" class="pure-menu-link"><i class="fa fa-key"></i> DeAuth Client</a>
|
||||
{{ if not .ClientIsServer }}
|
||||
<a href="/admin/clients/{{.ClientId}}/remove" class="pure-menu-link"><i class="fa fa-key"></i> DeAuth Client</a>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<a href="/admin/clients/{{.ClientID}}/add" class="pure-menu-link"><i class="fa fa-key"></i> Auth Client</a>
|
||||
<a href="/admin/clients/{{.ClientId}}/add" class="pure-menu-link"><i class="fa fa-key"></i> Auth Client</a>
|
||||
{{ end }}
|
||||
<ul class="pure-menu-list menu-bottom">
|
||||
{{ range $k, $v := .BottomMenu }}
|
||||
|
@ -1,12 +1,16 @@
|
||||
<div class="bottom-space center">
|
||||
<a id="btnAddTeam" class="pure-button pure-button-success" href="/admin/teams/new"><i class="fa fa-plus"></i> Add Team</a>
|
||||
</div>
|
||||
<table id="teams-table" class="hidden sortable pure-table pure-table-bordered center">
|
||||
{{ if not .TemplateData.Teams }}
|
||||
<div>No teams have been created</div>
|
||||
{{ else }}
|
||||
<table id="teams-table" class="sortable pure-table pure-table-bordered center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Key</th>
|
||||
<th>Members</th>
|
||||
<th>Game</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -16,6 +20,7 @@
|
||||
<td>{{ $v.Name }}</td>
|
||||
<td>{{ $v.UUID }}</td>
|
||||
<td>{{ len $v.Members }}</td>
|
||||
<td>{{ $v.Game.Name }}</td>
|
||||
<td>
|
||||
<a href="/admin/teams/{{ $v.UUID }}/edit" class="pure-button pure-button-plain"><i class="fa fa-pencil"></i></a>
|
||||
<a href="/admin/teams/{{ $v.UUID }}/delete" class="pure-button pure-button-plain"><i class="fa fa-trash"></i></a>
|
||||
@ -24,12 +29,4 @@
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
<script>
|
||||
snack.ready(function() {
|
||||
var tableBody = document.querySelector("#teams-table>tbody");
|
||||
if(tableBody.children.length>0) {
|
||||
// Show the table
|
||||
document.getElementById('teams-table').classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
|
35
templates/admin-votes.html
Normal file
35
templates/admin-votes.html
Normal file
@ -0,0 +1,35 @@
|
||||
<h2>Current Results</h2>
|
||||
<ol>
|
||||
{{ range $i, $v := .TemplateData.Results }}
|
||||
<li>{{ $v.Name }}</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
<table id="votes-table" class="sortable pure-table pure-table-bordered center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Client ID</th>
|
||||
<th>Rankings</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $i, $v := .TemplateData.AllVotes }}
|
||||
<tr>
|
||||
<td>{{ $v.Timestamp }}</td>
|
||||
<td>{{ $v.ClientId }}</td>
|
||||
<td>
|
||||
<ol>
|
||||
{{ range $ci, $cv := $v.Choices }}
|
||||
<li>{{ $cv.Name }}</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="left" colspan="3">{{ len .Site.Votes }} Total Votes</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
@ -1,13 +1,12 @@
|
||||
<script>
|
||||
var clientID = "{{.ClientID}}";
|
||||
var clientId = "{{.ClientId}}";
|
||||
</script>
|
||||
<div class="content">
|
||||
<aside class="flash center {{.FlashClass}}">
|
||||
{{.FlashMessage}}
|
||||
</aside>
|
||||
<div class="header">
|
||||
devICT Game Jam - {{.CurrentJam}}
|
||||
</div>
|
||||
<div class="content">
|
||||
{{ if .SubTitle }}
|
||||
<div class="header-menu">
|
||||
<h1>{{.SubTitle}}</h1>
|
||||
<h2>{{.SubTitle}}</h2>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
@ -1 +0,0 @@
|
||||
<div>Default Public Facing Page</div>
|
337
templates/public-voting.html
Normal file
337
templates/public-voting.html
Normal file
@ -0,0 +1,337 @@
|
||||
{{ if not .TemplateData.Teams }}
|
||||
<div>No games have been created</div>
|
||||
{{ else }}
|
||||
<div class="content">
|
||||
Rank one or more games from your favorite to least favorite.
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Your Choices</h2>
|
||||
<table id="ranked-table" class="pure-table pure-table-bordered center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Game Name</th>
|
||||
<th>Team Name</th>
|
||||
<th>Screenshots</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="content">
|
||||
<h2>Unranked Games</h2>
|
||||
<table id="unranked-table" class="pure-table pure-table-bordered center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Game Name</th>
|
||||
<th>Team Name</th>
|
||||
<th>Screenshots</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range $i, $v := .TemplateData.Teams }}
|
||||
<tr id="teamrow-{{$v.UUID}}" data-teamid="{{$v.UUID}}">
|
||||
<td class="unranked-actions"><a class="pure-button pure-button-primary" href="javascript:moveToRanked('{{$v.UUID}}');"><i class="fa fa-plus"></i> Add to Vote</a></td>
|
||||
<td class="voting-col game-name">{{ $v.Game.Name }}</td>
|
||||
<td class="voting-col team-name">{{ $v.Name }}</td>
|
||||
<td class="voting-col game-screenshots" data-sscount="{{len $v.Game.Screenshots}}">
|
||||
{{ if not $v.Game.Screenshots }}
|
||||
<i class="fa fa-image"></i> (No Screenshots)
|
||||
{{ else }}
|
||||
<a class="primary" tabindex="-1" href="javascript:showScreenshots('{{$v.UUID}}');"><i class="fa fa-image"></i> ({{ len $v.Game.Screenshots }}) Click to View</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="content half">
|
||||
<form action="/vote">
|
||||
<input id="uservote" type="hidden" name="uservote" value="" />
|
||||
<input id="timestamp" type="hidden" name="timestamp" value="{{.TemplateData.Timestamp}}" />
|
||||
<button class="pure-button pure-button-primary space-vertical pull-right" type="submit">Submit Vote!</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ range $i, $v := .TemplateData.Teams }}
|
||||
<div class="pure-control-group" id="screenshots-{{ $v.UUID }}" style="display:none;">
|
||||
<h3>{{ $v.Game.Name }} by {{ $v.Name }}</h3>
|
||||
<label class="control-label">Click to view original size</label>
|
||||
<div class="center-all horizontal-scroll thumbnail-container" id="thumbnail-container">
|
||||
{{ range $imgi, $imgv := $v.Game.Screenshots }}
|
||||
<a href="javascript:embiggenScreenshot(this.getElementsByTagName('img')[0]);">
|
||||
<img data-teamid="{{ $v.UUID }}" data-ssid="{{ $imgv.UUID }}" class="thumbnail" alt="{{ $imgv.Description }}" src="data:image/png;base64,{{ $imgv.Image }}" />
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<script>
|
||||
var teamNames = { };
|
||||
{{ range $i, $v := .TemplateData.Teams }}
|
||||
teamNames[{{$v.UUID}}] = { "team-name": "{{$v.Name}}", "game-name": "{{$v.Game.Name}}" };
|
||||
{{ end }}
|
||||
|
||||
function showScreenshots(tmuuid) {
|
||||
var screenshots = document.getElementById('screenshots-'+tmuuid).cloneNode(true);
|
||||
screenshots.style.display='';
|
||||
showModal({
|
||||
title: 'Screenshots',
|
||||
subtitle: teamNames[tmuuid].game-name,
|
||||
bodyNode: screenshots,
|
||||
buttons: [{
|
||||
title: 'Done',
|
||||
position: 'right',
|
||||
click: hideModal
|
||||
}]
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function embiggenScreenshot(img) {
|
||||
|
||||
}
|
||||
|
||||
function updateView() {
|
||||
updateButtonStates();
|
||||
var rankedCells = snack.wrap('#ranked-table>tbody>tr>td.rank-cell');
|
||||
for(var i = 0; i < rankedCells.length; i++) {
|
||||
rankedCells[i].innerText = i+1;
|
||||
}
|
||||
setUserVoteValue();
|
||||
}
|
||||
|
||||
function setUserVoteValue() {
|
||||
document.getElementById('uservote').value=getRankedCSV();
|
||||
}
|
||||
|
||||
// moveToRanked takes the uuid of a game/team and moves that game to the
|
||||
// bottom of the 'ranked' table
|
||||
function moveToRanked(tmUUID) {
|
||||
// First, find the team's row
|
||||
var rows = snack.wrap('#teamrow-'+tmUUID);
|
||||
if(rows.length > 0) {
|
||||
var row = rows[0];
|
||||
var delCell = row.getElementsByClassName('unranked-actions');
|
||||
if(delCell.length > 0) {
|
||||
delCell = delCell[0];
|
||||
}
|
||||
delCell.remove();
|
||||
var tbody = snack.wrap('#ranked-table>tbody')[0];
|
||||
var rankTd = document.createElement('td');
|
||||
rankTd.classList.add('rank-cell');
|
||||
row.prepend(rankTd);
|
||||
row.append(createRankedActionsTd(tmUUID));
|
||||
tbody.append(row)
|
||||
}
|
||||
updateView();
|
||||
}
|
||||
|
||||
function moveRankedUp(tmUUID) {
|
||||
var rows = snack.wrap('#ranked-table>tbody>tr');
|
||||
var numRows = rows.length;
|
||||
var tbody = snack.wrap('#ranked-table>tbody')[0];
|
||||
if(rows.length > 0) {
|
||||
// Just loop through the rows adding them to the table
|
||||
// if the _next_ row is the row for this team, add it now
|
||||
for(var i = 0; i < numRows; i++) {
|
||||
if(numRows > i && rows[i+1] != null
|
||||
&& rows[i+1].dataset.teamid == tmUUID) {
|
||||
// The next one is the one we're moving up
|
||||
// Append the _next_ one, then this one
|
||||
tbody.append(rows[i+1]);
|
||||
tbody.append(rows[i]);
|
||||
// Increment i manually, since we already added the next row
|
||||
i++;
|
||||
} else {
|
||||
// Otherwise just add the row
|
||||
tbody.append(rows[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateView();
|
||||
}
|
||||
|
||||
function updateButtonStates() {
|
||||
var upBtns = snack.wrap('.ranked-move-up');
|
||||
for(var i = 0; i < upBtns.length; i++) {
|
||||
if(i == 0) {
|
||||
upBtns[i].disabled=true;
|
||||
} else {
|
||||
upBtns[i].disabled=false;
|
||||
}
|
||||
}
|
||||
var downBtns = snack.wrap('.ranked-move-down');
|
||||
for(var i = 0; i < downBtns.length; i++) {
|
||||
if(i == downBtns.length-1) {
|
||||
downBtns[i].disabled=true;
|
||||
} else {
|
||||
downBtns[i].disabled=false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function moveRankedDown(tmUUID) {
|
||||
var rows = snack.wrap('#ranked-table>tbody>tr');
|
||||
var numRows = rows.length;
|
||||
var tbody = snack.wrap('#ranked-table>tbody')[0];
|
||||
if(numRows > 0) {
|
||||
// Just loop through the rows adding them to the table
|
||||
// When we hit the row for this team, delay it by one
|
||||
for(var i = 0; i < numRows; i++) {
|
||||
if(rows[i].dataset.teamid == tmUUID && numRows > i) {
|
||||
// This is the one we're moving down
|
||||
// Append the _next_ one, then this one
|
||||
tbody.append(rows[i+1]);
|
||||
tbody.append(rows[i]);
|
||||
// Increment i manually, since we already added the next row
|
||||
i++;
|
||||
} else {
|
||||
// Otherwise just add the row
|
||||
tbody.append(rows[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateView();
|
||||
}
|
||||
|
||||
// moveToRanked takes the uuid of a game/team and moves that game to the
|
||||
// bottom of the 'unranked' table
|
||||
function moveToUnranked(tmUUID) {
|
||||
// First, find the team's row
|
||||
var rows = snack.wrap('#teamrow-'+tmUUID);
|
||||
if(rows.length > 0) {
|
||||
var row = rows[0];
|
||||
// Remove the cells we don't need
|
||||
var actCell = row.getElementsByClassName('ranked-actions');
|
||||
if(actCell.length > 0) {
|
||||
actCell = actCell[0];
|
||||
}
|
||||
actCell.remove();
|
||||
var rankTd = row.getElementsByClassName('rank-cell');
|
||||
if(rankTd.length > 0) {
|
||||
rankTd = rankTd[0];
|
||||
}
|
||||
rankTd.remove();
|
||||
// Add the cells we do
|
||||
row.prepend(createUnrankedActionsTd(tmUUID));
|
||||
// And add the row to the unranked table
|
||||
var tbody = snack.wrap('#unranked-table>tbody')[0];
|
||||
tbody.append(row);
|
||||
}
|
||||
updateView();
|
||||
}
|
||||
|
||||
// getUnranked returns an array of games that haven't been ranked yet
|
||||
// (it builds the array from the 'unranked' table)
|
||||
function getUnranked() {
|
||||
return gameTableToArray('unranked');
|
||||
}
|
||||
|
||||
// getRanked returns an array of games that the user has ranked
|
||||
// (it builds the array from the 'ranked' table)
|
||||
function getRanked() {
|
||||
return gameTableToArray('ranked');
|
||||
}
|
||||
|
||||
// Converts either the 'ranked' or 'unranked' table to an array of objects
|
||||
// 'tbl' should be either 'ranked' or 'unranked'
|
||||
function gameTableToArray(tbl) {
|
||||
var ret = [];
|
||||
snack.wrap('#'+tbl+'-table>tbody>tr').each(function(ele, idx) {
|
||||
ret = ret.concat(getTeamObj(ele.dataset.teamid));
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
// getTeamObj returns an object for team tmUUID from table
|
||||
function getTeamObj(tmUUID) {
|
||||
var ret = null;
|
||||
var rows = snack.wrap('#teamrow-'+tmUUID);
|
||||
if(rows.length > 0) {
|
||||
var ele = rows[0];
|
||||
ret = {
|
||||
uuid: tmUUID,
|
||||
name: ele.getElementsByClassName('game-name')[0].innerText,
|
||||
teamName: ele.getElementsByClassName('team-name')[0].innerText,
|
||||
ssCount: ele.getElementsByClassName('game-screenshots')[0].dataset.sscount
|
||||
};
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// getRankedCSV pulls the getRanked array and just returns a CSV of
|
||||
// the team IDs in ranked order
|
||||
// (This is how the 'vote' post expects it)
|
||||
function getRankedCSV() {
|
||||
var r = getRanked();
|
||||
var ret = "";
|
||||
for(var i = 0; i < r.length; i++) {
|
||||
ret = ret + r[i].uuid+",";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// createRankedActionsTd creates the td that holds all of the action
|
||||
// buttons for a 'ranked' table row
|
||||
function createRankedActionsTd(tmUUID) {
|
||||
var td = document.createElement('td');
|
||||
td.classList.add('ranked-actions');
|
||||
var upBtn = document.createElement('button');
|
||||
upBtn.classList.add('ranked-move-up', 'pure-button', 'pure-button-toggle-first', 'pure-button-primary');
|
||||
upBtn.innerHTML = '<i class="fa fa-arrow-up"></i> Move Up';
|
||||
snack.listener({
|
||||
node: upBtn,
|
||||
event: 'click'
|
||||
}, function() {
|
||||
moveRankedUp(tmUUID);
|
||||
});
|
||||
td.appendChild(upBtn);
|
||||
var dnBtn = document.createElement('button');
|
||||
dnBtn.classList.add('ranked-move-down', 'pure-button', 'pure-button-toggle-middle', 'pure-button-primary');
|
||||
dnBtn.innerHTML = '<i class="fa fa-arrow-down"></i> Move Down';
|
||||
snack.listener({
|
||||
node: dnBtn,
|
||||
event: 'click'
|
||||
}, function() {
|
||||
moveRankedDown(tmUUID);
|
||||
});
|
||||
td.appendChild(dnBtn);
|
||||
var delBtn = document.createElement('button');
|
||||
delBtn.dataset.teamid=tmUUID
|
||||
delBtn.classList.add('ranked-remove', 'pure-button', 'pure-button-toggle-last', 'pure-button-error');
|
||||
delBtn.innerHTML = '<i class="fa fa-times"></i> Remove';
|
||||
snack.listener({
|
||||
node: delBtn,
|
||||
event: 'click'
|
||||
}, function (){
|
||||
moveToUnranked(tmUUID);
|
||||
});
|
||||
td.appendChild(delBtn);
|
||||
return td;
|
||||
}
|
||||
|
||||
// createUnrankedActionsTd created the td that holds the 'Add to Vote' button
|
||||
function createUnrankedActionsTd(tmUUID) {
|
||||
var td = document.createElement('td');
|
||||
td.classList.add('unranked-actions');
|
||||
var addBtn = document.createElement('button');
|
||||
addBtn.dataset.teamid=tmUUID
|
||||
addBtn.classList.add('pure-button', 'pure-button-toggle-last', 'pure-button-primary');
|
||||
addBtn.innerHTML = '<i class="fa fa-plus"></i> Add to Vote';
|
||||
var params = {
|
||||
node: addBtn,
|
||||
event: 'click'
|
||||
}
|
||||
snack.listener(params, function (){
|
||||
moveToRanked(addBtn.dataset.teamid);
|
||||
})
|
||||
td.appendChild(addBtn);
|
||||
return td;
|
||||
}
|
||||
</script>
|
1
templates/public-waiting.html
Normal file
1
templates/public-waiting.html
Normal file
@ -0,0 +1 @@
|
||||
<div>ICT Game Jam</div>
|
Loading…
Reference in New Issue
Block a user