Merge version_8_1783098560292 into main #7

Merged
bender merged 2 commits from version_8_1783098560292 into main 2026-07-03 17:14:27 +00:00
4 changed files with 246 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import HomePage from './pages/HomePage';
import BlogPage from "@/pages/BlogPage";
import ServiceDetailPage from './pages/ServiceDetailPage';
import MachineWalkthroughPage from "@/pages/MachineWalkthroughPage";
export default function App() {
return (
<Routes>
@@ -12,6 +13,7 @@ export default function App() {
<Route path="/blog" element={<BlogPage />} />
</Route>
<Route path="/services/:slug" element={<ServiceDetailPage />} />
<Route path="/machine-walkthrough" element={<MachineWalkthroughPage />} />
</Routes>
);
}

View File

@@ -24,6 +24,8 @@ export default function Layout() {
"href": "#contact"
},
{ name: "Blog", href: "/blog" },
{ name: "Machine Walkthrough", href: "/machine-walkthrough" },
];

View File

@@ -0,0 +1,241 @@
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
import { routes } from "@/routes";
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-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>
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/40 to-transparent pointer-events-none" />
<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>
<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>
</div>
);
}

View File

@@ -7,4 +7,5 @@ export interface Route {
export const routes: Route[] = [
{ path: '/', label: 'Home', pageFile: 'HomePage' },
{ path: '/blog', label: 'Blog', pageFile: 'BlogPage' },
{ path: '/machine-walkthrough', label: 'Machine Walkthrough', pageFile: 'MachineWalkthroughPage' },
];