Initial Commit

This commit is contained in:
2026-03-05 21:25:59 +02:00
commit 22b700e241
21 changed files with 1694 additions and 0 deletions

16
src/config/config.go Normal file
View File

@@ -0,0 +1,16 @@
package config
import "os"
type Config struct {
Port string
}
func Load() Config {
port := os.Getenv("PORT")
if port == "" {
port = "8002"
}
return Config{Port: port}
}

View File

@@ -0,0 +1,20 @@
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
"scrum-solitare/src/models"
)
type PageController struct{}
func NewPageController() *PageController {
return &PageController{}
}
func (pc *PageController) ShowRoomSetup(c *gin.Context) {
pageData := models.DefaultRoomSetupPageData()
c.HTML(http.StatusOK, "index.html", pageData)
}

19
src/main.go Normal file
View File

@@ -0,0 +1,19 @@
package main
import (
"log"
"scrum-solitare/src/config"
"scrum-solitare/src/controllers"
"scrum-solitare/src/server"
)
func main() {
cfg := config.Load()
pageController := controllers.NewPageController()
router := server.NewRouter(pageController)
if err := router.Run(":" + cfg.Port); err != nil {
log.Fatalf("server failed to start: %v", err)
}
}

View File

@@ -0,0 +1,20 @@
package middleware
import (
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
func StaticCacheControl() gin.HandlerFunc {
return func(c *gin.Context) {
ext := strings.ToLower(filepath.Ext(c.Request.URL.Path))
switch ext {
case ".css", ".js", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico":
c.Header("Cache-Control", "public, max-age=31536000, immutable")
c.Header("Vary", "Accept-Encoding")
}
c.Next()
}
}

31
src/models/room_setup.go Normal file
View File

@@ -0,0 +1,31 @@
package models
type RoomSetupPageData struct {
DefaultRoomName string
DefaultUsername string
DefaultMaxPeople int
DefaultScale string
DefaultRevealMode string
DefaultVotingTime int
DefaultModerator string
AllowSpectators bool
AnonymousVoting bool
AutoResetCards bool
DefaultStatus string
}
func DefaultRoomSetupPageData() RoomSetupPageData {
return RoomSetupPageData{
DefaultRoomName: "Sprint Planning",
DefaultUsername: "",
DefaultMaxPeople: 50,
DefaultScale: "fibonacci",
DefaultRevealMode: "manual",
DefaultVotingTime: 0,
DefaultModerator: "creator",
AllowSpectators: true,
AnonymousVoting: true,
AutoResetCards: true,
DefaultStatus: "Ready to create room.",
}
}

25
src/routes/routes.go Normal file
View File

@@ -0,0 +1,25 @@
package routes
import (
"net/http"
"github.com/gin-gonic/gin"
"scrum-solitare/src/controllers"
"scrum-solitare/src/middleware"
)
func Register(r *gin.Engine, pageController *controllers.PageController) {
registerStatic(r)
registerPages(r, pageController)
}
func registerStatic(r *gin.Engine) {
static := r.Group("/static")
static.Use(middleware.StaticCacheControl())
static.StaticFS("/", http.Dir("static"))
}
func registerPages(r *gin.Engine, pageController *controllers.PageController) {
r.GET("/", pageController.ShowRoomSetup)
}

20
src/server/router.go Normal file
View File

@@ -0,0 +1,20 @@
package server
import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"scrum-solitare/src/controllers"
"scrum-solitare/src/routes"
)
func NewRouter(pageController *controllers.PageController) *gin.Engine {
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
r.Use(gzip.Gzip(gzip.DefaultCompression))
r.LoadHTMLGlob("src/templates/*.html")
routes.Register(r, pageController)
return r
}

View File

@@ -0,0 +1,127 @@
{{ define "body" }}
<section class="window config-window" aria-label="Room configuration">
<div class="title-bar">
<span>CreateRoom.exe</span>
<div class="title-bar-controls" aria-hidden="true">
<button type="button">_</button>
<button type="button"></button>
<button type="button">×</button>
</div>
</div>
<div class="window-content">
<p class="intro-copy">Configure your Scrum Poker room and share the invite link with your team.</p>
<form id="room-config-form" class="room-form" novalidate>
<div class="config-layout">
<section class="config-panel">
<div class="field-group">
<label for="room-name">Room name</label>
<input type="text" id="room-name" name="roomName" maxlength="40" value="{{ .DefaultRoomName }}" placeholder="Sprint 32 Planning" required>
</div>
<div class="field-row">
<div class="field-group">
<label for="username">Your username</label>
<input type="text" id="username" name="username" maxlength="32" value="{{ .DefaultUsername }}" placeholder="alice_dev" required>
</div>
<div class="field-group">
<label for="max-people">Max people</label>
<div class="number-input-wrap">
<input type="number" id="max-people" name="maxPeople" min="2" max="50" value="{{ .DefaultMaxPeople }}" required>
</div>
</div>
</div>
<div class="field-row">
<div class="field-group">
<label for="estimation-scale">Estimation scale</label>
<select id="estimation-scale" name="estimationScale">
<option value="fibonacci" {{ if eq .DefaultScale "fibonacci" }}selected{{ end }}>Fibonacci (0,1,2,3,5,8,13,21,?)</option>
<option value="tshirt" {{ if eq .DefaultScale "tshirt" }}selected{{ end }}>T-Shirt (XS,S,M,L,XL,?)</option>
<option value="powers-of-two" {{ if eq .DefaultScale "powers-of-two" }}selected{{ end }}>Powers of 2 (1,2,4,8,16,32,?)</option>
</select>
</div>
<div class="field-group">
<label for="reveal-mode">Reveal mode</label>
<select id="reveal-mode" name="revealMode">
<option value="manual" {{ if eq .DefaultRevealMode "manual" }}selected{{ end }}>Manual reveal by moderator</option>
<option value="all-voted" {{ if eq .DefaultRevealMode "all-voted" }}selected{{ end }}>Auto reveal when everyone voted</option>
</select>
</div>
</div>
<div class="field-row">
<div class="field-group">
<label for="voting-timeout">Voting timeout (seconds)</label>
<div class="number-input-wrap number-with-unit">
<input type="number" id="voting-timeout" name="votingTimeout" min="0" max="3600" value="{{ .DefaultVotingTime }}">
<span class="input-unit">sec</span>
</div>
</div>
<div class="field-group">
<label for="moderator">Moderator role</label>
<select id="moderator" name="moderatorRole">
<option value="creator" {{ if eq .DefaultModerator "creator" }}selected{{ end }}>Room creator is moderator</option>
<option value="none" {{ if eq .DefaultModerator "none" }}selected{{ end }}>No fixed moderator</option>
</select>
</div>
</div>
<fieldset class="window options-box">
<legend>Room options</legend>
<label class="option-item">
<input type="checkbox" id="allow-spectators" name="allowSpectators" {{ if .AllowSpectators }}checked{{ end }}>
<span>Allow spectators (non-voting viewers)</span>
</label>
<label class="option-item">
<input type="checkbox" id="anonymous-voting" name="anonymousVoting" {{ if .AnonymousVoting }}checked{{ end }}>
<span>Anonymous voting until reveal</span>
</label>
<label class="option-item">
<input type="checkbox" id="auto-reset" name="autoReset" {{ if .AutoResetCards }}checked{{ end }}>
<span>Auto-reset cards after each reveal</span>
</label>
</fieldset>
</section>
<aside class="window preview-window" aria-label="Room preview">
<div class="title-bar">
<span>Room Preview</span>
</div>
<div class="window-content preview-content">
<div class="preview-meta">
<span id="preview-scale">Scale: {{ .DefaultScale }}</span>
<span id="preview-max-people">Max: {{ .DefaultMaxPeople }}</span>
</div>
<div class="preview-board" id="preview-board">
<div class="preview-cards" id="preview-cards"></div>
</div>
<div class="card-editor">
<label for="custom-card">Add card</label>
<div class="card-editor-row">
<input type="text" id="custom-card" maxlength="8" placeholder="e.g. 34 or ?">
<button type="button" id="add-card" class="btn">Add</button>
</div>
</div>
</div>
</aside>
</div>
<div class="status-line" id="config-status" role="status" aria-live="polite">
{{ .DefaultStatus }}
</div>
<div class="actions-row">
<button type="reset" class="btn">Reset</button>
<button type="submit" class="btn btn-primary">Create Room</button>
</div>
</form>
</div>
</section>
{{ end }}

10
src/templates/footer.html Normal file
View File

@@ -0,0 +1,10 @@
{{ define "footer" }}
</main>
<footer class="taskbar" aria-hidden="true">
<div class="taskbar-start">Start</div>
<div class="taskbar-status">Scrum Poker Setup</div>
</footer>
<script src="/static/js/app.js"></script>
</body>
</html>
{{ end }}

18
src/templates/header.html Normal file
View File

@@ -0,0 +1,18 @@
{{ define "header" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Retro Scrum Poker - Room Setup</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body>
<div class="top-bar">
<button class="btn" id="theme-toggle">Dark Mode</button>
</div>
<main id="desktop">
{{ end }}

5
src/templates/index.html Normal file
View File

@@ -0,0 +1,5 @@
{{ define "index.html" }}
{{ template "header" . }}
{{ template "body" . }}
{{ template "footer" . }}
{{ end }}