mobile: Optimizations for Maps Drawer, Legend, and Customer Profile Tabs
This commit is contained in:
@@ -39,7 +39,7 @@ GUIDELINES:
|
||||
`;
|
||||
|
||||
const Chatbot = () => {
|
||||
const { user } = useAuth();
|
||||
const { user, isAuthenticated } = useAuth();
|
||||
const { addMeeting, meetings, properties } = useMockStore();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMinimized, setIsMinimized] = useState(false);
|
||||
|
||||
@@ -56,7 +56,10 @@ export const ComparisonSlider = () => {
|
||||
"15-Minute Drone Scan",
|
||||
"Zero Safety Risk",
|
||||
"AI + Thermal Imaging",
|
||||
"~30% Lower Costs"
|
||||
"~30% Lower Costs",
|
||||
"~ 60% faster lead conversion",
|
||||
"~ Deploy Reps with confidence",
|
||||
"~ Evidence-driven damage detection unlocks Revenue"
|
||||
].map((text, i) => (
|
||||
<div key={i} className="flex items-center justify-end space-x-2 bg-black/40 backdrop-blur-sm px-3 py-1 rounded-lg border border-blue-500/20">
|
||||
<span className="text-sm md:text-base font-bold text-white">{text}</span>
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Icons } from './landing/constants';
|
||||
|
||||
// Updated with DFW specific locations (roughly positioned for a conceptual map)
|
||||
const hotspots = [
|
||||
{ id: 1, top: '45%', left: '65%', address: '2800 Routh St, Dallas', problem: 'Hail Impact (Severe)', gain: '+$28k Opportunity', weather: 'Hail Expected', status: 'Action Required' },
|
||||
{ id: 2, top: '20%', left: '55%', address: 'Legacy West, Plano', problem: 'Thermal Leak Detected', gain: '+$145k Commercial', weather: 'Clear', status: 'Monitoring' },
|
||||
{ id: 3, top: '60%', left: '35%', address: 'Sundance Sq, Fort Worth', problem: 'Missing Shingles', gain: '+$15k Repair', weather: 'Rain Soon', status: 'Urgent' },
|
||||
];
|
||||
|
||||
const IntelligenceMap = () => {
|
||||
const [activeSpot, setActiveSpot] = useState(null);
|
||||
const [showRescheduleDialog, setShowRescheduleDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate incoming weather alert
|
||||
const timer = setTimeout(() => {
|
||||
setShowRescheduleDialog(true);
|
||||
}, 2500);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
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>
|
||||
</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">
|
||||
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">
|
||||
|
||||
{/* Map Background - DFW Satellite Style */}
|
||||
<div className="absolute inset-0 bg-[#0f172a]">
|
||||
{/* Aerial/Satellite View of a Grid City */}
|
||||
<img
|
||||
src="https://images.unsplash.com/photo-1477959858617-67f85cf4f1df?q=80&w=2613&auto=format&fit=crop"
|
||||
alt="DFW Metroplex Map Overlay"
|
||||
className="w-full h-full object-cover opacity-50 mix-blend-luminosity hover:scale-105 transition-transform duration-[20s]"
|
||||
/>
|
||||
|
||||
{/* 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>
|
||||
|
||||
{/* 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">North Texas / DFW</div>
|
||||
</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>
|
||||
</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 bg-cyan-400/20 rounded-full animate-ping opacity-75"></div>
|
||||
|
||||
{/* 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-cyan-400 text-[10px] font-mono font-bold px-2 py-1 rounded border border-cyan-400/30 shadow-[0_0_10px_rgba(0,229,255,0.3)] whitespace-nowrap mb-1 opacity-0 group-hover/pin:opacity-100 transition-opacity">
|
||||
{spot.gain}
|
||||
</div>
|
||||
|
||||
{/* Pin Head */}
|
||||
<div className={`w-3 h-3 rounded-full border-2 border-white shadow-lg relative z-10 ${spot.problem.includes('Severe') ? 'bg-red-600 shadow-[0_0_15px_rgba(214,40,40,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>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Reschedule Dialog Overlay */}
|
||||
{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="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>
|
||||
</div>
|
||||
<button onClick={() => setShowRescheduleDialog(false)} className="text-gray-400 hover:text-black">✕</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 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>
|
||||
)}
|
||||
|
||||
{/* Right Sidebar Panel (Glass Detail View) */}
|
||||
{activeSpot && (
|
||||
<div className="absolute top-4 right-4 bottom-4 w-80 bg-slate-900/80 backdrop-blur-xl rounded-xl p-6 shadow-2xl border border-white/10 transform transition-transform duration-300 z-40 flex flex-col text-white">
|
||||
<div className="flex justify-between items-center mb-6 pb-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-cyan-400 rounded-full animate-pulse"></div>
|
||||
<h3 className="font-heading font-bold text-lg tracking-wide">INTEL_FEED</h3>
|
||||
</div>
|
||||
<button onClick={() => setActiveSpot(null)} className="text-gray-400 hover:text-white transition-colors">✕</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-grow space-y-6">
|
||||
<div className="p-4 bg-cyan-400/5 rounded-lg border border-cyan-400/20 relative overflow-hidden">
|
||||
<div className="absolute top-0 right-0 p-1 opacity-20">
|
||||
<Icons.Chart />
|
||||
</div>
|
||||
<label className="text-[10px] text-cyan-400 font-bold uppercase tracking-widest mb-1 block">Projected Value</label>
|
||||
<div className="text-3xl font-heading font-bold text-white">{activeSpot.gain}</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Location Target</label>
|
||||
<div className="text-white font-bold text-lg leading-tight mb-1">{activeSpot.address}</div>
|
||||
<div className="text-xs text-gray-400">Dallas-Fort Worth Metroplex</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-[10px] text-gray-500 font-bold uppercase tracking-widest mb-1 block">Detected Issue</label>
|
||||
<div className={`flex items-center gap-2 font-bold ${activeSpot.problem.includes('Severe') ? 'text-red-500' : 'text-yellow-400'}`}>
|
||||
<span>⚠</span> {activeSpot.problem}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 bg-white/5 rounded border border-white/10 text-xs text-gray-300 font-mono leading-relaxed">
|
||||
> ANALYSIS: 85% probability of insurance approval due to recent hail corridor overlap in zip code 75201.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full bg-cyan-400 hover:bg-white text-slate-900 font-bold py-3 rounded-lg transition-all duration-300 shadow-[0_0_20px_rgba(0,229,255,0.3)] hover:shadow-[0_0_30px_rgba(255,255,255,0.4)] uppercase tracking-widest text-xs">
|
||||
Dispatch Sales Rep
|
||||
</button>
|
||||
</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;
|
||||
+62
-10
@@ -1,14 +1,15 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Outlet, NavLink, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { useTheme } from '../context/ThemeContext';
|
||||
import { LayoutDashboard, Map, Calendar, LogOut, User, Home, MessageSquare, ChevronLeft, ChevronRight, Sun, Moon } from 'lucide-react';
|
||||
|
||||
import PageTransition from './PageTransition';
|
||||
import Chatbot from './Chatbot';
|
||||
import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png';
|
||||
|
||||
// Rainbow Sidebar Item Component
|
||||
const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
|
||||
const SidebarItem = ({ to, icon: Icon, label, isCollapsed, onClick }) => {
|
||||
const divRef = useRef(null);
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [opacity, setOpacity] = useState(0);
|
||||
@@ -29,6 +30,7 @@ const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
|
||||
<NavLink
|
||||
to={to}
|
||||
ref={divRef}
|
||||
onClick={onClick}
|
||||
onMouseMove={handleMouseMove}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@@ -76,10 +78,12 @@ const SidebarItem = ({ to, icon: Icon, label, isCollapsed }) => {
|
||||
|
||||
const Layout = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const { theme, toggleTheme } = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
@@ -99,14 +103,45 @@ const Layout = () => {
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-zinc-50 dark:bg-[#050505] text-zinc-900 dark:text-white overflow-hidden font-sans selection:bg-blue-500/20 dark:selection:bg-white/20 transition-colors duration-300">
|
||||
|
||||
{/* Mobile Header */}
|
||||
<div className="md:hidden fixed top-0 left-0 right-0 h-16 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-md border-b border-zinc-200 dark:border-white/5 flex items-center justify-between px-4 z-40">
|
||||
<div className="flex items-center gap-2">
|
||||
<img src={Logo} alt="Logo" className="w-8 h-8" />
|
||||
<span className="font-bold text-lg">LynkedUp</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors"
|
||||
>
|
||||
{isMobileMenuOpen ? <ChevronRight size={24} className="rotate-180 transition-transform" /> : <LayoutDashboard size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Backdrop */}
|
||||
{isMobileMenuOpen && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-40 md:hidden"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Sidebar */}
|
||||
{/* Enhanced glassmorphism with light/dark support */}
|
||||
<aside className={`${isCollapsed ? 'w-20' : 'w-72'} bg-white/60 dark:bg-zinc-900/60 backdrop-blur-2xl border-r border-zinc-200 dark:border-white/5 flex flex-col shrink-0 z-30 shadow-[4px_0_24px_rgba(0,0,0,0.05)] dark:shadow-[4px_0_24px_rgba(0,0,0,0.4)] relative transition-all duration-300 ease-in-out`}>
|
||||
<aside className={`
|
||||
fixed md:static inset-y-0 left-0 z-50
|
||||
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
|
||||
${isCollapsed ? 'w-20' : 'w-72'}
|
||||
bg-white/95 md:bg-white/60 dark:bg-zinc-900/95 md:dark:bg-zinc-900/60
|
||||
backdrop-blur-2xl border-r border-zinc-200 dark:border-white/5
|
||||
flex flex-col shrink-0 shadow-[4px_0_24px_rgba(0,0,0,0.05)] dark:shadow-[4px_0_24px_rgba(0,0,0,0.4)]
|
||||
transition-all duration-300 ease-in-out
|
||||
`}>
|
||||
|
||||
{/* Collapse Toggle Button */}
|
||||
{/* Collapse Toggle Button (Desktop Only) */}
|
||||
<button
|
||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||
className="absolute -right-3 top-9 w-6 h-6 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 rounded-full flex items-center justify-center text-zinc-500 hover:text-zinc-900 dark:hover:text-white shadow-sm z-50 transition-colors"
|
||||
className="hidden md:flex absolute -right-3 top-9 w-6 h-6 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-white/10 rounded-full items-center justify-center text-zinc-500 hover:text-zinc-900 dark:hover:text-white shadow-sm z-50 transition-colors"
|
||||
>
|
||||
{isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
|
||||
</button>
|
||||
@@ -128,24 +163,41 @@ const Layout = () => {
|
||||
|
||||
{user?.role === 'FIELD_AGENT' || user?.role === 'ADMIN' ? (
|
||||
<>
|
||||
<SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" isCollapsed={isCollapsed} />
|
||||
<SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" isCollapsed={isCollapsed} />
|
||||
<SidebarItem to="/emp/fa/dashboard" icon={LayoutDashboard} label="Dashboard" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
|
||||
<SidebarItem to="/emp/fa/maps" icon={Map} label="Territory Map" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{user?.role === 'ADMIN' && (
|
||||
<SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" isCollapsed={isCollapsed} />
|
||||
<SidebarItem to="/admin/schedule" icon={Calendar} label="Team Schedule" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
|
||||
)}
|
||||
|
||||
{/* Common Links */}
|
||||
<div className={`pt-6 mt-6 border-t border-zinc-200 dark:border-white/5 transition-all duration-300 ${isCollapsed ? 'flex justify-center' : ''}`}>
|
||||
<div className={`text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-4 pl-4 whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'opacity-0 h-0 scale-y-0' : 'opacity-100 h-auto scale-y-100'}`}>System</div>
|
||||
<SidebarItem to="/" icon={MessageSquare} label="Public Site" isCollapsed={isCollapsed} />
|
||||
<SidebarItem to="/" icon={MessageSquare} label="Public Site" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
|
||||
{user?.role === 'CUSTOMER' && (
|
||||
<SidebarItem to="/portal/profile" icon={User} label="My Profile" isCollapsed={isCollapsed} onClick={() => setIsMobileMenuOpen(false)} />
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto p-4 border-t border-zinc-200 dark:border-white/5 bg-zinc-50/50 dark:bg-black/20 relative z-10">
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} w-full mb-3 p-3 rounded-xl bg-white/40 dark:bg-white/5 border border-zinc-200 dark:border-white/5 backdrop-blur-md transition-all duration-300 hover:bg-white/60 dark:hover:bg-white/10 group text-zinc-600 dark:text-zinc-400`}
|
||||
title={isCollapsed ? "Toggle Theme" : ""}
|
||||
>
|
||||
<div className="shrink-0">
|
||||
{theme === 'dark' ? <Sun size={18} /> : <Moon size={18} />}
|
||||
</div>
|
||||
<span className={`font-medium text-sm whitespace-nowrap overflow-hidden transition-all duration-300 ${isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100'}`}>
|
||||
{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div className={`flex items-center ${isCollapsed ? 'justify-center' : 'space-x-3'} p-3 mb-3 rounded-xl bg-white/40 dark:bg-white/5 border border-zinc-200 dark:border-white/5 backdrop-blur-md transition-all duration-300 hover:bg-white/60 dark:hover:bg-white/10 group cursor-default`}>
|
||||
<div className="w-10 h-10 rounded-full bg-zinc-200 dark:bg-zinc-800 flex items-center justify-center text-zinc-600 dark:text-zinc-400 border border-zinc-300 dark:border-white/5 group-hover:border-zinc-400 dark:group-hover:border-white/20 transition-colors shrink-0">
|
||||
<User size={18} />
|
||||
@@ -168,7 +220,7 @@ const Layout = () => {
|
||||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-auto relative bg-zinc-50 dark:bg-[#09090b] scroll-smooth transition-colors duration-300">
|
||||
<main className="flex-1 overflow-auto relative bg-zinc-50 dark:bg-[#09090b] scroll-smooth transition-colors duration-300 pt-16 md:pt-0">
|
||||
<PageTransition>
|
||||
<Outlet />
|
||||
</PageTransition>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
import { Icons } from './landing/constants';
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: "Residential Repair",
|
||||
desc: "Leak detection & shingle replacement in record time. We identify micro-fractures invisible to the naked eye.",
|
||||
icon: Icons.Home,
|
||||
color: "from-blue-400 to-blue-600",
|
||||
glow: "shadow-blue-500/20"
|
||||
},
|
||||
// Commercial Development Removed as per request
|
||||
{
|
||||
title: "Lead & Data Solutions",
|
||||
desc: "Advanced lead scoring and real-time storm tracking alerts. Know where the damage is before the phone rings.",
|
||||
icon: Icons.Chart,
|
||||
color: "from-purple-400 to-indigo-600",
|
||||
glow: "shadow-purple-500/20"
|
||||
}
|
||||
];
|
||||
|
||||
const Services = () => {
|
||||
// Simple Tilt Logic
|
||||
const handleMouseMove = (e) => {
|
||||
const card = e.currentTarget;
|
||||
const rect = card.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const centerX = rect.width / 2;
|
||||
const centerY = rect.height / 2;
|
||||
|
||||
// Rotation calc (Max 5deg)
|
||||
const rotateX = ((y - centerY) / centerY) * -5;
|
||||
const rotateY = ((x - centerX) / centerX) * 5;
|
||||
|
||||
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`;
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e) => {
|
||||
e.currentTarget.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale(1)';
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-white relative overflow-hidden dark:bg-black/40">
|
||||
<div className="absolute inset-0 opacity-5 bg-grid-pattern bg-[length:40px_40px]"></div>
|
||||
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-slate-900 dark:text-white font-heading text-4xl font-bold">Our Core Services</h2>
|
||||
<p className="text-gray-500 mt-4 max-w-2xl mx-auto font-light">Leveraging military-grade technology for civilian roofing applications.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{services.map((service, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
className="group p-8 rounded-2xl border border-gray-100 dark:border-white/10 bg-white/80 dark:bg-white/5 backdrop-blur-sm shadow-xl transition-all duration-200 ease-out cursor-pointer relative overflow-hidden"
|
||||
style={{ transformStyle: 'preserve-3d', transition: 'transform 0.1s ease-out' }}
|
||||
>
|
||||
{/* Gradient Glow on Hover */}
|
||||
<div className={`absolute -inset-0.5 bg-gradient-to-br ${service.color} opacity-0 group-hover:opacity-20 blur-xl transition-opacity duration-300 pointer-events-none`}></div>
|
||||
|
||||
<div className="relative z-10 flex flex-col h-full">
|
||||
<div className={`w-16 h-16 mb-6 rounded-xl flex items-center justify-center text-white bg-gradient-to-br ${service.color} shadow-lg ${service.glow} transform group-hover:scale-110 transition-transform duration-300`}>
|
||||
<div className="w-8 h-8 flex items-center justify-center">
|
||||
<service.icon />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-heading font-bold text-slate-900 dark:text-white mb-3 group-hover:text-transparent group-hover:bg-clip-text group-hover:bg-gradient-to-r group-hover:from-blue-600 group-hover:to-cyan-500 dark:group-hover:from-blue-400 dark:group-hover:to-cyan-400 transition-all duration-300">
|
||||
{service.title}
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed mb-8 flex-grow">
|
||||
{service.desc}
|
||||
</p>
|
||||
|
||||
<div className="pt-6 border-t border-gray-100 dark:border-white/10 flex items-center justify-between mt-auto group-hover:border-cyan/20 transition-colors">
|
||||
<span className="text-xs font-bold text-gray-400 uppercase tracking-wider group-hover:text-cyan transition-colors">Explore Solution</span>
|
||||
<div className="w-8 h-8 rounded-full border border-gray-200 dark:border-white/20 text-gray-400 flex items-center justify-center transform group-hover:rotate-45 group-hover:border-cyan group-hover:text-cyan transition-all duration-300">
|
||||
↗
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default Services;
|
||||
@@ -77,5 +77,15 @@ export const Icons = {
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-full h-full" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
)
|
||||
),
|
||||
Building: (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-full h-full" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5M12 6.75h1.5M15 6.75h1.5M9 10.5h1.5M12 10.5h1.5M15 10.5h1.5M9 14.25h1.5M12 14.25h1.5M15 14.25h1.5M9 18h1.5M12 18h1.5M15 18h1.5" />
|
||||
</svg>
|
||||
),
|
||||
Home: (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-full h-full" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user