Files
LynkedUpPro_CRM/src/components/IntelligenceMap.jsx
T

548 lines
30 KiB
React

import React, { useState, useEffect, useRef } from 'react';
import ReactDOM from 'react-dom';
import { useNavigate } from 'react-router-dom';
import MapBg from '../assets/images/intel_map_dfw_twilight.webp';
import IntelligenceSidePanel from './dashboard/IntelligenceSidePanel';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
// ── 10 Hotspots — 5 Commercial + 5 Residential, DFW Metro ──
const hotspots = [
// Commercial
{
id: 1, top: '22%', left: '58%',
address: '8333 Douglas Ave, Dallas',
type: 'Commercial',
problem: 'Roof Membrane Uplift',
gain: '+$8.5M',
weather: 'Hail Expected',
status: 'Urgent',
specs: { built: '1998', maintenance: '5 yrs since last coating' },
management: { name: 'Sterling Properties', contact: 'Sign in for contact details' },
photos: [
{ id: 'c1-1', url: '/assets/images/properties/Apartment_Complex.jpg', caption: 'Exterior View' },
{ id: 'c1-2', url: '/assets/images/properties/Office_Roof_Top_View.jpg', caption: 'Roof Aerial' },
{ id: 'c1-3', url: '/assets/images/properties/Broken_Roof_Surface.jpg', caption: 'Membrane Damage' },
]
},
{
id: 2, top: '35%', left: '38%',
address: '5600 Tennyson Pkwy, Plano',
type: 'Commercial',
problem: 'Critical Membrane Failure',
gain: '+$22.5M',
weather: 'Clear',
status: 'Monitoring',
specs: { built: '1982', maintenance: '20 yrs since roof replacement' },
management: { name: 'Goldman Group', contact: 'Sign in for contact details' },
photos: [
{ id: 'c2-1', url: '/assets/images/properties/Warehouse_Roof_Aerial.jpg', caption: 'Aerial Survey' },
{ id: 'c2-2', url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'HVAC Systems' },
{ id: 'c2-3', url: '/assets/images/properties/Storm_Worn_Roof.jpg', caption: 'Surface Wear' },
{ id: 'c2-4', url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Substrate Cracking' },
]
},
{
id: 3, top: '48%', left: '72%',
address: '1900 Preston Rd, Frisco',
type: 'Commercial',
problem: 'Ponding Water Accumulation',
gain: '+$4.2M',
weather: 'Storm Watch',
status: 'Action Required',
specs: { built: '2005', maintenance: '3 yrs since drainage check' },
management: { name: 'Hillcrest Capital', contact: 'Sign in for contact details' },
photos: [
{ id: 'c3-1', url: '/assets/images/properties/Apartment_Complex.jpg', caption: 'Building Exterior' },
{ id: 'c3-2', url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'Rooftop Overview' },
{ id: 'c3-3', url: '/assets/images/properties/Broken_Roof_Surface.jpg', caption: 'Ponding Damage' },
]
},
{
id: 4, top: '62%', left: '28%',
address: '2100 N Hwy 360, Grand Prairie',
type: 'Commercial',
problem: 'HVAC Penetration Leak',
gain: '+$6.8M',
weather: 'Windy',
status: 'Monitoring',
specs: { built: '1995', maintenance: '8 yrs since waterproofing' },
management: { name: 'Apex Industrial Partners', contact: 'Sign in for contact details' },
photos: [
{ id: 'c4-1', url: '/assets/images/properties/Warehouse_Roof_Aerial.jpg', caption: 'Facility Aerial' },
{ id: 'c4-2', url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'HVAC Penetration' },
{ id: 'c4-3', url: '/assets/images/properties/Cracked_Storm_Shingles.jpg', caption: 'Seal Failure' },
]
},
{
id: 5, top: '15%', left: '20%',
address: '401 Commerce St, Fort Worth',
type: 'Commercial',
problem: 'Flashing Deterioration',
gain: '+$11.3M',
weather: 'Hail Expected',
status: 'Urgent',
specs: { built: '1990', maintenance: '12 yrs since seal replacement' },
management: { name: 'Tarrant Realty Holdings', contact: 'Sign in for contact details' },
photos: [
{ id: 'c5-1', url: '/assets/images/properties/Office_Roof_Top_View.jpg', caption: 'Roof Survey' },
{ id: 'c5-2', url: '/assets/images/properties/Apartment_Complex.jpg', caption: 'Building Facade' },
{ id: 'c5-3', url: '/assets/images/properties/Curled_Aging_Shingles.jpg', caption: 'Flashing Wear' },
{ id: 'c5-4', url: '/assets/images/properties/Storm_Worn_Roof.jpg', caption: 'Wind Exposure' },
]
},
// Residential
{
id: 6, top: '55%', left: '50%',
address: '4200 Beverly Dr, Highland Park',
type: 'Residential',
problem: 'Severe Hail Impact',
gain: '+$1.1M',
weather: 'Rain Soon',
status: 'Action Required',
specs: { roofSize: '62 sq', eventHistory: '8 yrs since last hail event' },
owner: { fico: '715', profile: 'Early Adopter / Long-term value' },
photos: [
{ id: 'r6-1', url: '/assets/images/properties/Luxury_Driveway_Residence.jpg', caption: 'Front Exterior' },
{ id: 'r6-2', url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', caption: 'Hail Damage' },
{ id: 'r6-3', url: '/assets/images/properties/Cracked_Storm_Shingles.jpg', caption: 'Storm Damage' },
]
},
{
id: 7, top: '72%', left: '65%',
address: '3505 Lexington Ave, University Park',
type: 'Residential',
problem: 'Asphalt Shingle Loss',
gain: '+$950K',
weather: 'Windy',
status: 'Low Priority',
specs: { roofSize: '45 sq', eventHistory: 'High Wind Zone' },
owner: { fico: '680', profile: 'First-time Buyer' },
photos: [
{ id: 'r7-1', url: '/assets/images/properties/Modern_White_Suburban_Home.jpg', caption: 'Front View' },
{ id: 'r7-2', url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Shingle Loss' },
{ id: 'r7-3', url: '/assets/images/properties/Roof_Inspection_Angle.jpg', caption: 'Drone Inspection' },
]
},
{
id: 8, top: '40%', left: '15%',
address: '7212 Covewood Dr, Garland',
type: 'Residential',
problem: 'Aging Composite Shingles',
gain: '+$420K',
weather: 'Clear',
status: 'Low Priority',
specs: { roofSize: '38 sq', eventHistory: 'No recent events' },
owner: { fico: '740', profile: 'Long-term Owner' },
photos: [
{ id: 'r8-1', url: '/assets/images/properties/Beige_Two_Story_House.jpg', caption: 'Street View' },
{ id: 'r8-2', url: '/assets/images/properties/Curled_Aging_Shingles.jpg', caption: 'Aging Shingles' },
]
},
{
id: 9, top: '75%', left: '42%',
address: '1520 Creekside Dr, Allen',
type: 'Residential',
problem: 'Ridge Cap Separation',
gain: '+$780K',
weather: 'Hail Expected',
status: 'Urgent',
specs: { roofSize: '55 sq', eventHistory: '2 hail events in 3 years' },
owner: { fico: '695', profile: 'Insurance-motivated' },
photos: [
{ id: 'r9-1', url: '/assets/images/properties/Brick_Front_Porch_Home.jpg', caption: 'Entryway' },
{ id: 'r9-2', url: '/assets/images/properties/Overlapping_Roof_Shingles.jpg', caption: 'Ridge Detail' },
{ id: 'r9-3', url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', caption: 'Hail Damage' },
{ id: 'r9-4', url: '/assets/images/properties/Storm_Worn_Roof.jpg', caption: 'Worn Surface' },
]
},
{
id: 10, top: '30%', left: '82%',
address: '905 W Main St, Richardson',
type: 'Residential',
problem: 'Granule Loss Pattern',
gain: '+$560K',
weather: 'Storm Watch',
status: 'Action Required',
specs: { roofSize: '42 sq', eventHistory: '5 yrs since installation' },
owner: { fico: '720', profile: 'Recently Purchased' },
photos: [
{ id: 'r10-1', url: '/assets/images/properties/Red_Shingle_Cottage.jpg', caption: 'Cottage Exterior' },
{ id: 'r10-2', url: '/assets/images/properties/Residential_Roof_Texture.jpg', caption: 'Roof Texture' },
{ id: 'r10-3', url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Granule Loss' },
]
},
];
const FILTERS = ['All', 'Commercial', 'Residential', 'Storm Path'];
const STORM_WEATHER = ['Hail Expected', 'Storm Watch', 'Rain Soon'];
const metrics = [
{ label: 'Total Pipeline', value: 57.1, prefix: '$', suffix: 'M', color: 'text-blue-500', stamp: '\u25CF Active', stampClass: 'permit-stamp--accent' },
{ label: 'Active Alerts', value: 7, prefix: '', suffix: '', color: 'text-amber-500', stamp: '\u25B2 Monitoring', stampClass: 'permit-stamp--accent' },
{ label: 'Urgent', value: 3, prefix: '', suffix: '', color: 'text-red-500', stamp: '\u2715 Critical', stampClass: 'permit-stamp--danger' },
{ label: 'Avg Roof Age', value: 18, prefix: '', suffix: ' yrs', color: 'text-emerald-500', stamp: '\u2713 Assessed', stampClass: 'permit-stamp--verified' },
];
const isSpotVisible = (spot, filter) => {
if (filter === 'All') return true;
if (filter === 'Commercial') return spot.type === 'Commercial';
if (filter === 'Residential') return spot.type === 'Residential';
if (filter === 'Storm Path') return STORM_WEATHER.includes(spot.weather);
return true;
};
const IntelligenceMap = () => {
const navigate = useNavigate();
const [activeSpot, setActiveSpot] = useState(null);
const [activeFilter, setActiveFilter] = useState('All');
const [hoveredSpot, setHoveredSpot] = useState(null);
const [showRescheduleDialog, setShowRescheduleDialog] = useState(false);
const sectionRef = useRef(null);
const pinRefs = useRef([]);
const metricRefs = useRef([]);
const mapGlowRef = useRef(null);
// Reschedule dialog timer
useEffect(() => {
const timer = setTimeout(() => setShowRescheduleDialog(true), 2500);
return () => clearTimeout(timer);
}, []);
// GSAP: Staggered pin entrance + metric counters
useEffect(() => {
const ctx = gsap.context(() => {
// Pin entrance
const pins = pinRefs.current.filter(Boolean);
pins.forEach(pin => gsap.set(pin, { opacity: 0, scale: 0, y: 20 }));
gsap.to(pins, {
opacity: 1, scale: 1, y: 0,
duration: 0.6, stagger: 0.08,
ease: 'back.out(1.7)',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 70%',
toggleActions: 'play none none none',
},
onComplete: () => {
// Clear GSAP inline styles so CSS transitions take over
pins.forEach(pin => {
if (pin) gsap.set(pin, { clearProps: 'opacity,transform' });
});
},
});
// Metric counters — proxy object approach
metricRefs.current.forEach((el) => {
if (!el) return;
const target = parseFloat(el.dataset.target);
const suffix = el.dataset.suffix || '';
const prefix = el.dataset.prefix || '';
const counter = { value: 0 };
el.innerText = prefix + '0' + suffix;
gsap.to(counter, {
value: target,
duration: 2,
ease: 'power1.out',
scrollTrigger: {
trigger: el,
start: 'top 85%',
toggleActions: 'play none none none',
},
onUpdate: () => {
const display = target % 1 !== 0
? counter.value.toFixed(1)
: Math.ceil(counter.value).toString();
el.innerText = prefix + display + suffix;
},
});
});
}, sectionRef);
return () => ctx.revert();
}, []);
// Mouse-follow glow on map
const handleMapMouseMove = (e) => {
if (!mapGlowRef.current) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
mapGlowRef.current.style.background =
`radial-gradient(350px circle at ${x}px ${y}px, rgba(6,182,212,0.08), transparent 60%)`;
};
const handleMapMouseLeave = () => {
if (mapGlowRef.current) mapGlowRef.current.style.background = 'transparent';
};
return (
<section
ref={sectionRef}
className="py-24 bg-zinc-100 dark:bg-zinc-900 border-t border-zinc-200 dark:border-white/5 relative overflow-hidden"
>
{/* Blueprint grid bg */}
<div className="absolute inset-0 blueprint-grid-fine"></div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
{/* ── Header ── */}
<div className="mb-8 text-center max-w-4xl mx-auto">
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-cyan-500/10 dark:bg-cyan-400/10 border border-cyan-500/20 dark:border-cyan-400/20 mb-4">
<div className="w-1.5 h-1.5 bg-cyan-500 dark:bg-cyan-400 rounded-full animate-pulse"></div>
<span className="text-cyan-600 dark:text-cyan-400 text-xs font-mono font-bold uppercase tracking-widest">Live CRM Data Feed</span>
</div>
<h2 className="text-3xl md:text-5xl font-black text-zinc-900 dark:text-white mb-4 tracking-tight">
Hyper-Local Sales Intelligence
</h2>
<p className="text-zinc-500 dark:text-zinc-400 text-lg max-w-2xl mx-auto">
Our systems overlay live NOAA weather data with property value insights. Maximize revenue by targeting neighborhoods directly in the storm path.
</p>
</div>
{/* ── Filter Bar ── */}
<div className="flex flex-wrap items-center justify-center gap-2 sm:gap-3 mb-8">
{FILTERS.map((filter) => (
<button
key={filter}
onClick={() => { setActiveFilter(filter); setActiveSpot(null); }}
className={`font-mono text-[10px] font-bold uppercase tracking-widest px-4 py-2 rounded-md border transition-all duration-200
${activeFilter === filter
? 'bg-cyan-500/10 dark:bg-cyan-400/10 text-cyan-600 dark:text-cyan-400 border-cyan-500/30 dark:border-cyan-400/30'
: 'bg-white dark:bg-white/5 text-zinc-500 dark:text-gray-500 border-zinc-200 dark:border-white/10 hover:text-zinc-900 dark:hover:text-white hover:border-zinc-400 dark:hover:border-white/20'
}
`}
>
{filter}
</button>
))}
</div>
{/* ══════════════════════════════════════════════ */}
{/* MAP CONTAINER — always dark (satellite image) */}
{/* ══════════════════════════════════════════════ */}
<div
className="relative w-full h-[400px] md:h-[550px] lg:h-[650px] bg-black rounded-2xl border border-zinc-300 dark:border-white/20 shadow-2xl overflow-hidden isolate ring-1 ring-zinc-200 dark:ring-white/10"
onMouseMove={handleMapMouseMove}
onMouseLeave={handleMapMouseLeave}
>
{/* Map Background */}
<div className="absolute inset-0 bg-[#0f172a]">
<img
src={MapBg}
alt="DFW Metroplex Map Overlay"
className="w-full h-full object-cover opacity-60 mix-blend-luminosity"
/>
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20"></div>
</div>
{/* Mouse-follow glow */}
<div
ref={mapGlowRef}
className="absolute inset-0 pointer-events-none z-10 transition-[background] duration-150"
/>
{/* Region Indicator — HUD */}
<div className="absolute top-4 left-4 z-30">
<div className="bg-slate-900/80 backdrop-blur-md text-white pl-3 pr-4 py-1.5 md:pl-4 md:pr-6 md:py-2 rounded-r-lg border-l-4 border-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.2)]">
<div className="text-[9px] md:text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-0.5">Active Region</div>
<div className="font-black text-sm md:text-lg leading-none">DFW Zone 2</div>
</div>
</div>
{/* Weather Widget */}
<div className="absolute top-0 right-0 w-full h-full bg-gradient-to-bl from-slate-900/80 via-transparent to-transparent pointer-events-none"></div>
<div className="absolute top-4 right-4 p-3 md:p-4 bg-black/60 backdrop-blur-md rounded-lg border border-white/10 max-w-[180px] md:max-w-xs z-30 hidden sm:block">
<div className="flex items-center gap-3 md:gap-4">
<div className="bg-white/10 p-1.5 md:p-2 rounded-full text-white">
<span className="text-xl md:text-2xl">{'\u26C8'}</span>
</div>
<div>
<div className="font-bold text-white text-xs md:text-sm">Severe Hail Watch</div>
<div className="text-[10px] md:text-xs text-cyan-400 animate-pulse">Approaching Tarrant County</div>
<div className="text-[9px] md:text-[10px] text-gray-500 font-mono mt-1">ETA: 15 MINS</div>
</div>
</div>
</div>
{/* ── Interactive Pins ── */}
{hotspots.map((spot, index) => {
const visible = isSpotVisible(spot, activeFilter);
const isDimmed = activeSpot && activeSpot.id !== spot.id;
const isCommercial = spot.type === 'Commercial';
return (
<div
key={spot.id}
ref={(el) => { pinRefs.current[index] = el; }}
className={`absolute group/pin z-20 cursor-pointer transition-all duration-300 ease-in-out
${!visible ? 'opacity-0 scale-0 pointer-events-none' : ''}
${visible && isDimmed ? 'opacity-30' : ''}
`}
style={{ top: spot.top, left: spot.left }}
onClick={() => setActiveSpot(activeSpot?.id === spot.id ? null : spot)}
onMouseEnter={() => setHoveredSpot(spot.id)}
onMouseLeave={() => setHoveredSpot(null)}
>
{/* Larger invisible touch target */}
<div className="absolute -inset-4 md:-inset-2" />
{/* Urgent Status Alert Pulse */}
{visible && spot.status === 'Urgent' && (
<div className="absolute -inset-8 rounded-full bg-red-500/30 animate-ping" style={{ animationDuration: '1.5s' }} />
)}
{/* Radar Ripple */}
{visible && (
<div className={`absolute ${isCommercial ? '-inset-5' : '-inset-4'} rounded-full animate-ping opacity-75 ${isCommercial ? 'bg-purple-500/20' : 'bg-cyan-400/20'
}`} />
)}
{/* Selected glow ring */}
{activeSpot?.id === spot.id && (
<div className={`absolute -inset-6 rounded-full blur-md animate-pulse ${isCommercial ? 'bg-purple-500/25' : 'bg-cyan-400/25'
}`} />
)}
{/* Pin cluster */}
<div className="relative flex flex-col items-center">
{/* Pin Head */}
<div className={`${isCommercial ? 'w-4 h-4' : 'w-3 h-3'} rounded-full border-2 border-white shadow-lg relative z-10 transition-transform duration-200 group-hover/pin:scale-125 ${isCommercial
? 'bg-purple-500 shadow-[0_0_15px_rgba(168,85,247,0.6)]'
: 'bg-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.6)]'
}`} />
{/* Pin Line */}
<div className="w-[1px] h-6 md:h-8 bg-gradient-to-b from-white to-transparent opacity-50" />
{/* Ground Shadow */}
<div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-5 h-1 bg-black/60 blur-sm rounded-[100%] translate-y-1" />
</div>
{/* Hover Tooltip */}
<div className={`absolute bottom-full left-1/2 -translate-x-1/2 mb-3 bg-slate-900/90 backdrop-blur-md text-[10px] font-mono px-3 py-2 rounded-lg border border-white/10 shadow-lg whitespace-nowrap pointer-events-none transition-all duration-200 z-30
${hoveredSpot === spot.id && activeSpot?.id !== spot.id ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-1'}
`}>
<div className="font-bold text-white text-[11px]">{spot.address}</div>
<div className={`font-bold mt-0.5 ${isCommercial ? 'text-purple-400' : 'text-cyan-400'}`}>
{spot.gain}
</div>
<div className="absolute top-full left-1/2 -translate-x-1/2 w-2 h-2 bg-slate-900/90 border-r border-b border-white/10 rotate-45 -mt-1" />
</div>
</div>
);
})}
{/* Reschedule Dialog */}
{showRescheduleDialog && (
<div className="absolute bottom-6 left-1/2 transform -translate-x-1/2 md:left-6 md:translate-x-0 z-50 w-[90%] md:w-96">
<div className="bg-white/95 backdrop-blur-xl rounded-xl shadow-2xl p-5 md:p-6 border-l-4 border-red-600 animate-[slideUp_0.5s_ease-out]">
<div className="flex justify-between items-start mb-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center text-red-600 font-bold text-lg">!</div>
<h4 className="font-bold text-slate-900 text-sm uppercase tracking-wide">Schedule Conflict</h4>
</div>
<button onClick={() => setShowRescheduleDialog(false)} className="text-gray-400 hover:text-black">{'\u2715'}</button>
</div>
<p className="text-gray-600 text-sm mb-4 leading-relaxed">
<strong>Heavy rainfall expected in Plano at 2:00 PM.</strong><br />
This conflicts with the inspection at <span className="text-slate-900 font-semibold">Legacy West</span>.
</p>
<div className="flex gap-3">
<button onClick={() => navigate('/login')} className="flex-1 bg-slate-900 text-white text-xs font-bold py-2.5 rounded hover:bg-slate-800 transition-colors uppercase tracking-wider">
Reschedule
</button>
<button onClick={() => setShowRescheduleDialog(false)} className="flex-1 bg-gray-100 text-gray-600 text-xs font-bold py-2.5 rounded hover:bg-gray-200 transition-colors uppercase tracking-wider">
Dismiss
</button>
</div>
</div>
</div>
)}
{/* Desktop Side Panel */}
{activeSpot && (
<div className="hidden md:block">
<IntelligenceSidePanel
data={activeSpot}
onClose={() => setActiveSpot(null)}
/>
</div>
)}
</div>
{/* ── Mobile Bottom Sheet ── */}
{activeSpot && ReactDOM.createPortal(
<div className="md:hidden fixed inset-0 z-[9999]">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => setActiveSpot(null)} />
<div data-lenis-prevent className="absolute bottom-0 left-0 right-0 max-h-[80vh] bg-white dark:bg-zinc-900 rounded-t-2xl border-t border-zinc-200 dark:border-white/10 overflow-y-auto overscroll-contain animate-[slideUpSheet_0.3s_ease-out] shadow-2xl touch-pan-y">
{/* Sheet header */}
<div className="sticky top-0 bg-white dark:bg-zinc-900 px-6 pt-4 pb-2 border-b border-zinc-100 dark:border-white/5 z-10">
<div className="w-10 h-1 bg-zinc-300 dark:bg-white/20 rounded-full mx-auto mb-3" />
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className={`w-2 h-2 rounded-full ${activeSpot.type === 'Commercial' ? 'bg-purple-500' : 'bg-cyan-400'}`} />
<h3 className="font-bold text-zinc-900 dark:text-white text-sm">{activeSpot.address}</h3>
</div>
<button onClick={() => setActiveSpot(null)} className="text-zinc-400 hover:text-zinc-900 dark:hover:text-white p-1">{'\u2715'}</button>
</div>
</div>
<IntelligenceSidePanel
data={activeSpot}
onClose={() => setActiveSpot(null)}
variant="sheet"
/>
</div>
</div>,
document.body
)}
{/* ── Aggregate Metrics ── */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-8">
{metrics.map((m, i) => (
<div key={i} className="bg-white dark:bg-white/5 p-5 rounded-xl border border-zinc-200 dark:border-white/10 relative overflow-hidden hover:-translate-y-1 transition-transform duration-200">
<div className="measure-ticks measure-ticks-top absolute top-0 left-3 right-3 h-2.5"></div>
<div className="text-[10px] font-mono font-bold uppercase tracking-widest text-zinc-400 dark:text-gray-500 mb-1">
{m.label}
</div>
<div
ref={(el) => { metricRefs.current[i] = el; }}
data-target={m.value}
data-suffix={m.suffix}
data-prefix={m.prefix}
className={`text-3xl font-mono font-bold ${m.color} tracking-tight`}
>
{m.prefix}0{m.suffix}
</div>
<div className="mt-3 pt-2 border-t border-zinc-100 dark:border-white/5">
<span className={`permit-stamp ${m.stampClass}`}>{m.stamp}</span>
</div>
<div className="measure-ticks absolute bottom-0 left-3 right-3 h-2.5"></div>
</div>
))}
</div>
</div>
<style>{`
@keyframes slideUp {
from { opacity: 0; transform: translate(-50%, 20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@media (min-width: 768px) {
@keyframes slideUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
}
`}</style>
</section>
);
};
export default IntelligenceMap;