diff --git a/src/assets/images/compare-new.webp b/src/assets/images/compare-new.webp new file mode 100644 index 0000000..f8a0aec Binary files /dev/null and b/src/assets/images/compare-new.webp differ diff --git a/src/assets/images/compare-old.webp b/src/assets/images/compare-old.webp new file mode 100644 index 0000000..25f0112 Binary files /dev/null and b/src/assets/images/compare-old.webp differ diff --git a/src/assets/images/hero_aerial_scan.webp b/src/assets/images/hero_aerial_scan.webp new file mode 100644 index 0000000..c0b9f32 Binary files /dev/null and b/src/assets/images/hero_aerial_scan.webp differ diff --git a/src/assets/images/intel_map_dfw_twilight.webp b/src/assets/images/intel_map_dfw_twilight.webp new file mode 100644 index 0000000..b8fb61d Binary files /dev/null and b/src/assets/images/intel_map_dfw_twilight.webp differ diff --git a/src/assets/images/process-2.webp b/src/assets/images/process-2.webp new file mode 100644 index 0000000..4e6dd41 Binary files /dev/null and b/src/assets/images/process-2.webp differ diff --git a/src/assets/images/process-3.webp b/src/assets/images/process-3.webp new file mode 100644 index 0000000..0061718 Binary files /dev/null and b/src/assets/images/process-3.webp differ diff --git a/src/assets/images/process.webp b/src/assets/images/process.webp new file mode 100644 index 0000000..bde30d5 Binary files /dev/null and b/src/assets/images/process.webp differ diff --git a/src/components/ComparisonSlider.jsx b/src/components/ComparisonSlider.jsx index 492e945..9bf0824 100644 --- a/src/components/ComparisonSlider.jsx +++ b/src/components/ComparisonSlider.jsx @@ -1,7 +1,7 @@ import React, { useState, useRef, useEffect } from 'react'; import { MoveHorizontal } from 'lucide-react'; -import CompareNew from '../assets/images/compare-new.png'; -import CompareOld from '../assets/images/compare-old.png'; +import CompareNew from '../assets/images/compare-new.webp'; +import CompareOld from '../assets/images/compare-old.webp'; export const ComparisonSlider = () => { const [sliderPos, setSliderPos] = useState(50); @@ -47,23 +47,33 @@ export const ComparisonSlider = () => { alt="The New Way" className="w-full h-full object-cover" /> -
- +
+ Our Way -
+
{[ "15-Minute Drone Scan", "Zero Safety Risk", "AI + Thermal Imaging", "~30% Lower Costs", - "~ 60% faster lead conversion", - "~ Deploy Reps with confidence", - "~ Evidence-driven damage detection unlocks Revenue" ].map((text, i) => ( -
- {text} -
+
+ {text} +
+ +
+
+ ))} + {/* Extended items — desktop only */} + {[ + "60% faster lead conversion", + "Deploy Reps with confidence", + "Evidence-driven damage detection", + ].map((text, i) => ( +
+ {text} +
@@ -84,29 +94,29 @@ export const ComparisonSlider = () => { style={{ width: containerRef.current?.offsetWidth }} /> {/* Overlay Text visible only when this side is dominant */} -
- +
+ The Old Way -
+
{[ "Dangerous Ladders & Falls", - "Guesswork Based Estimates", + "Guesswork Estimates", "Slow Turnaround Times", "Hidden Material Costs" ].map((text, i) => ( -
-
- +
+
+
- {text} + {text}
))}
- {/* Slider Handle */} + {/* Slider Handle — Desktop */}
{
- {/* Mobile Slider Handle (Always visible comparison line) */} + {/* Slider Handle — Mobile (visible circle + line) */}
+ > +
+ +
+
{/* Instructions Overlay (Fades out on interaction) */}
diff --git a/src/components/IntelligenceMap.jsx b/src/components/IntelligenceMap.jsx index 5d28440..30ff086 100644 --- a/src/components/IntelligenceMap.jsx +++ b/src/components/IntelligenceMap.jsx @@ -1,7 +1,7 @@ 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.png'; +import MapBg from '../assets/images/intel_map_dfw_twilight.webp'; import IntelligenceSidePanel from './dashboard/IntelligenceSidePanel'; import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; diff --git a/src/components/Services.jsx b/src/components/Services.jsx index 375b784..1404831 100644 --- a/src/components/Services.jsx +++ b/src/components/Services.jsx @@ -1,6 +1,10 @@ -import React from 'react'; +import React, { useRef, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { Icons } from './landing/constants'; +import gsap from 'gsap'; +import { ScrollTrigger } from 'gsap/ScrollTrigger'; + +gsap.registerPlugin(ScrollTrigger); const services = [ { @@ -12,7 +16,7 @@ const services = [ badge: "Licensed", badgeClass: "permit-stamp--verified", badgeIcon: "\u2713", - stat: "2,847", + stat: 5847, statLabel: "Properties Inspected", }, { @@ -24,13 +28,47 @@ const services = [ badge: "Certified", badgeClass: "permit-stamp--accent", badgeIcon: "\u25B9", - stat: "96%", + stat: 96, + statSuffix: "%", statLabel: "Lead Accuracy", } ]; const Services = () => { const navigate = useNavigate(); + const statRefs = useRef([]); + + useEffect(() => { + // Animated Stat Counters + statRefs.current.forEach((el) => { + if (!el) return; + + const target = parseFloat(el.dataset.target); + const suffix = el.dataset.suffix || ''; + const counter = { value: 0 }; + + el.innerText = '0' + suffix; + + const formatNumber = (num) => { + return Math.round(num).toLocaleString('en-US'); + }; + + gsap.to(counter, { + value: target, + duration: 2.5, + ease: "power2.out", + scrollTrigger: { + trigger: el, + start: "top 85%", + toggleActions: "play none none none", + }, + onUpdate: () => { + el.innerText = formatNumber(counter.value) + suffix; + }, + snap: { value: 1 } + }); + }); + }, []); const handleMouseMove = (e) => { const card = e.currentTarget; @@ -65,7 +103,7 @@ const Services = () => { key={idx} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} - className="group relative p-8 rounded-2xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-white/[0.04] hover:bg-zinc-50 dark:hover:bg-white/[0.07] backdrop-blur-sm transition-all duration-200 cursor-pointer overflow-hidden shadow-xl dark:shadow-[0_20px_60px_rgba(0,0,0,0.4)]" + className="group relative p-6 md:p-8 rounded-2xl border border-zinc-200 dark:border-white/10 bg-white dark:bg-white/[0.04] hover:bg-zinc-50 dark:hover:bg-white/[0.07] backdrop-blur-sm transition-all duration-200 cursor-pointer overflow-hidden shadow-xl dark:shadow-[0_20px_60px_rgba(0,0,0,0.4)]" style={{ transformStyle: 'preserve-3d', transition: 'transform 0.1s ease-out' }} > {/* Measurement ticks — top */} @@ -93,9 +131,16 @@ const Services = () => { {service.desc}

- {/* Inline stat */} + {/* Inline stat with animation */}
- {service.stat} + statRefs.current[idx] = el} + data-target={service.stat} + data-suffix={service.statSuffix || ''} + className="font-mono text-2xl font-bold text-zinc-900 dark:text-white tracking-tight" + > + 0{service.statSuffix || ''} + {service.statLabel}
diff --git a/src/index.css b/src/index.css index de75e73..97339d7 100644 --- a/src/index.css +++ b/src/index.css @@ -28,7 +28,9 @@ --crane-fill-surface: rgba(0, 0, 0, 0.04); } - html, body, #root { + html, + body, + #root { height: 100%; margin: 0; padding: 0; @@ -69,8 +71,13 @@ } @keyframes fadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + + to { + opacity: 1; + } } @keyframes slideInBottom { @@ -78,6 +85,7 @@ transform: translateY(1rem); opacity: 0; } + to { transform: translateY(0); opacity: 1; @@ -89,12 +97,16 @@ -ms-overflow-style: none; scrollbar-width: none; } + *::-webkit-scrollbar { display: none; } @layer utilities { - .no-scrollbar::-webkit-scrollbar { display: none; } + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; @@ -121,14 +133,17 @@ border-radius: 3px; opacity: 0.7; } + .permit-stamp--accent { color: #3b82f6; border-color: rgba(59, 130, 246, 0.4); } + .permit-stamp--verified { color: #10b981; border-color: rgba(16, 185, 129, 0.4); } + .permit-stamp--danger { color: #ef4444; border-color: rgba(239, 68, 68, 0.4); @@ -141,19 +156,20 @@ width: 100%; height: 20px; background: - linear-gradient(135deg, rgba(0,0,0,0.06) 25%, transparent 25%) -24px 0, - linear-gradient(225deg, rgba(0,0,0,0.06) 25%, transparent 25%) -24px 0, - linear-gradient(315deg, rgba(0,0,0,0.06) 25%, transparent 25%), - linear-gradient(45deg, rgba(0,0,0,0.06) 25%, transparent 25%); + linear-gradient(135deg, rgba(0, 0, 0, 0.06) 25%, transparent 25%) -24px 0, + linear-gradient(225deg, rgba(0, 0, 0, 0.06) 25%, transparent 25%) -24px 0, + linear-gradient(315deg, rgba(0, 0, 0, 0.06) 25%, transparent 25%), + linear-gradient(45deg, rgba(0, 0, 0, 0.06) 25%, transparent 25%); background-size: 48px 20px; background-color: transparent; } + .dark .shingle-divider { background: - linear-gradient(135deg, rgba(255,255,255,0.04) 25%, transparent 25%) -24px 0, - linear-gradient(225deg, rgba(255,255,255,0.04) 25%, transparent 25%) -24px 0, - linear-gradient(315deg, rgba(255,255,255,0.04) 25%, transparent 25%), - linear-gradient(45deg, rgba(255,255,255,0.04) 25%, transparent 25%); + linear-gradient(135deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%) -24px 0, + linear-gradient(225deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%) -24px 0, + linear-gradient(315deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%), + linear-gradient(45deg, rgba(255, 255, 255, 0.04) 25%, transparent 25%); background-size: 48px 20px; } } @@ -161,13 +177,11 @@ /* Safety stripe — diagonal hazard pattern for urgent/warning elements */ @layer components { .safety-stripe { - background: repeating-linear-gradient( - -45deg, - transparent, - transparent 4px, - rgba(245, 158, 11, 0.12) 4px, - rgba(245, 158, 11, 0.12) 8px - ); + background: repeating-linear-gradient(-45deg, + transparent, + transparent 4px, + rgba(245, 158, 11, 0.12) 4px, + rgba(245, 158, 11, 0.12) 8px); } } @@ -175,17 +189,19 @@ @layer components { .measure-ticks { background-image: - repeating-linear-gradient(90deg, rgba(0,0,0,0.15) 0px, rgba(0,0,0,0.15) 1.5px, transparent 1.5px, transparent 24px), - repeating-linear-gradient(90deg, rgba(0,0,0,0.07) 0px, rgba(0,0,0,0.07) 1px, transparent 1px, transparent 6px); + repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.15) 0px, rgba(0, 0, 0, 0.15) 1.5px, transparent 1.5px, transparent 24px), + repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.07) 0px, rgba(0, 0, 0, 0.07) 1px, transparent 1px, transparent 6px); background-size: 100% 100%, 100% 50%; background-repeat: no-repeat; background-position: bottom, bottom; } + .dark .measure-ticks { background-image: - repeating-linear-gradient(90deg, rgba(255,255,255,0.22) 0px, rgba(255,255,255,0.22) 1.5px, transparent 1.5px, transparent 24px), - repeating-linear-gradient(90deg, rgba(255,255,255,0.1) 0px, rgba(255,255,255,0.1) 1px, transparent 1px, transparent 6px); + repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.22) 0px, rgba(255, 255, 255, 0.22) 1.5px, transparent 1.5px, transparent 24px), + repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.1) 0px, rgba(255, 255, 255, 0.1) 1px, transparent 1px, transparent 6px); } + .measure-ticks-top { background-position: top, top; } @@ -195,49 +211,65 @@ @layer components { .blueprint-grid { background-image: - linear-gradient(rgba(59,130,246,0.1) 1px, transparent 1px), - linear-gradient(90deg, rgba(59,130,246,0.1) 1px, transparent 1px); + linear-gradient(rgba(59, 130, 246, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.1) 1px, transparent 1px); background-size: 48px 48px; } + .dark .blueprint-grid { background-image: - linear-gradient(rgba(59,130,246,0.05) 1px, transparent 1px), - linear-gradient(90deg, rgba(59,130,246,0.05) 1px, transparent 1px); + linear-gradient(rgba(59, 130, 246, 0.05) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.05) 1px, transparent 1px); } + .blueprint-grid-fine { background-image: - linear-gradient(rgba(59,130,246,0.06) 1px, transparent 1px), - linear-gradient(90deg, rgba(59,130,246,0.06) 1px, transparent 1px), - linear-gradient(rgba(59,130,246,0.1) 1px, transparent 1px), - linear-gradient(90deg, rgba(59,130,246,0.1) 1px, transparent 1px); + linear-gradient(rgba(59, 130, 246, 0.06) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.06) 1px, transparent 1px), + linear-gradient(rgba(59, 130, 246, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.1) 1px, transparent 1px); background-size: 12px 12px, 12px 12px, 48px 48px, 48px 48px; } + .dark .blueprint-grid-fine { background-image: - linear-gradient(rgba(59,130,246,0.03) 1px, transparent 1px), - linear-gradient(90deg, rgba(59,130,246,0.03) 1px, transparent 1px), - linear-gradient(rgba(59,130,246,0.06) 1px, transparent 1px), - linear-gradient(90deg, rgba(59,130,246,0.06) 1px, transparent 1px); + linear-gradient(rgba(59, 130, 246, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.03) 1px, transparent 1px), + linear-gradient(rgba(59, 130, 246, 0.06) 1px, transparent 1px), + linear-gradient(90deg, rgba(59, 130, 246, 0.06) 1px, transparent 1px); } } /* Mobile bottom sheet slide-up for Intelligence Map */ @keyframes slideUpSheet { - from { transform: translateY(100%); } - to { transform: translateY(0); } + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } } /* Crane swing — gentle pendulum sway for suspended CTA card */ @keyframes craneSwing { - 0%, 100% { transform: rotate(0deg); } - 25% { transform: rotate(1.5deg); } - 75% { transform: rotate(-1.5deg); } + + 0%, + 100% { + transform: rotate(3deg); + } + + 50% { + transform: rotate(-3deg); + } } + @layer components { .crane-swing { animation: craneSwing 4s ease-in-out infinite; transform-origin: top center; } + .crane-swing:hover { animation-play-state: paused; } @@ -245,6 +277,7 @@ /* === Attribution (existing, preserved) === */ @layer utilities { + .attribution-ghost, .content-ref-x7, .sys-ref-x7, @@ -270,8 +303,9 @@ background: transparent; user-select: text; } + .attribution-reveal::selection { background: #3b82f6; color: white; } -} +} \ No newline at end of file diff --git a/src/pages/Landing.jsx b/src/pages/Landing.jsx index 83acfd2..8422f06 100644 --- a/src/pages/Landing.jsx +++ b/src/pages/Landing.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import Chatbot from '../components/Chatbot'; import { useAuth } from '../context/AuthContext'; import { useTheme } from '../context/ThemeContext'; @@ -9,13 +9,14 @@ import { SpotlightCard } from '../components/SpotlightCard'; import { ComparisonSlider } from '../components/ComparisonSlider'; import Services from '../components/Services'; import IntelligenceMap from '../components/IntelligenceMap'; +import { motion, AnimatePresence } from 'framer-motion'; import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import Logo from '../assets/images/LynkedUp_Pro_F_logo_Y.png'; -import HeroImg from '../assets/images/hero_aerial_scan.png'; -import ProcessImg1 from '../assets/images/process.png'; -import ProcessImg2 from '../assets/images/process-2.png'; -import ProcessImg3 from '../assets/images/process-3.png'; +import HeroImg from '../assets/images/hero_aerial_scan.webp'; +import ProcessImg1 from '../assets/images/process.webp'; +import ProcessImg2 from '../assets/images/process-2.webp'; +import ProcessImg3 from '../assets/images/process-3.webp'; gsap.registerPlugin(ScrollTrigger); @@ -26,9 +27,32 @@ const Landing = () => { const textRef = useRef(null); const sectionsRef = useRef([]); const statRefs = useRef([]); - const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); + const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + const [hoveredSplit, setHoveredSplit] = useState(null); const cursorGlowRef = useRef(null); + // Scroll-aware nav — transparent at top, glass effect on scroll + useEffect(() => { + const handleScroll = () => setIsScrolled(window.scrollY > 50); + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => window.removeEventListener('scroll', handleScroll); + }, []); + + // Body scroll lock when mobile menu is open + useEffect(() => { + document.body.style.overflow = isMobileMenuOpen ? 'hidden' : ''; + return () => { document.body.style.overflow = ''; }; + }, [isMobileMenuOpen]); + + const scrollToSection = (id) => { + const el = document.getElementById(id); + if (el) { + el.scrollIntoView({ behavior: 'smooth' }); + setIsMobileMenuOpen(false); + } + }; + // Mouse-follow ambient light — tracks cursor across the page useEffect(() => { const handleMouseMove = (e) => { @@ -79,18 +103,24 @@ const Landing = () => { el.innerText = '0' + suffix; + // Helper function to format numbers with commas + const formatNumber = (num) => { + return Math.round(num).toLocaleString('en-US'); + }; + gsap.to(counter, { value: target, - duration: 2, - ease: "power1.out", + duration: 2.5, + ease: "power2.out", scrollTrigger: { trigger: el, start: "top 85%", toggleActions: "play none none none", }, onUpdate: () => { - el.innerText = Math.ceil(counter.value) + suffix; - } + el.innerText = formatNumber(counter.value) + suffix; + }, + snap: { value: 1 } }); }); @@ -115,96 +145,208 @@ const Landing = () => { }} /> - {/* Nav */} -