Optimize mobile layout, add animated counters, improve crane swing
This commit is contained in:
@@ -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"
|
||||
/>
|
||||
<div className="absolute top-10 right-10 flex flex-col items-end space-y-4 select-none">
|
||||
<span className="text-xl md:text-2xl font-black text-white bg-black/50 backdrop-blur-md px-4 py-2 rounded-xl border border-blue-500/50">
|
||||
<div className="absolute top-4 right-3 md:top-10 md:right-10 flex flex-col items-end space-y-2 md:space-y-4 select-none">
|
||||
<span className="text-base md:text-2xl font-black text-white bg-black/50 backdrop-blur-md px-3 py-1.5 md:px-4 md:py-2 rounded-xl border border-blue-500/50">
|
||||
Our Way
|
||||
</span>
|
||||
<div className="text-right space-y-2">
|
||||
<div className="text-right space-y-1.5 md:space-y-2">
|
||||
{[
|
||||
"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) => (
|
||||
<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>
|
||||
<div className="w-5 h-5 rounded-full bg-blue-500 flex items-center justify-center">
|
||||
<div key={i} className="flex items-center justify-end space-x-1.5 md:space-x-2 bg-black/40 backdrop-blur-sm px-2 py-0.5 md:px-3 md:py-1 rounded-lg border border-blue-500/20">
|
||||
<span className="text-[10px] md:text-base font-bold text-white">{text}</span>
|
||||
<div className="w-4 h-4 md:w-5 md:h-5 rounded-full bg-blue-500 flex items-center justify-center shrink-0">
|
||||
<svg className="w-2.5 h-2.5 md:w-3 md:h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Extended items — desktop only */}
|
||||
{[
|
||||
"60% faster lead conversion",
|
||||
"Deploy Reps with confidence",
|
||||
"Evidence-driven damage detection",
|
||||
].map((text, i) => (
|
||||
<div key={`ext-${i}`} className="hidden md: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-base font-bold text-white">{text}</span>
|
||||
<div className="w-5 h-5 rounded-full bg-blue-500 flex items-center justify-center shrink-0">
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" /></svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -84,29 +94,29 @@ export const ComparisonSlider = () => {
|
||||
style={{ width: containerRef.current?.offsetWidth }}
|
||||
/>
|
||||
{/* Overlay Text visible only when this side is dominant */}
|
||||
<div className="absolute top-10 left-10 flex flex-col items-start space-y-4 select-none" style={{ opacity: Math.max(0, (sliderPos - 10) / 40) }}>
|
||||
<span className="text-xl md:text-2xl font-black text-zinc-200 bg-black/60 backdrop-blur-md px-4 py-2 rounded-xl border border-red-500/50">
|
||||
<div className="absolute top-4 left-3 md:top-10 md:left-10 flex flex-col items-start space-y-2 md:space-y-4 select-none" style={{ opacity: Math.max(0, (sliderPos - 10) / 40) }}>
|
||||
<span className="text-base md:text-2xl font-black text-zinc-200 bg-black/60 backdrop-blur-md px-3 py-1.5 md:px-4 md:py-2 rounded-xl border border-red-500/50">
|
||||
The Old Way
|
||||
</span>
|
||||
<div className="text-left space-y-2">
|
||||
<div className="text-left space-y-1.5 md:space-y-2">
|
||||
{[
|
||||
"Dangerous Ladders & Falls",
|
||||
"Guesswork Based Estimates",
|
||||
"Guesswork Estimates",
|
||||
"Slow Turnaround Times",
|
||||
"Hidden Material Costs"
|
||||
].map((text, i) => (
|
||||
<div key={i} className="flex items-center space-x-2 bg-black/50 backdrop-blur-sm px-3 py-1 rounded-lg border border-red-500/20">
|
||||
<div className="w-5 h-5 rounded-full bg-red-500/80 flex items-center justify-center">
|
||||
<svg className="w-3 h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
<div key={i} className="flex items-center space-x-1.5 md:space-x-2 bg-black/50 backdrop-blur-sm px-2 py-0.5 md:px-3 md:py-1 rounded-lg border border-red-500/20">
|
||||
<div className="w-4 h-4 md:w-5 md:h-5 rounded-full bg-red-500/80 flex items-center justify-center shrink-0">
|
||||
<svg className="w-2.5 h-2.5 md:w-3 md:h-3 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M6 18L18 6M6 6l12 12" /></svg>
|
||||
</div>
|
||||
<span className="text-sm md:text-base font-bold text-zinc-300">{text}</span>
|
||||
<span className="text-[10px] md:text-base font-bold text-zinc-300">{text}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slider Handle */}
|
||||
{/* Slider Handle — Desktop */}
|
||||
<div
|
||||
className="absolute top-0 bottom-0 w-1 bg-white cursor-ew-resize hidden md:flex items-center justify-center shadow-[0_0_20px_rgba(0,0,0,0.5)]"
|
||||
style={{ left: `${sliderPos}%` }}
|
||||
@@ -116,11 +126,15 @@ export const ComparisonSlider = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Slider Handle (Always visible comparison line) */}
|
||||
{/* Slider Handle — Mobile (visible circle + line) */}
|
||||
<div
|
||||
className="absolute top-0 bottom-0 w-1 bg-white/50 md:hidden"
|
||||
className="absolute top-0 bottom-0 w-0.5 bg-white/70 md:hidden flex items-center justify-center"
|
||||
style={{ left: `${sliderPos}%` }}
|
||||
/>
|
||||
>
|
||||
<div className="w-9 h-9 bg-white rounded-full flex items-center justify-center shadow-lg shrink-0">
|
||||
<MoveHorizontal className="text-black" size={16} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Instructions Overlay (Fades out on interaction) */}
|
||||
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 text-white/80 text-sm font-medium bg-black/40 px-4 py-2 rounded-full backdrop-blur-sm pointer-events-none">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
</p>
|
||||
|
||||
{/* Inline stat */}
|
||||
{/* Inline stat with animation */}
|
||||
<div className="flex items-baseline gap-2 mb-6">
|
||||
<span className="font-mono text-2xl font-bold text-zinc-900 dark:text-white tracking-tight">{service.stat}</span>
|
||||
<span
|
||||
ref={(el) => 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 || ''}
|
||||
</span>
|
||||
<span className="text-xs text-zinc-500 font-medium uppercase tracking-wider">{service.statLabel}</span>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user