Inbound-Magnet aus dem Konzept (dev/klarheits-check/): 5 Fragen mit Skala + optionalem Freitext, deterministischer Reifegrad im Frontend, KI-Einordnung folgt in Phase 3 über einen serverseitigen Proxy. - Route + Seite /klarheits-check im Marketing-Layout (teilbare URL) - Fragenkatalog als config/klarheits_check.php (eine Wahrheit für Anzeige + späteren Prompt) - Wiederverwendbare Partial; Quiz-JS mit Auswahl, geführter Navigation (Check starten → Frage 1, „Weiter" pro Frage), deterministischem Reifegrad und Ergebnis-Rendering; Fetch an POST /klarheits-check/auswerten mit sauberem Fallback (Endpoint folgt in Phase 3) - Inline-Validierung mit Sprung zur offenen Frage; „Neu starten" hinter dem Audit-CTA - Hochkant-Tab rechts (mobil Pill unten rechts), verlinkt auf die Seite, auf der Seite selbst ausgeblendet - kc-*-Styles über die bestehenden Theme-Tokens (beide Wirkungs-Modi) - CSRF-Meta im Layout für den späteren POST - Feature-Tests (Rendering, geführte Navigation, Tab-Sichtbarkeit) Enthält erstmalig routes/marketing.php und das Theme-Stylesheet, da die Funktion darauf aufbaut. config/services.php (Anthropic/OpenAI) bleibt für den Phase-3-Commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
220 lines
9 KiB
JavaScript
220 lines
9 KiB
JavaScript
/* Marken-Klarheits-Check — Quiz-State, geführte Navigation, deterministischer Reifegrad,
|
|
* Ergebnis-Rendering. Die KI-Einordnung kommt (nur bei Freitext) vom serverseitigen Proxy
|
|
* POST /klarheits-check/auswerten. Schlägt der Call fehl oder fehlt Freitext, greift das
|
|
* deterministische Ergebnis — das Tool funktioniert immer.
|
|
* Funktioniert auf /klarheits-check wie im Overlay (mehrere [data-kc]-Roots).
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
// Reifegrad je Skala-Index — Spiegel zu config/klarheits_check.php.
|
|
const GRAD = ['scharf', 'unscharf', 'nicht entschieden', 'nicht entschieden'];
|
|
const GRADCLASS = ['scharf', 'unscharf', 'offen', 'offen'];
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
}
|
|
|
|
function scrollToEl(el) {
|
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
function gradLine(state) {
|
|
const c = { scharf: 0, unscharf: 0, offen: 0 };
|
|
state.forEach((s) => { if (s.sel != null) c[GRADCLASS[s.sel]]++; });
|
|
if (c.scharf >= 4) return 'Ihr Fundament steht weitgehend. Die Schärfe ist da, jetzt zählt, dass sie auch dokumentiert ist.';
|
|
if (c.offen >= 3) return 'Das Fundament ist an mehreren Stellen noch nicht entschieden. Das ist kein Mangel an Substanz, sondern an Festlegung.';
|
|
return 'Das Fundament steht in Teilen. An einigen Stellen ist die Entscheidung noch nicht getroffen oder nicht festgehalten.';
|
|
}
|
|
|
|
function initCheck(root) {
|
|
if (root.dataset.kcInit) return; // Doppel-Init vermeiden (Seite + Overlay teilen das Markup)
|
|
root.dataset.kcInit = '1';
|
|
|
|
const questions = Array.from(root.querySelectorAll('.kc-q'));
|
|
const state = questions.map(() => ({ sel: null, text: '' }));
|
|
|
|
// Antwort wählen → markieren, Freitextfeld dezent einladen, Weiter-Button zeigen.
|
|
root.querySelectorAll('[data-kc-opt]').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
const q = +btn.dataset.q;
|
|
const o = +btn.dataset.o;
|
|
state[q].sel = o;
|
|
root.querySelectorAll('[data-kc-opt][data-q="' + q + '"]').forEach((x) => x.setAttribute('aria-pressed', 'false'));
|
|
btn.setAttribute('aria-pressed', 'true');
|
|
|
|
const qEl = questions[q];
|
|
const field = qEl.querySelector('[data-kc-field]');
|
|
if (field && !state[q].text) field.classList.add('is-invited'); // Hervorhebung, KEIN Fokus
|
|
const next = qEl.querySelector('[data-kc-next]');
|
|
if (next) next.hidden = false;
|
|
|
|
hideError(root);
|
|
});
|
|
});
|
|
|
|
// Freitext → speichern; sobald etwas drinsteht, Einladungs-Hervorhebung lösen.
|
|
root.querySelectorAll('[data-kc-text]').forEach((t) => {
|
|
t.addEventListener('input', () => {
|
|
const q = +t.dataset.q;
|
|
state[q].text = t.value.trim();
|
|
const field = questions[q].querySelector('[data-kc-field]');
|
|
if (field) field.classList.toggle('is-invited', !state[q].text && state[q].sel != null);
|
|
});
|
|
});
|
|
|
|
// Weiter → nächste Frage, bei der letzten zur Auswertung.
|
|
root.querySelectorAll('[data-kc-next-btn]').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
const qEl = btn.closest('.kc-q');
|
|
const idx = questions.indexOf(qEl);
|
|
const target = questions[idx + 1] || root.querySelector('[data-kc-submit]');
|
|
scrollToEl(target);
|
|
});
|
|
});
|
|
|
|
const submitBtn = root.querySelector('[data-kc-submit]');
|
|
if (submitBtn) submitBtn.addEventListener('click', () => auswerten(root, state, submitBtn, questions));
|
|
|
|
const jumpBtn = root.querySelector('[data-kc-error-jump]');
|
|
if (jumpBtn) {
|
|
jumpBtn.addEventListener('click', () => {
|
|
const firstOpen = state.findIndex((s) => s.sel == null);
|
|
if (firstOpen !== -1) scrollToEl(questions[firstOpen]);
|
|
});
|
|
}
|
|
|
|
const resetBtn = root.querySelector('[data-kc-reset]');
|
|
if (resetBtn) resetBtn.addEventListener('click', () => window.location.reload());
|
|
}
|
|
|
|
function hideError(root) {
|
|
const err = root.querySelector('[data-kc-error]');
|
|
if (err) err.hidden = true;
|
|
}
|
|
|
|
async function auswerten(root, state, submitBtn, questions) {
|
|
if (state.some((s) => s.sel == null)) {
|
|
const err = root.querySelector('[data-kc-error]');
|
|
if (err) err.hidden = false;
|
|
return;
|
|
}
|
|
hideError(root);
|
|
|
|
submitBtn.disabled = true;
|
|
const loading = root.querySelector('[data-kc-loading]');
|
|
const hatText = state.some((s) => s.text);
|
|
if (loading && hatText) loading.hidden = false;
|
|
|
|
let ki = null;
|
|
if (hatText) {
|
|
try {
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
const res = await fetch('/klarheits-check/auswerten', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Accept: 'application/json',
|
|
'X-CSRF-TOKEN': meta ? meta.getAttribute('content') : '',
|
|
},
|
|
body: JSON.stringify({ antworten: state.map((s) => ({ skala: s.sel, text: s.text })) }),
|
|
});
|
|
if (res.ok) ki = await res.json();
|
|
} catch (_) { ki = null; }
|
|
}
|
|
|
|
render(root, state, ki, questions);
|
|
|
|
if (loading) loading.hidden = true;
|
|
|
|
// Ergebnis-Zustand: Fragen + kompletter Aktionsbereich weg, Hero-Start weg.
|
|
const form = root.querySelector('[data-kc-form]');
|
|
if (form) form.hidden = true;
|
|
const actions = root.querySelector('.kc-actions');
|
|
if (actions) actions.hidden = true;
|
|
document.querySelectorAll('[data-kc-start-link]').forEach((el) => { el.hidden = true; });
|
|
|
|
// „Neu starten“ hinter den Audit-CTA ans Ende des Ergebnisses verschieben.
|
|
const result = root.querySelector('[data-kc-result]');
|
|
const resetBtn = root.querySelector('[data-kc-reset]');
|
|
const lastBlock = result ? result.querySelector('.kc-r-block:last-child') : null;
|
|
if (resetBtn && lastBlock) {
|
|
resetBtn.hidden = false;
|
|
lastBlock.appendChild(resetBtn);
|
|
}
|
|
|
|
scrollToEl(result);
|
|
}
|
|
|
|
function render(root, state, ki, questions) {
|
|
const spiegelFor = (n) => {
|
|
if (!ki || !ki.spiegel) return '';
|
|
const m = ki.spiegel.find((s) => s.nr === n);
|
|
return m && m.text ? m.text : '';
|
|
};
|
|
|
|
let html = '<div class="kc-r-eyebrow meta">Ihr Ergebnis</div>'
|
|
+ '<div class="kc-r-gesamt">' + (ki && ki.gesamt ? escapeHtml(ki.gesamt) : escapeHtml(gradLine(state))) + '</div>';
|
|
|
|
questions.forEach((q, i) => {
|
|
const sel = state[i].sel;
|
|
const bw = (q.querySelector('.kc-q-bw') || {}).textContent || '';
|
|
const selBtn = root.querySelector('[data-kc-opt][data-q="' + i + '"][aria-pressed="true"]');
|
|
const optText = selBtn ? selBtn.textContent.trim() : '';
|
|
const spiegel = spiegelFor(i + 1);
|
|
html += '<div class="kc-r-row">'
|
|
+ '<div><div class="kc-r-grad ' + GRADCLASS[sel] + '">' + GRAD[sel] + '</div></div>'
|
|
+ '<div><div class="kc-r-label meta">' + escapeHtml(bw) + '</div>'
|
|
+ '<div class="kc-r-spiegel">' + (spiegel ? escapeHtml(spiegel) : 'Einschätzung: ' + escapeHtml(optText) + '.') + '</div></div>'
|
|
+ '</div>';
|
|
});
|
|
|
|
if (ki && ki.luecken && ki.luecken.length) {
|
|
html += '<div class="kc-r-block"><h3 class="meta">Lücke zwischen Selbstbild und Wortlaut</h3><ul>'
|
|
+ ki.luecken.map((l) => '<li>' + escapeHtml(l) + '</li>').join('') + '</ul></div>';
|
|
}
|
|
if (ki && ki.stimmigkeit && ki.stimmigkeit.length) {
|
|
html += '<div class="kc-r-block"><h3 class="meta">Innere Stimmigkeit</h3><ul>'
|
|
+ ki.stimmigkeit.map((l) => '<li>' + escapeHtml(l) + '</li>').join('') + '</ul></div>';
|
|
}
|
|
|
|
const schluss = ki && ki.schluss
|
|
? ki.schluss
|
|
: 'Wo das Fundament noch unscharf ist, liegt das nicht an Ihnen, sondern an einer Entscheidung, die noch aussteht. Genau diese Entscheidung hält das Marken-Klarheits-Audit als dokumentierte Marken-DNA fest.';
|
|
html += '<div class="kc-r-block"><div class="kc-r-schluss">' + escapeHtml(schluss) + '</div>'
|
|
+ '<a class="btn btn-nav-cta" href="/audit">Zum Marken-Klarheits-Audit</a></div>';
|
|
|
|
const result = root.querySelector('[data-kc-result]');
|
|
if (result) {
|
|
result.innerHTML = html;
|
|
result.hidden = false;
|
|
}
|
|
}
|
|
|
|
function init() {
|
|
document.querySelectorAll('[data-kc]').forEach(initCheck);
|
|
|
|
// „Check starten“ im Hero: sanft zum Anker scrollen statt hart zu springen.
|
|
document.querySelectorAll('[data-kc-start-link]').forEach((link) => {
|
|
if (link.dataset.kcBound) return;
|
|
link.dataset.kcBound = '1';
|
|
link.addEventListener('click', (e) => {
|
|
const href = link.getAttribute('href') || '';
|
|
const target = href.charAt(0) === '#' ? document.querySelector(href) : null;
|
|
if (target) {
|
|
e.preventDefault();
|
|
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
if (document.readyState !== 'loading') {
|
|
init();
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
}
|
|
|
|
// Für dynamisch eingefügte Overlays (CP 2.x) erneut aufrufbar.
|
|
window.MarkemachtKlarheitsCheck = { init: init };
|
|
})();
|