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