moved a lot

This commit is contained in:
vfs committed 2026-05-09 14:40:14 +02:00
1 parent 5772a7d855
commit 136a1c34b3
98 files changed
+6950 -2183

No files matched your search

+27
View File
@@ -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}}
+62
View File
@@ -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">&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}}
+31
View File
@@ -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">&#128499;</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">&#128100;</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">&#128202;</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}}
+54
View File
@@ -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 &amp; anmelden
</button>
</form>
{{end}}
</div>
</div>
{{end}}
+208
View File
@@ -0,0 +1,208 @@
{{template "base" .}}
{{define "title"}}{{.Issue.Title}}{{end}}
{{define "body"}}
<div style="margin-bottom:16px;">
<a href="/issues" class="link">&larr; 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}}
+101
View File
@@ -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}}
+44
View File
@@ -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}}
+33
View File
@@ -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}}
+27
View File
@@ -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> &nbsp; {{.User.Email}}</div>
<div><span style="color:var(--text-muted);">Telefon</span> &nbsp; {{.User.PhoneNumber}}</div>
<div><span style="color:var(--text-muted);">Land</span> &nbsp; {{.User.Country}}</div>
<div><span style="color:var(--text-muted);">Adresse</span> &nbsp; {{.User.Address}}</div>
<div><span style="color:var(--text-muted);">Geburtsdatum</span> &nbsp; {{.User.DateOfBirth.Format "02.01.2006"}}</div>
<div><span style="color:var(--text-muted);">Mitglied seit</span> &nbsp; {{.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}}
+82
View File
@@ -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}}
+34
View File
@@ -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}} &middot; 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}}