Initial commit: Complete Pennyzo Frontend with waitlist form implementation

- 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
This commit is contained in:
2025-09-17 20:16:59 +05:30
commit ee87f004ed
28 changed files with 5498 additions and 0 deletions

View File

@@ -0,0 +1,456 @@
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;