redesigned app for cleaner ux
This commit is contained in:
1 parent
fe468885b0
commit
2a3f542e98
33 files changed
+1379
-374
No files matched your search
@@ -1,11 +1,60 @@
|
||||
import React from "react";
|
||||
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"));
|
||||
@@ -22,11 +71,12 @@ const ContactForm = () => {
|
||||
const sendEmail = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
let isEmpty = Array.from(e.currentTarget.elements).some(
|
||||
(input) =>
|
||||
(input as HTMLInputElement).type !== "submit" &&
|
||||
(input as HTMLInputElement).value.trim() === ""
|
||||
);
|
||||
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();
|
||||
@@ -49,20 +99,31 @@ const ContactForm = () => {
|
||||
);
|
||||
|
||||
e.currentTarget.reset();
|
||||
setPhoneDigits("");
|
||||
setCountryCode("+351");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-10 mx-5 lg:mx-0 flex justify-center items-center">
|
||||
<form onSubmit={sendEmail} className="w-full md:w-auto">
|
||||
<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"
|
||||
className="form-input flex-1"
|
||||
type="text"
|
||||
name="user_name"
|
||||
placeholder={t("contact.form.name")}
|
||||
/>
|
||||
<input
|
||||
className="form-input"
|
||||
className="form-input flex-1"
|
||||
type="email"
|
||||
name="user_email"
|
||||
placeholder={t("contact.form.email")}
|
||||
@@ -70,17 +131,24 @@ const ContactForm = () => {
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row md:gap-3">
|
||||
<input
|
||||
className="form-input"
|
||||
className="form-input flex-1"
|
||||
type="text"
|
||||
name="user_company"
|
||||
placeholder={t("contact.form.company")}
|
||||
/>
|
||||
<input
|
||||
className="form-input"
|
||||
type="number"
|
||||
name="user_phone"
|
||||
placeholder={t("contact.form.phone")}
|
||||
/>
|
||||
<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
|
||||
@@ -91,20 +159,22 @@ const ContactForm = () => {
|
||||
></input>
|
||||
|
||||
<textarea
|
||||
className="form-input"
|
||||
className="form-input resize-none"
|
||||
name="message"
|
||||
placeholder={t("contact.form.message")}
|
||||
rows={6}
|
||||
rows={5}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-5 mb-10 lg:mt-12 mx-10 lg:mx-20 rounded-xl bg-gradient-to-br from-gold-light to-gold-dark p-0.5 shadow-lg">
|
||||
<button
|
||||
<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-medium md:px-8 py-3 text-2xl md:text-3xl bg-gradient-to-br from-custom-bglight to-custom-bgdark rounded-xl hover:bg-transparent hover:from-transparent hover:to-transparent transition duration-500"
|
||||
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")}
|
||||
</button>
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<ToastContainer
|
||||
@@ -119,7 +189,7 @@ const ContactForm = () => {
|
||||
pauseOnHover
|
||||
theme="dark"
|
||||
/>
|
||||
</form>
|
||||
</motion.form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user