Created
February 7, 2026 05:32
-
-
Save ashwath007/d019d45c063f265c40ec40e2261a6897 to your computer and use it in GitHub Desktop.
Competitor New UI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useMemo } from 'react'; | |
| import { | |
| Zap, | |
| Globe, | |
| Facebook, | |
| Youtube, | |
| ExternalLink, | |
| Target, | |
| MessageSquare, | |
| BadgePercent, | |
| Languages, | |
| ChevronDown, | |
| Calendar, | |
| MapPin, | |
| Filter, | |
| PieChart, | |
| BarChart3, | |
| TrendingUp, | |
| ArrowUpRight, | |
| Eye, | |
| Activity, | |
| Layers, | |
| Search, | |
| CheckSquare, | |
| Square, | |
| MousePointer2, | |
| RefreshCcw, | |
| UserCircle, | |
| Hash, | |
| Box, | |
| ChevronRight, | |
| BarChart as BarChartIcon, | |
| ArrowRight, | |
| Image as ImageIcon | |
| } from 'lucide-react'; | |
| import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Cell, Legend, ComposedChart, Line } from 'recharts'; | |
| // --- Global Data Structures --- | |
| const oemSpendData = [ | |
| { | |
| id: 'bajaj', | |
| name: 'Bajaj Auto', | |
| spend: 4.2, | |
| metaSpend: 2.8, | |
| googleSpend: 1.4, | |
| sov: 48, | |
| brands: 12, | |
| topBrand: 'Chetak', | |
| subBrands: [ | |
| { name: 'Chetak (EV)', brandKey: 'chetak', spend: 1.8, meta: 1.2, google: 0.6, sov: 22, volume: 450 }, | |
| { name: 'Pulsar Series', brandKey: 'chetak', spend: 1.4, meta: 0.9, google: 0.5, sov: 15, volume: 380 }, | |
| { name: 'Dominar', brandKey: 'chetak', spend: 0.6, meta: 0.4, google: 0.2, sov: 7, volume: 120 }, | |
| { name: 'Platina/CT', brandKey: 'chetak', spend: 0.4, meta: 0.3, google: 0.1, sov: 4, volume: 90 } | |
| ] | |
| }, | |
| { | |
| id: 'tvs', | |
| name: 'TVS Motor', | |
| spend: 2.8, | |
| metaSpend: 1.6, | |
| googleSpend: 1.2, | |
| sov: 22, | |
| brands: 8, | |
| topBrand: 'iQube', | |
| subBrands: [ | |
| { name: 'iQube (EV)', brandKey: 'iqube', spend: 0.9, meta: 0.5, google: 0.4, sov: 10, volume: 310 }, | |
| { name: 'Jupiter', brandKey: 'iqube', spend: 1.1, meta: 0.7, google: 0.4, sov: 7, volume: 280 }, | |
| { name: 'Apache', brandKey: 'iqube', spend: 0.5, meta: 0.3, google: 0.2, sov: 3, volume: 140 }, | |
| { name: 'NTORQ', brandKey: 'iqube', spend: 0.3, meta: 0.1, google: 0.2, sov: 2, volume: 110 } | |
| ] | |
| }, | |
| { | |
| id: 'ather', | |
| name: 'Ather Energy', | |
| spend: 1.5, | |
| metaSpend: 0.9, | |
| googleSpend: 0.6, | |
| sov: 18, | |
| brands: 3, | |
| topBrand: '450X', | |
| subBrands: [ | |
| { name: '450X', brandKey: 'ather', spend: 0.8, meta: 0.5, google: 0.3, sov: 10, volume: 190 }, | |
| { name: '450S', brandKey: 'ather', spend: 0.4, meta: 0.2, google: 0.2, sov: 5, volume: 90 }, | |
| { name: 'Rizta', brandKey: 'ather', spend: 0.3, meta: 0.2, google: 0.1, sov: 3, volume: 70 } | |
| ] | |
| }, | |
| ]; | |
| const segmentationData = [ | |
| { segment: 'Premium Motorcycle', meta: 1.2, google: 0.9, volume: 450 }, | |
| { segment: 'Economy Motorcycle', meta: 0.8, google: 0.6, volume: 820 }, | |
| { segment: 'Executive Motorcycle', meta: 1.0, google: 0.8, volume: 610 }, | |
| { segment: 'Premium Scooter', meta: 0.5, google: 0.4, volume: 210 }, | |
| { segment: 'Economy Scooter', meta: 0.7, google: 0.4, volume: 540 }, | |
| { segment: 'Executive Scooter', meta: 0.4, google: 0.3, volume: 320 }, | |
| { segment: 'EV Motorcycle', meta: 0.8, google: 0.4, volume: 290 }, | |
| { segment: 'EV Scooter', meta: 2.0, google: 1.2, volume: 1150 }, | |
| ]; | |
| const brandComparisonData = { | |
| chetak: ['iqube', 'ather'], | |
| iqube: ['chetak', 'ather'], | |
| ather: ['chetak', 'iqube'] | |
| }; | |
| const brandsData = { | |
| chetak: { | |
| name: 'Chetak', | |
| ratio: '70/30', | |
| totalSpend: '₹1.63 Cr', | |
| metaSpend: '₹1.14 Cr', | |
| googleSpend: '₹49.0 L', | |
| metaValue: 42, | |
| googleValue: 20, | |
| sov: 45, | |
| campaigns: [ | |
| { | |
| id: 'c1', | |
| url: 'https://www.chetak.com/enquire', | |
| cost: '₹11.2L', | |
| tags: ['Performance', 'Lead Gen'], | |
| type: 'conversion', | |
| images: [ | |
| 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?q=80&w=600&auto=format&fit=crop', | |
| 'https://images.unsplash.com/photo-1614165933833-315930353406?q=80&w=200&auto=format&fit=crop', | |
| 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?q=80&w=200&auto=format&fit=crop' | |
| ], | |
| efficacy: { ctr: '1.2%', cpm: '₹140', status: 'above' }, | |
| funnel: { traffic: '160k', bounce: '22%', conversion: '4.2%' }, | |
| strategy: 'Direct Lead Generation focus with sticky enquiry form.', | |
| offers: ['Special financing available', 'Extended warranty'], | |
| languages: 'English, Hindi, Marathi' | |
| }, | |
| { | |
| id: 'c2', | |
| url: 'https://www.chetak.com/experience', | |
| cost: '₹3.2L', | |
| tags: ['Brand', 'Awareness'], | |
| type: 'awareness', | |
| images: [ | |
| 'https://images.unsplash.com/photo-1620939511596-7377519a79c9?q=80&w=600&auto=format&fit=crop', | |
| 'https://images.unsplash.com/photo-1609630875171-b1321377ee53?q=80&w=200&auto=format&fit=crop' | |
| ], | |
| efficacy: { ctr: '0.8%', cpm: '₹95', status: 'below' }, | |
| funnel: { traffic: '450k', bounce: '45%', conversion: '0.8%' }, | |
| strategy: 'Lifestyle-led brand campaign focusing on EV transition.', | |
| offers: ['Festive Experience Passes'], | |
| languages: 'English, Hindi' | |
| } | |
| ] | |
| }, | |
| iqube: { | |
| name: 'TVS iQube', | |
| ratio: '60/40', | |
| totalSpend: '₹48.9L', | |
| metaSpend: '₹29.3 L', | |
| googleSpend: '₹19.6 L', | |
| metaValue: 12, | |
| googleValue: 10, | |
| sov: 25, | |
| campaigns: [ | |
| { | |
| id: 'i1', | |
| url: 'https://www.tvsmotor.com/book-ride', | |
| cost: '₹4.89L', | |
| tags: ['Performance', 'Lead Gen'], | |
| type: 'conversion', | |
| images: [ | |
| 'https://images.unsplash.com/photo-1609630875171-b1321377ee53?q=80&w=600&auto=format&fit=crop', | |
| 'https://images.unsplash.com/photo-1558981403-c5f9899a28bc?q=80&w=200&auto=format&fit=crop', | |
| 'https://images.unsplash.com/photo-1614165933833-315930353406?q=80&w=200&auto=format&fit=crop' | |
| ], | |
| efficacy: { ctr: '1.5%', cpm: '₹125', status: 'above' }, | |
| funnel: { traffic: '33k', bounce: '32%', conversion: '5.1%' }, | |
| strategy: 'Test-ride drive with geographic convenience focus.', | |
| offers: ['Festive Cashback: ₹5,000'], | |
| languages: 'English, Malayalam' | |
| } | |
| ] | |
| }, | |
| ather: { | |
| name: 'Ather 450X', | |
| ratio: '55/45', | |
| totalSpend: '₹15.3L', | |
| metaSpend: '₹8.4 L', | |
| googleSpend: '₹6.9 L', | |
| metaValue: 8, | |
| googleValue: 8, | |
| sov: 30, | |
| campaigns: [ | |
| { | |
| id: 'a1', | |
| url: 'https://www.atherenergy.com/flexipay', | |
| cost: '₹5.4L', | |
| tags: ['Mid-Funnel', 'Financing'], | |
| type: 'consideration', | |
| images: [ | |
| 'https://images.unsplash.com/photo-1556742049-0cfed4f7a07d?q=80&w=600&auto=format&fit=crop', | |
| 'https://images.unsplash.com/photo-1594144408214-496ff9430df4?q=80&w=200&auto=format&fit=crop' | |
| ], | |
| efficacy: { ctr: '2.1%', cpm: '₹180', status: 'below' }, | |
| funnel: { traffic: '8.7k', bounce: '64%', conversion: '1.8%' }, | |
| strategy: 'EMI calculator to lower high premium entry barrier.', | |
| offers: ['Zero Processing Fee'], | |
| languages: 'English, Hindi' | |
| } | |
| ] | |
| } | |
| }; | |
| const funnelStages = [ | |
| { id: 'awareness', label: 'Awareness', description: 'Brand Building & Top-Funnel Reach', color: 'bg-emerald-500', icon: Globe }, | |
| { id: 'consideration', label: 'Consideration', description: 'Product Specs & Comparison', color: 'bg-indigo-500', icon: Layers }, | |
| { id: 'conversion', label: 'Conversion', description: 'Lead Generation & Sales Intent', color: 'bg-blue-600', icon: Target } | |
| ]; | |
| const App = () => { | |
| const [viewMode, setViewMode] = useState('oem'); | |
| const [primaryBrand, setPrimaryBrand] = useState('chetak'); | |
| const [expandedOems, setExpandedOems] = useState([]); | |
| const [filters, setFilters] = useState({ | |
| dateRange: 'October 2024', | |
| states: ['All States'], | |
| platform: ['Meta', 'Google', 'YouTube'], | |
| type: 'all' | |
| }); | |
| const togglePlatform = (p) => { | |
| setFilters(prev => ({ | |
| ...prev, | |
| platform: prev.platform.includes(p) ? prev.platform.filter(item => item !== p) : [...prev.platform, p] | |
| })); | |
| }; | |
| const toggleOemExpansion = (id) => { | |
| setExpandedOems(prev => | |
| prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] | |
| ); | |
| }; | |
| const handleBrandClick = (brandKey) => { | |
| setPrimaryBrand(brandKey); | |
| setViewMode('competition'); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| }; | |
| const competitors = useMemo(() => brandComparisonData[primaryBrand] || [], [primaryBrand]); | |
| const activeBrandsList = useMemo(() => [primaryBrand, ...competitors], [primaryBrand, competitors]); | |
| const competitionChartData = useMemo(() => { | |
| return [brandsData[primaryBrand], ...competitors.map(c => brandsData[c])].map(brand => ({ | |
| name: brand.name, | |
| metaSOE: brand.metaValue, | |
| googleSOE: brand.googleValue, | |
| sov: brand.sov | |
| })); | |
| }, [primaryBrand, competitors]); | |
| return ( | |
| <div className="flex flex-col h-screen bg-[#FDFDFD] font-sans text-slate-900 overflow-hidden"> | |
| {/* GLOBAL HEADER & FILTER BAR */} | |
| <header className="bg-white border-b border-slate-200 sticky top-0 z-50 shadow-sm"> | |
| <div className="px-8 py-4 flex justify-between items-center border-b border-slate-100"> | |
| <div className="flex items-center gap-6"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-8 h-8 bg-black rounded flex items-center justify-center text-white font-bold shadow-lg">H</div> | |
| <span className="text-lg font-black tracking-tighter uppercase">HAWKY</span> | |
| </div> | |
| <div className="h-6 w-px bg-slate-200" /> | |
| <div className="flex bg-slate-100 p-1 rounded-lg"> | |
| <button onClick={() => setViewMode('oem')} className={`px-4 py-1.5 flex items-center gap-2 text-xs font-bold rounded-md transition-all ${viewMode === 'oem' ? 'bg-white shadow-sm text-black' : 'text-slate-500 hover:text-slate-700'}`}> | |
| <BarChart3 size={14} /> OEM Level | |
| </button> | |
| <button onClick={() => setViewMode('competition')} className={`px-4 py-1.5 flex items-center gap-2 text-xs font-bold rounded-md transition-all ${viewMode === 'competition' ? 'bg-white shadow-sm text-black' : 'text-slate-500 hover:text-slate-700'}`}> | |
| <Target size={14} /> Competition | |
| </button> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-4"> | |
| <span className="text-[10px] font-black text-slate-400 uppercase">TVS Digital Team Sync</span> | |
| <button className="h-9 px-4 bg-black text-white text-[10px] font-black uppercase rounded-md shadow-lg hover:bg-slate-800 transition-all">Export Dashboard</button> | |
| </div> | |
| </div> | |
| <div className="px-8 py-3 bg-white flex items-center gap-8 overflow-x-auto no-scrollbar border-b border-slate-100 shadow-sm"> | |
| <FilterGroup label="Selected Period"> | |
| <div className="flex items-center gap-2 px-3 py-1.5 border border-slate-200 rounded-md bg-slate-50 shadow-sm"> | |
| <Calendar size={14} className="text-slate-400" /> | |
| <select className="bg-transparent text-xs font-bold outline-none cursor-pointer" value={filters.dateRange} onChange={(e) => setFilters({...filters, dateRange: e.target.value})}> | |
| <option>October 2024</option> | |
| <option>September 2024</option> | |
| <option>Diwali Week</option> | |
| <option>Navratri (Festive)</option> | |
| </select> | |
| </div> | |
| </FilterGroup> | |
| <FilterGroup label="State Market"> | |
| <div className="flex items-center gap-2 px-3 py-1.5 border border-slate-200 rounded-md bg-slate-50 shadow-sm"> | |
| <MapPin size={14} className="text-slate-400" /> | |
| <span className="text-xs font-bold text-blue-600">Kerala + 2 Others</span> | |
| <ChevronDown size={12} className="text-slate-300" /> | |
| </div> | |
| </FilterGroup> | |
| <FilterGroup label="Objective Type"> | |
| <div className="flex bg-slate-100 p-1 rounded-lg"> | |
| {['all', 'perf', 'mid', 'brand'].map(t => ( | |
| <button key={t} onClick={() => setFilters({...filters, type: t})} className={`px-3 py-1 text-[10px] font-bold rounded-md transition-all ${filters.type === t ? 'bg-white shadow-sm text-black' : 'text-slate-400 hover:text-slate-600'}`}> | |
| {t.toUpperCase()} | |
| </button> | |
| ))} | |
| </div> | |
| </FilterGroup> | |
| <FilterGroup label="Platform Analysis"> | |
| <div className="flex items-center gap-4 py-1.5"> | |
| {['Meta', 'Google', 'YouTube'].map(p => ( | |
| <div key={p} onClick={() => togglePlatform(p)} className="flex items-center gap-2 cursor-pointer group"> | |
| <div className={`w-4 h-4 rounded border transition-all flex items-center justify-center ${filters.platform.includes(p) ? 'bg-black border-black shadow-sm' : 'bg-white border-slate-300'}`}> | |
| {filters.platform.includes(p) && <div className="w-1.5 h-1.5 bg-white rounded-full" />} | |
| </div> | |
| <span className={`text-[11px] font-bold ${filters.platform.includes(p) ? 'text-black' : 'text-slate-400'}`}>{p}</span> | |
| </div> | |
| ))} | |
| </div> | |
| </FilterGroup> | |
| {viewMode === 'competition' && ( | |
| <div className="ml-auto flex flex-col gap-1 min-w-max"> | |
| <label className="text-[9px] font-bold text-slate-400 uppercase tracking-wider text-right">Switch Analysis Brand</label> | |
| <select className="bg-black text-white rounded-md px-3 py-1.5 text-xs font-bold outline-none shadow-lg cursor-pointer" value={primaryBrand} onChange={(e) => setPrimaryBrand(e.target.value)}> | |
| <option value="chetak">Chetak</option> | |
| <option value="iqube">TVS iQube</option> | |
| <option value="ather">Ather 450X</option> | |
| </select> | |
| </div> | |
| )} | |
| </div> | |
| </header> | |
| {/* DASHBOARD BODY */} | |
| <main className="flex-1 overflow-y-auto p-8 space-y-16 bg-[#FBFBFC]"> | |
| {viewMode === 'oem' ? ( | |
| /* --- OEM PORTFOLIO VIEW --- */ | |
| <div className="space-y-12 max-w-7xl mx-auto animate-in fade-in slide-in-from-bottom-4 duration-500"> | |
| {/* TOP METRICS TILES */} | |
| <section className="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm group hover:border-black transition-all"> | |
| <div className="flex justify-between items-start mb-4"> | |
| <div className="p-2 rounded-lg bg-slate-50 text-slate-400"> | |
| <PieChart size={18} /> | |
| </div> | |
| <ArrowUpRight size={14} className="text-slate-300" /> | |
| </div> | |
| <p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Estimated Market Spend</p> | |
| <h4 className="text-2xl font-black text-slate-900 mb-2 uppercase tracking-tighter">₹12.4 Cr</h4> | |
| <div className="flex gap-4 pt-3 border-t border-slate-50"> | |
| <div className="flex flex-col"> | |
| <span className="text-[8px] font-black text-slate-400 uppercase">Meta Spend</span> | |
| <span className="text-xs font-black text-blue-600">₹7.8 Cr</span> | |
| </div> | |
| <div className="flex flex-col"> | |
| <span className="text-[8px] font-black text-slate-400 uppercase">Google Spend</span> | |
| <span className="text-xs font-black text-indigo-600">₹4.6 Cr</span> | |
| </div> | |
| </div> | |
| </div> | |
| <KPICard title="Market Comparison IDs" value={activeBrandsList.length.toString()} sub={activeBrandsList.map(b => brandsData[b]?.name || b).join(', ')} icon={Target} color="blue" /> | |
| <KPICard title="Analysis Date Range" value={filters.dateRange} sub="Selected Month Reporting" icon={Calendar} color="emerald" /> | |
| </section> | |
| {/* BRAND SEGMENTATION DISTRIBUTION */} | |
| <section className="bg-white rounded-2xl border border-slate-200 overflow-hidden shadow-sm"> | |
| <div className="p-6 border-b border-slate-100 flex justify-between items-center bg-white"> | |
| <div> | |
| <h3 className="font-black text-sm uppercase tracking-tighter text-slate-900">Category Spend Mix</h3> | |
| <p className="text-[10px] text-slate-400 font-bold uppercase mt-1">Cross-platform distribution by vehicle segment</p> | |
| </div> | |
| <div className="flex gap-4"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2.5 h-2.5 bg-blue-600 rounded-full" /> | |
| <span className="text-[9px] font-black text-slate-400 uppercase tracking-tight">Meta</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2.5 h-2.5 bg-slate-300 rounded-full" /> | |
| <span className="text-[9px] font-black text-slate-400 uppercase tracking-tight">Google</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2.5 h-2.5 bg-indigo-500 rounded-full" /> | |
| <span className="text-[9px] font-black text-slate-400 uppercase tracking-tight">Ad Vol</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-4 gap-0 divide-x divide-slate-100"> | |
| <div className="lg:col-span-3 p-6"> | |
| <div className="h-[400px] w-full"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <ComposedChart data={segmentationData} margin={{ top: 20, right: 30, left: 0, bottom: 20 }}> | |
| <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#F1F5F9" /> | |
| <XAxis dataKey="segment" axisLine={false} tickLine={false} tick={{fontSize: 8, fontWeight: 700, fill: '#64748b'}} interval={0}/> | |
| <YAxis yAxisId="left" axisLine={false} tickLine={false} tick={{fontSize: 9, fill: '#94a3b8'}} tickFormatter={(v) => `₹${v}Cr`}/> | |
| <YAxis yAxisId="right" orientation="right" axisLine={false} tickLine={false} tick={{fontSize: 9, fill: '#94a3b8'}} /> | |
| <Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} cursor={{ fill: '#F8FAFC' }}/> | |
| <Bar yAxisId="left" name="Meta Spend" dataKey="meta" stackId="a" fill="#2563eb" barSize={35} /> | |
| <Bar yAxisId="left" name="Google Spend" dataKey="google" stackId="a" fill="#cbd5e1" radius={[4, 4, 0, 0]} barSize={35} /> | |
| <Line yAxisId="right" type="monotone" name="Ad Volume" dataKey="volume" stroke="#6366f1" strokeWidth={3} dot={{ r: 4, fill: '#6366f1' }} /> | |
| </ComposedChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </div> | |
| <div className="p-6 bg-slate-50/50 space-y-4"> | |
| <h4 className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-4">Market Efficiency</h4> | |
| {segmentationData.slice(0, 4).map((s, i) => ( | |
| <div key={i} className="bg-white p-3 rounded-xl border border-slate-200 shadow-sm"> | |
| <p className="text-[8px] font-bold text-slate-400 uppercase mb-1">{s.segment}</p> | |
| <div className="flex justify-between items-baseline mb-2"> | |
| <span className="text-sm font-black text-slate-900">₹{(s.meta + s.google).toFixed(1)}Cr</span> | |
| <span className="text-[9px] font-bold text-indigo-600">{s.volume} Ads</span> | |
| </div> | |
| <div className="flex justify-between text-[8px] font-black uppercase text-slate-400"> | |
| <span>M: ₹{s.meta}Cr</span> | |
| <span>G: ₹{s.google}Cr</span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| </section> | |
| {/* MANUFACTURER BENCHMARK TABLE */} | |
| <section className="bg-white rounded-2xl border border-slate-200 overflow-hidden shadow-sm"> | |
| <div className="p-6 border-b border-slate-100 flex justify-between items-center bg-white"> | |
| <h3 className="font-black text-sm uppercase tracking-tighter">Manufacturer Benchmark Table</h3> | |
| <span className="text-[9px] font-black text-slate-400 bg-slate-100 px-2 py-0.5 rounded uppercase">Full Portfolio</span> | |
| </div> | |
| <table className="w-full text-left"> | |
| <thead className="bg-slate-50 border-b border-slate-100 uppercase text-[9px] font-black text-slate-400"> | |
| <tr> | |
| <th className="w-12 px-6 py-4"></th> | |
| <th className="px-6 py-4">Manufacturer</th> | |
| <th className="px-6 py-4 text-center">Meta Spend</th> | |
| <th className="px-6 py-4 text-center">Google Spend</th> | |
| <th className="px-6 py-4 text-center">SOV %</th> | |
| <th className="px-6 py-4">Primary Brand</th> | |
| </tr> | |
| </thead> | |
| <tbody className="divide-y divide-slate-100"> | |
| {oemSpendData.map(oem => ( | |
| <React.Fragment key={oem.id}> | |
| <tr className="hover:bg-slate-50 cursor-pointer transition-colors" onClick={() => toggleOemExpansion(oem.id)}> | |
| <td className="px-6 py-4"><ChevronRight size={16} className={`transition-transform duration-200 ${expandedOems.includes(oem.id) ? 'rotate-90' : 'text-slate-300'}`} /></td> | |
| <td className="px-6 py-4 font-black text-sm text-slate-900">{oem.name}</td> | |
| <td className="px-6 py-4 text-center text-blue-600 font-black">₹{oem.metaSpend}Cr</td> | |
| <td className="px-6 py-4 text-center text-indigo-600 font-black">₹{oem.googleSpend}Cr</td> | |
| <td className="px-6 py-4 text-center font-black">{oem.sov}%</td> | |
| <td className="px-6 py-4"><span className="text-[10px] font-black bg-slate-100 px-2 py-1 rounded border border-slate-200 shadow-sm">{oem.topBrand}</span></td> | |
| </tr> | |
| {expandedOems.includes(oem.id) && ( | |
| <tr className="bg-slate-50/30"> | |
| <td colSpan={6} className="px-12 py-6 border-b border-slate-100"> | |
| <div className="grid grid-cols-4 gap-4 animate-in fade-in slide-in-from-top-2 duration-300"> | |
| {oem.subBrands.map((brand, i) => ( | |
| <div | |
| key={i} | |
| className="bg-white p-4 rounded-xl border border-slate-200 shadow-sm hover:border-black transition-all cursor-pointer group/subbrand active:scale-95 shadow-sm" | |
| onClick={(e) => { e.stopPropagation(); handleBrandClick(brand.brandKey); }} | |
| > | |
| <div className="flex justify-between items-start mb-3"> | |
| <p className="text-xs font-black uppercase text-slate-900">{brand.name}</p> | |
| <Target size={12} className="text-slate-300 group-hover/subbrand:text-blue-600" /> | |
| </div> | |
| <div className="flex justify-between text-[10px] mb-1 font-bold"> | |
| <span className="text-slate-400 uppercase tracking-tighter">Meta</span> | |
| <span className="text-blue-600">₹{brand.meta}L</span> | |
| </div> | |
| <div className="flex justify-between text-[10px] font-bold"> | |
| <span className="text-slate-400 uppercase tracking-tighter">Google</span> | |
| <span className="text-indigo-600">₹{brand.google}L</span> | |
| </div> | |
| <div className="mt-3 pt-3 border-t border-slate-50 flex justify-between items-center text-[9px] font-bold uppercase text-slate-400"> | |
| <span>SOV: {brand.sov}%</span> | |
| <span className="text-blue-600 opacity-0 group-hover/subbrand:opacity-100 transition-opacity">Drill Down <ChevronRight size={10} className="inline ml-1" /></span> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </td> | |
| </tr> | |
| )} | |
| </React.Fragment> | |
| ))} | |
| </tbody> | |
| </table> | |
| </section> | |
| </div> | |
| ) : ( | |
| /* --- COMPETITION VIEW --- */ | |
| <div className="space-y-16 animate-in fade-in duration-500"> | |
| {/* Header Navigation for Competition View */} | |
| <div className="flex justify-between items-center bg-black text-white p-6 rounded-2xl shadow-xl"> | |
| <div> | |
| <SectionTag tier="COMP" label="Live Analysis" invert /> | |
| <h2 className="text-2xl font-black uppercase tracking-tighter mt-1"> | |
| Competition: {brandsData[primaryBrand]?.name} vs Alternatives | |
| </h2> | |
| </div> | |
| <button onClick={() => setViewMode('oem')} className="text-[10px] font-black border border-white/20 px-3 py-1.5 rounded-lg hover:bg-white/10 transition-colors">Return to OEM Portfolio</button> | |
| </div> | |
| {/* TIER 1: STRATEGIC COMPETITION OVERVIEW WITH PLATFORM SPLIT */} | |
| <section className="bg-white p-8 rounded-2xl border border-slate-200 shadow-sm relative overflow-hidden group"> | |
| <div className="absolute top-0 right-0 p-4 opacity-[0.03] group-hover:opacity-[0.05] transition-opacity"> | |
| <Target size={160} /> | |
| </div> | |
| <div className="flex justify-between items-start mb-10 relative z-10"> | |
| <div> | |
| <SectionTag tier="1" label="Strategic Overview" /> | |
| <h3 className="text-lg font-black tracking-tighter mt-1 uppercase text-slate-900">Share of Expense (Meta/Google) vs Share of Voice</h3> | |
| </div> | |
| <div className="flex gap-4"> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2 h-2 rounded-full bg-blue-600" /> | |
| <span className="text-[9px] font-black text-slate-400 uppercase">Meta SOE</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2 h-2 rounded-full bg-slate-300" /> | |
| <span className="text-[9px] font-black text-slate-400 uppercase">Google SOE</span> | |
| </div> | |
| <div className="flex items-center gap-2"> | |
| <div className="w-2 h-2 rounded-full bg-black" /> | |
| <span className="text-[9px] font-black text-slate-400 uppercase">SOV Share</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="h-72 relative z-10 w-full"> | |
| <ResponsiveContainer width="100%" height="100%"> | |
| <BarChart data={competitionChartData} margin={{ left: -20, bottom: 0 }}> | |
| <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#F1F5F9" /> | |
| <XAxis dataKey="name" axisLine={false} tickLine={false} tick={{fontSize: 10, fontWeight: 800, fill: '#64748b'}} dy={10} /> | |
| <YAxis axisLine={false} tickLine={false} tick={{fontSize: 9, fill: '#94a3b8'}} tickFormatter={(v) => `${v}%`} /> | |
| <Tooltip | |
| contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)' }} | |
| cursor={{ fill: '#F8FAFC' }} | |
| /> | |
| <Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ paddingBottom: '20px', fontSize: '9px', fontWeight: 'bold', textTransform: 'uppercase' }} /> | |
| {/* Stacked SOE bars: Meta + Google */} | |
| <Bar name="Meta Share of Exp." dataKey="metaSOE" stackId="soe" fill="#2563eb" barSize={32} /> | |
| <Bar name="Google Share of Exp." dataKey="googleSOE" stackId="soe" fill="#cbd5e1" radius={[4, 4, 0, 0]} barSize={32} /> | |
| {/* Separate SOV bar for direct comparison */} | |
| <Bar name="Share of Voice" dataKey="sov" fill="#000" radius={[4, 4, 0, 0]} barSize={32} /> | |
| </BarChart> | |
| </ResponsiveContainer> | |
| </div> | |
| </section> | |
| {/* TIER 3: SIDE-BY-SIDE PANELS */} | |
| <section className="space-y-6"> | |
| <div className="flex justify-between items-end"> | |
| <div> | |
| <SectionTag tier="3" label="Tactical Competition" /> | |
| <h3 className="text-lg font-black tracking-tighter uppercase text-slate-900">Side-by-Side Brand Analysis</h3> | |
| </div> | |
| <div className="text-[10px] font-black text-slate-400 uppercase tracking-wider flex gap-4"> | |
| <span>URL Classification Engine Active</span> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| {[primaryBrand, ...competitors].map((brandKey, idx) => ( | |
| <div key={brandKey} className={`space-y-6 ${idx === 0 ? '' : 'opacity-80'}`}> | |
| <div className={`p-6 rounded-2xl bg-white border ${idx === 0 ? 'border-black shadow-lg' : 'border-slate-200 shadow-sm'}`}> | |
| <div className="flex justify-between items-start mb-4"> | |
| <h4 className="font-black text-lg uppercase tracking-tight">{brandsData[brandKey].name}</h4> | |
| <span className="text-[10px] font-black bg-slate-100 px-2 py-0.5 rounded uppercase">Total {brandsData[brandKey].totalSpend}</span> | |
| </div> | |
| <div className="flex gap-6 text-[10px] font-black uppercase"> | |
| <div className="flex flex-col"><span className="text-slate-400 text-[8px]">Meta Spend</span><span className="text-blue-600">{brandsData[brandKey].metaSpend}</span></div> | |
| <div className="flex flex-col"><span className="text-slate-400 text-[8px]">Google Spend</span><span className="text-indigo-600">{brandsData[brandKey].googleSpend}</span></div> | |
| </div> | |
| <div className="mt-4 space-y-2"> | |
| <div className="flex justify-between text-[8px] font-bold text-slate-400 uppercase"> | |
| <span>Strategy Ratio</span> | |
| <span className="text-black font-black">{brandsData[brandKey].ratio}</span> | |
| </div> | |
| <div className="h-1.5 w-full bg-slate-100 rounded-full overflow-hidden flex"> | |
| <div className="h-full bg-black" style={{ width: brandsData[brandKey].ratio.split('/')[0] + '%' }}></div> | |
| <div className="h-full bg-slate-300" style={{ width: brandsData[brandKey].ratio.split('/')[1] + '%' }}></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| </section> | |
| {/* NEW TIER 4: STRATEGIC FUNNEL JOURNEY */} | |
| <section className="space-y-12"> | |
| <div className="flex flex-col gap-2"> | |
| <SectionTag tier="4" label="Funnel Analysis" /> | |
| <h3 className="text-xl font-black uppercase tracking-tighter text-slate-900">Funnel Journey & Spend Distribution</h3> | |
| <p className="text-xs text-slate-400 font-bold uppercase">Cross-brand tactical efficiency breakdown from Awareness to Conversion</p> | |
| </div> | |
| {funnelStages.map((stage, sIdx) => ( | |
| <div key={stage.id} className="space-y-8 p-8 rounded-3xl bg-white border border-slate-200 shadow-sm relative overflow-hidden group"> | |
| <div className="absolute top-0 right-0 p-4 opacity-[0.04]"> | |
| <stage.icon size={120} /> | |
| </div> | |
| <div className="flex justify-between items-end border-b border-slate-100 pb-6 relative z-10"> | |
| <div className="flex items-center gap-4"> | |
| <div className={`w-12 h-12 rounded-2xl ${stage.color} flex items-center justify-center text-white shadow-lg`}> | |
| <stage.icon size={24} /> | |
| </div> | |
| <div> | |
| <h4 className="text-lg font-black uppercase tracking-tighter">{stage.label} Stage</h4> | |
| <p className="text-[10px] text-slate-400 font-black uppercase tracking-widest">{stage.description}</p> | |
| </div> | |
| </div> | |
| <div className="flex gap-12 text-right"> | |
| <div> | |
| <p className="text-[9px] font-black text-slate-400 uppercase mb-1 tracking-tighter">Stage Meta Spend</p> | |
| <p className="text-xl font-black text-blue-600">₹{45 - (sIdx * 10)}.2 L</p> | |
| </div> | |
| <div> | |
| <p className="text-[9px] font-black text-slate-400 uppercase mb-1 tracking-tighter">Stage Google Spend</p> | |
| <p className="text-xl font-black text-indigo-600">₹{25 + (sIdx * 5)}.8 L</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="grid grid-cols-1 lg:grid-cols-3 gap-8 pt-4"> | |
| {[primaryBrand, ...competitors].map(brandKey => { | |
| const stageCampaigns = brandsData[brandKey].campaigns.filter(c => c.type === stage.id); | |
| return ( | |
| <div key={brandKey} className="space-y-4"> | |
| {stageCampaigns.length > 0 ? ( | |
| stageCampaigns.map(camp => ( | |
| <FunnelCampaignCard key={camp.id} camp={camp} brandName={brandsData[brandKey].name} /> | |
| )) | |
| ) : ( | |
| <div className="h-full min-h-[160px] rounded-2xl border border-dashed border-slate-200 flex flex-col items-center justify-center p-6 text-center bg-slate-50/20"> | |
| <p className="text-[9px] font-black text-slate-300 uppercase leading-relaxed tracking-wider">No active {stage.label} assets<br/>for {brandsData[brandKey].name}</p> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </div> | |
| </div> | |
| ))} | |
| </section> | |
| </div> | |
| )} | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| // --- Sub-components --- | |
| const FilterGroup = ({ label, children }) => ( | |
| <div className="flex flex-col gap-1 min-w-max"> | |
| <label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">{label}</label> | |
| {children} | |
| </div> | |
| ); | |
| const SectionTag = ({ tier, label, invert = false }) => ( | |
| <div className="flex items-center gap-2 mb-1"> | |
| <span className={`${invert ? 'bg-white text-black' : 'bg-black text-white'} text-[8px] font-black px-1.5 py-0.5 rounded shadow-sm uppercase`}>TIER {tier}</span> | |
| <span className={`text-[10px] font-black ${invert ? 'text-white/50' : 'text-slate-400'} uppercase tracking-widest`}>{label}</span> | |
| </div> | |
| ); | |
| const KPICard = ({ title, value, sub, icon: Icon, color = "slate" }) => ( | |
| <div className="bg-white p-6 rounded-2xl border border-slate-200 shadow-sm group hover:border-black transition-all"> | |
| <div className="flex justify-between items-start mb-4"> | |
| <div className={`p-2 rounded-lg bg-slate-50 ${color === 'blue' ? 'text-blue-600' : color === 'emerald' ? 'text-emerald-600' : 'text-slate-400'}`}> | |
| <Icon size={18} /> | |
| </div> | |
| <ArrowUpRight size={14} className="text-slate-300" /> | |
| </div> | |
| <p className="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">{title}</p> | |
| <h4 className="text-2xl font-black text-slate-900 mb-1 tracking-tighter uppercase">{value}</h4> | |
| <p className="text-[10px] text-slate-500 font-bold uppercase overflow-hidden whitespace-nowrap text-ellipsis">{sub}</p> | |
| </div> | |
| ); | |
| const FunnelCampaignCard = ({ camp, brandName }) => ( | |
| <div className="bg-white border border-slate-200 rounded-3xl overflow-hidden shadow-sm hover:shadow-xl transition-all flex flex-col group/card"> | |
| <div className="p-4 bg-slate-50 border-b border-slate-100 flex justify-between items-center"> | |
| <span className="text-[9px] font-black uppercase text-slate-400 tracking-tighter">{brandName} Asset</span> | |
| <div className="bg-emerald-100 text-emerald-700 text-[10px] font-black px-2 py-0.5 rounded border border-emerald-200">{camp.cost}</div> | |
| </div> | |
| <div className="p-4 space-y-4"> | |
| <div className="flex flex-col gap-1 min-w-0"> | |
| <div className="text-blue-600 text-[10px] font-bold truncate flex items-center gap-1 group-hover:text-blue-700"> | |
| <Globe size={10} /> {camp.url} | |
| </div> | |
| <div className="flex gap-1.5 mt-1.5"> | |
| {camp.tags.map((t, idx) => ( | |
| <span key={idx} className="text-[8px] font-black uppercase bg-white text-slate-400 px-2 py-0.5 rounded-full border border-slate-200 shadow-sm"> | |
| {t} | |
| </span> | |
| ))} | |
| </div> | |
| </div> | |
| {/* IMPROVED PREVIEW WITH MULTI-IMAGE GALLERY */} | |
| <div className="space-y-2"> | |
| <div className="relative aspect-[16/10] rounded-2xl overflow-hidden border border-slate-100 bg-gray-50 group shadow-inner"> | |
| <img src={camp.images[0]} alt="Primary Creative" className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-1000" /> | |
| <div className="absolute inset-0 bg-black/10 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"> | |
| <button className="bg-white text-black text-[9px] font-black px-4 py-2 rounded-full shadow-xl flex items-center gap-2"> | |
| <Search size={12} /> Asset Analysis | |
| </button> | |
| </div> | |
| </div> | |
| {camp.images.length > 1 && ( | |
| <div className="flex gap-2 overflow-x-auto no-scrollbar pb-1"> | |
| {camp.images.slice(1).map((img, i) => ( | |
| <div key={i} className="w-16 aspect-square rounded-lg overflow-hidden border border-slate-100 bg-slate-50 flex-shrink-0 cursor-pointer hover:border-blue-400 transition-all opacity-80 hover:opacity-100"> | |
| <img src={img} alt={`Asset ${i+2}`} className="w-full h-full object-cover" /> | |
| </div> | |
| ))} | |
| <div className="w-16 aspect-square rounded-lg border border-dashed border-slate-200 flex flex-col items-center justify-center bg-slate-50/50 flex-shrink-0 group/add cursor-pointer"> | |
| <ImageIcon size={12} className="text-slate-300 group-hover/add:text-blue-400 transition-colors" /> | |
| <span className="text-[6px] font-black text-slate-300 mt-1 uppercase tracking-widest group-hover/add:text-blue-400">+2 Assets</span> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="grid grid-cols-2 gap-px bg-slate-200 border border-slate-200 rounded-xl overflow-hidden shadow-sm"> | |
| <div className="bg-white p-3 text-center"> | |
| <p className="text-[7px] font-black text-slate-400 uppercase tracking-widest mb-0.5">CTR (Efficacy)</p> | |
| <p className={`text-xs font-black ${camp.efficacy.status === 'above' ? 'text-emerald-600' : 'text-slate-900'}`}>{camp.efficacy.ctr}</p> | |
| </div> | |
| <div className="bg-white p-3 text-center"> | |
| <p className="text-[7px] font-black text-slate-400 uppercase tracking-widest mb-0.5">CPM (Market)</p> | |
| <p className="text-xs font-black text-slate-900">{camp.efficacy.cpm}</p> | |
| </div> | |
| </div> | |
| <div className="bg-slate-50 p-3 rounded-2xl border border-slate-100 group-hover/card:border-slate-300 transition-colors"> | |
| <div className="flex items-center gap-1.5 text-[8px] font-black text-slate-400 uppercase mb-1.5 tracking-widest"> | |
| <MessageSquare size={10} className="text-slate-900" /> Primary Messaging | |
| </div> | |
| <p className="text-[10px] text-slate-700 font-bold leading-relaxed italic line-clamp-2 uppercase tracking-tight">"{camp.strategy}"</p> | |
| </div> | |
| <div className="grid grid-cols-3 pt-3 border-t border-slate-100 text-center gap-2"> | |
| <div><p className="text-[7px] font-black text-slate-400 uppercase mb-0.5 tracking-widest">Traffic</p><p className="text-[10px] font-black">{camp.funnel.traffic}</p></div> | |
| <div><p className="text-[7px] font-black text-slate-400 uppercase mb-0.5 tracking-widest">Conv.</p><p className="text-[10px] font-black text-emerald-600">{camp.funnel.conversion}</p></div> | |
| <div><p className="text-[7px] font-black text-slate-400 uppercase mb-0.5 tracking-widest">Bounce</p><p className="text-[10px] font-black text-red-500">{camp.funnel.bounce}</p></div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| export default App; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment