prettier ui and better login handling

This commit is contained in:
2025-11-06 22:19:20 +01:00
parent 09e65803c9
commit d3a71e1d0f
7 changed files with 516 additions and 73 deletions

View File

@@ -15,7 +15,10 @@ type difficultyLevel struct {
func adminLoginHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
http.ServeFile(w, r, "templates/adminLogin.html")
err := adminLoginTemplate.Execute(w, false)
if err != nil {
http.Error(w, "Could not render template", http.StatusInternalServerError)
}
case http.MethodPost:
if err := r.ParseForm(); err != nil {
http.Error(w, "Error parsing form", http.StatusBadRequest)
@@ -29,7 +32,10 @@ func adminLoginHandler(w http.ResponseWriter, r *http.Request) {
).Scan(new(int))
switch {
case err == sql.ErrNoRows:
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
err := adminLoginTemplate.Execute(w, true)
if err != nil {
http.Error(w, "Could not render template", http.StatusInternalServerError)
}
case err != nil:
http.Error(w, "Database error", http.StatusInternalServerError)
default:
@@ -82,7 +88,7 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
http.ServeFile(w, r, "templates/adminPanel.html")
http.ServeFileFS(w, r, templatesFS, "templates/adminPanel.html")
}
func adminTeamsHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -41,7 +41,10 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
err := db.QueryRow("SELECT 1 FROM teams WHERE password = ?", hashedPassword).Scan(new(int))
switch {
case err == sql.ErrNoRows:
http.Error(w, "No team found", http.StatusUnauthorized)
err = LoginTemplate.Execute(w, true)
if err != nil {
http.Error(w, "Could not render template", http.StatusInternalServerError)
}
return
case err != nil:
http.Error(w, "Could not retrieve team", http.StatusInternalServerError)
@@ -68,7 +71,10 @@ func loginHandler(w http.ResponseWriter, r *http.Request) {
}
}
case http.MethodGet:
http.ServeFileFS(w, r, templatesFS, "templates/login.html")
err := LoginTemplate.Execute(w, false)
if err != nil {
http.Error(w, "Could not render template", http.StatusInternalServerError)
}
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}

View File

@@ -99,3 +99,5 @@ var AdminCipherTemplate = template.Must(template.ParseFS(templatesFS, "templates
var AdminPositionsTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminPositions.html"))
var AdminQRsTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminQR.html"))
var AdminPenaltiesTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminPenalties.html"))
var LoginTemplate = template.Must(template.ParseFS(templatesFS, "templates/login.html"))
var adminLoginTemplate = template.Must(template.ParseFS(templatesFS, "templates/adminLogin.html"))

View File

@@ -2,17 +2,33 @@
<html lang="cs">
<head>
<meta charset="UTF-8">
<title>Admin Login</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin — přihlášení</title>
</head>
<body>
<h1>Admin Login</h1>
<form method="post">
Uživatelské jméno: <input type="text" name="username"><br>
Heslo: <input type="password" name="password"><br>
<input type="submit" value="Přihlásit se">
{{if .}}
<p style="color: red;"><strong>Chybné uživatelské jméno nebo heslo.</strong></p>
{{end}}
<form method="post" autocomplete="off">
<div>
<label for="username">Uživatelské jméno</label><br>
<input id="username" name="username" type="text" required>
</div>
<div>
<label for="password">Heslo</label><br>
<input id="password" name="password" type="password" required>
</div>
<div>
<input type="submit" value="Přihlásit se">
</div>
</form>
<p><a href="/admin">Zpět na admin panel</a></p>
</body>
</html>

View File

@@ -2,56 +2,269 @@
<html lang="cs">
<head>
<meta charset="UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Zadání šifry {{.Order}}</title>
<style>
:root {
--bg: #f5f7fb;
--card: #ffffff;
--accent: #0b6efd;
--muted: #6b7280;
--danger: #ef4444;
--border: #e6e9ee;
--radius: 12px;
--gap: 14px;
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
color-scheme: light;
}
html,
body {
height: 100%;
margin: 0;
background: linear-gradient(180deg, #f8fbff, var(--bg));
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 900px;
margin: 28px auto;
padding: 20px;
}
.card {
background: var(--card);
border-radius: var(--radius);
padding: 20px;
box-shadow: 0 8px 24px rgba(20, 30, 60, 0.06);
border: 1px solid var(--border);
}
h1 {
margin: 0 0 8px 0;
font-size: 20px;
color: #0f172a;
}
.meta {
color: var(--muted);
margin-bottom: 18px;
font-size: 14px;
}
.assignment {
font-size: 16px;
line-height: 1.5;
margin-bottom: 18px;
color: #0f172a;
}
.controls {
display: flex;
gap: var(--gap);
flex-wrap: wrap;
align-items: center;
margin-bottom: 18px;
}
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 10px;
border: 1px solid rgba(11, 110, 253, 0.12);
background: linear-gradient(180deg, var(--accent), #0654d6);
color: #fff;
text-decoration: none;
cursor: pointer;
font-weight: 600;
}
.btn.ghost {
background: #fff;
color: #0f172a;
border: 1px solid var(--border);
box-shadow: none;
}
.btn.warn {
background: var(--danger);
border-color: rgba(239, 68, 68, 0.9);
}
.helpbox {
background: #f8faff;
border: 1px solid rgba(11, 110, 253, 0.08);
padding: 12px;
border-radius: 8px;
color: #0b2540;
}
.solution {
background: #fbffef;
border: 1px solid rgba(34, 197, 94, 0.12);
padding: 12px;
border-radius: 8px;
color: #0b3a12;
}
form {
margin: 0;
}
label {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 6px;
}
input[type="text"] {
width: 100%;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid var(--border);
font-size: 14px;
}
input[type="submit"],
button[type="button"] {
padding: 9px 12px;
border-radius: 8px;
border: none;
cursor: pointer;
}
.row {
display: flex;
gap: 12px;
align-items: center;
margin-bottom: 8px;
flex-wrap: wrap;
}
.gps {
display: flex;
gap: 8px;
align-items: center;
}
input[readonly] {
background: #f3f4f6;
border: 1px solid var(--border);
}
.muted {
color: var(--muted);
font-size: 13px;
}
.error {
color: var(--danger);
font-weight: 600;
}
@media (max-width:640px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.row {
flex-direction: column;
align-items: stretch;
}
}
</style>
</head>
<body>
<h1>Zadání šifry {{.Order}}</h1>
<p>{{.Assignment}}</p>
<hr>
{{if eq .Help 0}}
<p>Požádat o malou nápovědu.</p>
<form method="post">
<input type="hidden" name="help" value="1">
<input type="submit" value="Zobrazit nápovědu">
</form>
{{else if eq .Help 1}}
<p>Nápověda: {{.HelpText}}</p>
<p>Vzdát se a ukázat pozici další šifry.</p>
<form method="post">
<input type="hidden" name="help" value="2">
<input type="submit" value="Vzdát se">
</form>
{{else}}
<p>Řešení: {{.Solution}}</p>
{{end}}
<hr>
{{if ne .Help 2}}
{{if .Wrong}}<p style="color:red;">Špatné řešení, zkus to znovu.</p>{{end}}
<form method="post">
Řešení: <input type="text" name="solution"><br>
<input type="submit" value="Odeslat">
</form>
{{else}}
<p>
Souřadnice další šifry:
<input id="gps" value="{{.Coordinates}}" readonly />
<br>
<button onclick="copyToClipboard()">Zkopírovat do schránky</button>
<br>
Nápověda k nalezení pozice: {{.PositionHint}}
</p>
<p>Nápověda k nalezení cíle: {{.FinalClue}}</p>
{{end}}
<div class="wrap">
<div class="card">
<h1>Zadání šifry {{.Order}}</h1>
<div class="meta">Aktuální úkol</div>
<div class="assignment">
{{.Assignment}}
</div>
<div class="controls">
{{if eq .Help 0}}
<div>
<div class="muted">Požádat o malou nápovědu</div>
<form method="post" style="margin-top:8px;">
<input type="hidden" name="help" value="1">
<input class="btn" type="submit" value="Zobrazit nápovědu">
</form>
</div>
{{else if eq .Help 1}}
<div style="flex:1">
<div class="helpbox">Nápověda: {{.HelpText}}</div>
<div style="margin-top:12px">
<div class="muted">Pokud se chcete vzdát a získat pozici další šifry:</div>
<form method="post" style="margin-top:8px;">
<input type="hidden" name="help" value="2">
<input class="btn warn" type="submit" value="Vzdát se">
</form>
</div>
</div>
{{else}}
<div style="flex:1">
<div class="solution">Řešení: {{.Solution}}</div>
</div>
{{end}}
</div>
<hr>
{{if ne .Help 2}}
{{if .Wrong}}<p class="error">Špatné řešení, zkus to znovu.</p>{{end}}
<form method="post" style="margin-top:12px;">
<label for="solution">Zadejte řešení</label>
<div class="row">
<input id="solution" name="solution" type="text" autocomplete="off"
placeholder="Zadejte odpověď...">
<input class="btn ghost" type="submit" value="Odeslat">
</div>
</form>
{{else}}
<div>
<p class="muted">Souřadnice další šifry</p>
<div class="row">
<div class="gps" style="flex:1">
<input id="gps" value="{{.Coordinates}}" readonly>
<button class="btn ghost" type="button" onclick="copyToClipboard()">Zkopírovat</button>
</div>
</div>
<p class="muted" style="margin-top:8px">Nápověda k nalezení pozice: <strong>{{.PositionHint}}</strong>
</p>
<p class="muted">Nápověda k nalezení cíle: <strong>{{.FinalClue}}</strong></p>
</div>
{{end}}
</div>
</div>
<script>
function copyToClipboard() {
const el = document.getElementById('gps');
if (!el) return;
const text = el.value || el.innerText;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => { fallbackCopy(el); });
} else {
fallbackCopy(el);
}
}
function fallbackCopy(el) {
el.select ? el.select() : null;
try {
document.execCommand('copy');
} catch (e) { /* ignore */ }
window.getSelection ? window.getSelection().removeAllRanges() : null;
}
</script>
</body>
<script>
function copyToClipboard() {
let copyText = document.querySelector("#gps");
copyText.select();
document.execCommand("copy");
}
</script>
</html>

View File

@@ -3,17 +3,123 @@
<head>
<meta charset="UTF-8">
<title>Login</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Přihlášení</title>
<style>
:root {
--bg: #f5f7fb;
--card: #ffffff;
--accent: #0b6efd;
--muted: #6b7280;
--radius: 12px;
--border: #e6e9ee;
--error-bg: #fff1f2;
--error-border: #fecaca;
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
}
html,
body {
height: 100%;
margin: 0;
background: linear-gradient(180deg, #f8fbff, var(--bg));
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 420px;
margin: 72px auto;
padding: 16px;
}
.card {
background: var(--card);
padding: 20px;
border-radius: var(--radius);
box-shadow: 0 10px 30px rgba(20, 30, 60, 0.06);
border: 1px solid var(--border);
}
h1 {
margin: 0 0 6px 0;
font-size: 20px;
color: #0f172a;
}
.sub {
color: var(--muted);
margin-bottom: 12px;
}
.error {
background: var(--error-bg);
border: 1px solid var(--error-border);
color: #9f1239;
padding: 10px 12px;
border-radius: 10px;
margin-bottom: 12px;
font-weight: 600;
}
form {
display: flex;
flex-direction: column;
gap: 12px;
}
label {
font-size: 13px;
color: var(--muted);
}
input[type="password"] {
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border);
font-size: 15px;
}
.btn {
display: inline-block;
padding: 10px 12px;
border-radius: 10px;
background: var(--accent);
color: #fff;
text-decoration: none;
border: none;
cursor: pointer;
font-weight: 700;
}
@media (max-width:420px) {
.wrap {
margin: 32px 12px;
}
}
</style>
</head>
<body>
<h1>Login</h1>
<form action="/login" method="post">
<label for="password">Heslo:</label>
<input type="password" id="password" name="password" required>
<br>
<button type="submit">Přihlásit se</button>
</form>
<div class="wrap">
<div class="card">
<h1>Přihlášení do týmu</h1>
<div class="sub">Zadejte své týmové heslo</div>
{{if .}}
<div class="error">Chybné heslo — zkuste to prosím znovu.</div>
{{end}}
<form action="/login" method="post">
<div>
<label for="password">Heslo</label>
<input id="password" name="password" type="password" required autocomplete="current-password">
</div>
<div>
<button class="btn" type="submit">Přihlásit se</button>
</div>
</form>
</div>
</div>
</body>
</html>

View File

@@ -3,18 +3,112 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Tým: {{.TeamName}}</title>
<style>
:root {
--bg: #f5f7fb;
--card: #ffffff;
--accent: #0b6efd;
--muted: #6b7280;
--radius: 12px;
--border: #e6e9ee;
font-family: Inter, system-ui, -apple-system, "Segoe UI", Roboto, Arial;
}
html,
body {
height: 100%;
margin: 0;
background: linear-gradient(180deg, #f8fbff, var(--bg));
-webkit-font-smoothing: antialiased;
}
.wrap {
max-width: 720px;
margin: 36px auto;
padding: 16px;
}
.card {
background: var(--card);
padding: 18px;
border-radius: var(--radius);
box-shadow: 0 8px 20px rgba(20, 30, 60, 0.06);
border: 1px solid var(--border);
}
h1 {
margin: 0 0 6px 0;
font-size: 20px;
color: #0f172a;
}
.meta {
color: var(--muted);
margin-bottom: 14px;
font-size: 14px;
}
.info {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
margin-bottom: 16px;
}
.details {
font-size: 15px;
color: #0f172a;
}
.label {
color: var(--muted);
font-size: 13px;
}
.logout {
background: transparent;
border: 1px solid var(--border);
padding: 8px 12px;
border-radius: 10px;
cursor: pointer;
color: #0f172a;
font-weight: 600;
}
.logout:hover {
box-shadow: 0 4px 12px rgba(11, 110, 253, 0.06);
}
@media (max-width:560px) {
.info {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<h1>Tým: {{.TeamName}}</h1>
Obtížnost: {{.Difficulty}}<br>
Poslední šifra : {{.LastCipher}}<br>
Penalizace: {{.Penalties}}<br>
<hr>
<form action="/logout" method="get">
<input type="submit" value="Odhlásit">
</form>
<div class="wrap">
<div class="card">
<h1>Tým: {{.TeamName}}</h1>
<div class="meta">Přehled týmu</div>
<div class="info">
<div class="details">
<div><span class="label">Obtížnost:</span> {{.Difficulty}}</div>
<div><span class="label">Právě řeší šifru:</span> {{.LastCipher}}</div>
<div><span class="label">Penalizace (min):</span> {{.Penalties}}</div>
</div>
<form action="/logout" method="get">
<button class="logout" type="submit">Odhlásit</button>
</form>
</div>
</div>
</div>
</body>
</html>