commit
6ad85d68f7
@ -1,2 +1,11 @@
|
|||||||
# ictgj-voting
|
# ictgj-voting
|
||||||
The ICT GameJam Voting Application
|
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 (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
@ -40,8 +41,25 @@ func handleAdmin(w http.ResponseWriter, req *http.Request) {
|
|||||||
handleAdminGames(w, req, page)
|
handleAdminGames(w, req, page)
|
||||||
case "clients":
|
case "clients":
|
||||||
handleAdminClients(w, req, page)
|
handleAdminClients(w, req, page)
|
||||||
|
case "votes":
|
||||||
|
handleAdminVotes(w, req, page)
|
||||||
|
case "mode":
|
||||||
|
handleAdminSetMode(w, req, page)
|
||||||
default:
|
default:
|
||||||
|
page.TemplateData = getCondorcetResult()
|
||||||
page.show("admin-main.html", w)
|
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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
"encoding/base64"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
@ -10,23 +12,51 @@ import (
|
|||||||
func handleAdminGames(w http.ResponseWriter, req *http.Request, page *pageData) {
|
func handleAdminGames(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
page.SubTitle = "Games"
|
page.SubTitle = "Games"
|
||||||
gameId := vars["id"]
|
teamId := vars["id"]
|
||||||
teamId := req.FormValue("teamid")
|
if teamId == "" {
|
||||||
if strings.TrimSpace(teamId) != "" {
|
// Games List
|
||||||
page.session.setStringValue("teamid", teamId)
|
type gamesPageData struct {
|
||||||
page.TeamID = teamId
|
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"] {
|
switch vars["function"] {
|
||||||
case "save":
|
case "save":
|
||||||
name := req.FormValue("gamename")
|
name := req.FormValue("gamename")
|
||||||
|
desc := req.FormValue("gamedesc")
|
||||||
if dbIsValidTeam(teamId) {
|
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:
|
case "screenshotupload":
|
||||||
page.SubTitle = "Add New Game"
|
if err := saveScreenshots(teamId, req); err != nil {
|
||||||
page.show("admin-addgame.html", w)
|
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"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func refreshTeamsInMemory() {
|
||||||
|
site.Teams = dbGetAllTeams()
|
||||||
|
}
|
||||||
|
|
||||||
func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) {
|
func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData) {
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
page.SubTitle = "Teams"
|
page.SubTitle = "Teams"
|
||||||
teamId := vars["id"]
|
teamId := vars["id"]
|
||||||
if teamId == "new" {
|
if teamId == "new" {
|
||||||
|
// Add a new team
|
||||||
switch vars["function"] {
|
switch vars["function"] {
|
||||||
case "save":
|
case "save":
|
||||||
name := req.FormValue("teamname")
|
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")
|
page.session.setFlashMessage("Team "+name+" created!", "success")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
refreshTeamsInMemory()
|
||||||
redirect("/admin/teams", w, req)
|
redirect("/admin/teams", w, req)
|
||||||
default:
|
default:
|
||||||
page.SubTitle = "Add New Team"
|
page.SubTitle = "Add New Team"
|
||||||
page.show("admin-addteam.html", w)
|
page.show("admin-addteam.html", w)
|
||||||
}
|
}
|
||||||
} else if teamId != "" {
|
} else if teamId != "" {
|
||||||
|
// Functions for existing team
|
||||||
if dbIsValidTeam(teamId) {
|
if dbIsValidTeam(teamId) {
|
||||||
switch vars["function"] {
|
switch vars["function"] {
|
||||||
case "save":
|
case "save":
|
||||||
@ -41,12 +48,17 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
|
|||||||
} else {
|
} else {
|
||||||
page.session.setFlashMessage("Team Updated!", "success")
|
page.session.setFlashMessage("Team Updated!", "success")
|
||||||
}
|
}
|
||||||
|
refreshTeamsInMemory()
|
||||||
redirect("/admin/teams", w, req)
|
redirect("/admin/teams", w, req)
|
||||||
case "delete":
|
case "delete":
|
||||||
var err error
|
var err error
|
||||||
|
t := dbGetTeam(teamId)
|
||||||
if err = dbDeleteTeam(teamId); err != nil {
|
if err = dbDeleteTeam(teamId); err != nil {
|
||||||
page.session.setFlashMessage("Error deleting team: "+err.Error(), "error")
|
page.session.setFlashMessage("Error deleting team: "+err.Error(), "error")
|
||||||
|
} else {
|
||||||
|
page.session.setFlashMessage("Team "+t.Name+" Deleted", "success")
|
||||||
}
|
}
|
||||||
|
refreshTeamsInMemory()
|
||||||
redirect("/admin/teams", w, req)
|
redirect("/admin/teams", w, req)
|
||||||
case "savemember":
|
case "savemember":
|
||||||
mbrName := req.FormValue("newmembername")
|
mbrName := req.FormValue("newmembername")
|
||||||
@ -58,14 +70,17 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
|
|||||||
} else {
|
} else {
|
||||||
page.session.setFlashMessage(mbrName+" added to team!", "success")
|
page.session.setFlashMessage(mbrName+" added to team!", "success")
|
||||||
}
|
}
|
||||||
|
refreshTeamsInMemory()
|
||||||
redirect("/admin/teams/"+teamId, w, req)
|
redirect("/admin/teams/"+teamId, w, req)
|
||||||
case "deletemember":
|
case "deletemember":
|
||||||
mbrId := req.FormValue("memberid")
|
mbrId := req.FormValue("memberid")
|
||||||
|
m, _ := dbGetTeamMember(teamId, mbrId)
|
||||||
if err := dbDeleteTeamMember(teamId, mbrId); err != nil {
|
if err := dbDeleteTeamMember(teamId, mbrId); err != nil {
|
||||||
page.session.setFlashMessage("Error deleting team member: "+err.Error(), "error")
|
page.session.setFlashMessage("Error deleting team member: "+err.Error(), "error")
|
||||||
} else {
|
} 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)
|
redirect("/admin/teams/"+teamId, w, req)
|
||||||
default:
|
default:
|
||||||
page.SubTitle = "Edit Team"
|
page.SubTitle = "Edit Team"
|
||||||
@ -78,10 +93,10 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
|
|||||||
redirect("/admin/teams", w, req)
|
redirect("/admin/teams", w, req)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Team List
|
||||||
type teamsPageData struct {
|
type teamsPageData struct {
|
||||||
Teams []Team
|
Teams []Team
|
||||||
}
|
}
|
||||||
|
|
||||||
page.TemplateData = teamsPageData{Teams: dbGetAllTeams()}
|
page.TemplateData = teamsPageData{Teams: dbGetAllTeams()}
|
||||||
page.SubTitle = "Teams"
|
page.SubTitle = "Teams"
|
||||||
page.show("admin-teams.html", w)
|
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 {
|
div.bottom-space {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img.thumbnail {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
@ -4,7 +4,11 @@ html {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
color: #777;
|
color: #777;
|
||||||
min-ehgiht: 100%;
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.content {
|
div.content {
|
||||||
@ -13,10 +17,44 @@ div.content {
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.half {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
margin-left: 0;
|
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 {
|
#menu {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -29,6 +67,14 @@ div.content {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#menu .pure-menu-nonlink {
|
||||||
|
color: #777;
|
||||||
|
padding: .5em 1em;
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
#menu .menu-button {
|
#menu .menu-button {
|
||||||
display: inline;
|
display: inline;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -57,8 +103,117 @@ div.content {
|
|||||||
background-color: #191818;
|
background-color: #191818;
|
||||||
}
|
}
|
||||||
|
|
||||||
input.uuid-field {
|
div.horizontal-scroll {
|
||||||
width: 360px;
|
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) {
|
@media (min-width: 40em) {
|
||||||
@ -92,6 +247,16 @@ input.uuid-field {
|
|||||||
margin-left: 150px;
|
margin-left: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aside.flash.error {
|
||||||
|
background-color: #DD0000;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside.flash.success {
|
||||||
|
background-color: #229af9;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin-left: 150px;
|
margin-left: 150px;
|
||||||
}
|
}
|
||||||
|
@ -15,3 +15,52 @@ document.onkeydown = function(evt) {
|
|||||||
toggleAdminPanel();
|
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":""}
|
|
186
main.go
186
main.go
@ -1,11 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
//go:generate esc -o assets.go assets templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@ -21,17 +21,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const AppName = "gjvote"
|
const AppName = "gjvote"
|
||||||
|
const DbName = AppName + ".db"
|
||||||
|
|
||||||
// SiteData is stuff that stays the same
|
// SiteData is stuff that stays the same
|
||||||
type siteData struct {
|
type siteData struct {
|
||||||
Title string `json:"title"`
|
Title string
|
||||||
Port int `json:"port"`
|
Port int
|
||||||
SessionName string `json:"session"`
|
SessionName string
|
||||||
ServerDir string `json:"dir"`
|
ServerDir string
|
||||||
DevMode bool `json:"devmode"`
|
DevMode bool
|
||||||
DB string `json:"db"`
|
|
||||||
|
|
||||||
CurrentJam string
|
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
|
// pageData is stuff that changes per request
|
||||||
@ -51,10 +63,12 @@ type pageData struct {
|
|||||||
HideAdminMenu bool
|
HideAdminMenu bool
|
||||||
session *pageSession
|
session *pageSession
|
||||||
CurrentJam string
|
CurrentJam string
|
||||||
ClientID string
|
ClientId string
|
||||||
ClientIsAuth bool
|
ClientIsAuth bool
|
||||||
|
ClientIsServer bool
|
||||||
TeamID string
|
TeamID string
|
||||||
|
|
||||||
|
PublicMode int
|
||||||
TemplateData interface{}
|
TemplateData interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,23 +83,27 @@ var sessionSecret = "JCOP5e8ohkTcOzcSMe74"
|
|||||||
var sessionStore = sessions.NewCookieStore([]byte(sessionSecret))
|
var sessionStore = sessions.NewCookieStore([]byte(sessionSecret))
|
||||||
var site *siteData
|
var site *siteData
|
||||||
var r *mux.Router
|
var r *mux.Router
|
||||||
var configFile string
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
configFile = "./config.json"
|
|
||||||
loadConfig()
|
loadConfig()
|
||||||
saveConfig()
|
dbSaveSiteConfig(site)
|
||||||
initialize()
|
initialize()
|
||||||
|
|
||||||
r = mux.NewRouter()
|
r = mux.NewRouter()
|
||||||
r.StrictSlash(true)
|
r.StrictSlash(true)
|
||||||
|
|
||||||
s := http.StripPrefix("/assets/", http.FileServer(http.Dir(site.ServerDir+"assets/")))
|
if site.DevMode {
|
||||||
r.PathPrefix("/assets/").Handler(s)
|
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
|
// Public Subrouter
|
||||||
pub := r.PathPrefix("/").Subrouter()
|
pub := r.PathPrefix("/").Subrouter()
|
||||||
pub.HandleFunc("/", handleMain)
|
pub.HandleFunc("/", handleMain)
|
||||||
|
pub.HandleFunc("/vote", handlePublicSaveVote)
|
||||||
|
|
||||||
// API Subrouter
|
// API Subrouter
|
||||||
//api := r.PathPrefix("/api").Subtrouter()
|
//api := r.PathPrefix("/api").Subtrouter()
|
||||||
@ -98,6 +116,7 @@ func main() {
|
|||||||
admin.HandleFunc("/{category}", handleAdmin)
|
admin.HandleFunc("/{category}", handleAdmin)
|
||||||
admin.HandleFunc("/{category}/{id}", handleAdmin)
|
admin.HandleFunc("/{category}/{id}", handleAdmin)
|
||||||
admin.HandleFunc("/{category}/{id}/{function}", handleAdmin)
|
admin.HandleFunc("/{category}/{id}/{function}", handleAdmin)
|
||||||
|
admin.HandleFunc("/{category}/{id}/{function}/{subid}", handleAdmin)
|
||||||
|
|
||||||
http.Handle("/", r)
|
http.Handle("/", r)
|
||||||
|
|
||||||
@ -108,25 +127,47 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() {
|
func loadConfig() {
|
||||||
site = new(siteData)
|
site = dbGetSiteConfig()
|
||||||
// Defaults:
|
|
||||||
site.Title = "ICT GameJam"
|
|
||||||
site.Port = 8080
|
|
||||||
site.SessionName = "ict-gamejam"
|
|
||||||
site.ServerDir = "./"
|
|
||||||
site.DevMode = false
|
|
||||||
site.DB = AppName + ".db"
|
|
||||||
|
|
||||||
jsonInp, err := ioutil.ReadFile(configFile)
|
if len(os.Args) > 1 {
|
||||||
if err == nil {
|
for _, v := range os.Args {
|
||||||
assertError(json.Unmarshal(jsonInp, &site))
|
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() {
|
func initialize() {
|
||||||
@ -170,6 +211,10 @@ func initialize() {
|
|||||||
} else {
|
} else {
|
||||||
fmt.Println(err.Error())
|
fmt.Println(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load all votes into memory
|
||||||
|
site.Votes = dbGetAllVotes()
|
||||||
|
site.Teams = dbGetAllTeams()
|
||||||
}
|
}
|
||||||
|
|
||||||
func loggingHandler(h http.Handler) http.Handler {
|
func loggingHandler(h http.Handler) http.Handler {
|
||||||
@ -201,13 +246,13 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
|
|||||||
p.Site = site
|
p.Site = site
|
||||||
p.SubTitle = "GameJam Voting"
|
p.SubTitle = "GameJam Voting"
|
||||||
p.Stylesheets = make([]string, 0, 0)
|
p.Stylesheets = make([]string, 0, 0)
|
||||||
p.Stylesheets = append(p.Stylesheets, "/assets/css/pure-min.css")
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/css/pure-min.css")
|
||||||
p.Stylesheets = append(p.Stylesheets, "/assets/css/grids-responsive-min.css")
|
p.Stylesheets = append(p.Stylesheets, "/assets/vendor/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/font-awesome/css/font-awesome.min.css")
|
||||||
p.Stylesheets = append(p.Stylesheets, "/assets/css/gjvote.css")
|
p.Stylesheets = append(p.Stylesheets, "/assets/css/gjvote.css")
|
||||||
|
|
||||||
p.HeaderScripts = make([]string, 0, 0)
|
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 = make([]string, 0, 0)
|
||||||
p.Scripts = append(p.Scripts, "/assets/js/gjvote.js")
|
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
|
// Build the menu
|
||||||
if p.LoggedIn {
|
if p.LoggedIn {
|
||||||
p.Menu = append(p.Menu, menuItem{"Admin", "/admin", "fa-key"})
|
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{"Teams", "/admin/teams", "fa-users"})
|
||||||
p.Menu = append(p.Menu, menuItem{"Games", "/admin/games", "fa-gamepad"})
|
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{"Users", "/admin/users", "fa-user"})
|
||||||
p.BottomMenu = append(p.BottomMenu, menuItem{"Logout", "/admin/dologout", "fa-sign-out"})
|
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.FlashClass = "error"
|
||||||
}
|
}
|
||||||
|
|
||||||
p.ClientID = p.session.getClientID()
|
p.ClientId = p.session.getClientId()
|
||||||
p.ClientIsAuth = dbClientIsAuth(p.ClientID)
|
p.ClientIsAuth = clientIsAuthenticated(p.ClientId, req)
|
||||||
|
p.ClientIsServer = clientIsServer(req)
|
||||||
|
// TeamID is for team self-administration
|
||||||
p.TeamID, _ = p.session.getStringValue("teamid")
|
p.TeamID, _ = p.session.getStringValue("teamid")
|
||||||
|
|
||||||
|
// Public Mode
|
||||||
|
p.PublicMode = dbGetPublicSiteMode()
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -263,13 +314,11 @@ func (p *pageData) show(tmplName string, w http.ResponseWriter) error {
|
|||||||
// outputTemplate
|
// outputTemplate
|
||||||
// Spit out a template
|
// Spit out a template
|
||||||
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
func outputTemplate(tmplName string, tmplData interface{}, w http.ResponseWriter) error {
|
||||||
_, err := os.Stat("templates/" + tmplName)
|
// TODO: Use embedded files for these... Hopefully?
|
||||||
if err == nil {
|
n := "/templates/" + tmplName
|
||||||
t := template.New(tmplName)
|
l := template.Must(template.New("layout").Parse(FSMustString(site.DevMode, n)))
|
||||||
t, _ = t.ParseFiles("templates/" + tmplName)
|
t := template.Must(l.Parse(FSMustString(site.DevMode, n)))
|
||||||
return t.Execute(w, tmplData)
|
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
|
// redirect can be used only for GET redirects
|
||||||
@ -277,10 +326,53 @@ func redirect(url string, w http.ResponseWriter, req *http.Request) {
|
|||||||
http.Redirect(w, req, url, 303)
|
http.Redirect(w, req, url, 303)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printOutput(out string) {
|
func resetToDefaults() {
|
||||||
if site.DevMode {
|
def := GetDefaultSiteConfig()
|
||||||
fmt.Print(out)
|
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) {
|
func assertError(err error) {
|
||||||
|
78
model.go
78
model.go
@ -10,11 +10,26 @@ import (
|
|||||||
var db *boltease.DB
|
var db *boltease.DB
|
||||||
var dbOpened int
|
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 {
|
func openDatabase() error {
|
||||||
dbOpened += 1
|
dbOpened += 1
|
||||||
if dbOpened == 1 {
|
if dbOpened == 1 {
|
||||||
var err error
|
var err error
|
||||||
db, err = boltease.Create(site.DB, 0600, nil)
|
db, err = boltease.Create(DbName, 0600, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -82,3 +97,64 @@ func dbGetCurrentJam() (string, error) {
|
|||||||
}
|
}
|
||||||
return ret, err
|
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 {
|
type Client struct {
|
||||||
UUID string
|
UUID string
|
||||||
Auth bool
|
Auth bool
|
||||||
|
Name string
|
||||||
|
IP string
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbGetAllClients() []Client {
|
func dbGetAllClients() []Client {
|
||||||
@ -35,9 +37,32 @@ func dbGetClient(id string) *Client {
|
|||||||
cl := new(Client)
|
cl := new(Client)
|
||||||
cl.UUID = id
|
cl.UUID = id
|
||||||
cl.Auth = dbClientIsAuth(id)
|
cl.Auth = dbClientIsAuth(id)
|
||||||
|
cl.Name, _ = db.GetValue([]string{"clients", id}, "name")
|
||||||
|
cl.IP, _ = db.GetValue([]string{"clients", id}, "ip")
|
||||||
return cl
|
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 {
|
func dbAddDeauthClient(cid, ip string) error {
|
||||||
var err error
|
var err error
|
||||||
if err = db.OpenDB(); err != nil {
|
if err = db.OpenDB(); err != nil {
|
||||||
|
143
model_games.go
143
model_games.go
@ -1,12 +1,147 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/pborman/uuid"
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/pborman/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
UUID *uuid.UUID
|
|
||||||
Name string
|
Name string
|
||||||
|
TeamId string
|
||||||
|
Description string
|
||||||
|
Screenshots []Screenshot
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbIsValidGame(id string) bool {
|
type Screenshot struct {
|
||||||
return true
|
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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/pborman/uuid"
|
"github.com/pborman/uuid"
|
||||||
)
|
)
|
||||||
@ -105,6 +104,7 @@ func dbGetTeam(id string) *Team {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tm.Members, _ = dbGetTeamMembers(id)
|
tm.Members, _ = dbGetTeamMembers(id)
|
||||||
|
tm.Game = dbGetTeamGame(id)
|
||||||
return tm
|
return tm
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,14 +213,11 @@ func dbGetTeamMembers(teamid string) ([]TeamMember, error) {
|
|||||||
for _, v := range memberUuids {
|
for _, v := range memberUuids {
|
||||||
var mbr *TeamMember
|
var mbr *TeamMember
|
||||||
if mbr, err = dbGetTeamMember(teamid, v); err == nil {
|
if mbr, err = dbGetTeamMember(teamid, v); err == nil {
|
||||||
fmt.Println("Finding Team Members", teamid, mbr.Name)
|
|
||||||
ret = append(ret, *mbr)
|
ret = append(ret, *mbr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
}
|
}
|
||||||
return ret, nil
|
return ret, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func dbGetTeamMember(teamid, mbrid string) (*TeamMember, error) {
|
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)
|
p.session.Save(p.req, p.w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *pageSession) getClientID() string {
|
func (p *pageSession) getClientId() string {
|
||||||
var clientId string
|
var clientId string
|
||||||
var err error
|
var err error
|
||||||
if clientId, err = p.getStringValue("client_id"); err != nil {
|
if clientId, err = p.getStringValue("client_id"); err != nil {
|
||||||
|
@ -1,30 +1,64 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func initPublicPage(w http.ResponseWriter, req *http.Request) *pageData {
|
func initPublicPage(w http.ResponseWriter, req *http.Request) *pageData {
|
||||||
p := InitPageData(w, req)
|
p := InitPageData(w, req)
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMain(w http.ResponseWriter, req *http.Request) {
|
func handleMain(w http.ResponseWriter, req *http.Request) {
|
||||||
page := initPublicPage(w, req)
|
page := initPublicPage(w, req)
|
||||||
page.SubTitle = ""
|
page.SubTitle = ""
|
||||||
|
switch dbGetPublicSiteMode() {
|
||||||
for _, tmpl := range []string{
|
case SiteModeWaiting:
|
||||||
"htmlheader.html",
|
page.show("public-waiting.html", w)
|
||||||
"admin-menu.html",
|
case SiteModeVoting:
|
||||||
"header.html",
|
loadVotingPage(w, req)
|
||||||
"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>
|
<label for="teamname">Team Name</label>
|
||||||
<input id="teamname" name="teamname" type="text" placeholder="Team Name" value="" autofocus>
|
<input id="teamname" name="teamname" type="text" placeholder="Team Name" value="" autofocus>
|
||||||
</div>
|
</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>
|
<button type="submit" class="pure-button pure-button-primary">Add Team</button>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</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">
|
<table id="clients-table" class="hidden sortable pure-table pure-table-bordered center">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@ -17,12 +21,4 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<script>
|
{{ end }}
|
||||||
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>
|
|
||||||
|
@ -1,26 +1,67 @@
|
|||||||
{{ $uuid := .TemplateData.UUID }}
|
{{ $uuid := .TemplateData.UUID }}
|
||||||
<div class="center">
|
<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>
|
<fieldset>
|
||||||
<div class="pure-control-group">
|
<div class="left big-space">
|
||||||
<span>{{ .TemplateData.Name }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label class="control-label" for="teamname">Team Name</label>
|
<label class="control-label" for="teamname">Team Name</label>
|
||||||
<input id="teamname" name="teamname" value="{{ .TemplateData.Name }}" placeholder="Team Name">
|
<input id="teamname" name="teamname" value="{{ .TemplateData.Name }}" placeholder="Team Name">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="pure-control-group reset-pull">
|
<div class="pure-control-group reset-pull">
|
||||||
<a href="/admin/teams" class="pull-left space pure-button pure-button-plain">Cancel</a>
|
<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="submit" class="pull-right space pure-button pure-button-primary">Update Team</button>
|
||||||
<button type="button" id="btnDeleteUser" class="pull-right space pure-button pure-button-error">Delete</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>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h2>Members</h2>
|
<hr />
|
||||||
<table>
|
<div class="left">
|
||||||
|
<h3>Team Members</h3>
|
||||||
|
<table class="center padding hide">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
@ -44,27 +85,47 @@
|
|||||||
<td>
|
<td>
|
||||||
<form action="/admin/teams/{{ $uuid }}/deletemember" method="POST">
|
<form action="/admin/teams/{{ $uuid }}/deletemember" method="POST">
|
||||||
<input type="hidden" name="memberid" value="{{ $v.UUID }}"/>
|
<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>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6" class="center">Add a new member</td>
|
<td colspan="6">Add a new member</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="6">
|
<td colspan="6" class="padding">
|
||||||
<form action="/admin/teams/{{ $uuid }}/savemember" method="POST">
|
<form class="pure-form" action="/admin/teams/{{ $uuid }}/savemember" method="POST">
|
||||||
<input id="newmembername" name="newmembername" value="" placeholder="Member Name" />
|
<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="newmemberslackid" name="newmemberslackid" value="" placeholder="@SlackID" />
|
||||||
<input id="newmembertwitter" name="newmembertwitter" value="" placeholder="@Twitter" />
|
<input id="newmembertwitter" name="newmembertwitter" value="" placeholder="@Twitter" />
|
||||||
<input id="newmemberemail" name="newmemberemail" value="" placeholder="user@email.com" />
|
<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>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
snack.listener(
|
snack.listener(
|
||||||
@ -72,7 +133,7 @@
|
|||||||
function() {
|
function() {
|
||||||
showModal({
|
showModal({
|
||||||
title: 'Delete Team',
|
title: 'Delete Team',
|
||||||
subtitle: '({{ .TemplateData.Name }} - {{ $uuid}})',
|
subtitle: '({{ .TemplateData.Name }})',
|
||||||
body: 'Are you sure? This cannot be undone.',
|
body: 'Are you sure? This cannot be undone.',
|
||||||
buttons: [{
|
buttons: [{
|
||||||
title:'Cancel',
|
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>
|
</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">
|
<div class="content">
|
||||||
<button onclick="window.location.href='/admin/votes'">Votes</button>
|
<div>
|
||||||
<button onclick="window.location.href='/admin/teams'">Teams</button>
|
<h3>Public Mode</h3>
|
||||||
<button onclick="window.location.href='/admin/games'">Games</button>
|
<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/users'">Users</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>
|
</div>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<div id="menu" class="{{if .HideAdminMenu}}hidden{{end}}">
|
<div id="menu" class="{{if .HideAdminMenu}}hidden{{end}}">
|
||||||
<div class="pure-menu">
|
<div class="pure-menu">
|
||||||
<a class="pure-menu-heading" href="/admin/">Admin</a>
|
|
||||||
<a href="#menu" class="menu-button">
|
<a href="#menu" class="menu-button">
|
||||||
<i class="fa fa-bars"></i>
|
<i class="fa fa-bars"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -12,11 +11,13 @@
|
|||||||
</li>
|
</li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
{{if .ClientIsAuth}}
|
{{ if .ClientIsAuth }}
|
||||||
<a href="/admin/clients/{{.ClientID}}/remove" class="pure-menu-link"><i class="fa fa-key"></i> DeAuth Client</a>
|
{{ if not .ClientIsServer }}
|
||||||
{{else}}
|
<a href="/admin/clients/{{.ClientId}}/remove" class="pure-menu-link"><i class="fa fa-key"></i> DeAuth Client</a>
|
||||||
<a href="/admin/clients/{{.ClientID}}/add" class="pure-menu-link"><i class="fa fa-key"></i> Auth Client</a>
|
{{ end }}
|
||||||
{{end}}
|
{{ else }}
|
||||||
|
<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">
|
<ul class="pure-menu-list menu-bottom">
|
||||||
{{ range $k, $v := .BottomMenu }}
|
{{ range $k, $v := .BottomMenu }}
|
||||||
<li class="pure-menu-item">
|
<li class="pure-menu-item">
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<div class="bottom-space center">
|
<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>
|
<a id="btnAddTeam" class="pure-button pure-button-success" href="/admin/teams/new"><i class="fa fa-plus"></i> Add Team</a>
|
||||||
</div>
|
</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>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Key</th>
|
<th>Key</th>
|
||||||
<th>Members</th>
|
<th>Members</th>
|
||||||
|
<th>Game</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -16,6 +20,7 @@
|
|||||||
<td>{{ $v.Name }}</td>
|
<td>{{ $v.Name }}</td>
|
||||||
<td>{{ $v.UUID }}</td>
|
<td>{{ $v.UUID }}</td>
|
||||||
<td>{{ len $v.Members }}</td>
|
<td>{{ len $v.Members }}</td>
|
||||||
|
<td>{{ $v.Game.Name }}</td>
|
||||||
<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 }}/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>
|
<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 }}
|
{{ end }}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<script>
|
{{ end }}
|
||||||
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>
|
|
||||||
|
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>
|
<script>
|
||||||
var clientID = "{{.ClientID}}";
|
var clientId = "{{.ClientId}}";
|
||||||
</script>
|
</script>
|
||||||
<div class="content">
|
<aside class="flash center {{.FlashClass}}">
|
||||||
<aside class="flash center {{.FlashClass}}">
|
|
||||||
{{.FlashMessage}}
|
{{.FlashMessage}}
|
||||||
</aside>
|
</aside>
|
||||||
<div class="header">
|
<div class="content">
|
||||||
devICT Game Jam - {{.CurrentJam}}
|
{{ if .SubTitle }}
|
||||||
</div>
|
|
||||||
<div class="header-menu">
|
<div class="header-menu">
|
||||||
<h1>{{.SubTitle}}</h1>
|
<h2>{{.SubTitle}}</h2>
|
||||||
</div>
|
</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