diff --git a/cmd/web/handlers.go b/cmd/web/handlers.go index ede1f96..1d4d498 100644 --- a/cmd/web/handlers.go +++ b/cmd/web/handlers.go @@ -4,6 +4,7 @@ import ( api "code.dlmw.ch/dlmw/qv/internal" "code.dlmw.ch/dlmw/qv/internal/models" "code.dlmw.ch/dlmw/qv/internal/validator" + "code.dlmw.ch/dlmw/qv/ui" "database/sql" "encoding/json" "errors" @@ -15,6 +16,17 @@ import ( "time" ) +func (app *application) createElectionPage(w http.ResponseWriter, r *http.Request) { + content, err := ui.Files.ReadFile("create-election.html") + if err != nil { + app.serverError(w, r, fmt.Errorf("couldn't read create-election.html")) + return + } + + w.Header().Set("Content-Type", "text/html") + w.Write(content) +} + type createElectionRequestWithValidator struct { api.CreateElectionRequest validator.Validator diff --git a/cmd/web/routes.go b/cmd/web/routes.go index 0ffedb9..924b4ed 100644 --- a/cmd/web/routes.go +++ b/cmd/web/routes.go @@ -2,6 +2,7 @@ package main import ( "code.dlmw.ch/dlmw/qv/internal/models" + "code.dlmw.ch/dlmw/qv/ui" "github.com/justinas/alice" "log/slog" "net/http" @@ -17,6 +18,9 @@ type application struct { func (app *application) routes() http.Handler { mux := http.NewServeMux() + mux.Handle("GET /static/", http.FileServerFS(ui.Files)) + mux.HandleFunc("GET /election/create", app.createElectionPage) + mux.HandleFunc("POST /election", app.createElection) mux.HandleFunc("POST /election/{id}/votes", app.createVotes) diff --git a/ui/create-election.html b/ui/create-election.html new file mode 100644 index 0000000..3884f6d --- /dev/null +++ b/ui/create-election.html @@ -0,0 +1,66 @@ + + + + + + Create New Election + + + +
+
+

Create New Election

+
+
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+
+
+
+ + diff --git a/ui/static/styles.css b/ui/static/styles.css new file mode 100644 index 0000000..977212b --- /dev/null +++ b/ui/static/styles.css @@ -0,0 +1,163 @@ +:root { + --primary-color: #2563eb; + --error-color: #dc2626; + --background-color: #f8fafc; + --border-color: #e2e8f0; + --text-color: #1e293b; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + line-height: 1.6; + color: var(--text-color); + background: var(--background-color); + min-height: 100vh; +} + +.container { + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + padding: 2rem; +} + +main { + width: 100%; + max-width: 600px; + background: white; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 2rem; +} + +.form { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.form-row { + display: flex; + gap: 1rem; + flex-wrap: wrap; +} + +.form-row .form-group { + flex: 1 1 200px; + min-width: 0; +} + +.form-group { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +h1 { + margin-bottom: 2rem; + color: var(--text-color); + font-size: 1.5rem; +} + +label { + font-weight: 500; +} + +input, select { + padding: 0.5rem; + border: 1px solid var(--border-color); + border-radius: 4px; + font-size: 1rem; +} + +input:focus, select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); +} + +#choices-container { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.choice-input { + display: flex; + gap: 0.5rem; +} + +.choice-input input { + flex: 1; +} + +button { + background: var(--primary-color); + color: white; + border: none; + padding: 0.75rem 1.5rem; + border-radius: 4px; + cursor: pointer; + font-size: 1rem; + transition: background-color 0.2s; +} + +button:hover { + background-color: #1d4ed8; +} + +#add-choice { + background: transparent; + color: var(--primary-color); + border: 1px solid var(--primary-color); + margin-top: 0.5rem; +} + +#add-choice:hover { + background: rgba(37, 99, 235, 0.1); +} + +.remove-choice { + padding: 0.5rem 1rem; + background: transparent; + color: var(--error-color); + border: 1px solid var(--error-color); + flex-shrink: 0; +} + +.remove-choice:hover { + background: rgba(220, 38, 38, 0.1); +} + +.form-actions { + display: flex; + justify-content: flex-end; + margin-top: 1rem; +} + +@media (max-width: 640px) { + .container { + padding: 0; + } + + main { + border-radius: 0; + box-shadow: none; + padding: 1rem; + } + + .form-actions { + flex-direction: column; + } + + button { + width: 100%; + } +} diff --git a/ui/ui.go b/ui/ui.go new file mode 100644 index 0000000..a0674a9 --- /dev/null +++ b/ui/ui.go @@ -0,0 +1,6 @@ +package ui + +import "embed" + +//go:embed *.html static +var Files embed.FS