diff --git a/assets/js/kontaktformular.js b/assets/js/kontaktformular.js index 5176287..8e29dcf 100644 --- a/assets/js/kontaktformular.js +++ b/assets/js/kontaktformular.js @@ -1,152 +1,229 @@ -window.onload = function () { - const FORMDEBUG = false - const btn = document.getElementById('kontaktformular-btn') - const kontaktformular = document.getElementById('kontaktformular') +// Configuration and Messages +const debugEnabled = true +const mouseDebugEnabled = false +const zsrCheckEnabled = false +const interactionThreshold = 15 // Time in seconds +const interactionCountThreshold = 5 // Number of interactions +let botDetected = false +let currentMessages = [] - // custom Validation messages - const messagesGerman = { +const messages = { + de: { required: 'Bitte füllen Sie dieses Feld aus', email: 'Bitte geben Sie eine gültige E-Mail-Adresse ein', - minlength: 'Bitte geben Sie mindestens {0} Zeichen ein', - maxlength: 'Bitte geben Sie maximal {0} Zeichen ein', - min: 'Bitte geben Sie mindestens {0} ein', - max: 'Bitte geben Sie maximal {0} ein', - range: 'Bitte geben Sie zwischen {0} und {1} ein', - } - const messagesFrench = { + success: 'Die Bestellung wurde erfolgreich übermittelt!', + thankyou: 'Vielen Dank für Ihre Anfrage.', + zsrTooltip: 'Bitte geben Sie eine gültige ZSR-Nummer, oder "beantragt" ein.', + captcha: 'Geben Sie den angezeigten Captcha-Code ein', + captchaButton: 'Überprüfen', + }, + fr: { required: 'Veuillez remplir ce champ', email: 'Veuillez saisir une adresse email valide', - minlength: 'Veuillez saisir au moins {0} caractères', - maxlength: 'Veuillez saisir au plus {0} caractères', - min: 'Veuillez saisir au moins {0} caractères', - max: 'Veuillez saisir au plus {0} caractères', - range: 'Veuillez saisir au moins {0} et au plus {1} caractères', + success: 'La commande a bien été envoyée!', + thankyou: 'Merci de votre demande.', + zsrTooltip: 'Veuillez saisir une ZSR-Nummer valide, ou indiquer "demandé".', + captcha: 'Entrez le code Captcha affiché', + captchaButton: 'Vérifier', + }, +} + +// DOM Selectors +const debugLabel = document.createElement('div') +const submitButton = document.getElementById('kontaktformular-btn') +const form = document.querySelector('form#kontaktformular') +const notification = document.getElementById('notification') +const zsrTooltip = document.getElementById('zsr-tooltip') +const honeypotInput1 = document.getElementById('age') +const honeypotInput2 = document.getElementById('hobbies') +const verifyEmailInput = document.getElementById('verify_email') +const emailInput = document.getElementById('mail') +const textInputs = document.querySelectorAll('input[type="text"]') +const captcha = document.querySelectorAll('.captcha') +const captchaInput = document.querySelectorAll('.captcha-input') +const captchaVerifyButton = document.querySelectorAll('.captcha-verify') +const botBadge = document.createElement('div') +if (debugEnabled) { + botBadge.className = 'bot-badge' + document.body.appendChild(botBadge) + botBadge.setAttribute( + 'style', + 'position: fixed; top: 0; right: 0; z-index: 9999; background-color: red; color: white; font-weight: bold; height:20px; width:20px' + ) +} + +// Utility variables +let startTime = Date.now() +let interactionCount = 0 +let userInteracted = false +let lastInteractionTime = null +const mousePositions = [] +const interactionTimes = [] +let isStraightLine = true + +// Utility functions +function log(thing) { + console.log(thing) +} + +function getCurrentLangMessages() { + log(messages[document.documentElement.lang.split('-')[0]]) + return messages[document.documentElement.lang.split('-')[0]] +} + +function setUserInteracted() { + userInteracted = true + interactionCount++ + checkForBotBehavior() +} + +function handleMouseMove(event) { + mousePositions.push({ x: event.clientX, y: event.clientY }) + if (debugEnabled && mouseDebugEnabled) log('Mouse Position:', { x: event.clientX, y: event.clientY }) + + if (mousePositions.length > 2) { + const len = mousePositions.length + const { x: x1, y: y1 } = mousePositions[len - 3] + const { x: x2, y: y2 } = mousePositions[len - 2] + const { x: x3, y: y3 } = mousePositions[len - 1] + + // Calculate the area of the triangle formed by three consecutive points + const area = 0.5 * Math.abs(x1 * y2 + x2 * y3 + x3 * y1 - y1 * x2 - y2 * x3 - y3 * x1) + if (debugEnabled && mouseDebugEnabled) log('Triangle Area:', area) + + if (area > 0.5) { + // Threshold for detecting non-straight line, adjust as needed + isStraightLine = false + if (debugEnabled && mouseDebugEnabled) log('Detected non-straight line movement.') + } + } +} + +function checkForBotBehavior() { + let timeSpent = (Date.now() - startTime) / 1000 + botDetected = + !userInteracted || + interactionCount === 0 || + honeypotInput1.value !== '' || + honeypotInput2.value !== '' || + verifyEmailInput.value !== '' + if (debugEnabled) + if (!botDetected) { + botBadge.style.backgroundColor = 'green' + } else { + botBadge.style.backgroundColor = 'red' + } + console.log( + 'Bot Detected: ' + + botDetected + + ' userInteracted:' + + userInteracted + + ' interactionCount:' + + interactionCount + + ' timeSpent:' + + timeSpent + + ' isStraightLine:' + + isStraightLine + + ' mousePositions:' + + mousePositions.length + + ' honeypotInput1:' + + honeypotInput1.value + + ' honeypotInput2:' + + honeypotInput2.value + + ' verifyEmailInput:' + + verifyEmailInput.value + ) +} +function handleSubmit(e) { + e.preventDefault() + + currentMessages = getCurrentLangMessages() + + checkForBotBehavior() + if (botDetected) { + fakeOut() + return false } - // custom Validation rules - // determine which language depending on html lang attribute - const lang = document.documentElement.lang - console.log('lang', lang) - const messages = lang === 'de-DE' ? messagesGerman : messagesFrench - // set custom validation messages for each validator - console.log('messages', messages) + const formData = new FormData(form) + const formDataEncoded = new URLSearchParams(formData).toString() + const formURL = form.action + '.json' - let textInputs = document.querySelectorAll('input[type="text"]') - const emailInput = document.getElementById('email') - - Array.from(textInputs).forEach(function (input) { - input.addEventListener('invalid', function () { - this.setCustomValidity(messages['required']) + fetch(formURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', // Wichtig für die Vermeidung von CORS-Problemen + }, + body: formDataEncoded, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Network response was not ok') + } + return response.json() }) - }) - - emailInput.addEventListener('invalid', function () { - this.setCustomValidity(messages['email']) - }) - - - // initieiere Zeitmessung zur Botprevention - var startTime = Date.now() - - // Messe ob mit der Seite agiert wird - var userInteracted = false - - function setUserInteracted() { - var timeSpent = (Date.now() - startTime) / 1000 // Zeit in Sekunden - if (timeSpent > 5) { - btn.disabled = false - } - userInteracted = true - } - setTimeout(function () { - if (userInteracted) { - btn.disabled = false - } - }, 5000) - // Eventlistener für Interaktionen - setzt userInteracted auf true bei Interaktion + .then((data) => { + submitButton.disabled = true + submitButton.innerHTML = ` + + ` + setTimeout(() => { + // if data.success is true, show a success message + if (data) { + submitButton.style.display = 'none' + notification.innerHTML = `${currentMessages.thankyou}` + notification.className = 'bg-green-500 text-white px-4 py-2 rounded block' + } else { + submitButton.style.display = 'none' + notification.textContent = 'Es gab ein Problem mit Ihrer Anfrage' + notification.className = 'bg-blue-500 text-white px-4 py-2 rounded block' + } + }, 3000) + }) + .catch((error) => { + submitButton.style.display = 'none' + notification.textContent = 'Fehler beim Senden der Nachricht.' + notification.className = 'bg-blue-500 text-white px-4 py-2 rounded block' + console.error(error) + }) +} +function fakeOut() { + submitButton.disabled = true + submitButton.innerHTML = ` + + ` + setTimeout(() => { + submitButton.style.display = 'none' + notification.innerHTML = `${currentMessages.thankyou}` + notification.className = 'bg-green-500 text-white px-4 py-2 rounded block' + }, 3000) +} +function init() { + // Event Listeners document.addEventListener('mousedown', setUserInteracted) document.addEventListener('touchstart', setUserInteracted) document.addEventListener('keydown', setUserInteracted) - - kontaktformular.addEventListener('submit', function (e) { - e.preventDefault() - - const form = e.target - const notification = document.getElementById('notification') - const zsrTooltip = document.getElementById('zsr-tooltip') - - // Spinner und button disabled anzeigen - - var endTime = Date.now() - var timeSpent = (endTime - startTime) / 1000 // Zeit in Sekunden - - // Setze die Werte für die Botvalidierung zum Auswerten in PHP - document.getElementById('age').value = timeSpent - document.getElementById('hobbies').value = userInteracted ? 'true' : 'false' - - btn.innerHTML = ` - - - - - - ` - - btn.disabled = true - - if (FORMDEBUG) { - console.log('userInteracted: ' + userInteracted) - console.log('timeSpent: ' + timeSpent) - console.log('hobbies: ' + document.getElementById('hobbies').value) - console.log('age: ' + document.getElementById('age').value) - console.log('verify_email(honeypot): ' + document.getElementById('verify_email').value) - } - - // Konvertiere das JSON-Objekt in einen String, um es zu senden - const formData = new FormData(form) - const formDataEncoded = new URLSearchParams(formData).toString() - const formURL = form.action + '.json' - - fetch(formURL, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', // Wichtig für die Vermeidung von CORS-Problemen - }, - body: formDataEncoded, + document.addEventListener('mousemove', handleMouseMove) + form.addEventListener('submit', handleSubmit) + emailInput.addEventListener('invalid', () => { + const currentMessages = getCurrentLangMessages() + emailInput.setCustomValidity(currentMessages.email) + }) + textInputs.forEach((input) => { + input.addEventListener('invalid', () => { + const currentMessages = getCurrentLangMessages() + input.setCustomValidity(currentMessages.required) }) - .then((response) => { - if (!response.ok) { - throw new Error('Network response was not ok') - } - return response.json() - }) - .then((data) => { - // Erfolgsnachricht anzeigen - // TODO Nachricht anpassen wenn französische Version - notification.textContent = 'Anfrage erfolgreich versendet.' - btn.className = 'submitbutton text-white mx-auto submit-after-valid-captchaaaa fadeOut' - setTimeout(() => { - btn.style.visibility = 'hidden' - btn.style.display = 'none' - notification.style.visibility = 'visible' - notification.style.display = 'block' - notification.classList.remove('fadeIn') // Remove fadeIn class - void notification.offsetWidth - notification.className = 'bg-green-500 text-white px-4 py-2 rounded block fadeIn' - }, 1000) - // setTimeout(() => notification.className = 'bg-green-500 text-white px-4 py-2 rounded hidden', 5000); // Benachrichtigung nach 5 Sekunden ausblenden - }) - .catch((error) => { - // Fehlermeldung anzeigen - notification.textContent = 'Fehler beim Senden der Nachricht.' - console.log(error) - notification.className = 'bg-red-500 text-white px-4 py-2 rounded block' - // setTimeout(() => notification.className = 'bg-red-500 text-white px-4 py-2 rounded hidden', 5000); // Benachrichtigung nach 5 Sekunden ausblenden - }) + }) + log('captchaInput', captchaInput) + captchaInput?.forEach((input) => { + input.setAttribute('placeholder', getCurrentLangMessages().captcha) + }) + captchaVerifyButton?.forEach((button) => { + button.textContent = getCurrentLangMessages().captchaButton }) } + +document.addEventListener('DOMContentLoaded', function () { + init() +})