Einreich-Formular: "+" Floating-Button + /api/submit + Overlay mit Text/Medien-Upload

This commit is contained in:
Hermes Agent
2026-06-17 14:58:50 +02:00
parent 7189528c20
commit 5aeb2e3e90
+211
View File
@@ -1857,6 +1857,48 @@ def public_frontend():
.chat-overlay { bottom:5rem; right:0.5rem; width:calc(100vw - 1rem); max-height:60vh; }
.chat-bubble { bottom:1rem; right:1rem; }
}
/* ── Submit Overlay ────────────────────────────────────────── */
.submit-bubble { position:fixed; bottom:1.5rem; right:5.5rem; z-index:9999;
width:56px; height:56px; border-radius:50%; background:#28a745;
color:#fff; border:none; cursor:pointer; font-size:1.8rem; font-weight:300; line-height:1;
box-shadow:0 6px 24px rgba(0,0,0,0.25); transition:transform 0.2s,box-shadow 0.2s;
display:flex; align-items:center; justify-content:center; }
.submit-bubble:hover { transform:scale(1.08); box-shadow:0 8px 32px rgba(0,0,0,0.35); }
.submit-bubble:active { transform:scale(0.95); }
.submit-overlay { position:fixed; bottom:5.5rem; right:1.5rem; z-index:9998;
width:400px; max-width:calc(100vw - 2rem); max-height:70vh;
background:#fff; border-radius:18px; box-shadow:0 12px 48px rgba(0,0,0,0.22);
display:none; flex-direction:column; overflow:hidden; }
.submit-overlay.open { display:flex; }
.submit-header { background:#28a745; color:#fff; padding:0.9rem 1.2rem;
font-weight:700; font-size:0.9rem; display:flex; align-items:center; justify-content:space-between; }
.submit-header button { background:none; border:none; color:#fff; font-size:1.3rem;
cursor:pointer; opacity:0.8; padding:0; line-height:1; }
.submit-header button:hover { opacity:1; }
.submit-body { flex:1; overflow-y:auto; padding:1.2rem; max-height:55vh; background:#fafafa; }
.submit-body .hint { font-size:0.75rem; color:#888; margin-bottom:1rem; line-height:1.5; }
.submit-body label { display:block; font-size:0.7rem; font-weight:600; color:#555;
text-transform:uppercase; letter-spacing:0.03em; margin-bottom:0.2rem; margin-top:0.8rem; }
.submit-body label:first-of-type { margin-top:0; }
.submit-body input, .submit-body textarea { width:100%; border:1.5px solid #ddd;
border-radius:10px; padding:0.55rem 0.8rem; font-size:0.82rem; font-family:inherit;
outline:none; transition:border-color 0.15s; }
.submit-body input:focus, .submit-body textarea:focus { border-color:#28a745; }
.submit-body textarea { resize:vertical; min-height:70px; }
.submit-body input[type="file"] { padding:0.4rem 0; border:none; font-size:0.78rem; }
.submit-body .submit-btn { width:100%; background:#28a745; color:#fff; border:none;
border-radius:10px; padding:0.7rem; font-weight:700; font-size:0.88rem;
cursor:pointer; margin-top:1rem; transition:background 0.15s; font-family:inherit; }
.submit-body .submit-btn:hover { background:#1e7e34; }
.submit-body .submit-btn:disabled { opacity:0.5; cursor:default; }
.submit-body .status-msg { text-align:center; font-size:0.8rem; margin-top:0.6rem;
min-height:1.2rem; }
@media(max-width:600px) {
.submit-overlay { bottom:5rem; right:0.5rem; width:calc(100vw - 1rem); max-height:65vh; }
.submit-bubble { bottom:1rem; right:5rem; }
}
</style>
</head>
<body>
@@ -1887,6 +1929,9 @@ def public_frontend():
💬<span class="bubble-dot"></span>
</button>
<!-- ── Submit Bubble ────────────────────────────────────────── -->
<button class="submit-bubble" id="submitBubble" title="Etwas einreichen" aria-label="Einreich-Formular öffnen">+</button>
<!-- ── Chat Overlay ─────────────────────────────────────────── -->
<div class="chat-overlay" id="chatOverlay">
<div class="chat-header">
@@ -1905,6 +1950,101 @@ def public_frontend():
</div>
</div>
<!-- ── Submit Overlay ───────────────────────────────────────── -->
<div class="submit-overlay" id="submitOverlay">
<div class="submit-header">
<span>📨 Etwas einreichen</span>
<button id="submitClose" aria-label="Schließen">✕</button>
</div>
<div class="submit-body">
<div class="hint">
🎲 <strong>Schick uns, was dich bewegt!</strong> Texte, Fotos, Videos oder Sprachnachrichten —
alles rund um Brettspiele und die SPIEL Essen. Wir schauen uns alles an und
veröffentlichen die spannendsten Beiträge.<br>
<span style="color:#888;">Nur Text oder nur eine Datei reicht völlig.</span>
</div>
<label>Dein Name (optional)</label>
<input type="text" id="subName" placeholder="Vorname oder Spitzname" maxlength="100">
<label>Nachricht (optional, reicht auch ohne Text)</label>
<textarea id="subText" placeholder="Was möchtest du mitteilen? ..." maxlength="5000"></textarea>
<label>📎 Foto / Video / Audio (optional)</label>
<input type="file" id="subMedia" accept="image/*,video/*,audio/*">
<label>📍 Halle / Stand (optional)</label>
<input type="text" id="subHalle" placeholder="z.B. H3 B123" maxlength="20">
<label>🎯 Spieltitel (optional)</label>
<input type="text" id="subSpiel" placeholder="Name des Brettspiels" maxlength="200">
<button class="submit-btn" id="submitSend">📨 Absenden</button>
<div class="status-msg" id="submitStatus"></div>
</div>
</div>
<script>
(function() {
const sBubble = document.getElementById('submitBubble');
const sOverlay = document.getElementById('submitOverlay');
const sClose = document.getElementById('submitClose');
const sSend = document.getElementById('submitSend');
const sStatus = document.getElementById('submitStatus');
let sOpen = false;
sBubble.addEventListener('click', () => {
sOpen = !sOpen;
sOverlay.classList.toggle('open', sOpen);
const chatO = document.getElementById('chatOverlay');
if (chatO) chatO.classList.remove('open');
});
sClose.addEventListener('click', () => { sOpen = false; sOverlay.classList.remove('open'); });
sSend.addEventListener('click', async () => {
const name = document.getElementById('subName').value.trim();
const text = document.getElementById('subText').value.trim();
const fileInput = document.getElementById('subMedia');
const file = fileInput.files[0];
const halle = document.getElementById('subHalle').value.trim();
const spiel = document.getElementById('subSpiel').value.trim();
if (!text && !file) {
sStatus.textContent = 'Bitte Text oder eine Datei angeben.';
sStatus.style.color = '#dc3545';
return;
}
sSend.disabled = true;
sSend.textContent = '⏳ Wird gesendet...';
sStatus.textContent = '';
const formData = new FormData();
if (name) formData.append('name', name);
if (text) formData.append('text', text);
if (file) formData.append('media', file);
if (halle) formData.append('halle', halle);
if (spiel) formData.append('spiel_titel', spiel);
try {
const r = await fetch('/api/submit', { method:'POST', body:formData });
const data = await r.json();
if (r.ok) {
sStatus.textContent = data.message;
sStatus.style.color = '#28a745';
document.getElementById('subName').value = '';
document.getElementById('subText').value = '';
fileInput.value = '';
document.getElementById('subHalle').value = '';
document.getElementById('subSpiel').value = '';
setTimeout(() => { sOverlay.classList.remove('open'); sOpen = false; sStatus.textContent = ''; }, 2500);
} else {
sStatus.textContent = data.error || 'Fehler beim Senden.';
sStatus.style.color = '#dc3545';
}
} catch(e) {
sStatus.textContent = 'Netzwerkfehler. Versuch es später nochmal.';
sStatus.style.color = '#dc3545';
}
sSend.disabled = false;
sSend.textContent = '📨 Absenden';
});
})();
<script>
(function() {
const bubble = document.getElementById('chatBubble');
@@ -2148,6 +2288,77 @@ def api_chat():
return jsonify({'reply': reply})
@app.route('/api/submit', methods=['POST'])
def api_submit():
"""Public submission endpoint — text + optional media upload."""
db = get_db()
name = (request.form.get('name') or '').strip()[:100]
text = (request.form.get('text') or '').strip()[:5000]
halle = (request.form.get('halle') or '').strip()[:20]
spiel_titel = (request.form.get('spiel_titel') or '').strip()[:200]
# Require at least text or a file
file = request.files.get('media')
if not text and (not file or not file.filename):
return jsonify({'error': 'Bitte Text oder eine Datei (Foto/Video/Audio) angeben.'}), 400
media_path = None
msg_type = 'text'
if file and file.filename:
# Determine type from extension
ext = os.path.splitext(file.filename)[1].lower()
mime_map = {
'.jpg': 'image', '.jpeg': 'image', '.png': 'image', '.webp': 'image', '.gif': 'image',
'.mp4': 'video', '.webm': 'video',
'.ogg': 'audio', '.mp3': 'audio', '.wav': 'audio',
}
msg_type = mime_map.get(ext, 'document')
# Add type prefix to content
prefix = {
'image': '\n[🖼️ Bild]',
'video': '\n[🎬 Video]',
'audio': '\n[🎤 Sprachnachricht]',
'document': '\n[📎 Dokument]',
}.get(msg_type, f'\n[{msg_type}]')
if text:
text = text + prefix
else:
text = prefix.strip()
# Save file
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
safe_name = f"web_{ts}{ext}"
save_path = MEDIA_DIR / safe_name
try:
file.save(str(save_path))
media_path = str(save_path)
except Exception:
return jsonify({'error': 'Datei konnte nicht gespeichert werden.'}), 500
# Validate halle
halle_valid = None
if halle:
import re
if re.match(r'^H[1-7]\s*[A-Z]\d{1,4}$', halle, re.IGNORECASE):
halle_valid = halle.upper()
sender_id = 'web_' + (name.replace(' ', '_') if name else 'anonym')
sender_name = name or None
db.execute(
"""INSERT INTO submissions
(channel, sender_id, sender_name, type, content, media_path, status, halle, spiel_titel)
VALUES (?, ?, ?, ?, ?, ?, 'new', ?, ?)""",
('web', sender_id, sender_name, msg_type, text or None, media_path, halle_valid, spiel_titel or None),
)
db.commit()
return jsonify({'ok': True, 'message': 'Danke für deine Einreichung! Wir schauen sie uns an. 🎲'})
@app.route("/beitrag/<int:sub_id>")
def public_detail(sub_id: int):
"""Public detail view — full media, text, and metadata."""