433 lines
15 KiB
JavaScript
433 lines
15 KiB
JavaScript
/* ToolCTRL — Landing Page Interaktion */
|
|
(function () {
|
|
"use strict";
|
|
|
|
/* ---- Jahr im Footer ---- */
|
|
var yearEl = document.getElementById("year");
|
|
if (yearEl) yearEl.textContent = String(new Date().getFullYear());
|
|
|
|
/* ---- Mobile-Navigation ---- */
|
|
var toggle = document.querySelector(".nav-toggle");
|
|
var mobileNav = document.querySelector(".mobile-nav");
|
|
var header = document.querySelector(".site-header");
|
|
function updateHeaderState() {
|
|
if (!header) return;
|
|
header.classList.toggle("is-scrolled", window.scrollY > 12);
|
|
}
|
|
updateHeaderState();
|
|
window.addEventListener("scroll", updateHeaderState, { passive: true });
|
|
|
|
if (toggle && mobileNav) {
|
|
toggle.addEventListener("click", function () {
|
|
var open = mobileNav.hasAttribute("hidden");
|
|
if (open) {
|
|
mobileNav.removeAttribute("hidden");
|
|
toggle.setAttribute("aria-expanded", "true");
|
|
} else {
|
|
mobileNav.setAttribute("hidden", "");
|
|
toggle.setAttribute("aria-expanded", "false");
|
|
}
|
|
});
|
|
mobileNav.querySelectorAll("a").forEach(function (a) {
|
|
a.addEventListener("click", function () {
|
|
mobileNav.setAttribute("hidden", "");
|
|
toggle.setAttribute("aria-expanded", "false");
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ---- Console-Easter-Eggs fuer neugierige Inspektoren ---- */
|
|
var inspectAscii = [
|
|
" _______ _______ _______ ___ _______ _______ _______ ",
|
|
"| || || || | | || || |",
|
|
"|_ _|| _ || _ || | | ||_ _|| ___|",
|
|
" | | | | | || | | || | | | | | | |___ ",
|
|
" | | | |_| || |_| || |___ | _| | | | ___|",
|
|
" | | | || || || |_ | | | |___ ",
|
|
" |___| |_______||_______||_______||_______| |___| |_______|"
|
|
].join("\n");
|
|
var inspectHints = [
|
|
"CTRL-KEY EVENT: Inspect gedrueckt. Bitte keine Produktivsysteme anfassen.",
|
|
"Hidden combo: window.toolctrl.press()",
|
|
"Status: schwarz/weiss. Farbe wurde zurueck in den Werkzeugkoffer gelegt."
|
|
];
|
|
var inspectLogged = false;
|
|
|
|
function logControlConsole(reason) {
|
|
if (reason === "devtools" && inspectLogged) return;
|
|
if (reason === "devtools") inspectLogged = true;
|
|
console.log(
|
|
"%c" + inspectAscii,
|
|
"font-family: monospace; font-weight: 900; color: #000; background: #fff; padding: 10px; border: 3px solid #000;"
|
|
);
|
|
console.log("%c" + inspectHints[Math.floor(Math.random() * inspectHints.length)],
|
|
"font-family: monospace; color: #fff; background: #000; padding: 6px 8px;");
|
|
}
|
|
|
|
window.toolctrl = window.toolctrl || {};
|
|
window.toolctrl.press = function () {
|
|
console.log("%c[ CTRL ] *klack* Ablauf uebernommen.", "font-family: monospace; font-weight: 900;");
|
|
return "pressed";
|
|
};
|
|
|
|
window.addEventListener("load", function () {
|
|
setTimeout(function () { logControlConsole("load"); }, 450);
|
|
});
|
|
setInterval(function () {
|
|
var open = Math.abs(window.outerWidth - window.innerWidth) > 160 ||
|
|
Math.abs(window.outerHeight - window.innerHeight) > 160;
|
|
document.documentElement.toggleAttribute("data-inspecting", open);
|
|
if (open) logControlConsole("devtools");
|
|
}, 1200);
|
|
|
|
/* ---- Dezente Scroll-Reveals ---- */
|
|
var reduceMotion = window.matchMedia &&
|
|
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
var revealEls = Array.prototype.slice.call(document.querySelectorAll([
|
|
".hero-copy > *",
|
|
".hero-visual",
|
|
".trust-strip span",
|
|
".stance-head > *",
|
|
".stance-card",
|
|
".product-copy > *",
|
|
".product-ui",
|
|
".velocity-copy > *",
|
|
".velocity-item",
|
|
".section-head",
|
|
".card",
|
|
".industry-card",
|
|
".step",
|
|
".arbeit-copy > *",
|
|
".kanban-wrap",
|
|
".event-poster",
|
|
".mini-terminal",
|
|
".event-card",
|
|
".kontakt-copy > *",
|
|
".contact-promises li",
|
|
".lead-form",
|
|
".td-hero .wrap > *",
|
|
".td-feature > *",
|
|
".td-talk",
|
|
".speaker-card",
|
|
".shot",
|
|
".gj-teaser"
|
|
].join(",")));
|
|
|
|
revealEls.forEach(function (el, index) {
|
|
el.classList.add("reveal", "reveal-delay-" + (index % 5));
|
|
});
|
|
|
|
if (reduceMotion || !("IntersectionObserver" in window)) {
|
|
revealEls.forEach(function (el) { el.classList.add("is-visible"); });
|
|
} else {
|
|
var observer = new IntersectionObserver(function (entries) {
|
|
entries.forEach(function (entry) {
|
|
if (!entry.isIntersecting) return;
|
|
entry.target.classList.add("is-visible");
|
|
observer.unobserve(entry.target);
|
|
});
|
|
}, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
|
|
revealEls.forEach(function (el) { observer.observe(el); });
|
|
}
|
|
|
|
/* ---- Hero Workflow Nodes: ziehbare Kontrollpunkte ---- */
|
|
var board = document.querySelector("[data-control-board]");
|
|
if (board) {
|
|
var lineSvg = board.querySelector(".control-lines");
|
|
var logoNode = board.querySelector("[data-logo-node]");
|
|
var nodes = Array.prototype.slice.call(board.querySelectorAll("[data-node]"));
|
|
var draggableItems = nodes.concat(logoNode ? [logoNode] : []);
|
|
var activeDrag = null;
|
|
var lineFrame = 0;
|
|
|
|
if (logoNode) {
|
|
logoNode.addEventListener("dragstart", function (e) {
|
|
e.preventDefault();
|
|
});
|
|
}
|
|
|
|
function boardRect() {
|
|
return board.getBoundingClientRect();
|
|
}
|
|
|
|
function centerOf(el, b) {
|
|
var r = el.getBoundingClientRect();
|
|
return {
|
|
x: r.left - b.left + r.width / 2,
|
|
y: r.top - b.top + r.height / 2
|
|
};
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
return Math.max(min, Math.min(max, value));
|
|
}
|
|
|
|
function updateLines() {
|
|
if (!lineSvg || !logoNode) return;
|
|
var b = boardRect();
|
|
var target = centerOf(logoNode, b);
|
|
lineSvg.setAttribute("viewBox", "0 0 " + b.width + " " + b.height);
|
|
nodes.forEach(function (node) {
|
|
var path = lineSvg.querySelector('[data-line="' + node.dataset.node + '"]');
|
|
if (!path) return;
|
|
var nodeCenter = centerOf(node, b);
|
|
var isOutput = node.classList.contains("is-output");
|
|
var start = isOutput ? target : nodeCenter;
|
|
var end = isOutput ? nodeCenter : target;
|
|
var bend = Math.max(44, Math.abs(end.x - start.x) * 0.38);
|
|
var c1x = start.x + (end.x > start.x ? bend : -bend);
|
|
var c2x = end.x + (end.x > start.x ? -bend : bend);
|
|
var d = "M " + start.x + " " + start.y +
|
|
" C " + c1x + " " + start.y +
|
|
", " + c2x + " " + end.y +
|
|
", " + end.x + " " + end.y;
|
|
path.classList.toggle("is-output", isOutput);
|
|
path.setAttribute("d", d);
|
|
});
|
|
lineFrame = 0;
|
|
}
|
|
|
|
function requestLineUpdate() {
|
|
if (lineFrame) return;
|
|
lineFrame = window.requestAnimationFrame(updateLines);
|
|
}
|
|
|
|
draggableItems.forEach(function (item) {
|
|
item.addEventListener("pointerdown", function (e) {
|
|
var r = item.getBoundingClientRect();
|
|
if (item === logoNode) item.classList.add("is-positioned");
|
|
activeDrag = {
|
|
node: item,
|
|
isLogo: item === logoNode,
|
|
offsetX: e.clientX - r.left,
|
|
offsetY: e.clientY - r.top
|
|
};
|
|
item.classList.add("is-dragging");
|
|
item.setPointerCapture(e.pointerId);
|
|
e.preventDefault();
|
|
});
|
|
|
|
item.addEventListener("pointermove", function (e) {
|
|
if (!activeDrag || activeDrag.node !== item) return;
|
|
var b = boardRect();
|
|
var x = clamp(e.clientX - b.left - activeDrag.offsetX, 8, b.width - item.offsetWidth - 8);
|
|
var y = clamp(e.clientY - b.top - activeDrag.offsetY, 8, b.height - item.offsetHeight - 8);
|
|
item.style.left = x + "px";
|
|
item.style.top = y + "px";
|
|
if (activeDrag.isLogo) {
|
|
item.classList.add("is-positioned");
|
|
} else {
|
|
item.style.removeProperty("--x");
|
|
item.style.removeProperty("--y");
|
|
}
|
|
requestLineUpdate();
|
|
});
|
|
|
|
item.addEventListener("pointerup", function (e) {
|
|
if (!activeDrag || activeDrag.node !== item) return;
|
|
item.classList.remove("is-dragging");
|
|
item.releasePointerCapture(e.pointerId);
|
|
activeDrag = null;
|
|
requestLineUpdate();
|
|
});
|
|
|
|
item.addEventListener("pointercancel", function () {
|
|
item.classList.remove("is-dragging");
|
|
activeDrag = null;
|
|
});
|
|
});
|
|
|
|
window.addEventListener("resize", requestLineUpdate, { passive: true });
|
|
updateLines();
|
|
setTimeout(requestLineUpdate, 150);
|
|
}
|
|
|
|
/* ---- Produktwirkung: klickbare Workflow-Zustaende ---- */
|
|
var productUi = document.querySelector(".product-ui");
|
|
if (productUi) {
|
|
var productStates = {
|
|
eingang: {
|
|
kicker: "Heute · 14 aktive Vorgänge",
|
|
title: "Workflow Control",
|
|
pill: "3 Blocker",
|
|
flow: ["Anfrage", "Prüfung", "Angebot", "Termin", "Rechnung"],
|
|
active: 1,
|
|
done: 0,
|
|
metrics: [["Antwortzeit", "18 min"], ["Auto-Schritte", "74%"], ["Offene Übergaben", "03"]],
|
|
rows: [["Kunde wartet auf Rückmeldung", "Jetzt", "is-hot"], ["Dokumente aus Postfach übernommen", "Auto", ""], ["Team-Zuständigkeit gesetzt", "11:15", ""], ["Status an Leitung gemeldet", "Erledigt", "is-done"]]
|
|
},
|
|
angebote: {
|
|
kicker: "Vertrieb · 8 Angebote offen",
|
|
title: "Angebote steuern",
|
|
pill: "2 fällig",
|
|
flow: ["Lead", "Kalkulation", "Freigabe", "Versand", "Follow-up"],
|
|
active: 2,
|
|
done: 1,
|
|
metrics: [["Durchlauf", "2,4 h"], ["Vorlagen", "91%"], ["Rückfragen", "05"]],
|
|
rows: [["Angebot braucht interne Freigabe", "Heute", "is-hot"], ["PDF aus Kundendaten erstellt", "Auto", ""], ["Nachfass-Termin gesetzt", "Morgen", ""], ["Alte Version archiviert", "Erledigt", "is-done"]]
|
|
},
|
|
planung: {
|
|
kicker: "Team · 21 Einsätze geplant",
|
|
title: "Planung sichtbar",
|
|
pill: "1 Konflikt",
|
|
flow: ["Auftrag", "Ressource", "Slot", "Route", "Update"],
|
|
active: 2,
|
|
done: 1,
|
|
metrics: [["Auslastung", "82%"], ["Auto-Routen", "68%"], ["Konflikte", "01"]],
|
|
rows: [["Termin überschneidet sich", "Prüfen", "is-hot"], ["Route für Außendienst gebaut", "Auto", ""], ["Materialliste angehängt", "09:40", ""], ["Kunde informiert", "Erledigt", "is-done"]]
|
|
},
|
|
abschluss: {
|
|
kicker: "Abschluss · 36 Vorgänge fertig",
|
|
title: "Betrieb im Blick",
|
|
pill: "0 kritisch",
|
|
flow: ["Abnahme", "Doku", "Rechnung", "Feedback", "Archiv"],
|
|
active: 3,
|
|
done: 2,
|
|
metrics: [["Abnahmen", "96%"], ["Auto-Doku", "88%"], ["Offen", "00"]],
|
|
rows: [["Feedback für nächste Version notiert", "Neu", ""], ["Rechnung vorbereitet", "Auto", ""], ["Abnahme protokolliert", "13:20", "is-done"], ["Projekt sauber archiviert", "Erledigt", "is-done"]]
|
|
}
|
|
};
|
|
var productTabs = Array.prototype.slice.call(productUi.querySelectorAll("[data-product-tab]"));
|
|
var productKicker = productUi.querySelector("[data-product-kicker]");
|
|
var productTitle = productUi.querySelector("[data-product-title]");
|
|
var productPill = productUi.querySelector("[data-product-pill]");
|
|
var productFlow = productUi.querySelector("[data-product-flow]");
|
|
var productMetrics = productUi.querySelector("[data-product-metrics]");
|
|
var productTable = productUi.querySelector("[data-product-table]");
|
|
|
|
function setProductState(key) {
|
|
var state = productStates[key];
|
|
if (!state) return;
|
|
productUi.classList.add("is-switching");
|
|
productTabs.forEach(function (tab) {
|
|
tab.classList.toggle("active", tab.dataset.productTab === key);
|
|
});
|
|
window.setTimeout(function () {
|
|
if (productKicker) productKicker.textContent = state.kicker;
|
|
if (productTitle) productTitle.textContent = state.title;
|
|
if (productPill) productPill.textContent = state.pill;
|
|
if (productFlow) {
|
|
productFlow.innerHTML = state.flow.map(function (label, index) {
|
|
var cls = index === state.active ? "active" : (index <= state.done ? "done" : "");
|
|
return '<span class="' + cls + '">' + label + '</span>';
|
|
}).join("");
|
|
}
|
|
if (productMetrics) {
|
|
productMetrics.innerHTML = state.metrics.map(function (metric) {
|
|
return "<div><span>" + metric[0] + "</span><strong>" + metric[1] + "</strong></div>";
|
|
}).join("");
|
|
}
|
|
if (productTable) {
|
|
productTable.innerHTML = state.rows.map(function (row) {
|
|
return '<div class="ui-row ' + row[2] + '"><span>' + row[0] + '</span><b>' + row[1] + '</b></div>';
|
|
}).join("");
|
|
}
|
|
productUi.classList.remove("is-switching");
|
|
}, 90);
|
|
}
|
|
|
|
productTabs.forEach(function (tab) {
|
|
tab.addEventListener("click", function () {
|
|
setProductState(tab.dataset.productTab);
|
|
});
|
|
});
|
|
}
|
|
|
|
/* ---- Lead-Formular ---- */
|
|
var form = document.getElementById("lead-form");
|
|
var status = document.getElementById("form-status");
|
|
if (!form) return;
|
|
|
|
function setStatus(msg, kind) {
|
|
if (!status) return;
|
|
status.textContent = msg;
|
|
status.className = "form-status " + (kind || "");
|
|
status.hidden = false;
|
|
}
|
|
|
|
function fieldRow(input) {
|
|
return input.closest(".form-row");
|
|
}
|
|
|
|
function validate() {
|
|
var ok = true;
|
|
["f-name", "f-email", "f-nachricht"].forEach(function (id) {
|
|
var el = document.getElementById(id);
|
|
var row = fieldRow(el);
|
|
var valid = el.value.trim() !== "";
|
|
if (id === "f-email" && valid) {
|
|
valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(el.value.trim());
|
|
}
|
|
if (row) row.classList.toggle("invalid", !valid);
|
|
if (!valid) ok = false;
|
|
});
|
|
return ok;
|
|
}
|
|
|
|
form.addEventListener("submit", function (e) {
|
|
e.preventDefault();
|
|
|
|
if (!validate()) {
|
|
setStatus("Bitte füllen Sie die markierten Pflichtfelder aus.", "err");
|
|
return;
|
|
}
|
|
|
|
var btn = form.querySelector("button[type=submit]");
|
|
var payload = {
|
|
name: form.name.value.trim(),
|
|
firma: form.firma.value.trim(),
|
|
email: form.email.value.trim(),
|
|
telefon: form.telefon.value.trim(),
|
|
nachricht: form.nachricht.value.trim(),
|
|
website: form.website.value.trim(), // Honeypot
|
|
quelle: "Messe — Landing Page"
|
|
};
|
|
|
|
if (btn) { btn.disabled = true; btn.textContent = "Wird gesendet …"; }
|
|
setStatus("Anfrage wird übermittelt …", "");
|
|
|
|
fetch("/api/lead", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(function (res) {
|
|
return res.json().then(function (data) {
|
|
return { ok: res.ok, data: data };
|
|
});
|
|
})
|
|
.then(function (r) {
|
|
if (r.ok) {
|
|
form.reset();
|
|
setStatus(
|
|
"Vielen Dank — Ihre Anfrage ist bei uns eingegangen. Wir melden uns zeitnah.",
|
|
"ok"
|
|
);
|
|
} else {
|
|
setStatus(
|
|
(r.data && r.data.error) ||
|
|
"Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
|
|
"err"
|
|
);
|
|
}
|
|
})
|
|
.catch(function () {
|
|
setStatus(
|
|
"Verbindung fehlgeschlagen. Bitte schreiben Sie uns direkt an info@tool-ctrl.de.",
|
|
"err"
|
|
);
|
|
})
|
|
.finally(function () {
|
|
if (btn) { btn.disabled = false; btn.textContent = "Anfrage senden"; }
|
|
});
|
|
});
|
|
|
|
/* Markierung beim Tippen entfernen */
|
|
form.querySelectorAll("input, textarea").forEach(function (el) {
|
|
el.addEventListener("input", function () {
|
|
var row = fieldRow(el);
|
|
if (row) row.classList.remove("invalid");
|
|
});
|
|
});
|
|
})();
|