198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import React, { useState } from "react";
|
|
import emailjs from "@emailjs/browser";
|
|
import { ToastContainer, toast } from "react-toastify";
|
|
import "react-toastify/dist/ReactToastify.css";
|
|
import { useTranslation } from "react-i18next";
|
|
import { motion, useReducedMotion } from "framer-motion";
|
|
import PhoneCodeSelect from "./PhoneCodeSelect";
|
|
|
|
const PHONE_PATTERNS: Record<string, string> = {
|
|
"+351": "### ### ###",
|
|
"+34": "### ### ###",
|
|
"+33": "# ## ## ## ##",
|
|
"+44": "#### ######",
|
|
"+1": "(###) ###-####",
|
|
"+55": "(##) #####-####",
|
|
};
|
|
|
|
const digitsOnly = (value: string) => value.replace(/\D/g, "");
|
|
|
|
const maxDigitsFor = (pattern: string) =>
|
|
pattern.split("").filter((c) => c === "#").length;
|
|
|
|
const formatPhoneNumber = (digits: string, pattern: string) => {
|
|
let result = "";
|
|
let digitIndex = 0;
|
|
for (let i = 0; i < pattern.length && digitIndex < digits.length; i++) {
|
|
const ch = pattern[i];
|
|
if (ch === "#") {
|
|
result += digits[digitIndex];
|
|
digitIndex++;
|
|
} else {
|
|
result += ch;
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
|
|
const ContactForm = () => {
|
|
const { t } = useTranslation();
|
|
const prefersReducedMotion = useReducedMotion();
|
|
const [countryCode, setCountryCode] = useState("+351");
|
|
const [phoneDigits, setPhoneDigits] = useState("");
|
|
|
|
const activePattern = PHONE_PATTERNS[countryCode];
|
|
const formattedPhone = formatPhoneNumber(phoneDigits, activePattern);
|
|
|
|
const handleCountryChange = (code: string) => {
|
|
setCountryCode(code);
|
|
setPhoneDigits((prev) =>
|
|
prev.slice(0, maxDigitsFor(PHONE_PATTERNS[code]))
|
|
);
|
|
};
|
|
|
|
const handlePhoneChange = (raw: string) => {
|
|
const digits = digitsOnly(raw).slice(0, maxDigitsFor(activePattern));
|
|
setPhoneDigits(digits);
|
|
};
|
|
|
|
const notify = () => {
|
|
toast.success(t("contact.toasts.success"));
|
|
};
|
|
|
|
const errorToast = () => {
|
|
toast.error(t("contact.toasts.error"));
|
|
};
|
|
|
|
const emptyForm = () => {
|
|
toast.warning(t("contact.toasts.empty"));
|
|
};
|
|
|
|
const sendEmail = (e: React.FormEvent<HTMLFormElement>) => {
|
|
e.preventDefault();
|
|
|
|
const isEmpty = Array.from(e.currentTarget.elements).some((el) => {
|
|
if (el.tagName !== "INPUT" && el.tagName !== "TEXTAREA") return false;
|
|
const field = el as HTMLInputElement | HTMLTextAreaElement;
|
|
if (field.type === "hidden") return false;
|
|
return field.value.trim() === "";
|
|
});
|
|
|
|
if (isEmpty) {
|
|
emptyForm();
|
|
return;
|
|
}
|
|
|
|
emailjs
|
|
.sendForm("service_o7kxotj", "template_f30zlog", e.currentTarget, {
|
|
publicKey: "vcSOwPedqkRH3nmr9",
|
|
})
|
|
.then(
|
|
(result) => {
|
|
console.log(result.text);
|
|
notify();
|
|
},
|
|
(error) => {
|
|
console.log(error.text);
|
|
errorToast();
|
|
}
|
|
);
|
|
|
|
e.currentTarget.reset();
|
|
setPhoneDigits("");
|
|
setCountryCode("+351");
|
|
};
|
|
|
|
return (
|
|
<div className="mt-10 mx-5 mb-10 lg:mx-0 flex justify-center items-center">
|
|
<motion.form
|
|
onSubmit={sendEmail}
|
|
className="w-full md:w-[42rem] rounded-3xl glass-panel shadow-glass-lg p-6 md:p-10"
|
|
initial={prefersReducedMotion ? { opacity: 0 } : { opacity: 0, y: 24 }}
|
|
whileInView={
|
|
prefersReducedMotion ? { opacity: 1 } : { opacity: 1, y: 0 }
|
|
}
|
|
viewport={{ once: true, margin: "-80px" }}
|
|
transition={{ type: "spring", damping: 22, stiffness: 140 }}
|
|
>
|
|
<div className="flex flex-col md:flex-row md:gap-3">
|
|
<input
|
|
className="form-input flex-1"
|
|
type="text"
|
|
name="user_name"
|
|
placeholder={t("contact.form.name")}
|
|
/>
|
|
<input
|
|
className="form-input flex-1"
|
|
type="email"
|
|
name="user_email"
|
|
placeholder={t("contact.form.email")}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col md:flex-row md:gap-3">
|
|
<input
|
|
className="form-input flex-1"
|
|
type="text"
|
|
name="user_company"
|
|
placeholder={t("contact.form.company")}
|
|
/>
|
|
<div className="phone-input-group flex-1 flex items-center">
|
|
<PhoneCodeSelect value={countryCode} onChange={handleCountryChange} />
|
|
<span className="h-5 w-px bg-white/10 mx-2" aria-hidden="true" />
|
|
<input
|
|
className="phone-number-input"
|
|
type="tel"
|
|
inputMode="numeric"
|
|
name="user_phone"
|
|
value={formattedPhone}
|
|
onChange={(e) => handlePhoneChange(e.target.value)}
|
|
placeholder={t("contact.form.phone")}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col">
|
|
<input
|
|
className="form-input"
|
|
type="text"
|
|
name="user_question"
|
|
placeholder={t("contact.form.question")}
|
|
></input>
|
|
|
|
<textarea
|
|
className="form-input resize-none"
|
|
name="message"
|
|
placeholder={t("contact.form.message")}
|
|
rows={5}
|
|
></textarea>
|
|
</div>
|
|
|
|
<div className="flex mt-6 rounded-xl bg-gradient-to-br from-gold-light to-gold-mid p-0.5 shadow-gold-glow">
|
|
<motion.button
|
|
type="submit"
|
|
className="flex-1 font-semibold px-8 py-3 text-xl md:text-2xl bg-custom-bgdark text-white rounded-[0.65rem] hover:bg-transparent transition-colors duration-300"
|
|
whileTap={{ scale: 0.97 }}
|
|
transition={{ type: "spring", stiffness: 500, damping: 30 }}
|
|
>
|
|
{t("contact.form.button")}
|
|
</motion.button>
|
|
</div>
|
|
|
|
<ToastContainer
|
|
position="top-center"
|
|
autoClose={5000}
|
|
hideProgressBar
|
|
newestOnTop={false}
|
|
closeOnClick
|
|
rtl={false}
|
|
pauseOnFocusLoss
|
|
draggable
|
|
pauseOnHover
|
|
theme="dark"
|
|
/>
|
|
</motion.form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ContactForm;
|