commit 143748dc0dc55002f1a0eeff157d4e631ed0f48c Author: ToolCTRL Date: Fri May 22 16:43:28 2026 +0200 Initial commit: ToolCTRL Landing Page Statische Landing Page mit Lead-Formular, Impressum und Datenschutzerklärung, ausgeliefert von einem abhängigkeitsfreien Node-Server mit lokaler Lead-Erfassung. Co-Authored-By: Claude Opus 4.7 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..497b9bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Lead-Daten — enthalten personenbezogene Daten, gehören nicht ins Repo +leads/ + +# OS / Editor +.DS_Store + +# Node +node_modules/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..c41ade8 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# ToolCTRL — Landing Page + +Eine-Seiten-Website im Visitenkarten-Stil für den Messeauftritt: Landing Page +plus Lead-Formular zur Kontaktsammlung. Monochrom (weiß / schwarz), an die +ToolCTRL Brand Identity angelehnt, ohne externe Abhängigkeiten. + +## Starten + +```bash +node server.js +``` + +Der Server sucht automatisch einen freien Port (zuerst `4317`) und gibt die +URL aus, z. B. `http://127.0.0.1:4317`. Stoppen mit `Strg + C`. + +Voraussetzung: Node.js (getestet mit v26). Keine Installation, keine `npm`-Pakete. + +## Aufbau + +``` +public/ statische Website (HTML, CSS, JS, Assets) + index.html Landing Page + Kontaktformular + styles.css Designsystem + script.js Formular-Logik & Mobile-Navigation + assets/ Logos, Emblem, Favicon, Icon-Set +server.js abhängigkeitsfreier Node-HTTP-Server + Lead-Endpunkt +leads/ gesammelte Anfragen (wird automatisch angelegt) + leads.csv Anfragen als CSV — direkt in Excel öffenbar + leads.ndjson dieselben Anfragen als JSON pro Zeile +``` + +## Leads + +Jede abgesendete Anfrage landet sofort lokal auf der Platte — in `leads.csv` +**und** `leads.ndjson`. So geht auf der Messe keine Anfrage verloren, auch +ohne Internet. Ein Honeypot-Feld fängt einfache Spam-Bots ab. + +Endpunkt: `POST /api/lead` (JSON). Pflichtfelder: `name`, `email`, `nachricht`. + +## Hinweis für die Messe + +Für den QR-Code am Stand sollte der Rechner mit dem Server im selben WLAN +erreichbar sein. Soll die Seite öffentlich laufen, hinter einen Reverse-Proxy +mit HTTPS setzen oder bei einem Hoster bereitstellen — `tool-ctrl.de`. diff --git a/public/assets/emblem-a.png b/public/assets/emblem-a.png new file mode 100644 index 0000000..6726f0e Binary files /dev/null and b/public/assets/emblem-a.png differ diff --git a/public/assets/emblem-b.png b/public/assets/emblem-b.png new file mode 100644 index 0000000..d0a9003 Binary files /dev/null and b/public/assets/emblem-b.png differ diff --git a/public/assets/emblem-black.svg b/public/assets/emblem-black.svg new file mode 100644 index 0000000..2a3d0b8 --- /dev/null +++ b/public/assets/emblem-black.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/emblem-white.svg b/public/assets/emblem-white.svg new file mode 100644 index 0000000..7a7ea7e --- /dev/null +++ b/public/assets/emblem-white.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/favicon.png b/public/assets/favicon.png new file mode 100644 index 0000000..39e00f1 Binary files /dev/null and b/public/assets/favicon.png differ diff --git a/public/assets/fonts/schibsted-grotesk-latin-ext.woff2 b/public/assets/fonts/schibsted-grotesk-latin-ext.woff2 new file mode 100644 index 0000000..42fd93c Binary files /dev/null and b/public/assets/fonts/schibsted-grotesk-latin-ext.woff2 differ diff --git a/public/assets/fonts/schibsted-grotesk-latin.woff2 b/public/assets/fonts/schibsted-grotesk-latin.woff2 new file mode 100644 index 0000000..dbca3fd Binary files /dev/null and b/public/assets/fonts/schibsted-grotesk-latin.woff2 differ diff --git a/public/assets/icons/chart.svg b/public/assets/icons/chart.svg new file mode 100644 index 0000000..32e1428 --- /dev/null +++ b/public/assets/icons/chart.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/columns.svg b/public/assets/icons/columns.svg new file mode 100644 index 0000000..306f930 --- /dev/null +++ b/public/assets/icons/columns.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/icons/download.svg b/public/assets/icons/download.svg new file mode 100644 index 0000000..56d7b5b --- /dev/null +++ b/public/assets/icons/download.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/icons/envelope.svg b/public/assets/icons/envelope.svg new file mode 100644 index 0000000..df5f1b9 --- /dev/null +++ b/public/assets/icons/envelope.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/graph.svg b/public/assets/icons/graph.svg new file mode 100644 index 0000000..6e613af --- /dev/null +++ b/public/assets/icons/graph.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/lock.svg b/public/assets/icons/lock.svg new file mode 100644 index 0000000..171dee2 --- /dev/null +++ b/public/assets/icons/lock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/notifications.svg b/public/assets/icons/notifications.svg new file mode 100644 index 0000000..45e2b45 --- /dev/null +++ b/public/assets/icons/notifications.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/settings.svg b/public/assets/icons/settings.svg new file mode 100644 index 0000000..6e1df48 --- /dev/null +++ b/public/assets/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/share.svg b/public/assets/icons/share.svg new file mode 100644 index 0000000..585ea35 --- /dev/null +++ b/public/assets/icons/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/icons/text_files.svg b/public/assets/icons/text_files.svg new file mode 100644 index 0000000..7829196 --- /dev/null +++ b/public/assets/icons/text_files.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/icons/time.svg b/public/assets/icons/time.svg new file mode 100644 index 0000000..a916e2f --- /dev/null +++ b/public/assets/icons/time.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/upload.svg b/public/assets/icons/upload.svg new file mode 100644 index 0000000..976d08d --- /dev/null +++ b/public/assets/icons/upload.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/assets/icons/user_2.svg b/public/assets/icons/user_2.svg new file mode 100644 index 0000000..8f961d6 --- /dev/null +++ b/public/assets/icons/user_2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/icons/view_eye.svg b/public/assets/icons/view_eye.svg new file mode 100644 index 0000000..a376f6f --- /dev/null +++ b/public/assets/icons/view_eye.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/logo-a.png b/public/assets/logo-a.png new file mode 100644 index 0000000..11d53af Binary files /dev/null and b/public/assets/logo-a.png differ diff --git a/public/assets/logo-b.png b/public/assets/logo-b.png new file mode 100644 index 0000000..f744832 Binary files /dev/null and b/public/assets/logo-b.png differ diff --git a/public/assets/logo-black.svg b/public/assets/logo-black.svg new file mode 100644 index 0000000..dfccc6b --- /dev/null +++ b/public/assets/logo-black.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/logo-white.svg b/public/assets/logo-white.svg new file mode 100644 index 0000000..f802b1b --- /dev/null +++ b/public/assets/logo-white.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/datenschutz.html b/public/datenschutz.html new file mode 100644 index 0000000..4eb760a --- /dev/null +++ b/public/datenschutz.html @@ -0,0 +1,163 @@ + + + + + +Datenschutzerklärung — ToolCTRL + + + + + + + + + +
+

Datenschutzerklärung

+ + + +

1. Verantwortlicher

+

+ Verantwortlich für die Datenverarbeitung auf dieser Website ist: +

+

+ R&D² UG (haftungsbeschränkt)
+ Orbinstraße 1
+ 76646 Bruchsal
+ E-Mail: admin@tool-ctrl.de +

+

Die vollständigen Anbieterangaben finden Sie im Impressum.

+ +

2. Hosting und Server-Logfiles

+

+ Diese Website wird bei einem externen Dienstleister gehostet: + [ Hosting-Anbieter — vom Betreiber zu ergänzen ]. + Beim Aufruf der Website verarbeitet dieser automatisch technische + Informationen, die Ihr Browser übermittelt (sogenannte Server-Logfiles): +

+
    +
  • IP-Adresse des anfragenden Geräts
  • +
  • Datum und Uhrzeit des Zugriffs
  • +
  • aufgerufene Seite beziehungsweise Datei
  • +
  • übertragene Datenmenge
  • +
  • verwendeter Browsertyp und Betriebssystem
  • +
+

+ Diese Daten dienen ausschließlich der technischen Auslieferung der Website, + der Systemsicherheit und der Fehleranalyse. Rechtsgrundlage ist unser + berechtigtes Interesse an einem sicheren und stabilen Betrieb + (Art. 6 Abs. 1 lit. f DSGVO). Eine Zusammenführung dieser Daten mit anderen + Datenquellen findet nicht statt. +

+ +

3. Kontaktformular

+

+ Wenn Sie uns über das Kontaktformular eine Anfrage senden, verarbeiten wir + die von Ihnen eingegebenen Daten: +

+
    +
  • Name (Pflichtangabe)
  • +
  • E-Mail-Adresse (Pflichtangabe)
  • +
  • Ihre Nachricht (Pflichtangabe)
  • +
  • Firma (freiwillig)
  • +
  • Telefonnummer (freiwillig)
  • +
+

+ Diese Daten verwenden wir ausschließlich, um Ihre Anfrage zu bearbeiten und + mit Ihnen Kontakt aufzunehmen. Rechtsgrundlage ist Art. 6 Abs. 1 lit. b + DSGVO (Durchführung vorvertraglicher Maßnahmen) sowie unser berechtigtes + Interesse an der Beantwortung Ihrer Anfrage (Art. 6 Abs. 1 lit. f DSGVO). + Soweit Sie mit dem Absenden ausdrücklich einwilligen, ist zusätzlich + Art. 6 Abs. 1 lit. a DSGVO Rechtsgrundlage. Eine erteilte Einwilligung + können Sie jederzeit mit Wirkung für die Zukunft widerrufen. +

+

+ Die Angabe der Pflichtfelder ist erforderlich, um Ihre Anfrage bearbeiten + zu können; die übrigen Angaben sind freiwillig. +

+ +

4. Speicherdauer

+

+ Wir speichern personenbezogene Daten nur so lange, wie es für die genannten + Zwecke erforderlich ist. Daten aus Kontaktanfragen löschen wir, sobald Ihre + Anfrage abschließend bearbeitet ist und keine gesetzlichen + Aufbewahrungspflichten entgegenstehen. Server-Logfiles werden nach kurzer + Zeit automatisch gelöscht. +

+ +

5. Keine Cookies, kein Tracking

+

+ Diese Website verwendet keine Cookies, kein Tracking und + keine Analyse- oder Marketing-Dienste. Es werden keine Profile über Ihr + Verhalten erstellt. Verwendete Schriftarten werden lokal von unserem eigenen + Server geladen; eine Verbindung zu Servern Dritter (etwa Google Fonts) + besteht dabei nicht. +

+ +

6. SSL-/TLS-Verschlüsselung

+

+ Diese Website nutzt aus Sicherheitsgründen eine Verschlüsselung (SSL/TLS). + Eine verschlüsselte Verbindung erkennen Sie am „https://" in der Adresszeile + Ihres Browsers. +

+ +

7. Ihre Rechte

+

+ Hinsichtlich Ihrer personenbezogenen Daten stehen Ihnen folgende Rechte zu: +

+
    +
  • Auskunft über die zu Ihnen gespeicherten Daten (Art. 15 DSGVO)
  • +
  • Berichtigung unrichtiger Daten (Art. 16 DSGVO)
  • +
  • Löschung Ihrer Daten (Art. 17 DSGVO)
  • +
  • Einschränkung der Verarbeitung (Art. 18 DSGVO)
  • +
  • Datenübertragbarkeit (Art. 20 DSGVO)
  • +
  • Widerspruch gegen die Verarbeitung (Art. 21 DSGVO)
  • +
  • Widerruf einer erteilten Einwilligung (Art. 7 Abs. 3 DSGVO)
  • +
+

+ Zur Ausübung Ihrer Rechte genügt eine formlose Nachricht an + admin@tool-ctrl.de. +

+ +

8. Beschwerderecht bei der Aufsichtsbehörde

+

+ Sie haben das Recht, sich bei einer Datenschutz-Aufsichtsbehörde über die + Verarbeitung Ihrer personenbezogenen Daten zu beschweren (Art. 77 DSGVO). + Zuständig ist in der Regel die Aufsichtsbehörde Ihres üblichen + Aufenthaltsorts oder die des Verantwortlichen. +

+ +

9. Aktualität dieser Erklärung

+

+ Diese Datenschutzerklärung hat den Stand Mai 2026. Durch die + Weiterentwicklung der Website oder geänderte gesetzliche Vorgaben kann eine + Anpassung erforderlich werden. +

+
+ + + + + diff --git a/public/impressum.html b/public/impressum.html new file mode 100644 index 0000000..5664cab --- /dev/null +++ b/public/impressum.html @@ -0,0 +1,104 @@ + + + + + +Impressum — ToolCTRL + + + + + + + + + +
+

Impressum

+ + +

Anbieter

+

+ R&D² UG (haftungsbeschränkt)
+ Orbinstraße 1
+ 76646 Bruchsal
+ Deutschland +

+ +

Vertreten durch

+

+ [ Name der vertretungsberechtigten Person(en) — bei Gesellschaften ] +

+ +

Kontakt

+

+ Telefon: [ Telefonnummer ]
+ E-Mail: admin@tool-ctrl.de
+ Web: tool-ctrl.de +

+ +

Registereintrag

+

+ [ Registergericht — falls vorhanden, sonst entfällt dieser Abschnitt ]
+ [ Registernummer (z. B. HRB …) ] +

+ +

Umsatzsteuer-Identifikationsnummer

+

+ Umsatzsteuer-Identifikationsnummer gemäß § 27a Umsatzsteuergesetz:
+ [ USt-IdNr. — falls vorhanden, sonst entfällt dieser Abschnitt ] +

+ +

Verantwortlich für den Inhalt nach § 18 Abs. 2 MStV

+

+ [ Name der verantwortlichen Person ]
+ Orbinstraße 1
+ 76646 Bruchsal +

+ +

Haftung für Inhalte

+

+ Die Inhalte dieser Website wurden mit größtmöglicher Sorgfalt erstellt. + Für die Richtigkeit, Vollständigkeit und Aktualität der Inhalte können wir + jedoch keine Gewähr übernehmen. Als Diensteanbieter sind wir gemäß den + allgemeinen Gesetzen für eigene Inhalte auf diesen Seiten verantwortlich. +

+ +

Haftung für Links

+

+ Diese Website enthält Links zu externen Websites Dritter, auf deren Inhalte + wir keinen Einfluss haben. Für die Inhalte der verlinkten Seiten ist stets + der jeweilige Anbieter oder Betreiber verantwortlich. Eine permanente + inhaltliche Kontrolle der verlinkten Seiten ist ohne konkrete Anhaltspunkte + einer Rechtsverletzung nicht zumutbar. +

+ +

Urheberrecht

+

+ Die auf dieser Website veröffentlichten Inhalte unterliegen dem deutschen + Urheberrecht. Beiträge Dritter sind als solche gekennzeichnet. Jede + Vervielfältigung, Bearbeitung oder Verbreitung außerhalb der Grenzen des + Urheberrechts bedarf der vorherigen schriftlichen Zustimmung. +

+
+ + + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..d0b474d --- /dev/null +++ b/public/index.html @@ -0,0 +1,380 @@ + + + + + +ToolCTRL — from vision to version + + + + + + + + + + +
+ + +
+
+
+

+ Software und Automatisierung,
+ die einfach läuft. +

+

+ ToolCTRL plant, baut und betreibt individuelle Software — von der + Prozessautomatisierung über interne Tools bis zur Integration + bestehender Systeme. Schnell umgesetzt, sauber konstruiert, + zuverlässig im Betrieb. +

+ +
+ + + +
+
+ + +
+
+
+ Leistungen +

Die digitale Infrastruktur Ihres Betriebs.

+

+ Wir arbeiten dort, wo Tabellen, manuelle Schritte und getrennte + Systeme zu viel Zeit kosten. Daraus bauen wir eine tragfähige + Infrastruktur: Software, Automatisierung und Schnittstellen, + zugeschnitten auf Ihren Ablauf, nicht umgekehrt. +

+
+ +
+
+ +

Prozessautomatisierung

+

Wiederkehrende Abläufe werden zu verlässlichen Workflows, die + ohne manuelles Zutun ausgelöst, verarbeitet und dokumentiert + werden.

+
+
+ +

Individuelle Software

+

Anwendungen, die exakt zu Ihrem Geschäft passen. Schlank + gebaut, klar bedienbar und auf Dauer wartbar.

+
+
+ +

Dashboards & Datenflüsse

+

Verstreute Daten an einem Ort: aktuell, übersichtlich und + als Grundlage für schnelle Entscheidungen.

+
+
+ +

Schnittstellen & Integrationen

+

Bestehende Tools, APIs und Systeme greifen ineinander, statt + nebeneinander zu laufen.

+
+
+ +

Interne Tools

+

Werkzeuge für das eigene Team, die verstreute Tabellenlösungen + ablösen und mit Ihren Abläufen mitwachsen.

+
+
+ +

Betrieb & Weiterentwicklung

+

Nach dem Launch ist nicht Schluss: Wartung, Anpassung und + Ausbau aus einer Hand.

+
+
+
+
+ + +
+
+
+ Vorgehen +

In vier klaren Schritten.

+

+ Kein Wasserfall, kein Blindflug. Ein Ablauf, der Ergebnisse früh + sichtbar macht und Sie an jeder Stelle einbezieht. +

+
+ +
    +
  1. + 01 +

    Analyse

    +

    Wir sehen uns Ihren Ablauf an und legen offen, wo Zeit, Daten + und Klarheit verloren gehen.

    +
  2. +
  3. + 02 +

    Konzept

    +

    Ein konkreter Lösungsvorschlag mit Aufwand, Umfang und + erwartetem Nutzen, transparent kalkuliert.

    +
  4. +
  5. + 03 +

    Umsetzung

    +

    Wir bauen in kurzen Zyklen. Sie sehen früh ein funktionierendes + Ergebnis statt nur Folien.

    +
  6. +
  7. + 04 +

    Betrieb

    +

    Übergabe, Einweisung und laufende Pflege, damit die Lösung + dauerhaft trägt.

    +
  8. +
+
+
+ + +
+
+
+ Organisation +

Struktur, die in der Software steckt.

+

+ Ob Lead-Verarbeitung, Vertriebspipeline oder Projektboard: Wir + bauen die Software, in der Aufgaben, Status und Fortschritt + sichtbar werden und Abläufe automatisch weiterlaufen. +

+
    +
  • Lead- und Vertriebspipelines
  • +
  • Aufgaben- und Projektboards
  • +
  • Workflow-Automatisierung und Status-Tracking
  • +
+
+ + + +
+
+ + +
+
+
+ Kontakt +

Sprechen wir über Ihr Vorhaben.

+

+ Beschreiben Sie kurz, woran es hakt. Wir melden uns mit einer + ehrlichen Einschätzung. Unverbindlich und ohne Verkaufsdruck. +

+
+
+
E-Mail
+
admin@tool-ctrl.de
+
+ +
+
+ +
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ +

Mit dem Absenden stimmen Sie der Bearbeitung + Ihrer Angaben gemäß der + Datenschutzerklärung zu.

+
+ +
+
+
+ +
+ + + + + + + diff --git a/public/script.js b/public/script.js new file mode 100644 index 0000000..8d440a0 --- /dev/null +++ b/public/script.js @@ -0,0 +1,127 @@ +/* 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"); + 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"); + }); + }); + } + + /* ---- 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 admin@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"); + }); + }); +})(); diff --git a/public/styles.css b/public/styles.css new file mode 100644 index 0000000..1979281 --- /dev/null +++ b/public/styles.css @@ -0,0 +1,818 @@ +/* ============================================================ + ToolCTRL — Landing Page + Monochrom · Weiß / Schwarz · scharfe Kanten · Haarlinien + ============================================================ */ + +/* ---------- Hausschrift: Schibsted Grotesk (lokal gehostet) ---------- */ +@font-face { + font-family: "Schibsted Grotesk"; + font-style: normal; + font-weight: 400 800; + font-display: swap; + src: url(/assets/fonts/schibsted-grotesk-latin.woff2) format("woff2"); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, + U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, + U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: "Schibsted Grotesk"; + font-style: normal; + font-weight: 400 800; + font-display: swap; + src: url(/assets/fonts/schibsted-grotesk-latin-ext.woff2) format("woff2"); + unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, + U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, + U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, + U+A720-A7FF; +} + +:root { + --black: #0A0A0A; + --ink: #141414; + --gray-1:#5C5C5C; + --gray-2:#8C8C8C; + --line: #E2E2E2; + --line-strong:#0A0A0A; + --paper: #FFFFFF; + --paper-2:#F6F6F4; + --wrap: 1200px; + --grotesk: "Schibsted Grotesk", "Helvetica Neue", Arial, sans-serif; +} + +* { box-sizing: border-box; margin: 0; padding: 0; } + +html { scroll-behavior: smooth; } + +body { + font-family: var(--grotesk); + color: var(--ink); + background: var(--paper); + line-height: 1.55; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +img { display: block; max-width: 100%; } + +a { color: inherit; text-decoration: none; } + +.wrap { + width: 100%; + max-width: var(--wrap); + margin: 0 auto; + padding: 0 32px; +} + +/* ---------- Eyebrow — als Mini-Keycap ---------- */ +.eyebrow { + display: inline-block; + font-size: 10px; + letter-spacing: .16em; + text-transform: uppercase; + font-weight: 700; + color: var(--paper); + background: #1C1C1C; + padding: 7px 13px 8px; + border-radius: 4px; + box-shadow: 0 3px 0 0 #000000, inset 0 1.5px 0 0 rgba(255,255,255,.16); +} +.eyebrow-light { + color: var(--black); + background: var(--paper); + box-shadow: 0 3px 0 0 #B5B5B5, inset 0 1.5px 0 0 rgba(255,255,255,.9); +} + +/* ---------- Buttons — als physische Tastenkappen ---------- */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 14px; + font-weight: 600; + letter-spacing: .01em; + padding: 15px 28px; + border: 1.5px solid var(--black); + border-radius: 7px; + position: relative; + cursor: pointer; + white-space: nowrap; + transition: transform .09s ease, box-shadow .09s ease, + background .15s ease, color .15s ease; +} +/* Tastendruck — Kappe fährt auf die Grundfläche herunter */ +.btn:active { transform: translateY(8px); } + +/* schwarze Tastenkappe */ +.btn-solid { + background: linear-gradient(180deg, #3C3C3C 0%, #1A1A1A 52%, #0C0C0C 100%); + color: var(--paper); + box-shadow: + 0 8px 0 0 #000000, + 0 17px 18px -7px rgba(0,0,0,.55), + inset 0 2px 0 0 rgba(255,255,255,.24), + inset 0 -10px 13px -11px rgba(0,0,0,.95); +} +.btn-solid:hover { + background: linear-gradient(180deg, #484848 0%, #232323 52%, #121212 100%); +} +.btn-solid:active { + box-shadow: + 0 0 0 0 #000000, + 0 5px 8px -3px rgba(0,0,0,.5), + inset 0 2px 0 0 rgba(255,255,255,.16), + inset 0 -8px 11px -11px rgba(0,0,0,.9); +} + +/* weiße Tastenkappe */ +.btn-ghost { + background: linear-gradient(180deg, #FFFFFF 0%, #F1F1EF 58%, #E5E5E2 100%); + color: var(--black); + box-shadow: + 0 8px 0 0 #0A0A0A, + 0 17px 18px -8px rgba(0,0,0,.34), + inset 0 2px 0 0 rgba(255,255,255,.95), + inset 0 -9px 11px -11px rgba(0,0,0,.42); +} +.btn-ghost:hover { + background: linear-gradient(180deg, #FFFFFF 0%, #EAEAE7 58%, #DBDBD8 100%); +} +.btn-ghost:active { + box-shadow: + 0 0 0 0 #0A0A0A, + 0 5px 7px -3px rgba(0,0,0,.3), + inset 0 2px 0 0 rgba(255,255,255,.9), + inset 0 -7px 10px -11px rgba(0,0,0,.38); +} + +/* helle Tastenkappe (auf dunklem Grund) */ +.btn-light { + background: linear-gradient(180deg, #FFFFFF 0%, #EAEAEA 100%); + color: var(--black); + border-color: #FFFFFF; + box-shadow: + 0 8px 0 0 #6E6E6E, + 0 17px 18px -8px rgba(0,0,0,.45), + inset 0 2px 0 0 rgba(255,255,255,.95); +} +.btn-light:hover { + background: linear-gradient(180deg, #FFFFFF 0%, #DCDCDC 100%); +} +.btn-light:active { + box-shadow: + 0 0 0 0 #6E6E6E, + 0 5px 7px -3px rgba(0,0,0,.4), + inset 0 2px 0 0 rgba(255,255,255,.9); +} + +/* kompakte Variante (Header) — flachere Tiefe, passt in die Leiste */ +.btn-sm { padding: 11px 20px; font-size: 13px; } +.btn-sm.btn-solid { + box-shadow: + 0 5px 0 0 #000000, + 0 9px 10px -6px rgba(0,0,0,.5), + inset 0 2px 0 0 rgba(255,255,255,.22), + inset 0 -7px 10px -11px rgba(0,0,0,.9); +} +.btn-sm.btn-solid:active { + box-shadow: + 0 0 0 0 #000000, + 0 3px 5px -2px rgba(0,0,0,.45), + inset 0 2px 0 0 rgba(255,255,255,.16); +} +.btn-sm:active { transform: translateY(5px); } + +/* ============================================================ + HEADER + ============================================================ */ +.site-header { + position: sticky; + top: 0; + z-index: 50; + background: rgba(255,255,255,.94); + backdrop-filter: blur(8px); + border-bottom: 1px solid var(--line); +} +.header-inner { + display: flex; + align-items: center; + justify-content: space-between; + height: 74px; +} +.brand-logo { height: 26px; width: auto; } +.nav { + display: flex; + align-items: center; + gap: 34px; +} +.nav > a:not(.btn) { + font-size: 14px; + font-weight: 500; + color: var(--ink); + padding-bottom: 2px; + border-bottom: 1.5px solid transparent; +} +.nav > a:not(.btn):hover { border-bottom-color: var(--black); } + +.nav-toggle { + display: none; + flex-direction: column; + gap: 5px; + background: none; + border: 1.5px solid var(--black); + padding: 11px 10px; + cursor: pointer; +} +.nav-toggle span { + width: 20px; height: 1.5px; + background: var(--black); + display: block; +} +.mobile-nav { + /* Standardmäßig nie sichtbar — wird nur im Mobil-Breakpoint + per JS (hidden-Attribut entfernen) eingeblendet. */ + display: none; + flex-direction: column; + gap: 4px; + padding: 16px 32px 24px; + border-bottom: 1px solid var(--line); + background: var(--paper); +} +.mobile-nav a:not(.btn) { + padding: 12px 0; + font-size: 16px; + font-weight: 500; + border-bottom: 1px solid var(--line); +} +.mobile-nav .btn { margin-top: 12px; } + +/* ============================================================ + HERO + ============================================================ */ +.hero { + border-bottom: 1px solid var(--line); +} +.hero-grid { + display: grid; + grid-template-columns: 1.05fr .95fr; + gap: 64px; + align-items: center; + padding: 96px 32px 104px; +} +.hero-title { + font-size: clamp(36px, 4.6vw, 62px); + line-height: 1.05; + letter-spacing: -.022em; + font-weight: 700; + margin: 0 0 24px; +} +.hero-lead { + font-size: 17px; + color: var(--gray-1); + max-width: 33em; + margin-bottom: 32px; +} +.hero-actions { + display: flex; + gap: 14px; + flex-wrap: wrap; +} +.hero-tag { + margin-top: 34px; + padding-top: 22px; + border-top: 1px solid var(--line); + font-size: 13px; + letter-spacing: .04em; + color: var(--gray-2); + font-weight: 500; +} + +/* ---------- Hero visual: node graph ---------- */ +.visual-frame { + border: 1.5px solid var(--black); + background: var(--paper); + position: relative; +} +.visual-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + letter-spacing: .14em; + text-transform: uppercase; + font-weight: 600; + padding: 12px 16px; + border-bottom: 1px solid var(--line); +} +.visual-label .dot { + width: 7px; height: 7px; + background: var(--black); + border-radius: 50%; +} +.node-graph { display: block; width: 100%; height: auto; } + +.gnode rect { + fill: var(--paper); + stroke: var(--black); + stroke-width: 1.5; +} +.gnode text { + font-family: var(--grotesk); + font-size: 13px; + font-weight: 600; + fill: var(--black); +} +.cap-legend { + font-family: var(--grotesk); + font-size: 31px; + font-weight: 700; + letter-spacing: .01em; + fill: var(--paper); +} + +.visual-meta { + display: flex; + gap: 8px; + margin-top: 14px; + flex-wrap: wrap; +} +.visual-meta span { + font-size: 11px; + letter-spacing: .08em; + text-transform: uppercase; + font-weight: 600; + color: var(--gray-1); + border: 1px solid var(--line); + padding: 6px 10px; +} + +/* ============================================================ + SECTIONS (generic) + ============================================================ */ +.section { padding: 100px 0; } +.section-alt { background: var(--paper-2); } +.section-head { max-width: 620px; margin-bottom: 56px; } +.section-title { + font-size: clamp(28px, 3.2vw, 42px); + line-height: 1.12; + letter-spacing: -.02em; + font-weight: 700; + margin: 18px 0 16px; +} +.section-intro { + font-size: 16px; + color: var(--gray-1); +} + +/* ---------- Capability cards ---------- */ +.cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + border-top: 1px solid var(--line-strong); + border-left: 1px solid var(--line-strong); +} +.card { + border-right: 1px solid var(--line-strong); + border-bottom: 1px solid var(--line-strong); + padding: 36px 32px 40px; + background: var(--paper); + transition: background .15s ease; +} +.card:hover { background: var(--paper-2); } +/* Icon sitzt auf einem weißen 3D-Keycap */ +.icon-key { + display: inline-flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + margin-bottom: 28px; + background: var(--paper); + border: 1.6px solid var(--black); + border-radius: 8px; + box-shadow: 0 5px 0 0 var(--black); +} +.card-icon { + width: 27px; + height: 27px; +} +.card h3 { + font-size: 19px; + font-weight: 700; + letter-spacing: -.01em; + margin-bottom: 10px; +} +.card p { + font-size: 15px; + color: var(--gray-1); +} + +/* ---------- Steps ---------- */ +.steps { + display: grid; + grid-template-columns: repeat(4, 1fr); + list-style: none; + gap: 0; + border-top: 1px solid var(--line-strong); +} +.step { + padding: 34px 26px 38px; + border-right: 1px solid var(--line); +} +.step:last-child { border-right: none; } +.step-num { + display: block; + font-size: 13px; + font-weight: 700; + letter-spacing: .12em; + color: var(--gray-2); + margin-bottom: 26px; +} +.step h3 { + font-size: 20px; + font-weight: 700; + margin-bottom: 10px; +} +.step p { + font-size: 15px; + color: var(--gray-1); +} + +/* ============================================================ + ARBEITSWEISE + KANBAN + ============================================================ */ +.arbeit-grid { + display: grid; + grid-template-columns: .9fr 1.1fr; + gap: 64px; + align-items: center; +} +.checklist { + list-style: none; + margin-top: 28px; +} +.checklist li { + font-size: 15px; + padding: 14px 0 14px 28px; + border-top: 1px solid var(--line); + position: relative; + color: var(--ink); +} +.checklist li:last-child { border-bottom: 1px solid var(--line); } +.checklist li::before { + content: ""; + position: absolute; + left: 0; + top: 0; + bottom: 0; + margin-block: auto; + width: 12px; + height: 1.5px; + background: var(--black); +} + +/* Kanban — bewusst unscharf, nur angedeutet */ +.kanban-wrap { + position: relative; + border: 1.5px solid var(--black); + background: var(--paper); + overflow: hidden; + padding: 22px; +} +.kanban { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 14px; + filter: blur(3.5px); + opacity: .7; + user-select: none; +} +.kb-col { display: flex; flex-direction: column; gap: 12px; } +.kb-head { + font-size: 12px; + font-weight: 700; + letter-spacing: .06em; + text-transform: uppercase; + color: var(--gray-1); + padding-bottom: 8px; + border-bottom: 1.5px solid var(--black); +} +.kb-card { + border: 1px solid var(--black); + background: var(--paper); + padding: 14px 12px; + display: flex; + flex-direction: column; + gap: 8px; + min-height: 64px; +} +.kb-card.kb-active { background: var(--black); } +.kb-card.kb-active .kb-line { background: #6E6E6E; } +.kb-card.kb-done { border-style: dashed; } +.kb-line { + height: 8px; + background: #DADADA; + display: block; +} +.kb-line.w35 { width: 35%; } .kb-line.w40 { width: 40%; } +.kb-line.w45 { width: 45%; } .kb-line.w50 { width: 50%; } +.kb-line.w55 { width: 55%; } .kb-line.w60 { width: 60%; } +.kb-line.w65 { width: 65%; } .kb-line.w70 { width: 70%; } +.kb-line.w75 { width: 75%; } .kb-line.w80 { width: 80%; } +.kanban-fade { + position: absolute; + inset: 0; + background: + linear-gradient(180deg, rgba(255,255,255,0) 55%, rgba(255,255,255,.92) 100%), + radial-gradient(120% 80% at 50% 40%, rgba(255,255,255,0) 50%, rgba(255,255,255,.6) 100%); + pointer-events: none; +} + +/* ============================================================ + KONTAKT + ============================================================ */ +.section-dark { + background: var(--black); + color: var(--paper); +} +.kontakt-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 72px; + align-items: start; +} +.section-dark .section-intro { color: #A6A6A6; } +.contact-list { + margin-top: 34px; + border-top: 1px solid #2B2B2B; +} +.contact-list > div { + display: grid; + grid-template-columns: 90px 1fr; + gap: 16px; + padding: 16px 0; + border-bottom: 1px solid #2B2B2B; +} +.contact-list dt { + font-size: 11px; + letter-spacing: .14em; + text-transform: uppercase; + font-weight: 600; + color: var(--gray-2); + padding-top: 2px; +} +.contact-list dd { font-size: 15px; } +.contact-list a:hover { text-decoration: underline; } + +/* ---------- Form ---------- */ +.lead-form { + background: var(--paper-2); + color: var(--ink); + padding: 36px 34px; + border-radius: 14px; +} +.form-row { margin-bottom: 20px; } +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} +.form-row label { + display: block; + font-size: 12px; + font-weight: 700; + letter-spacing: .08em; + text-transform: uppercase; + color: var(--ink); + margin-bottom: 8px; +} +.form-row label span { color: var(--gray-2); } +.form-row input, +.form-row textarea { + width: 100%; + font-family: var(--grotesk); + font-size: 15px; + color: var(--ink); + background: var(--paper); + border: 1.5px solid var(--line); + border-radius: 7px; + padding: 12px 14px; + transition: border-color .15s ease; +} +.form-row input:focus, +.form-row textarea:focus { + outline: none; + border-color: var(--black); +} +.form-row textarea { resize: vertical; } +.form-row.invalid input, +.form-row.invalid textarea { border-color: #C0392B; } + +.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } + +.form-foot { + display: flex; + align-items: center; + gap: 18px; + margin-top: 6px; +} +.form-foot .btn { flex: none; } +.form-note { + font-size: 12px; + color: var(--gray-2); + line-height: 1.45; +} +.form-note a { + color: var(--ink); + text-decoration: underline; + text-underline-offset: 2px; +} +.form-status { + margin-top: 18px; + font-size: 14px; + font-weight: 600; + padding: 12px 14px; + border: 1.5px solid var(--black); + border-radius: 7px; +} +.form-status.ok { background: var(--black); color: var(--paper); } +.form-status.err { border-color: #C0392B; color: #C0392B; } + +/* ============================================================ + FOOTER + ============================================================ */ +.site-footer { + background: var(--black); + color: var(--paper); + padding-top: 64px; +} +.footer-inner { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + padding-bottom: 56px; +} +.footer-logo { height: 24px; width: auto; margin-bottom: 16px; } +.footer-brand p { + font-size: 13px; + color: var(--gray-2); + letter-spacing: .03em; +} +.footer-cols { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; +} +.footer-col { display: flex; flex-direction: column; gap: 12px; } +.footer-h { + font-size: 11px; + letter-spacing: .14em; + text-transform: uppercase; + font-weight: 700; + color: var(--gray-2); + margin-bottom: 4px; +} +.footer-col a { + font-size: 14px; + color: #D4D4D4; +} +.footer-col a:hover { color: var(--paper); } +.footer-base { + display: flex; + justify-content: space-between; + padding-top: 22px; + padding-bottom: 28px; + border-top: 1px solid #262626; + font-size: 12px; + color: var(--gray-2); +} +.footer-base a:hover { color: var(--paper); } + +/* ============================================================ + RESPONSIVE + ============================================================ */ +@media (max-width: 960px) { + .nav { display: none; } + .nav-toggle { display: flex; } + /* Mobil-Menü nur sichtbar, wenn JS das hidden-Attribut entfernt */ + .mobile-nav:not([hidden]) { display: flex; } + .hero-grid { grid-template-columns: 1fr; gap: 48px; padding: 60px 32px 72px; } + .cards { grid-template-columns: 1fr 1fr; } + .steps { grid-template-columns: 1fr 1fr; } + .step:nth-child(2) { border-right: none; } + .step:nth-child(1), .step:nth-child(2) { border-bottom: 1px solid var(--line); } + .arbeit-grid { grid-template-columns: 1fr; gap: 44px; } + .kontakt-grid { grid-template-columns: 1fr; gap: 44px; } + .footer-inner { grid-template-columns: 1fr; gap: 36px; } +} + +@media (max-width: 620px) { + .wrap { padding: 0 20px; } + .mobile-nav { padding: 16px 20px 24px; } + .section { padding: 64px 0; } + .section-head { margin-bottom: 36px; } + .hero-actions .btn { flex: 1 1 auto; } + .cards { grid-template-columns: 1fr; } + .card { border-right: 1px solid var(--line-strong); } + .steps { grid-template-columns: 1fr; } + .step { border-right: none; border-bottom: 1px solid var(--line); } + .step:last-child { border-bottom: none; } + .form-grid { grid-template-columns: 1fr; gap: 0; } + .form-foot { flex-direction: column; align-items: stretch; gap: 14px; } + .form-foot .btn { width: 100%; } + .lead-form { padding: 26px 22px; } + .footer-cols { grid-template-columns: 1fr; } + .footer-base { flex-direction: column; gap: 8px; } +} + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } +} + +/* ============================================================ + RECHTLICHE SEITEN (Impressum / Datenschutz) + ============================================================ */ +.legal-back { + font-size: 14px; + font-weight: 500; + color: var(--ink); + padding-bottom: 2px; + border-bottom: 1.5px solid transparent; +} +.legal-back:hover { border-bottom-color: var(--black); } + +.legal-main { + max-width: 780px; + margin: 0 auto; + padding: 64px clamp(20px, 5vw, 32px) 88px; +} +.legal-main h1 { + font-size: clamp(30px, 4vw, 44px); + line-height: 1.1; + letter-spacing: -.02em; + font-weight: 700; + margin-bottom: 14px; +} +.legal-intro { + font-size: 16px; + color: var(--gray-1); + margin-bottom: 10px; +} +.legal-updated { + font-size: 13px; + color: var(--gray-2); + padding-bottom: 28px; + margin-bottom: 8px; + border-bottom: 1px solid var(--line); +} +.legal-main h2 { + font-size: 19px; + font-weight: 700; + letter-spacing: -.01em; + margin: 38px 0 12px; +} +.legal-main p { + font-size: 15px; + line-height: 1.7; + color: var(--gray-1); + margin-bottom: 12px; +} +.legal-main ul { + margin: 0 0 12px; + padding-left: 20px; +} +.legal-main li { + font-size: 15px; + line-height: 1.7; + color: var(--gray-1); + margin-bottom: 5px; +} +.legal-main a { + color: var(--ink); + text-decoration: underline; + text-underline-offset: 2px; +} +.legal-main strong { color: var(--ink); font-weight: 700; } + +/* Platzhalter — vor dem Live-Gang zu ersetzen */ +.legal-tbd { + background: var(--paper-2); + border: 1px dashed var(--black); + padding: 1px 7px; + font-weight: 600; + color: var(--ink); +} + +.legal-footer { + background: var(--black); + color: var(--gray-2); + padding: 26px 0; +} +.legal-footer-inner { + display: flex; + justify-content: space-between; + gap: 12px 24px; + font-size: 13px; + flex-wrap: wrap; +} +.legal-footer a { color: #D4D4D4; } +.legal-footer a:hover { color: var(--paper); } diff --git a/server.js b/server.js new file mode 100644 index 0000000..3974a5c --- /dev/null +++ b/server.js @@ -0,0 +1,218 @@ +/* ============================================================ + ToolCTRL — Landing Page Server + Abhängigkeitsfrei (nur Node-Standardbibliothek). + Liefert /public und nimmt Leads unter POST /api/lead entgegen. + ============================================================ */ + +"use strict"; + +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +const ROOT = __dirname; +const PUBLIC_DIR = path.join(ROOT, "public"); +const LEADS_DIR = path.join(ROOT, "leads"); +const LEADS_JSON = path.join(LEADS_DIR, "leads.ndjson"); +const LEADS_CSV = path.join(LEADS_DIR, "leads.csv"); + +/* Bevorzugte Ports — der erste freie wird genutzt. */ +const PORT_CANDIDATES = [4317, 4318, 4417, 5317, 8317, 0]; +const HOST = "127.0.0.1"; + +const MIME = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".ico": "image/x-icon", + ".json": "application/json; charset=utf-8", + ".woff2": "font/woff2" +}; + +/* ---------- Lead-Speicher vorbereiten ---------- */ +function ensureLeadStore() { + if (!fs.existsSync(LEADS_DIR)) fs.mkdirSync(LEADS_DIR, { recursive: true }); + if (!fs.existsSync(LEADS_CSV)) { + fs.writeFileSync( + LEADS_CSV, + "zeitpunkt,name,firma,email,telefon,nachricht,quelle\n", + "utf8" + ); + } +} + +function csvCell(value) { + const s = String(value == null ? "" : value); + return '"' + s.replace(/"/g, '""').replace(/\r?\n/g, " ") + '"'; +} + +function saveLead(lead) { + ensureLeadStore(); + fs.appendFileSync(LEADS_JSON, JSON.stringify(lead) + "\n", "utf8"); + const row = [ + lead.zeitpunkt, + lead.name, + lead.firma, + lead.email, + lead.telefon, + lead.nachricht, + lead.quelle + ] + .map(csvCell) + .join(","); + fs.appendFileSync(LEADS_CSV, row + "\n", "utf8"); +} + +/* ---------- Statische Dateien ---------- */ +function serveStatic(req, res) { + let urlPath = decodeURIComponent(req.url.split("?")[0]); + if (urlPath === "/") urlPath = "/index.html"; + + /* Path-Traversal verhindern */ + const filePath = path.normalize(path.join(PUBLIC_DIR, urlPath)); + if (!filePath.startsWith(PUBLIC_DIR)) { + res.writeHead(403).end("Forbidden"); + return; + } + + fs.stat(filePath, (err, stat) => { + if (err || !stat.isFile()) { + res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" }); + res.end("

404

Seite nicht gefunden.

"); + return; + } + const ext = path.extname(filePath).toLowerCase(); + res.writeHead(200, { + "Content-Type": MIME[ext] || "application/octet-stream", + "Cache-Control": "no-cache" + }); + fs.createReadStream(filePath).pipe(res); + }); +} + +/* ---------- Lead-Endpunkt ---------- */ +function handleLead(req, res) { + let body = ""; + let tooLarge = false; + + req.on("data", (chunk) => { + body += chunk; + if (body.length > 64 * 1024) { + tooLarge = true; + req.destroy(); + } + }); + + req.on("end", () => { + if (tooLarge) return sendJSON(res, 413, { error: "Anfrage zu groß." }); + + let data; + try { + data = JSON.parse(body || "{}"); + } catch (e) { + return sendJSON(res, 400, { error: "Ungültiges Format." }); + } + + /* Honeypot — befülltes Feld = Bot. Still mit OK quittieren. */ + if (data.website && String(data.website).trim() !== "") { + return sendJSON(res, 200, { ok: true }); + } + + const name = String(data.name || "").trim(); + const email = String(data.email || "").trim(); + const nachricht = String(data.nachricht || "").trim(); + + if (!name || !email || !nachricht) { + return sendJSON(res, 400, { + error: "Bitte Name, E-Mail und Nachricht angeben." + }); + } + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { + return sendJSON(res, 400, { error: "Bitte eine gültige E-Mail angeben." }); + } + + const lead = { + zeitpunkt: new Date().toISOString(), + name: name.slice(0, 200), + firma: String(data.firma || "").trim().slice(0, 200), + email: email.slice(0, 200), + telefon: String(data.telefon || "").trim().slice(0, 80), + nachricht: nachricht.slice(0, 4000), + quelle: String(data.quelle || "Landing Page").slice(0, 120) + }; + + try { + saveLead(lead); + } catch (e) { + console.error("[lead] Speichern fehlgeschlagen:", e.message); + return sendJSON(res, 500, { + error: "Speichern fehlgeschlagen. Bitte später erneut versuchen." + }); + } + + console.log( + "[lead] " + lead.zeitpunkt + " — " + lead.name + + (lead.firma ? " (" + lead.firma + ")" : "") + " <" + lead.email + ">" + ); + sendJSON(res, 200, { ok: true }); + }); +} + +function sendJSON(res, code, obj) { + res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(obj)); +} + +/* ---------- Router ---------- */ +const server = http.createServer((req, res) => { + const route = req.url.split("?")[0]; + + if (route === "/api/lead") { + if (req.method !== "POST") { + return sendJSON(res, 405, { error: "Methode nicht erlaubt." }); + } + return handleLead(req, res); + } + + if (req.method !== "GET" && req.method !== "HEAD") { + res.writeHead(405).end("Method Not Allowed"); + return; + } + serveStatic(req, res); +}); + +/* ---------- Freien Port suchen und starten ---------- */ +function listen(candidates) { + if (candidates.length === 0) { + console.error("Kein freier Port gefunden."); + process.exit(1); + } + const port = candidates[0]; + server.once("error", (err) => { + if (err.code === "EADDRINUSE") { + console.log("Port " + port + " belegt — versuche nächsten …"); + listen(candidates.slice(1)); + } else { + console.error("Serverfehler:", err.message); + process.exit(1); + } + }); + server.listen(port, HOST, () => { + const actual = server.address().port; + ensureLeadStore(); + console.log(""); + console.log(" ToolCTRL — Landing Page läuft"); + console.log(" ----------------------------------------"); + console.log(" URL: http://" + HOST + ":" + actual); + console.log(" Leads: " + LEADS_CSV); + console.log(" " + LEADS_JSON); + console.log(" Stop: Strg + C"); + console.log(""); + }); +} + +listen(PORT_CANDIDATES);