Changes of custom captcha module

This commit is contained in:
priyanshuvish
2025-09-26 16:03:21 +05:30
parent e78c8672e9
commit 22990882d6
6 changed files with 985 additions and 772 deletions

View File

@@ -0,0 +1,737 @@
import React, { useState, useRef } from "react";
import { motion } from "framer-motion";
import { Button } from "../components/ui/button";
import { ShimmerButton } from "../components/ui/shimmer-button";
import { Card, CardContent } from "../components/ui/card";
import { Input } from "../components/ui/input";
import { Textarea } from "../components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../components/ui/select";
import { Checkbox } from "../components/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "../components/ui/radio-group";
import { Label } from "../components/ui/label";
import { MathVerificationPopup } from "../components/MathVerificationPopup";
import { useStoreContactUsMutation } from "@/src/services/storeContactUs";
import { useNavigate } from "react-router-dom";
import * as Yup from "yup";
import {
Rocket,
Upload,
FileText,
CheckCircle,
ArrowRight,
Shield,
} from "lucide-react";
interface AboutYourProjectProps {
// You can add any props you might need to pass from the parent component
}
// Country codes constant
const COUNTRY_CODES: Record<string, string> = {
us: "+1",
uk: "+44",
ca: "+1",
au: "+61",
in: "+91",
de: "+49",
fr: "+33",
other: "+",
};
export const AboutYourProject: React.FC<AboutYourProjectProps> = () => {
const [isHumanVerified, setIsHumanVerified] = useState(false);
const [showVerificationPopup, setShowVerificationPopup] = useState(false);
const [formData, setFormData] = useState({
name: "",
email: "",
country: "",
phone: "",
services: "",
budget: "",
projectDescription: "",
developmentStage: "",
timeline: "",
ndaRequired: false,
agreeTerms: false,
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
const [submitContactForm, { isLoading }] = useStoreContactUsMutation();
const navigate = useNavigate();
// Updated Validation Schema (removed recaptcha)
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"),
}).test(
'human-verification',
'Human verification is required',
function(value) {
return isHumanVerified;
}
);
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<string, string> = {};
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<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
const validFiles = Array.from(files).filter(
(file) => file.size <= 10 * 1024 * 1024
);
setAttachedFiles((prev) => [...prev, ...validFiles]);
}
};
const removeFile = (index: number) => {
setAttachedFiles((prev) => prev.filter((_, i) => i !== index));
};
const handleHumanVerification = () => {
setIsHumanVerified(true);
setShowVerificationPopup(false);
};
const handleVerificationCheckbox = (checked: boolean) => {
if (checked) {
setShowVerificationPopup(true);
} else {
setIsHumanVerified(false);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isHumanVerified) {
setErrors(prev => ({
...prev,
humanVerification: "Please complete the human verification"
}));
setShowVerificationPopup(true);
return;
}
const isValid = await validateForm();
if (!isValid) return;
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");
if (attachedFiles.length > 0) {
attachedFiles.forEach((file) => {
formDataToSend.append("contact_us_attachment", file);
});
}
formDataToSend.append("ip", "192.168.1.10");
formDataToSend.append("user_agent", navigator.userAgent);
await submitContactForm(formDataToSend).unwrap();
// Reset form
setFormData({
name: "",
email: "",
country: "",
phone: "",
services: "",
budget: "",
projectDescription: "",
developmentStage: "",
timeline: "",
ndaRequired: false,
agreeTerms: false,
});
setAttachedFiles([]);
setIsHumanVerified(false);
navigate("/thank-you");
} catch (error) {
console.error("Form submission error:", error);
alert("Failed to submit the form. Please try again.");
}
};
// Helper components for form fields (keep the same as before)
const renderInputField = (
field: keyof typeof formData,
label: string,
placeholder: string,
type = "text"
) => (
<div className="space-y-3" id={field}>
<label className="block text-sm font-medium text-white">{label} *</label>
<Input
type={type}
placeholder={placeholder}
className={`bg-gray-800/30 border-gray-600/50 text-white h-12 text-base ${errors[field] ? "border-red-500" : ""
}`}
value={formData[field] as string}
onChange={(e) => setFormData({ ...formData, [field]: e.target.value })}
onBlur={() => handleBlur(field)}
/>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
const renderSelectField = (
field: keyof typeof formData,
label: string,
placeholder: string,
options: { value: string; label: string }[],
onValueChange?: (value: string) => void
) => (
<div className="space-y-3" id={field}>
<label className="block text-sm font-medium text-white">{label} *</label>
<Select
value={formData[field] as string}
onValueChange={(value) => {
const updated = { ...formData, [field]: value };
setFormData(updated);
setTouched({ ...touched, [field]: true });
validateField(field, updated);
if (onValueChange) {
onValueChange(value);
}
}}
>
<SelectTrigger
className={`bg-gray-800/30 border-gray-600/50 text-white h-12 min-h-12 ${errors[field] ? "border-red-500" : ""
}`}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
const renderTextarea = (
field: keyof typeof formData,
label: string,
placeholder: string,
rows = 6
) => (
<div className="space-y-3" id={field}>
<div className="flex justify-between">
<label className="block text-sm font-medium text-white">
{label} *
</label>
<span className="text-sm text-gray-400">
{formData[field]?.toString().length || 0}/2000
</span>
</div>
<Textarea
placeholder={placeholder}
rows={rows}
className={`bg-gray-800/30 border-gray-600/50 text-white text-base resize-none ${errors[field] ? "border-red-500" : ""
}`}
value={formData[field] as string}
onChange={(e) => setFormData({ ...formData, [field]: e.target.value })}
onBlur={() => handleBlur(field)}
/>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
const renderRadioGroup = (
field: keyof typeof formData,
label: string,
options: { value: string; label: string }[],
cols: string
) => (
<div className="space-y-4" id={field}>
<label className="block text-sm font-medium text-white">{label} *</label>
{errors[field] && (
<p className="text-red-400 text-sm mb-2">{errors[field]}</p>
)}
<RadioGroup
value={formData[field] as string}
onValueChange={(value) => {
const updated = { ...formData, [field]: value };
setFormData(updated);
setTouched({ ...touched, [field]: true });
validateField(field, updated);
}}
className={`grid ${cols} gap-4`}
>
{options.map((option) => (
<div
key={option.value}
className="flex items-center space-x-3 p-4 bg-gray-800/20 rounded-lg border border-gray-700/30"
>
<RadioGroupItem
value={option.value}
id={`${field}-${option.value}`}
className="border-gray-600"
/>
<Label
htmlFor={`${field}-${option.value}`}
className="text-white cursor-pointer"
>
{option.label}
</Label>
</div>
))}
</RadioGroup>
</div>
);
const renderCheckbox = (
field: keyof typeof formData,
label: React.ReactNode,
required = false
) => (
<div className="space-y-2">
<div className="flex items-start space-x-4 p-4 bg-gray-800/20 rounded-lg border border-gray-700/30">
<Checkbox
id={field}
checked={formData[field] as boolean}
onCheckedChange={(checked) => {
const updated = { ...formData, [field]: checked };
setFormData(updated);
setTouched({ ...touched, [field]: true });
validateField(field, updated);
}}
className="mt-1"
/>
<label
htmlFor={field}
className="text-gray-300 leading-relaxed cursor-pointer"
>
{label}
</label>
</div>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
return (
<section className="py-32 bg-wdi-grey">
<div className="container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-semibold leading-tight mb-6">
<span className="text-white">Tell Us About Your </span>
<span className="text-[#E5195E]">Project</span>
</h2>
<p className="text-lg text-gray-300 leading-relaxed max-w-2xl mx-auto">
Fill out the form below and our experts will get back to you within
24 hours
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
className="max-w-4xl mx-auto"
>
<Card className="bg-gray-900/30 backdrop-blur-md border-gray-700/30 rounded-3xl overflow-hidden shadow-2xl">
<CardContent className="p-12">
<form onSubmit={handleSubmit} className="space-y-10">
{/* Personal Information Section */}
<div className="space-y-8">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4">
Personal Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{renderInputField(
"name",
"Your Name",
"Enter your full name"
)}
{renderInputField(
"email",
"Email Address",
"your.email@company.com",
"email"
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{renderSelectField(
"country",
"Country",
"Select your country",
[
{ value: "us", label: "United States" },
{ value: "uk", label: "United Kingdom" },
{ value: "ca", label: "Canada" },
{ value: "au", label: "Australia" },
{ value: "in", label: "India" },
{ value: "de", label: "Germany" },
{ value: "fr", label: "France" },
{ value: "other", label: "Other" },
],
(value) => {
const updated = { ...formData, country: value };
if (!formData.phone || formData.phone.startsWith("+")) {
updated.phone = COUNTRY_CODES[value] || "";
}
setFormData(updated);
setTouched({ ...touched, country: true });
validateField("country", updated);
}
)}
{renderInputField(
"phone",
"Contact Number",
formData.country ? `${COUNTRY_CODES[formData.country] || "+"} (XXX) XXX-XXXX` : "+XX (XXX) XXX-XXXX"
)}
</div>
</div>
{/* Project Information Section */}
<div className="space-y-8">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4">
Project Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{renderSelectField(
"services",
"Select Services",
"Choose primary service",
[
{
value: "mobile-app",
label: "Mobile App Development",
},
{ value: "web-development", label: "Web Development" },
{ value: "ai-ml", label: "AI & Machine Learning" },
{ value: "ui-ux", label: "UI/UX Design" },
{ value: "enterprise", label: "Enterprise Solutions" },
{ value: "consultation", label: "Consultation" },
]
)}
{renderSelectField(
"budget",
"Budget Range",
"Select budget range",
[
{ value: "under-25k", label: "Under $25,000" },
{ value: "25k-50k", label: "$25,000 - $50,000" },
{ value: "50k-100k", label: "$50,000 - $100,000" },
{ value: "100k-250k", label: "$100,000 - $250,000" },
{ value: "250k-500k", label: "$250,000 - $500,000" },
{ value: "500k-plus", label: "$500,000+" },
]
)}
</div>
{renderTextarea(
"projectDescription",
"Project Description",
"Tell us about your project vision, goals, and key requirements..."
)}
{renderRadioGroup(
"developmentStage",
"Current Development Stage",
[
{ value: "idea", label: "Idea" },
{ value: "designed", label: "Designed Solution" },
{ value: "prototype", label: "Prototype/Spec" },
{ value: "mvp", label: "MVP" },
],
"grid-cols-2 md:grid-cols-4"
)}
</div>
{/* Additional Details Section */}
<div className="space-y-8">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4">
Additional Details
</h3>
<div className="space-y-4">
<label className="block text-sm font-medium text-white">
Project Attachments
</label>
<div className="border-2 border-dashed border-gray-600/50 rounded-xl p-8 text-center bg-gray-800/10">
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-300 mb-2 font-medium">
Upload Additional Files
</p>
<p className="text-sm text-gray-500 mb-6">
Attach wireframes, designs, or requirements. Max file
size: 10MB
</p>
<input
type="file"
multiple
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.fig"
onChange={handleFileUpload}
className="hidden"
id="file-upload"
/>
<Button
type="button"
variant="outline"
onClick={() =>
document.getElementById("file-upload")?.click()
}
className="border-gray-600 text-white hover:bg-gray-800 h-12"
>
<Upload className="w-4 h-4 mr-2" />
Choose Files
</Button>
</div>
{attachedFiles.length > 0 && (
<div className="space-y-3">
{attachedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between bg-gray-800/30 p-4 rounded-lg border border-gray-700/30"
>
<div className="flex items-center space-x-3">
<FileText className="w-5 h-5 text-[#E5195E]" />
<span className="text-white font-medium">
{file.name}
</span>
<span className="text-gray-400 text-sm">
({(file.size / 1024 / 1024).toFixed(1)} MB)
</span>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeFile(index)}
className="text-red-400 hover:text-red-300 hover:bg-red-900/20"
>
Remove
</Button>
</div>
))}
</div>
)}
</div>
{renderRadioGroup(
"timeline",
"Expected Start Timeline",
[
{ value: "1-month", label: "1 Month" },
{ value: "6-month", label: "6 Months" },
{ value: "1-year", label: "1 Year" },
{ value: "1.5-year", label: "1.5 Years" },
{ value: "2-plus-year", label: "2+ Years" },
],
"grid-cols-2 md:grid-cols-5"
)}
</div>
{/* Legal & Agreements */}
<div className="space-y-6">
{renderCheckbox(
"ndaRequired",
"I want to protect my data by signing an NDA (Non-Disclosure Agreement)"
)}
{renderCheckbox(
"agreeTerms",
<>
I agree to the{" "}
<span className="text-[#E5195E] underline">
terms & conditions
</span>{" "}
and{" "}
<span className="text-[#E5195E] underline">
privacy policy
</span>{" "}
*
</>,
true
)}
</div>
{/* Security Verification Section */}
<div className="space-y-6">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4 flex items-center gap-3">
<Shield className="w-5 h-5 text-[#E5195E]" />
Security Verification
</h3>
<div className="space-y-4">
<div className="flex items-start space-x-4 p-4 bg-gray-800/20 rounded-lg border border-gray-700/30">
<Checkbox
id="human-verification"
checked={isHumanVerified}
onCheckedChange={handleVerificationCheckbox}
className="mt-1"
/>
<label
htmlFor="human-verification"
className="text-gray-300 leading-relaxed cursor-pointer"
>
I'm not a robot
</label>
</div>
{isHumanVerified && (
<div className="flex items-center justify-center gap-2 text-green-400 text-sm p-3 bg-green-900/20 rounded-lg">
<CheckCircle className="w-4 h-4" />
<span>Verification successful</span>
</div>
)}
{errors.humanVerification && (
<p className="text-red-400 text-sm mt-1">
{errors.humanVerification}
</p>
)}
</div>
</div>
{/* Submit Button */}
<div className="pt-8">
<ShimmerButton
type="submit"
className="w-full py-6 text-xl rounded-2xl shadow-lg hover:shadow-xl"
disabled={!formData.agreeTerms || !isHumanVerified || isLoading}
>
<div className="inline-flex items-center justify-center gap-3">
{isLoading ? (
<span>Submitting...</span>
) : (
<>
<Rocket className="w-6 h-6 flex-shrink-0" />
<span>Submit Project Request</span>
<ArrowRight className="w-5 h-5" />
</>
)}
</div>
</ShimmerButton>
{(!formData.agreeTerms || !isHumanVerified) && (
<p className="text-center text-sm text-gray-400 mt-3">
{!formData.agreeTerms && !isHumanVerified
? "Please agree to terms and complete verification to submit"
: !formData.agreeTerms
? "Please agree to terms and conditions to submit"
: "Please complete the security verification to submit"}
</p>
)}
</div>
</form>
</CardContent>
</Card>
</motion.div>
</div>
{/* Math Verification Popup */}
<MathVerificationPopup
isOpen={showVerificationPopup}
onVerify={handleHumanVerification}
onClose={() => setShowVerificationPopup(false)}
/>
</section>
);
};

View File

@@ -0,0 +1,176 @@
import React, { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Button } from "./ui/button";
import { X } from "lucide-react";
interface MathVerificationPopupProps {
isOpen: boolean;
onVerify: () => void;
onClose: () => void;
}
export const MathVerificationPopup: React.FC<MathVerificationPopupProps> = ({
isOpen,
onVerify,
onClose,
}) => {
const [num1, setNum1] = useState(0);
const [num2, setNum2] = useState(0);
const [operator, setOperator] = useState<"+" | "-">("+");
const [userAnswer, setUserAnswer] = useState("");
const [error, setError] = useState("");
const [attempts, setAttempts] = useState(0);
const generateNewProblem = () => {
let newNum1 = Math.floor(Math.random() * 10) + 1;
let newNum2 = Math.floor(Math.random() * 10) + 1;
const newOperator = Math.random() > 0.5 ? "+" : "-";
// Ensure subtraction always has the larger number first
if (newOperator === "-" && newNum1 < newNum2) {
[newNum1, newNum2] = [newNum2, newNum1];
}
setNum1(newNum1);
setNum2(newNum2);
setOperator(newOperator);
setUserAnswer("");
setError("");
};
useEffect(() => {
if (isOpen) {
generateNewProblem();
setAttempts(0);
}
}, [isOpen]);
const correctAnswer = operator === "+" ? num1 + num2 : num1 - num2;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!userAnswer.trim()) {
setError("Please enter your answer");
return;
}
const answer = parseInt(userAnswer);
if (isNaN(answer)) {
setError("Please enter a valid number");
return;
}
if (answer === correctAnswer) {
onVerify();
} else {
const newAttempts = attempts + 1;
setAttempts(newAttempts);
setError(`Incorrect answer. Attempt ${newAttempts} of 3.`);
if (newAttempts >= 3) {
generateNewProblem();
setAttempts(0);
}
}
};
const handleOverlayClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick={handleOverlayClick}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-gray-900 border border-gray-700 rounded-2xl max-w-md w-full p-6 shadow-2xl"
>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-white">
Verify You're Human
</h3>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="text-gray-400 hover:text-white"
>
<X className="w-5 h-5" />
</Button>
</div>
<div className="space-y-6">
<div className="text-center">
<p className="text-gray-300 mb-4">
Solve this simple math problem to continue:
</p>
<div className="flex items-center justify-center space-x-4 text-3xl font-bold text-white mb-6">
<span className="bg-gray-800 px-4 py-3 rounded-lg min-w-[60px] text-center">
{num1}
</span>
<span className="text-[#E5195E]">{operator}</span>
<span className="bg-gray-800 px-4 py-3 rounded-lg min-w-[60px] text-center">
{num2}
</span>
<span>=</span>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
value={userAnswer}
onChange={(e) => {
setUserAnswer(e.target.value.replace(/[^0-9-]/g, ""));
setError("");
}}
className="bg-gray-800 border border-gray-600 rounded-lg px-4 py-3 text-center w-20 text-white focus:outline-none focus:border-[#E5195E]"
autoFocus
maxLength={3}
/>
</div>
{error && (
<p className="text-red-400 text-sm mt-2">{error}</p>
)}
</div>
<div className="flex gap-3">
<Button
type="button"
variant="outline"
onClick={generateNewProblem}
className="flex-1 border-gray-600 text-gray-300 hover:text-white"
>
New Problem
</Button>
<Button
onClick={handleSubmit}
className="flex-1 bg-[#E5195E] hover:bg-[#c41450] text-white"
>
Verify Answer
</Button>
</div>
<p className="text-xs text-gray-500 text-center">
This helps us prevent spam and automated submissions.
</p>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
};

View File

@@ -7,13 +7,16 @@ interface ShimmerButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
} }
const ShimmerButton = React.forwardRef<HTMLButtonElement, ShimmerButtonProps>( const ShimmerButton = React.forwardRef<HTMLButtonElement, ShimmerButtonProps>(
({ className, children, ...props }, ref) => { ({ className, children, disabled, ...props }, ref) => {
return ( return (
<button <button
ref={ref} ref={ref}
disabled={disabled}
className={cn( className={cn(
"relative inline-flex items-center justify-center gap-2 rounded-md bg-accent px-6 py-3 text-sm font-medium text-accent-foreground transition-all duration-300 hover:bg-accent/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent whitespace-nowrap overflow-hidden", "relative inline-flex items-center justify-center gap-2 rounded-md bg-accent px-6 py-3 text-sm font-medium text-accent-foreground transition-all duration-300 whitespace-nowrap overflow-hidden",
"before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent cursor-pointer", !disabled
? "hover:bg-accent/90 cursor-pointer before:absolute before:inset-0 before:-translate-x-full before:animate-[shimmer_2s_infinite] before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent"
: "bg-gray-700 text-gray-400 cursor-not-allowed before:!hidden",
className className
)} )}
{...props} {...props}
@@ -26,4 +29,4 @@ const ShimmerButton = React.forwardRef<HTMLButtonElement, ShimmerButtonProps>(
ShimmerButton.displayName = "ShimmerButton"; ShimmerButton.displayName = "ShimmerButton";
export { ShimmerButton }; export { ShimmerButton };

View File

@@ -32,9 +32,7 @@ import {
Wrench, Wrench,
Zap Zap
} from "lucide-react"; } from "lucide-react";
import { Footer } from "../components/Footer";
import { GridPattern } from "../components/GridPattern"; import { GridPattern } from "../components/GridPattern";
import { Navigation } from "../components/Navigation";
import { ImageWithFallback } from "../components/figma/ImageWithFallback"; import { ImageWithFallback } from "../components/figma/ImageWithFallback";
import { Button } from "../components/ui/button"; import { Button } from "../components/ui/button";
import ranoutofLogo from "../src/images/ranoutof-logo.webp"; import ranoutofLogo from "../src/images/ranoutof-logo.webp";
@@ -56,14 +54,14 @@ const getTechIcon = (tech: string) => {
"Redux": <Layers className="w-4 h-4" />, "Redux": <Layers className="w-4 h-4" />,
"PWA": <Monitor className="w-4 h-4" /> "PWA": <Monitor className="w-4 h-4" />
}; };
return techIconMap[tech] || <Wrench className="w-4 h-4" />; return techIconMap[tech] || <Wrench className="w-4 h-4" />;
}; };
const projectDetails = { const projectDetails = {
title: "RanOutOf Simplify Grocery Planning with AI-Powered Lists", title: "RanOutOf Simplify Grocery Planning with AI-Powered Lists",
subtitle: "A voice-enabled, reminder-rich, and collaborative grocery management app designed to simplify household planning and eliminate stockouts", subtitle: "A voice-enabled, reminder-rich, and collaborative grocery management app designed to simplify household planning and eliminate stockouts",
technologies: ["React Native", "Laravel", "Node.js", "MySQL", "Firebase"], technologies: ["React Native", "Node.js", "MySQL", "AWS"],
industries: ["Consumer Tech", "Retail Technology", "Productivity"], industries: ["Consumer Tech", "Retail Technology", "Productivity"],
duration: "15 weeks, October 2024 January 2025", duration: "15 weeks, October 2024 January 2025",
teamSize: "4 developers, 2 designers, 1 QA, 1 PM", teamSize: "4 developers, 2 designers, 1 QA, 1 PM",
@@ -71,9 +69,9 @@ const projectDetails = {
}; };
const keyAchievements = [ const keyAchievements = [
{ label: "Product Categories", value: "200+", description: "Comprehensive grocery taxonomy" }, { label: "Product Categories", value: "200+", description: "Over 25,000 items available" },
{ label: "List Sharing", value: "100%", description: "Real-time collaborative features" }, { label: "List Sharing", value: "100%", description: "Real-time collaboration" },
{ label: "Smart Alerts", value: "24/7", description: "Inventory expiry reminders" } { label: "Smart Alerts", value: "24/7", description: "Expiry and restock reminders" }
]; ];
const businessObjectives = [ const businessObjectives = [
@@ -148,7 +146,7 @@ const developmentPhases = [
}, },
{ {
phase: "UI/UX Design", phase: "UI/UX Design",
duration: "2 weeks", duration: "2 weeks",
description: "User interface design, prototyping, and user experience optimization", description: "User interface design, prototyping, and user experience optimization",
icon: <Palette className="w-5 h-5" /> icon: <Palette className="w-5 h-5" />
}, },
@@ -183,7 +181,7 @@ const resultsMetrics = [
const technicalAchievements = [ const technicalAchievements = [
"Voice recognition with 95% accuracy across multiple languages", "Voice recognition with 95% accuracy across multiple languages",
"Real-time synchronization with conflict resolution algorithms", "Real-time synchronization with conflict resolution algorithms",
"Smart inventory prediction based on consumption patterns", "Smart inventory prediction based on consumption patterns",
"Seamless barcode integration with 200+ product categories" "Seamless barcode integration with 200+ product categories"
]; ];
@@ -207,8 +205,8 @@ const futureRoadmap = [
features: ["AI-powered shopping list suggestions", "Store discount tracking integration", "Recipe-based automatic list generation"] features: ["AI-powered shopping list suggestions", "Store discount tracking integration", "Recipe-based automatic list generation"]
}, },
{ {
phase: "Phase 3", phase: "Phase 3",
features: ["Voice command grocery planning with smart assistants", "Household budget tracking and spend reports", "Integration with grocery delivery services"] features: ["Voice command grocery planning with smart assistants", "Household budget tracking and spend reports", "Integration with grocery delivery and eCommerce platforms"]
} }
]; ];
@@ -218,11 +216,11 @@ export const RanOutOfProject = () => {
return ( return (
<div className="dark min-h-screen bg-background"> <div className="dark min-h-screen bg-background">
{/* <Navigation /> */} {/* <Navigation /> */}
{/* Section 1: Hero with Heading, Subheading, and Image */} {/* Section 1: Hero with Heading, Subheading, and Image */}
<section className="relative pt-32 pb-24 bg-background overflow-hidden"> <section className="relative pt-32 pb-24 bg-background overflow-hidden">
<GridPattern strokeDasharray="4 2" /> <GridPattern strokeDasharray="4 2" />
<div className="relative z-10 container mx-auto px-4 lg:px-6"> <div className="relative z-10 container mx-auto px-4 lg:px-6">
{/* Back Button */} {/* Back Button */}
<motion.div <motion.div
@@ -254,11 +252,11 @@ export const RanOutOfProject = () => {
Case Study Case Study
</Badge> </Badge>
</div> </div>
<h1 className="text-4xl lg:text-6xl font-semibold text-foreground mb-8 leading-tight"> <h1 className="text-4xl lg:text-6xl font-semibold text-foreground mb-8 leading-tight">
{projectDetails.title} {projectDetails.title}
</h1> </h1>
<p className="text-xl text-muted-foreground mb-10 leading-relaxed max-w-2xl"> <p className="text-xl text-muted-foreground mb-10 leading-relaxed max-w-2xl">
{projectDetails.subtitle} {projectDetails.subtitle}
</p> </p>
@@ -289,10 +287,10 @@ export const RanOutOfProject = () => {
<div className="absolute inset-0 bg-gradient-to-br from-accent/5 via-transparent to-green-500/5" /> <div className="absolute inset-0 bg-gradient-to-br from-accent/5 via-transparent to-green-500/5" />
<div className="absolute top-20 right-20 w-64 h-64 bg-accent/10 rounded-full blur-3xl opacity-20" /> <div className="absolute top-20 right-20 w-64 h-64 bg-accent/10 rounded-full blur-3xl opacity-20" />
<div className="absolute bottom-20 left-20 w-48 h-48 bg-green-500/10 rounded-full blur-3xl opacity-20" /> <div className="absolute bottom-20 left-20 w-48 h-48 bg-green-500/10 rounded-full blur-3xl opacity-20" />
<div className="container mx-auto px-4 lg:px-6 relative z-10"> <div className="container mx-auto px-4 lg:px-6 relative z-10">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
{/* Section Header */} {/* Section Header */}
<motion.div <motion.div
initial={{ opacity: 0, y: 30 }} initial={{ opacity: 0, y: 30 }}
@@ -304,7 +302,7 @@ export const RanOutOfProject = () => {
Project Details Project Details
</h2> </h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto"> <p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Comprehensive overview of technologies, timeline, and key achievements that revolutionized household grocery management RanOutOf is a smart, AI-powered grocery management app designed for households, featuring voice input, reminders, and collaborative list sharing. This case study highlights WDIs expertise in crafting an intuitive, family-friendly solution that simplifies grocery planning and boosts efficiency.
</p> </p>
</motion.div> </motion.div>
@@ -328,9 +326,9 @@ export const RanOutOfProject = () => {
</div> </div>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{projectDetails.technologies.map((tech) => ( {projectDetails.technologies.map((tech) => (
<Badge <Badge
key={tech} key={tech}
variant="outline" variant="outline"
className="text-base border-border/40 bg-background/30 hover:bg-accent/10 hover:border-accent/40 flex items-center gap-2 px-4 py-2 transition-all duration-300" className="text-base border-border/40 bg-background/30 hover:bg-accent/10 hover:border-accent/40 flex items-center gap-2 px-4 py-2 transition-all duration-300"
> >
<span className="text-accent">{getTechIcon(tech)}</span> <span className="text-accent">{getTechIcon(tech)}</span>
@@ -339,7 +337,7 @@ export const RanOutOfProject = () => {
))} ))}
</div> </div>
</div> </div>
{/* Industries */} {/* Industries */}
<div> <div>
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
@@ -350,9 +348,9 @@ export const RanOutOfProject = () => {
</div> </div>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{projectDetails.industries.map((industry) => ( {projectDetails.industries.map((industry) => (
<Badge <Badge
key={industry} key={industry}
variant="secondary" variant="secondary"
className="text-base bg-green-500/10 border-green-500/20 text-green-100 hover:bg-green-500/20 px-4 py-2 transition-all duration-300" className="text-base bg-green-500/10 border-green-500/20 text-green-100 hover:bg-green-500/20 px-4 py-2 transition-all duration-300"
> >
{industry} {industry}
@@ -376,7 +374,7 @@ export const RanOutOfProject = () => {
</div> </div>
<p className="text-lg text-muted-foreground pl-15">{projectDetails.duration}</p> <p className="text-lg text-muted-foreground pl-15">{projectDetails.duration}</p>
</div> </div>
{/* Team */} {/* Team */}
<div> <div>
<div className="flex items-center gap-3 mb-6"> <div className="flex items-center gap-3 mb-6">
@@ -398,9 +396,9 @@ export const RanOutOfProject = () => {
</div> </div>
<div className="flex gap-3 pl-15"> <div className="flex gap-3 pl-15">
{projectDetails.platforms.map((platform) => ( {projectDetails.platforms.map((platform) => (
<Badge <Badge
key={platform} key={platform}
variant="outline" variant="outline"
className="text-base border-orange-400/40 bg-orange-500/10 text-orange-100 hover:bg-orange-500/20 px-3 py-1" className="text-base border-orange-400/40 bg-orange-500/10 text-orange-100 hover:bg-orange-500/20 px-3 py-1"
> >
{platform} {platform}
@@ -427,7 +425,7 @@ export const RanOutOfProject = () => {
Measurable outcomes that demonstrate RanOutOf's success in transforming household grocery management Measurable outcomes that demonstrate RanOutOf's success in transforming household grocery management
</p> </p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{keyAchievements.map((achievement, index) => ( {keyAchievements.map((achievement, index) => (
<motion.div <motion.div
@@ -435,7 +433,7 @@ export const RanOutOfProject = () => {
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.6 + index * 0.1 }} transition={{ duration: 0.6, delay: 0.6 + index * 0.1 }}
whileHover={{ whileHover={{
scale: 1.02, scale: 1.02,
y: -4, y: -4,
transition: { duration: 0.3, ease: "easeOut" } transition: { duration: 0.3, ease: "easeOut" }
@@ -444,25 +442,25 @@ export const RanOutOfProject = () => {
> >
{/* Card Background Gradient */} {/* Card Background Gradient */}
<div className="absolute inset-0 bg-gradient-to-br from-accent/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" /> <div className="absolute inset-0 bg-gradient-to-br from-accent/5 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500" />
{/* Content */} {/* Content */}
<div className="relative z-10 text-center"> <div className="relative z-10 text-center">
{/* Value */} {/* Value */}
<div className="text-3xl lg:text-4xl font-bold text-accent mb-4 group-hover:text-accent transition-colors duration-300"> <div className="text-3xl lg:text-4xl font-bold text-accent mb-4 group-hover:text-accent transition-colors duration-300">
{achievement.value} {achievement.value}
</div> </div>
{/* Label */} {/* Label */}
<div className="text-xl font-semibold text-foreground mb-3 group-hover:text-foreground transition-colors duration-300"> <div className="text-xl font-semibold text-foreground mb-3 group-hover:text-foreground transition-colors duration-300">
{achievement.label} {achievement.label}
</div> </div>
{/* Description */} {/* Description */}
<div className="text-base text-muted-foreground leading-relaxed group-hover:text-muted-foreground transition-colors duration-300"> <div className="text-base text-muted-foreground leading-relaxed group-hover:text-muted-foreground transition-colors duration-300">
{achievement.description} {achievement.description}
</div> </div>
</div> </div>
{/* Hover Effect Line */} {/* Hover Effect Line */}
<div className="absolute bottom-0 left-0 w-full h-1 bg-gradient-to-r from-accent to-accent/50 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left" /> <div className="absolute bottom-0 left-0 w-full h-1 bg-gradient-to-r from-accent to-accent/50 transform scale-x-0 group-hover:scale-x-100 transition-transform duration-500 origin-left" />
</motion.div> </motion.div>
@@ -487,7 +485,7 @@ export const RanOutOfProject = () => {
Executive Summary Executive Summary
</h2> </h2>
<p className="text-xl text-muted-foreground leading-relaxed"> <p className="text-xl text-muted-foreground leading-relaxed">
RanOutOf is a mobile-first solution tailored for modern households that frequently forget or mismanage their grocery shopping. The app simplifies list-making, enables shared household planning, and manages pantry inventory using reminders, barcodes, and smart grouping features to eliminate stockouts and reduce food waste. RanOutOf is a mobile-first app designed to help modern households simplify grocery shopping and stay organized. The app streamlines list creation, supports collaborative household planning, and efficiently manages pantry inventory with features like reminders, barcode scanning, and smart grouping. By helping users maintain control over their groceries, RanOutOf reduces the risk of stockouts and minimizes food waste, making everyday shopping easier and more efficient.
</p> </p>
</motion.div> </motion.div>
</div> </div>
@@ -508,13 +506,13 @@ export const RanOutOfProject = () => {
Project Overview Project Overview
</h2> </h2>
</motion.div> </motion.div>
<div className="grid lg:grid-cols-3 gap-12"> <div className="grid lg:grid-cols-3 gap-12">
{/* Background & Context */} {/* Background & Context */}
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
whileHover={{ whileHover={{
scale: 1.05, scale: 1.05,
y: -4, y: -4,
transition: { duration: 0.3, ease: "easeOut" } transition: { duration: 0.3, ease: "easeOut" }
@@ -545,7 +543,7 @@ export const RanOutOfProject = () => {
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
whileHover={{ whileHover={{
scale: 1.05, scale: 1.05,
y: -4, y: -4,
transition: { duration: 0.3, ease: "easeOut" } transition: { duration: 0.3, ease: "easeOut" }
@@ -568,7 +566,7 @@ export const RanOutOfProject = () => {
</motion.div> </motion.div>
<h3 className="text-2xl font-semibold text-foreground mb-6">Target Audience</h3> <h3 className="text-2xl font-semibold text-foreground mb-6">Target Audience</h3>
<p className="text-muted-foreground leading-relaxed"> <p className="text-muted-foreground leading-relaxed">
Tech-savvy individuals and families (age 2545) who want to simplify their grocery management and improve household planning through mobile convenience and collaborative features. Tech-savvy individuals and families (age 1565) who want to simplify their grocery management and improve household planning through mobile convenience.
</p> </p>
</motion.div> </motion.div>
@@ -576,7 +574,7 @@ export const RanOutOfProject = () => {
<motion.div <motion.div
initial={{ opacity: 0, y: 20 }} initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }} whileInView={{ opacity: 1, y: 0 }}
whileHover={{ whileHover={{
scale: 1.05, scale: 1.05,
y: -4, y: -4,
transition: { duration: 0.3, ease: "easeOut" } transition: { duration: 0.3, ease: "easeOut" }
@@ -630,7 +628,7 @@ export const RanOutOfProject = () => {
A comprehensive suite of tools designed to revolutionize household grocery management with smart automation and collaboration. A comprehensive suite of tools designed to revolutionize household grocery management with smart automation and collaboration.
</p> </p>
</div> </div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{coreFeatures.map((feature, index) => ( {coreFeatures.map((feature, index) => (
<motion.div <motion.div
@@ -724,7 +722,7 @@ export const RanOutOfProject = () => {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Server className="w-5 h-5 text-accent" /> <Server className="w-5 h-5 text-accent" />
<span className="text-muted-foreground">Backend: Laravel & Node.js</span> <span className="text-muted-foreground">Backend: Node.js</span>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Database className="w-5 h-5 text-accent" /> <Database className="w-5 h-5 text-accent" />
@@ -732,11 +730,11 @@ export const RanOutOfProject = () => {
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Cloud className="w-5 h-5 text-accent" /> <Cloud className="w-5 h-5 text-accent" />
<span className="text-muted-foreground">Cloud: Firebase Services</span> <span className="text-muted-foreground">Cloud: AWS</span>
</div> </div>
</div> </div>
</div> </div>
<div> <div>
<h3 className="text-xl font-semibold text-foreground mb-6">Key Highlights</h3> <h3 className="text-xl font-semibold text-foreground mb-6">Key Highlights</h3>
<div className="space-y-3"> <div className="space-y-3">
@@ -781,10 +779,10 @@ export const RanOutOfProject = () => {
Development Process & Methodology Development Process & Methodology
</h2> </h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto"> <p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Agile development approach with milestone-based delivery ensuring quality and timely completion. RanOutOf follows an Agile development approach with milestone-based delivery. This ensures timely completion while maintaining high quality through iterative progress and continuous feedback.
</p> </p>
</div> </div>
<div className="grid lg:grid-cols-5 gap-8"> <div className="grid lg:grid-cols-5 gap-8">
{developmentPhases.map((phase, index) => ( {developmentPhases.map((phase, index) => (
<motion.div <motion.div
@@ -809,7 +807,7 @@ export const RanOutOfProject = () => {
{phase.description} {phase.description}
</p> </p>
</div> </div>
{/* Connector Line */} {/* Connector Line */}
{index < developmentPhases.length - 1 && ( {index < developmentPhases.length - 1 && (
<div className="hidden lg:block absolute top-1/2 -right-4 w-8 h-0.5 bg-border transform -translate-y-1/2" /> <div className="hidden lg:block absolute top-1/2 -right-4 w-8 h-0.5 bg-border transform -translate-y-1/2" />
@@ -836,10 +834,10 @@ export const RanOutOfProject = () => {
Results & Impact Results & Impact
</h2> </h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto"> <p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Comprehensive performance metrics demonstrating RanOutOf's success in transforming household grocery management. RanOutOf delivers comprehensive performance metrics showcasing its success in transforming household grocery management. Key indicators include improved inventory accuracy, reduced stockouts, increased shared list usage, and measurable reductions in food waste, demonstrating real value to users.
</p> </p>
</div> </div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8 mb-16">
{resultsMetrics.map((metric, index) => ( {resultsMetrics.map((metric, index) => (
<motion.div <motion.div
@@ -969,10 +967,10 @@ export const RanOutOfProject = () => {
Future Roadmap Future Roadmap
</h2> </h2>
<p className="text-xl text-muted-foreground max-w-3xl mx-auto"> <p className="text-xl text-muted-foreground max-w-3xl mx-auto">
Planned enhancements and features to further revolutionize grocery management. Planned enhancements and features to further revolutionize grocery management apps.
</p> </p>
</div> </div>
<div className="grid lg:grid-cols-2 gap-12"> <div className="grid lg:grid-cols-2 gap-12">
{futureRoadmap.map((roadmap, index) => ( {futureRoadmap.map((roadmap, index) => (
<motion.div <motion.div
@@ -1019,7 +1017,7 @@ export const RanOutOfProject = () => {
className="h-12 w-auto object-contain" className="h-12 w-auto object-contain"
/> />
</div> </div>
<div className="flex justify-center mb-8"> <div className="flex justify-center mb-8">
<div className="flex text-yellow-400"> <div className="flex text-yellow-400">
{[...Array(5)].map((_, i) => ( {[...Array(5)].map((_, i) => (
@@ -1027,11 +1025,11 @@ export const RanOutOfProject = () => {
))} ))}
</div> </div>
</div> </div>
<blockquote className="text-2xl lg:text-3xl text-foreground mb-8 leading-relaxed italic"> <blockquote className="text-2xl lg:text-3xl text-foreground mb-8 leading-relaxed italic">
"WDI delivered a clean, powerful solution that has made grocery planning easier than ever. The list templates, reminders, and inventory alerts are game-changers for our users." "WDI delivered a clean, powerful solution that has made grocery planning easier than ever. The list templates, reminders, and inventory alerts are game-changers for our users."
</blockquote> </blockquote>
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="text-lg font-semibold text-foreground"> <div className="text-lg font-semibold text-foreground">
Rishabh Jain Rishabh Jain
@@ -1061,7 +1059,7 @@ export const RanOutOfProject = () => {
<p className="text-xl text-muted-foreground mb-12 max-w-3xl mx-auto"> <p className="text-xl text-muted-foreground mb-12 max-w-3xl mx-auto">
Partner with WDI to design solutions that streamline daily life with precision tech and intuitive design. Partner with WDI to design solutions that streamline daily life with precision tech and intuitive design.
</p> </p>
<div className="flex flex-col sm:flex-row gap-4 justify-center"> <div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button <Button
size="lg" size="lg"

View File

@@ -122,7 +122,7 @@ export const SeezunProject = () => {
> >
<Button <Button
variant="ghost" variant="ghost"
onClick={() => navigate('/')} onClick={() => navigate('/case-studies')}
className="text-muted-foreground hover:text-foreground flex items-center gap-2 px-0" className="text-muted-foreground hover:text-foreground flex items-center gap-2 px-0"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
@@ -138,29 +138,29 @@ export const SeezunProject = () => {
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
> >
<h1 className="text-4xl lg:text-6xl font-semibold text-foreground mb-6 leading-tight"> <h1 className="text-4xl lg:text-6xl font-semibold text-foreground mb-6 leading-tight">
Revolutionize the Way You Shop South Asian Clothing with{" "} <span className="text-accent">Seezun:</span>{" "}
<span className="text-accent">Seezun</span> Your Seasonal Fashion & Lifestyle Hub
</h1> </h1>
<p className="text-xl text-muted-foreground mb-6 leading-relaxed"> <p className="text-xl text-muted-foreground mb-6 leading-relaxed">
Rent, Buy, or Sell Old &amp; New Clothes Rent, Buy, or Sell South Asian Clothing
</p> </p>
<p className="text-lg text-muted-foreground mb-8 leading-relaxed"> <p className="text-lg text-muted-foreground mb-8 leading-relaxed">
Step into a new way of buying. Rent, buy, or sell South Asian clothing from others. Whether it be new or pre-loved clothing, Seezun covers it all. Discover a new way to update your wardrobe, rent, buy, or sell both new and preloved South Asian outfits on Seezun. Enjoy simple, seamless exchanges for every style and every occasion.
</p> </p>
<div className="flex flex-wrap gap-3 mb-6"> <div className="flex flex-wrap gap-3 mb-6">
<span className="px-4 py-2 bg-accent text-accent-foreground font-semibold rounded-[10px]"> <span className="px-4 py-2 bg-accent text-accent-foreground font-semibold rounded-[10px]">
+200% Transaction Growth South Asian Style
</span> </span>
<span className="px-3 py-1 bg-muted text-muted-foreground text-sm rounded-[10px]"> <span className="px-3 py-2 bg-muted text-muted-foreground text-sm rounded-[10px]">
E-commerce E-commerce
</span> </span>
<span className="px-3 py-1 bg-muted text-muted-foreground text-sm rounded-[10px]"> <span className="px-3 py-2 bg-muted text-muted-foreground text-sm rounded-[10px]">
Marketplace Marketplace
</span> </span>
<span className="px-3 py-1 bg-muted text-muted-foreground text-sm rounded-[10px]"> <span className="px-3 py-2 bg-muted text-muted-foreground text-sm rounded-[10px]">
Fashion Fashion
</span> </span>
</div> </div>

View File

@@ -50,6 +50,7 @@ import {
Shield, Shield,
} from "lucide-react"; } from "lucide-react";
import GlobalOffices from "@/components/GlobalOffices"; import GlobalOffices from "@/components/GlobalOffices";
import { AboutYourProject } from "@/components/AboutYourProject";
@@ -93,707 +94,6 @@ const HeroSection = () => {
); );
}; };
// Project Form Section
const ProjectFormSection = () => {
const [isRecaptchaVerified, setIsRecaptchaVerified] = useState(false);
// 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"),
recaptcha: Yup.string()
.required("Please complete the reCAPTCHA verification")
.test('recaptcha', 'reCAPTCHA is required', () => {
return isRecaptchaVerified; // This will validate against your state
}),
});
const [formData, setFormData] = useState({
name: "",
email: "",
country: "",
phone: "",
services: "",
budget: "",
projectDescription: "",
developmentStage: "",
timeline: "",
ndaRequired: false,
agreeTerms: false,
recaptcha: "",
});
const [errors, setErrors] = useState<Record<string, string>>({});
const [touched, setTouched] = useState<Record<string, boolean>>({});
const [attachedFiles, setAttachedFiles] = useState<File[]>([]);
const [recaptchaToken, setRecaptchaToken] = useState<string>("");
const recaptchaRef = useRef<ReCaptchaRef>(null);
const [submitContactForm, { isLoading }] = useStoreContactUsMutation();
const navigate = useNavigate();
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<string, string> = {};
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<HTMLInputElement>) => {
const files = event.target.files;
if (files) {
const validFiles = Array.from(files).filter(
(file) => file.size <= 10 * 1024 * 1024
);
setAttachedFiles((prev) => [...prev, ...validFiles]);
}
};
const removeFile = (index: number) => {
setAttachedFiles((prev) => prev.filter((_, i) => i !== index));
};
const handleRecaptchaVerify = (token: string) => {
setRecaptchaToken(token);
setIsRecaptchaVerified(true);
setFormData((prev) => ({ ...prev, recaptcha: token }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!isRecaptchaVerified) {
setErrors(prev => ({
...prev,
recaptcha: "Please complete the reCAPTCHA verification"
}));
return;
}
const isValid = await validateForm();
if (!isValid) return;
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");
if (attachedFiles.length > 0) {
attachedFiles.forEach((file) => {
formDataToSend.append("contact_us_attachment", file);
});
}
formDataToSend.append("ip", "192.168.1.10");
formDataToSend.append("user_agent", navigator.userAgent);
await submitContactForm(formDataToSend).unwrap();
// Reset form
setFormData({
name: "",
email: "",
country: "",
phone: "",
services: "",
budget: "",
projectDescription: "",
developmentStage: "",
timeline: "",
ndaRequired: false,
agreeTerms: false,
recaptcha: "",
});
setAttachedFiles([]);
if (recaptchaRef.current) {
recaptchaRef.current.reset();
}
setIsRecaptchaVerified(false);
setRecaptchaToken("");
navigate("/thank-you");
} catch (error) {
console.error("Form submission error:", error);
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"
) => (
<div className="space-y-3" id={field}>
<label className="block text-sm font-medium text-white">{label} *</label>
<Input
type={type}
placeholder={placeholder}
className={`bg-gray-800/30 border-gray-600/50 text-white h-12 text-base ${errors[field] ? "border-red-500" : ""
}`}
value={formData[field] as string}
onChange={(e) => setFormData({ ...formData, [field]: e.target.value })}
onBlur={() => handleBlur(field)}
/>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
const renderSelectField = (
field: keyof typeof formData,
label: string,
placeholder: string,
options: { value: string; label: string }[],
onValueChange?: (value: string) => void
) => (
<div className="space-y-3" id={field}>
<label className="block text-sm font-medium text-white">{label} *</label>
<Select
value={formData[field] as string}
onValueChange={(value) => {
const updated = { ...formData, [field]: value };
setFormData(updated);
setTouched({ ...touched, [field]: true });
validateField(field, updated);
if (onValueChange) {
onValueChange(value);
}
}}
>
<SelectTrigger
className={`bg-gray-800/30 border-gray-600/50 text-white h-12 min-h-12 ${errors[field] ? "border-red-500" : ""
}`}
>
<SelectValue placeholder={placeholder} />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
// Add this near the top of your file with other constants
const COUNTRY_CODES: Record<string, string> = {
us: "+1",
uk: "+44",
ca: "+1",
au: "+61",
in: "+91",
de: "+49",
fr: "+33",
other: "+",
};
const renderTextarea = (
field: keyof typeof formData,
label: string,
placeholder: string,
rows = 6
) => (
<div className="space-y-3" id={field}>
<div className="flex justify-between">
<label className="block text-sm font-medium text-white">
{label} *
</label>
<span className="text-sm text-gray-400">
{formData[field]?.toString().length || 0}/2000
</span>
</div>
<Textarea
placeholder={placeholder}
rows={rows}
className={`bg-gray-800/30 border-gray-600/50 text-white text-base resize-none ${errors[field] ? "border-red-500" : ""
}`}
value={formData[field] as string}
onChange={(e) => setFormData({ ...formData, [field]: e.target.value })}
onBlur={() => handleBlur(field)}
/>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
const renderRadioGroup = (
field: keyof typeof formData,
label: string,
options: { value: string; label: string }[],
cols: string
) => (
<div className="space-y-4" id={field}>
<label className="block text-sm font-medium text-white">{label} *</label>
{errors[field] && (
<p className="text-red-400 text-sm mb-2">{errors[field]}</p>
)}
<RadioGroup
value={formData[field] as string}
onValueChange={(value) => {
const updated = { ...formData, [field]: value };
setFormData(updated);
setTouched({ ...touched, [field]: true });
validateField(field, updated);
}}
className={`grid ${cols} gap-4`}
>
{options.map((option) => (
<div
key={option.value}
className="flex items-center space-x-3 p-4 bg-gray-800/20 rounded-lg border border-gray-700/30"
>
<RadioGroupItem
value={option.value}
id={`${field}-${option.value}`}
className="border-gray-600"
/>
<Label
htmlFor={`${field}-${option.value}`}
className="text-white cursor-pointer"
>
{option.label}
</Label>
</div>
))}
</RadioGroup>
</div>
);
const renderCheckbox = (
field: keyof typeof formData,
label: React.ReactNode,
required = false
) => (
<div className="space-y-2">
<div className="flex items-start space-x-4 p-4 bg-gray-800/20 rounded-lg border border-gray-700/30">
<Checkbox
id={field}
checked={formData[field] as boolean}
onCheckedChange={(checked) => {
const updated = { ...formData, [field]: checked };
setFormData(updated);
setTouched({ ...touched, [field]: true });
validateField(field, updated);
}}
className="mt-1"
/>
<label
htmlFor={field}
className="text-gray-300 leading-relaxed cursor-pointer"
>
{label}
</label>
</div>
{errors[field] && (
<p className="text-red-400 text-sm mt-1">{errors[field]}</p>
)}
</div>
);
return (
<section className="py-32 bg-wdi-grey">
<div className="container mx-auto px-6 lg:px-8">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
viewport={{ once: true }}
className="text-center mb-16"
>
<h2 className="text-3xl lg:text-4xl font-semibold leading-tight mb-6">
<span className="text-white">Tell Us About Your </span>
<span className="text-[#E5195E]">Project</span>
</h2>
<p className="text-lg text-gray-300 leading-relaxed max-w-2xl mx-auto">
Fill out the form below and our experts will get back to you within
24 hours
</p>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, delay: 0.2 }}
viewport={{ once: true }}
className="max-w-4xl mx-auto"
>
<Card className="bg-gray-900/30 backdrop-blur-md border-gray-700/30 rounded-3xl overflow-hidden shadow-2xl">
<CardContent className="p-12">
<form onSubmit={handleSubmit} className="space-y-10">
{/* Personal Information Section */}
<div className="space-y-8">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4">
Personal Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{renderInputField(
"name",
"Your Name",
"Enter your full name"
)}
{renderInputField(
"email",
"Email Address",
"your.email@company.com",
"email"
)}
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{renderSelectField(
"country",
"Country",
"Select your country",
[
{ value: "us", label: "United States" },
{ value: "uk", label: "United Kingdom" },
{ value: "ca", label: "Canada" },
{ value: "au", label: "Australia" },
{ value: "in", label: "India" },
{ value: "de", label: "Germany" },
{ value: "fr", label: "France" },
{ value: "other", label: "Other" },
],
(value) => {
// When country changes, update the phone field if it's empty or starts with a +
const updated = { ...formData, country: value };
if (!formData.phone || formData.phone.startsWith("+")) {
updated.phone = COUNTRY_CODES[value] || "";
}
setFormData(updated);
setTouched({ ...touched, country: true });
validateField("country", updated);
}
)}
{renderInputField(
"phone",
"Contact Number",
formData.country ? `${COUNTRY_CODES[formData.country] || "+"} (XXX) XXX-XXXX` : "+XX (XXX) XXX-XXXX"
)}
</div>
</div>
{/* Project Information Section */}
<div className="space-y-8">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4">
Project Information
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
{renderSelectField(
"services",
"Select Services",
"Choose primary service",
[
{
value: "mobile-app",
label: "Mobile App Development",
},
{ value: "web-development", label: "Web Development" },
{ value: "ai-ml", label: "AI & Machine Learning" },
{ value: "ui-ux", label: "UI/UX Design" },
{ value: "enterprise", label: "Enterprise Solutions" },
{ value: "consultation", label: "Consultation" },
]
)}
{renderSelectField(
"budget",
"Budget Range",
"Select budget range",
[
{ value: "under-25k", label: "Under $25,000" },
{ value: "25k-50k", label: "$25,000 - $50,000" },
{ value: "50k-100k", label: "$50,000 - $100,000" },
{ value: "100k-250k", label: "$100,000 - $250,000" },
{ value: "250k-500k", label: "$250,000 - $500,000" },
{ value: "500k-plus", label: "$500,000+" },
]
)}
</div>
{renderTextarea(
"projectDescription",
"Project Description",
"Tell us about your project vision, goals, and key requirements..."
)}
{renderRadioGroup(
"developmentStage",
"Current Development Stage",
[
{ value: "idea", label: "Idea" },
{ value: "designed", label: "Designed Solution" },
{ value: "prototype", label: "Prototype/Spec" },
{ value: "mvp", label: "MVP" },
],
"grid-cols-2 md:grid-cols-4"
)}
</div>
{/* Additional Details Section */}
<div className="space-y-8">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4">
Additional Details
</h3>
<div className="space-y-4">
<label className="block text-sm font-medium text-white">
Project Attachments
</label>
<div className="border-2 border-dashed border-gray-600/50 rounded-xl p-8 text-center bg-gray-800/10">
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-300 mb-2 font-medium">
Upload Additional Files
</p>
<p className="text-sm text-gray-500 mb-6">
Attach wireframes, designs, or requirements. Max file
size: 10MB
</p>
<input
type="file"
multiple
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.fig"
onChange={handleFileUpload}
className="hidden"
id="file-upload"
/>
<Button
type="button"
variant="outline"
onClick={() =>
document.getElementById("file-upload")?.click()
}
className="border-gray-600 text-white hover:bg-gray-800 h-12"
>
<Upload className="w-4 h-4 mr-2" />
Choose Files
</Button>
</div>
{attachedFiles.length > 0 && (
<div className="space-y-3">
{attachedFiles.map((file, index) => (
<div
key={index}
className="flex items-center justify-between bg-gray-800/30 p-4 rounded-lg border border-gray-700/30"
>
<div className="flex items-center space-x-3">
<FileText className="w-5 h-5 text-[#E5195E]" />
<span className="text-white font-medium">
{file.name}
</span>
<span className="text-gray-400 text-sm">
({(file.size / 1024 / 1024).toFixed(1)} MB)
</span>
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeFile(index)}
className="text-red-400 hover:text-red-300 hover:bg-red-900/20"
>
Remove
</Button>
</div>
))}
</div>
)}
</div>
{renderRadioGroup(
"timeline",
"Expected Start Timeline",
[
{ value: "1-month", label: "1 Month" },
{ value: "6-month", label: "6 Months" },
{ value: "1-year", label: "1 Year" },
{ value: "1.5-year", label: "1.5 Years" },
{ value: "2-plus-year", label: "2+ Years" },
],
"grid-cols-2 md:grid-cols-5"
)}
</div>
{/* Legal & Agreements */}
<div className="space-y-6">
{renderCheckbox(
"ndaRequired",
"I want to protect my data by signing an NDA (Non-Disclosure Agreement)"
)}
{renderCheckbox(
"agreeTerms",
<>
I agree to the{" "}
<span className="text-[#E5195E] underline">
terms & conditions
</span>{" "}
and{" "}
<span className="text-[#E5195E] underline">
privacy policy
</span>{" "}
*
</>,
true
)}
</div>
{/* Security Verification Section */}
<div className="space-y-6">
<h3 className="text-xl font-semibold text-white border-b border-gray-700 pb-4 flex items-center gap-3">
<Shield className="w-5 h-5 text-[#E5195E]" />
Security Verification
</h3>
<div className="space-y-4">
<label className="block text-sm font-medium text-white text-center">
Please verify that you're not a robot *
</label>
<CustomReCaptcha
ref={recaptchaRef}
siteKey="6LfLmWcqAAAAAI5si2QlIliue1K8fhNpMcOs-poL"
onVerify={handleRecaptchaVerify}
onExpired={() => setIsRecaptchaVerified(false)}
onError={() => setIsRecaptchaVerified(false)}
className="w-full"
/>
{isRecaptchaVerified && (
<div className="flex items-center justify-center gap-2 text-green-400 text-sm">
<CheckCircle className="w-4 h-4" />
<span>Verification successful</span>
</div>
)}
{errors.recaptcha && (
<p className="text-red-400 text-sm text-center mt-3">
{errors.recaptcha}
</p>
)}
</div>
</div>
{/* Submit Button */}
<div className="pt-8">
<ShimmerButton
type="submit"
className="w-full py-6 text-xl rounded-2xl shadow-lg hover:shadow-xl"
disabled={!formData.agreeTerms || isLoading}
>
<div className="inline-flex items-center justify-center gap-3">
{isLoading ? (
<span>Submitting...</span>
) : (
<>
<Rocket className="w-6 h-6 flex-shrink-0" />
<span>Submit Project Request</span>
<ArrowRight className="w-5 h-5" />
</>
)}
</div>
</ShimmerButton>
{(!formData.agreeTerms || !isRecaptchaVerified) && (
<p className="text-center text-sm text-gray-400 mt-3">
{!formData.agreeTerms && !isRecaptchaVerified
? "Please agree to terms and complete verification to submit"
: !formData.agreeTerms
? "Please agree to terms and conditions to submit"
: "Please complete the security verification to submit"}
</p>
)}
</div>
</form>
</CardContent>
</Card>
</motion.div>
</div>
</section>
);
};
// Make Your Vision Come to Life Section // Make Your Vision Come to Life Section
const VisionSection = () => { const VisionSection = () => {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -840,7 +140,6 @@ const VisionSection = () => {
// We'd love to talk to you Section // We'd love to talk to you Section
const ProcessStepsSection = () => { const ProcessStepsSection = () => {
const navigate = useNavigate();
const steps = [ const steps = [
{ {
step: "1", step: "1",
@@ -1293,7 +592,7 @@ export const StartAProject = () => {
<div className="dark min-h-screen bg-background"> <div className="dark min-h-screen bg-background">
{/* <Navigation /> */} {/* <Navigation /> */}
<HeroSection /> <HeroSection />
<ProjectFormSection /> <AboutYourProject />
<VisionSection /> <VisionSection />
<ProcessStepsSection /> <ProcessStepsSection />
<WhyPartnerSection /> <WhyPartnerSection />