Quelle: D:/Claude-Projekte/PummelHummel/feedback_modul/ (Standard-Pattern, auch in arduino.lehrstun.de + meta.lehrstun.de erprobt). 1:1 nach 3d.lehrstun.de/feedback_modul/ kopiert. app.py erweitert: - init_feedback_modul-Aufruf mit unseren Adaptern - _alph_user_kontext: Lehrer = admin, SuS = profil mit Vorname+Tier, Anonym = kein Widget - _alph_widget_aktiv: liest einstellung.feedback_widget_aktiv - _alph_admin_unauthorized: Redirect zu admin.admin_login Hart-Aus via ENV ALPH_DEAKTIVIERT=1 weiterhin moeglich. Soft-Aus via einstellung.feedback_widget_aktiv='0' (per Admin-Inline-Edit). DB: - feedback-Tabelle via feedback_modul.schema.DDL angelegt (idempotent) - einstellung.feedback_widget_aktiv auf '1' gesetzt (Alpha-Test-Modus) Test: - Eingeloggter SuS sieht das Alph-Widget auf /lektion/1 - Anonym (kein Login) sieht es nicht (user_kontext gibt None) - Admin sieht es mit Rolle 'admin' Naechster Schritt: Deploy auf Hetzner (3d.lehrstun.de), Markus loggt sich als Test-Schueler ein und gibt Feedback zu den 14 Lektionen.
445 lines
18 KiB
JavaScript
445 lines
18 KiB
JavaScript
/* Alph der PummelHummel — Alphatest-Feedback-Widget
|
||
* ----------------------------------------------------
|
||
* Self-contained: HTML + CSS + SVG + Logik in dieser einen Datei.
|
||
* Einbindung:
|
||
*
|
||
* <script src=".../alph.js"
|
||
* data-endpoint="/api/feedback"
|
||
* data-app="meine-app"
|
||
* data-user='{"tier":"otter","kasten":7}'
|
||
* defer></script>
|
||
*
|
||
* Liest config aus data-Attributen vom <script>-Tag:
|
||
* data-endpoint POST-URL fuer Feedback (Pflicht)
|
||
* data-app Identifier der App (Pflicht, z.B. "arduino-werkstatt")
|
||
* data-user JSON-String mit user_kontext (optional, Server kann auch selbst setzen)
|
||
* data-position "bottom-right" (Default) | "bottom-left"
|
||
*
|
||
* Schickt JSON mit:
|
||
* { kategorie, text, url_pfad, seitentitel, viewport, sprache,
|
||
* element_selector?, element_text? }
|
||
*/
|
||
|
||
(function () {
|
||
'use strict';
|
||
if (window.__alphMounted) return;
|
||
window.__alphMounted = true;
|
||
|
||
var script = document.currentScript ||
|
||
(function () {
|
||
var all = document.getElementsByTagName('script');
|
||
return all[all.length - 1];
|
||
})();
|
||
|
||
var ENDPOINT = script.getAttribute('data-endpoint') || '/api/feedback';
|
||
var APP_NAME = script.getAttribute('data-app') || 'unknown';
|
||
var POSITION = script.getAttribute('data-position') || 'bottom-right';
|
||
var USER_RAW = script.getAttribute('data-user') || '';
|
||
var USER_KONTEXT = null;
|
||
if (USER_RAW) {
|
||
try { USER_KONTEXT = JSON.parse(USER_RAW); }
|
||
catch (e) { USER_KONTEXT = null; }
|
||
}
|
||
|
||
// Cookie-Helper fuer "diese Stunde verstecken"
|
||
function getCookie(name) {
|
||
var m = document.cookie.match(new RegExp('(^|; )' + name + '=([^;]*)'));
|
||
return m ? decodeURIComponent(m[2]) : null;
|
||
}
|
||
function setCookie(name, value, hours) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + hours * 60 * 60 * 1000);
|
||
document.cookie = name + '=' + encodeURIComponent(value) +
|
||
';expires=' + d.toUTCString() + ';path=/;SameSite=Lax';
|
||
}
|
||
|
||
if (getCookie('alph_versteckt') === '1') return;
|
||
|
||
// === CSS injizieren ============================================
|
||
var css = `
|
||
.alph-host { all: initial; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
||
.alph-host * { box-sizing: border-box; }
|
||
.alph-fab {
|
||
position: fixed;
|
||
${POSITION === 'bottom-left' ? 'left: 1.5rem' : 'right: 1.5rem'};
|
||
bottom: 1.5rem;
|
||
width: 110px; height: 110px;
|
||
z-index: 99998;
|
||
cursor: pointer;
|
||
user-select: none;
|
||
animation: alph-float 6s ease-in-out infinite;
|
||
transition: transform 0.15s;
|
||
filter: drop-shadow(0 8px 14px rgba(0,0,0,0.28));
|
||
}
|
||
.alph-fab:hover { transform: scale(1.15) rotate(-6deg); animation-play-state: paused; }
|
||
.alph-fab:active { transform: scale(0.95); }
|
||
.alph-fab svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
||
.alph-wing { transform-origin: 50% 50%; animation: alph-wing 0.13s ease-in-out infinite alternate; }
|
||
.alph-wing-r { animation-delay: 0.04s; }
|
||
.alph-antenna { transform-origin: 50% 100%; animation: alph-antenna 4s ease-in-out infinite; }
|
||
.alph-tooltip {
|
||
position: absolute;
|
||
bottom: 120px;
|
||
${POSITION === 'bottom-left' ? 'left: 0' : 'right: 0'};
|
||
background: #2C3E50; color: white;
|
||
padding: 0.5rem 0.9rem; border-radius: 8px;
|
||
font-size: 0.92rem; font-weight: 500; white-space: nowrap;
|
||
opacity: 0; pointer-events: none;
|
||
transition: opacity 0.15s;
|
||
box-shadow: 0 4px 10px rgba(0,0,0,0.2);
|
||
}
|
||
.alph-fab:hover .alph-tooltip { opacity: 1; }
|
||
.alph-close {
|
||
position: absolute;
|
||
top: 2px; right: 2px;
|
||
width: 26px; height: 26px; border-radius: 50%;
|
||
background: #E74C3C; color: white;
|
||
font-size: 18px; font-weight: bold; line-height: 24px;
|
||
text-align: center;
|
||
cursor: pointer; opacity: 0;
|
||
transition: opacity 0.15s;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||
}
|
||
.alph-fab:hover .alph-close { opacity: 1; }
|
||
.alph-fab.alph-shake { animation: alph-shake 0.5s ease-in-out !important; }
|
||
|
||
/* Eine einzige 6-Sekunden-Schleife: ruhiges Schweben + bei 70-92% ein Hop */
|
||
@keyframes alph-float {
|
||
0% { transform: translateY(0) translateX(0) rotate(-2deg); }
|
||
10% { transform: translateY(-8px) translateX(2px) rotate(0deg); }
|
||
20% { transform: translateY(-14px) translateX(0) rotate(2deg); }
|
||
30% { transform: translateY(-8px) translateX(-2px) rotate(0deg); }
|
||
40% { transform: translateY(0) translateX(0) rotate(-2deg); }
|
||
55% { transform: translateY(-10px) translateX(2px) rotate(1deg); }
|
||
65% { transform: translateY(-4px) translateX(0) rotate(-1deg); }
|
||
/* Hop! Aufmerksamkeitsmacher */
|
||
72% { transform: translateY(-26px) rotate(-9deg) scale(1.08); }
|
||
78% { transform: translateY(-6px) rotate(7deg) scale(1.02); }
|
||
84% { transform: translateY(-20px) rotate(-4deg) scale(1.05); }
|
||
90% { transform: translateY(-4px) rotate(2deg) scale(1); }
|
||
100% { transform: translateY(0) translateX(0) rotate(-2deg); }
|
||
}
|
||
@keyframes alph-wing {
|
||
0% { transform: scaleY(1) rotate(0deg); }
|
||
100% { transform: scaleY(0.35) rotate(14deg); }
|
||
}
|
||
@keyframes alph-shake {
|
||
0%, 100% { transform: translateX(0) rotate(0) scale(1); }
|
||
20% { transform: translateX(-6px) rotate(-6deg) scale(1.06); }
|
||
40% { transform: translateX(6px) rotate(5deg) scale(1.10); }
|
||
60% { transform: translateX(-4px) rotate(-3deg) scale(1.05); }
|
||
80% { transform: translateX(4px) rotate(2deg) scale(1.02); }
|
||
}
|
||
@keyframes alph-antenna {
|
||
0%, 100% { transform: rotate(0deg); }
|
||
50% { transform: rotate(4deg); }
|
||
}
|
||
|
||
.alph-overlay {
|
||
position: fixed; inset: 0;
|
||
background: rgba(0,0,0,0.45);
|
||
z-index: 99999;
|
||
display: flex; align-items: center; justify-content: center;
|
||
animation: alph-fade-in 0.2s ease-out;
|
||
}
|
||
.alph-modal {
|
||
background: white; color: #2C3E50;
|
||
width: min(440px, 92vw); max-height: 92vh; overflow-y: auto;
|
||
border-radius: 12px; padding: 1.4rem;
|
||
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
|
||
animation: alph-pop-in 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||
}
|
||
.alph-modal h2 {
|
||
font-size: 1.15rem; margin: 0 0 0.4rem 0;
|
||
color: #2C3E50; display: flex; align-items: center; gap: 0.5rem;
|
||
}
|
||
.alph-modal p.alph-sub {
|
||
color: #7F8C8D; font-size: 0.88rem; margin: 0 0 1rem 0;
|
||
}
|
||
.alph-kat-grid {
|
||
display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem;
|
||
margin-bottom: 0.9rem;
|
||
}
|
||
.alph-kat-btn {
|
||
background: #F8F9FA; border: 2px solid #DEE2E6;
|
||
border-radius: 8px; padding: 0.7rem 0.6rem;
|
||
text-align: left; cursor: pointer;
|
||
font-size: 0.88rem; color: #2C3E50;
|
||
transition: all 0.15s; font-family: inherit;
|
||
}
|
||
.alph-kat-btn:hover { border-color: #00979D; background: #EBF8F9; }
|
||
.alph-kat-btn.alph-active {
|
||
border-color: #00979D; background: #00979D; color: white;
|
||
}
|
||
.alph-kat-btn .alph-emoji { font-size: 1.1rem; margin-right: 0.3rem; }
|
||
.alph-kat-btn small { display: block; opacity: 0.75; font-size: 0.74rem; margin-top: 0.15rem; }
|
||
.alph-modal textarea {
|
||
width: 100%; padding: 0.7rem; min-height: 100px;
|
||
border: 1px solid #DEE2E6; border-radius: 6px;
|
||
font-family: inherit; font-size: 0.95rem; resize: vertical;
|
||
color: #2C3E50;
|
||
}
|
||
.alph-modal textarea:focus { outline: none; border-color: #00979D; box-shadow: 0 0 0 3px rgba(0,151,157,0.15); }
|
||
.alph-context {
|
||
font-size: 0.78rem; color: #7F8C8D;
|
||
background: #F8F9FA; padding: 0.5rem 0.7rem; border-radius: 4px;
|
||
margin: 0.6rem 0; font-family: 'Fira Code', Consolas, monospace;
|
||
}
|
||
.alph-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; }
|
||
.alph-btn {
|
||
padding: 0.55rem 1.2rem; border-radius: 6px; border: none;
|
||
cursor: pointer; font-size: 0.92rem; font-family: inherit;
|
||
transition: background 0.15s;
|
||
}
|
||
.alph-btn-primary { background: #00979D; color: white; }
|
||
.alph-btn-primary:hover { background: #007A80; }
|
||
.alph-btn-primary:disabled { background: #B5C4C5; cursor: not-allowed; }
|
||
.alph-btn-secondary { background: #ECF0F1; color: #2C3E50; }
|
||
.alph-btn-secondary:hover { background: #D5DBDB; }
|
||
.alph-error {
|
||
background: #FADBD8; color: #943126;
|
||
padding: 0.5rem 0.8rem; border-radius: 4px; font-size: 0.88rem;
|
||
margin-top: 0.5rem;
|
||
}
|
||
.alph-success {
|
||
background: #D5F5E3; color: #1E8449;
|
||
padding: 1rem; border-radius: 6px; text-align: center;
|
||
}
|
||
.alph-success .alph-success-icon { font-size: 2rem; display: block; margin-bottom: 0.4rem; }
|
||
|
||
@keyframes alph-fade-in { from { opacity: 0; } to { opacity: 1; } }
|
||
@keyframes alph-pop-in {
|
||
from { opacity: 0; transform: scale(0.85) translateY(20px); }
|
||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.alph-fab { width: 88px; height: 88px; bottom: 1rem; right: 1rem; }
|
||
.alph-tooltip { bottom: 96px; font-size: 0.84rem; }
|
||
.alph-kat-grid { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
/* Reduzierte Bewegungs-Praeferenz respektieren (Barrierefreiheit) */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.alph-fab { animation: none; }
|
||
.alph-wing, .alph-antenna { animation: none; }
|
||
}
|
||
`;
|
||
var styleEl = document.createElement('style');
|
||
styleEl.textContent = css;
|
||
document.head.appendChild(styleEl);
|
||
|
||
// === Hummel-SVG ================================================
|
||
var hummelSVG = `
|
||
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" aria-label="Alph der PummelHummel">
|
||
<!-- Linker Fluegel -->
|
||
<ellipse class="alph-wing alph-wing-l" cx="32" cy="35" rx="14" ry="22"
|
||
fill="#E8F4FD" fill-opacity="0.85" stroke="#B0C4DE" stroke-width="1"/>
|
||
<!-- Rechter Fluegel -->
|
||
<ellipse class="alph-wing alph-wing-r" cx="68" cy="35" rx="14" ry="22"
|
||
fill="#E8F4FD" fill-opacity="0.85" stroke="#B0C4DE" stroke-width="1"/>
|
||
<!-- Body (Pummel) -->
|
||
<ellipse cx="50" cy="58" rx="28" ry="26" fill="#FFCC33" stroke="#2C3E50" stroke-width="2"/>
|
||
<!-- Streifen -->
|
||
<path d="M 26 50 Q 50 46 74 50" stroke="#2C3E50" stroke-width="5" fill="none" stroke-linecap="round"/>
|
||
<path d="M 28 66 Q 50 70 72 66" stroke="#2C3E50" stroke-width="5" fill="none" stroke-linecap="round"/>
|
||
<!-- T-Shirt mit Alpha -->
|
||
<rect x="38" y="54" width="24" height="14" rx="3" fill="white" stroke="#2C3E50" stroke-width="1.2"/>
|
||
<text x="50" y="65" font-family="Georgia, serif" font-size="11" font-weight="bold"
|
||
text-anchor="middle" fill="#00979D">α</text>
|
||
<!-- Kopf -->
|
||
<circle cx="50" cy="32" r="13" fill="#2C3E50"/>
|
||
<!-- Augen -->
|
||
<circle cx="46" cy="30" r="2.5" fill="white"/>
|
||
<circle cx="54" cy="30" r="2.5" fill="white"/>
|
||
<circle cx="46" cy="30.5" r="1.2" fill="#2C3E50"/>
|
||
<circle cx="54" cy="30.5" r="1.2" fill="#2C3E50"/>
|
||
<!-- Antennen (wackeln) -->
|
||
<g class="alph-antenna">
|
||
<path d="M 44 22 Q 41 14 38 12" stroke="#2C3E50" stroke-width="1.8" fill="none" stroke-linecap="round"/>
|
||
<path d="M 56 22 Q 59 14 62 12" stroke="#2C3E50" stroke-width="1.8" fill="none" stroke-linecap="round"/>
|
||
<circle cx="38" cy="12" r="2" fill="#FFCC33" stroke="#2C3E50" stroke-width="1"/>
|
||
<circle cx="62" cy="12" r="2" fill="#FFCC33" stroke="#2C3E50" stroke-width="1"/>
|
||
</g>
|
||
<!-- Mund -->
|
||
<path d="M 47 36 Q 50 38 53 36" stroke="white" stroke-width="1.5" fill="none" stroke-linecap="round"/>
|
||
<!-- Stinger -->
|
||
<path d="M 50 84 L 47 92 L 53 92 Z" fill="#2C3E50"/>
|
||
</svg>
|
||
`;
|
||
|
||
// === DOM aufbauen =============================================
|
||
var host = document.createElement('div');
|
||
host.className = 'alph-host';
|
||
host.innerHTML = `
|
||
<div class="alph-fab" id="alph-fab" role="button" aria-label="Feedback geben — Alph der PummelHummel" tabindex="0">
|
||
<span class="alph-close" id="alph-close" title="Heute ausblenden">×</span>
|
||
${hummelSVG}
|
||
<span class="alph-tooltip">Hier was kaputt? Sag's Alph!</span>
|
||
</div>
|
||
`;
|
||
document.body.appendChild(host);
|
||
|
||
var fab = host.querySelector('#alph-fab');
|
||
var closeBtn = host.querySelector('#alph-close');
|
||
|
||
closeBtn.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
setCookie('alph_versteckt', '1', 4); // 4 Stunden weg
|
||
host.remove();
|
||
});
|
||
|
||
fab.addEventListener('click', oeffneModal);
|
||
fab.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oeffneModal(); }
|
||
});
|
||
|
||
// === Modal-Logik ==============================================
|
||
var KATEGORIEN = [
|
||
{ id: 'code', emoji: '🐛', label: 'Codefehler', hint: 'Sketch tut nicht, Compiler-Error' },
|
||
{ id: 'link', emoji: '🔗', label: 'Link kaputt', hint: 'Knopf fuehrt ins Leere' },
|
||
{ id: 'verstehen', emoji: '❓', label: 'Versteh ich nicht', hint: 'Erklaerung unklar' },
|
||
{ id: 'fehlt', emoji: '❌', label: 'Etwas fehlt', hint: 'Info / Bauteil / Schritt' },
|
||
{ id: 'sonstiges', emoji: '💬', label: 'Sonstiges', hint: 'Idee, Lob, anderes' },
|
||
];
|
||
|
||
var modalEl = null;
|
||
var ausgewaehlteKat = null;
|
||
|
||
function oeffneModal() {
|
||
fab.classList.add('alph-shake');
|
||
setTimeout(function () { fab.classList.remove('alph-shake'); }, 400);
|
||
if (modalEl) return;
|
||
|
||
ausgewaehlteKat = null;
|
||
modalEl = document.createElement('div');
|
||
modalEl.className = 'alph-overlay';
|
||
var katBtns = KATEGORIEN.map(function (k) {
|
||
return `<button type="button" class="alph-kat-btn" data-kat="${k.id}">
|
||
<span class="alph-emoji">${k.emoji}</span>${k.label}
|
||
<small>${k.hint}</small>
|
||
</button>`;
|
||
}).join('');
|
||
|
||
modalEl.innerHTML = `
|
||
<div class="alph-modal" role="dialog" aria-label="Feedback geben">
|
||
<h2>🐝 Alph hört zu</h2>
|
||
<p class="alph-sub">Was ist dir aufgefallen? Wähl eine Kategorie:</p>
|
||
<div class="alph-kat-grid">${katBtns}</div>
|
||
<textarea id="alph-text" placeholder="Beschreib's kurz — was hast du gemacht, was ist passiert?"></textarea>
|
||
<div class="alph-context" id="alph-ctx"></div>
|
||
<div id="alph-msg"></div>
|
||
<div class="alph-actions">
|
||
<button type="button" class="alph-btn alph-btn-secondary" id="alph-cancel">Abbrechen</button>
|
||
<button type="button" class="alph-btn alph-btn-primary" id="alph-send" disabled>Absenden</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
host.appendChild(modalEl);
|
||
|
||
modalEl.querySelector('#alph-ctx').textContent =
|
||
'Seite: ' + window.location.pathname + ' · Titel: ' + (document.title || '—');
|
||
|
||
modalEl.querySelectorAll('.alph-kat-btn').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
modalEl.querySelectorAll('.alph-kat-btn').forEach(function (b) {
|
||
b.classList.remove('alph-active');
|
||
});
|
||
btn.classList.add('alph-active');
|
||
ausgewaehlteKat = btn.getAttribute('data-kat');
|
||
pruefeAbsendbar();
|
||
});
|
||
});
|
||
modalEl.querySelector('#alph-text').addEventListener('input', pruefeAbsendbar);
|
||
modalEl.querySelector('#alph-cancel').addEventListener('click', schliesseModal);
|
||
modalEl.querySelector('#alph-send').addEventListener('click', sende);
|
||
modalEl.addEventListener('click', function (e) {
|
||
if (e.target === modalEl) schliesseModal();
|
||
});
|
||
document.addEventListener('keydown', escListener);
|
||
}
|
||
|
||
function escListener(e) {
|
||
if (e.key === 'Escape') schliesseModal();
|
||
}
|
||
function schliesseModal() {
|
||
if (!modalEl) return;
|
||
modalEl.remove();
|
||
modalEl = null;
|
||
document.removeEventListener('keydown', escListener);
|
||
}
|
||
|
||
function pruefeAbsendbar() {
|
||
if (!modalEl) return;
|
||
var text = modalEl.querySelector('#alph-text').value.trim();
|
||
modalEl.querySelector('#alph-send').disabled = !(ausgewaehlteKat && text.length >= 3);
|
||
}
|
||
|
||
function sende() {
|
||
if (!modalEl || !ausgewaehlteKat) return;
|
||
var sendBtn = modalEl.querySelector('#alph-send');
|
||
var msgEl = modalEl.querySelector('#alph-msg');
|
||
var text = modalEl.querySelector('#alph-text').value.trim();
|
||
sendBtn.disabled = true;
|
||
sendBtn.textContent = 'Sende...';
|
||
msgEl.innerHTML = '';
|
||
|
||
var payload = {
|
||
app: APP_NAME,
|
||
kategorie: ausgewaehlteKat,
|
||
text: text,
|
||
url_pfad: window.location.pathname + window.location.search,
|
||
seitentitel: document.title || '',
|
||
viewport: window.innerWidth + 'x' + window.innerHeight,
|
||
sprache: navigator.language || ''
|
||
};
|
||
// user_kontext nur senden, wenn vom Server via data-user mitgegeben
|
||
// (Drop-In-Modus). Bei Flask-Blueprint-Integration wird er sowieso
|
||
// serverseitig gesetzt und ueberschrieben.
|
||
if (USER_KONTEXT) {
|
||
payload.user_kontext = USER_KONTEXT;
|
||
}
|
||
|
||
fetch(ENDPOINT, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
credentials: 'same-origin'
|
||
}).then(function (r) {
|
||
return r.json().then(function (j) { return { ok: r.ok, body: j }; });
|
||
}).then(function (res) {
|
||
if (res.ok && res.body && res.body.ok) {
|
||
erfolgZeigen();
|
||
} else {
|
||
var err = (res.body && res.body.error) || 'Unbekannter Fehler';
|
||
if (err === 'rate_limit') err = 'Zu viele Reports — kurz warten.';
|
||
if (err === 'text_zu_kurz') err = 'Text muss mindestens 3 Zeichen lang sein.';
|
||
msgEl.innerHTML = '<div class="alph-error">' + err + '</div>';
|
||
sendBtn.disabled = false;
|
||
sendBtn.textContent = 'Absenden';
|
||
}
|
||
}).catch(function () {
|
||
msgEl.innerHTML = '<div class="alph-error">Netzwerkfehler. Versuch nochmal.</div>';
|
||
sendBtn.disabled = false;
|
||
sendBtn.textContent = 'Absenden';
|
||
});
|
||
}
|
||
|
||
function erfolgZeigen() {
|
||
if (!modalEl) return;
|
||
var modal = modalEl.querySelector('.alph-modal');
|
||
modal.innerHTML = `
|
||
<div class="alph-success">
|
||
<span class="alph-success-icon">🐝</span>
|
||
<strong>Danke!</strong><br>
|
||
Alph hat's notiert.
|
||
</div>
|
||
`;
|
||
setTimeout(schliesseModal, 1500);
|
||
}
|
||
|
||
// Klick-Capture Mode (optional, fuer "Codefehler" und "Link kaputt")
|
||
// — bewusst nicht implementiert in Erstversion; Element-Selector wird
|
||
// im Backend leergelassen. Erweiterbar ueber Page-Click-Handler.
|
||
})();
|