Working on editing team games

This commit is contained in:
Brian Buller 2017-06-15 12:35:53 -05:00
parent 597623d71b
commit 1faf4b9aa1
11 changed files with 518 additions and 87 deletions

View File

@ -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
}

View File

@ -1,8 +1,12 @@
package main package main
import ( import (
"fmt"
"io"
"mime/multipart"
"net/http" "net/http"
"strings" "os"
"strconv"
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
@ -10,23 +14,63 @@ 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 Games []Game
} }
if gameId == "new" { page.TemplateData = gamesPageData{Games: dbGetAllGames()}
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)
} }
} }
} }
func saveScreenshots(teamId string, req *http.Request) error {
err := req.ParseMultipartForm((1 << 10) * 24)
if err != nil {
return err
}
for _, fheaders := range req.MultipartForm.File {
for _, hdr := range fheaders {
// open uploaded
var infile multipart.File
if infile, err = hdr.Open(); err != nil {
return err
}
// open destination
var outfile *os.File
if outfile, err = os.Create("./uploaded/" + hdr.Filename); err != nil {
return err
}
// 32K buffer copy
var written int64
if written, err = io.Copy(outfile, infile); err != nil {
return err
}
fmt.Println("uploaded file:" + hdr.Filename + ";length:" + strconv.Itoa(int(written)))
}
}
return nil
}

View File

@ -11,6 +11,7 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
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")
@ -30,6 +31,7 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
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":
@ -44,8 +46,11 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
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")
} }
redirect("/admin/teams", w, req) redirect("/admin/teams", w, req)
case "savemember": case "savemember":
@ -61,10 +66,11 @@ func handleAdminTeams(w http.ResponseWriter, req *http.Request, page *pageData)
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")
} }
redirect("/admin/teams/"+teamId, w, req) redirect("/admin/teams/"+teamId, w, req)
default: default:
@ -78,10 +84,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)

View File

@ -17,6 +17,14 @@ div.content {
margin-left: 0; margin-left: 0;
} }
input.file {
padding: .5em .6em;
display: inline-block;
border: 1px solid #ccc;
box-shadow: inset 0 1px 3px #ddd;
border-radius: 4px;
}
#menu { #menu {
width: 100%; width: 100%;
height: 40px; height: 40px;
@ -29,6 +37,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 +73,87 @@ div.content {
background-color: #191818; background-color: #191818;
} }
input.uuid-field { div.horizontal-scroll {
width: 360px; overflow-x: scroll;
}
img.thumbnail {
width: 100px;
height: 100px;
}
.thumbnail-container {
display: block;
height: 120px;
background-color: #EEE;
}
.padding {
padding: 5px;
}
.space {
margin: 5px;
}
.space-sides {
margin-left: 5px;
margin-right: 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 td {
text-align: center;
}
table.padding td {
padding: 5px;
}
.pure-button-error {
background-color: #DD0000;
color: #FFFFFF;
}
#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 +187,11 @@ input.uuid-field {
margin-left: 150px; margin-left: 150px;
} }
aside.flash.success {
background-color: #229af9;
color: #FFFFFF;
}
.content { .content {
margin-left: 150px; margin-left: 150px;
} }

View File

@ -15,3 +15,51 @@ 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';
}
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);
}
}

View File

@ -53,6 +53,7 @@ type pageData struct {
CurrentJam string CurrentJam string
ClientID string ClientID string
ClientIsAuth bool ClientIsAuth bool
ClientIsServer bool
TeamID string TeamID string
TemplateData interface{} TemplateData interface{}
@ -237,7 +238,8 @@ func InitPageData(w http.ResponseWriter, req *http.Request) *pageData {
} }
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)
p.TeamID, _ = p.session.getStringValue("teamid") p.TeamID, _ = p.session.getStringValue("teamid")
return p return p

View File

@ -1,12 +1,145 @@
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
}
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)
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)
} }

View File

@ -105,6 +105,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,7 +214,6 @@ 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)
} }
} }

View File

@ -1,26 +1,66 @@
{{ $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">
<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">
{{ 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 class="thumbnail" alt="{{ $v.Description }}" src="{{ $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 +84,43 @@
<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>
<script> <script>
snack.listener( snack.listener(
@ -72,7 +128,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 +143,14 @@
}); });
} }
); );
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>

View File

@ -12,11 +12,15 @@
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{if .ClientIsAuth}} {{ if .ClientIsAuth }}
{{ if .ClientIsServer }}
<span class="pure-menu-nonlink"><i class="fa fa-server"></i> Server Mode</span>
{{ 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}}/remove" class="pure-menu-link"><i class="fa fa-key"></i> DeAuth Client</a>
{{else}} {{ 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}} {{ 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">

View File

@ -1,13 +1,13 @@
<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="content">
<div class="header"> <div class="header">
devICT Game Jam - {{.CurrentJam}} devICT Game Jam - {{.CurrentJam}}
</div> </div>
<div class="header-menu"> <div class="header-menu">
<h1>{{.SubTitle}}</h1> <h2>{{.SubTitle}}</h2>
</div> </div>