➕ Einreich-Formular: "+" Floating-Button + /api/submit + Overlay mit Text/Medien-Upload
This commit is contained in:
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user