| @ -0,0 +1,24 @@ | |||||
| # Logs | |||||
| logs | |||||
| *.log | |||||
| npm-debug.log* | |||||
| yarn-debug.log* | |||||
| yarn-error.log* | |||||
| pnpm-debug.log* | |||||
| lerna-debug.log* | |||||
| node_modules | |||||
| dist | |||||
| dist-ssr | |||||
| *.local | |||||
| # Editor directories and files | |||||
| .vscode/* | |||||
| !.vscode/extensions.json | |||||
| .idea | |||||
| .DS_Store | |||||
| *.suo | |||||
| *.ntvs* | |||||
| *.njsproj | |||||
| *.sln | |||||
| *.sw? | |||||
| @ -0,0 +1,116 @@ | |||||
| import React, { useState } from 'react'; | |||||
| import Header from './src/Header/Header'; | |||||
| import BenefitsSidebar from './src/BasicDetails/BenefitsSidebar'; | |||||
| import QuoteForm from './src/BasicDetails/QuoteForm'; | |||||
| import AIAdvisor from './Components/AIAdvisor'; | |||||
| import WhyChooseUs from './Components/WhyChooseUs'; | |||||
| import { FormData, InsuranceQuote, VehicleInfo } from './types'; | |||||
| import QuoteListPage from './src/Quotation/QuoteListPage'; | |||||
| import ProposalPage from './src/KYCDetails/ProposalPage'; | |||||
| import CheckoutPage from './src/VehicleDetails/CheckoutPage'; | |||||
| import BasicDetails from './src/BasicDetails'; | |||||
| const App: React.FC = () => { | |||||
| const [step, setStep] = useState(1); | |||||
| const [formData, setFormData] = useState<FormData>({ | |||||
| registrationNumber: '', | |||||
| previousPolicyNumber: '', | |||||
| expiryDate: '', | |||||
| coverType: 'comprehensive', | |||||
| claimStatus: 'no', | |||||
| ncb: 0, | |||||
| }); | |||||
| const [selectedQuote, setSelectedQuote] = useState<InsuranceQuote | null>(null); | |||||
| const vehicleInfo: VehicleInfo = { | |||||
| model: 'H NESS CB350 DLX2 DUALTONE', | |||||
| make: 'HONDA MOTORCYCLE AND SCOOTER INDIA (P) LTD', | |||||
| fuelType: 'PETROL', | |||||
| variant: 'HIGHNESS CB 350 DLX 2', | |||||
| registeredCity: 'R.T.O.BORIVALI, Maharashtra', | |||||
| registeredDate: '01-Feb-2021', | |||||
| chassisNumber: 'ME4NC586AMA005171', | |||||
| engineNumber: 'NC58EA1008396', | |||||
| previousPolicyNumber: '2546754321', | |||||
| previousInsurer: 'TATA General Insurance', | |||||
| previousPolicyEndDate: '2025-07-31', | |||||
| regNo: formData.registrationNumber || 'MH47AX9310' | |||||
| }; | |||||
| const handleFormChange = (updates: Partial<FormData>) => { | |||||
| setFormData((prev) => ({ ...prev, ...updates })); | |||||
| }; | |||||
| const handleSubmit = (e: React.FormEvent) => { | |||||
| e.preventDefault(); | |||||
| setStep(2); | |||||
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |||||
| }; | |||||
| const handleBuyNow = (quote: InsuranceQuote) => { | |||||
| setSelectedQuote(quote); | |||||
| setStep(3); | |||||
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |||||
| }; | |||||
| const handleKycComplete = () => { | |||||
| setStep(4); | |||||
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |||||
| }; | |||||
| const userProfile = { | |||||
| name: 'Akanksha Sahai Srivastava', | |||||
| email: 'akshri.inv@gmail.com', | |||||
| avatarLetter: 'A' | |||||
| }; | |||||
| return ( | |||||
| <div className="min-h-screen flex flex-col bg-[#f8fafc]"> | |||||
| <Header user={step >= 2 ? userProfile : undefined} /> | |||||
| {step === 1 && ( | |||||
| <BasicDetails | |||||
| formData={formData} | |||||
| onChange={handleFormChange} | |||||
| onSubmit={handleSubmit} | |||||
| /> | |||||
| )} | |||||
| {step === 2 && ( | |||||
| <QuoteListPage onBuyNow={handleBuyNow} /> | |||||
| )} | |||||
| {step === 3 && selectedQuote && ( | |||||
| <ProposalPage quote={selectedQuote} vehicle={vehicleInfo} onNext={handleKycComplete} /> | |||||
| )} | |||||
| {step === 4 && selectedQuote && ( | |||||
| <CheckoutPage quote={selectedQuote} vehicle={vehicleInfo} /> | |||||
| )} | |||||
| <footer className="bg-white py-12 border-t border-slate-100 mt-auto"> | |||||
| <div className="container mx-auto px-4 text-center"> | |||||
| <div className="flex justify-center mb-6 opacity-30 grayscale"> | |||||
| <div className="flex flex-col leading-none text-left"> | |||||
| <span className="text-xl font-bold" style={{ color: '#1a2b4b', fontFamily: 'serif' }}>nivesh</span> | |||||
| <span className="text-[8px] font-bold tracking-[0.2em] uppercase" style={{ color: '#1a2b4b' }}>Insurance</span> | |||||
| </div> | |||||
| </div> | |||||
| <div className="max-w-3xl mx-auto space-y-3"> | |||||
| <p className="text-slate-500 text-[10px] font-medium leading-relaxed"> | |||||
| A wholly owned subsidiary of Providential Platforms Private Limited (<a href="https://www.nivesh.com" target="_blank" rel="noopener noreferrer" className="text-[#e31e24] hover:underline">https://www.nivesh.com</a>) | |||||
| </p> | |||||
| <p className="text-slate-400 text-[10px] font-medium leading-relaxed"> | |||||
| Nivesh Insurance BROKERS PRIVATE LIMITED is a Direct Broker (Life & General) registered by IRDAI vide Registration Code IRDA/DB856/21, Certificate of Registration No. 769 valid upto 02-09-2027. | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| </footer> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default App; | |||||
| @ -0,0 +1,101 @@ | |||||
| import React, { useEffect, useState, useRef } from 'react'; | |||||
| import { GoogleGenAI } from "@google/genai"; | |||||
| import { FormData } from '../types'; | |||||
| interface AIAdvisorProps { | |||||
| formData: FormData; | |||||
| } | |||||
| const AIAdvisor: React.FC<AIAdvisorProps> = ({ formData }) => { | |||||
| // New pitchable initial line | |||||
| const [advice, setAdvice] = useState<string>("Smart recommendations tailored to your vehicle's profile."); | |||||
| const [isLoading, setIsLoading] = useState(false); | |||||
| const abortControllerRef = useRef<AbortController | null>(null); | |||||
| const getExpertAdvice = async (data: FormData) => { | |||||
| if (!data.registrationNumber && data.ncb === 0 && data.claimStatus === 'no') return; | |||||
| setIsLoading(true); | |||||
| if (abortControllerRef.current) abortControllerRef.current.abort(); | |||||
| abortControllerRef.current = new AbortController(); | |||||
| try { | |||||
| const ai = new GoogleGenAI({ apiKey: process.env.API_KEY || '' }); | |||||
| const prompt = ` | |||||
| You are a Senior Insurance Underwriter at Nivesh Insurance. | |||||
| Analyze this 2-wheeler insurance profile and provide ONE professional, concise guidance tip (max 15 words). | |||||
| Profile Details: | |||||
| - Vehicle Reg: ${data.registrationNumber || 'Not provided'} | |||||
| - Previous Claim: ${data.claimStatus} | |||||
| - Current No Claim Bonus: ${data.ncb}% | |||||
| - Chosen Cover: ${data.coverType} | |||||
| Focus on technical insurance advice like NCB protection, Zero-Dep value, or IDV optimization. | |||||
| Do NOT mention "AI", "Algorithms", or "Premium Discounts". Use an authoritative, professional tone. | |||||
| `; | |||||
| const response = await ai.models.generateContent({ | |||||
| model: 'gemini-3-flash-preview', | |||||
| contents: prompt, | |||||
| config: { | |||||
| temperature: 0.6, | |||||
| maxOutputTokens: 60, | |||||
| } | |||||
| }); | |||||
| const text = response.text; | |||||
| if (text) { | |||||
| setAdvice(text.trim()); | |||||
| } | |||||
| } catch (error: any) { | |||||
| if (error.name !== 'AbortError') { | |||||
| setAdvice("Maintain a consistent insurance history for seamless future claim processing."); | |||||
| } | |||||
| } finally { | |||||
| setIsLoading(false); | |||||
| } | |||||
| }; | |||||
| useEffect(() => { | |||||
| const handler = setTimeout(() => { | |||||
| getExpertAdvice(formData); | |||||
| }, 1200); | |||||
| return () => clearTimeout(handler); | |||||
| }, [formData.registrationNumber, formData.claimStatus, formData.ncb, formData.coverType]); | |||||
| return ( | |||||
| <div className="bg-white border-2 border-slate-100 rounded-3xl p-6 flex gap-5 items-center relative overflow-hidden group hover:border-[#1a2b4b]/20 transition-all duration-500 shadow-sm"> | |||||
| <div className="absolute top-0 right-0 -mr-4 -mt-4 w-24 h-24 bg-[#1a2b4b]/5 rounded-full blur-2xl group-hover:bg-[#e31e24]/5 transition-colors duration-500"></div> | |||||
| <div className="flex-shrink-0 relative"> | |||||
| <div className={`w-12 h-12 bg-slate-50 border border-slate-100 rounded-2xl flex items-center justify-center shadow-inner transition-transform duration-500 ${isLoading ? 'scale-95' : 'group-hover:scale-105'}`}> | |||||
| <svg className={`w-6 h-6 ${isLoading ? 'text-[#e31e24] animate-pulse' : 'text-[#1a2b4b]'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /> | |||||
| </svg> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex-grow"> | |||||
| <div className="flex items-center gap-2 mb-1"> | |||||
| <h4 className="text-[10px] font-black text-[#e31e24] uppercase tracking-[0.2em]">Smart Insight</h4> | |||||
| {isLoading && <span className="text-[8px] font-bold text-slate-400 animate-pulse italic">Optimizing...</span>} | |||||
| </div> | |||||
| <p className={`text-sm font-semibold text-[#1a2b4b] leading-snug transition-all duration-500 ${isLoading ? 'opacity-40' : 'opacity-100'}`}> | |||||
| {advice} | |||||
| </p> | |||||
| </div> | |||||
| <div className="hidden md:block flex-shrink-0 ml-2"> | |||||
| <div className="px-3 py-1 bg-[#1a2b4b]/5 rounded-full border border-[#1a2b4b]/10"> | |||||
| <span className="text-[9px] font-black text-[#1a2b4b]">PROTECT</span> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default AIAdvisor; | |||||
| @ -0,0 +1,119 @@ | |||||
| import React, { useState } from 'react'; | |||||
| import { InsuranceQuote } from '../types'; | |||||
| interface QuoteCardProps { | |||||
| quote: InsuranceQuote; | |||||
| onBuy?: () => void; | |||||
| } | |||||
| const QuoteCard: React.FC<QuoteCardProps> = ({ quote, onBuy }) => { | |||||
| const [isExpanded, setIsExpanded] = useState(false); | |||||
| // Specific features to highlight on the front line | |||||
| const highlightFeatures = quote.features.filter(f => | |||||
| ['PERSONAL_ACCIDENT_COVER', 'ROAD_SIDE_ASSISTANCE', 'ZERO_DEPRECIATION', 'ENGINE_COVER', 'Tax'].includes(f.label) | |||||
| ).slice(0, 4); // Keep only one line worth of features | |||||
| return ( | |||||
| <div className="bg-white rounded-2xl border border-slate-200 shadow-sm hover:shadow-md transition-shadow overflow-hidden mb-4"> | |||||
| {/* Top Row: Insurer & Pricing */} | |||||
| <div className="p-5 flex flex-col md:flex-row items-center gap-6"> | |||||
| {/* Insurer Logo & Name */} | |||||
| <div className="flex items-center gap-4 w-full md:w-1/3"> | |||||
| <div className="w-14 h-14 bg-white rounded-lg p-1 flex items-center justify-center border border-slate-100 flex-shrink-0"> | |||||
| <img src={quote.insurerLogo} alt={quote.insurerName} className="max-h-full max-w-full object-contain" /> | |||||
| </div> | |||||
| <div> | |||||
| <h3 className="text-[#1a2b4b] font-bold text-sm leading-tight line-clamp-2">{quote.insurerName}</h3> | |||||
| <div className="flex items-center gap-2 mt-1"> | |||||
| <span className="bg-emerald-50 text-emerald-600 text-[9px] font-black px-1.5 py-0.5 rounded uppercase border border-emerald-100">Quick Approval</span> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {/* IDV Value Display */} | |||||
| <div className="flex-1 flex items-center justify-center md:border-x border-slate-100"> | |||||
| <div className="text-center md:px-4"> | |||||
| <p className="text-[10px] text-slate-400 uppercase font-black tracking-widest mb-0.5">IDV Value</p> | |||||
| <p className="text-sm font-bold text-[#1a2b4b]">₹ {quote.idvValue.toLocaleString()}</p> | |||||
| </div> | |||||
| </div> | |||||
| {/* Action & Price */} | |||||
| <div className="flex items-center gap-4 w-full md:w-auto justify-between md:justify-end"> | |||||
| <div className="text-right"> | |||||
| <p className="text-[10px] text-slate-400 font-bold line-through">₹ {(quote.premium + 150).toLocaleString()}</p> | |||||
| <p className="text-2xl font-black text-[#1a2b4b] leading-none"> | |||||
| <span className="text-xs font-bold mr-0.5">₹</span> | |||||
| {quote.premium.toLocaleString()} | |||||
| </p> | |||||
| </div> | |||||
| <button | |||||
| onClick={onBuy} | |||||
| className="bg-[#2b458c] hover:bg-[#1a2b4b] text-white px-8 py-3 rounded-xl font-black text-sm transition-all shadow-lg shadow-[#2b458c]/10" | |||||
| > | |||||
| BUY NOW | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| {/* NEW SECTION: Key features in one line below Buy Now */} | |||||
| <div className="px-5 pb-4"> | |||||
| <div className="flex flex-wrap gap-4 items-center"> | |||||
| <span className="text-[9px] font-black text-slate-400 uppercase tracking-widest mr-2">Key Benefits:</span> | |||||
| {highlightFeatures.map((f, i) => ( | |||||
| <div key={i} className="flex items-center gap-1.5"> | |||||
| <svg className="w-3 h-3 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7" /></svg> | |||||
| <span className="text-[10px] text-[#1a2b4b] font-bold"> | |||||
| {f.label.replace(/_/g, ' ')}: <span className="text-slate-500 font-black">{typeof f.value === 'number' && f.value > 1000 ? `₹${(f.value/100000).toFixed(1)}L` : f.value}</span> | |||||
| </span> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| {/* Expand/Collapse Toggle Bar */} | |||||
| <button | |||||
| onClick={() => setIsExpanded(!isExpanded)} | |||||
| className="w-full py-2 bg-slate-50/50 border-t border-slate-50 flex items-center justify-center gap-2 hover:bg-slate-50 transition-colors" | |||||
| > | |||||
| <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest"> | |||||
| {isExpanded ? 'Hide Details' : 'View All Policy Features'} | |||||
| </span> | |||||
| <svg | |||||
| className={`w-3 h-3 text-slate-400 transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`} | |||||
| fill="none" stroke="currentColor" viewBox="0 0 24 24" | |||||
| > | |||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /> | |||||
| </svg> | |||||
| </button> | |||||
| {/* Expanded Content */} | |||||
| {isExpanded && ( | |||||
| <div className="p-6 bg-white border-t border-slate-100 animate-in fade-in slide-in-from-top-2 duration-300"> | |||||
| <h4 className="text-[#1a2b4b] text-[10px] font-black uppercase tracking-[0.2em] mb-4 opacity-50">Detailed Coverage & Breakdown</h4> | |||||
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-8 gap-y-2"> | |||||
| {quote.features.map((feature, idx) => ( | |||||
| <div key={idx} className="flex items-center justify-between gap-2 text-[10px] py-1 border-b border-slate-50 last:border-0"> | |||||
| <div className="flex items-center gap-2 overflow-hidden"> | |||||
| <svg className="w-3 h-3 text-emerald-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M5 13l4 4L19 7" /> | |||||
| </svg> | |||||
| <span className="text-slate-500 font-bold uppercase tracking-tight truncate">{feature.label.replace(/_/g, ' ')}</span> | |||||
| </div> | |||||
| <span className="text-[#1a2b4b] font-black flex-shrink-0"> | |||||
| {feature.value === true || feature.value === 'True' || feature.value === 'Included' || feature.value === 'Yes' ? 'Yes' : | |||||
| feature.value === false || feature.value === 'False' || feature.value === 'No' ? 'No' : | |||||
| feature.value} | |||||
| </span> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default QuoteCard; | |||||
| @ -0,0 +1,58 @@ | |||||
| import React, { useState, useEffect } from 'react'; | |||||
| const WhyChooseUs: React.FC = () => { | |||||
| const points = [ | |||||
| { title: "99.2% Settlement", subtitle: "Industry leading claim record", icon: "🏆" }, | |||||
| { title: "24/7 Support", subtitle: "Round the clock assistance", icon: "📞" }, | |||||
| { title: "Instant Policy", subtitle: "Buy and download in 2 minutes", icon: "⚡" }, | |||||
| { title: "Paperless Claims", subtitle: "Fully digital hassle-free process", icon: "📲" }, | |||||
| { title: "Lowest Prices", subtitle: "Best quotes guaranteed", icon: "💰" } | |||||
| ]; | |||||
| const [currentIndex, setCurrentIndex] = useState(0); | |||||
| useEffect(() => { | |||||
| const timer = setInterval(() => { | |||||
| setCurrentIndex((prev) => (prev + 1) % points.length); | |||||
| }, 3000); | |||||
| return () => clearInterval(timer); | |||||
| }, []); | |||||
| return ( | |||||
| <div className="bg-[#f1f5f9] rounded-3xl p-6 border border-slate-200 overflow-hidden relative"> | |||||
| <div className="flex items-center justify-between mb-4"> | |||||
| <h3 className="text-[#1a2b4b] text-xs font-black uppercase tracking-widest">Why Choose Us?</h3> | |||||
| <div className="flex gap-1"> | |||||
| {points.map((_, i) => ( | |||||
| <div | |||||
| key={i} | |||||
| className={`w-1.5 h-1.5 rounded-full transition-all duration-300 ${i === currentIndex ? 'bg-[#e31e24] w-4' : 'bg-slate-300'}`} | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| <div className="relative h-16"> | |||||
| {points.map((point, idx) => ( | |||||
| <div | |||||
| key={idx} | |||||
| className={`absolute inset-0 flex items-center gap-4 transition-all duration-700 ease-in-out ${ | |||||
| idx === currentIndex ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-8 pointer-events-none' | |||||
| }`} | |||||
| > | |||||
| <div className="w-12 h-12 bg-white rounded-2xl flex items-center justify-center text-2xl shadow-sm border border-slate-100"> | |||||
| {point.icon} | |||||
| </div> | |||||
| <div> | |||||
| <h4 className="text-[#1a2b4b] font-bold text-sm leading-none mb-1">{point.title}</h4> | |||||
| <p className="text-slate-500 text-[11px] font-medium">{point.subtitle}</p> | |||||
| </div> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default WhyChooseUs; | |||||
| @ -0,0 +1,20 @@ | |||||
| <div align="center"> | |||||
| <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" /> | |||||
| </div> | |||||
| # Run and deploy your AI Studio app | |||||
| This contains everything you need to run your app locally. | |||||
| View your app in AI Studio: https://ai.studio/apps/drive/13U7FfFtZi6YY5N1GH86-GpTd7TFEKQyc | |||||
| ## Run Locally | |||||
| **Prerequisites:** Node.js | |||||
| 1. Install dependencies: | |||||
| `npm install` | |||||
| 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key | |||||
| 3. Run the app: | |||||
| `npm run dev` | |||||
| @ -0,0 +1,34 @@ | |||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8" /> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |||||
| <title>Nivesh Insurance | 2-Wheeler Quote</title> | |||||
| <script src="https://cdn.tailwindcss.com"></script> | |||||
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |||||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |||||
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |||||
| <style> | |||||
| body { | |||||
| font-family: 'Inter', sans-serif; | |||||
| background-color: #f8fafc; | |||||
| } | |||||
| </style> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "@google/genai": "https://esm.sh/@google/genai@^1.34.0", | |||||
| "react/": "https://esm.sh/react@^19.2.3/", | |||||
| "react": "https://esm.sh/react@^19.2.3", | |||||
| "react-dom/": "https://esm.sh/react-dom@^19.2.3/" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <link rel="stylesheet" href="/index.css"> | |||||
| </head> | |||||
| <body> | |||||
| <div id="root"></div> | |||||
| <script type="module" src="/index.tsx"></script> | |||||
| </body> | |||||
| </html> | |||||
| @ -0,0 +1,38 @@ | |||||
| import React from 'react'; | |||||
| import ReactDOM from 'react-dom/client'; | |||||
| import App from './App'; | |||||
| import PageRoutes from './src/Routes'; | |||||
| import Header from './src/Header/Header'; | |||||
| const rootElement = document.getElementById('root'); | |||||
| if (!rootElement) { | |||||
| throw new Error("Could not find root element to mount to"); | |||||
| } | |||||
| const root = ReactDOM.createRoot(rootElement); | |||||
| root.render( | |||||
| <React.StrictMode> | |||||
| {/* <App /> */} | |||||
| <Header /> | |||||
| <PageRoutes /> | |||||
| <footer className="bg-white py-12 border-t border-slate-100 mt-auto"> | |||||
| <div className="container mx-auto px-4 text-center"> | |||||
| <div className="flex justify-center mb-6 opacity-30 grayscale"> | |||||
| <div className="flex flex-col leading-none text-left"> | |||||
| <span className="text-xl font-bold" style={{ color: '#1a2b4b', fontFamily: 'serif' }}>nivesh</span> | |||||
| <span className="text-[8px] font-bold tracking-[0.2em] uppercase" style={{ color: '#1a2b4b' }}>Insurance</span> | |||||
| </div> | |||||
| </div> | |||||
| <div className="max-w-3xl mx-auto space-y-3"> | |||||
| <p className="text-slate-500 text-[10px] font-medium leading-relaxed"> | |||||
| A wholly owned subsidiary of Providential Platforms Private Limited (<a href="https://www.nivesh.com" target="_blank" rel="noopener noreferrer" className="text-[#e31e24] hover:underline">https://www.nivesh.com</a>) | |||||
| </p> | |||||
| <p className="text-slate-400 text-[10px] font-medium leading-relaxed"> | |||||
| Nivesh Insurance BROKERS PRIVATE LIMITED is a Direct Broker (Life & General) registered by IRDAI vide Registration Code IRDA/DB856/21, Certificate of Registration No. 769 valid upto 02-09-2027. | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| </footer> | |||||
| </React.StrictMode> | |||||
| ); | |||||
| @ -0,0 +1,7 @@ | |||||
| { | |||||
| "name": "Nivesh Insurance - 2-Wheeler full journey", | |||||
| "description": "A modern, high-conversion insurance quote generation landing page for two-wheeler insurance, featuring AI-assisted guidance and a clean, user-friendly interface.", | |||||
| "requestFramePermissions": [ | |||||
| "camera" | |||||
| ] | |||||
| } | |||||
| @ -0,0 +1,24 @@ | |||||
| { | |||||
| "name": "nivesh-insurance---2-wheeler-full-journey", | |||||
| "private": true, | |||||
| "version": "0.0.0", | |||||
| "type": "module", | |||||
| "scripts": { | |||||
| "dev": "vite", | |||||
| "build": "vite build", | |||||
| "preview": "vite preview", | |||||
| "start": "react-scripts start" | |||||
| }, | |||||
| "dependencies": { | |||||
| "@google/genai": "^1.34.0", | |||||
| "react": "^19.2.3", | |||||
| "react-dom": "^19.2.3", | |||||
| "react-router-dom": "^7.12.0" | |||||
| }, | |||||
| "devDependencies": { | |||||
| "@types/node": "^22.14.0", | |||||
| "@vitejs/plugin-react": "^5.0.0", | |||||
| "typescript": "~5.8.2", | |||||
| "vite": "^6.2.0" | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,70 @@ | |||||
| import React from 'react'; | |||||
| const BenefitsSidebar: React.FC = () => { | |||||
| const coveragePoints = [ | |||||
| "Damage due to accident of carrying truck / conveyance", | |||||
| "Fire, lightening or explosion", | |||||
| "Theft / pilferage", | |||||
| "Loading and unloading damage", | |||||
| "Malicious damage", | |||||
| "Overturning or Derailment", | |||||
| "Breakage of bridge", | |||||
| "Collision", | |||||
| "Damage to the goods / cargo due to sea / river / lake water entering inside the vehicle" | |||||
| ]; | |||||
| return ( | |||||
| <div className="space-y-4"> | |||||
| <div className="bg-[#f8f9fb] border border-slate-200 rounded-3xl p-6 shadow-sm"> | |||||
| <h3 className="text-[#1a2b4b] text-lg font-bold mb-6 flex items-center gap-2"> | |||||
| <span className="w-1.5 h-6 bg-[#e31e24] rounded-full"></span> | |||||
| Standard Risks Covered | |||||
| </h3> | |||||
| <div className="space-y-4"> | |||||
| {coveragePoints.map((point, idx) => ( | |||||
| <div key={idx} className="flex gap-3 items-start group"> | |||||
| <div className="flex-shrink-0 mt-0.5"> | |||||
| <div className="w-5 h-5 rounded-full bg-emerald-50 flex items-center justify-center border border-emerald-100 group-hover:bg-emerald-500 group-hover:border-emerald-500 transition-colors duration-300"> | |||||
| <svg | |||||
| className="w-2.5 h-2.5 text-emerald-600 group-hover:text-white transition-colors duration-300" | |||||
| fill="none" | |||||
| viewBox="0 0 24 24" | |||||
| stroke="currentColor" | |||||
| > | |||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="4" d="M5 13l4 4L19 7" /> | |||||
| </svg> | |||||
| </div> | |||||
| </div> | |||||
| <p className="text-[12px] font-medium text-[#1a2b4b]/80 leading-snug"> | |||||
| {point} | |||||
| </p> | |||||
| </div> | |||||
| ))} | |||||
| </div> | |||||
| <div className="mt-8 pt-6 border-t border-slate-200"> | |||||
| <div className="flex items-center justify-between p-3 bg-white rounded-2xl border border-slate-100"> | |||||
| <div className="flex items-center gap-2"> | |||||
| <div className="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></div> | |||||
| <span className="text-[10px] font-bold text-slate-500 uppercase">Settlement Ratio</span> | |||||
| </div> | |||||
| <span className="text-sm font-black text-[#1a2b4b]">99.2%</span> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div className="p-6 rounded-3xl bg-gradient-to-br from-[#1a2b4b] to-[#253b66] text-white relative overflow-hidden group"> | |||||
| <div className="absolute top-[-10px] right-[-10px] w-24 h-24 bg-white/5 rounded-full blur-2xl group-hover:bg-[#e31e24]/10 transition-all duration-500"></div> | |||||
| <h4 className="text-sm font-bold mb-2 text-white/90 uppercase tracking-widest">Instant Support</h4> | |||||
| <p className="text-xs text-white/70 mb-4">Questions about coverage? Our experts are here to help you choose.</p> | |||||
| <button className="w-full py-3 bg-[#e31e24] hover:bg-[#c41a1f] text-white rounded-xl text-xs font-bold transition-all shadow-lg shadow-black/20"> | |||||
| Talk to an Expert | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default BenefitsSidebar; | |||||
| @ -0,0 +1,171 @@ | |||||
| import React from 'react'; | |||||
| import { FormData } from '@/types'; | |||||
| import { useNavigate } from 'react-router-dom'; | |||||
| interface QuoteFormProps { | |||||
| data: FormData; | |||||
| onChange: (updates: Partial<FormData>) => void; | |||||
| onSubmit: (e: React.FormEvent) => void; | |||||
| } | |||||
| const QuoteForm: React.FC<QuoteFormProps> = ({ data, onChange, onSubmit }) => { | |||||
| console.log("SKJDHJSHDSJD", data); | |||||
| const navigate = useNavigate() | |||||
| const handleSubmit = (e: React.FormEvent) => { | |||||
| e.preventDefault(); | |||||
| onSubmit(e); | |||||
| // navigate("/quote-list"); | |||||
| }; | |||||
| const handleRedirection = () => { | |||||
| if (data.registrationNumber || data.expiryDate) { | |||||
| navigate("/quote-list"); | |||||
| } | |||||
| }; | |||||
| return ( | |||||
| <form onSubmit={handleSubmit} className="space-y-8"> | |||||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6"> | |||||
| {/* Registration Number */} | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-black uppercase tracking-widest text-slate-400"> | |||||
| Vehicle Registration Number <span className="text-[#e31e24]">*</span> | |||||
| </label> | |||||
| <input | |||||
| type="text" | |||||
| placeholder="MH 12 AB 1234" | |||||
| className="w-full px-5 py-4 rounded-2xl border-2 border-slate-100 bg-white focus:border-[#e31e24] focus:ring-4 focus:ring-[#e31e24]/5 outline-none transition-all placeholder:text-slate-300 uppercase font-bold text-[#1a2b4b]" | |||||
| value={data.registrationNumber} | |||||
| onChange={(e) => onChange({ registrationNumber: e.target.value })} | |||||
| required | |||||
| /> | |||||
| </div> | |||||
| {/* Previous Policy Number */} | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-black uppercase tracking-widest text-slate-400"> | |||||
| Previous Policy Number | |||||
| </label> | |||||
| <input | |||||
| type="text" | |||||
| placeholder="Optional" | |||||
| className="w-full px-5 py-4 rounded-2xl border-2 border-slate-100 bg-white focus:border-[#1a2b4b] focus:ring-4 focus:ring-[#1a2b4b]/5 outline-none transition-all placeholder:text-slate-300 text-[#1a2b4b]" | |||||
| value={data.previousPolicyNumber} | |||||
| onChange={(e) => onChange({ previousPolicyNumber: e.target.value })} | |||||
| /> | |||||
| </div> | |||||
| {/* Expiry Date */} | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-black uppercase tracking-widest text-slate-400"> | |||||
| Expiry Date <span className="text-[#e31e24]">*</span> | |||||
| </label> | |||||
| <input | |||||
| type="date" | |||||
| className="w-full px-5 py-4 rounded-2xl border-2 border-slate-100 bg-white focus:border-[#1a2b4b] focus:ring-4 focus:ring-[#1a2b4b]/5 outline-none transition-all font-medium text-[#1a2b4b]" | |||||
| value={data.expiryDate} | |||||
| onChange={(e) => onChange({ expiryDate: e.target.value })} | |||||
| required | |||||
| /> | |||||
| </div> | |||||
| {/* Cover Type */} | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-black uppercase tracking-widest text-slate-400"> | |||||
| Cover Type | |||||
| </label> | |||||
| <div className="relative"> | |||||
| <select | |||||
| className="w-full px-5 py-4 rounded-2xl border-2 border-slate-100 focus:border-[#1a2b4b] focus:ring-4 focus:ring-[#1a2b4b]/5 outline-none transition-all bg-white appearance-none cursor-pointer font-medium text-[#1a2b4b]" | |||||
| value={data.coverType} | |||||
| onChange={(e) => onChange({ coverType: e.target.value as any })} | |||||
| > | |||||
| <option value="comprehensive">Comprehensive Cover</option> | |||||
| <option value="third-party">Third Party Only</option> | |||||
| <option value="own-damage">Standalone Own Damage</option> | |||||
| </select> | |||||
| <div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400"> | |||||
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {/* Claim Status */} | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-black uppercase tracking-widest text-slate-400"> | |||||
| Claimed Last Year? | |||||
| </label> | |||||
| <div className="flex gap-3 p-1.5 bg-slate-50 rounded-2xl border border-slate-100"> | |||||
| <button | |||||
| type="button" | |||||
| className={`flex-1 py-3 rounded-xl font-bold text-sm transition-all ${data.claimStatus === 'no' | |||||
| ? 'bg-white text-[#e31e24] shadow-sm' | |||||
| : 'text-slate-400 hover:text-slate-600' | |||||
| }`} | |||||
| onClick={() => onChange({ claimStatus: 'no' })} | |||||
| > | |||||
| No | |||||
| </button> | |||||
| <button | |||||
| type="button" | |||||
| className={`flex-1 py-3 rounded-xl font-bold text-sm transition-all ${data.claimStatus === 'yes' | |||||
| ? 'bg-white text-[#e31e24] shadow-sm' | |||||
| : 'text-slate-400 hover:text-slate-600' | |||||
| }`} | |||||
| onClick={() => onChange({ claimStatus: 'yes' })} | |||||
| > | |||||
| Yes | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| {/* NCB Percentage */} | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-black uppercase tracking-widest text-slate-400 flex justify-between"> | |||||
| No Claim Bonus % | |||||
| <span className="text-[#e31e24] font-black">SAVE UP TO 50%</span> | |||||
| </label> | |||||
| <div className="relative"> | |||||
| <select | |||||
| className="w-full px-5 py-4 rounded-2xl border-2 border-slate-100 focus:border-[#1a2b4b] focus:ring-4 focus:ring-[#1a2b4b]/5 outline-none transition-all bg-white appearance-none cursor-pointer font-bold text-[#1a2b4b]" | |||||
| value={data.ncb} | |||||
| onChange={(e) => onChange({ ncb: parseInt(e.target.value) })} | |||||
| disabled={data.claimStatus === 'yes'} | |||||
| > | |||||
| <option value="0">0% (New Policy)</option> | |||||
| <option value="20">20%</option> | |||||
| <option value="25">25%</option> | |||||
| <option value="35">35%</option> | |||||
| <option value="45">45%</option> | |||||
| <option value="50">50%</option> | |||||
| </select> | |||||
| <div className="absolute right-5 top-1/2 -translate-y-1/2 pointer-events-none text-slate-400"> | |||||
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div className="pt-6 border-t border-slate-50"> | |||||
| <button | |||||
| type="submit" | |||||
| className="w-full bg-[#e31e24] hover:bg-[#c41a1f] text-white font-black py-5 px-8 rounded-2xl shadow-xl shadow-[#e31e24]/20 hover:shadow-[#e31e24]/30 hover:-translate-y-0.5 transition-all flex items-center justify-center gap-3 text-lg" | |||||
| // onClick={() => navigate("/quote-list")} | |||||
| onClick={() => handleRedirection()} | |||||
| > | |||||
| Check Live Prices | |||||
| <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" /> | |||||
| </svg> | |||||
| </button> | |||||
| </div> | |||||
| </form> | |||||
| ); | |||||
| }; | |||||
| export default QuoteForm; | |||||
| @ -0,0 +1,84 @@ | |||||
| import AIAdvisor from '@/Components/AIAdvisor'; | |||||
| import WhyChooseUs from '@/Components/WhyChooseUs'; | |||||
| import React, { useState } from 'react'; | |||||
| import QuoteForm from './QuoteForm'; | |||||
| import BenefitsSidebar from './BenefitsSidebar'; | |||||
| import { FormData } from '@/types'; | |||||
| const BasicDetails = (props) => { | |||||
| const { handleSubmit } = props; | |||||
| const [formData, setFormData] = useState<FormData>({ | |||||
| registrationNumber: '', | |||||
| previousPolicyNumber: '', | |||||
| expiryDate: '', | |||||
| coverType: 'comprehensive', | |||||
| claimStatus: 'no', | |||||
| ncb: 0, | |||||
| }); | |||||
| const handleFormChange = (updates: Partial<FormData>) => { | |||||
| setFormData((prev) => ({ ...prev, ...updates })); | |||||
| }; | |||||
| return ( | |||||
| <main className="flex-grow container mx-auto px-4 py-8 lg:py-16"> | |||||
| <div className="max-w-6xl mx-auto"> | |||||
| <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start"> | |||||
| <aside className="lg:col-span-4 space-y-6 lg:sticky lg:top-28"> | |||||
| <div className="mb-6"> | |||||
| <h1 className="text-3xl font-bold text-[#1a2b4b] leading-tight mb-2"> | |||||
| Protect your <br /><span className="text-[#e31e24]">2-Wheeler</span> today | |||||
| </h1> | |||||
| <p className="text-slate-500 text-sm"> | |||||
| Join 2M+ riders who trust Nivesh for their journeys. | |||||
| </p> | |||||
| </div> | |||||
| <BenefitsSidebar /> | |||||
| </aside> | |||||
| <div className="lg:col-span-8 space-y-8"> | |||||
| <div className="bg-white rounded-3xl shadow-[0_10px_40px_-15px_rgba(0,0,0,0.08)] border border-slate-100 overflow-hidden"> | |||||
| <div className="p-8 lg:p-10"> | |||||
| <div className="flex items-center justify-between mb-8"> | |||||
| <h2 className="text-2xl font-bold text-[#1a2b4b]"> | |||||
| Get a Quick Quote | |||||
| </h2> | |||||
| <div className="text-[10px] font-black text-emerald-600 bg-emerald-50 px-3 py-1 rounded-full uppercase tracking-widest border border-emerald-100"> | |||||
| Live Prices | |||||
| </div> | |||||
| </div> | |||||
| <QuoteForm | |||||
| data={formData} | |||||
| onChange={handleFormChange} | |||||
| onSubmit={handleSubmit} | |||||
| /> | |||||
| </div> | |||||
| <div className="bg-[#1a2b4b] p-5 text-center"> | |||||
| <p className="text-[11px] text-white/80 font-bold uppercase tracking-widest flex items-center justify-center gap-2"> | |||||
| <svg className="w-4 h-4 text-emerald-400" fill="currentColor" viewBox="0 0 20 20"> | |||||
| <path fillRule="evenodd" d="M2.166 4.999A11.954 11.954 0 0010 1.944 11.954 11.954 0 0017.834 5c.11.65.166 1.32.166 2.001 0 5.225-3.34 9.67-8 11.317C5.34 16.67 2 12.225 2 7c0-.682.057-1.35.166-2.001zm11.541 3.708a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /> | |||||
| </svg> | |||||
| IRDAI Certified Insurance Broker | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> | |||||
| <AIAdvisor | |||||
| formData={formData} | |||||
| /> | |||||
| <WhyChooseUs /> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </main> | |||||
| ); | |||||
| }; | |||||
| export default BasicDetails; | |||||
| @ -0,0 +1,8 @@ | |||||
| export const formFileds = { | |||||
| registrationNumber: '', | |||||
| previousPolicyNumber: '', | |||||
| expiryDate: '', | |||||
| coverType: 'comprehensive', | |||||
| claimStatus: 'no', | |||||
| ncb: 0, | |||||
| } | |||||
| @ -0,0 +1,65 @@ | |||||
| import React from 'react'; | |||||
| interface HeaderProps { | |||||
| user?: { | |||||
| name: string; | |||||
| email: string; | |||||
| avatarLetter: string; | |||||
| }; | |||||
| } | |||||
| const Header: React.FC<HeaderProps> = ({ user }) => { | |||||
| return ( | |||||
| <header className="bg-white border-b border-slate-100 sticky top-0 z-50"> | |||||
| <div className="container mx-auto px-4 h-20 flex items-center justify-between"> | |||||
| <div className="flex items-center gap-2"> | |||||
| {/* Brand Logo Recreation */} | |||||
| <div className="flex flex-col leading-none"> | |||||
| <span className="text-3xl font-bold tracking-tight" style={{ color: '#e31e24', fontFamily: 'serif' }}> | |||||
| nivesh | |||||
| </span> | |||||
| <span className="text-[11px] font-bold tracking-[0.2em] uppercase mt-[-2px]" style={{ color: '#1a2b4b' }}> | |||||
| Insurance | |||||
| </span> | |||||
| </div> | |||||
| </div> | |||||
| <nav className="hidden md:flex items-center gap-10"> | |||||
| <div className="relative group"> | |||||
| <a href="#" className="text-[#1a2b4b] font-medium transition-colors flex items-center gap-1"> | |||||
| Insurance | |||||
| </a> | |||||
| </div> | |||||
| <a href="#" className="text-slate-600 hover:text-[#e31e24] font-medium transition-colors">Insurance Advisors</a> | |||||
| <div className="relative group"> | |||||
| <a href="#" className="text-slate-600 hover:text-[#e31e24] font-medium transition-colors flex items-center gap-1"> | |||||
| Support | |||||
| <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg> | |||||
| </a> | |||||
| </div> | |||||
| </nav> | |||||
| <div className="flex items-center gap-4"> | |||||
| {user ? ( | |||||
| <div className="flex items-center gap-4 bg-slate-50 border border-slate-100 rounded-xl px-4 py-2"> | |||||
| <div className="text-right hidden sm:block"> | |||||
| <p className="text-xs font-bold text-[#1a2b4b]">{user.name}</p> | |||||
| <p className="text-[10px] text-slate-400">{user.email}</p> | |||||
| </div> | |||||
| <div className="w-10 h-10 rounded-full bg-indigo-600 flex items-center justify-center text-white font-bold text-lg shadow-sm border-2 border-white"> | |||||
| {user.avatarLetter} | |||||
| </div> | |||||
| </div> | |||||
| ) : ( | |||||
| <button className="bg-[#1a2b4b] text-white px-5 py-2 rounded-lg font-semibold text-sm hover:bg-[#253b66] transition-all"> | |||||
| My Account | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| </header> | |||||
| ); | |||||
| }; | |||||
| export default Header; | |||||
| @ -0,0 +1,200 @@ | |||||
| import React, { useState } from 'react'; | |||||
| import { InsuranceQuote, VehicleInfo, ProposalData } from '@/types'; | |||||
| const ProposalPage: React.FC = () => { | |||||
| const [proposal, setProposal] = useState<ProposalData>({ | |||||
| pan: 'AATPU7693N', | |||||
| name: 'Suketu Upadhyay', | |||||
| dob: '1984-07-03', | |||||
| phone: '9833604121', | |||||
| email: 'Suketu123_Upadhyay@hotmail.com' | |||||
| }); | |||||
| const quote: InsuranceQuote = { | |||||
| id: 'QUOTE123', | |||||
| insurerName: 'TATA General Insurance', | |||||
| insurerLogo: 'https://via.placeholder.com/100x40.png?text=TATA', | |||||
| premium: 5000, | |||||
| idvValue: 120000, | |||||
| features: ['24x7 Support', 'Roadside Assistance'] | |||||
| }; | |||||
| const vehicle: VehicleInfo = { | |||||
| model: 'H NESS CB350 DLX2 DUALTONE', | |||||
| make: 'HONDA MOTORCYCLE AND SCOOTER INDIA (P) LTD', | |||||
| fuelType: 'PETROL', | |||||
| variant: 'HIGHNESS CB 350 DLX 2', | |||||
| registeredCity: 'R.T.O.BORIVALI, Maharashtra', | |||||
| registeredDate: '01-Feb-2021', | |||||
| chassisNumber: 'ME4NC586AMA005171', | |||||
| engineNumber: 'NC58EA1008396', | |||||
| previousPolicyNumber: '2546754321', | |||||
| previousInsurer: 'TATA General Insurance', | |||||
| previousPolicyEndDate: '2025-07-31', | |||||
| regNo: 'MH47AX9310' | |||||
| }; | |||||
| const tax = Math.round(quote.premium * 0.18); | |||||
| const basePrice = quote.premium; | |||||
| const total = basePrice + tax; | |||||
| return ( | |||||
| <div className="container mx-auto px-4 py-8 space-y-6 animate-in fade-in duration-500"> | |||||
| {/* 1. Vehicle Details Header */} | |||||
| <div className="bg-[#1a2b4b] text-white rounded-2xl shadow-xl overflow-hidden relative border border-white/10"> | |||||
| <div className="p-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-x-8 gap-y-6"> | |||||
| <HeaderDetail label="MODEL:" value={vehicle.model} /> | |||||
| <HeaderDetail label="MAKE:" value={vehicle.make} /> | |||||
| <HeaderDetail label="FUEL_TYPE:" value={vehicle.fuelType} /> | |||||
| <HeaderDetail label="VARIANT:" value={vehicle.variant} /> | |||||
| <HeaderDetail label="REGISTERED_CITY:" value={vehicle.registeredCity} /> | |||||
| <HeaderDetail label="REGISTERED_DATE:" value={vehicle.registeredDate} /> | |||||
| <HeaderDetail label="CHASSIS_NUMBER:" value={vehicle.chassisNumber} /> | |||||
| <HeaderDetail label="ENGINE_NUMBER:" value={vehicle.engineNumber} /> | |||||
| <HeaderDetail label="PREVIOUS_INSURER:" value={vehicle.previousInsurer} /> | |||||
| <HeaderDetail label="PREVIOUS_POLICY_NUMBER:" value={vehicle.previousPolicyNumber} /> | |||||
| <HeaderDetail label="PREVIOUS_POLICY_END_DATE:" value={vehicle.previousPolicyEndDate} /> | |||||
| <HeaderDetail label="REGISTRATION_NO:" value={vehicle.regNo} /> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex flex-col lg:flex-row gap-8"> | |||||
| {/* 2. KYC Information Form (Left) */} | |||||
| <div className="lg:w-2/3 bg-white rounded-3xl p-8 border border-slate-200 shadow-sm self-start"> | |||||
| <h3 className="text-lg font-black text-[#1a2b4b] mb-6 flex items-center gap-2"> | |||||
| Owner Information | |||||
| <span className="w-1.5 h-1.5 rounded-full bg-[#2b458c]"></span> | |||||
| </h3> | |||||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-6"> | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-bold text-slate-400">Pan *</label> | |||||
| <input | |||||
| type="text" | |||||
| value={proposal.pan} | |||||
| onChange={e => setProposal({ ...proposal, pan: e.target.value })} | |||||
| className="w-full px-4 py-3.5 bg-white rounded-lg border border-slate-200 focus:ring-2 focus:ring-[#2b458c] font-medium text-[#1a2b4b] outline-none transition-all" | |||||
| /> | |||||
| </div> | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-bold text-slate-400">Name *</label> | |||||
| <input | |||||
| type="text" | |||||
| value={proposal.name} | |||||
| onChange={e => setProposal({ ...proposal, name: e.target.value })} | |||||
| className="w-full px-4 py-3.5 bg-white rounded-lg border border-slate-200 focus:ring-2 focus:ring-[#2b458c] font-medium text-[#1a2b4b] outline-none transition-all" | |||||
| /> | |||||
| </div> | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-bold text-slate-400">Date of Birth *</label> | |||||
| <input | |||||
| type="date" | |||||
| value={proposal.dob} | |||||
| onChange={e => setProposal({ ...proposal, dob: e.target.value })} | |||||
| className="w-full px-4 py-3.5 bg-white rounded-lg border border-slate-200 focus:ring-2 focus:ring-[#2b458c] font-medium text-[#1a2b4b] outline-none transition-all" | |||||
| /> | |||||
| </div> | |||||
| <div className="space-y-2"> | |||||
| <label className="text-xs font-bold text-slate-400">Phone Number *</label> | |||||
| <input | |||||
| type="tel" | |||||
| value={proposal.phone} | |||||
| onChange={e => setProposal({ ...proposal, phone: e.target.value })} | |||||
| className="w-full px-4 py-3.5 bg-white rounded-lg border border-slate-200 focus:ring-2 focus:ring-[#2b458c] font-medium text-[#1a2b4b] outline-none transition-all" | |||||
| /> | |||||
| </div> | |||||
| <div className="md:col-span-2 space-y-2"> | |||||
| <label className="text-xs font-bold text-slate-400">Email *</label> | |||||
| <input | |||||
| type="email" | |||||
| value={proposal.email} | |||||
| onChange={e => setProposal({ ...proposal, email: e.target.value })} | |||||
| className="w-full px-4 py-3.5 bg-white rounded-lg border border-slate-200 focus:ring-2 focus:ring-[#2b458c] font-medium text-[#1a2b4b] outline-none transition-all" | |||||
| /> | |||||
| </div> | |||||
| </div> | |||||
| <div className="mt-12 flex justify-end"> | |||||
| <button | |||||
| // onClick={onNext} | |||||
| className="bg-[#e31e24] hover:bg-[#c41a1f] text-white px-12 py-4 rounded-lg font-bold transition-all shadow-lg hover:-translate-y-0.5" | |||||
| > | |||||
| Proceed to KYC | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| {/* 3. Premium Breakup (Right) */} | |||||
| <div className="lg:w-1/3"> | |||||
| <div className="bg-white rounded-3xl p-7 border border-slate-200 shadow-sm space-y-8 lg:sticky lg:top-28"> | |||||
| <div className="flex items-center justify-between py-2 border-b border-slate-50"> | |||||
| <div className="flex flex-col"> | |||||
| <span className="text-[9px] font-black text-slate-400 uppercase tracking-widest mb-1">Insurance Partner</span> | |||||
| <p className="text-[11px] font-bold text-[#1a2b4b] max-w-[180px] leading-tight">{quote.insurerName}</p> | |||||
| </div> | |||||
| <img src={quote.insurerLogo} alt={quote.insurerName} className="h-10 w-auto object-contain" /> | |||||
| </div> | |||||
| <div className="space-y-4"> | |||||
| <PriceRow label="BASE PRICE" value={basePrice} /> | |||||
| <PriceRow label="CONVIENCE FEE" value={0} /> | |||||
| <PriceRow label="TAX" value={tax} /> | |||||
| <PriceRow label="PROCESSING FEE" value={0} /> | |||||
| <div className="pt-6 border-t-2 border-slate-100 flex justify-between items-center"> | |||||
| <span className="text-sm font-black text-[#1a2b4b] uppercase tracking-wider">Total</span> | |||||
| <span className="text-2xl font-black text-[#1a2b4b]">₹{total.toLocaleString()}</span> | |||||
| </div> | |||||
| </div> | |||||
| <div className="pt-4 text-center"> | |||||
| <p className="text-[10px] text-slate-400 leading-relaxed font-medium"> | |||||
| By clicking on 'Proceed', I agree to the <a href="#" className="text-[#e31e24] font-bold hover:underline">terms & conditions</a> | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| // Reusable Components | |||||
| const HeaderDetail = ({ label, value }: { label: string; value: string }) => ( | |||||
| <div className="flex flex-col gap-0.5"> | |||||
| <span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{label}</span> | |||||
| <span className="text-[11px] font-black text-white uppercase break-words leading-tight">{value || '-'}</span> | |||||
| </div> | |||||
| ); | |||||
| const PriceRow = ({ label, value }: { label: string; value: number }) => ( | |||||
| <div className="flex justify-between items-center"> | |||||
| <span className="text-[11px] font-bold text-slate-400 uppercase tracking-wide">{label}</span> | |||||
| <span className="text-sm font-black text-[#1a2b4b]">₹{value.toLocaleString()}</span> | |||||
| </div> | |||||
| ); | |||||
| const FormInput = ({ | |||||
| label, | |||||
| value, | |||||
| onChange, | |||||
| type = 'text', | |||||
| colSpan = 1 | |||||
| }: { | |||||
| label: string; | |||||
| value: string; | |||||
| onChange: (val: string) => void; | |||||
| type?: string; | |||||
| colSpan?: number; | |||||
| }) => ( | |||||
| <div className={colSpan > 1 ? 'md:col-span-2 space-y-2' : 'space-y-2'}> | |||||
| <label className="text-xs font-bold text-slate-400">{label}</label> | |||||
| <input | |||||
| type={type} | |||||
| value={value} | |||||
| onChange={e => onChange(e.target.value)} | |||||
| className="w-full px-4 py-3.5 bg-white rounded-lg border border-slate-200 focus:ring-2 focus:ring-[#2b458c] font-medium text-[#1a2b4b] outline-none transition-all" | |||||
| /> | |||||
| </div> | |||||
| ); | |||||
| export default ProposalPage; | |||||
| @ -0,0 +1,247 @@ | |||||
| import React, { useState } from 'react'; | |||||
| import QuoteCard from '@/Components/QuoteCard'; | |||||
| import { InsuranceQuote } from '@/types'; | |||||
| interface QuoteListPageProps { | |||||
| onBuyNow: (quote: InsuranceQuote) => void; | |||||
| } | |||||
| const QuoteListPage: React.FC<QuoteListPageProps> = ({ onBuyNow }) => { | |||||
| const [isEditingIDV, setIsEditingIDV] = useState(false); | |||||
| const [idvType, setIdvType] = useState<'lowest' | 'recommended' | 'custom'>('recommended'); | |||||
| const [customIDV, setCustomIDV] = useState(9507); | |||||
| // Tenure State | |||||
| const [isEditingTenure, setIsEditingTenure] = useState(false); | |||||
| const [tenure, setTenure] = useState<'1 Year' | '2 Year' | '3 Year'>('1 Year'); | |||||
| const minIDV = 6914; | |||||
| const maxIDV = 9507; | |||||
| const currentIDV = idvType === 'custom' ? customIDV : (idvType === 'lowest' ? minIDV : maxIDV); | |||||
| const dummyQuotes: InsuranceQuote[] = [ | |||||
| { | |||||
| id: '1', | |||||
| insurerName: 'Universal Sompo General Insurance Ltd', | |||||
| insurerLogo: 'https://companieslogo.com/img/orig/UNIVERSAL_SOMPO.NS-4b7b3d9c.png?t=1659939525', | |||||
| idvValue: currentIDV, | |||||
| premium: tenure === '1 Year' ? 2117 : tenure === '2 Year' ? 3850 : 5200, | |||||
| features: [ | |||||
| { label: 'PERSONAL_ACCIDENT_COVER', value: 1500000 }, | |||||
| { label: 'ROAD_SIDE_ASSISTANCE', value: 'Included' }, | |||||
| { label: 'ELECTRICAL_ACCESSORIES_COVER', value: 0 }, | |||||
| { label: 'ENGINE_COVER', value: 0 }, | |||||
| { label: 'KEY_COVER', value: 'No' }, | |||||
| { label: 'TYRE_COVER', value: 0 }, | |||||
| { label: 'PER_DAY_CASH', value: 0 }, | |||||
| { label: 'Convience Fee', value: 0 }, | |||||
| { label: 'Tax', value: 381 }, | |||||
| { label: 'Manual Review', value: 'False' }, | |||||
| { label: 'DEPRECIATION_COVER', value: 'No' }, | |||||
| { label: 'CONSUMABLES_COVER', value: 'No' }, | |||||
| { label: 'NON_ELECTRICAL_ACCESSORIES_COVER', value: 0 }, | |||||
| { label: 'EXTERNAL_CNG_OR_LPG_COVER', value: 'No' }, | |||||
| { label: 'PERSONAL_BAGGAGE_COVER', value: 0 }, | |||||
| { label: 'RETURN_TO_INVOICE', value: 0 }, | |||||
| { label: 'Base Price', value: 2117 }, | |||||
| { label: 'Processing Fee', value: 0 }, | |||||
| { label: 'Offer validity', value: 'P15D' } | |||||
| ] | |||||
| }, | |||||
| { | |||||
| id: '2', | |||||
| insurerName: 'Digit Two Wheeler Insurance', | |||||
| insurerLogo: 'https://cdn.freelogovectors.net/wp-content/uploads/2023/11/go-digit-logo-freelogovectors.net_.png', | |||||
| idvValue: 9100.00, | |||||
| premium: 942, | |||||
| features: [ | |||||
| { label: 'PERSONAL_ACCIDENT_COVER', value: 1500000 }, | |||||
| { label: 'ZERO_DEPRECIATION', value: 'Yes' }, | |||||
| { label: 'Tax', value: 142 }, | |||||
| { label: 'Base Price', value: 800 } | |||||
| ] | |||||
| } | |||||
| ]; | |||||
| return ( | |||||
| <div className="container mx-auto px-4 py-6 md:py-10"> | |||||
| <div className="flex flex-col lg:flex-row gap-8"> | |||||
| {/* Left Sidebar Filters */} | |||||
| <aside className="lg:w-1/3 xl:w-1/4"> | |||||
| <div className="sticky top-28 space-y-6"> | |||||
| <div className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm"> | |||||
| <h3 className="text-[#1a2b4b] text-xs font-black uppercase tracking-widest mb-6 flex items-center justify-between"> | |||||
| Modify Quote | |||||
| <span className="w-1.5 h-1.5 rounded-full bg-emerald-500"></span> | |||||
| </h3> | |||||
| <div className="space-y-6"> | |||||
| {/* Tenure */} | |||||
| <div className="p-3 bg-slate-50 rounded-xl border border-slate-100"> | |||||
| <div className="flex items-center justify-between mb-1"> | |||||
| <h4 className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Policy Tenure</h4> | |||||
| {!isEditingTenure && ( | |||||
| <button | |||||
| onClick={() => setIsEditingTenure(true)} | |||||
| className="text-[#e31e24] hover:scale-110 transition-transform" | |||||
| > | |||||
| <svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg> | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| {!isEditingTenure ? ( | |||||
| <p className="text-sm font-bold text-[#1a2b4b]">{tenure}</p> | |||||
| ) : ( | |||||
| <div className="flex flex-col gap-2 mt-2 animate-in fade-in slide-in-from-top-1"> | |||||
| {(['1 Year', '2 Year', '3 Year'] as const).map((t) => ( | |||||
| <button | |||||
| key={t} | |||||
| onClick={() => { | |||||
| setTenure(t); | |||||
| setIsEditingTenure(false); | |||||
| }} | |||||
| className={`w-full text-left px-3 py-2 rounded-lg text-xs font-bold transition-all border ${ | |||||
| tenure === t | |||||
| ? 'bg-white border-[#e31e24] text-[#e31e24] shadow-sm' | |||||
| : 'bg-slate-100 border-slate-200 text-[#1a2b4b] hover:bg-white' | |||||
| }`} | |||||
| > | |||||
| {t} | |||||
| </button> | |||||
| ))} | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| {/* IDV Section */} | |||||
| <div className="space-y-4"> | |||||
| <div className="flex items-center justify-between"> | |||||
| <h4 className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Cover Value (IDV):</h4> | |||||
| {!isEditingIDV && ( | |||||
| <button | |||||
| onClick={() => setIsEditingIDV(true)} | |||||
| className="text-[#e31e24] text-[10px] font-black uppercase hover:underline" | |||||
| > | |||||
| Edit | |||||
| </button> | |||||
| )} | |||||
| </div> | |||||
| <p className="text-[10px] text-slate-400 leading-snug"> | |||||
| The maximum value you will receive in case of total damage or theft. | |||||
| </p> | |||||
| <div className="pt-2"> | |||||
| <input | |||||
| type="range" | |||||
| min={minIDV} | |||||
| max={maxIDV} | |||||
| value={currentIDV} | |||||
| onChange={(e) => { setCustomIDV(parseInt(e.target.value)); setIdvType('custom'); }} | |||||
| className="w-full h-1.5 bg-slate-100 rounded-full appearance-none cursor-pointer accent-[#1a2b4b]" | |||||
| /> | |||||
| <div className="flex justify-between text-[10px] font-bold text-slate-400 mt-1"> | |||||
| <span>₹ {minIDV.toLocaleString()}</span> | |||||
| <span>₹ {maxIDV.toLocaleString()}</span> | |||||
| </div> | |||||
| </div> | |||||
| {!isEditingIDV ? ( | |||||
| <div className="bg-indigo-50 border border-indigo-100 rounded-xl p-3 text-center"> | |||||
| <p className="text-[10px] font-black text-indigo-700 uppercase"> | |||||
| {idvType === 'lowest' ? 'Cheapest' : idvType === 'recommended' ? 'Recommended' : 'Custom'} IDV | |||||
| </p> | |||||
| <p className="text-sm font-black text-indigo-900">₹ {currentIDV.toLocaleString()}</p> | |||||
| </div> | |||||
| ) : ( | |||||
| <div className="space-y-2 animate-in fade-in slide-in-from-top-1"> | |||||
| <button | |||||
| onClick={() => { setIdvType('lowest'); setIsEditingIDV(false); }} | |||||
| className={`w-full text-left p-3 rounded-xl border transition-all ${idvType === 'lowest' ? 'border-[#e31e24] bg-red-50' : 'border-slate-100 bg-white hover:bg-slate-50'}`} | |||||
| > | |||||
| <p className="text-xs font-bold text-[#1a2b4b]">Lowest ₹ {minIDV.toLocaleString()}</p> | |||||
| <p className="text-[9px] text-slate-500 font-medium">Cheapest premium</p> | |||||
| </button> | |||||
| <button | |||||
| onClick={() => { setIdvType('recommended'); setIsEditingIDV(false); }} | |||||
| className={`w-full text-left p-3 rounded-xl border transition-all ${idvType === 'recommended' ? 'border-[#e31e24] bg-red-50' : 'border-slate-100 bg-white hover:bg-slate-50'}`} | |||||
| > | |||||
| <p className="text-xs font-bold text-[#1a2b4b]">Recommended ₹ {maxIDV.toLocaleString()}</p> | |||||
| <p className="text-[9px] text-slate-500 font-medium">Based on your bike age</p> | |||||
| </button> | |||||
| <div className={`p-3 rounded-xl border transition-all ${idvType === 'custom' ? 'border-[#e31e24] bg-red-50' : 'border-slate-100 bg-white'}`}> | |||||
| <div className="flex items-center justify-between"> | |||||
| <p className="text-xs font-bold text-[#1a2b4b]">Set your own</p> | |||||
| <input | |||||
| type="number" | |||||
| min={minIDV} | |||||
| max={maxIDV} | |||||
| value={customIDV} | |||||
| onChange={(e) => { | |||||
| const val = parseInt(e.target.value); | |||||
| if (!isNaN(val)) { setCustomIDV(val); setIdvType('custom'); } | |||||
| }} | |||||
| className="w-20 px-2 py-1 text-xs font-bold border rounded bg-white outline-none focus:border-[#e31e24]" | |||||
| /> | |||||
| </div> | |||||
| <button | |||||
| onClick={() => setIsEditingIDV(false)} | |||||
| className="w-full mt-2 py-1 text-[9px] font-black text-[#e31e24] uppercase border border-[#e31e24]/20 rounded" | |||||
| > | |||||
| Apply Custom | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div className="bg-white rounded-2xl border border-slate-200 p-4 shadow-sm text-center"> | |||||
| <p className="text-[10px] text-slate-400 font-medium">Looking for specific add-ons?</p> | |||||
| <button className="text-[10px] font-black text-[#e31e24] uppercase mt-1 hover:underline">Customize Coverages</button> | |||||
| </div> | |||||
| </div> | |||||
| </aside> | |||||
| {/* Main Quotes List */} | |||||
| <main className="lg:w-2/3 xl:w-3/4"> | |||||
| <div className="mb-6 flex items-center justify-between"> | |||||
| <h2 className="text-[#1a2b4b] font-bold"> | |||||
| Found below <span className="text-[#e31e24]">Quotes</span> for your ride | |||||
| </h2> | |||||
| <div className="flex items-center gap-2"> | |||||
| <span className="text-[10px] font-black text-slate-400 uppercase">Sort by:</span> | |||||
| <select className="text-[11px] font-bold bg-transparent border-none focus:ring-0 cursor-pointer text-[#1a2b4b]"> | |||||
| <option>Lowest Premium</option> | |||||
| <option>Highest IDV</option> | |||||
| </select> | |||||
| </div> | |||||
| </div> | |||||
| <div className="space-y-4"> | |||||
| {dummyQuotes.map(q => <QuoteCard key={q.id} quote={q} onBuy={() => onBuyNow(q)} />)} | |||||
| </div> | |||||
| <div className="mt-12 text-center p-8 border-2 border-dashed border-slate-200 rounded-3xl"> | |||||
| <div className="inline-flex items-center gap-3 text-slate-400 text-sm font-medium"> | |||||
| <div className="flex gap-1"> | |||||
| <div className="w-1.5 h-1.5 rounded-full bg-slate-300 animate-bounce"></div> | |||||
| <div className="w-1.5 h-1.5 rounded-full bg-slate-300 animate-bounce [animation-delay:0.2s]"></div> | |||||
| <div className="w-1.5 h-1.5 rounded-full bg-slate-300 animate-bounce [animation-delay:0.4s]"></div> | |||||
| </div> | |||||
| Updating latest rates from insurers... | |||||
| </div> | |||||
| </div> | |||||
| </main> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| export default QuoteListPage; | |||||
| @ -0,0 +1,43 @@ | |||||
| import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; | |||||
| import BasicDetails from "../BasicDetails"; | |||||
| import QuoteListPage from "../Quotation/QuoteListPage"; | |||||
| import ProposalPage from "../KYCDetails/ProposalPage"; | |||||
| // import { | |||||
| // BasicDetails | |||||
| // } from "../Pages"; | |||||
| // Routes config | |||||
| const routes = [ | |||||
| { | |||||
| path: "/basic-details", | |||||
| element: <BasicDetails />, | |||||
| }, | |||||
| { | |||||
| path: "/quote-list", | |||||
| element: <QuoteListPage />, | |||||
| }, | |||||
| { | |||||
| path: "/kyc-details", | |||||
| element: <ProposalPage />, | |||||
| }, | |||||
| ]; | |||||
| const PageRoutes = () => { | |||||
| return ( | |||||
| <BrowserRouter> | |||||
| <Routes> | |||||
| <Route path="/" element={<Navigate to="/basic-details" replace />} /> | |||||
| {routes?.map(({ path, element }, index) => ( | |||||
| <Route | |||||
| // key={index} | |||||
| path={path} element={element} /> | |||||
| ))} | |||||
| </Routes> | |||||
| </BrowserRouter> | |||||
| ); | |||||
| }; | |||||
| export default PageRoutes; | |||||
| @ -0,0 +1,257 @@ | |||||
| import React, { useState } from 'react'; | |||||
| import { InsuranceQuote, ProposalData, VehicleInfo } from '@/types'; | |||||
| interface CheckoutPageProps { | |||||
| quote: InsuranceQuote; | |||||
| } | |||||
| const CheckoutPage: React.FC<CheckoutPageProps & { vehicle: VehicleInfo }> = ({ quote, vehicle }) => { | |||||
| const [isSubmitted, setIsSubmitted] = useState(false); | |||||
| const [isPaid, setIsPaid] = useState(false); | |||||
| const [proposal, setProposal] = useState<ProposalData>({ | |||||
| pan: 'AATPU7693N', | |||||
| name: 'Suketu Upadhyay', | |||||
| dob: '1984-07-03', | |||||
| phone: '9833604121', | |||||
| email: 'Suketu123_Upadhyay@hotmail.com', | |||||
| nomineeName: 'Vasudha Upadhyay', | |||||
| nomineeDob: '1986-06-03', | |||||
| nomineeRelationship: 'Spouse', | |||||
| previousTPPolicyNo: '', | |||||
| previousTPPolicyIssuer: '', | |||||
| appointeeName: '', | |||||
| appointeeRelationship: '', | |||||
| isHypothecated: 'No', | |||||
| bankName: '' | |||||
| }); | |||||
| const tax = Math.round(quote.premium * 0.18); | |||||
| const basePrice = quote.premium; | |||||
| const total = basePrice + tax; | |||||
| const handleSubmit = (e: React.FormEvent) => { | |||||
| e.preventDefault(); | |||||
| setIsSubmitted(true); | |||||
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |||||
| }; | |||||
| const handlePay = () => { | |||||
| // Simulating redirect to payment gateway and back | |||||
| setIsPaid(true); | |||||
| }; | |||||
| return ( | |||||
| <div className="relative min-h-screen"> | |||||
| <div className={`container mx-auto px-4 py-8 space-y-6 transition-all duration-500 ${isPaid ? 'blur-sm pointer-events-none' : ''} animate-in fade-in duration-700`}> | |||||
| {/* 1. High-Density Vehicle Header (Top) */} | |||||
| <div className="bg-white rounded-2xl border border-slate-200 shadow-sm overflow-hidden"> | |||||
| <div className="p-6 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4 gap-x-12 gap-y-4"> | |||||
| <HeaderItem label="MODEL:" value={vehicle.model} /> | |||||
| <HeaderItem label="MAKE:" value={vehicle.make} /> | |||||
| <HeaderItem label="FUEL_TYPE:" value={vehicle.fuelType} /> | |||||
| <HeaderItem label="VARIANT:" value={vehicle.variant} /> | |||||
| <HeaderItem label="REGISTERED_CITY:" value={vehicle.registeredCity} /> | |||||
| <HeaderItem label="REGISTERED_DATE:" value={vehicle.registeredDate} /> | |||||
| <HeaderItem label="CHASSIS_NUMBER:" value={vehicle.chassisNumber} /> | |||||
| <HeaderItem label="ENGINE_NUMBER:" value={vehicle.engineNumber} /> | |||||
| <HeaderItem label="PREVIOUS_POLICY_NUMBER:" value={vehicle.previousPolicyNumber} /> | |||||
| <HeaderItem label="PREVIOUS_INSURER:" value={vehicle.previousInsurer} /> | |||||
| <HeaderItem label="PREVIOUS_POLICY_END_DATE:" value={vehicle.previousPolicyEndDate} /> | |||||
| <HeaderItem label="VEHICLE REG NO:" value={vehicle.regNo} /> | |||||
| </div> | |||||
| </div> | |||||
| <div className="flex flex-col lg:flex-row gap-8"> | |||||
| {/* 2. Full Proposal Form (Left) */} | |||||
| <div className={`lg:w-2/3 bg-white rounded-3xl p-8 border border-slate-200 shadow-sm transition-opacity duration-500 ${isSubmitted ? 'opacity-80' : ''}`}> | |||||
| <div className="grid grid-cols-1 md:grid-cols-2 gap-x-8 gap-y-5"> | |||||
| <FormField label="Vehicle Reg No. *" value={vehicle.regNo} readOnly={true} /> | |||||
| <FormField label="Chassis number *" value={vehicle.chassisNumber} readOnly={true} /> | |||||
| <FormField label="Engine number *" value={vehicle.engineNumber} readOnly={true} /> | |||||
| <FormField label="Previous policy No. *" value={vehicle.previousPolicyNumber} readOnly={isSubmitted} /> | |||||
| <FormField label="Previous policy Issuer *" value={vehicle.previousInsurer} readOnly={isSubmitted} /> | |||||
| <FormField label="Previous TP policy Issuer" value={proposal.previousTPPolicyIssuer || 'Previous TP policy Issuer'} placeholder readOnly={isSubmitted} /> | |||||
| <FormField label="Previous TP policy No." value={proposal.previousTPPolicyNo || 'Previous TP policy No.'} placeholder readOnly={isSubmitted} /> | |||||
| <FormField label="Nominee Name *" value={proposal.nomineeName} readOnly={isSubmitted} /> | |||||
| <FormField label="Nominee DOB *" value={proposal.nomineeDob} type="date" readOnly={isSubmitted} /> | |||||
| <FormField label="Nominee Relationship *" value={proposal.nomineeRelationship} isSelect readOnly={isSubmitted} /> | |||||
| <FormField label="Appointee name" value={proposal.appointeeName || 'Appointee name'} placeholder readOnly={isSubmitted} /> | |||||
| <FormField label="Appointee relationship with nominee" value={proposal.appointeeRelationship || 'Appointee relationship with nominee'} placeholder readOnly={isSubmitted} /> | |||||
| <FormField label="Hypothecation/Loan flag" value={proposal.isHypothecated} isSelect readOnly={isSubmitted} /> | |||||
| <FormField label="Bank name if hypothecated" value={proposal.bankName || 'Bank name if hypothecated'} placeholder readOnly={isSubmitted} /> | |||||
| </div> | |||||
| <div className="mt-12 flex justify-center"> | |||||
| <button | |||||
| onClick={handleSubmit} | |||||
| disabled={isSubmitted} | |||||
| className={`px-16 py-3 rounded-lg font-bold transition-all shadow-lg uppercase tracking-wide ${isSubmitted | |||||
| ? 'bg-slate-400 cursor-not-allowed opacity-60 text-white' | |||||
| : 'bg-[#2b458c]/80 hover:bg-[#1e3a8a] text-white' | |||||
| }`} | |||||
| > | |||||
| {isSubmitted ? 'Submitted' : 'Submit'} | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| {/* 3. Summary Card (Right) */} | |||||
| <div className="lg:w-1/3"> | |||||
| <div className="bg-white rounded-3xl p-8 border border-slate-200 shadow-sm space-y-8 lg:sticky lg:top-28"> | |||||
| <div className="flex items-center gap-4 py-2 border-b border-slate-50"> | |||||
| <img src={quote.insurerLogo} alt={quote.insurerName} className="h-12 w-12 object-contain" /> | |||||
| <div className="flex flex-col"> | |||||
| <span className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-0.5">Insurance Partner</span> | |||||
| <p className="text-[11px] font-bold text-[#1a2b4b] leading-tight">{quote.insurerName}</p> | |||||
| </div> | |||||
| </div> | |||||
| <div className="space-y-4"> | |||||
| <PriceDetail label="BASE PRICE" value={basePrice} /> | |||||
| <PriceDetail label="CONVIENCE FEE" value={0} /> | |||||
| <PriceDetail label="TAX" value={tax} /> | |||||
| <PriceDetail label="PROCESSING FEE" value={0} /> | |||||
| <div className="pt-6 border-t border-slate-100 flex justify-between items-center"> | |||||
| <span className="text-[13px] font-bold text-[#1a2b4b]">Total</span> | |||||
| <span className="text-xl font-black text-[#1a2b4b]">₹{total.toLocaleString()}</span> | |||||
| </div> | |||||
| </div> | |||||
| {isSubmitted && ( | |||||
| <div className="pt-4 animate-in slide-in-from-bottom-4 duration-500"> | |||||
| <button | |||||
| onClick={handlePay} | |||||
| className="w-full bg-[#1e3a8a] hover:bg-[#162a63] text-white py-4 rounded-xl font-black text-sm shadow-xl transition-all hover:-translate-y-1" | |||||
| > | |||||
| Pay securely | |||||
| </button> | |||||
| </div> | |||||
| )} | |||||
| <div className="pt-2 text-center"> | |||||
| <p className="text-[10px] text-slate-400 leading-relaxed font-medium"> | |||||
| By clicking on '{isSubmitted ? 'Pay now' : 'Submit'}', I agree to the <a href="#" className="text-blue-500 font-bold hover:underline">terms & conditions</a> | |||||
| </p> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| {/* 4. Success Modal Overlay */} | |||||
| {isPaid && ( | |||||
| <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 backdrop-blur-[2px] animate-in fade-in duration-300"> | |||||
| <div className="bg-white rounded-[2rem] shadow-2xl w-full max-w-lg p-10 text-center space-y-8 animate-in zoom-in-95 duration-500"> | |||||
| <div className="space-y-2"> | |||||
| <h2 className="text-xl md:text-2xl font-bold text-[#1a2b4b]"> | |||||
| Your policy purchased successfully ! | |||||
| </h2> | |||||
| </div> | |||||
| <div className="flex justify-center"> | |||||
| <div className="relative"> | |||||
| <div className="w-24 h-24 rounded-full bg-emerald-100 flex items-center justify-center animate-pulse"> | |||||
| <div className="w-20 h-20 rounded-full bg-[#22c55e] flex items-center justify-center shadow-lg shadow-emerald-200"> | |||||
| <svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="4" d="M5 13l4 4L19 7" /> | |||||
| </svg> | |||||
| </div> | |||||
| </div> | |||||
| {/* Decorative particles */} | |||||
| <div className="absolute -top-2 -right-2 w-3 h-3 bg-emerald-400 rounded-full animate-ping"></div> | |||||
| <div className="absolute top-1/2 -left-6 w-2 h-2 bg-emerald-300 rounded-full animate-ping delay-75"></div> | |||||
| </div> | |||||
| </div> | |||||
| <div className="space-y-6"> | |||||
| <p className="text-[#1a2b4b] font-bold text-sm"> | |||||
| Policy No: <span className="text-slate-500">USGI/WEBAG/1126181/00/000</span> | |||||
| </p> | |||||
| <div className="space-y-4"> | |||||
| <button className="w-full bg-[#2b458c] hover:bg-[#1e3a8a] text-white py-3.5 rounded-xl font-bold text-sm shadow-xl transition-all hover:scale-[1.02]"> | |||||
| Download Policy | |||||
| </button> | |||||
| <button | |||||
| onClick={() => window.location.reload()} | |||||
| className="block w-full text-blue-500 font-bold text-sm hover:underline" | |||||
| > | |||||
| Back to Homepage | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| const HeaderItem = ({ label, value }: { label: string; value: string }) => ( | |||||
| <div className="flex flex-col gap-0.5"> | |||||
| <span className="text-[10px] font-medium text-slate-400">{label}</span> | |||||
| <span className="text-[11px] font-bold text-[#1a2b4b] uppercase break-words"> | |||||
| {value || '-'} | |||||
| </span> | |||||
| </div> | |||||
| ); | |||||
| const FormField = ({ | |||||
| label, | |||||
| value, | |||||
| readOnly, | |||||
| placeholder, | |||||
| type = "text", | |||||
| isSelect = false | |||||
| }: { | |||||
| label: string; | |||||
| value?: string; | |||||
| readOnly?: boolean; | |||||
| placeholder?: boolean; | |||||
| type?: string; | |||||
| isSelect?: boolean; | |||||
| }) => ( | |||||
| <div className="space-y-1.5"> | |||||
| <label className={`text-[11px] font-medium transition-colors ${readOnly ? 'text-slate-300' : 'text-[#1a2b4b]/60'}`}> | |||||
| {label} | |||||
| </label> | |||||
| <div className="relative"> | |||||
| <input | |||||
| type={type} | |||||
| defaultValue={value} | |||||
| readOnly={readOnly} | |||||
| className={`w-full px-4 py-2.5 rounded-lg border outline-none text-[12px] font-medium transition-all ${readOnly | |||||
| ? 'bg-slate-50 border-slate-100 text-slate-400 cursor-not-allowed' | |||||
| : 'bg-white border-slate-200 text-[#1a2b4b] focus:border-[#2b458c]' | |||||
| } ${placeholder && !readOnly ? 'text-slate-300 italic' : ''} ${isSelect && !readOnly ? 'cursor-pointer' : '' | |||||
| }`} | |||||
| /> | |||||
| {isSelect && !readOnly && ( | |||||
| <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none opacity-20"> | |||||
| <svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" /></svg> | |||||
| </div> | |||||
| )} | |||||
| {type === 'date' && !readOnly && ( | |||||
| <div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none opacity-20"> | |||||
| <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| const PriceDetail = ({ label, value }: { label: string; value: number }) => ( | |||||
| <div className="flex justify-between items-center"> | |||||
| <span className="text-[11px] font-bold text-slate-400 tracking-tight">{label}</span> | |||||
| <span className="text-[12px] font-bold text-[#1a2b4b]">₹{value.toLocaleString()}</span> | |||||
| </div> | |||||
| ); | |||||
| export default CheckoutPage; | |||||
| @ -0,0 +1,27 @@ | |||||
| import { GoogleGenAI } from "@google/genai"; | |||||
| // Standard service pattern to keep AI logic separate | |||||
| export const getInsuranceInsight = async (formData: any) => { | |||||
| const ai = new GoogleGenAI({ apiKey: process.env.API_KEY || '' }); | |||||
| const prompt = ` | |||||
| Based on the following two-wheeler insurance details, provide a short 1-sentence helpful advice to the user. | |||||
| Details: | |||||
| - Cover: ${formData.coverType} | |||||
| - Claims made: ${formData.claimStatus} | |||||
| - NCB: ${formData.ncb}% | |||||
| Keep it professional, helpful, and encouraging. | |||||
| `; | |||||
| try { | |||||
| const response = await ai.models.generateContent({ | |||||
| model: 'gemini-3-flash-preview', | |||||
| contents: prompt, | |||||
| }); | |||||
| return response.text; | |||||
| } catch (error) { | |||||
| console.error("Gemini Error:", error); | |||||
| return null; | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,29 @@ | |||||
| { | |||||
| "compilerOptions": { | |||||
| "target": "ES2022", | |||||
| "experimentalDecorators": true, | |||||
| "useDefineForClassFields": false, | |||||
| "module": "ESNext", | |||||
| "lib": [ | |||||
| "ES2022", | |||||
| "DOM", | |||||
| "DOM.Iterable" | |||||
| ], | |||||
| "skipLibCheck": true, | |||||
| "types": [ | |||||
| "node" | |||||
| ], | |||||
| "moduleResolution": "bundler", | |||||
| "isolatedModules": true, | |||||
| "moduleDetection": "force", | |||||
| "allowJs": true, | |||||
| "jsx": "react-jsx", | |||||
| "paths": { | |||||
| "@/*": [ | |||||
| "./*" | |||||
| ] | |||||
| }, | |||||
| "allowImportingTsExtensions": true, | |||||
| "noEmit": true | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,76 @@ | |||||
| export interface FormData { | |||||
| registrationNumber: string; | |||||
| previousPolicyNumber: string; | |||||
| expiryDate: string; | |||||
| coverType: 'comprehensive' | 'third-party' | 'own-damage'; | |||||
| claimStatus: 'yes' | 'no'; | |||||
| ncb: number; | |||||
| } | |||||
| export interface Benefit { | |||||
| title: string; | |||||
| description: string; | |||||
| icon: string; | |||||
| } | |||||
| export interface QuoteFeature { | |||||
| label: string; | |||||
| value: string | number | boolean; | |||||
| } | |||||
| // export interface InsuranceQuote { | |||||
| // id?: string; | |||||
| // insurerName: string; | |||||
| // insurerLogo: string; | |||||
| // premium: number; | |||||
| // idvValue?: number; | |||||
| // features?: string[]; | |||||
| // } | |||||
| export interface Feature { | |||||
| label: string; | |||||
| value: string | number; | |||||
| } | |||||
| export interface InsuranceQuote { | |||||
| id: string; | |||||
| insurerName: string; | |||||
| insurerLogo: string; | |||||
| idvValue: number; | |||||
| premium: number; | |||||
| features: Feature[] | string[]; | |||||
| } | |||||
| export interface VehicleInfo { | |||||
| model: string; | |||||
| make: string; | |||||
| fuelType: string; | |||||
| variant: string; | |||||
| registeredCity: string; | |||||
| registeredDate: string; | |||||
| chassisNumber: string; | |||||
| engineNumber: string; | |||||
| previousPolicyNumber: string; | |||||
| previousInsurer: string; | |||||
| previousPolicyEndDate: string; | |||||
| regNo: string; | |||||
| } | |||||
| export interface ProposalData { | |||||
| pan: string; | |||||
| name: string; | |||||
| dob: string; | |||||
| phone: string; | |||||
| email: string; | |||||
| // Final Review Fields | |||||
| nomineeName?: string; | |||||
| nomineeDob?: string; | |||||
| nomineeRelationship?: string; | |||||
| appointeeName?: string; | |||||
| appointeeRelationship?: string; | |||||
| isHypothecated?: string; | |||||
| bankName?: string; | |||||
| previousTPPolicyIssuer?: string; | |||||
| previousTPPolicyNo?: string; | |||||
| } | |||||
| @ -0,0 +1,23 @@ | |||||
| import path from 'path'; | |||||
| import { defineConfig, loadEnv } from 'vite'; | |||||
| import react from '@vitejs/plugin-react'; | |||||
| export default defineConfig(({ mode }) => { | |||||
| const env = loadEnv(mode, '.', ''); | |||||
| return { | |||||
| server: { | |||||
| port: 3000, | |||||
| host: '0.0.0.0', | |||||
| }, | |||||
| plugins: [react()], | |||||
| define: { | |||||
| 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY), | |||||
| 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY) | |||||
| }, | |||||
| resolve: { | |||||
| alias: { | |||||
| '@': path.resolve(__dirname, '.'), | |||||
| } | |||||
| } | |||||
| }; | |||||
| }); | |||||
Powered by TurnKey Linux.