Bob AI: Populate the newly-created page at src/pages/MachineWalkthro
This commit is contained in:
@@ -1,94 +1,241 @@
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { routes } from "@/routes";
|
||||
import NavbarCentered from "@/components/ui/NavbarCentered";
|
||||
import HeroOverlay from "@/components/sections/hero/HeroOverlay";
|
||||
import FeaturesMediaCards from "@/components/sections/features/FeaturesMediaCards";
|
||||
import ContactCta from "@/components/sections/contact/ContactCta";
|
||||
import FooterSimple from "@/components/sections/footer/FooterSimple";
|
||||
import { Info, MapPin, ChevronLeft } from "lucide-react";
|
||||
import { cls } from "@/lib/utils";
|
||||
|
||||
type ViewKey = 'exterior' | 'cab' | 'machinery' | 'electrical' | 'boom' | 'maintenance';
|
||||
|
||||
type Hotspot =
|
||||
| { id: string; x: number; y: number; label: string; type: 'nav'; target: ViewKey }
|
||||
| { id: string; x: number; y: number; label: string; type: 'info'; info: string };
|
||||
|
||||
type View = {
|
||||
title: string;
|
||||
imageSrc: string;
|
||||
hotspots: Hotspot[];
|
||||
};
|
||||
|
||||
const VIEWS: Record<ViewKey, View> = {
|
||||
exterior: {
|
||||
title: "P&H 4200 Exterior",
|
||||
imageSrc: "https://picsum.photos/seed/1329000375/1200/800",
|
||||
hotspots: [
|
||||
{ id: 'enter', x: 50, y: 60, label: "Enter the Machine", target: 'cab', type: 'nav' }
|
||||
]
|
||||
},
|
||||
cab: {
|
||||
title: "Operator's Cab",
|
||||
imageSrc: "https://picsum.photos/seed/1496099219/1200/800",
|
||||
hotspots: [
|
||||
{ id: 'controls', x: 40, y: 50, label: "Dual Joystick Controls", info: "Ergonomic control center with panoramic visibility.", type: 'info' },
|
||||
{ id: 'display', x: 60, y: 45, label: "Diagnostic Displays", info: "Real-time operational data and payload monitoring.", type: 'info' }
|
||||
]
|
||||
},
|
||||
machinery: {
|
||||
title: "Machinery House",
|
||||
imageSrc: "https://picsum.photos/seed/1110967395/1200/800",
|
||||
hotspots: [
|
||||
{ id: 'hoist', x: 30, y: 60, label: "Hoist Motors", info: "Massive hoist motors designed for continuous high-torque output.", type: 'info' },
|
||||
{ id: 'swing', x: 70, y: 55, label: "Swing Transmissions", info: "Planetary gearboxes for smooth, rapid swing cycles.", type: 'info' }
|
||||
]
|
||||
},
|
||||
electrical: {
|
||||
title: "Electrical Room",
|
||||
imageSrc: "https://picsum.photos/seed/1776604305/1200/800",
|
||||
hotspots: [
|
||||
{ id: 'ac', x: 50, y: 40, label: "AC Drive Systems", info: "Advanced IGBT AC drives for precise motor control.", type: 'info' },
|
||||
{ id: 'panels', x: 20, y: 50, label: "Power Distribution", info: "Climate-controlled electronics bays.", type: 'info' }
|
||||
]
|
||||
},
|
||||
boom: {
|
||||
title: "Boom & Dipper",
|
||||
imageSrc: "https://picsum.photos/seed/691466050/1200/800",
|
||||
hotspots: [
|
||||
{ id: 'dipper', x: 50, y: 70, label: "High-Capacity Dipper", info: "Engineered for maximum payload and optimal dig geometry.", type: 'info' },
|
||||
{ id: 'crowd', x: 50, y: 30, label: "Crowd Machinery", info: "Rack and pinion crowd for positive digging force.", type: 'info' }
|
||||
]
|
||||
},
|
||||
maintenance: {
|
||||
title: "Maintenance Deck",
|
||||
imageSrc: "https://picsum.photos/seed/429735176/1200/800",
|
||||
hotspots: [
|
||||
{ id: 'lube', x: 40, y: 60, label: "Auto-Lube System", info: "Centralized lubrication for all major pivot points.", type: 'info' },
|
||||
{ id: 'access', x: 80, y: 50, label: "Safe Access", info: "Wide walkways and tie-off points for maintenance personnel.", type: 'info' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
export default function MachineWalkthroughPage() {
|
||||
const [currentView, setCurrentView] = useState<ViewKey>('exterior');
|
||||
const [activeInfo, setActiveInfo] = useState<string | null>(null);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
const handleNavigate = (target: ViewKey) => {
|
||||
setIsTransitioning(true);
|
||||
setActiveInfo(null);
|
||||
setTimeout(() => {
|
||||
setCurrentView(target);
|
||||
setIsTransitioning(false);
|
||||
}, 800);
|
||||
};
|
||||
|
||||
const view = VIEWS[currentView];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||
<NavbarCentered
|
||||
logo="P&H 4200 Tour"
|
||||
navItems={routes.map((r) => ({ name: r.label, href: r.path }))}
|
||||
ctaButton={{ text: "Request Specs", href: "/contact" }}
|
||||
/>
|
||||
<div
|
||||
className="min-h-svh flex flex-col font-sans bg-background text-foreground"
|
||||
style={{
|
||||
'--background': '#09090b',
|
||||
'--foreground': '#f4f4f5',
|
||||
'--card': '#18181b',
|
||||
'--primary-cta': '#18181b',
|
||||
'--primary-cta-text': '#f97316',
|
||||
'--secondary-cta': '#27272a',
|
||||
'--secondary-cta-text': '#f4f4f5',
|
||||
'--accent': '#f97316',
|
||||
'--background-accent': '#c2410c',
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<main className="relative flex-grow h-[calc(100vh-5rem)] mt-20 overflow-hidden bg-background">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentView}
|
||||
initial={{ opacity: 0, scale: 1.1 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 1.2 }}
|
||||
transition={{ duration: 0.8, ease: "easeInOut" }}
|
||||
className="absolute inset-0"
|
||||
>
|
||||
<img
|
||||
src={view.imageSrc}
|
||||
alt={view.title}
|
||||
className="w-full h-full object-cover opacity-60"
|
||||
/>
|
||||
{currentView === 'exterior' && (
|
||||
<motion.img
|
||||
src={view.imageSrc}
|
||||
alt={view.title}
|
||||
initial={{ scale: 1 }}
|
||||
animate={{ scale: 1.1 }}
|
||||
transition={{ duration: 20, repeat: Infinity, repeatType: "reverse", ease: "linear" }}
|
||||
className="absolute inset-0 w-full h-full object-cover opacity-50 mix-blend-overlay"
|
||||
/>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<main className="flex-grow" id="hero">
|
||||
<HeroOverlay
|
||||
tag="Virtual Experience"
|
||||
title="P&H 4200 Electric Rope Shovel"
|
||||
description="Experience the sheer scale and engineering mastery of the world's premier mining shovel. Step inside the machine."
|
||||
primaryButton={{ text: "Enter the Machine", href: "#interiors" }}
|
||||
secondaryButton={{ text: "Watch Exterior Flyby", href: "#" }}
|
||||
imageSrc="https://images.unsplash.com/photo-1578319439584-104c94d37305?auto=format&fit=crop&q=80"
|
||||
textAnimation="fade-blur"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/40 to-transparent pointer-events-none" />
|
||||
|
||||
<div id="interiors">
|
||||
<FeaturesMediaCards
|
||||
tag="Interior Compartments"
|
||||
title="Explore the Anatomy"
|
||||
description="Navigate through the critical zones of the P&H 4200. Select a compartment to view detailed schematics and operational data."
|
||||
items={[
|
||||
{
|
||||
title: "Operator's Cab",
|
||||
description: "Ergonomic control center with panoramic visibility, dual joystick controls, and advanced diagnostic displays.",
|
||||
imageSrc: "https://images.unsplash.com/photo-1581092160562-40aa08e78837?auto=format&fit=crop&q=80"
|
||||
},
|
||||
{
|
||||
title: "Machinery House",
|
||||
description: "The beating heart housing the massive hoist and swing motors, designed for continuous high-torque output.",
|
||||
imageSrc: "https://images.unsplash.com/photo-1581092335397-9583eb92d232?auto=format&fit=crop&q=80"
|
||||
},
|
||||
{
|
||||
title: "Electrical Room",
|
||||
description: "Advanced AC drive systems, power distribution panels, and climate-controlled electronics bays.",
|
||||
imageSrc: "https://images.unsplash.com/photo-1581092580497-e0d23cbdf1dc?auto=format&fit=crop&q=80"
|
||||
},
|
||||
{
|
||||
title: "Boom & Dipper",
|
||||
description: "High-strength steel structures engineered for maximum payload and optimal dig geometry.",
|
||||
imageSrc: "https://images.unsplash.com/photo-1504307651254-35680f356dfd?auto=format&fit=crop&q=80"
|
||||
}
|
||||
]}
|
||||
textAnimation="slide-up"
|
||||
/>
|
||||
<div className="absolute inset-0 z-10">
|
||||
<AnimatePresence>
|
||||
{!isTransitioning && view.hotspots.map((hotspot) => (
|
||||
<motion.div
|
||||
key={hotspot.id}
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="absolute -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-2"
|
||||
style={{ left: `${hotspot.x}%`, top: `${hotspot.y}%` }}
|
||||
>
|
||||
{hotspot.type === 'nav' ? (
|
||||
<button
|
||||
onClick={() => handleNavigate(hotspot.target)}
|
||||
className="group relative flex items-center justify-center w-16 h-16 rounded-full bg-accent/20 border-2 border-accent text-accent hover:bg-accent hover:text-background transition-all duration-300 shadow-lg shadow-accent/40 hover:shadow-xl hover:shadow-accent/60"
|
||||
>
|
||||
<MapPin className="w-6 h-6" />
|
||||
<span className="absolute top-full mt-3 whitespace-nowrap text-sm font-bold tracking-wider uppercase text-accent group-hover:text-accent/80 drop-shadow-md">
|
||||
{hotspot.label}
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setActiveInfo(activeInfo === hotspot.id ? null : hotspot.id)}
|
||||
className={cls(
|
||||
"group relative flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all duration-300",
|
||||
activeInfo === hotspot.id
|
||||
? "bg-accent border-accent text-background shadow-lg shadow-accent/60"
|
||||
: "bg-card/80 border-accent/50 text-accent hover:border-accent hover:bg-accent/20"
|
||||
)}
|
||||
>
|
||||
<Info className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<AnimatePresence>
|
||||
{activeInfo === hotspot.id && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
className="absolute top-full left-1/2 -translate-x-1/2 mt-4 w-64 p-4 rounded-lg bg-card/95 border border-foreground/10 shadow-2xl backdrop-blur-sm"
|
||||
>
|
||||
<h4 className="text-accent font-bold mb-2">{hotspot.label}</h4>
|
||||
<p className="text-sm text-foreground/80 leading-relaxed">{hotspot.info}</p>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<ContactCta
|
||||
tag="End of Tour"
|
||||
text="Ready to step back outside or discuss technical specifications?"
|
||||
primaryButton={{ text: "Back to Exterior", href: "#hero" }}
|
||||
secondaryButton={{ text: "Contact Engineering", href: "/contact" }}
|
||||
textAnimation="fade-blur"
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{currentView !== 'exterior' && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
className="absolute bottom-8 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-6 w-full max-w-content-width px-4"
|
||||
>
|
||||
<div className="flex flex-wrap justify-center gap-2 p-2 rounded-theme bg-card/80 backdrop-blur-md border border-foreground/10 shadow-2xl">
|
||||
{(Object.keys(VIEWS) as ViewKey[]).filter(k => k !== 'exterior').map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => handleNavigate(key)}
|
||||
className={cls(
|
||||
"px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300",
|
||||
currentView === key
|
||||
? "bg-accent text-background shadow-md shadow-accent/40"
|
||||
: "text-foreground/60 hover:text-accent hover:bg-background/50"
|
||||
)}
|
||||
>
|
||||
{VIEWS[key].title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleNavigate('exterior')}
|
||||
className="flex items-center gap-2 px-6 py-3 rounded-full bg-card/80 border border-foreground/20 text-foreground/80 hover:text-foreground hover:border-foreground/50 transition-all backdrop-blur-md"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
Back to Exterior
|
||||
</button>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="absolute top-8 left-8 z-20">
|
||||
<motion.div
|
||||
key={currentView}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className="flex flex-col gap-1"
|
||||
>
|
||||
<span className="text-accent text-sm font-bold tracking-widest uppercase">
|
||||
{currentView === 'exterior' ? 'Virtual Tour' : 'Interior Compartment'}
|
||||
</span>
|
||||
<h1 className="text-4xl md:text-5xl font-bold text-white drop-shadow-lg">
|
||||
{view.title}
|
||||
</h1>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<FooterSimple
|
||||
brand="Record Engineering (Pty) Ltd"
|
||||
copyright="© 2024 Record Engineering (Pty) Ltd. All rights reserved."
|
||||
columns={[
|
||||
{
|
||||
title: "Contact",
|
||||
items: [
|
||||
{ label: "sales@recordeng.com", href: "mailto:sales@recordeng.com" },
|
||||
{ label: "+27 11 555 0198", href: "tel:+27115550198" }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Legal",
|
||||
items: [
|
||||
{ label: "Privacy Policy", href: "/privacy" },
|
||||
{ label: "Terms of Service", href: "/terms" }
|
||||
]
|
||||
}
|
||||
]}
|
||||
links={[
|
||||
{ label: "LinkedIn", href: "#" },
|
||||
{ label: "YouTube", href: "#" }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user