Anfrageformular fertig - vier Schritte, Zwischenspeicherung, echte Absendung
Masterplan S.10 "Projektanfrage ohne Informationsverlust" umgesetzt.
End-to-End gegen die echte Datenbank belegt: Anfrage A-2608-0002 liegt drin.
38/38 Browsertests, 45/45 Handy-Abnahme.
Aufbau: 4 Schritte + Zusammenfassung + Dankeseite.
- Zwischenspeicherung nach jedem Tastendruck. Geschlossener Tab, leerer
Akku oder versehentliches Zurueck kosten keine Arbeit. Entwuerfe
verfallen nach 30 Tagen -- ein halbes Jahr alter Entwurf verwirrt mehr
als er hilft.
- Folgefragen erscheinen nur passend zur Auswahl und werden beim
Zurueckwechseln NICHT mitgeschickt, sonst landen alte Shop-Antworten in
einer Onepager-Anfrage.
- Die Zusammenfassung wird aus den Feldern gelesen, nicht aus einem
nebenher gepflegten Objekt -- sie kann dadurch nie etwas anderes
behaupten als das, was tatsaechlich abgeschickt wird.
- Telefon wird zur Pflicht, sobald "Per Telefon" gewaehlt ist. Ein Wunsch,
der nicht erfuellbar ist, ist schlimmer als eine Pflichtangabe.
Drei Fehler, die erst der Test gefunden hat:
1. Der "Zurueck"-Knopf stand auch auf Schritt 1 da. Ursache: das
hidden-Attribut wird nur ueber "display:none" umgesetzt und verliert
gegen jedes eigene display -- und Knoepfe sind inline-flex. Zentral
behoben ueber ".wd [hidden] { display:none !important }".
2. Nach erfolgreichem Absenden legte zeigeSeite() den geloeschten Entwurf
sofort wieder an. Beim naechsten Besuch haette eine bereits gesendete
Anfrage erneut im Formular gestanden.
3. Der Server meldet bei Spamverdacht bewusst Erfolg ohne Nummer. Ein
echter Mensch, der zufaellig sehr schnell war, haette eine Dankeseite
mit "—" gesehen und auf eine Antwort gewartet, die nie kommt. Jetzt
ein ehrlicher Hinweis mit zweitem Weg, und der Entwurf bleibt erhalten.
Pruefskripte geschaerft:
- i18n-Pruefung liest jetzt auch assets/js/wd-<seite>.js, sonst meldet sie
reihenweise "unbenutzt" fuer Schluessel, die aus dem Seitenskript kommen.
- Handy-Abnahme ueberspringt absichtlich unsichtbare Eingabefelder
(Auswahlkacheln, Honigtopf). Fehlalarme sind das Ende jeder Pruefung,
weil man sie irgendwann pauschal ignoriert.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -197,6 +197,25 @@ body.wd::after {
|
|||||||
}
|
}
|
||||||
.wd-skip:focus { left: 0; }
|
.wd-skip:focus { left: 0; }
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------
|
||||||
|
[hidden] MUSS gewinnen.
|
||||||
|
|
||||||
|
Das hidden-Attribut wird vom Browser nur mit "display: none" aus dem
|
||||||
|
Standard-Stylesheet umgesetzt — jede eigene display-Angabe schlaegt es.
|
||||||
|
Und genau die haben hier viele Elemente: Knoepfe sind inline-flex,
|
||||||
|
Karten sind flex, Raster sind grid.
|
||||||
|
|
||||||
|
Folge ohne diese Regel: el.hidden = true im JavaScript hat schlicht
|
||||||
|
keine Wirkung, und das Element bleibt sichtbar. Genau so aufgefallen im
|
||||||
|
Anfrageformular (22.08.2026) -- der "Zurueck"-Knopf stand auch auf dem
|
||||||
|
ersten Schritt da, obwohl es dorthin nichts zurueckzugehen gibt.
|
||||||
|
|
||||||
|
Das !important ist hier ausnahmsweise richtig: "versteckt" ist eine
|
||||||
|
Aussage ueber den Zustand, nicht ueber die Gestaltung, und darf von
|
||||||
|
keiner Gestaltungsregel ueberstimmt werden.
|
||||||
|
--------------------------------------------------------------------- */
|
||||||
|
.wd [hidden] { display: none !important; }
|
||||||
|
|
||||||
.wd-visuell-versteckt {
|
.wd-visuell-versteckt {
|
||||||
position: absolute; width: 1px; height: 1px; overflow: hidden;
|
position: absolute; width: 1px; height: 1px; overflow: hidden;
|
||||||
clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
|
clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
|
||||||
|
|||||||
@@ -292,6 +292,16 @@ window.I18N_WD = {
|
|||||||
fr: "Note-le — avec ce numéro, ta demande est retrouvée immédiatement si tu nous relances.",
|
fr: "Note-le — avec ce numéro, ta demande est retrouvée immédiatement si tu nous relances.",
|
||||||
pt: "Aponta-o — com este número o teu pedido é encontrado de imediato se voltares a contactar.",
|
pt: "Aponta-o — com este número o teu pedido é encontrado de imediato se voltares a contactar.",
|
||||||
},
|
},
|
||||||
|
/* Fallback, wenn der Server aus Spamschutzgruenden keine Nummer
|
||||||
|
vergeben hat. Bewusst ohne Schuldzuweisung und mit zweitem Weg. */
|
||||||
|
af_danke_ohne_nummer: {
|
||||||
|
de: "Falls du innerhalb von zwei Tagen nichts hörst, schreib mir bitte kurz direkt eine E-Mail — dann ist unterwegs etwas verloren gegangen.",
|
||||||
|
"de-CH": "Falls du innert zwei Täg nüt ghörsch, schriib mer bitte churz e E-Mail — denn isch underwägs öppis verlore gange.",
|
||||||
|
en: "If you do not hear back within two days, please send me a short e-mail directly — then something got lost along the way.",
|
||||||
|
fr: "Si tu n'as pas de nouvelles sous deux jours, envoie-moi un court e-mail — c'est que quelque chose s'est perdu en route.",
|
||||||
|
pt: "Se não tiveres resposta em dois dias, envia-me um e-mail curto — é porque algo se perdeu pelo caminho.",
|
||||||
|
},
|
||||||
|
|
||||||
af_danke_zurueck: { de: "Zurück zur Startseite", "de-CH": "Zrugg zur Startsite", en: "Back to the home page", fr: "Retour à l'accueil", pt: "Voltar ao início" },
|
af_danke_zurueck: { de: "Zurück zur Startseite", "de-CH": "Zrugg zur Startsite", en: "Back to the home page", fr: "Retour à l'accueil", pt: "Voltar ao início" },
|
||||||
|
|
||||||
/* --- Fehlermeldungen --- */
|
/* --- Fehlermeldungen --- */
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
wd-anfrage.js — Steuerung des mehrstufigen Anfrageformulars
|
||||||
|
(Masterplan S.10 "Projektanfrage ohne Informationsverlust")
|
||||||
|
|
||||||
|
Aufbau: 4 Eingabeschritte + Zusammenfassung + Dankeseite.
|
||||||
|
|
||||||
|
Leitgedanken:
|
||||||
|
- Nichts geht verloren. Jede Eingabe landet sofort im Browserspeicher;
|
||||||
|
ein geschlossener Tab, ein leerer Akku oder ein versehentliches
|
||||||
|
Zurück kostet keine Arbeit.
|
||||||
|
- Geprüft wird beim Weitergehen, nicht beim Tippen. Wer noch mitten im
|
||||||
|
Wort ist, soll nicht angemeckert werden.
|
||||||
|
- Die Zusammenfassung zeigt, was WIRKLICH abgeschickt wird — aus den
|
||||||
|
Feldern gelesen, nicht aus dem, was das Skript zu wissen glaubt.
|
||||||
|
===================================================================== */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var SPEICHER = "wd-anfrage-entwurf";
|
||||||
|
var LETZTE_SEITE_EINGABE = 4; // danach kommt die Zusammenfassung
|
||||||
|
var SEITE_ZUSAMMENFASSUNG = 5;
|
||||||
|
var SEITE_DANKE = 6;
|
||||||
|
|
||||||
|
var form, seiten, zaehler, marken, meldung, knopfLeiste, btnZurueck, btnWeiter;
|
||||||
|
var aktuelleSeite = 1;
|
||||||
|
var geoeffnetAm = Date.now();
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Werte einsammeln. Bewusst direkt aus dem Formular statt aus einem
|
||||||
|
nebenher gepflegten Objekt: so kann die Anzeige nie etwas anderes
|
||||||
|
behaupten als das, was tatsächlich in den Feldern steht.
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function werte() {
|
||||||
|
var d = new FormData(form);
|
||||||
|
var funktionen = d.getAll("funktionen");
|
||||||
|
|
||||||
|
/* Kategoriespezifische Felder tragen das Präfix "d_" und wandern
|
||||||
|
gesammelt in ein Unterobjekt. Neue Folgefragen brauchen dadurch
|
||||||
|
weder hier noch in der Datenbank eine Änderung. */
|
||||||
|
var details = {};
|
||||||
|
form.querySelectorAll("[name^='d_']").forEach(function (el) {
|
||||||
|
/* Nur Felder mitnehmen, die gerade sichtbar sind — sonst schickt
|
||||||
|
jemand, der von "Shop" auf "Onepager" zurückwechselt, immer noch
|
||||||
|
seine alten Shop-Antworten mit. */
|
||||||
|
if (el.closest("[hidden]")) return;
|
||||||
|
var wert = (el.value || "").trim();
|
||||||
|
if (wert) details[el.name.slice(2)] = wert;
|
||||||
|
});
|
||||||
|
|
||||||
|
var vorhanden = {};
|
||||||
|
form.querySelectorAll("[name^='v_']").forEach(function (el) {
|
||||||
|
if (el.checked) vorhanden[el.name.slice(2)] = el.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
paket: d.get("paket") || "",
|
||||||
|
ziel: (d.get("ziel") || "").trim(),
|
||||||
|
zielgruppe: (d.get("zielgruppe") || "").trim(),
|
||||||
|
seiten: (d.get("seiten") || "").trim(),
|
||||||
|
funktionen: funktionen,
|
||||||
|
details: details,
|
||||||
|
hatTexte: vorhanden.texte || "",
|
||||||
|
hatBilder: vorhanden.bilder || "",
|
||||||
|
hatLogo: vorhanden.logo || "",
|
||||||
|
hatDomain: vorhanden.domain || "",
|
||||||
|
budget: d.get("budget") || "",
|
||||||
|
termin: (d.get("termin") || "").trim(),
|
||||||
|
name: (d.get("name") || "").trim(),
|
||||||
|
email: (d.get("email") || "").trim(),
|
||||||
|
telefon: (d.get("telefon") || "").trim(),
|
||||||
|
kontaktweg: d.get("kontaktweg") || "email",
|
||||||
|
bemerkung: (d.get("bemerkung") || "").trim(),
|
||||||
|
datenschutz: !!d.get("datenschutz"),
|
||||||
|
website: (d.get("website") || "").trim(), // Honigtopf
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Zwischenspeicherung (Masterplan S.10: "Zwischenspeicherung verhindert
|
||||||
|
Datenverlust")
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function speichern() {
|
||||||
|
try {
|
||||||
|
var w = werte();
|
||||||
|
delete w.website; // Honigtopf nie speichern
|
||||||
|
sessionStorageSchreiben(JSON.stringify({ stand: w, seite: aktuelleSeite, zeit: Date.now() }));
|
||||||
|
} catch (e) { /* Speicher voll oder gesperrt — kein Grund abzubrechen */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionStorageSchreiben(text) {
|
||||||
|
/* localStorage, NICHT sessionStorage: der Entwurf soll auch morgen
|
||||||
|
noch da sein. Das ist der ganze Zweck. */
|
||||||
|
try { localStorage.setItem(SPEICHER, text); } catch (e) { /* egal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function gespeichertesLesen() {
|
||||||
|
try {
|
||||||
|
var roh = localStorage.getItem(SPEICHER);
|
||||||
|
if (!roh) return null;
|
||||||
|
var daten = JSON.parse(roh);
|
||||||
|
/* Entwürfe verfallen nach 30 Tagen. Ein halbes Jahr alter Entwurf
|
||||||
|
verwirrt mehr, als er hilft. */
|
||||||
|
if (!daten.zeit || Date.now() - daten.zeit > 30 * 24 * 3600 * 1000) {
|
||||||
|
localStorage.removeItem(SPEICHER);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return daten;
|
||||||
|
} catch (e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function entwurfLoeschen() {
|
||||||
|
try { localStorage.removeItem(SPEICHER); } catch (e) { /* egal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function wiederherstellen(daten) {
|
||||||
|
var w = daten.stand || {};
|
||||||
|
function setz(name, wert) {
|
||||||
|
var el = form.querySelector("[name='" + name + "']");
|
||||||
|
if (el && typeof wert === "string") el.value = wert;
|
||||||
|
}
|
||||||
|
setz("ziel", w.ziel); setz("zielgruppe", w.zielgruppe); setz("seiten", w.seiten);
|
||||||
|
setz("termin", w.termin); setz("name", w.name); setz("email", w.email);
|
||||||
|
setz("telefon", w.telefon); setz("bemerkung", w.bemerkung);
|
||||||
|
var budget = form.querySelector("[name='budget']");
|
||||||
|
if (budget && w.budget) budget.value = w.budget;
|
||||||
|
|
||||||
|
if (w.paket) {
|
||||||
|
var p = form.querySelector("[name='paket'][value='" + w.paket + "']");
|
||||||
|
if (p) p.checked = true;
|
||||||
|
}
|
||||||
|
if (w.kontaktweg) {
|
||||||
|
var k = form.querySelector("[name='kontaktweg'][value='" + w.kontaktweg + "']");
|
||||||
|
if (k) k.checked = true;
|
||||||
|
}
|
||||||
|
(w.funktionen || []).forEach(function (f) {
|
||||||
|
var el = form.querySelector("[name='funktionen'][value='" + f + "']");
|
||||||
|
if (el) el.checked = true;
|
||||||
|
});
|
||||||
|
Object.keys(w.details || {}).forEach(function (k) {
|
||||||
|
var el = form.querySelector("[name='d_" + k + "']");
|
||||||
|
if (el) el.value = w.details[k];
|
||||||
|
});
|
||||||
|
[["texte", w.hatTexte], ["bilder", w.hatBilder], ["logo", w.hatLogo], ["domain", w.hatDomain]]
|
||||||
|
.forEach(function (paar) {
|
||||||
|
if (!paar[1]) return;
|
||||||
|
var el = form.querySelector("[name='v_" + paar[0] + "'][value='" + paar[1] + "']");
|
||||||
|
if (el) el.checked = true;
|
||||||
|
});
|
||||||
|
if (w.datenschutz) {
|
||||||
|
var ds = form.querySelector("[name='datenschutz']");
|
||||||
|
if (ds) ds.checked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Tabelle "Was hast du schon?" — per JS gebaut, damit die vier Zeilen
|
||||||
|
nicht viermal fast gleich im HTML stehen.
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function baueVorhandenTabelle() {
|
||||||
|
var koerper = document.getElementById("af-vorhanden");
|
||||||
|
if (!koerper) return;
|
||||||
|
var zeilen = [
|
||||||
|
["texte", "af3_texte"],
|
||||||
|
["bilder", "af3_bilder"],
|
||||||
|
["logo", "af3_logo"],
|
||||||
|
["domain", "af3_domain"],
|
||||||
|
];
|
||||||
|
var antworten = [["ja", "af3_ja"], ["teilweise", "af3_teilweise"], ["nein", "af3_nein"]];
|
||||||
|
|
||||||
|
koerper.innerHTML = zeilen.map(function (z) {
|
||||||
|
var knoepfe = antworten.map(function (a) {
|
||||||
|
return '<label class="wd-wahl-kachel" style="display:inline-block;margin:0 .3rem .3rem 0">' +
|
||||||
|
'<input type="radio" name="v_' + z[0] + '" value="' + a[0] + '" />' +
|
||||||
|
'<span class="wd-wahl-inhalt" style="padding:.55rem .8rem .55rem 2.4rem">' +
|
||||||
|
'<span class="wd-wahl-titel" style="font-size:.9rem" data-i18n="' + a[1] + '">' +
|
||||||
|
window.WD.t(a[1]) + "</span></span></label>";
|
||||||
|
}).join("");
|
||||||
|
return '<tr><td style="white-space:nowrap;vertical-align:middle"><strong data-i18n="' + z[1] + '">' +
|
||||||
|
window.WD.t(z[1]) + "</strong></td><td>" + knoepfe + "</td></tr>";
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Prüfung je Schritt
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function fehlerSetzen(feldId, text) {
|
||||||
|
var feld = document.getElementById(feldId);
|
||||||
|
if (!feld) return;
|
||||||
|
feld.classList.toggle("wd-feld--fehler", !!text);
|
||||||
|
var p = feld.querySelector(".wd-feld-fehler");
|
||||||
|
if (p) p.textContent = text || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function fehlerLeeren() {
|
||||||
|
form.querySelectorAll(".wd-feld--fehler").forEach(function (f) {
|
||||||
|
f.classList.remove("wd-feld--fehler");
|
||||||
|
var p = f.querySelector(".wd-feld-fehler");
|
||||||
|
if (p) p.textContent = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dieselbe absichtlich einfache Prüfung wie auf dem Server: Die einzige
|
||||||
|
zuverlässige Kontrolle ist ohnehin, ob die Antwortmail ankommt. Eine
|
||||||
|
strenge Regel lehnt vor allem gültige Adressen ab. */
|
||||||
|
function istEmail(s) {
|
||||||
|
var t = String(s || "").trim().toLowerCase();
|
||||||
|
var teile = t.split("@");
|
||||||
|
return teile.length === 2 && !!teile[0] && teile[1].indexOf(".") > 0 && !/\s/.test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruefeSeite(nr) {
|
||||||
|
fehlerLeeren();
|
||||||
|
var w = werte();
|
||||||
|
var fehler = [];
|
||||||
|
|
||||||
|
if (nr === 1) {
|
||||||
|
if (!w.paket) { fehlerSetzen("feld-paket", window.WD.t("af_fehler_paket")); fehler.push("feld-paket"); }
|
||||||
|
if (!w.ziel) { fehlerSetzen("feld-ziel", window.WD.t("af_fehler_ziel")); fehler.push("feld-ziel"); }
|
||||||
|
}
|
||||||
|
if (nr === 4) {
|
||||||
|
if (!w.name) { fehlerSetzen("feld-name", window.WD.t("af_fehler_name")); fehler.push("feld-name"); }
|
||||||
|
if (!istEmail(w.email)) { fehlerSetzen("feld-email", window.WD.t("af_fehler_email")); fehler.push("feld-email"); }
|
||||||
|
/* Wer telefonisch kontaktiert werden möchte, muss eine Nummer
|
||||||
|
angeben — sonst kann der Wunsch gar nicht erfüllt werden. */
|
||||||
|
if (w.kontaktweg === "telefon" && !w.telefon) {
|
||||||
|
fehlerSetzen("feld-telefon", window.WD.t("af_fehler_telefon")); fehler.push("feld-telefon");
|
||||||
|
}
|
||||||
|
if (!w.datenschutz) {
|
||||||
|
fehlerSetzen("feld-datenschutz", window.WD.t("af_fehler_datenschutz")); fehler.push("feld-datenschutz");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fehler.length) {
|
||||||
|
window.WD.meldung(meldung, window.WD.t("af_fehler_schritt"), "fehler");
|
||||||
|
var erstes = document.getElementById(fehler[0]);
|
||||||
|
if (erstes) {
|
||||||
|
erstes.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||||
|
var eingabe = erstes.querySelector("input, textarea, select");
|
||||||
|
if (eingabe) setTimeout(function () { eingabe.focus({ preventScroll: true }); }, 300);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
window.WD.meldungAus(meldung);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Navigation
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function zeigeSeite(nr) {
|
||||||
|
aktuelleSeite = nr;
|
||||||
|
seiten.forEach(function (s) {
|
||||||
|
s.classList.toggle("af-sichtbar", Number(s.dataset.seite) === nr);
|
||||||
|
});
|
||||||
|
|
||||||
|
marken.forEach(function (m) {
|
||||||
|
var i = Number(m.dataset.marke);
|
||||||
|
m.classList.toggle("af-aktiv", i === nr);
|
||||||
|
m.classList.toggle("af-erledigt", i < nr);
|
||||||
|
});
|
||||||
|
|
||||||
|
var fortschritt = document.getElementById("af-fortschritt");
|
||||||
|
var hinweis = document.getElementById("af-speicherhinweis");
|
||||||
|
|
||||||
|
if (nr === SEITE_DANKE) {
|
||||||
|
knopfLeiste.hidden = true;
|
||||||
|
if (fortschritt) fortschritt.hidden = true;
|
||||||
|
if (hinweis) hinweis.hidden = true;
|
||||||
|
} else {
|
||||||
|
knopfLeiste.hidden = false;
|
||||||
|
if (fortschritt) fortschritt.hidden = false;
|
||||||
|
if (hinweis) hinweis.hidden = false;
|
||||||
|
btnZurueck.hidden = nr === 1;
|
||||||
|
btnWeiter.textContent =
|
||||||
|
nr === SEITE_ZUSAMMENFASSUNG ? window.WD.t("af_senden")
|
||||||
|
: nr === LETZTE_SEITE_EINGABE ? window.WD.t("af_pruefen")
|
||||||
|
: window.WD.t("af_weiter");
|
||||||
|
zaehler.textContent =
|
||||||
|
window.WD.t("af_schritt") + " " + Math.min(nr, 4) + " " + window.WD.t("af_von") + " 4";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nach oben, aber nur bis zum Formular — ein Sprung ganz nach oben
|
||||||
|
zwingt sonst bei jedem Schritt zum Zurückscrollen. */
|
||||||
|
var rahmen = document.getElementById("af-rahmen");
|
||||||
|
if (rahmen) {
|
||||||
|
var y = rahmen.getBoundingClientRect().top + window.scrollY - 80;
|
||||||
|
window.scrollTo({ top: Math.max(0, y), behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Auf der Dankeseite NICHT mehr speichern.
|
||||||
|
Sonst passiert Folgendes: absenden() loescht den Entwurf, ruft dann
|
||||||
|
zeigeSeite(DANKE) auf — und diese Zeile legt ihn sofort wieder an.
|
||||||
|
Beim naechsten Besuch stuenden die Daten einer bereits gesendeten
|
||||||
|
Anfrage wieder im Formular, samt "Wir haben deine letzte Eingabe
|
||||||
|
wiederhergestellt". Genau so im Selbsttest aufgefallen. */
|
||||||
|
if (nr !== SEITE_DANKE) speichern();
|
||||||
|
}
|
||||||
|
|
||||||
|
function folgefragenAktualisieren() {
|
||||||
|
var w = werte();
|
||||||
|
var shop = w.paket === "shop" || w.funktionen.indexOf("shop") >= 0;
|
||||||
|
var vw = w.paket === "verwaltung" || w.funktionen.indexOf("login") >= 0;
|
||||||
|
var eShop = document.getElementById("af-folge-shop");
|
||||||
|
var eVw = document.getElementById("af-folge-verwaltung");
|
||||||
|
if (eShop) eShop.hidden = !shop;
|
||||||
|
if (eVw) eVw.hidden = !vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Zusammenfassung
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function beschriftung(schluessel, roh) {
|
||||||
|
if (!roh) return null;
|
||||||
|
var t = window.WD.t(schluessel);
|
||||||
|
return t === schluessel ? roh : t;
|
||||||
|
}
|
||||||
|
|
||||||
|
function baueZusammenfassung() {
|
||||||
|
var w = werte();
|
||||||
|
var ziel = document.getElementById("af-zusammenfassung");
|
||||||
|
var sicher = window.WD.sicher;
|
||||||
|
var leer = window.WD.t("af_zus_leer");
|
||||||
|
|
||||||
|
function zeile(was, wert) {
|
||||||
|
var istLeer = !wert;
|
||||||
|
return '<div class="af-zus-zeile"><span class="af-zus-was">' + sicher(was) + "</span>" +
|
||||||
|
'<span class="af-zus-wert' + (istLeer ? " af-leer" : "") + '">' +
|
||||||
|
sicher(istLeer ? leer : wert) + "</span></div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
function block(titel, schrittNr, zeilen) {
|
||||||
|
return '<div class="af-zus-block"><div class="af-zus-kopf"><h3>' + sicher(titel) + "</h3>" +
|
||||||
|
'<button type="button" data-zu="' + schrittNr + '">' + sicher(window.WD.t("af_zus_aendern")) + "</button>" +
|
||||||
|
"</div>" + zeilen.join("") + "</div>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var paketName = {
|
||||||
|
onepager: "pk_onepager_h3", website: "pk_website_h3", shop: "pk_shop_h3",
|
||||||
|
verwaltung: "pk_verwaltung_h3", betreuung: "betreuung_h3", unklar: "af1_unklar",
|
||||||
|
}[w.paket];
|
||||||
|
|
||||||
|
var funktionsNamen = {
|
||||||
|
kontaktformular: "af2_f_kontakt", galerie: "af2_f_galerie", termin: "af2_f_termin",
|
||||||
|
shop: "af2_f_shop", login: "af2_f_login", blog: "af2_f_blog",
|
||||||
|
sprachen: "af2_f_sprachen", karte: "af2_f_karte", newsletter: "af2_f_newsletter", app: "af2_f_app",
|
||||||
|
};
|
||||||
|
var funktionenText = w.funktionen
|
||||||
|
.map(function (f) { return beschriftung(funktionsNamen[f], f); })
|
||||||
|
.filter(Boolean)
|
||||||
|
/* HTML-Entities aus dem Wörterbuch (&) in echte Zeichen —
|
||||||
|
hier ist reiner Text gefragt, er wird gleich escaped. */
|
||||||
|
.map(function (s) { return s.replace(/&/g, "&"); })
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
var antwortNamen = { ja: "af3_ja", teilweise: "af3_teilweise", nein: "af3_nein" };
|
||||||
|
var budgetNamen = {
|
||||||
|
"bis-500": "af3_budget_1", "500-1000": "af3_budget_2",
|
||||||
|
"1000-2500": "af3_budget_3", "ueber-2500": "af3_budget_4", unklar: "af3_budget_5",
|
||||||
|
};
|
||||||
|
|
||||||
|
var html = "";
|
||||||
|
html += block(window.WD.t("af_s1"), 1, [
|
||||||
|
zeile(window.WD.t("af1_paket_label"), beschriftung(paketName, w.paket)),
|
||||||
|
zeile(window.WD.t("af1_ziel_label"), w.ziel),
|
||||||
|
zeile(window.WD.t("af1_zielgruppe_label"), w.zielgruppe),
|
||||||
|
]);
|
||||||
|
var zeilen2 = [
|
||||||
|
zeile(window.WD.t("af2_seiten_label"), w.seiten),
|
||||||
|
zeile(window.WD.t("af2_funktionen_label"), funktionenText),
|
||||||
|
];
|
||||||
|
Object.keys(w.details).forEach(function (k) {
|
||||||
|
zeilen2.push(zeile(k.replace(/_/g, " "), w.details[k]));
|
||||||
|
});
|
||||||
|
html += block(window.WD.t("af_s2"), 2, zeilen2);
|
||||||
|
|
||||||
|
html += block(window.WD.t("af_s3"), 3, [
|
||||||
|
zeile(window.WD.t("af3_texte"), beschriftung(antwortNamen[w.hatTexte], w.hatTexte)),
|
||||||
|
zeile(window.WD.t("af3_bilder"), beschriftung(antwortNamen[w.hatBilder], w.hatBilder)),
|
||||||
|
zeile(window.WD.t("af3_logo"), beschriftung(antwortNamen[w.hatLogo], w.hatLogo)),
|
||||||
|
zeile(window.WD.t("af3_domain"), beschriftung(antwortNamen[w.hatDomain], w.hatDomain)),
|
||||||
|
zeile(window.WD.t("af3_budget_label"), beschriftung(budgetNamen[w.budget], w.budget)),
|
||||||
|
zeile(window.WD.t("af3_termin_label"), w.termin),
|
||||||
|
]);
|
||||||
|
html += block(window.WD.t("af_s4"), 4, [
|
||||||
|
zeile(window.WD.t("af4_name"), w.name),
|
||||||
|
zeile(window.WD.t("af4_email"), w.email),
|
||||||
|
zeile(window.WD.t("af4_telefon"), w.telefon),
|
||||||
|
zeile(window.WD.t("af4_weg"), window.WD.t(w.kontaktweg === "telefon" ? "af4_weg_telefon" : "af4_weg_email")),
|
||||||
|
zeile(window.WD.t("af4_bemerkung"), w.bemerkung),
|
||||||
|
]);
|
||||||
|
|
||||||
|
ziel.innerHTML = html;
|
||||||
|
ziel.querySelectorAll("[data-zu]").forEach(function (b) {
|
||||||
|
b.addEventListener("click", function () { zeigeSeite(Number(b.dataset.zu)); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Absenden
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
async function absenden() {
|
||||||
|
var w = werte();
|
||||||
|
window.WD.knopfSperren(btnWeiter, true, window.WD.t("af_sendet"));
|
||||||
|
window.WD.meldungAus(meldung);
|
||||||
|
|
||||||
|
try {
|
||||||
|
var antwort = await window.WD.api("/webdesign/anfrage", {
|
||||||
|
method: "POST",
|
||||||
|
body: Object.assign({}, w, {
|
||||||
|
sprache: window.WD.getSprache(),
|
||||||
|
geoeffnetAm: geoeffnetAm,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Erst NACH bestätigtem Eingang löschen. Bricht das Absenden ab,
|
||||||
|
sind die Eingaben noch da und man kann es erneut versuchen. */
|
||||||
|
/* Der Server meldet bei verdächtigen Absendungen bewusst Erfolg,
|
||||||
|
speichert aber nichts und liefert KEINE Nummer (Honigtopf oder
|
||||||
|
verdächtig kurze Ausfüllzeit — siehe routes/webdesign-anfragen.js).
|
||||||
|
Das ist gegenüber automatischen Skripten richtig so.
|
||||||
|
|
||||||
|
Für einen echten Menschen, der zufällig sehr schnell war, wäre
|
||||||
|
eine Dankeseite mit "—" als Nummer aber eine stille Lüge: er
|
||||||
|
glaubt, die Anfrage sei unterwegs, und wartet auf eine Antwort,
|
||||||
|
die nie kommt. Deshalb hier ohne Nummer ein ehrlicher Hinweis mit
|
||||||
|
einem zweiten Weg. */
|
||||||
|
var nummerFeld = document.getElementById("af-nummer");
|
||||||
|
if (antwort.nummer) {
|
||||||
|
entwurfLoeschen();
|
||||||
|
nummerFeld.textContent = antwort.nummer;
|
||||||
|
} else {
|
||||||
|
/* Entwurf ABSICHTLICH behalten — falls wirklich nichts ankam,
|
||||||
|
soll die Arbeit nicht weg sein. */
|
||||||
|
nummerFeld.textContent = "—";
|
||||||
|
nummerFeld.style.fontSize = "1rem";
|
||||||
|
nummerFeld.style.letterSpacing = "normal";
|
||||||
|
nummerFeld.textContent = window.WD.t("af_danke_ohne_nummer");
|
||||||
|
}
|
||||||
|
zeigeSeite(SEITE_DANKE);
|
||||||
|
} catch (fehler) {
|
||||||
|
/* Feldbezogene Fehler vom Server anzeigen — der Server ist die
|
||||||
|
maßgebliche Prüfung, nicht der Browser. */
|
||||||
|
var felder = fehler && fehler.daten && fehler.daten.felder;
|
||||||
|
if (felder) {
|
||||||
|
Object.keys(felder).forEach(function (f) { fehlerSetzen("feld-" + f, felder[f]); });
|
||||||
|
}
|
||||||
|
window.WD.meldung(meldung, fehler.message || window.WD.t("fehler_allg"), "fehler");
|
||||||
|
window.WD.knopfSperren(btnWeiter, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Start
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function start() {
|
||||||
|
form = document.getElementById("af-form");
|
||||||
|
if (!form) return;
|
||||||
|
seiten = Array.prototype.slice.call(form.querySelectorAll(".af-seite"));
|
||||||
|
marken = Array.prototype.slice.call(document.querySelectorAll(".af-schritt-marke"));
|
||||||
|
zaehler = document.getElementById("af-zaehler");
|
||||||
|
meldung = document.getElementById("af-meldung");
|
||||||
|
knopfLeiste = document.getElementById("af-knopfleiste");
|
||||||
|
btnZurueck = document.getElementById("af-zurueck");
|
||||||
|
btnWeiter = document.getElementById("af-weiter");
|
||||||
|
|
||||||
|
baueVorhandenTabelle();
|
||||||
|
|
||||||
|
/* Paket aus der Adresszeile vorauswählen — die Leistungsseite
|
||||||
|
verlinkt mit ?paket=shop. Spart einen Klick und zeigt, dass der
|
||||||
|
Weg mitgedacht ist. */
|
||||||
|
var params = new URLSearchParams(location.search);
|
||||||
|
var paket = params.get("paket");
|
||||||
|
if (paket) {
|
||||||
|
var el = form.querySelector("[name='paket'][value='" + paket.replace(/[^a-z]/g, "") + "']");
|
||||||
|
if (el) el.checked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entwurf = gespeichertesLesen();
|
||||||
|
if (entwurf) {
|
||||||
|
wiederherstellen(entwurf);
|
||||||
|
var box = document.getElementById("af-wieder");
|
||||||
|
if (box) box.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("af-verwerfen").addEventListener("click", function () {
|
||||||
|
entwurfLoeschen();
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
folgefragenAktualisieren();
|
||||||
|
|
||||||
|
btnWeiter.addEventListener("click", function () {
|
||||||
|
if (aktuelleSeite === SEITE_ZUSAMMENFASSUNG) return absenden();
|
||||||
|
if (!pruefeSeite(aktuelleSeite)) return;
|
||||||
|
if (aktuelleSeite === LETZTE_SEITE_EINGABE) {
|
||||||
|
baueZusammenfassung();
|
||||||
|
zeigeSeite(SEITE_ZUSAMMENFASSUNG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
zeigeSeite(aktuelleSeite + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
btnZurueck.addEventListener("click", function () {
|
||||||
|
if (aktuelleSeite > 1) zeigeSeite(aktuelleSeite - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Eingaben laufend sichern und Folgefragen nachziehen. "input" statt
|
||||||
|
"change", damit auch beim Tippen gespeichert wird. */
|
||||||
|
form.addEventListener("input", function () { folgefragenAktualisieren(); speichern(); });
|
||||||
|
form.addEventListener("change", function () { folgefragenAktualisieren(); speichern(); });
|
||||||
|
|
||||||
|
/* Enter im Formular soll weitergehen, nicht absenden — sonst ist die
|
||||||
|
Anfrage nach dem ersten Feld unterwegs. */
|
||||||
|
form.addEventListener("submit", function (e) { e.preventDefault(); btnWeiter.click(); });
|
||||||
|
|
||||||
|
/* Telefon wird bei "Per Telefon" zur Pflicht — das Wörtchen
|
||||||
|
"optional" muss dann verschwinden, sonst ist die spätere
|
||||||
|
Fehlermeldung unfair. */
|
||||||
|
form.querySelectorAll("[name='kontaktweg']").forEach(function (r) {
|
||||||
|
r.addEventListener("change", function () {
|
||||||
|
var opt = document.getElementById("af-telefon-opt");
|
||||||
|
if (opt) opt.hidden = r.value === "telefon" && r.checked;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Nach einem Sprachwechsel Knopfbeschriftung, Zähler und die per JS
|
||||||
|
gebauten Teile neu setzen. */
|
||||||
|
document.addEventListener("wd-sprache-geaendert", function () {
|
||||||
|
baueVorhandenTabelle();
|
||||||
|
wiederherstellen({ stand: werte() }); // Auswahl der Tabelle retten
|
||||||
|
if (aktuelleSeite === SEITE_ZUSAMMENFASSUNG) baueZusammenfassung();
|
||||||
|
zeigeSeite(aktuelleSeite);
|
||||||
|
});
|
||||||
|
|
||||||
|
zeigeSeite(entwurf && entwurf.seite && entwurf.seite <= LETZTE_SEITE_EINGABE ? entwurf.seite : 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.WD) start();
|
||||||
|
else document.addEventListener("wd-bereit", start);
|
||||||
|
})();
|
||||||
@@ -27,6 +27,7 @@ const SEITEN = [
|
|||||||
"/webdesign/ablauf.html",
|
"/webdesign/ablauf.html",
|
||||||
"/webdesign/ueber.html",
|
"/webdesign/ueber.html",
|
||||||
"/webdesign/faq.html",
|
"/webdesign/faq.html",
|
||||||
|
"/webdesign/anfrage.html",
|
||||||
];
|
];
|
||||||
const BREITEN = [320, 390, 430, 768, 1440];
|
const BREITEN = [320, 390, 430, 768, 1440];
|
||||||
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
|
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
|
||||||
@@ -88,6 +89,22 @@ for (const pfad of SEITEN) {
|
|||||||
if (s.display === "none" || s.visibility === "hidden") continue;
|
if (s.display === "none" || s.visibility === "hidden") continue;
|
||||||
const r = el.getBoundingClientRect();
|
const r = el.getBoundingClientRect();
|
||||||
if (r.height === 0 || r.width === 0) continue;
|
if (r.height === 0 || r.width === 0) continue;
|
||||||
|
|
||||||
|
/* Absichtlich unsichtbare Eingabefelder ueberspringen.
|
||||||
|
Bei Auswahlkacheln (.wd-wahl-kachel) liegt das echte Ankreuzfeld
|
||||||
|
unsichtbar hinter der Kachel -- angetippt wird die KACHEL, und
|
||||||
|
die ist gross genug. Ebenso der Honigtopf im Anfrageformular,
|
||||||
|
der fuer Menschen gar nicht existieren soll.
|
||||||
|
Ohne diese Ausnahme meldet die Pruefung fuenf Fehler, die keine
|
||||||
|
sind -- und Fehlalarme sind das Ende jeder Pruefung, weil man
|
||||||
|
sie irgendwann pauschal ignoriert. */
|
||||||
|
if (el.tagName === "INPUT") {
|
||||||
|
if (parseFloat(s.opacity) === 0) continue; // unsichtbar hinter der Kachel
|
||||||
|
if (r.right < 0 || r.left > vp) continue; // aus dem Bild geschoben
|
||||||
|
const label = el.closest("label");
|
||||||
|
if (label && label.getBoundingClientRect().height >= 44) continue; // Label ist das Ziel
|
||||||
|
}
|
||||||
|
|
||||||
if (r.height < 44) {
|
if (r.height < 44) {
|
||||||
klein.push((el.tagName.toLowerCase() + " „" + (el.textContent || "").trim().slice(0, 24) + "“ ")
|
klein.push((el.tagName.toLowerCase() + " „" + (el.textContent || "").trim().slice(0, 24) + "“ ")
|
||||||
+ Math.round(r.height) + "px");
|
+ Math.round(r.height) + "px");
|
||||||
|
|||||||
@@ -58,8 +58,21 @@ for (const datei of seiten) {
|
|||||||
}
|
}
|
||||||
const titelKey = (html.match(/data-i18n-titel="([^"]+)"/) || [])[1];
|
const titelKey = (html.match(/data-i18n-titel="([^"]+)"/) || [])[1];
|
||||||
if (titelKey) benutzt.add(titelKey);
|
if (titelKey) benutzt.add(titelKey);
|
||||||
// Schlüssel, die nur im Seitenskript per WD.t(...) geholt werden
|
// Schlüssel, die nur per WD.t(...) geholt werden — im Seitenskript
|
||||||
for (const m of html.matchAll(/WD\.t\("([a-z0-9_]+)"\)/g)) benutzt.add(m[1]);
|
// unten in der HTML-Datei UND in einer zugehörigen assets/js/wd-<name>.js.
|
||||||
|
// Ohne den zweiten Teil meldet die Prüfung reihenweise "unbenutzt" für
|
||||||
|
// Schlüssel, die sehr wohl benutzt werden — nur eben aus einer anderen
|
||||||
|
// Datei (aufgefallen beim Anfrageformular, 22.08.2026).
|
||||||
|
const quellen = [html];
|
||||||
|
const seitenSkript = `assets/js/wd-${name}.js`;
|
||||||
|
if (existsSync(seitenSkript)) quellen.push(readFileSync(seitenSkript, "utf8"));
|
||||||
|
for (const quelle of quellen) {
|
||||||
|
for (const m of quelle.matchAll(/WD\.t\("([a-z0-9_]+)"\)/g)) benutzt.add(m[1]);
|
||||||
|
// Schlüssel aus Nachschlagetabellen, z.B. { ja: "af3_ja", ... }
|
||||||
|
for (const m of quelle.matchAll(/["']((?:af|pk|zg|pf|ue|fq|ab|ls|pr|s\d)[a-z0-9_]*)["']\s*[,}\]]/g)) {
|
||||||
|
benutzt.add(m[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const fehlend = [];
|
const fehlend = [];
|
||||||
const unvollstaendig = [];
|
const unvollstaendig = [];
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
/* =====================================================================
|
||||||
|
test-webdesign-formular.mjs — Anfrageformular im echten Browser
|
||||||
|
|
||||||
|
Spielt das Formular so durch, wie ein Mensch es tut, und prüft die
|
||||||
|
Dinge, die man beim Klicken NICHT sieht:
|
||||||
|
- Pflichtfelder blockieren das Weitergehen
|
||||||
|
- Folgefragen erscheinen nur passend zur Auswahl
|
||||||
|
- Eingaben überleben das Schließen des Tabs
|
||||||
|
- die Zusammenfassung zeigt wirklich das Eingegebene
|
||||||
|
- Absenden erzeugt eine echte Anfragenummer
|
||||||
|
|
||||||
|
Standardmäßig wird NICHT abgeschickt (sonst entsteht bei jedem Lauf ein
|
||||||
|
Datensatz in der echten Datenbank). Mit --senden wird einmal wirklich
|
||||||
|
abgeschickt:
|
||||||
|
node test-webdesign-formular.mjs --senden
|
||||||
|
===================================================================== */
|
||||||
|
import { chromium } from "playwright";
|
||||||
|
|
||||||
|
const BASIS = "http://127.0.0.1:4199";
|
||||||
|
const CODE = "DOGI-TEST-1";
|
||||||
|
const WIRKLICH_SENDEN = process.argv.includes("--senden");
|
||||||
|
|
||||||
|
let ok = 0, schlecht = 0;
|
||||||
|
const pruefe = (name, b, zusatz = "") => {
|
||||||
|
if (b) { ok++; console.log(" ok " + name); }
|
||||||
|
else { schlecht++; console.log(" FEHLT " + name + " " + zusatz); }
|
||||||
|
};
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const kontext = await browser.newContext();
|
||||||
|
const seite = await kontext.newPage();
|
||||||
|
const konsole = [];
|
||||||
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||||||
|
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||||||
|
|
||||||
|
async function anmelden(p) {
|
||||||
|
await p.goto(BASIS + "/webdesign/zugang.html", { waitUntil: "networkidle" });
|
||||||
|
await p.fill("#zg-code-dogfather", CODE);
|
||||||
|
await Promise.all([
|
||||||
|
p.waitForURL((u) => !u.pathname.endsWith("zugang.html"), { timeout: 15000 }),
|
||||||
|
p.click("#zg-form-dogfather button[type=submit]"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\n=== Anfrageformular im Browser ===\n");
|
||||||
|
|
||||||
|
await anmelden(seite);
|
||||||
|
|
||||||
|
/* ---------- 1. Aufbau ---------- */
|
||||||
|
console.log("1. Aufbau");
|
||||||
|
await seite.goto(BASIS + "/webdesign/anfrage.html", { waitUntil: "networkidle" });
|
||||||
|
await seite.waitForTimeout(400);
|
||||||
|
pruefe("Schritt 1 ist sichtbar", await seite.locator('[data-seite="1"]').isVisible());
|
||||||
|
pruefe("Schritt 2 ist verborgen", !(await seite.locator('[data-seite="2"]').isVisible()));
|
||||||
|
pruefe("Zaehler zeigt Schritt 1 von 4", (await seite.locator("#af-zaehler").textContent()).includes("1"));
|
||||||
|
pruefe("Zurueck ist auf Schritt 1 ausgeblendet", !(await seite.locator("#af-zurueck").isVisible()));
|
||||||
|
pruefe("Tabelle 'Was hast du schon' wurde gebaut",
|
||||||
|
(await seite.locator("#af-vorhanden tr").count()) === 4);
|
||||||
|
|
||||||
|
/* ---------- 2. Pflichtfelder ---------- */
|
||||||
|
console.log("\n2. Pflichtfelder blockieren");
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(400);
|
||||||
|
pruefe("bleibt auf Schritt 1", await seite.locator('[data-seite="1"]').isVisible());
|
||||||
|
pruefe("Paket wird bemaengelt", await seite.locator("#feld-paket.wd-feld--fehler").count() > 0);
|
||||||
|
pruefe("Ziel wird bemaengelt", await seite.locator("#feld-ziel.wd-feld--fehler").count() > 0);
|
||||||
|
pruefe("Sammelmeldung erscheint", await seite.locator("#af-meldung").isVisible());
|
||||||
|
|
||||||
|
/* ---------- 3. Vorauswahl per Adresszeile ---------- */
|
||||||
|
console.log("\n3. Vorauswahl ueber ?paket=");
|
||||||
|
await seite.goto(BASIS + "/webdesign/anfrage.html?paket=shop", { waitUntil: "networkidle" });
|
||||||
|
await seite.waitForTimeout(400);
|
||||||
|
pruefe("Paket 'shop' ist vorausgewaehlt",
|
||||||
|
await seite.locator('[name="paket"][value="shop"]').isChecked());
|
||||||
|
|
||||||
|
/* ---------- 4. Durchlauf ---------- */
|
||||||
|
console.log("\n4. Durchlauf");
|
||||||
|
await seite.fill("#af-ziel", "Wir wollen unsere Haekelwerke endlich online verkaufen.");
|
||||||
|
await seite.fill("#af-zielgruppe", "Bastelfans, 25-55 Jahre");
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(500);
|
||||||
|
pruefe("Schritt 2 erreicht", await seite.locator('[data-seite="2"]').isVisible());
|
||||||
|
pruefe("Shop-Folgefragen erscheinen bei Paket 'shop'",
|
||||||
|
await seite.locator("#af-folge-shop").isVisible(),
|
||||||
|
"-> Masterplan S.10 'Intelligente Felder'");
|
||||||
|
pruefe("Verwaltungs-Folgefragen bleiben verborgen",
|
||||||
|
!(await seite.locator("#af-folge-verwaltung").isVisible()));
|
||||||
|
|
||||||
|
await seite.fill("#af-shop-anzahl", "40");
|
||||||
|
await seite.check('[name="funktionen"][value="galerie"]');
|
||||||
|
await seite.check('[name="funktionen"][value="login"]');
|
||||||
|
await seite.waitForTimeout(300);
|
||||||
|
pruefe("Verwaltungs-Folgefragen erscheinen nach 'Kundenbereich'",
|
||||||
|
await seite.locator("#af-folge-verwaltung").isVisible());
|
||||||
|
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(500);
|
||||||
|
pruefe("Schritt 3 erreicht", await seite.locator('[data-seite="3"]').isVisible());
|
||||||
|
await seite.check('[name="v_texte"][value="teilweise"]');
|
||||||
|
await seite.check('[name="v_bilder"][value="ja"]');
|
||||||
|
await seite.check('[name="v_logo"][value="nein"]');
|
||||||
|
await seite.check('[name="v_domain"][value="ja"]');
|
||||||
|
await seite.selectOption("#af-budget", "1000-2500");
|
||||||
|
await seite.fill("#af-termin", "moeglichst im Oktober");
|
||||||
|
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(500);
|
||||||
|
pruefe("Schritt 4 erreicht", await seite.locator('[data-seite="4"]').isVisible());
|
||||||
|
pruefe("Knopf heisst jetzt 'Angaben pruefen'",
|
||||||
|
/pr[üu]fen/i.test(await seite.locator("#af-weiter").textContent()));
|
||||||
|
|
||||||
|
/* ---------- 5. Telefonpflicht ---------- */
|
||||||
|
console.log("\n5. Telefon wird Pflicht bei 'Per Telefon'");
|
||||||
|
await seite.fill("#af-name", "Testfirma Meier");
|
||||||
|
await seite.fill("#af-email", "[email protected]");
|
||||||
|
await seite.check('[name="kontaktweg"][value="telefon"]');
|
||||||
|
await seite.check("#af-datenschutz");
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(400);
|
||||||
|
pruefe("ohne Nummer wird blockiert", await seite.locator("#feld-telefon.wd-feld--fehler").count() > 0,
|
||||||
|
"-> sonst ein Wunsch, der gar nicht erfuellbar ist");
|
||||||
|
await seite.check('[name="kontaktweg"][value="email"]');
|
||||||
|
|
||||||
|
/* ---------- 6. Datenschutz ---------- */
|
||||||
|
await seite.uncheck("#af-datenschutz");
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(400);
|
||||||
|
pruefe("ohne Datenschutz-Zustimmung wird blockiert",
|
||||||
|
await seite.locator("#feld-datenschutz.wd-feld--fehler").count() > 0);
|
||||||
|
await seite.check("#af-datenschutz");
|
||||||
|
await seite.fill("#af-bemerkung", "Bitte moeglichst bald melden.");
|
||||||
|
|
||||||
|
/* ---------- 7. Zusammenfassung ---------- */
|
||||||
|
console.log("\n6. Zusammenfassung");
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(600);
|
||||||
|
pruefe("Zusammenfassung erreicht", await seite.locator('[data-seite="5"]').isVisible());
|
||||||
|
const zusText = await seite.locator("#af-zusammenfassung").textContent();
|
||||||
|
pruefe("zeigt das eingegebene Ziel", zusText.includes("Haekelwerke"));
|
||||||
|
pruefe("zeigt Name und E-Mail", zusText.includes("Testfirma Meier") && zusText.includes("[email protected]"));
|
||||||
|
pruefe("zeigt angekreuzte Funktionen", /Galerie|galerie/i.test(zusText));
|
||||||
|
pruefe("zeigt die Shop-Folgefrage", zusText.includes("40"));
|
||||||
|
pruefe("leere Felder als 'keine Angabe'", /keine Angabe/i.test(zusText));
|
||||||
|
pruefe("Knopf heisst jetzt 'Anfrage absenden'",
|
||||||
|
/absenden/i.test(await seite.locator("#af-weiter").textContent()));
|
||||||
|
pruefe("jeder Block hat einen Aendern-Knopf",
|
||||||
|
(await seite.locator("#af-zusammenfassung [data-zu]").count()) === 4);
|
||||||
|
|
||||||
|
await seite.locator('[data-zu="1"]').click();
|
||||||
|
await seite.waitForTimeout(400);
|
||||||
|
pruefe("'Aendern' springt zum richtigen Schritt", await seite.locator('[data-seite="1"]').isVisible());
|
||||||
|
pruefe("Eingaben sind dabei erhalten",
|
||||||
|
(await seite.inputValue("#af-ziel")).includes("Haekelwerke"));
|
||||||
|
|
||||||
|
/* ---------- 8. Zwischenspeicherung ---------- */
|
||||||
|
console.log("\n7. Zwischenspeicherung");
|
||||||
|
const neuerTab = await kontext.newPage();
|
||||||
|
await anmelden(neuerTab);
|
||||||
|
await neuerTab.goto(BASIS + "/webdesign/anfrage.html", { waitUntil: "networkidle" });
|
||||||
|
await neuerTab.waitForTimeout(600);
|
||||||
|
pruefe("Hinweis auf wiederhergestellte Eingaben erscheint",
|
||||||
|
await neuerTab.locator("#af-wieder").isVisible());
|
||||||
|
pruefe("Ziel wurde wiederhergestellt",
|
||||||
|
(await neuerTab.inputValue("#af-ziel")).includes("Haekelwerke"));
|
||||||
|
pruefe("Name wurde wiederhergestellt",
|
||||||
|
(await neuerTab.inputValue("#af-name")) === "Testfirma Meier");
|
||||||
|
pruefe("Paketwahl wurde wiederhergestellt",
|
||||||
|
await neuerTab.locator('[name="paket"][value="shop"]').isChecked());
|
||||||
|
await neuerTab.click("#af-verwerfen");
|
||||||
|
await neuerTab.waitForTimeout(600);
|
||||||
|
pruefe("'Neu anfangen' leert das Formular",
|
||||||
|
(await neuerTab.inputValue("#af-ziel")) === "");
|
||||||
|
await neuerTab.close();
|
||||||
|
|
||||||
|
/* ---------- 9. Absenden ---------- */
|
||||||
|
console.log("\n8. Absenden");
|
||||||
|
if (!WIRKLICH_SENDEN) {
|
||||||
|
console.log(" (uebersprungen -- mit --senden wird wirklich abgeschickt)");
|
||||||
|
} else {
|
||||||
|
await seite.goto(BASIS + "/webdesign/anfrage.html", { waitUntil: "networkidle" });
|
||||||
|
/* Der Server weist Absendungen unter 3 Sekunden als vermutlich
|
||||||
|
automatisch ab (Spamschutz, siehe routes/webdesign-anfragen.js).
|
||||||
|
Ein Testlauf ist naturgemaess schneller als ein Mensch -- ohne diese
|
||||||
|
Wartezeit prueft der Test versehentlich den Spamschutz statt des
|
||||||
|
Formulars. Genau so beim ersten Lauf passiert. */
|
||||||
|
await seite.waitForTimeout(3500);
|
||||||
|
await seite.check('[name="paket"][value="onepager"]');
|
||||||
|
await seite.fill("#af-ziel", "TESTANFRAGE aus dem Selbsttest - bitte archivieren.");
|
||||||
|
await seite.click("#af-weiter"); await seite.waitForTimeout(400);
|
||||||
|
await seite.click("#af-weiter"); await seite.waitForTimeout(400);
|
||||||
|
await seite.click("#af-weiter"); await seite.waitForTimeout(400);
|
||||||
|
await seite.fill("#af-name", "Selbsttest");
|
||||||
|
await seite.fill("#af-email", "[email protected]");
|
||||||
|
await seite.check("#af-datenschutz");
|
||||||
|
await seite.click("#af-weiter"); await seite.waitForTimeout(600);
|
||||||
|
await seite.click("#af-weiter");
|
||||||
|
await seite.waitForTimeout(6000);
|
||||||
|
|
||||||
|
const aufDanke = await seite.locator('[data-seite="6"]').isVisible();
|
||||||
|
pruefe("Dankeseite erscheint", aufDanke,
|
||||||
|
aufDanke ? "" : "Meldung: " + (await seite.locator("#af-meldung").textContent().catch(() => "")));
|
||||||
|
if (aufDanke) {
|
||||||
|
const nummer = (await seite.locator("#af-nummer").textContent()).trim();
|
||||||
|
pruefe("echte Anfragenummer im Format A-JJMM-0001", /^A-\d{4}-\d{4}$/.test(nummer), nummer);
|
||||||
|
console.log(" -> Nummer: " + nummer);
|
||||||
|
pruefe("Entwurf wurde nach Erfolg geloescht",
|
||||||
|
(await seite.evaluate(() => localStorage.getItem("wd-anfrage-entwurf"))) === null);
|
||||||
|
pruefe("Schrittanzeige ist auf der Dankeseite weg",
|
||||||
|
!(await seite.locator("#af-fortschritt").isVisible()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log(konsole.length ? "\nKONSOLENFEHLER:\n" + [...new Set(konsole)].join("\n") : "\nKeine Konsolenfehler.");
|
||||||
|
console.log(`\n=== ${ok} bestanden, ${schlecht} gescheitert ===\n`);
|
||||||
|
process.exit(schlecht === 0 && konsole.length === 0 ? 0 : 1);
|
||||||
@@ -0,0 +1,425 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de" data-i18n-titel="seiten_titel">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Projekt anfragen — Dogfather Webdesign</title>
|
||||||
|
<meta name="description" content="Vier kurze Schritte zu deiner unverbindlichen Projektanfrage. Kostenlos, ohne Vertrag, mit Zusammenfassung vor dem Absenden." />
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive" />
|
||||||
|
<link rel="icon" type="image/png" href="/assets/img/favicon.png" />
|
||||||
|
<meta name="theme-color" content="#05070b" />
|
||||||
|
<link rel="manifest" href="/webdesign/app.webmanifest" />
|
||||||
|
<link rel="apple-touch-icon" href="/assets/img/wd-app-180.png" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
|
||||||
|
<link rel="stylesheet" href="/assets/css/main.css?v=20260822wd2" />
|
||||||
|
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260822wd2" />
|
||||||
|
<style>
|
||||||
|
/* ---------- Schrittanzeige ----------
|
||||||
|
Zeigt alle vier Schritte gleichzeitig statt nur "3 von 4". Man sieht
|
||||||
|
dadurch von Anfang an, wie lang das Formular wirklich ist — die
|
||||||
|
wichtigste Maßnahme gegen Abbrüche. */
|
||||||
|
.af-schritte { display: flex; gap: .4rem; margin-bottom: .8rem; }
|
||||||
|
.af-schritt-marke { flex: 1; text-align: center; }
|
||||||
|
.af-schritt-balken {
|
||||||
|
height: 5px; border-radius: 999px; background: rgba(127,208,232,.14);
|
||||||
|
margin-bottom: .45rem; overflow: hidden;
|
||||||
|
}
|
||||||
|
.af-schritt-balken span {
|
||||||
|
display: block; height: 100%; width: 0;
|
||||||
|
background: linear-gradient(90deg, var(--wd-blau-tief), var(--wd-blau));
|
||||||
|
transition: width var(--wd-zeit-lang) cubic-bezier(.4,0,.2,1);
|
||||||
|
}
|
||||||
|
.af-schritt-marke.af-erledigt .af-schritt-balken span,
|
||||||
|
.af-schritt-marke.af-aktiv .af-schritt-balken span { width: 100%; }
|
||||||
|
.af-schritt-name {
|
||||||
|
font-size: .74rem; font-weight: 700; letter-spacing: .04em;
|
||||||
|
color: var(--wd-text-soft);
|
||||||
|
}
|
||||||
|
.af-schritt-marke.af-aktiv .af-schritt-name { color: var(--wd-blau); }
|
||||||
|
.af-schritt-marke.af-erledigt .af-schritt-name { color: var(--wd-text-mid); }
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
/* Auf schmalen Bildschirmen nur der aktive Name — vier Namen
|
||||||
|
nebeneinander werden sonst auf zwei Buchstaben gequetscht. */
|
||||||
|
.af-schritt-marke:not(.af-aktiv) .af-schritt-name { display: none; }
|
||||||
|
.af-schritt-marke.af-aktiv { flex: 2; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.af-zaehler { font-size: .85rem; color: var(--wd-text-soft); margin: 0 0 1.4rem; }
|
||||||
|
|
||||||
|
/* ---------- Seiten des Formulars ---------- */
|
||||||
|
.af-seite { display: none; }
|
||||||
|
.af-seite.af-sichtbar { display: block; animation: af-rein .3s ease both; }
|
||||||
|
@keyframes af-rein { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .af-seite.af-sichtbar { animation: none; } }
|
||||||
|
|
||||||
|
.af-seite > h2 { font-size: clamp(1.35rem, 3.4vw, 1.9rem); margin-bottom: .8em; }
|
||||||
|
|
||||||
|
/* Honigtopf: für Menschen unsichtbar, für automatische Skripte ein
|
||||||
|
ganz normales Feld. Bewusst NICHT display:none — manche Skripte
|
||||||
|
überspringen versteckte Felder gezielt. So wirkt es echt. */
|
||||||
|
.af-falle {
|
||||||
|
position: absolute !important; left: -9999px !important;
|
||||||
|
width: 1px; height: 1px; overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.af-knopfleiste {
|
||||||
|
display: flex; gap: .7rem; margin-top: 1.6rem;
|
||||||
|
padding-top: 1.2rem; border-top: 1px solid var(--wd-rand);
|
||||||
|
}
|
||||||
|
.af-knopfleiste .af-vor { margin-left: auto; }
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.af-knopfleiste { flex-direction: column-reverse; }
|
||||||
|
.af-knopfleiste .af-vor { margin-left: 0; }
|
||||||
|
.af-knopfleiste .wd-btn { width: 100%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Zusammenfassung ---------- */
|
||||||
|
.af-zus-block { border-bottom: 1px solid var(--wd-rand); padding: .9rem 0; }
|
||||||
|
.af-zus-block:last-of-type { border-bottom: none; }
|
||||||
|
.af-zus-kopf { display: flex; align-items: baseline; gap: .8rem; margin-bottom: .5rem; }
|
||||||
|
.af-zus-kopf h3 { margin: 0; font-size: .78rem; letter-spacing: .12em; text-transform: uppercase; color: var(--wd-blau); }
|
||||||
|
.af-zus-kopf button {
|
||||||
|
margin-left: auto; background: none; border: none; cursor: pointer;
|
||||||
|
color: var(--wd-blau); font-weight: 700; font-size: .85rem;
|
||||||
|
min-height: 44px; padding: 0 .3rem;
|
||||||
|
}
|
||||||
|
.af-zus-kopf button:hover { color: var(--wd-blau-hell); text-decoration: underline; }
|
||||||
|
.af-zus-zeile { display: grid; grid-template-columns: minmax(0,.42fr) minmax(0,1fr); gap: .3rem 1rem; margin-bottom: .35rem; }
|
||||||
|
@media (max-width: 560px) { .af-zus-zeile { grid-template-columns: 1fr; gap: 0; margin-bottom: .7rem; } }
|
||||||
|
.af-zus-was { color: var(--wd-text-soft); font-size: .93rem; }
|
||||||
|
.af-zus-wert { color: var(--wd-text); font-size: .97rem; overflow-wrap: break-word; }
|
||||||
|
.af-zus-wert.af-leer { color: var(--wd-text-soft); font-style: italic; }
|
||||||
|
|
||||||
|
/* ---------- Danke ---------- */
|
||||||
|
.af-danke { text-align: center; padding: clamp(1.5rem,4vw,2.5rem) 0; }
|
||||||
|
.af-danke-haken {
|
||||||
|
width: 74px; height: 74px; margin: 0 auto 1.2rem;
|
||||||
|
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||||
|
background: rgba(111,214,163,.12); border: 2px solid var(--wd-ok);
|
||||||
|
font-size: 2.1rem; color: var(--wd-ok);
|
||||||
|
}
|
||||||
|
.af-nummer {
|
||||||
|
display: inline-block; margin: .3rem 0 .6rem;
|
||||||
|
font-family: var(--font-head); font-weight: 800;
|
||||||
|
font-size: clamp(1.5rem,4vw,2rem); letter-spacing: .06em;
|
||||||
|
color: var(--wd-gold);
|
||||||
|
padding: .5rem 1.2rem; border-radius: 12px;
|
||||||
|
background: rgba(232,200,119,.08); border: 1px solid rgba(232,200,119,.32);
|
||||||
|
}
|
||||||
|
|
||||||
|
.af-wieder {
|
||||||
|
display: flex; flex-wrap: wrap; align-items: center; gap: .7rem 1rem;
|
||||||
|
margin-bottom: 1.4rem;
|
||||||
|
}
|
||||||
|
.af-wieder[hidden] { display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="wd">
|
||||||
|
|
||||||
|
<div id="wd-kopf"></div>
|
||||||
|
|
||||||
|
<main id="wd-inhalt">
|
||||||
|
<section class="wd-abschnitt wd-abschnitt--eng" style="padding-top:clamp(2rem,5vw,3.2rem)">
|
||||||
|
<div class="wd-container wd-container--schmal">
|
||||||
|
<span class="wd-eyebrow" data-i18n="af_eyebrow">Projektanfrage</span>
|
||||||
|
<h1 data-i18n="af_h1">Erzähl mir von deinem Vorhaben</h1>
|
||||||
|
<p class="wd-lead" data-i18n="af_lead">Vier kurze Schritte, etwa fünf Minuten. Unverbindlich, kostenlos — und du siehst am Ende alles noch einmal, bevor du absendest.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="wd-abschnitt" style="padding-top:0">
|
||||||
|
<div class="wd-container wd-container--schmal">
|
||||||
|
|
||||||
|
<!-- Hinweis auf wiederhergestellte Eingaben -->
|
||||||
|
<div class="wd-notiz af-wieder" id="af-wieder" hidden>
|
||||||
|
<p style="margin:0;flex:1" data-i18n="af_wiederhergestellt">Wir haben deine letzte Eingabe wiederhergestellt.</p>
|
||||||
|
<button type="button" class="wd-btn wd-btn--still wd-btn--klein" id="af-verwerfen" data-i18n="af_verwerfen">Neu anfangen</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-karte" id="af-rahmen">
|
||||||
|
|
||||||
|
<!-- Schrittanzeige -->
|
||||||
|
<div id="af-fortschritt">
|
||||||
|
<div class="af-schritte" role="list">
|
||||||
|
<div class="af-schritt-marke" role="listitem" data-marke="1">
|
||||||
|
<div class="af-schritt-balken"><span></span></div>
|
||||||
|
<div class="af-schritt-name" data-i18n="af_s1">Vorhaben</div>
|
||||||
|
</div>
|
||||||
|
<div class="af-schritt-marke" role="listitem" data-marke="2">
|
||||||
|
<div class="af-schritt-balken"><span></span></div>
|
||||||
|
<div class="af-schritt-name" data-i18n="af_s2">Umfang</div>
|
||||||
|
</div>
|
||||||
|
<div class="af-schritt-marke" role="listitem" data-marke="3">
|
||||||
|
<div class="af-schritt-balken"><span></span></div>
|
||||||
|
<div class="af-schritt-name" data-i18n="af_s3">Inhalte & Rahmen</div>
|
||||||
|
</div>
|
||||||
|
<div class="af-schritt-marke" role="listitem" data-marke="4">
|
||||||
|
<div class="af-schritt-balken"><span></span></div>
|
||||||
|
<div class="af-schritt-name" data-i18n="af_s4">Kontakt</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="af-zaehler" id="af-zaehler" aria-live="polite">Schritt 1 von 4</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-meldung" id="af-meldung" hidden></div>
|
||||||
|
|
||||||
|
<form id="af-form" novalidate>
|
||||||
|
|
||||||
|
<!-- Honigtopf -->
|
||||||
|
<div class="af-falle" aria-hidden="true">
|
||||||
|
<label for="af-website">Website (bitte frei lassen)</label>
|
||||||
|
<input type="text" id="af-website" name="website" tabindex="-1" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SCHRITT 1 ============ -->
|
||||||
|
<div class="af-seite af-sichtbar" data-seite="1">
|
||||||
|
<h2 data-i18n="af1_h2">Worum geht es ungefähr?</h2>
|
||||||
|
|
||||||
|
<div class="wd-feld" id="feld-paket">
|
||||||
|
<span class="wd-feld-legende" data-i18n="af1_paket_label">Art des Projekts</span>
|
||||||
|
<span class="wd-feld-hilfe" data-i18n="af1_paket_hilfe">Nur eine grobe Richtung — wenn du unsicher bist, wähle das Letzte.</span>
|
||||||
|
<div class="wd-wahl wd-wahl--2" role="radiogroup" aria-labelledby="feld-paket">
|
||||||
|
<label class="wd-wahl-kachel">
|
||||||
|
<input type="radio" name="paket" value="onepager" />
|
||||||
|
<span class="wd-wahl-inhalt">
|
||||||
|
<span class="wd-wahl-titel" data-i18n="pk_onepager_h3">Onepager</span>
|
||||||
|
<span class="wd-wahl-text"><span data-i18n="ab_preis">ab</span> 300 €</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="wd-wahl-kachel">
|
||||||
|
<input type="radio" name="paket" value="website" />
|
||||||
|
<span class="wd-wahl-inhalt">
|
||||||
|
<span class="wd-wahl-titel" data-i18n="pk_website_h3">Professionelle Website</span>
|
||||||
|
<span class="wd-wahl-text"><span data-i18n="ab_preis">ab</span> 500 €</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="wd-wahl-kachel">
|
||||||
|
<input type="radio" name="paket" value="shop" />
|
||||||
|
<span class="wd-wahl-inhalt">
|
||||||
|
<span class="wd-wahl-titel" data-i18n="pk_shop_h3">Shop & umfangreiche Website</span>
|
||||||
|
<span class="wd-wahl-text"><span data-i18n="ab_preis">ab</span> 1.000 €</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="wd-wahl-kachel">
|
||||||
|
<input type="radio" name="paket" value="verwaltung" />
|
||||||
|
<span class="wd-wahl-inhalt">
|
||||||
|
<span class="wd-wahl-titel" data-i18n="pk_verwaltung_h3">Buchhaltungs- & Steuerverwaltungsseiten</span>
|
||||||
|
<span class="wd-wahl-text"><span data-i18n="ab_preis">ab</span> 1.500 €</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="wd-wahl-kachel">
|
||||||
|
<input type="radio" name="paket" value="betreuung" />
|
||||||
|
<span class="wd-wahl-inhalt">
|
||||||
|
<span class="wd-wahl-titel" data-i18n="betreuung_h3">Laufende Betreuung</span>
|
||||||
|
<span class="wd-wahl-text" data-i18n="betreuung_preis">ab 49 € / Monat</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="wd-wahl-kachel">
|
||||||
|
<input type="radio" name="paket" value="unklar" />
|
||||||
|
<span class="wd-wahl-inhalt">
|
||||||
|
<span class="wd-wahl-titel" data-i18n="af1_unklar">Ich weiß es noch nicht</span>
|
||||||
|
<span class="wd-wahl-text" data-i18n="af1_unklar_text">Wir finden im Gespräch heraus, was passt.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p class="wd-feld-fehler" data-fehler="paket"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld" id="feld-ziel">
|
||||||
|
<label for="af-ziel"><span data-i18n="af1_ziel_label">Was soll die Seite erreichen?</span><span class="wd-pflicht" aria-hidden="true">*</span></label>
|
||||||
|
<span class="wd-feld-hilfe" data-i18n="af1_ziel_hilfe">Ein, zwei Sätze reichen.</span>
|
||||||
|
<textarea id="af-ziel" name="ziel" rows="3" data-i18n-attr="placeholder:af1_ziel_platzhalter"></textarea>
|
||||||
|
<p class="wd-feld-fehler" data-fehler="ziel"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld">
|
||||||
|
<label for="af-zielgruppe"><span data-i18n="af1_zielgruppe_label">Wer sind deine Kunden?</span> <span class="wd-leise" data-i18n="af_optional">optional</span></label>
|
||||||
|
<input type="text" id="af-zielgruppe" name="zielgruppe" data-i18n-attr="placeholder:af1_zielgruppe_platzhalter" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SCHRITT 2 ============ -->
|
||||||
|
<div class="af-seite" data-seite="2">
|
||||||
|
<h2 data-i18n="af2_h2">Wie groß soll es werden?</h2>
|
||||||
|
|
||||||
|
<div class="wd-feld">
|
||||||
|
<label for="af-seiten"><span data-i18n="af2_seiten_label">Welche Bereiche brauchst du?</span> <span class="wd-leise" data-i18n="af_optional">optional</span></label>
|
||||||
|
<textarea id="af-seiten" name="seiten" rows="2" data-i18n-attr="placeholder:af2_seiten_platzhalter"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld">
|
||||||
|
<span class="wd-feld-legende" data-i18n="af2_funktionen_label">Was soll die Seite können?</span>
|
||||||
|
<span class="wd-feld-hilfe" data-i18n="af2_funktionen_hilfe">Alles, was du hier ankreuzt, wird im Angebot berücksichtigt. Nichts davon ist Pflicht.</span>
|
||||||
|
<div class="wd-wahl wd-wahl--2">
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="kontaktformular" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_kontakt">Kontaktformular</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="galerie" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_galerie">Bildergalerie</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="termin" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_termin">Terminbuchung</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="shop" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_shop">Produkte verkaufen</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="login" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_login">Geschützter Kundenbereich</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="blog" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_blog">Blog oder Neuigkeiten</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="sprachen" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_sprachen">Mehrere Sprachen</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="karte" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_karte">Karte / Anfahrt</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="newsletter" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_newsletter">Newsletter</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="checkbox" name="funktionen" value="app" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af2_f_app">Als App installierbar</span></span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Folgefragen: erscheinen nur, wenn sie zur Auswahl passen
|
||||||
|
(Masterplan S.10 "Intelligente Felder"). -->
|
||||||
|
<div class="wd-notiz" id="af-folge-shop" hidden style="margin-bottom:1.15rem">
|
||||||
|
<h3 style="font-size:.78rem;letter-spacing:.12em;text-transform:uppercase;color:var(--wd-blau);margin:0 0 .8rem" data-i18n="af2_shop_h3">Zum Verkauf</h3>
|
||||||
|
<div class="wd-feld" style="margin-bottom:.9rem">
|
||||||
|
<label for="af-shop-anzahl" data-i18n="af2_shop_anzahl">Wie viele Produkte ungefähr?</label>
|
||||||
|
<input type="text" id="af-shop-anzahl" name="d_shop_anzahl" inputmode="numeric" />
|
||||||
|
</div>
|
||||||
|
<div class="wd-feld" style="margin-bottom:.9rem">
|
||||||
|
<label for="af-shop-versand" data-i18n="af2_shop_versand">Verschickst du selbst?</label>
|
||||||
|
<input type="text" id="af-shop-versand" name="d_shop_versand" />
|
||||||
|
</div>
|
||||||
|
<div class="wd-feld" style="margin-bottom:0">
|
||||||
|
<label for="af-shop-zahlung" data-i18n="af2_shop_zahlung">Gewünschte Zahlungsarten</label>
|
||||||
|
<input type="text" id="af-shop-zahlung" name="d_shop_zahlung" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-notiz" id="af-folge-verwaltung" hidden>
|
||||||
|
<h3 style="font-size:.78rem;letter-spacing:.12em;text-transform:uppercase;color:var(--wd-blau);margin:0 0 .8rem" data-i18n="af2_vw_h3">Zur Verwaltung</h3>
|
||||||
|
<div class="wd-feld" style="margin-bottom:.9rem">
|
||||||
|
<label for="af-vw-rollen" data-i18n="af2_vw_rollen">Wie viele Personen arbeiten damit?</label>
|
||||||
|
<input type="text" id="af-vw-rollen" name="d_vw_rollen" />
|
||||||
|
</div>
|
||||||
|
<div class="wd-feld" style="margin-bottom:0">
|
||||||
|
<label for="af-vw-dokumente" data-i18n="af2_vw_dokumente">Welche Unterlagen sollen hinein?</label>
|
||||||
|
<input type="text" id="af-vw-dokumente" name="d_vw_dokumente" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SCHRITT 3 ============ -->
|
||||||
|
<div class="af-seite" data-seite="3">
|
||||||
|
<h2 data-i18n="af3_h2">Was hast du schon, und was ist der Rahmen?</h2>
|
||||||
|
<p class="wd-klein wd-leise" data-i18n="af3_hilfe">Bitte ehrlich antworten — fehlende Texte und Bilder sind der häufigste Grund, warum Projekte länger dauern.</p>
|
||||||
|
|
||||||
|
<div class="wd-tabelle-rahmen" style="margin:1.2rem 0 1.5rem">
|
||||||
|
<table class="wd-tabelle" style="min-width:0">
|
||||||
|
<tbody id="af-vorhanden"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld">
|
||||||
|
<label for="af-budget" data-i18n="af3_budget_label">Welcher Rahmen ist realistisch?</label>
|
||||||
|
<span class="wd-feld-hilfe" data-i18n="af3_budget_hilfe">Keine Bindung — die Angabe hilft nur, dir kein Angebot zu schreiben, das an deinem Rahmen vorbeigeht.</span>
|
||||||
|
<select id="af-budget" name="budget">
|
||||||
|
<option value="" data-i18n="af3_budget_wahl">Bitte auswählen</option>
|
||||||
|
<option value="bis-500" data-i18n="af3_budget_1">bis 500 €</option>
|
||||||
|
<option value="500-1000" data-i18n="af3_budget_2">500 – 1.000 €</option>
|
||||||
|
<option value="1000-2500" data-i18n="af3_budget_3">1.000 – 2.500 €</option>
|
||||||
|
<option value="ueber-2500" data-i18n="af3_budget_4">über 2.500 €</option>
|
||||||
|
<option value="unklar" data-i18n="af3_budget_5">Weiß ich noch nicht</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld">
|
||||||
|
<label for="af-termin"><span data-i18n="af3_termin_label">Bis wann sollte es fertig sein?</span> <span class="wd-leise" data-i18n="af_optional">optional</span></label>
|
||||||
|
<span class="wd-feld-hilfe" data-i18n="af3_termin_hilfe">Die Zeitrahmen auf der Preisseite gelten ab Eingang der Anzahlung, nicht ab heute.</span>
|
||||||
|
<input type="text" id="af-termin" name="termin" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ SCHRITT 4 ============ -->
|
||||||
|
<div class="af-seite" data-seite="4">
|
||||||
|
<h2 data-i18n="af4_h2">Wie erreiche ich dich?</h2>
|
||||||
|
|
||||||
|
<div class="wd-feld" id="feld-name">
|
||||||
|
<label for="af-name"><span data-i18n="af4_name">Name</span><span class="wd-pflicht" aria-hidden="true">*</span></label>
|
||||||
|
<input type="text" id="af-name" name="name" autocomplete="name" data-i18n-attr="placeholder:af4_name_platzhalter" />
|
||||||
|
<p class="wd-feld-fehler" data-fehler="name"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld" id="feld-email">
|
||||||
|
<label for="af-email"><span data-i18n="af4_email">E-Mail</span><span class="wd-pflicht" aria-hidden="true">*</span></label>
|
||||||
|
<input type="email" id="af-email" name="email" autocomplete="email" inputmode="email" />
|
||||||
|
<p class="wd-feld-fehler" data-fehler="email"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld">
|
||||||
|
<span class="wd-feld-legende" data-i18n="af4_weg">Wie darf ich mich melden?</span>
|
||||||
|
<div class="wd-wahl wd-wahl--2">
|
||||||
|
<label class="wd-wahl-kachel"><input type="radio" name="kontaktweg" value="email" checked /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af4_weg_email">Per E-Mail</span></span></label>
|
||||||
|
<label class="wd-wahl-kachel"><input type="radio" name="kontaktweg" value="telefon" /><span class="wd-wahl-inhalt"><span class="wd-wahl-titel" data-i18n="af4_weg_telefon">Per Telefon</span></span></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld" id="feld-telefon">
|
||||||
|
<label for="af-telefon"><span data-i18n="af4_telefon">Telefon</span> <span class="wd-leise" id="af-telefon-opt" data-i18n="af_optional">optional</span></label>
|
||||||
|
<input type="tel" id="af-telefon" name="telefon" autocomplete="tel" inputmode="tel" />
|
||||||
|
<p class="wd-feld-fehler" data-fehler="telefon"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld">
|
||||||
|
<label for="af-bemerkung"><span data-i18n="af4_bemerkung">Noch etwas, das ich wissen sollte?</span> <span class="wd-leise" data-i18n="af_optional">optional</span></label>
|
||||||
|
<span class="wd-feld-hilfe" data-i18n="af4_bemerkung_hilfe">Auch Fragen an mich gehören hier hinein.</span>
|
||||||
|
<textarea id="af-bemerkung" name="bemerkung" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wd-feld" id="feld-datenschutz">
|
||||||
|
<label class="wd-wahl-kachel" style="display:block">
|
||||||
|
<input type="checkbox" name="datenschutz" id="af-datenschutz" />
|
||||||
|
<span class="wd-wahl-inhalt">
|
||||||
|
<span class="wd-wahl-text" style="color:var(--wd-text-mid)" data-i18n="af4_datenschutz">Ich bin damit einverstanden, dass meine Angaben zur Bearbeitung dieser Anfrage gespeichert werden.</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<p class="wd-feld-fehler" data-fehler="datenschutz"></p>
|
||||||
|
<p class="wd-klein" style="margin:.6rem 0 0">
|
||||||
|
<a href="/kontakt.html" target="_blank" rel="noopener" data-i18n="af4_datenschutz_link">Datenschutz nachlesen</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ ZUSAMMENFASSUNG ============ -->
|
||||||
|
<div class="af-seite" data-seite="5">
|
||||||
|
<h2 data-i18n="af_zus_h2">Passt alles so?</h2>
|
||||||
|
<p class="wd-klein wd-leise" data-i18n="af_zus_hilfe">Das schickst du ab. Du kannst jeden Punkt noch ändern.</p>
|
||||||
|
<div id="af-zusammenfassung" style="margin-top:1.2rem"></div>
|
||||||
|
<div class="wd-hinweis" style="margin-top:1.4rem">
|
||||||
|
<p class="wd-hinweis-titel" data-i18n="af_kein_vertrag_titel">Das ist noch kein Auftrag</p>
|
||||||
|
<p data-i18n="af_kein_vertrag_text">Mit dem Absenden entsteht kein Vertrag und keine Zahlungspflicht. Du bekommst zuerst ein schriftliches Angebot und entscheidest dann in Ruhe.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ============ DANKE ============ -->
|
||||||
|
<div class="af-seite" data-seite="6">
|
||||||
|
<div class="af-danke">
|
||||||
|
<div class="af-danke-haken" aria-hidden="true">✓</div>
|
||||||
|
<h2 data-i18n="af_danke_h2">Angekommen!</h2>
|
||||||
|
<p data-i18n="af_danke_text">Deine Anfrage liegt bei mir. Ich melde mich innerhalb weniger Tage.</p>
|
||||||
|
<p class="wd-klein wd-leise" style="margin-bottom:.2rem" data-i18n="af_danke_nummer">Deine Anfragenummer</p>
|
||||||
|
<p class="af-nummer" id="af-nummer">—</p>
|
||||||
|
<p class="wd-klein wd-leise" data-i18n="af_danke_notieren">Notier sie dir — mit dieser Nummer findest du deine Anfrage sofort wieder.</p>
|
||||||
|
<div class="wd-btn-reihe" style="justify-content:center;margin-top:1.4rem">
|
||||||
|
<a class="wd-btn wd-btn--zweit" href="/webdesign/" data-i18n="af_danke_zurueck">Zurück zur Startseite</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="af-knopfleiste" id="af-knopfleiste">
|
||||||
|
<button type="button" class="wd-btn wd-btn--still" id="af-zurueck" data-i18n="af_zurueck">Zurück</button>
|
||||||
|
<button type="button" class="wd-btn wd-btn--haupt af-vor" id="af-weiter" data-i18n="af_weiter">Weiter</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="wd-klein wd-leise" style="margin-top:1rem;text-align:center" id="af-speicherhinweis" data-i18n="af_gespeichert">Deine Eingaben werden automatisch in diesem Browser gespeichert.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<div id="wd-fuss"></div>
|
||||||
|
|
||||||
|
<script src="/assets/js/i18n-wd-anfrage.js?v=20260822wd2"></script>
|
||||||
|
<script src="/assets/js/wd-core.js?v=20260822wd2"></script>
|
||||||
|
<script src="/assets/js/wd-anfrage.js?v=20260822wd2"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user