moved a lot
This commit is contained in:
1 parent
5772a7d855
commit
136a1c34b3
98 files changed
+6950
-2183
No files matched your search
@@ -0,0 +1,27 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Konto aktivieren{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="auth-section">
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-title">Konto aktivieren</h2>
|
||||
<p class="auth-subtitle">Geben Sie Ihren Aktivierungstoken ein, um Ihr Konto freizuschalten.</p>
|
||||
|
||||
{{if .FormErrors}}
|
||||
<div class="alert alert--error">
|
||||
{{range .FormErrors}}<p>{{.}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/users/activated">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="token">Aktivierungstoken</label>
|
||||
<input class="form-input" id="token" name="token" type="text"
|
||||
placeholder="Token aus der E-Mail einfügen" value="{{.Token}}">
|
||||
</div>
|
||||
<button class="btn btn--primary btn--full" type="submit">Aktivieren</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,62 @@
|
||||
{{define "base"}}
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{template "title" .}} — DPÖ</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=Montserrat:wght@600;700&family=Source+Sans+3:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/style.css"/>
|
||||
</head>
|
||||
<body>
|
||||
{{if .AuthenticatedUser}}
|
||||
<nav style="border-bottom:1px solid var(--border); background:var(--bg-card);">
|
||||
<div class="container" style="display:flex; align-items:center; gap:20px; padding:10px 0;">
|
||||
<a href="/" class="link">Start</a>
|
||||
<a href="/issues" class="link">Abstimmungen</a>
|
||||
<a href="/mps" class="link">Nationalrat</a>
|
||||
<a href="/users/me" class="link">Mein Profil</a>
|
||||
{{if hasPermission "users:read"}}<a href="/users" class="link">Users</a>{{end}}
|
||||
<form method="POST" action="/logout" style="margin-left:auto;">
|
||||
<button type="submit" class="btn">Abmelden</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
{{end}}
|
||||
|
||||
<div style="text-align:center; padding: 40px 0 24px;">
|
||||
<img src="/static/logo.svg" alt="DPÖ Logo" style="height: 600px; width: auto;">
|
||||
</div>
|
||||
|
||||
<main class="site-main">
|
||||
<div class="container">
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="container">
|
||||
<div class="footer-inner">
|
||||
<span class="footer-brand">DPÖ — Digitale Partei Österreich</span>
|
||||
<span class="footer-copy">© 2025</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
const ws = new WebSocket("wss://localhost:8443/ws");
|
||||
ws.onopen = () => console.log("WS connected");
|
||||
ws.onmessage = (event) => console.log(event.data);
|
||||
ws.onclose = () => console.log("WS closed");
|
||||
setInterval(() => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: "ping", timestamp: Date(), random: Math.random() }));
|
||||
}
|
||||
}, 1000);
|
||||
</script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,31 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Übersicht{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Willkommen, {{.Name}}</h1>
|
||||
<p class="page-subtitle">Ihre demokratische Plattform für Österreich</p>
|
||||
</div>
|
||||
|
||||
<div class="card-grid">
|
||||
<div class="card">
|
||||
<div class="card-icon">🗳</div>
|
||||
<h2 class="card-title">Aktuelle Abstimmungen</h2>
|
||||
<p class="card-text">Beteiligen Sie sich an laufenden Abstimmungen und demokratischen Initiativen.</p>
|
||||
<a href="/issues" class="btn btn--primary">Abstimmungen ansehen</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">👤</div>
|
||||
<h2 class="card-title">Mein Profil</h2>
|
||||
<p class="card-text">Verwalten Sie Ihre persönlichen Daten und Kontoeinstellungen.</p>
|
||||
<a href="/users/me" class="btn btn--secondary">Profil ansehen</a>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-icon">📊</div>
|
||||
<h2 class="card-title">Ergebnisse</h2>
|
||||
<p class="card-text">Sehen Sie die Ergebnisse abgeschlossener Abstimmungen ein.</p>
|
||||
<a href="#" class="btn btn--secondary">Ergebnisse ansehen</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,54 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Willkommen{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="hero">
|
||||
<div class="hero-content">
|
||||
<div class="hero-label">Digitale Demokratie für Österreich</div>
|
||||
<h1 class="hero-title">Gestalten Sie die<br><span class="accent">Zukunft Österreichs</span></h1>
|
||||
<p class="hero-text">
|
||||
Beteiligen Sie sich an der demokratischen Entscheidungsfindung —
|
||||
sicher, transparent und vollständig digital.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="auth-section">
|
||||
<div class="auth-card">
|
||||
<h2 class="auth-title">Anmelden</h2>
|
||||
<p class="auth-subtitle">Melden Sie sich an, um an Abstimmungen teilzunehmen.</p>
|
||||
|
||||
{{if .FormErrors}}
|
||||
<div class="alert alert--error">
|
||||
{{range .FormErrors}}<p>{{.}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/login">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">E-Mail-Adresse</label>
|
||||
<input class="form-input" type="email" id="email" name="email"
|
||||
placeholder="ihre@email.at" required autocomplete="email">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Passwort</label>
|
||||
<input class="form-input" type="password" id="password" name="password"
|
||||
placeholder="••••••••" required autocomplete="current-password">
|
||||
</div>
|
||||
<button class="btn btn--primary btn--full" type="submit">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-divider">Noch kein Konto? <a href="/register" class="link">Registrieren</a></p>
|
||||
|
||||
{{if .IsDevelopment}}
|
||||
<form method="POST" action="/dev-login" style="margin-top:12px;">
|
||||
<button class="btn btn--full" type="submit"
|
||||
style="background:none; border:1px solid orange; color:orange;">
|
||||
Dev: Neues Testkonto erstellen & anmelden
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,208 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}{{.Issue.Title}}{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div style="margin-bottom:16px;">
|
||||
<a href="/issues" class="link">← Zurück</a>
|
||||
</div>
|
||||
|
||||
<div class="card" style="margin-bottom:24px;">
|
||||
<div class="card-title" style="font-size:22px;">{{.Issue.Title}}</div>
|
||||
<div class="card-text" style="margin-top:8px;">{{.Issue.Description}}</div>
|
||||
<div style="font-size:13px; color:var(--text-muted); margin-top:8px;">
|
||||
{{.Issue.StartTime.Format "02.01.2006"}} – {{.Issue.EndTime.Format "02.01.2006"}}
|
||||
</div>
|
||||
{{if .CanWriteIssues}}
|
||||
<div style="display:flex; gap:8px; margin-top:16px; flex-wrap:wrap;">
|
||||
<button class="btn btn--danger"
|
||||
hx-delete="/issues/{{.Issue.ID}}"
|
||||
hx-confirm="Abstimmung wirklich löschen?">Löschen</button>
|
||||
</div>
|
||||
<details style="margin-top:16px; border-top:1px solid var(--border); padding-top:16px;">
|
||||
<summary style="cursor:pointer; font-weight:600; list-style:none;">Bearbeiten</summary>
|
||||
<form hx-patch="/issues/{{.Issue.ID}}" style="margin-top:12px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Titel</label>
|
||||
<input class="form-input" name="title" type="text" value="{{.Issue.Title}}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Beschreibung</label>
|
||||
<textarea class="form-input" name="description" rows="3">{{.Issue.Description}}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Beginn</label>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<input class="form-input" name="start_date" type="date"
|
||||
value="{{.Issue.StartTime.Format "2006-01-02"}}" style="flex:1; min-width:0;">
|
||||
{{template "time-input" (dict "name" "start_clock" "value" (.Issue.StartTime.Format "15:04"))}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ende</label>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<input class="form-input" name="end_date" type="date"
|
||||
value="{{.Issue.EndTime.Format "2006-01-02"}}" style="flex:1; min-width:0;">
|
||||
{{template "time-input" (dict "name" "end_clock" "value" (.Issue.EndTime.Format "15:04"))}}
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn--primary" type="submit">Speichern</button>
|
||||
</form>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<h2 style="margin-bottom:16px;">Optionen</h2>
|
||||
|
||||
{{if .Issue.CanVote}}
|
||||
<div class="card" style="margin-bottom:24px;">
|
||||
<p style="margin-bottom:12px; font-weight:600;">Ihre Stimme abgeben</p>
|
||||
{{range .Issue.Options}}
|
||||
<label style="display:flex; align-items:center; gap:8px; margin-bottom:8px; cursor:pointer;">
|
||||
<input type="radio" name="vote-option" value="{{.ID}}">
|
||||
<span>{{.Label}}</span>
|
||||
</label>
|
||||
{{end}}
|
||||
<div id="vote-status" style="font-size:13px; margin-top:8px;"></div>
|
||||
<button class="btn btn--primary" style="margin-top:12px;" onclick="castVote()">Abstimmen</button>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card-grid">
|
||||
{{range .Issue.Options}}
|
||||
<div class="card">
|
||||
<div class="card-title">{{.Label}}</div>
|
||||
<div style="font-size:28px; font-weight:700; margin-top:8px;">{{.VoteCount}}</div>
|
||||
<div style="font-size:13px; color:var(--text-muted);">Stimmen</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const issueID = {{.Issue.ID}};
|
||||
|
||||
// ── Blind-signature voting ──────────────────────────────────────────────────
|
||||
|
||||
function modPow(base, exp, mod) {
|
||||
if (mod === 1n) return 0n;
|
||||
let result = 1n;
|
||||
base = base % mod;
|
||||
while (exp > 0n) {
|
||||
if (exp % 2n === 1n) result = (result * base) % mod;
|
||||
exp >>= 1n;
|
||||
base = (base * base) % mod;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function modInverse(a, m) {
|
||||
let [old_r, r] = [a, m];
|
||||
let [old_s, s] = [1n, 0n];
|
||||
while (r !== 0n) {
|
||||
const q = old_r / r;
|
||||
[old_r, r] = [r, old_r - q * r];
|
||||
[old_s, s] = [s, old_s - q * s];
|
||||
}
|
||||
return ((old_s % m) + m) % m;
|
||||
}
|
||||
|
||||
function bigIntToBase64(n) {
|
||||
let hex = n.toString(16);
|
||||
if (hex.length % 2) hex = "0" + hex;
|
||||
const bytes = new Uint8Array(hex.length / 2);
|
||||
for (let i = 0; i < bytes.length; i++)
|
||||
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
function base64ToBigInt(b64) {
|
||||
const bin = atob(b64);
|
||||
let hex = "";
|
||||
for (let i = 0; i < bin.length; i++)
|
||||
hex += bin.charCodeAt(i).toString(16).padStart(2, "0");
|
||||
return BigInt("0x" + hex);
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
async function castVote() {
|
||||
const selected = document.querySelector('input[name="vote-option"]:checked');
|
||||
if (!selected) { alert("Bitte eine Option auswählen."); return; }
|
||||
const optionID = parseInt(selected.value);
|
||||
const status = document.getElementById("vote-status");
|
||||
status.textContent = "Wird verarbeitet…";
|
||||
|
||||
try {
|
||||
// 1. Get public key
|
||||
const pkRes = await fetch(`/issues/${issueID}/pubkey`);
|
||||
if (!pkRes.ok) throw new Error("Öffentlicher Schlüssel nicht abrufbar.");
|
||||
const { public_key } = await pkRes.json();
|
||||
const N = BigInt("0x" + public_key.n);
|
||||
const e = BigInt(public_key.e);
|
||||
|
||||
// 2. Generate nonce and compute vote message: SHA-256(issueID||optionID||nonce)
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(32));
|
||||
const msgBuf = new Uint8Array(16 + nonce.length);
|
||||
const view = new DataView(msgBuf.buffer);
|
||||
view.setBigUint64(0, BigInt(issueID), false);
|
||||
view.setBigUint64(8, BigInt(optionID), false);
|
||||
msgBuf.set(nonce, 16);
|
||||
const hashBuf = await crypto.subtle.digest("SHA-256", msgBuf);
|
||||
const hashHex = Array.from(new Uint8Array(hashBuf))
|
||||
.map(b => b.toString(16).padStart(2, "0")).join("");
|
||||
const m = BigInt("0x" + hashHex);
|
||||
|
||||
// 3. Pick random blinding factor r in [2, N-1]
|
||||
let r;
|
||||
do {
|
||||
const rb = crypto.getRandomValues(new Uint8Array(256));
|
||||
r = BigInt("0x" + Array.from(rb).map(b => b.toString(16).padStart(2, "0")).join(""));
|
||||
} while (r < 2n || r >= N);
|
||||
|
||||
// 4. Blind: m' = m * r^e mod N
|
||||
const blinded = (m * modPow(r, e, N)) % N;
|
||||
|
||||
// 5. Request blind signature
|
||||
const bsRes = await fetch(`/issues/${issueID}/blind-sign`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ blinded_vote: Array.from(atob(bigIntToBase64(blinded))).map(c => c.charCodeAt(0)) }),
|
||||
});
|
||||
if (!bsRes.ok) {
|
||||
const err = await bsRes.json();
|
||||
throw new Error(err.error?.message ?? "Blind-Sign fehlgeschlagen.");
|
||||
}
|
||||
const { signed } = await bsRes.json();
|
||||
const sPrime = base64ToBigInt(signed);
|
||||
|
||||
// 6. Unblind: s = s' * r^(-1) mod N
|
||||
const signature = (sPrime * modInverse(r, N)) % N;
|
||||
|
||||
// 7. Cast vote
|
||||
const voteRes = await fetch("/votes", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
issue_id: issueID,
|
||||
option_id: optionID,
|
||||
nonce: bytesToBase64(nonce),
|
||||
signature: bigIntToBase64(signature),
|
||||
}),
|
||||
});
|
||||
if (!voteRes.ok) {
|
||||
const err = await voteRes.json();
|
||||
throw new Error(err.error?.message ?? "Abstimmung fehlgeschlagen.");
|
||||
}
|
||||
|
||||
status.style.color = "green";
|
||||
status.textContent = "Stimme erfolgreich abgegeben!";
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} catch (err) {
|
||||
status.style.color = "red";
|
||||
status.textContent = err.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,101 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Abstimmungen{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Abstimmungen</h1>
|
||||
<p class="page-subtitle">Aktuelle Abstimmungen der Digitalen Partei Österreich</p>
|
||||
</div>
|
||||
|
||||
{{if .Issues}}
|
||||
<div class="card-grid">
|
||||
{{range .Issues}}
|
||||
<div class="card">
|
||||
<div class="card-title">{{.Title}}</div>
|
||||
<div class="card-text">{{.Description}}</div>
|
||||
<div style="font-size:13px; color:var(--text-muted); margin-top:4px;">
|
||||
{{.StartTime.Format "02.01.2006"}} – {{.EndTime.Format "02.01.2006"}}
|
||||
</div>
|
||||
<div style="display:flex; gap:8px; margin-top:12px; flex-wrap:wrap;">
|
||||
<a href="/issues/{{.ID}}" class="btn btn--primary">Details</a>
|
||||
{{if $.CanWriteIssues}}
|
||||
<button class="btn btn--danger"
|
||||
hx-delete="/issues/{{.ID}}"
|
||||
hx-confirm="Abstimmung wirklich löschen?">Löschen</button>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p style="color: var(--text-muted);">Keine Abstimmungen vorhanden.</p>
|
||||
{{end}}
|
||||
|
||||
{{if .CanWriteIssues}}
|
||||
<details style="margin-top:32px;">
|
||||
<summary class="btn btn--primary" style="display:inline-block; cursor:pointer; list-style:none;">+ Neue Abstimmung</summary>
|
||||
<div class="auth-card" style="max-width:520px; margin-top:16px;">
|
||||
<h2 class="auth-title">Neue Abstimmung</h2>
|
||||
<form method="POST" action="/issues">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Titel</label>
|
||||
<input class="form-input" name="title" type="text" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Beschreibung</label>
|
||||
<textarea class="form-input" name="description" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Beginn</label>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<input class="form-input" name="start_date" type="date" required style="flex:1; min-width:0;">
|
||||
{{template "time-input" (dict "name" "start_clock" "value" "12:00" "required" true)}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Ende</label>
|
||||
<div style="display:flex; gap:8px;">
|
||||
<input class="form-input" name="end_date" type="date" required style="flex:1; min-width:0;">
|
||||
{{template "time-input" (dict "name" "end_clock" "value" "12:00" "required" true)}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Optionen (mind. 2)</label>
|
||||
<div id="options-list" style="display:flex; flex-direction:column; gap:8px;">
|
||||
<input class="form-input" name="options" type="text" placeholder="Option 1" required>
|
||||
<input class="form-input" name="options" type="text" placeholder="Option 2" required>
|
||||
</div>
|
||||
<button type="button" class="btn btn--secondary" style="margin-top:8px;" onclick="addOption()">+ Option hinzufügen</button>
|
||||
</div>
|
||||
<button class="btn btn--primary" type="submit">Erstellen</button>
|
||||
</form>
|
||||
<script>
|
||||
(function() {
|
||||
function pad(n) { return String(n).padStart(2, '0'); }
|
||||
function fmtDate(d) {
|
||||
return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate());
|
||||
}
|
||||
const start = new Date();
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 7);
|
||||
document.querySelector('input[name="start_date"]').value = fmtDate(start);
|
||||
document.querySelector('input[name="end_date"]').value = fmtDate(end);
|
||||
})();
|
||||
|
||||
function addOption() {
|
||||
const list = document.getElementById('options-list');
|
||||
const n = list.children.length + 1;
|
||||
const input = document.createElement('input');
|
||||
input.className = 'form-input';
|
||||
input.name = 'options';
|
||||
input.type = 'text';
|
||||
input.placeholder = 'Option ' + n;
|
||||
list.appendChild(input);
|
||||
input.focus();
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,44 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Nationalrat{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Nationalrat</h1>
|
||||
<p class="page-subtitle">{{.Total}} Abgeordnete — XXVIII. Gesetzgebungsperiode</p>
|
||||
</div>
|
||||
|
||||
{{range .Groups}}
|
||||
<section style="margin-bottom: 40px;">
|
||||
<h2 style="font-size: 18px; color: var(--dark); margin-bottom: 12px; display: flex; align-items: baseline; gap: 10px;">
|
||||
{{.Name}}
|
||||
<span style="font-size: 13px; font-weight: 500; color: var(--text-muted); font-family: 'Source Sans 3', sans-serif;">{{len .Members}} Mandate</span>
|
||||
</h2>
|
||||
<div style="background: var(--bg-card); border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow);">
|
||||
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
||||
<thead>
|
||||
<tr style="background: var(--bg); border-bottom: 1px solid var(--border);">
|
||||
<th style="text-align: left; padding: 10px 16px; font-weight: 600; color: var(--text-muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;">Name</th>
|
||||
<th style="text-align: left; padding: 10px 16px; font-weight: 600; color: var(--text-muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;">Fraktion</th>
|
||||
<th style="text-align: left; padding: 10px 16px; font-weight: 600; color: var(--text-muted); font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;">Wahlkreis</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Members}}
|
||||
<tr style="border-bottom: 1px solid var(--border);">
|
||||
<td style="padding: 10px 16px;">
|
||||
<a href="https://www.parlament.gv.at{{.Path}}" target="_blank" rel="noopener"
|
||||
style="color: var(--text); font-weight: 500; text-decoration: none;">
|
||||
{{.FirstName}} {{.LastName}}
|
||||
</a>
|
||||
</td>
|
||||
<td style="padding: 10px 16px; color: var(--text-muted); font-weight: 500;">{{.Faction}}</td>
|
||||
<td style="padding: 10px 16px; color: var(--text-muted);">{{.Constituency}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -0,0 +1,33 @@
|
||||
{{define "time-input"}}
|
||||
<span style="display:inline-flex; align-items:center; gap:4px;">
|
||||
<select class="form-input" style="width:68px; padding-left:8px; padding-right:4px; text-align:center;"></select>
|
||||
<span style="font-weight:600; color:var(--text-muted);">:</span>
|
||||
<select class="form-input" style="width:68px; padding-left:8px; padding-right:4px; text-align:center;"></select>
|
||||
<input type="hidden" name="{{.name}}" value="{{.value}}">
|
||||
</span>
|
||||
<script>
|
||||
(function() {
|
||||
const wrap = document.currentScript.previousElementSibling;
|
||||
const [hSel, mSel] = wrap.querySelectorAll('select');
|
||||
const hidden = wrap.querySelector('input[type="hidden"]');
|
||||
const parts = (hidden.value || '12:00').split(':');
|
||||
const initH = parseInt(parts[0], 10) || 0;
|
||||
const initM = parseInt(parts[1], 10) || 0;
|
||||
for (let h = 0; h < 24; h++) {
|
||||
const o = new Option(String(h).padStart(2, '0'), h);
|
||||
if (h === initH) o.selected = true;
|
||||
hSel.appendChild(o);
|
||||
}
|
||||
for (let m = 0; m < 60; m++) {
|
||||
const o = new Option(String(m).padStart(2, '0'), m);
|
||||
if (m === initM) o.selected = true;
|
||||
mSel.appendChild(o);
|
||||
}
|
||||
function sync() {
|
||||
hidden.value = String(+hSel.value).padStart(2, '0') + ':' + String(+mSel.value).padStart(2, '0');
|
||||
}
|
||||
hSel.addEventListener('change', sync);
|
||||
mSel.addEventListener('change', sync);
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -0,0 +1,27 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Mein Profil{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Mein Profil</h1>
|
||||
</div>
|
||||
|
||||
<div class="card" style="max-width:520px;">
|
||||
<div class="card-title">{{.User.Name}}{{if .User.AltName}} / {{.User.AltName}}{{end}}</div>
|
||||
<div style="margin-top:12px; display:flex; flex-direction:column; gap:8px; font-size:15px;">
|
||||
<div><span style="color:var(--text-muted);">E-Mail</span> {{.User.Email}}</div>
|
||||
<div><span style="color:var(--text-muted);">Telefon</span> {{.User.PhoneNumber}}</div>
|
||||
<div><span style="color:var(--text-muted);">Land</span> {{.User.Country}}</div>
|
||||
<div><span style="color:var(--text-muted);">Adresse</span> {{.User.Address}}</div>
|
||||
<div><span style="color:var(--text-muted);">Geburtsdatum</span> {{.User.DateOfBirth.Format "02.01.2006"}}</div>
|
||||
<div><span style="color:var(--text-muted);">Mitglied seit</span> {{.User.Created.Format "02.01.2006"}}</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:20px; border-top:1px solid var(--border); padding-top:16px;">
|
||||
<button class="btn btn--danger"
|
||||
hx-delete="/users/{{.User.ID}}"
|
||||
hx-confirm="Konto wirklich unwiderruflich löschen?">Konto löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,82 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Registrieren{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="page-header" style="text-align:center; border-bottom: none; margin-bottom: 32px;">
|
||||
<h1 class="page-title">Konto erstellen</h1>
|
||||
<p class="page-subtitle">Werden Sie Mitglied der Digitalen Partei Österreich</p>
|
||||
</div>
|
||||
|
||||
<div class="auth-section">
|
||||
<div class="auth-card" style="max-width: 520px;">
|
||||
|
||||
{{if .FormErrors}}
|
||||
<div class="alert alert--error">
|
||||
{{range .FormErrors}}<p>{{.}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<form method="POST" action="/register">
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap: 0 16px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">Vollständiger Name</label>
|
||||
<input class="form-input" type="text" id="name" name="name"
|
||||
placeholder="Maria Muster" required autocomplete="name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="alt_name">Alternativer Name <span style="font-weight:400; color:var(--text-muted)">(optional)</span></label>
|
||||
<input class="form-input" type="text" id="alt_name" name="alt_name"
|
||||
placeholder="Spitzname">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">E-Mail-Adresse</label>
|
||||
<input class="form-input" type="email" id="email" name="email"
|
||||
placeholder="ihre@email.at" required autocomplete="email">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="username">Benutzername</label>
|
||||
<input class="form-input" type="text" id="username" name="username"
|
||||
placeholder="maria.muster" required autocomplete="username">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">Passwort</label>
|
||||
<input class="form-input" type="password" id="password" name="password"
|
||||
placeholder="••••••••" required autocomplete="new-password">
|
||||
</div>
|
||||
|
||||
<div style="display:grid; grid-template-columns:1fr 1fr; gap: 0 16px;">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="phone_number">Telefonnummer</label>
|
||||
<input class="form-input" type="tel" id="phone_number" name="phone_number"
|
||||
placeholder="+43 660 123 4567" autocomplete="tel">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="date_of_birth">Geburtsdatum</label>
|
||||
<input class="form-input" type="date" id="date_of_birth" name="date_of_birth" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="address">Adresse</label>
|
||||
<input class="form-input" type="text" id="address" name="address"
|
||||
placeholder="Musterstraße 1, 1010 Wien" autocomplete="street-address">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="country">Land</label>
|
||||
<input class="form-input" type="text" id="country" name="country"
|
||||
placeholder="AT" value="AT" maxlength="2" autocomplete="country">
|
||||
</div>
|
||||
|
||||
<button class="btn btn--primary btn--full" type="submit">Registrieren</button>
|
||||
</form>
|
||||
|
||||
<p class="auth-divider">Bereits ein Konto? <a href="/" class="link">Anmelden</a></p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,34 @@
|
||||
{{template "base" .}}
|
||||
|
||||
{{define "title"}}Mitglieder{{end}}
|
||||
|
||||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Mitglieder</h1>
|
||||
<p class="page-subtitle">Registrierte Mitglieder der Digitalen Partei Österreich</p>
|
||||
</div>
|
||||
|
||||
{{if .Users}}
|
||||
<div class="card-grid">
|
||||
{{range .Users}}
|
||||
<div class="card">
|
||||
<div class="card-title">{{.Name}}{{if .AltName}} / {{.AltName}}{{end}}</div>
|
||||
<div class="card-text">{{.Email}}</div>
|
||||
<div style="font-size:13px; color:var(--text-muted); margin-top:4px;">
|
||||
{{.Country}} · Beigetreten {{.Created.Format "02.01.2006"}}
|
||||
</div>
|
||||
{{if not .Activated}}
|
||||
<div style="font-size:12px; color:var(--text-muted); margin-top:4px;">Nicht aktiviert</div>
|
||||
{{end}}
|
||||
{{if $.CanManageUsers}}
|
||||
<button class="btn btn--danger" style="margin-top:12px;"
|
||||
hx-delete="/users/{{.ID}}"
|
||||
hx-confirm="Mitglied wirklich löschen?">Löschen</button>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p style="color: var(--text-muted);">Keine Mitglieder vorhanden.</p>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in new issue
Block a user