➕ 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-overlay { bottom:5rem; right:0.5rem; width:calc(100vw - 1rem); max-height:60vh; }
|
||||||
.chat-bubble { bottom:1rem; right:1rem; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -1887,6 +1929,9 @@ def public_frontend():
|
|||||||
💬<span class="bubble-dot"></span>
|
💬<span class="bubble-dot"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- ── Submit Bubble ────────────────────────────────────────── -->
|
||||||
|
<button class="submit-bubble" id="submitBubble" title="Etwas einreichen" aria-label="Einreich-Formular öffnen">+</button>
|
||||||
|
|
||||||
<!-- ── Chat Overlay ─────────────────────────────────────────── -->
|
<!-- ── Chat Overlay ─────────────────────────────────────────── -->
|
||||||
<div class="chat-overlay" id="chatOverlay">
|
<div class="chat-overlay" id="chatOverlay">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
@@ -1905,6 +1950,101 @@ def public_frontend():
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<script>
|
||||||
(function() {
|
(function() {
|
||||||
const bubble = document.getElementById('chatBubble');
|
const bubble = document.getElementById('chatBubble');
|
||||||
@@ -2148,6 +2288,77 @@ def api_chat():
|
|||||||
return jsonify({'reply': reply})
|
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>")
|
@app.route("/beitrag/<int:sub_id>")
|
||||||
def public_detail(sub_id: int):
|
def public_detail(sub_id: int):
|
||||||
"""Public detail view — full media, text, and metadata."""
|
"""Public detail view — full media, text, and metadata."""
|
||||||
|
|||||||
Reference in New Issue
Block a user