- Added React TypeScript frontend with Vite build system - Implemented luxury-themed UI with custom Tailwind colors - Created comprehensive waitlist form with conditional logic: * Personal information (name, email) * Company details (name, type, GST registration) * Conditional CIN/PAN fields based on company type and GST status * Industry selection and goals questionnaire - Added form validation and error handling - Integrated waitlist form with hero section and live counter - Implemented responsive design with elegant animations - Added proper gitignore for frontend project
456 lines
22 KiB
TypeScript
456 lines
22 KiB
TypeScript
import React, { useState } from 'react';
|
||
|
||
interface WaitlistFormProps {
|
||
isVisible: boolean;
|
||
onClose: () => void;
|
||
onBack: () => void;
|
||
}
|
||
|
||
interface FormData {
|
||
firstName: string;
|
||
lastName: string;
|
||
email: string;
|
||
companyName: string;
|
||
companyType: string;
|
||
gstRegistered: string;
|
||
cin: string;
|
||
panNumber: string;
|
||
industry: string;
|
||
goals: string;
|
||
}
|
||
|
||
const WaitlistForm: React.FC<WaitlistFormProps> = ({ isVisible, onClose, onBack }) => {
|
||
const [formData, setFormData] = useState<FormData>({
|
||
firstName: '',
|
||
lastName: '',
|
||
email: '',
|
||
companyName: '',
|
||
companyType: '',
|
||
gstRegistered: '',
|
||
cin: '',
|
||
panNumber: '',
|
||
industry: '',
|
||
goals: ''
|
||
});
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
const [errors, setErrors] = useState<Partial<FormData>>({});
|
||
|
||
const companyTypes = [
|
||
'Pvt Ltd',
|
||
'LLP',
|
||
'Partnership',
|
||
'Sole Proprietorship',
|
||
'One Person Company (OPC)',
|
||
'Public Limited Company',
|
||
'Other'
|
||
];
|
||
|
||
const industries = [
|
||
'Information Technology',
|
||
'Finance & Banking',
|
||
'E-commerce',
|
||
'Manufacturing',
|
||
'Healthcare',
|
||
'Education',
|
||
'Real Estate',
|
||
'Retail',
|
||
'Consulting',
|
||
'Marketing & Advertising',
|
||
'Food & Beverage',
|
||
'Transportation',
|
||
'Other'
|
||
];
|
||
|
||
const handleInputChange = (field: keyof FormData, value: string) => {
|
||
setFormData(prev => {
|
||
const newData = { ...prev, [field]: value };
|
||
|
||
// Clear conditional fields when company type changes
|
||
if (field === 'companyType') {
|
||
// Clear both fields when company type changes
|
||
newData.cin = '';
|
||
newData.panNumber = '';
|
||
}
|
||
|
||
// Clear conditional fields when GST registration changes to "yes"
|
||
if (field === 'gstRegistered' && value === 'yes') {
|
||
newData.cin = '';
|
||
newData.panNumber = '';
|
||
}
|
||
|
||
return newData;
|
||
});
|
||
|
||
// Clear error when user starts typing
|
||
if (errors[field]) {
|
||
setErrors(prev => ({ ...prev, [field]: '' }));
|
||
}
|
||
};
|
||
|
||
const validateForm = (): boolean => {
|
||
const newErrors: Partial<FormData> = {};
|
||
|
||
if (!formData.firstName.trim()) newErrors.firstName = 'First name is required';
|
||
if (!formData.lastName.trim()) newErrors.lastName = 'Last name is required';
|
||
if (!formData.email.trim()) {
|
||
newErrors.email = 'Email is required';
|
||
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
|
||
newErrors.email = 'Please enter a valid email';
|
||
}
|
||
if (!formData.companyName.trim()) newErrors.companyName = 'Company name is required';
|
||
if (!formData.companyType) newErrors.companyType = 'Company type is required';
|
||
if (!formData.gstRegistered) newErrors.gstRegistered = 'GST registration status is required';
|
||
|
||
// Conditional validation - only when GST is NOT registered
|
||
if (formData.gstRegistered === 'no') {
|
||
if (['Pvt Ltd', 'LLP'].includes(formData.companyType)) {
|
||
// For Private Limited and LLP - CIN is required only when GST is not registered
|
||
if (!formData.cin.trim()) newErrors.cin = 'CIN is required for Private Limited and LLP companies';
|
||
} else if (['Partnership', 'Sole Proprietorship', 'One Person Company (OPC)', 'Other'].includes(formData.companyType)) {
|
||
// For Partnership, Proprietorship, Solo/Individual - PAN is required only when GST is not registered
|
||
if (!formData.panNumber.trim()) newErrors.panNumber = 'PAN Number is required for this company type';
|
||
}
|
||
}
|
||
|
||
if (!formData.industry) newErrors.industry = 'Industry is required';
|
||
if (!formData.goals.trim()) newErrors.goals = 'Please tell us about your goals';
|
||
|
||
setErrors(newErrors);
|
||
return Object.keys(newErrors).length === 0;
|
||
};
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
|
||
if (!validateForm()) return;
|
||
|
||
setIsLoading(true);
|
||
|
||
// Simulate form submission
|
||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||
|
||
setIsLoading(false);
|
||
console.log('Waitlist form submitted:', formData);
|
||
// Handle success - maybe show success message or close form
|
||
onClose();
|
||
};
|
||
|
||
if (!isVisible) return null;
|
||
|
||
return (
|
||
<div className="fixed inset-0 z-[60] flex items-center justify-center bg-deep-forest/80 backdrop-blur-sm">
|
||
<div className="relative max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||
{/* Close Button */}
|
||
<button
|
||
onClick={onClose}
|
||
className="absolute -top-2 -right-2 w-8 h-8 bg-luxury-gold hover:bg-luxury-gold/90 text-deep-forest rounded-full flex items-center justify-center font-bold z-10 transition-all duration-200 shadow-lg"
|
||
>
|
||
×
|
||
</button>
|
||
|
||
{/* Back Button */}
|
||
<button
|
||
onClick={onBack}
|
||
className="absolute -top-2 -left-2 w-8 h-8 bg-luxury-gold/20 hover:bg-luxury-gold/30 text-luxury-gold border border-luxury-gold/30 rounded-full flex items-center justify-center font-bold z-10 transition-all duration-200 shadow-lg"
|
||
>
|
||
←
|
||
</button>
|
||
|
||
{/* Form Container */}
|
||
<div className="bg-gradient-to-br from-deep-forest/95 to-rich-brown/90 backdrop-blur-2xl border border-luxury-gold/30 rounded-2xl shadow-2xl overflow-hidden">
|
||
{/* Header */}
|
||
<div className="bg-gradient-to-r from-luxury-gold/10 to-luxury-gold/5 px-8 py-6 border-b border-luxury-gold/20">
|
||
<div className="text-center">
|
||
<img
|
||
src="/assets/Dark-Icon-cropped.svg"
|
||
alt="Pennyzo Icon"
|
||
className="w-16 h-16 mx-auto mb-4"
|
||
/>
|
||
<h2 className="text-luxury-gold font-luxury-serif text-2xl font-bold mb-2">
|
||
Join Our Exclusive Waitlist
|
||
</h2>
|
||
<p className="text-cream/70 font-elegant-sans text-sm">
|
||
Complete your application to secure your spot
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Form */}
|
||
<div className="p-8">
|
||
<form onSubmit={handleSubmit} className="space-y-6">
|
||
{/* Personal Information Section */}
|
||
<div>
|
||
<h3 className="text-luxury-gold font-elegant-sans text-lg font-semibold mb-4 flex items-center">
|
||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||
</svg>
|
||
Personal Information
|
||
</h3>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{/* First Name */}
|
||
<div>
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
First Name <span className="text-red-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.firstName}
|
||
onChange={(e) => handleInputChange('firstName', e.target.value)}
|
||
className={`w-full bg-deep-forest/50 border ${errors.firstName ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream placeholder-cream/40 focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
placeholder="Enter your first name"
|
||
/>
|
||
{errors.firstName && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.firstName}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* Last Name */}
|
||
<div>
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
Last Name <span className="text-red-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.lastName}
|
||
onChange={(e) => handleInputChange('lastName', e.target.value)}
|
||
className={`w-full bg-deep-forest/50 border ${errors.lastName ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream placeholder-cream/40 focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
placeholder="Enter your last name"
|
||
/>
|
||
{errors.lastName && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.lastName}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Email */}
|
||
<div className="mt-4">
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
Email Address <span className="text-red-400">*</span>
|
||
</label>
|
||
<input
|
||
type="email"
|
||
value={formData.email}
|
||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||
className={`w-full bg-deep-forest/50 border ${errors.email ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream placeholder-cream/40 focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
placeholder="Enter your email address"
|
||
/>
|
||
{errors.email && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.email}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Company Information Section */}
|
||
<div>
|
||
<h3 className="text-luxury-gold font-elegant-sans text-lg font-semibold mb-4 flex items-center">
|
||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||
</svg>
|
||
Company Information
|
||
</h3>
|
||
|
||
{/* Company Name */}
|
||
<div className="mb-4">
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
Company Name <span className="text-red-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.companyName}
|
||
onChange={(e) => handleInputChange('companyName', e.target.value)}
|
||
className={`w-full bg-deep-forest/50 border ${errors.companyName ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream placeholder-cream/40 focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
placeholder="Enter your company name"
|
||
/>
|
||
{errors.companyName && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.companyName}</p>
|
||
)}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||
{/* Company Type */}
|
||
<div>
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
Company Type <span className="text-red-400">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.companyType}
|
||
onChange={(e) => handleInputChange('companyType', e.target.value)}
|
||
className={`w-full bg-deep-forest/50 border ${errors.companyType ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
>
|
||
<option value="">Select company type</option>
|
||
{companyTypes.map(type => (
|
||
<option key={type} value={type} className="bg-deep-forest text-cream">
|
||
{type}
|
||
</option>
|
||
))}
|
||
</select>
|
||
{errors.companyType && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.companyType}</p>
|
||
)}
|
||
</div>
|
||
|
||
{/* GST Registered */}
|
||
<div>
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
GST Registered <span className="text-red-400">*</span>
|
||
</label>
|
||
<div className="flex space-x-4 pt-2">
|
||
<label className="flex items-center space-x-2 cursor-pointer">
|
||
<input
|
||
type="radio"
|
||
name="gstRegistered"
|
||
value="yes"
|
||
checked={formData.gstRegistered === 'yes'}
|
||
onChange={(e) => handleInputChange('gstRegistered', e.target.value)}
|
||
className="w-4 h-4 text-luxury-gold bg-deep-forest/50 border-luxury-gold/20 focus:ring-luxury-gold/50 focus:ring-2"
|
||
/>
|
||
<span className="text-cream/90 font-elegant-sans text-sm">Yes</span>
|
||
</label>
|
||
<label className="flex items-center space-x-2 cursor-pointer">
|
||
<input
|
||
type="radio"
|
||
name="gstRegistered"
|
||
value="no"
|
||
checked={formData.gstRegistered === 'no'}
|
||
onChange={(e) => handleInputChange('gstRegistered', e.target.value)}
|
||
className="w-4 h-4 text-luxury-gold bg-deep-forest/50 border-luxury-gold/20 focus:ring-luxury-gold/50 focus:ring-2"
|
||
/>
|
||
<span className="text-cream/90 font-elegant-sans text-sm">No</span>
|
||
</label>
|
||
</div>
|
||
{errors.gstRegistered && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.gstRegistered}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Conditional CIN Field - shows when company type is Pvt Ltd or LLP AND GST is NOT registered */}
|
||
{formData.gstRegistered === 'no' && ['Pvt Ltd', 'LLP'].includes(formData.companyType) && (
|
||
<div className="mb-4">
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
CIN (Company Identification Number) <span className="text-red-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.cin}
|
||
onChange={(e) => handleInputChange('cin', e.target.value.toUpperCase())}
|
||
className={`w-full bg-deep-forest/50 border ${errors.cin ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream placeholder-cream/40 focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
placeholder="e.g., L12345MH2020PLC123456"
|
||
maxLength={21}
|
||
/>
|
||
{errors.cin && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.cin}</p>
|
||
)}
|
||
<p className="text-cream/50 text-xs mt-1 font-elegant-sans">
|
||
21-digit Corporate Identity Number (required when GST not registered)
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Conditional PAN Number Field - shows for other company types AND GST is NOT registered */}
|
||
{formData.gstRegistered === 'no' && ['Partnership', 'Sole Proprietorship', 'One Person Company (OPC)', 'Other'].includes(formData.companyType) && (
|
||
<div className="mb-4">
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
PAN Number <span className="text-red-400">*</span>
|
||
</label>
|
||
<input
|
||
type="text"
|
||
value={formData.panNumber}
|
||
onChange={(e) => handleInputChange('panNumber', e.target.value.toUpperCase())}
|
||
className={`w-full bg-deep-forest/50 border ${errors.panNumber ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream placeholder-cream/40 focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
placeholder="e.g., ABCDE1234F"
|
||
maxLength={10}
|
||
/>
|
||
{errors.panNumber && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.panNumber}</p>
|
||
)}
|
||
<p className="text-cream/50 text-xs mt-1 font-elegant-sans">
|
||
10-digit Permanent Account Number (required when GST not registered)
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
{/* Industry */}
|
||
<div>
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
Industry <span className="text-red-400">*</span>
|
||
</label>
|
||
<select
|
||
value={formData.industry}
|
||
onChange={(e) => handleInputChange('industry', e.target.value)}
|
||
className={`w-full bg-deep-forest/50 border ${errors.industry ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans`}
|
||
>
|
||
<option value="">Select your industry</option>
|
||
{industries.map(industry => (
|
||
<option key={industry} value={industry} className="bg-deep-forest text-cream">
|
||
{industry}
|
||
</option>
|
||
))}
|
||
</select>
|
||
{errors.industry && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.industry}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Qualifying Question Section */}
|
||
<div>
|
||
<h3 className="text-luxury-gold font-elegant-sans text-lg font-semibold mb-4 flex items-center">
|
||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||
</svg>
|
||
Tell Us About Your Goals
|
||
</h3>
|
||
|
||
<div>
|
||
<label className="block text-cream/90 font-elegant-sans text-sm font-medium mb-2">
|
||
What do you want to achieve with Pennyzo? <span className="text-red-400">*</span>
|
||
</label>
|
||
<textarea
|
||
value={formData.goals}
|
||
onChange={(e) => handleInputChange('goals', e.target.value)}
|
||
rows={4}
|
||
className={`w-full bg-deep-forest/50 border ${errors.goals ? 'border-red-400' : 'border-luxury-gold/20'} rounded-lg px-4 py-3 text-cream placeholder-cream/40 focus:outline-none focus:ring-2 focus:ring-luxury-gold/50 focus:border-luxury-gold/50 transition-all duration-300 font-elegant-sans resize-none`}
|
||
placeholder="Tell us about your business goals, challenges you're facing, or what you hope to achieve with our platform..."
|
||
/>
|
||
{errors.goals && (
|
||
<p className="text-red-400 text-xs mt-1 font-elegant-sans">{errors.goals}</p>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Submit Button */}
|
||
<div className="pt-4">
|
||
<button
|
||
type="submit"
|
||
disabled={isLoading}
|
||
className="w-full bg-gradient-to-r from-luxury-gold to-luxury-gold/90 hover:from-luxury-gold/90 hover:to-luxury-gold text-deep-forest font-elegant-sans font-semibold py-4 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 relative overflow-hidden group"
|
||
>
|
||
{isLoading ? (
|
||
<div className="flex items-center justify-center space-x-2">
|
||
<div className="w-5 h-5 border-2 border-deep-forest border-t-transparent rounded-full animate-spin"></div>
|
||
<span>Submitting Application...</span>
|
||
</div>
|
||
) : (
|
||
<>
|
||
<span className="relative z-10">Submit Application</span>
|
||
<div className="absolute inset-0 bg-cream transform scale-x-0 group-hover:scale-x-100 transition-transform origin-left duration-300"></div>
|
||
</>
|
||
)}
|
||
</button>
|
||
</div>
|
||
|
||
{/* Privacy Notice */}
|
||
<div className="text-center text-cream/50 font-elegant-sans text-xs">
|
||
By submitting this form, you agree to our terms of service and privacy policy.
|
||
Your information will be kept confidential and used only for waitlist management.
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default WaitlistForm; |