feat: Revamped design - improved dark mode visibility, mobile UX fixes, updated trust signals
This commit is contained in:
+433
-125
@@ -1,197 +1,453 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Icons } from './landing/constants';
|
||||
import MapBg from '../assets/images/hyper_intelligence_bg_image.jpg';
|
||||
import MapBg from '../assets/images/intel_map_dfw_twilight.png';
|
||||
import IntelligenceSidePanel from './dashboard/IntelligenceSidePanel';
|
||||
import gsap from 'gsap';
|
||||
import { ScrollTrigger } from 'gsap/ScrollTrigger';
|
||||
|
||||
// Updated with Residential & Commercial mix
|
||||
gsap.registerPlugin(ScrollTrigger);
|
||||
|
||||
// ── 10 Hotspots — 5 Commercial + 5 Residential, DFW Metro ──
|
||||
const hotspots = [
|
||||
// Commercial
|
||||
{
|
||||
id: 1,
|
||||
top: '45%',
|
||||
left: '65%',
|
||||
address: 'Preston Royal Village',
|
||||
id: 1, top: '22%', left: '58%',
|
||||
address: '8333 Douglas Ave, Dallas',
|
||||
type: 'Commercial',
|
||||
problem: 'Roof Membrane Uplift',
|
||||
gain: '+$8.5M Value',
|
||||
gain: '+$8.5M',
|
||||
weather: 'Hail Expected',
|
||||
status: 'Urgent',
|
||||
specs: { built: '1998', maintenance: '5 years since last coating' },
|
||||
specs: { built: '1998', maintenance: '5 yrs since last coating' },
|
||||
management: { name: 'Sterling Properties', contact: 'Sign in for contact details' },
|
||||
photos: [
|
||||
{ id: 1, url: '/assets/images/properties/Warehouse_Roof_Aerial.jpg', caption: 'Warehouse Roof Aerial' },
|
||||
{ id: 2, url: '/assets/images/properties/Office_Roof_Top_View.jpg', caption: 'Office Roof Top' }
|
||||
{ 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: '28%',
|
||||
left: '42%',
|
||||
address: 'Highland Park Place',
|
||||
id: 2, top: '35%', left: '38%',
|
||||
address: '5600 Tennyson Pkwy, Plano',
|
||||
type: 'Commercial',
|
||||
problem: 'Critical Membrane Failure',
|
||||
gain: '$22.5 Million',
|
||||
gain: '+$22.5M',
|
||||
weather: 'Clear',
|
||||
status: 'Monitoring',
|
||||
specs: { built: '1982', maintenance: '20 years since last roof replacement permit' },
|
||||
specs: { built: '1982', maintenance: '20 yrs since roof replacement' },
|
||||
management: { name: 'Goldman Group', contact: 'Sign in for contact details' },
|
||||
photos: [
|
||||
{ id: 3, url: '/assets/images/properties/Mall_Rooftop_HVAC_Units.jpg', caption: 'Rooftop HVAC Units' }
|
||||
{ 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: '55%',
|
||||
left: '25%',
|
||||
address: '4200 Beverly Dr',
|
||||
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',
|
||||
gain: '+$1.1M',
|
||||
weather: 'Rain Soon',
|
||||
status: 'Action Required',
|
||||
specs: { roofSize: '62 sq Roof', eventHistory: '8 years since last known hail event' },
|
||||
specs: { roofSize: '62 sq', eventHistory: '8 yrs since last hail event' },
|
||||
owner: { fico: '715', profile: 'Early Adopter / Long-term value' },
|
||||
photos: [
|
||||
{ id: 4, url: '/assets/images/properties/Hail_Damaged_Shingles.jpg', caption: 'Hail Damage Close-up' },
|
||||
{ id: 5, url: '/assets/images/properties/Broken_Roof_Surface.jpg', caption: 'Broken Surface' }
|
||||
{ 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: 4,
|
||||
top: '70%',
|
||||
left: '48%',
|
||||
address: '3505 Lexington Ave',
|
||||
id: 7, top: '72%', left: '65%',
|
||||
address: '3505 Lexington Ave, University Park',
|
||||
type: 'Residential',
|
||||
problem: 'Asphalt Shingle Loss',
|
||||
gain: '+$950k Home',
|
||||
gain: '+$950K',
|
||||
weather: 'Windy',
|
||||
status: 'Low Priority',
|
||||
specs: { roofSize: '45 sq Roof', eventHistory: 'High Wind Zone' },
|
||||
specs: { roofSize: '45 sq', eventHistory: 'High Wind Zone' },
|
||||
owner: { fico: '680', profile: 'First-time Buyer' },
|
||||
photos: [
|
||||
{ id: 6, url: '/assets/images/properties/Cracked_Asphalt_Shingles.jpg', caption: 'Cracked Shingles' },
|
||||
{ id: 7, url: '/assets/images/properties/Curled_Aging_Shingles.jpg', caption: 'Aging Shingles' }
|
||||
{ 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(() => {
|
||||
// Simulate incoming weather alert
|
||||
const timer = setTimeout(() => {
|
||||
setShowRescheduleDialog(true);
|
||||
}, 2500);
|
||||
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 className="py-24 bg-gray-900 border-t border-white/5 relative overflow-hidden">
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="mb-12 text-center max-w-4xl mx-auto">
|
||||
<div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-cyan-400/10 border border-cyan-400/20 mb-4">
|
||||
<div className="w-1.5 h-1.5 bg-cyan-400 rounded-full animate-pulse"></div>
|
||||
<span className="text-cyan-400 text-xs font-mono font-bold uppercase tracking-widest">Live CRM Data Feed</span>
|
||||
<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-heading font-bold text-white mb-6">Hyper-Local Sales Intelligence</h2>
|
||||
<p className="text-gray-400 text-lg max-w-2xl mx-auto">
|
||||
<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>
|
||||
|
||||
{/* Map Container Frame */}
|
||||
<div className="relative w-full h-[650px] bg-black rounded-2xl border border-white/20 shadow-2xl overflow-hidden group isolate ring-1 ring-white/10">
|
||||
{/* ── 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 Background - DFW Satellite Style */}
|
||||
{/* ══════════════════════════════════════════════ */}
|
||||
{/* 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]">
|
||||
{/* Aerial/Satellite View of a Grid City */}
|
||||
<img
|
||||
src={MapBg}
|
||||
alt="DFW Metroplex Map Overlay"
|
||||
className="w-full h-full object-cover opacity-60 mix-blend-luminosity hover:scale-105 transition-transform duration-[20s]"
|
||||
className="w-full h-full object-cover opacity-60 mix-blend-luminosity"
|
||||
/>
|
||||
|
||||
{/* Map Grid Overlay */}
|
||||
<div className="absolute inset-0 bg-[url('https://grainy-gradients.vercel.app/noise.svg')] opacity-20"></div>
|
||||
<div className="absolute inset-0 bg-grid-pattern opacity-10 bg-[length:40px_40px] border-t border-white/5"></div>
|
||||
</div>
|
||||
|
||||
{/* Region Indicator - HUD Style */}
|
||||
<div className="absolute top-6 left-6 z-30">
|
||||
<div className="bg-slate-900/80 backdrop-blur-md text-white pl-4 pr-6 py-2 rounded-r-lg border-l-4 border-cyan-400 shadow-[0_0_15px_rgba(0,229,255,0.2)] flex items-center gap-3">
|
||||
<div>
|
||||
<div className="text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-0.5">Active Region</div>
|
||||
<div className="font-heading font-bold text-lg leading-none">DFW Zone 2</div>
|
||||
</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 Overlay 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-6 right-6 p-4 bg-black/60 backdrop-blur-md rounded-lg border border-white/10 max-w-xs z-30">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="bg-white/10 p-2 rounded-full text-white">
|
||||
<span className="text-2xl">⛈</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-white text-sm">Severe Hail Watch</div>
|
||||
<div className="text-xs text-cyan-400 animate-pulse">Approaching Tarrant County</div>
|
||||
<div className="text-[10px] text-gray-500 font-mono mt-1">ETA: 15 MINS</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) => (
|
||||
<div
|
||||
key={spot.id}
|
||||
className="absolute group/pin z-20 cursor-pointer"
|
||||
style={{ top: spot.top, left: spot.left }}
|
||||
onClick={() => setActiveSpot(spot)}
|
||||
>
|
||||
{/* Radar Ripple Effect */}
|
||||
<div className={`absolute -inset-4 rounded-full animate-ping opacity-75 ${spot.type === 'Commercial' ? 'bg-purple-500/20' : 'bg-cyan-400/20'}`}></div>
|
||||
{/* ── Interactive Pins ── */}
|
||||
{hotspots.map((spot, index) => {
|
||||
const visible = isSpotVisible(spot, activeFilter);
|
||||
const isDimmed = activeSpot && activeSpot.id !== spot.id;
|
||||
const isCommercial = spot.type === 'Commercial';
|
||||
|
||||
{/* Price Tag Pin */}
|
||||
<div className="relative flex flex-col items-center transform group-hover/pin:scale-110 transition-transform duration-200">
|
||||
<div className={`bg-slate-900/90 backdrop-blur text-[10px] font-mono font-bold px-2 py-1 rounded border shadow-[0_0_10px_rgba(0,0,0,0.5)] whitespace-nowrap mb-1 opacity-0 group-hover/pin:opacity-100 transition-opacity ${spot.type === 'Commercial' ? 'text-purple-400 border-purple-400/30' : 'text-cyan-400 border-cyan-400/30'}`}>
|
||||
{spot.gain}
|
||||
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" />
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* Pin Head */}
|
||||
<div className={`w-3 h-3 rounded-full border-2 border-white shadow-lg relative z-10 ${spot.type === 'Commercial' ? '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)]'}`}></div>
|
||||
|
||||
{/* Pin Line */}
|
||||
<div className="w-[1px] h-8 bg-gradient-to-b from-white to-transparent opacity-50"></div>
|
||||
|
||||
{/* Ground Shadow */}
|
||||
<div className="absolute bottom-0 w-6 h-1.5 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>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Reschedule Dialog Overlay */}
|
||||
{/* Reschedule Dialog */}
|
||||
{showRescheduleDialog && (
|
||||
<div className="absolute bottom-8 left-1/2 transform -translate-x-1/2 md:left-8 md:translate-x-0 z-50 w-[90%] md:w-96">
|
||||
<div className="bg-white/95 backdrop-blur-xl rounded-xl shadow-2xl p-6 border-l-4 border-red-600 animate-[slideUp_0.5s_ease-out]">
|
||||
<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>
|
||||
<div>
|
||||
<h4 className="font-bold text-slate-900 text-sm uppercase tracking-wide">Schedule Conflict</h4>
|
||||
</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">✕</button>
|
||||
<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 />
|
||||
<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
|
||||
@@ -204,29 +460,81 @@ const IntelligenceMap = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right Sidebar Panel (Glass Detail View) */}
|
||||
{/* Desktop Side Panel */}
|
||||
{activeSpot && (
|
||||
<IntelligenceSidePanel
|
||||
data={activeSpot}
|
||||
onClose={() => setActiveSpot(null)}
|
||||
/>
|
||||
<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>
|
||||
@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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user