eis-website/app.py

342 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from flask import Flask, request, redirect
from datetime import datetime, timezone, timedelta
app = Flask(__name__)
# ─── Zeitzone: CEST (UTC+2) ──────────────────────────────────────────────────
BERLIN = timezone(timedelta(hours=2))
def jetzt():
return datetime.now(BERLIN).strftime("%d.%m.%Y, %H:%M Uhr")
# ─── Kommentare im Arbeitsspeicher ───────────────────────────────────────────
# Schema: {"name": str, "text": str, "zeit": str,
# "antworten": [{"name": str, "text": str, "zeit": str}]}
eintraege = []
# ─────────────────────────────────────────────────────────────────────────────
# Hilfsfunktion: gemeinsames HTML-Gerüst
# ─────────────────────────────────────────────────────────────────────────────
def seite(inhalt):
return """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flask-App David & Karo</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: Arial, sans-serif;
background: #f9f9f9;
color: #333;
min-height: 100vh;
}
.topbar {
background: #fff;
border-bottom: 1px solid #e0e0e0;
padding: 0 20px;
height: 56px;
display: flex;
align-items: center;
gap: 16px;
position: sticky;
top: 0;
z-index: 100;
}
.topbar-logo {
font-size: 20px;
font-weight: bold;
color: #cc181e;
letter-spacing: -1px;
text-decoration: none;
}
.topbar nav { display: flex; gap: 12px; margin-left: 16px; }
.topbar nav a {
font-size: 13px;
color: #555;
text-decoration: none;
padding: 4px 10px;
border-radius: 2px;
border: 1px solid transparent;
transition: border-color 0.15s;
}
.topbar nav a:hover { border-color: #ccc; background: #f5f5f5; }
.topbar nav a.aktiv { color: #cc181e; border-color: #cc181e; }
.container {
max-width: 900px;
margin: 30px auto;
padding: 0 20px;
}
</style>
</head>
<body>
<div class="topbar">
<a href="/app/" class="topbar-logo">&#9654; Flask-App</a>
<nav>
<a href="/app/pinnwand" class="aktiv">Kommentare</a>
</nav>
</div>
<div class="container">""" + inhalt + """
</div>
</body>
</html>"""
# ─────────────────────────────────────────────────────────────────────────────
# Route 1: Startseite
# ─────────────────────────────────────────────────────────────────────────────
@app.route("/")
def hello():
return redirect("/app/pinnwand")
# ─────────────────────────────────────────────────────────────────────────────
# Route 2: Frage-Formular (GET + POST)
# ─────────────────────────────────────────────────────────────────────────────
@app.route("/frage", methods=["GET", "POST"])
def frage():
return redirect("/app/pinnwand")
# ─────────────────────────────────────────────────────────────────────────────
# Route 3: Pinnwand YouTube-Kommentar-Style mit Antwort-Funktion
# ─────────────────────────────────────────────────────────────────────────────
PINNWAND_CSS = """
<style>
.pw-header { font-size:14px; color:#333; margin-bottom:18px;
border-bottom:1px solid #e0e0e0; padding-bottom:10px; }
.pw-header strong { font-size:16px; }
/* ── Eingabe-Bereich ── */
.pw-input-row { display:flex; gap:12px; align-items:flex-start; margin-bottom:22px; }
.pw-avatar { width:40px; height:40px; border-radius:50%; color:#fff;
display:flex; align-items:center; justify-content:center;
font-size:18px; font-weight:bold; flex-shrink:0; }
.pw-input-wrap { flex:1; }
.pw-input-wrap input[type=text] {
width:100%; border:none; border-bottom:1px solid #bbb;
background:transparent; padding:6px 2px; font-size:14px; color:#333;
outline:none; transition:border-color .2s; margin-bottom:8px; }
.pw-input-wrap input[type=text]:focus { border-bottom-color:#1155cc; }
.pw-input-wrap input[type=text]::placeholder { color:#aaa; }
.pw-btn-row { display:flex; justify-content:flex-end; }
.pw-btn { padding:6px 14px; border:none; border-radius:2px; font-size:13px;
cursor:pointer; font-weight:bold; background:#167ac6; color:#fff; }
.pw-btn:hover { background:#1266a8; }
/* ── Kommentar-Liste ── */
.pw-count { font-size:14px; color:#555; margin-bottom:12px; }
.pw-comment { display:flex; gap:12px; padding:12px 0; border-bottom:1px solid #f0f0f0; }
.pw-comment:last-child { border-bottom:none; }
.pw-comment-body { flex:1; min-width:0; }
.pw-meta { display:flex; align-items:baseline; gap:8px; margin-bottom:4px; flex-wrap:wrap; }
.pw-username { font-size:13px; font-weight:bold; color:#167ac6; }
.pw-time { font-size:12px; color:#999; }
.pw-text { font-size:14px; color:#333; line-height:1.5; word-break:break-word; }
.pw-actions { display:flex; gap:10px; margin-top:6px; align-items:center; }
.pw-action-btn { font-size:12px; color:#888; background:none; border:none;
cursor:pointer; padding:0; }
.pw-action-btn:hover { color:#333; }
.pw-thumb { font-size:13px; }
/* ── Antwort-Formular (toggle per JS) ── */
.pw-reply-form { display:none; margin-top:10px; padding:10px 0 4px 0;
border-top:1px solid #f0f0f0; }
.pw-reply-form input[type=text] {
width:100%; border:none; border-bottom:1px solid #ccc; background:transparent;
padding:5px 2px; font-size:13px; color:#333; outline:none; margin-bottom:6px;
transition:border-color .2s; }
.pw-reply-form input[type=text]:focus { border-bottom-color:#1155cc; }
.pw-reply-form input[type=text]::placeholder { color:#bbb; }
.pw-reply-btn-row { display:flex; justify-content:flex-end; gap:8px; }
.pw-reply-cancel { font-size:12px; color:#888; background:none; border:none;
cursor:pointer; padding:4px 10px; font-weight:bold; }
.pw-reply-cancel:hover { color:#333; }
.pw-reply-submit { padding:5px 12px; border:none; border-radius:2px; font-size:12px;
cursor:pointer; font-weight:bold; background:#167ac6; color:#fff; }
.pw-reply-submit:hover { background:#1266a8; }
/* ── Antworten unter Kommentar ── */
.pw-replies { margin-left:52px; margin-top:6px; }
.pw-reply-item { display:flex; gap:10px; padding:8px 0; border-top:1px solid #f5f5f5; }
.pw-reply-avatar { width:28px; height:28px; border-radius:50%; color:#fff;
display:flex; align-items:center; justify-content:center;
font-size:12px; font-weight:bold; flex-shrink:0; }
.pw-reply-body { flex:1; min-width:0; }
.pw-show-replies-btn { font-size:12px; color:#167ac6; background:none; border:none;
cursor:pointer; padding:4px 0; margin-top:4px; font-weight:bold; }
.pw-show-replies-btn:hover { text-decoration:underline; }
.pw-leer { color:#aaa; font-size:14px; text-align:center; padding:30px 0; }
</style>
<script>
function toggleReplyForm(idx) {
var f = document.getElementById('rf-' + idx);
if (!f) return;
if (f.style.display === 'none' || f.style.display === '') {
f.style.display = 'block';
f.querySelector('input[name="nachricht"]').focus();
} else {
f.style.display = 'none';
}
}
function toggleReplies(idx) {
var box = document.getElementById('replies-' + idx);
var btn = document.getElementById('sb-' + idx);
if (!box || !btn) return;
if (box.style.display === 'none' || box.style.display === '') {
box.style.display = 'block';
btn.textContent = btn.textContent.replace('','').replace('anzeigen','ausblenden');
} else {
box.style.display = 'none';
btn.textContent = btn.textContent.replace('','').replace('ausblenden','anzeigen');
}
}
</script>
"""
def esc(s):
return (s.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;"))
def avatar_farbe(name):
farben = ["#e53935", "#8e24aa", "#1e88e5", "#00897b",
"#43a047", "#fb8c00", "#6d4c41", "#546e7a"]
idx = sum(ord(c) for c in name) % len(farben)
return farben[idx]
def render_antworten(antworten):
html = ""
for a in antworten:
farbe = avatar_farbe(a["name"])
initial = a["name"][0].upper()
html += f"""
<div class="pw-reply-item">
<div class="pw-reply-avatar" style="background:{farbe}">{initial}</div>
<div class="pw-reply-body">
<div class="pw-meta">
<span class="pw-username">{esc(a['name'])}</span>
<span class="pw-time">{esc(a['zeit'])}</span>
</div>
<div class="pw-text">{esc(a['text'])}</div>
</div>
</div>"""
return html
@app.route("/pinnwand", methods=["GET", "POST"])
def pinnwand():
if request.method == "POST":
name = request.form.get("name", "").strip() or "Anonym"
text = request.form.get("nachricht", "").strip()
parent_raw = request.form.get("parent_id", "").strip()
if text:
if parent_raw.isdigit():
idx = int(parent_raw)
if 0 <= idx < len(eintraege):
eintraege[idx]["antworten"].append(
{"name": name, "text": text, "zeit": jetzt()}
)
else:
eintraege.append(
{"name": name, "text": text, "zeit": jetzt(), "antworten": []}
)
return redirect("/app/pinnwand")
# ── Kommentare rendern (neueste zuerst, echter Index für parent_id) ──
komm_html = ""
for real_idx, e in reversed(list(enumerate(eintraege))):
farbe = avatar_farbe(e["name"])
initial = e["name"][0].upper()
n_ant = len(e["antworten"])
# Block: Antworten anzeigen/verbergen
antworten_block = ""
if n_ant > 0:
label = f"{n_ant} Antwort{'en' if n_ant != 1 else ''}"
antworten_block = f"""
<button class="pw-show-replies-btn" id="sb-{real_idx}"
onclick="toggleReplies({real_idx})">
{label} anzeigen
</button>
<div class="pw-replies" id="replies-{real_idx}" style="display:none">
{render_antworten(e['antworten'])}
</div>"""
komm_html += f"""
<div class="pw-comment" id="c-{real_idx}">
<div class="pw-avatar" style="background:{farbe}">{initial}</div>
<div class="pw-comment-body">
<div class="pw-meta">
<span class="pw-username">{esc(e['name'])}</span>
<span class="pw-time">{esc(e['zeit'])}</span>
</div>
<div class="pw-text">{esc(e['text'])}</div>
<div class="pw-actions">
<button class="pw-action-btn pw-thumb">&#128077;</button>
<button class="pw-action-btn pw-thumb">&#128078;</button>
<button class="pw-action-btn"
onclick="toggleReplyForm({real_idx})">Antworten</button>
</div>
<!-- Antwort-Formular (initial versteckt) -->
<div class="pw-reply-form" id="rf-{real_idx}">
<form method="post">
<input type="hidden" name="parent_id" value="{real_idx}">
<input type="text" name="name"
placeholder="Dein Name (optional)">
<input type="text" name="nachricht"
placeholder="Antwort hinzufügen ..." required>
<div class="pw-reply-btn-row">
<button type="button" class="pw-reply-cancel"
onclick="toggleReplyForm({real_idx})">Abbrechen</button>
<button type="submit" class="pw-reply-submit">Antworten</button>
</div>
</form>
</div>
{antworten_block}
</div>
</div>"""
if not eintraege:
komm_html = '<p class="pw-leer">Noch keine Kommentare. Sei der Erste! 💬</p>'
n = len(eintraege)
anzahl_text = f"{n} Kommentar{'e' if n != 1 else ''}"
return seite(PINNWAND_CSS + f"""
<div class="pw-header"><strong>Kommentare</strong></div>
<form method="post">
<div class="pw-input-row">
<div class="pw-avatar" style="background:#888; font-size:22px">&#128100;</div>
<div class="pw-input-wrap">
<input type="text" name="name"
placeholder="Dein Name (optional)">
<input type="text" name="nachricht"
placeholder="Öffentlichen Kommentar hinzufügen ..." autofocus required>
<div class="pw-btn-row">
<button type="submit" class="pw-btn">Kommentieren</button>
</div>
</div>
</div>
</form>
<p class="pw-count">{anzahl_text}</p>
<div id="kommentare">{komm_html}</div>
""")
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
app.run(host="0.0.0.0", port=9002) # isa2 → Port 9002