Design-Fix: Medien-Toggle, SVG-Icons, blauer +-Button, Delete-Redirect

- Medien-Publish: Statt absolut positioniertem Emoji-Label → CSS-Toggle-Switch
  in sauberer Toolbar unter dem Media-Element (BSN-Blau statt Teal)
- Emoji-Icons (🖼️🎬🎤💬) → moderne 16×16 SVG-Icons in #20228a
  (Admin-Detail + Frontend)
- Grüner +-Button → BSN-Blau (#20228a), gesamte Submit-UI umgefärbt
- Delete-Bug gefixt: innerHTML führte <script> nicht aus → Client
  macht jetzt Toast-Animation + window.location selbst
This commit is contained in:
Hermes Agent
2026-06-17 15:31:24 +02:00
parent 402e231d2f
commit a55b290ce3
+54 -19
View File
@@ -1074,13 +1074,10 @@ def submission_detail(sub_id: int):
with open(path, "rb") as f: with open(path, "rb") as f:
b64 = base64.b64encode(f.read()).decode() b64 = base64.b64encode(f.read()).decode()
src = f"data:{mime};base64,{b64}" src = f"data:{mime};base64,{b64}"
media_html += f'<div class="media-item" style="position:relative;margin:0.5rem 0;">' media_html += f'<div class="media-item" style="margin:0.5rem 0;background:var(--surface);border-radius:var(--radius-sm);overflow:hidden;">'
# Checkbox overlay # Media element
media_html += f'<label style="position:absolute;top:8px;right:8px;z-index:10;background:rgba(0,0,0,0.7);padding:0.3rem 0.6rem;border-radius:6px;display:flex;align-items:center;gap:0.3rem;cursor:pointer;font-size:0.78rem;color:#4ecdc4;">'
media_html += f'<input type="checkbox" name="media_publish_{i}" value="1" {checked} style="accent-color:#4ecdc4;"> 📢 Veröffentlichen'
media_html += f'</label>'
if mime.startswith("image/"): if mime.startswith("image/"):
media_html += f'<img src="{src}" style="max-width:100%;max-height:400px;border-radius:6px;display:block;">' media_html += f'<img src="{src}" style="width:100%;max-height:400px;object-fit:contain;display:block;background:#000;">'
elif mime.startswith("video/"): elif mime.startswith("video/"):
# Generate thumbnail for poster # Generate thumbnail for poster
thumb = generate_video_thumbnail(path) thumb = generate_video_thumbnail(path)
@@ -1092,16 +1089,32 @@ def submission_detail(sub_id: int):
poster_attr = f' poster="data:image/jpeg;base64,{thumb_b64}"' poster_attr = f' poster="data:image/jpeg;base64,{thumb_b64}"'
except Exception: except Exception:
pass pass
media_html += f'<video controls preload="metadata" {poster_attr} style="max-width:100%;max-height:400px;"><source src="{src}" type="{mime}"></video>' media_html += f'<video controls preload="metadata" {poster_attr} style="width:100%;max-height:400px;background:#000;"><source src="{src}" type="{mime}"></video>'
elif mime.startswith("audio/"): elif mime.startswith("audio/"):
media_html += f'<audio controls style="width:100%;"><source src="{src}" type="{mime}"></audio>' media_html += f'<audio controls style="width:100%;padding:0.8rem;"><source src="{src}" type="{mime}"></audio>'
else: else:
media_html += f'<p>📎 <a href="{src}" download>{os.path.basename(path)}</a></p>' media_html += f'<p style="padding:0.8rem;">📎 <a href="{src}" download>{os.path.basename(path)}</a></p>'
# Toggle bar
media_html += f'<div class="publish-bar" style="display:flex;align-items:center;gap:0.6rem;padding:0.5rem 0.8rem;border-top:1px solid var(--border);">'
media_html += f'<label class="toggle-switch">'
media_html += f'<input type="checkbox" name="media_publish_{i}" value="1" {checked}>'
media_html += f'<span class="toggle-slider"></span>'
media_html += f'</label>'
media_html += f'<span style="font-size:0.78rem;font-weight:500;color:var(--text2);">Veröffentlichen</span>'
media_html += f'</div>'
media_html += f'</div>' media_html += f'</div>'
except Exception as e: except Exception as e:
media_html += f'<p style="color:#dc3545;">Fehler: {e}</p>' media_html += f'<p style="color:#dc3545;">Fehler: {e}</p>'
type_emoji = {"text": "💬", "image": "🖼️", "audio": "🎤", "video": "🎬", "document": "📎"}.get(row["type"], "📌") # SVG type icons (16x16, BSN blue)
_svg_icons = {
"text": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="2" width="13" height="10" rx="2" stroke="#20228a" stroke-width="1.5"/><path d="M11 11l2 3M5 11l-2 3" stroke="#20228a" stroke-width="1.5" stroke-linecap="round"/></svg>',
"image": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="1.5" width="13" height="13" rx="2" stroke="#20228a" stroke-width="1.5"/><circle cx="5" cy="5.5" r="1.5" fill="#20228a"/><path d="M1.5 11.5l3.5-3 2.5 2 3-3.5 4 4.5" stroke="#20228a" stroke-width="1.5" stroke-linejoin="round"/></svg>',
"audio": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="4" width="3.5" height="8" rx="1" fill="#20228a"/><path d="M5 6.5l4-4v11l-4-4" stroke="#20228a" stroke-width="1.5" stroke-linejoin="round"/><path d="M9.5 4.5c1.5.8 1.5 4.2 0 5" stroke="#20228a" stroke-width="1.5" stroke-linecap="round"/></svg>',
"video": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="#20228a" stroke-width="1.5"/><path d="M6 5l4 3-4 3V5z" fill="#20228a"/></svg>',
"document": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><path d="M3 1.5h6l4 4v9a1 1 0 01-1 1H3a1 1 0 01-1-1v-12a1 1 0 011-1z" stroke="#20228a" stroke-width="1.5"/><path d="M9 1.5V6h4" stroke="#20228a" stroke-width="1.5"/></svg>',
}
type_emoji = _svg_icons.get(row["type"], _svg_icons["document"])
return f"""<!DOCTYPE html> return f"""<!DOCTYPE html>
<html lang="de"> <html lang="de">
@@ -1137,6 +1150,14 @@ def submission_detail(sub_id: int):
.btn-done:hover {{ background:rgba(48,209,88,0.28); }} .btn-done:hover {{ background:rgba(48,209,88,0.28); }}
.btn-delete {{ background:rgba(255,69,58,0.12); color:#ff453a; border:1px solid rgba(255,69,58,0.2); }} .btn-delete {{ background:rgba(255,69,58,0.12); color:#ff453a; border:1px solid rgba(255,69,58,0.2); }}
.btn-delete:hover {{ background:rgba(255,69,58,0.2); }} .btn-delete:hover {{ background:rgba(255,69,58,0.2); }}
/* Toggle switch — publish bar */
.toggle-switch {{ position:relative; display:inline-block; width:40px; height:22px; flex-shrink:0; }}
.toggle-switch input {{ opacity:0; width:0; height:0; }}
.toggle-slider {{ position:absolute; cursor:pointer; top:0; left:0; right:0; bottom:0; background:var(--surface2); border-radius:22px; transition:all 0.25s ease; border:1.5px solid var(--border); }}
.toggle-slider:before {{ content:""; position:absolute; height:16px; width:16px; left:2px; bottom:2px; background:#fff; border-radius:50%; transition:all 0.25s ease; box-shadow:0 1px 3px rgba(0,0,0,0.2); }}
.toggle-switch input:checked + .toggle-slider {{ background:var(--accent); border-color:var(--accent); }}
.toggle-switch input:checked + .toggle-slider:before {{ transform:translateX(18px); }}
.btn-flag {{ background:rgba(255,69,58,0.1); color:#ff453a; }} .btn-flag {{ background:rgba(255,69,58,0.1); color:#ff453a; }}
.btn-flag:hover {{ background:rgba(255,69,58,0.2); }} .btn-flag:hover {{ background:rgba(255,69,58,0.2); }}
.btn-publish {{ background:rgba(32,34,138,0.2); color:var(--accent2); border:1px solid var(--accent); }} .btn-publish {{ background:rgba(32,34,138,0.2); color:var(--accent2); border:1px solid var(--accent); }}
@@ -1290,8 +1311,15 @@ if (_delBtn) {{
_status.style.display = 'inline'; _status.style.display = 'inline';
_status.textContent = ''; _status.textContent = '';
fetch('/submission/' + {row['id']} + '/delete', {{method:'POST'}}) fetch('/submission/' + {row['id']} + '/delete', {{method:'POST'}})
.then(function(r) {{ return r.text(); }}) .then(function(r) {{
.then(function(html) {{ document.body.innerHTML = html; }}) if (!r.ok) throw new Error('Server-Fehler');
// Redirect directly — the server now returns JSON
document.body.innerHTML = '<div style=\"display:flex;align-items:center;justify-content:center;min-height:100vh;background:#0f0f1a;\"><div style=\"background:rgba(220,53,69,0.15);color:#dc3545;padding:1.5rem 2.5rem;border-radius:8px;font-size:1.15rem;font-weight:600;text-align:center;box-shadow:0 4px 32px rgba(0,0,0,0.6);animation:fadeInOut 1.8s ease forwards;\">🗑️ Beitrag #' + {row['id']} + ' gelöscht</div></div>';
var style = document.createElement('style');
style.textContent = '@keyframes fadeInOut{{0%{{opacity:0;transform:scale(0.9)}}15%{{opacity:1;transform:scale(1)}}75%{{opacity:1;transform:scale(1)}}100%{{opacity:0;transform:scale(0.95)}}}}';
document.head.appendChild(style);
setTimeout(function(){{ window.location='/admin'; }}, 1800);
}})
.catch(function(err) {{ .catch(function(err) {{
_status.textContent = 'Fehler: ' + err.message; _status.textContent = 'Fehler: ' + err.message;
_delBtn.disabled = false; _delBtn.disabled = false;
@@ -1683,7 +1711,14 @@ def public_frontend():
except Exception: except Exception:
pass pass
type_emoji = {"text": "\U0001f4ac", "image": "\U0001f5bc\ufe0f", "audio": "\U0001f3a4", "video": "\U0001f3ac", "document": "\U0001f4ce"}.get(r["type"], "\U0001f4cc") _fe_svg_icons = {
"text": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="2" width="13" height="10" rx="2" stroke="#20228a" stroke-width="1.5"/><path d="M11 11l2 3M5 11l-2 3" stroke="#20228a" stroke-width="1.5" stroke-linecap="round"/></svg>',
"image": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="1.5" width="13" height="13" rx="2" stroke="#20228a" stroke-width="1.5"/><circle cx="5" cy="5.5" r="1.5" fill="#20228a"/><path d="M1.5 11.5l3.5-3 2.5 2 3-3.5 4 4.5" stroke="#20228a" stroke-width="1.5" stroke-linejoin="round"/></svg>',
"audio": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="4" width="3.5" height="8" rx="1" fill="#20228a"/><path d="M5 6.5l4-4v11l-4-4" stroke="#20228a" stroke-width="1.5" stroke-linejoin="round"/><path d="M9.5 4.5c1.5.8 1.5 4.2 0 5" stroke="#20228a" stroke-width="1.5" stroke-linecap="round"/></svg>',
"video": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="#20228a" stroke-width="1.5"/><path d="M6 5l4 3-4 3V5z" fill="#20228a"/></svg>',
"document": '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" style="vertical-align:middle;margin-right:3px;"><path d="M3 1.5h6l4 4v9a1 1 0 01-1 1H3a1 1 0 01-1-1v-12a1 1 0 011-1z" stroke="#20228a" stroke-width="1.5"/><path d="M9 1.5V6h4" stroke="#20228a" stroke-width="1.5"/></svg>',
}
type_emoji = _fe_svg_icons.get(r["type"], _fe_svg_icons["document"])
name = (r["publish_name"] or r["sender_name"] or "Anonym").strip() name = (r["publish_name"] or r["sender_name"] or "Anonym").strip()
if not name or name.lower() in ("unknown", "anonym"): if not name or name.lower() in ("unknown", "anonym"):
name = "Anonym" name = "Anonym"
@@ -1870,7 +1905,7 @@ def public_frontend():
/* ── Submit Overlay ────────────────────────────────────────── */ /* ── Submit Overlay ────────────────────────────────────────── */
.submit-bubble { position:fixed; bottom:1.5rem; right:5.5rem; z-index:9999; .submit-bubble { position:fixed; bottom:1.5rem; right:5.5rem; z-index:9999;
width:56px; height:56px; border-radius:50%; background:#28a745; width:56px; height:56px; border-radius:50%; background:#20228a;
color:#fff; border:none; cursor:pointer; font-size:1.8rem; font-weight:300; line-height:1; 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; 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; } display:flex; align-items:center; justify-content:center; }
@@ -1882,7 +1917,7 @@ def public_frontend():
background:#fff; border-radius:18px; box-shadow:0 12px 48px rgba(0,0,0,0.22); background:#fff; border-radius:18px; box-shadow:0 12px 48px rgba(0,0,0,0.22);
display:none; flex-direction:column; overflow:hidden; } display:none; flex-direction:column; overflow:hidden; }
.submit-overlay.open { display:flex; } .submit-overlay.open { display:flex; }
.submit-header { background:#28a745; color:#fff; padding:0.9rem 1.2rem; .submit-header { background:#20228a; color:#fff; padding:0.9rem 1.2rem;
font-weight:700; font-size:0.9rem; display:flex; align-items:center; justify-content:space-between; } 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; .submit-header button { background:none; border:none; color:#fff; font-size:1.3rem;
cursor:pointer; opacity:0.8; padding:0; line-height:1; } cursor:pointer; opacity:0.8; padding:0; line-height:1; }
@@ -1895,13 +1930,13 @@ def public_frontend():
.submit-body input, .submit-body textarea { width:100%; border:1.5px solid #ddd; .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; border-radius:10px; padding:0.55rem 0.8rem; font-size:0.82rem; font-family:inherit;
outline:none; transition:border-color 0.15s; } 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 textarea { resize:vertical; min-height:70px; }
.submit-body input:focus, .submit-body textarea:focus { border-color:#20228a; }
.submit-body input[type="file"] { padding:0.4rem 0; border:none; font-size:0.78rem; } .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; .submit-body .submit-btn { width:100%; background:#20228a; color:#fff; border:none;
border-radius:10px; padding:0.7rem; font-weight:700; font-size:0.88rem; 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; } cursor:pointer; margin-top:1rem; transition:background 0.15s; font-family:inherit; }
.submit-body .submit-btn:hover { background:#1e7e34; } .submit-body .submit-btn:hover { background:#161e6e; }
.submit-body .submit-btn:disabled { opacity:0.5; cursor:default; } .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; .submit-body .status-msg { text-align:center; font-size:0.8rem; margin-top:0.6rem;
min-height:1.2rem; } min-height:1.2rem; }
@@ -2031,7 +2066,7 @@ def public_frontend():
const data = await r.json(); const data = await r.json();
if (r.ok) { if (r.ok) {
sStatus.textContent = data.message; sStatus.textContent = data.message;
sStatus.style.color = '#28a745'; sStatus.style.color = '#20228a';
document.getElementById('subName').value = ''; document.getElementById('subName').value = '';
document.getElementById('subText').value = ''; document.getElementById('subText').value = '';
fileInput.value = ''; fileInput.value = '';