Add captcha verification to contact form and improve error handling
This commit is contained in:
parent
5bb164f1c5
commit
b010563b62
9 changed files with 187 additions and 31 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useRef } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import "./contact-form.style.scss";
|
import "./contact-form.style.scss";
|
||||||
|
|
@ -9,6 +9,28 @@ import YellowPlanet from "../../assets/images/main/contact-form/Planet Yellow.sv
|
||||||
import FormInput from "../form-input/form-input.component";
|
import FormInput from "../form-input/form-input.component";
|
||||||
import CustomButton from "../custom-button/custom-button.component";
|
import CustomButton from "../custom-button/custom-button.component";
|
||||||
|
|
||||||
|
const verifyCaptcha = async (captchaToken) => {
|
||||||
|
const keyId = "8e5272fb9c08";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`https://cap.midcreative.eu/${keyId}/siteverify`,
|
||||||
|
{
|
||||||
|
response: captchaToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response.data.success;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Captcha verification failed:", error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const newPost = async (data) => {
|
const newPost = async (data) => {
|
||||||
return axios
|
return axios
|
||||||
.post("https://midcreative.eu/mail/", data, {
|
.post("https://midcreative.eu/mail/", data, {
|
||||||
|
|
@ -17,7 +39,10 @@ const newPost = async (data) => {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((res) => true)
|
.then((res) => true)
|
||||||
.catch((err) => err);
|
.catch((err) => {
|
||||||
|
console.error("Form submission error:", err);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const ContactForm = () => {
|
const ContactForm = () => {
|
||||||
|
|
@ -34,6 +59,8 @@ const ContactForm = () => {
|
||||||
|
|
||||||
const [success, setSuccess] = useState();
|
const [success, setSuccess] = useState();
|
||||||
const [error, setError] = useState();
|
const [error, setError] = useState();
|
||||||
|
const [captchaToken, setCaptchaToken] = useState(null);
|
||||||
|
const [captchaError, setCaptchaError] = useState(false);
|
||||||
|
|
||||||
const setName = (name) => {
|
const setName = (name) => {
|
||||||
setFormData({ ...formData, name: name });
|
setFormData({ ...formData, name: name });
|
||||||
|
|
@ -53,21 +80,69 @@ const ContactForm = () => {
|
||||||
phoneInputRef.current.clean();
|
phoneInputRef.current.clean();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePost = (e) => {
|
useEffect(() => {
|
||||||
|
const handleCaptchaCompleted = (event) => {
|
||||||
|
if (event.detail && event.detail.token) {
|
||||||
|
setCaptchaToken(event.detail.token);
|
||||||
|
setCaptchaError(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const captchaWidget = document.querySelector('cap-widget');
|
||||||
|
if (captchaWidget) {
|
||||||
|
captchaWidget.addEventListener('cap-completed', handleCaptchaCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (captchaWidget) {
|
||||||
|
captchaWidget.removeEventListener('cap-completed', handleCaptchaCompleted);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handlePost = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
newPost(formData).then((res) => {
|
if (!captchaToken) {
|
||||||
if (res === true) {
|
setCaptchaError(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCaptchaValid = await verifyCaptcha(captchaToken);
|
||||||
|
|
||||||
|
if (!isCaptchaValid) {
|
||||||
|
setCaptchaError(true);
|
||||||
|
setCaptchaToken(null);
|
||||||
|
const captchaWidget = document.querySelector('cap-widget');
|
||||||
|
if (captchaWidget && captchaWidget.reset) {
|
||||||
|
captchaWidget.reset();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await newPost(formData);
|
||||||
|
|
||||||
|
if (response === true) {
|
||||||
cleanFields();
|
cleanFields();
|
||||||
setError(false);
|
setError(false);
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
|
setCaptchaToken(null);
|
||||||
|
setCaptchaError(false);
|
||||||
|
const captchaWidget = document.querySelector('cap-widget');
|
||||||
|
if (captchaWidget && captchaWidget.reset) {
|
||||||
|
captchaWidget.reset();
|
||||||
|
}
|
||||||
setTimeout(() => setSuccess(false), 6000);
|
setTimeout(() => setSuccess(false), 6000);
|
||||||
} else {
|
} else {
|
||||||
cleanFields();
|
|
||||||
setError(true);
|
setError(true);
|
||||||
setSuccess(false);
|
setSuccess(false);
|
||||||
}
|
}
|
||||||
});
|
} catch (err) {
|
||||||
|
console.error("Form submission failed:", err);
|
||||||
|
setError(true);
|
||||||
|
setSuccess(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -113,6 +188,16 @@ const ContactForm = () => {
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<cap-widget
|
||||||
|
data-cap-api-endpoint="https://cap.midcreative.eu/8e5272fb9c08/api/"
|
||||||
|
></cap-widget>
|
||||||
|
|
||||||
|
{captchaError && (
|
||||||
|
<p className="error" style={{ marginTop: '10px' }}>
|
||||||
|
{t("contacts.captchaError") || "Please complete the captcha"}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<CustomButton text={t("contacts.send")} />
|
<CustomButton text={t("contacts.send")} />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Link } from "gatsby";
|
||||||
import "./footer.style.scss";
|
import "./footer.style.scss";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { getLocalizedPath } from "../../utils/navigation";
|
||||||
|
|
||||||
import Contacts from "../contacts/contacts.component";
|
import Contacts from "../contacts/contacts.component";
|
||||||
import TopLine from "../../assets/images/main/footer/top-line.png";
|
import TopLine from "../../assets/images/main/footer/top-line.png";
|
||||||
|
|
@ -27,21 +29,21 @@ const MidFooter = () => {
|
||||||
</div>
|
</div>
|
||||||
<nav>
|
<nav>
|
||||||
<h6>{t("footer.navigation")}</h6>
|
<h6>{t("footer.navigation")}</h6>
|
||||||
<a href="#top" className="gray mobile">
|
<Link to={getLocalizedPath("/#top")} className="gray mobile">
|
||||||
{t("menu.home")}
|
{t("menu.home")}
|
||||||
</a>
|
</Link>
|
||||||
<a href="#services" className="gray">
|
<Link to={getLocalizedPath("/#services")} className="gray">
|
||||||
{t("menu.services")}
|
{t("menu.services")}
|
||||||
</a>
|
</Link>
|
||||||
<a href="#about-us" className="gray">
|
<Link to={getLocalizedPath("/#about-us")} className="gray">
|
||||||
{t("menu.about_us")}
|
{t("menu.about_us")}
|
||||||
</a>
|
</Link>
|
||||||
<a href="#projects" className="gray">
|
<Link to={getLocalizedPath("/#projects")} className="gray">
|
||||||
{t("menu.portfolio")}
|
{t("menu.portfolio")}
|
||||||
</a>
|
</Link>
|
||||||
<a href="#contact-us" className="gray">
|
<Link to={getLocalizedPath("/#contact-us")} className="gray">
|
||||||
{t("menu.contact_us")}
|
{t("menu.contact_us")}
|
||||||
</a>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
<div>
|
<div>
|
||||||
<img src={Logo} alt="" />
|
<img src={Logo} alt="" />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Link } from "gatsby";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import LanguageSwitch from "../language-switch/language-switch.component";
|
import LanguageSwitch from "../language-switch/language-switch.component";
|
||||||
|
import { getLocalizedPath } from "../../utils/navigation";
|
||||||
|
|
||||||
import Logo from "../../assets/images/main/header/logo.svg";
|
import Logo from "../../assets/images/main/header/logo.svg";
|
||||||
import Logo2 from "../../assets/images/main/footer/logo.svg";
|
import Logo2 from "../../assets/images/main/footer/logo.svg";
|
||||||
|
|
@ -64,10 +65,10 @@ const MidHeader = () => {
|
||||||
<img src={Logo} alt="MID" />
|
<img src={Logo} alt="MID" />
|
||||||
</div>
|
</div>
|
||||||
<nav className="navigation">
|
<nav className="navigation">
|
||||||
<Link to="/#services">{t("menu.services")}</Link>
|
<Link to={getLocalizedPath("/#services")}>{t("menu.services")}</Link>
|
||||||
<Link to="/#about-us">{t("menu.about_us")}</Link>
|
<Link to={getLocalizedPath("/#about-us")}>{t("menu.about_us")}</Link>
|
||||||
<Link to="/#projects">{t("menu.portfolio")}</Link>
|
<Link to={getLocalizedPath("/#projects")}>{t("menu.portfolio")}</Link>
|
||||||
<Link to="/#contact-us">{t("menu.contact_us")}</Link>
|
<Link to={getLocalizedPath("/#contact-us")}>{t("menu.contact_us")}</Link>
|
||||||
|
|
||||||
<LanguageSwitch />
|
<LanguageSwitch />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
@ -91,11 +92,11 @@ const MidHeader = () => {
|
||||||
<img src={Logo2} alt="MID Creative Digital Agency" />
|
<img src={Logo2} alt="MID Creative Digital Agency" />
|
||||||
|
|
||||||
<nav className="navigation">
|
<nav className="navigation">
|
||||||
<a href="#top">{t("menu.home")}</a>
|
<Link to={getLocalizedPath("/#top")}>{t("menu.home")}</Link>
|
||||||
<a href="#services">{t("menu.services")}</a>
|
<Link to={getLocalizedPath("/#services")}>{t("menu.services")}</Link>
|
||||||
<a href="#about-us">{t("menu.about_us")}</a>
|
<Link to={getLocalizedPath("/#about-us")}>{t("menu.about_us")}</Link>
|
||||||
<a href="#projects">{t("menu.portfolio")}</a>
|
<Link to={getLocalizedPath("/#projects")}>{t("menu.portfolio")}</Link>
|
||||||
<a href="#contact-us">{t("menu.contact_us")}</a>
|
<Link to={getLocalizedPath("/#contact-us")}>{t("menu.contact_us")}</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="header-contacts">
|
<div className="header-contacts">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { navigate } from "gatsby";
|
||||||
import i18n from "../../i18n";
|
import i18n from "../../i18n";
|
||||||
|
|
||||||
import "./language-switch.style.scss";
|
import "./language-switch.style.scss";
|
||||||
|
|
@ -6,6 +7,13 @@ import "./language-switch.style.scss";
|
||||||
const LanguageSwitch = ({ t }) => {
|
const LanguageSwitch = ({ t }) => {
|
||||||
const changeLanguage = (lng) => {
|
const changeLanguage = (lng) => {
|
||||||
i18n.changeLanguage(lng);
|
i18n.changeLanguage(lng);
|
||||||
|
|
||||||
|
// Navigate to the appropriate route
|
||||||
|
if (lng === "lv") {
|
||||||
|
navigate("/lv");
|
||||||
|
} else {
|
||||||
|
navigate("/");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ export const Seo = ({ title, description, pathname, children }) => {
|
||||||
gtag('config', 'G-8M9CZ79T4L');
|
gtag('config', 'G-8M9CZ79T4L');
|
||||||
`}
|
`}
|
||||||
</Script>
|
</Script>
|
||||||
|
<Script
|
||||||
|
src="https://cap.midcreative.eu/8e5272fb9c08/widget.js"
|
||||||
|
strategy="idle"
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,13 @@ i18n
|
||||||
.use(initReactI18next) // passes i18n down to react-i18next
|
.use(initReactI18next) // passes i18n down to react-i18next
|
||||||
.init({
|
.init({
|
||||||
resources,
|
resources,
|
||||||
lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources
|
lng: typeof window !== "undefined" && window.location.pathname.startsWith("/lv") ? "lv" : "en", // Detect language from URL
|
||||||
// you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage
|
|
||||||
// if you're using a language detector, do not define the lng option
|
|
||||||
fallbackLng: "en", // use en if detected lng is not available
|
fallbackLng: "en", // use en if detected lng is not available
|
||||||
|
detection: {
|
||||||
|
// Disable automatic detection to respect URL-based language
|
||||||
|
order: ["path", "localStorage", "navigator"],
|
||||||
|
caches: ["localStorage"],
|
||||||
|
},
|
||||||
|
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false, // react already safes from xss
|
escapeValue: false, // react already safes from xss
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,18 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
import { Seo } from "../components/seo";
|
import { Seo } from "../components/seo";
|
||||||
|
import i18n from "../i18n";
|
||||||
|
|
||||||
import "../assets/stylesheets/style.scss";
|
import "../assets/stylesheets/style.scss";
|
||||||
|
|
||||||
import MidMainPage from "../main/main.component";
|
import MidMainPage from "../main/main.component";
|
||||||
|
|
||||||
const IndexPage = () => {
|
const IndexPage = () => {
|
||||||
|
useEffect(() => {
|
||||||
|
// Ensure English is set for the root route
|
||||||
|
i18n.changeLanguage("en");
|
||||||
|
}, []);
|
||||||
|
|
||||||
return <MidMainPage></MidMainPage>;
|
return <MidMainPage></MidMainPage>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,8 @@
|
||||||
"phone": "phone",
|
"phone": "phone",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"success": "Thank you for sharing your contacts! We will get in touch!",
|
"success": "Thank you for sharing your contacts! We will get in touch!",
|
||||||
"error": "Looks like something went wrong, please try to reach us directly "
|
"error": "Looks like something went wrong, please try to reach us directly ",
|
||||||
|
"captchaError": "Please complete the captcha verification"
|
||||||
},
|
},
|
||||||
"privacy": {
|
"privacy": {
|
||||||
"title": "Privacy Policy",
|
"title": "Privacy Policy",
|
||||||
|
|
@ -144,5 +145,27 @@
|
||||||
"registration_nr": "Registration nr.",
|
"registration_nr": "Registration nr.",
|
||||||
"address": "Address",
|
"address": "Address",
|
||||||
"navigation": "Navigation"
|
"navigation": "Navigation"
|
||||||
|
},
|
||||||
|
"landingPromo": {
|
||||||
|
"title": "Professional Landing Page Development in Just 2 Business Days – €150!",
|
||||||
|
"subtitle": "Looking to attract new customers quickly and effectively? Our experienced team specializes in creating efficient and modern landing pages that will help achieve your business goals.",
|
||||||
|
"whatYouGet": "What you get:",
|
||||||
|
"benefit1": "Customized landing page tailored to your brand and needs",
|
||||||
|
"benefit2": "Modern and user-friendly design",
|
||||||
|
"benefit3": "Mobile-responsive solution",
|
||||||
|
"benefit4": "Optimized page loading speed and SEO basics",
|
||||||
|
"benefit5": "Free consultation on content and structure creation",
|
||||||
|
"whyUs": "Why choose us:",
|
||||||
|
"feature1Title": "Fast Development",
|
||||||
|
"feature1": "Guaranteed delivery in just 2 business days",
|
||||||
|
"feature2Title": "Fixed Price",
|
||||||
|
"feature2": "€150 - no hidden costs",
|
||||||
|
"feature3Title": "Experienced Team",
|
||||||
|
"feature3": "Friendly and responsive team with website development experience",
|
||||||
|
"feature4Title": "Long-term Support",
|
||||||
|
"feature4": "Support even after page creation",
|
||||||
|
"ctaTitle": "Contact us today and get your new landing page in just two days!",
|
||||||
|
"ctaText": "Book a consultation – we'll help bring your ideas to life!",
|
||||||
|
"ctaButton": "Contact Us"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,35 @@
|
||||||
"phone": "Telefons",
|
"phone": "Telefons",
|
||||||
"send": "Nosūtīt",
|
"send": "Nosūtīt",
|
||||||
"success": "Paldies ",
|
"success": "Paldies ",
|
||||||
"error": "Izskatās, ka kautkas nestrādā :( Uzrakstiet mums uz "
|
"error": "Izskatās, ka kautkas nestrādā :( Uzrakstiet mums uz ",
|
||||||
|
"captchaError": "Lūdzu, aizpildiet captcha verifikāciju"
|
||||||
},
|
},
|
||||||
"footer": {
|
"footer": {
|
||||||
"company": "Kompānija",
|
"company": "Kompānija",
|
||||||
"registration_nr": "Reģistrācijas nr.",
|
"registration_nr": "Reģistrācijas nr.",
|
||||||
"address": "Adrese",
|
"address": "Adrese",
|
||||||
"navigation": "Navigācija"
|
"navigation": "Navigācija"
|
||||||
|
},
|
||||||
|
"landingPromo": {
|
||||||
|
"title": "Profesionāla landing lapu izstrāde tikai 2 darba dienu laikā – 150 €!",
|
||||||
|
"subtitle": "Vai vēlaties ātri un kvalitatīvi piesaistīt jaunus klientus? Mūsu pieredzējusī komanda specializējas efektīvu un modernu landing lapu izstrādē, kas palīdzēs sasniegt jūsu biznesa mērķus.",
|
||||||
|
"whatYouGet": "Ko jūs iegūstat:",
|
||||||
|
"benefit1": "Individuāli pielāgotu landing lapu atbilstoši jūsu zīmolam un vajadzībām",
|
||||||
|
"benefit2": "Mūsdienīgu un lietotājam draudzīgu dizainu",
|
||||||
|
"benefit3": "Mobilajām ierīcēm pielāgotu risinājumu",
|
||||||
|
"benefit4": "Optimizētu lapas ielādes ātrumu un SEO pamatus",
|
||||||
|
"benefit5": "Bezmaksas konsultāciju par satura un struktūras izveidi",
|
||||||
|
"whyUs": "Kāpēc izvēlēties mūs:",
|
||||||
|
"feature1Title": "Ātra izstrāde",
|
||||||
|
"feature1": "Garantēta izstrāde tikai 2 darba dienu laikā",
|
||||||
|
"feature2Title": "Fiksēta cena",
|
||||||
|
"feature2": "150 € - bez slēptām izmaksām",
|
||||||
|
"feature3Title": "Pieredzējuša komanda",
|
||||||
|
"feature3": "Draudzīga un atsaucīga komanda ar pieredzi mājaslapu izstrādē",
|
||||||
|
"feature4Title": "Ilgtermiņa atbalsts",
|
||||||
|
"feature4": "Atbalsts arī pēc lapas izveides",
|
||||||
|
"ctaTitle": "Sazinieties ar mums jau šodien un saņemiet savu jauno landing lapu tikai divu dienu laikā!",
|
||||||
|
"ctaText": "Piesakieties konsultācijai – mēs palīdzēsim īstenot jūsu idejas!",
|
||||||
|
"ctaButton": "Sazināties ar mums"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue