634 lines
36 KiB
TypeScript
634 lines
36 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
Search, FileDown, Info, LayoutGrid, Zap, Sparkles, Loader2,
|
|
TrendingUp, TrendingDown, Minus, Target, Eye, Layers,
|
|
BarChart3, Cloud, Activity, Globe, Rocket, ShieldCheck,
|
|
Target as TargetIcon, UserCheck, MessageSquare, Copy,
|
|
ArrowUpRight, CheckCircle2, Lightbulb, AlertTriangle, Clock,
|
|
Users, Newspaper, Smile, Frown, Meh, Star, Compass, LogOut
|
|
} from 'lucide-react';
|
|
import { useMsal } from '@azure/msal-react';
|
|
import { InteractionRequiredAuthError } from '@azure/msal-browser';
|
|
import { loginRequest } from './authConfig';
|
|
import { DashboardData, SearchState, CompetitorClaim, MarketMetric, WhitespaceItem, Creator, NewsSentiment, RadarMetric } from './types';
|
|
import { fetchMarketInsights } from './geminiService';
|
|
|
|
const VerifiedSourceBadge: React.FC<{ source?: string; methodology?: string }> = ({ source, methodology }) => {
|
|
if (!source) return null;
|
|
return (
|
|
<div className="relative group/badge inline-block">
|
|
<div className="flex items-center gap-1.5 px-2 py-0.5 bg-green-50 text-green-700 text-[8px] font-black uppercase tracking-widest rounded-full border border-green-100 cursor-help transition-all hover:bg-green-100">
|
|
<CheckCircle2 size={10} />
|
|
Verified Source
|
|
</div>
|
|
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 w-56 p-3 bg-black text-white text-[9px] rounded-xl opacity-0 invisible group-hover/badge:opacity-100 group-hover/badge:visible transition-all shadow-2xl z-[100] pointer-events-none border border-white/10">
|
|
<div className="font-black text-[#FFD700] mb-1 uppercase tracking-tighter">Primary Source:</div>
|
|
<div className="mb-2 font-medium opacity-90">{source}</div>
|
|
<div className="font-black text-[#FFD700] mb-1 uppercase tracking-tighter">Methodology:</div>
|
|
<div className="font-medium opacity-90 leading-tight">{methodology || "Grounded Strategic Synthesis"}</div>
|
|
<div className="absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-black"></div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const WhitespaceCard: React.FC<{ item: WhitespaceItem }> = ({ item }) => {
|
|
const isOpportunity = item.type === 'OPPORTUNITY';
|
|
return (
|
|
<div className={`p-5 rounded-[2rem] border-2 transition-all hover:scale-[1.02] h-full ${
|
|
isOpportunity
|
|
? 'bg-[#FFD700]/5 border-[#FFD700] text-black shadow-[0_10px_30px_-15px_rgba(255,215,0,0.2)]'
|
|
: 'bg-gray-50 border-black/10 text-gray-500'
|
|
}`}>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
{isOpportunity ? <Lightbulb size={16} className="text-black" /> : <AlertTriangle size={16} className="text-black/40" />}
|
|
<span className={`text-[8px] font-black uppercase tracking-widest ${isOpportunity ? 'text-black' : 'text-black/40'}`}>
|
|
{item.type}
|
|
</span>
|
|
</div>
|
|
<h4 className="text-[11px] font-black mb-2 uppercase tracking-tight text-black line-clamp-1">{item.title}</h4>
|
|
<p className="text-[10px] leading-relaxed font-medium opacity-80 line-clamp-3">{item.description}</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const Logo = () => (
|
|
<div className="relative flex items-center justify-center w-10 h-10 bg-black rounded-xl group-hover:bg-[#FFD700] transition-all duration-300 shadow-lg shrink-0">
|
|
<Compass size={22} className="text-[#FFD700] group-hover:text-black transition-colors" />
|
|
<div className="absolute top-1.5 right-1.5">
|
|
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse" />
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const App: React.FC = () => {
|
|
const { instance, accounts } = useMsal();
|
|
const activeAccount = accounts[0];
|
|
|
|
const handleLogout = () => {
|
|
instance.logoutRedirect({ postLogoutRedirectUri: process.env.AZURE_REDIRECT_URI });
|
|
};
|
|
|
|
const [search, setSearch] = useState<SearchState & { country: string }>({
|
|
product: '',
|
|
category: '',
|
|
country: '',
|
|
loading: false,
|
|
error: null
|
|
});
|
|
const [data, setData] = useState<DashboardData | null>(null);
|
|
const [hoveredClaim, setHoveredClaim] = useState<CompetitorClaim | null>(null);
|
|
const [loadMessage, setLoadMessage] = useState('Initiating Audit...');
|
|
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
|
|
|
const handleSearch = async (e?: React.FormEvent) => {
|
|
e?.preventDefault();
|
|
if (!search.product || !search.category || search.loading) return;
|
|
|
|
// Validate session before running the search
|
|
try {
|
|
await instance.acquireTokenSilent({ ...loginRequest, account: activeAccount });
|
|
} catch (err) {
|
|
if (err instanceof InteractionRequiredAuthError) {
|
|
setSearch(prev => ({ ...prev, error: 'Your session has expired. Redirecting you to sign in again...' }));
|
|
setTimeout(() => instance.loginRedirect(loginRequest), 2500);
|
|
return;
|
|
}
|
|
// Transient silent-token error — log but don't block the search
|
|
console.warn('Silent token refresh failed (non-interaction):', err);
|
|
}
|
|
|
|
setSearch(prev => ({ ...prev, loading: true, error: null }));
|
|
setData(null);
|
|
setLoadMessage('Grounded Search Active...');
|
|
|
|
const messages = [
|
|
'Scanning Regional Dynamics...',
|
|
'Aggregating Competitor Claims...',
|
|
'Analyzing Social Velocity...',
|
|
'Synthesizing Executive Brief...'
|
|
];
|
|
let msgIndex = 0;
|
|
const interval = setInterval(() => {
|
|
msgIndex = (msgIndex + 1) % messages.length;
|
|
setLoadMessage(messages[msgIndex]);
|
|
}, 3000);
|
|
|
|
try {
|
|
const locationContext = search.country ? `in ${search.country}` : "across the APAC region";
|
|
const result = await fetchMarketInsights(`${search.product} ${locationContext}`, search.category);
|
|
setData(result);
|
|
setLastUpdated(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }));
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
setSearch(prev => ({
|
|
...prev,
|
|
error: err.message || 'System busy. Please try again.'
|
|
}));
|
|
} finally {
|
|
clearInterval(interval);
|
|
setSearch(prev => ({ ...prev, loading: false }));
|
|
}
|
|
};
|
|
|
|
const handleExportHtml = () => {
|
|
if (!data) return;
|
|
const htmlContent = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>APAC Strategy & Insights Engine Report - ${search.product}</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700;900&display=swap" rel="stylesheet">
|
|
<style>body { font-family: 'Inter', sans-serif; }</style>
|
|
</head>
|
|
<body>
|
|
${document.getElementById('root')?.innerHTML || ''}
|
|
</body>
|
|
</html>
|
|
`;
|
|
const blob = new Blob([htmlContent], { type: 'text/html' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `APAC_Strategy_Insights_Engine_${search.product.replace(/\s+/g, '_')}.html`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const getSentimentIcon = (sentiment: string) => {
|
|
switch (sentiment) {
|
|
case 'positive': return <Smile className="text-green-500" size={16} />;
|
|
case 'negative': return <Frown className="text-red-500" size={16} />;
|
|
default: return <Meh className="text-gray-400" size={16} />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-white text-black selection:bg-[#FFD700] pb-20">
|
|
<style>{`
|
|
@keyframes exclamation-glow {
|
|
0%, 100% { filter: drop-shadow(0 0 0px #FFD700); transform: scale(1); }
|
|
50% { filter: drop-shadow(0 0 8px #FFD700); transform: scale(1.1); }
|
|
}
|
|
.exclamation-active {
|
|
animation: exclamation-glow 1.5s ease-in-out infinite;
|
|
display: inline-block;
|
|
}
|
|
@media print {
|
|
.no-print { display: none !important; }
|
|
}
|
|
`}</style>
|
|
|
|
<header className="sticky top-0 z-50 bg-white border-b-2 border-black/5 px-6 py-5">
|
|
<div className="max-w-[1600px] mx-auto flex flex-col md:flex-row items-center gap-6">
|
|
<div className="flex items-center gap-4 shrink-0 group cursor-pointer" onClick={() => window.location.reload()}>
|
|
<Logo />
|
|
<div className="flex flex-col justify-center">
|
|
<span className="font-black tracking-tight text-[15px] leading-[1.1] text-black uppercase">APAC Strategy</span>
|
|
<span className="font-black tracking-tight text-[15px] leading-[1.1] text-black uppercase">& Insights Engine</span>
|
|
</div>
|
|
<div className="h-8 w-[1px] bg-gray-200 no-print ml-2" />
|
|
</div>
|
|
|
|
<form onSubmit={handleSearch} className="flex-grow flex gap-3 w-full no-print">
|
|
<input
|
|
placeholder="Brand/Product (e.g. Dyson)"
|
|
value={search.product}
|
|
onChange={e => setSearch(p => ({...p, product: e.target.value}))}
|
|
className="flex-grow px-4 py-3 bg-gray-50 border-2 border-gray-100 rounded-xl focus:border-black outline-none text-sm font-bold transition-all"
|
|
/>
|
|
<input
|
|
placeholder="Category (e.g. Hair Care)"
|
|
value={search.category}
|
|
onChange={e => setSearch(p => ({...p, category: e.target.value}))}
|
|
className="flex-grow px-4 py-3 bg-gray-50 border-2 border-gray-100 rounded-xl focus:border-black outline-none text-sm font-bold transition-all"
|
|
/>
|
|
<div className="relative flex-grow max-w-[200px]">
|
|
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={16} />
|
|
<input
|
|
placeholder="Market"
|
|
value={search.country}
|
|
onChange={e => setSearch(p => ({...p, country: e.target.value}))}
|
|
className="w-full pl-10 pr-4 py-3 bg-gray-50 border-2 border-gray-100 rounded-xl focus:border-black outline-none text-sm font-bold transition-all"
|
|
/>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={search.loading || !search.product || !search.category}
|
|
className="bg-[#FFD700] hover:bg-black hover:text-[#FFD700] text-black px-8 py-3 rounded-xl font-black uppercase tracking-[0.2em] text-xs transition-all flex items-center justify-center border-2 border-black/5 active:scale-95 disabled:opacity-50 min-w-[140px]"
|
|
>
|
|
{search.loading ? <Loader2 size={18} className="animate-spin" /> : 'Analyse'}
|
|
</button>
|
|
</form>
|
|
|
|
{/* User info + logout */}
|
|
{activeAccount && (
|
|
<div className="flex items-center gap-3 shrink-0 no-print">
|
|
<div className="hidden md:flex flex-col items-end">
|
|
<span className="text-[11px] font-black text-black uppercase tracking-tight leading-none">{activeAccount.name?.split(' ')[0]}</span>
|
|
<span className="text-[9px] font-bold text-gray-400 uppercase tracking-widest">{activeAccount.username}</span>
|
|
</div>
|
|
<button
|
|
onClick={handleLogout}
|
|
title="Sign out"
|
|
className="flex items-center justify-center w-10 h-10 bg-gray-50 hover:bg-black hover:text-white text-black rounded-xl border-2 border-black/8 hover:border-black transition-all active:scale-95"
|
|
>
|
|
<LogOut size={16} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</header>
|
|
|
|
<main className="max-w-[1600px] mx-auto px-6 py-8 space-y-16">
|
|
{search.error && (
|
|
<div className="bg-amber-50 text-amber-900 p-8 rounded-2xl font-black text-center border-2 border-amber-200 animate-in fade-in slide-in-from-top-4">
|
|
<p className="mb-4">{search.error}</p>
|
|
<button onClick={() => handleSearch()} className="px-6 py-2 bg-black text-white rounded-lg text-xs uppercase tracking-widest">Retry Audit</button>
|
|
</div>
|
|
)}
|
|
|
|
{search.loading && !data && (
|
|
<div className="flex flex-col items-center justify-center py-48 animate-in fade-in duration-700">
|
|
<div className="relative mb-8 group">
|
|
<Logo />
|
|
</div>
|
|
<h2 className="text-2xl font-black uppercase tracking-[0.4em] text-center">{loadMessage}</h2>
|
|
</div>
|
|
)}
|
|
|
|
{data && (
|
|
<>
|
|
{/* 1. Header: Status & Metrics */}
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between px-2">
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex h-2 w-2 rounded-full bg-green-500 animate-pulse" />
|
|
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-gray-500">Live Market Pulse</span>
|
|
</div>
|
|
{lastUpdated && (
|
|
<div className="flex items-center gap-2 text-[10px] font-black text-gray-300 uppercase tracking-widest">
|
|
<Clock size={12} />
|
|
Audit Sync: {lastUpdated}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Metric Strip */}
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
|
|
{(Object.values(data.marketMetrics) as MarketMetric[]).map((m, i) => m && (
|
|
<div key={i} className="bg-white border border-gray-200 rounded-3xl flex flex-col hover:shadow-lg transition-all border-t-8 border-t-[#FFD700] group min-h-[160px] relative">
|
|
<div className="p-6 flex-grow flex flex-col">
|
|
<div className="flex justify-between items-start mb-3">
|
|
<span className="text-[10px] font-black text-gray-400 group-hover:text-black uppercase tracking-widest block">{m.label}</span>
|
|
<VerifiedSourceBadge source={m.source} methodology={m.methodology} />
|
|
</div>
|
|
<div className="text-3xl font-black flex items-center justify-between mb-3">
|
|
{m.value}
|
|
{m.trend === 'up' ? <TrendingUp size={20} className="text-green-500" /> : <TrendingDown size={20} className="text-red-500" />}
|
|
</div>
|
|
<p className="text-[10px] text-gray-500 font-bold leading-relaxed">{m.explanation}</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 2. SOS & Velocity Row */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
|
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
|
|
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Share of Search</h2>
|
|
<BarChart3 size={16} className="text-black group-hover:text-[#FFD700]" />
|
|
</div>
|
|
<div className="space-y-6">
|
|
{data.shareOfSearch?.map((item, i) => (
|
|
<div key={i}>
|
|
<div className="flex justify-between text-[11px] font-black mb-2">
|
|
<span className="uppercase">{item.competitor}</span>
|
|
<span className="bg-black text-white px-2 py-0.5 rounded text-[8px] group-hover:bg-[#FFD700] group-hover:text-black transition-colors">{item.percentage}%</span>
|
|
</div>
|
|
<div className="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
|
|
<div className="h-full bg-black group-hover:bg-[#FFD700] transition-all" style={{ width: `${item.percentage}%` }} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
|
<div className="flex justify-between items-center mb-10 border-b border-gray-100 pb-4">
|
|
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Cultural Velocity</h2>
|
|
<Activity size={18} className="text-black group-hover:text-[#FFD700]" />
|
|
</div>
|
|
<div className="space-y-8">
|
|
{data.platformVelocity?.map((pv, i) => (
|
|
<div key={i}>
|
|
<div className="flex justify-between text-[11px] font-black mb-3">
|
|
<span className="uppercase">{pv.platform} <span className="text-gray-400 ml-2">/ {pv.region}</span></span>
|
|
<span className="bg-black text-white px-2 py-0.5 rounded text-[9px] group-hover:bg-[#FFD700] group-hover:text-black transition-colors">{pv.value}%</span>
|
|
</div>
|
|
<div className="h-2 w-full bg-gray-100 rounded-full overflow-hidden">
|
|
<div className="h-full bg-black group-hover:bg-[#FFD700] transition-all" style={{ width: `${pv.value}%` }} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
<section className="space-y-8">
|
|
<div className="flex items-center gap-3 px-2">
|
|
<Cloud size={18} className="text-[#FFD700]" />
|
|
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Grounded Competitor Claim Density</h2>
|
|
</div>
|
|
<div className="bg-black border-2 border-[#FFD700]/20 rounded-[3rem] p-10 relative overflow-hidden shadow-2xl">
|
|
<div className="flex flex-wrap gap-x-8 gap-y-4 justify-center items-center py-4 relative z-10">
|
|
{data.competitorClaims.map((claim, idx) => (
|
|
<button
|
|
key={idx}
|
|
onMouseEnter={() => setHoveredClaim(claim)}
|
|
onMouseLeave={() => setHoveredClaim(null)}
|
|
className="font-black text-white/80 hover:text-[#FFD700] transition-all uppercase tracking-tighter leading-none whitespace-nowrap"
|
|
style={{ fontSize: `${16 + (claim.frequency * 0.4)}px`, opacity: 0.6 + (claim.frequency / 70) }}
|
|
>
|
|
{claim.keyword}
|
|
</button>
|
|
))}
|
|
</div>
|
|
{hoveredClaim && (
|
|
<div className="mt-8 bg-white/5 backdrop-blur-md p-6 rounded-[2rem] border-2 border-[#FFD700]/30 animate-in fade-in slide-in-from-top-2 shadow-2xl text-white">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-[10px] font-black text-black bg-[#FFD700] px-2 py-0.5 rounded uppercase">{hoveredClaim.keyword}</span>
|
|
<VerifiedSourceBadge source={hoveredClaim.source} methodology={hoveredClaim.methodology} />
|
|
</div>
|
|
</div>
|
|
<p className="text-[12px] font-bold text-gray-300 italic">"{hoveredClaim.explanation}"</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* 4. Benchmarking Row: Efficacy Benchmark & High-Impact Creators */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
|
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
|
|
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">Efficacy Benchmark</h2>
|
|
<Target size={16} className="text-black group-hover:text-[#FFD700]" />
|
|
</div>
|
|
<div className="space-y-6">
|
|
{data.radarMetrics?.map((metric, i) => (
|
|
<div key={i} className="space-y-2">
|
|
<div className="flex justify-between text-[11px] font-black uppercase">
|
|
<span>{metric.label}</span>
|
|
<span>{metric.score}/100</span>
|
|
</div>
|
|
<div className="h-1.5 w-full bg-gray-100 rounded-full overflow-hidden relative">
|
|
<div className="absolute top-0 left-0 h-full bg-black group-hover:bg-[#FFD700] transition-all" style={{ width: `${metric.score}%` }} />
|
|
<div className="absolute top-0 h-full w-[2px] bg-[#FFD700]" style={{ left: `${metric.benchmark}%` }} />
|
|
</div>
|
|
<div className="text-[8px] font-black text-gray-400 uppercase tracking-widest">Benchmark: {metric.benchmark}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="bg-white border-2 border-black rounded-[2.5rem] p-8 group hover:border-[#FFD700] transition-all h-full overflow-hidden">
|
|
<div className="flex justify-between items-center mb-8 border-b border-gray-100 pb-4">
|
|
<h2 className="text-[10px] font-black uppercase tracking-[0.3em] text-gray-400 group-hover:text-black">High-Impact Creators</h2>
|
|
<Users size={16} className="text-black group-hover:text-[#FFD700]" />
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{data.topCreators?.slice(0, 3).map((creator, i) => (
|
|
<div key={i} className="flex items-center gap-4 p-4 bg-gray-50 rounded-2xl border-2 border-transparent hover:border-[#FFD700] transition-all">
|
|
<div className="w-12 h-12 bg-black text-[#FFD700] rounded-full flex items-center justify-center font-black text-lg group-hover:bg-[#FFD700] group-hover:text-black transition-colors">
|
|
{creator.name.charAt(0)}
|
|
</div>
|
|
<div className="flex-grow">
|
|
<div className="text-[11px] font-black uppercase">{creator.name} <span className="text-gray-400 ml-1">@{creator.handle}</span></div>
|
|
<div className="text-[9px] font-bold text-gray-500 uppercase tracking-widest">{creator.category}</div>
|
|
<div className="text-[10px] font-medium text-black mt-1 italic">"{creator.impact}"</div>
|
|
</div>
|
|
<Star size={14} className="text-[#FFD700]" fill="#FFD700" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
|
|
{/* 5. Whitespace Analysis Row */}
|
|
<section className="space-y-8">
|
|
<div className="flex items-center gap-3 px-2">
|
|
<TargetIcon size={18} className="text-[#FFD700]" />
|
|
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Strategic Whitespace Analysis</h2>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{data.whitespaceAnalysis?.map((item, i) => (
|
|
<WhitespaceCard key={i} item={item} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* 6. Intelligence Core */}
|
|
<section className="bg-black text-white border-2 border-black rounded-[3rem] p-10 lg:p-16 relative overflow-hidden shadow-2xl">
|
|
<div className="absolute top-0 right-0 w-96 h-96 bg-[#FFD700]/5 blur-[120px] rounded-full -mr-32 -mt-32" />
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-20">
|
|
<div className="space-y-12">
|
|
<div>
|
|
<div className="flex items-center gap-3 mb-6">
|
|
<ShieldCheck size={20} className="text-[#FFD700]" />
|
|
<h2 className="text-[10px] font-black tracking-[0.4em] uppercase text-gray-400">Executive Intelligence</h2>
|
|
</div>
|
|
<p className="text-xl font-bold tracking-tight leading-relaxed text-gray-300 italic border-l-2 border-[#FFD700]/30 pl-8">
|
|
{data.summary.overview}
|
|
</p>
|
|
</div>
|
|
<div className="space-y-8">
|
|
<h3 className="text-[10px] font-black text-[#FFD700] uppercase tracking-[0.4em] border-b border-[#FFD700]/10 pb-4 flex items-center gap-2">
|
|
<Zap size={14} fill="#FFD700" /> Strategic Takeaways
|
|
</h3>
|
|
<ul className="space-y-6">
|
|
{data.summary.strategicTakeaways.map((point, idx) => (
|
|
<li key={idx} className="flex gap-6 items-start group">
|
|
<span className="text-[#FFD700] font-black bg-[#FFD700]/10 w-8 h-8 rounded-xl flex items-center justify-center flex-shrink-0 text-xs border border-[#FFD700]/20">
|
|
{idx + 1}
|
|
</span>
|
|
<span className="text-sm font-medium leading-relaxed text-gray-300 group-hover:text-white transition-colors">{point}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-12 bg-white/5 p-10 rounded-[2.5rem] border border-white/10">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Rocket size={20} className="text-[#FFD700]" />
|
|
<h2 className="text-[10px] font-black uppercase tracking-[0.4em] text-gray-400">Tactical Brief</h2>
|
|
</div>
|
|
<button className="text-white/40 hover:text-[#FFD700] transition-colors no-print">
|
|
<Copy size={16} />
|
|
</button>
|
|
</div>
|
|
<div className="space-y-8">
|
|
<div className="flex gap-6 items-start">
|
|
<TargetIcon className="text-[#FFD700] shrink-0 mt-1" size={20} />
|
|
<div>
|
|
<div className="text-[10px] font-black uppercase text-gray-500 mb-1 tracking-widest">Core Objective</div>
|
|
<p className="text-sm font-bold text-white">{data.agencyBrief.objective}</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-6 items-start">
|
|
<UserCheck className="text-[#FFD700] shrink-0 mt-1" size={20} />
|
|
<div>
|
|
<div className="text-[10px] font-black uppercase text-gray-500 mb-1 tracking-widest">Target Persona</div>
|
|
<p className="text-sm font-bold text-white">{data.agencyBrief.targetAudience}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-8 pt-8 border-t border-white/10">
|
|
<div className="text-[10px] font-black uppercase tracking-[0.4em] text-[#FFD700] flex items-center gap-2">
|
|
<Sparkles size={14} fill="#FFD700" /> Actionable Strategies
|
|
</div>
|
|
<ul className="grid grid-cols-1 gap-4">
|
|
{data.agencyBrief.creativeHooks.map((hook, i) => (
|
|
<li key={i} className="group flex gap-4 items-start bg-white/5 p-4 rounded-2xl border border-transparent hover:border-[#FFD700]/30 transition-all">
|
|
<span className="text-[#FFD700] font-black text-lg leading-none">/</span>
|
|
<p className="text-xs font-bold leading-relaxed text-gray-300 group-hover:text-white transition-colors">{hook}</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* 7. Cultural Nuance Section (Always 4 items requested via service) */}
|
|
<section className="space-y-8">
|
|
<div className="flex items-center gap-3 px-2">
|
|
<Globe size={18} className="text-[#FFD700]" />
|
|
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Cultural Nuance</h2>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{data.culturalNuances?.slice(0, 4).map((nuance, i) => (
|
|
<div key={i} className="bg-white border border-gray-200 rounded-3xl flex flex-col hover:shadow-lg transition-all border-t-8 border-t-[#FFD700] relative">
|
|
<div className="p-8 flex-grow flex flex-col space-y-6">
|
|
<div className="flex justify-between items-start">
|
|
<h3 className="text-xl font-black tracking-tight text-black uppercase">{nuance.term}</h3>
|
|
<VerifiedSourceBadge source={nuance.sourceTitle} methodology="Verified Regional Anthro-Data" />
|
|
</div>
|
|
<p className="text-[11px] font-bold text-gray-500 leading-relaxed italic border-l-2 border-gray-100 pl-4">{nuance.insight}</p>
|
|
<div className="bg-black text-white p-5 rounded-2xl mt-auto border-l-4 border-[#FFD700]">
|
|
<div className="text-[8px] font-black uppercase tracking-[0.2em] text-[#FFD700] mb-2">Strategic Pivot</div>
|
|
<p className="text-[10px] font-black leading-snug">{nuance.strategyTip}</p>
|
|
</div>
|
|
</div>
|
|
<div className="px-8 py-4 bg-gray-50 border-t border-gray-100 flex justify-end">
|
|
<a href={nuance.sourceUrl} target="_blank" rel="noopener noreferrer" className="text-[9px] font-black uppercase text-gray-400 hover:text-black flex items-center gap-1">
|
|
Full Source <ArrowUpRight size={10} />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* 8. Industry Pulse Section (Moved to End) */}
|
|
<section className="space-y-8">
|
|
<div className="flex items-center gap-3 px-2">
|
|
<Newspaper size={18} className="text-[#FFD700]" />
|
|
<h2 className="text-[11px] font-black uppercase tracking-[0.4em] text-gray-400">Industry Pulse</h2>
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{data.newsFeed?.map((news, i) => (
|
|
<div key={i} className="bg-white border border-gray-100 rounded-3xl p-6 hover:shadow-xl transition-all group flex flex-col min-h-[180px]">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div className="text-[9px] font-black text-gray-400 uppercase tracking-widest truncate max-w-[120px]">{news.source}</div>
|
|
<div className="flex items-center gap-1.5 px-2 py-1 bg-gray-50 rounded-full">
|
|
{getSentimentIcon(news.sentiment)}
|
|
<span className="text-[10px] font-black">{news.score}</span>
|
|
</div>
|
|
</div>
|
|
<h3 className="text-xs font-black mb-4 leading-tight group-hover:text-[#FFD700] transition-colors line-clamp-3">"{news.headline}"</h3>
|
|
<div className="mt-auto flex justify-end">
|
|
<ArrowUpRight size={14} className="text-gray-300" />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* 9. Audit Trail & Reference Hub (Bottom) */}
|
|
<footer className="pt-20 pb-20 border-t-2 border-gray-100">
|
|
<div className="bg-gray-50 p-12 rounded-[3.5rem] border-2 border-gray-100 relative overflow-hidden">
|
|
<div className="absolute top-0 right-0 w-80 h-80 bg-black/5 rounded-full blur-[100px] -mr-40 -mt-40" />
|
|
<h3 className="text-[11px] font-black text-gray-400 uppercase tracking-[0.4em] mb-12 flex items-center gap-3">
|
|
<Layers size={16} /> Grounded Audit Trail & Reference Hub
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{data.summary?.sources?.map((source, idx) => (
|
|
<a
|
|
key={idx}
|
|
href={source.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="flex items-center justify-between p-6 bg-white rounded-[1.5rem] text-[10px] font-black hover:bg-black hover:text-white transition-all group shadow-sm border border-gray-100 uppercase tracking-tighter hover:scale-[1.02]"
|
|
>
|
|
<span className="truncate max-w-[220px]">{source.title}</span>
|
|
<ArrowUpRight size={16} className="text-[#FFD700] group-hover:translate-x-1 transition-transform" />
|
|
</a>
|
|
))}
|
|
</div>
|
|
<div className="mt-12 pt-8 border-t border-gray-200/50 text-center">
|
|
<p className="text-[9px] font-black text-gray-300 uppercase tracking-[0.3em]">System processed via Gemini-3 High-Density Reasoning Platform</p>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</>
|
|
)}
|
|
</main>
|
|
|
|
{/* Persistent Controls */}
|
|
<div className="fixed bottom-10 left-10 group z-50 no-print">
|
|
<div className="bg-black text-white p-4 rounded-xl cursor-help hover:bg-[#FFD700] hover:text-black transition-colors shadow-2xl border border-white/10">
|
|
<Info size={24} />
|
|
</div>
|
|
<div className="absolute bottom-full mb-6 left-0 w-80 bg-white border-4 border-black p-8 rounded-3xl shadow-2xl invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all z-[200]">
|
|
<h4 className="text-sm font-black uppercase tracking-widest mb-4 border-b-2 border-[#FFD700] pb-2 inline-block">Grounded Intelligence</h4>
|
|
<p className="text-[12px] leading-relaxed text-gray-600 font-bold">
|
|
The APAC Strategy & Insights Engine utilizes Gemini-3-Pro with Google Search grounding to synthesize real-time audits. Every metric is backed by live market data.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{data && (
|
|
<div className="fixed bottom-10 right-10 flex flex-col items-end gap-3 z-[60] no-print">
|
|
<button
|
|
onClick={handleExportHtml}
|
|
className="bg-black text-white hover:bg-gray-800 px-6 py-3 rounded-xl shadow-xl flex items-center gap-3 font-bold uppercase tracking-widest text-[10px] transition-all border border-white/20 active:scale-95 w-48"
|
|
>
|
|
<FileDown size={18} className="text-[#FFD700]" />
|
|
<span>Export HTML</span>
|
|
</button>
|
|
<button
|
|
onClick={() => window.print()}
|
|
className="bg-[#FFD700] text-black hover:bg-black hover:text-white px-6 py-3 rounded-xl shadow-2xl flex items-center gap-4 font-black uppercase tracking-widest text-[10px] transition-all border-4 border-black active:scale-95 w-48"
|
|
>
|
|
<FileDown size={18} />
|
|
<span>Export PDF</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Side Accents */}
|
|
<div className="fixed top-0 left-0 w-1.5 h-full bg-[#FFD700] pointer-events-none opacity-10 z-10 no-print" />
|
|
<div className="fixed top-0 right-0 w-1.5 h-full bg-black pointer-events-none opacity-5 z-10 no-print" />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App;
|