diff --git a/package-lock.json b/package-lock.json index 0a0cfda..cf5f389 100644 --- a/package-lock.json +++ b/package-lock.json @@ -60,7 +60,8 @@ "sonner": "^2.0.3", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.9" + "vaul": "^0.9.9", + "yup": "^1.6.1" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", @@ -5939,6 +5940,12 @@ "react-is": "^16.13.1" } }, + "node_modules/property-expr": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz", + "integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -6664,6 +6671,12 @@ "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==", "license": "MIT" }, + "node_modules/tiny-case": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz", + "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -6683,6 +6696,12 @@ "node": ">=8.0" } }, + "node_modules/toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==", + "license": "MIT" + }, "node_modules/ts-api-utils": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", @@ -7049,6 +7068,30 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/yup": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz", + "integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==", + "license": "MIT", + "dependencies": { + "property-expr": "^2.0.5", + "tiny-case": "^1.0.3", + "toposort": "^2.0.2", + "type-fest": "^2.19.0" + } + }, + "node_modules/yup/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 61fe41c..fb469ec 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,8 @@ "sonner": "^2.0.3", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", - "vaul": "^0.9.9" + "vaul": "^0.9.9", + "yup": "^1.6.1" }, "devDependencies": { "@tailwindcss/postcss": "^4.1.11", diff --git a/pages/StartAProject.tsx b/pages/StartAProject.tsx index feedfb5..60edea0 100644 --- a/pages/StartAProject.tsx +++ b/pages/StartAProject.tsx @@ -20,7 +20,10 @@ import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group"; import { Label } from "../components/ui/label"; import { GridPattern } from "../components/GridPattern"; import CustomReCaptcha, { ReCaptchaRef } from "../components/CustomReCaptcha"; -// import { navigate } from "../App"; +import { useStoreContactUsMutation } from "@/src/services/storeContactUs"; +import { useNavigate } from "react-router-dom"; +import { navigateTo } from "@/App"; +import * as Yup from "yup"; import { Rocket, Users, @@ -47,14 +50,36 @@ import { Headphones, Shield, } from "lucide-react"; -import { useStoreContactUsMutation } from "@/src/services/storeContactUs"; -import { useNavigate } from "react-router-dom"; -import { navigateTo } from "@/App"; -// Enhanced Hero Section - Centered +// Validation Schema +const validationSchema = Yup.object().shape({ + name: Yup.string() + .required("Name is required") + .min(2, "Name must be at least 2 characters") + .max(50, "Name must not exceed 50 characters"), + email: Yup.string() + .required("Email is required") + .email("Invalid email address"), + country: Yup.string().required("Country is required"), + phone: Yup.string() + .required("Phone number is required") + .matches(/^[\d\s+\-().]{10,}$/, "Please enter a valid phone number"), + services: Yup.string().required("Service selection is required"), + budget: Yup.string().required("Budget range is required"), + projectDescription: Yup.string() + .required("Project description is required") + .min(50, "Description should be at least 50 characters") + .max(2000, "Description must not exceed 2000 characters"), + developmentStage: Yup.string().required("Development stage is required"), + timeline: Yup.string().required("Timeline is required"), + ndaRequired: Yup.boolean(), + agreeTerms: Yup.boolean() + .oneOf([true], "You must agree to the terms and conditions") + .required("You must agree to the terms and conditions"), +}); + +// Hero Section const HeroSection = () => { - const navigate = useNavigate(); - return (
@@ -64,7 +89,6 @@ const HeroSection = () => { transition={{ duration: 0.8 }} className="text-center max-w-4xl mx-auto" > - {/* Project Label */} { - {/* Main Heading - Centered */}

Turn Your Next Big Idea into Reality

-

Connect with Mobile App and AI Development Experts Today.

- - {/* Contact Options */} - {/*
- -
*/}
); }; -// Project Form Section - Separate with better spacing +// Project Form Section const ProjectFormSection = () => { - const navigate = useNavigate(); - const [formData, setFormData] = useState({ name: "", email: "", @@ -127,20 +134,64 @@ const ProjectFormSection = () => { agreeTerms: false, }); + const [errors, setErrors] = useState>({}); + const [touched, setTouched] = useState>({}); const [attachedFiles, setAttachedFiles] = useState([]); const [recaptchaToken, setRecaptchaToken] = useState(""); const [isRecaptchaVerified, setIsRecaptchaVerified] = useState(false); const recaptchaRef = useRef(null); + const [submitContactForm, { isLoading }] = useStoreContactUsMutation(); - const [submitContactForm, { isLoading, isSuccess, isError }] = - useStoreContactUsMutation(); + const handleBlur = (field: string) => { + setTouched({ ...touched, [field]: true }); + validateField(field); + }; + + const validateField = async (field: string, dataToValidate = formData) => { + try { + await validationSchema.validateAt(field, dataToValidate); + setErrors((prev) => ({ ...prev, [field]: "" })); + } catch (err) { + if (err instanceof Yup.ValidationError) { + setErrors((prev) => ({ ...prev, [field]: err.message })); + } + } + }; + + const validateForm = async () => { + try { + await validationSchema.validate(formData, { abortEarly: false }); + setErrors({}); + return true; + } catch (err) { + if (err instanceof Yup.ValidationError) { + const newErrors: Record = {}; + err.inner.forEach((error) => { + if (error.path) { + newErrors[error.path] = error.message; + } + }); + setErrors(newErrors); + + // Scroll to first error + const firstError = err.inner[0]; + if (firstError?.path) { + const element = document.getElementById(firstError.path); + if (element) { + element.scrollIntoView({ behavior: "smooth", block: "center" }); + } + } + } + return false; + } + }; const handleFileUpload = (event: React.ChangeEvent) => { const files = event.target.files; if (files) { const validFiles = Array.from(files).filter( (file) => file.size <= 10 * 1024 * 1024 - ); // 10MB limit + ); setAttachedFiles((prev) => [...prev, ...validFiles]); } }; @@ -154,137 +205,234 @@ const ProjectFormSection = () => { setIsRecaptchaVerified(true); }; - const handleRecaptchaExpired = () => { - setRecaptchaToken(""); - setIsRecaptchaVerified(false); - }; + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - const handleRecaptchaError = () => { - setRecaptchaToken(""); - setIsRecaptchaVerified(false); - }; + const isValid = await validateForm(); + if (!isValid) return; - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + try { + const formDataToSend = new FormData(); + formDataToSend.append("t_id", "xyz123"); + formDataToSend.append("name", formData.name); + formDataToSend.append("email", formData.email); + formDataToSend.append("country", formData.country); + formDataToSend.append("phone_number", formData.phone); + formDataToSend.append("service", formData.services); + formDataToSend.append("budget", formData.budget); + formDataToSend.append("message", formData.projectDescription); + formDataToSend.append("development_stage", formData.developmentStage); + formDataToSend.append("startTime", formData.timeline); + formDataToSend.append("nda_signing", formData.ndaRequired ? "1" : "0"); + formDataToSend.append("from_page", "contact-us"); - // Validation checks - if (!formData.name.trim()) { - alert("Please enter your name."); - return; - } + if (attachedFiles.length > 0) { + attachedFiles.forEach((file) => { + formDataToSend.append("contact_us_attachment", file); + }); + } - if (!formData.email.trim()) { - alert("Please enter your email address."); - return; - } + formDataToSend.append("ip", "192.168.1.10"); + formDataToSend.append("user_agent", navigator.userAgent); - if (!formData.country) { - alert("Please select your country."); - return; - } + await submitContactForm(formDataToSend).unwrap(); - if (!formData.phone.trim()) { - alert("Please enter your contact number."); - return; - } - - if (!formData.services) { - alert("Please select a service."); - return; - } - - if (!formData.budget) { - alert("Please select a budget range."); - return; - } - - if (!formData.projectDescription.trim()) { - alert("Please describe your project."); - return; - } - - if (!formData.developmentStage) { - alert("Please select your current development stage."); - return; - } - - if (!formData.timeline) { - alert("Please select your expected timeline."); - return; - } - - if (!formData.agreeTerms) { - alert("Please agree to the terms and conditions."); - return; - } - - // if (!isRecaptchaVerified) { - // alert("Please complete the reCAPTCHA verification."); - // return; - // } - - try { - const formDataToSend = new FormData(); - - // Map your form fields to the API expected fields - formDataToSend.append('t_id', 'xyz123'); - formDataToSend.append('name', formData.name); - formDataToSend.append('email', formData.email); - formDataToSend.append('country', formData.country); - formDataToSend.append('phone_number', formData.phone); - formDataToSend.append('service', formData.services); - formDataToSend.append('budget', formData.budget); - formDataToSend.append('message', formData.projectDescription); - formDataToSend.append('development_stage', formData.developmentStage); - formDataToSend.append('startTime', formData.timeline); - formDataToSend.append('nda_signing', formData.ndaRequired ? '1' : '0'); - formDataToSend.append('from_page', 'contact-us'); - - // Add files if any - if (attachedFiles.length > 0) { - attachedFiles.forEach((file) => { - formDataToSend.append('contact_us_attachment', file); + // Reset form + setFormData({ + name: "", + email: "", + country: "", + phone: "", + services: "", + budget: "", + projectDescription: "", + developmentStage: "", + timeline: "", + ndaRequired: false, + agreeTerms: false, }); + setAttachedFiles([]); + + if (recaptchaRef.current) { + recaptchaRef.current.reset(); + } + setIsRecaptchaVerified(false); + setRecaptchaToken(""); + + navigateTo("/thank-you"); + } catch (error) { + console.error("Form submission error:", error); + alert("Failed to submit the form. Please try again."); } - - // Add IP and user agent (you might want to get these dynamically) - formDataToSend.append('ip', '192.168.1.10'); - formDataToSend.append('user_agent', navigator.userAgent); - - // Submit the form - await submitContactForm(formDataToSend).unwrap(); - - // Reset form on success - setFormData({ - name: '', - email: '', - country: '', - phone: '', - services: '', - budget: '', - projectDescription: '', - developmentStage: '', - timeline: '', - ndaRequired: false, - agreeTerms: false - }); - setAttachedFiles([]); - - // Reset reCAPTCHA - if (recaptchaRef.current) { - recaptchaRef.current.reset(); - } - setIsRecaptchaVerified(false); - setRecaptchaToken(''); - - // Redirect to thank you page - navigateTo('/thank-you'); - } catch (error) { - console.error('Form submission error:', error); - // Handle error (show error message to user) - alert('Failed to submit the form. Please try again.'); - } -}; + }; + + // Helper components for form fields + const renderInputField = ( + field: keyof typeof formData, + label: string, + placeholder: string, + type = "text" + ) => ( +
+ + setFormData({ ...formData, [field]: e.target.value })} + onBlur={() => handleBlur(field)} + /> + {errors[field] && ( +

{errors[field]}

+ )} +
+ ); + + const renderSelectField = ( + field: keyof typeof formData, + label: string, + placeholder: string, + options: { value: string; label: string }[] + ) => ( +
+ + + {errors[field] && ( +

{errors[field]}

+ )} +
+ ); + + const renderTextarea = ( + field: keyof typeof formData, + label: string, + placeholder: string, + rows = 6 + ) => ( +
+
+ + + {formData[field]?.toString().length || 0}/2000 + +
+