Compare commits
14 Commits
version_2_
...
version_8_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
067aa9c650 | ||
|
|
8d22a47359 | ||
| f075b114bc | |||
|
|
8c5952d938 | ||
| fb0c1893fc | |||
|
|
ec986ba755 | ||
| 14058137c7 | |||
|
|
70dbf05d84 | ||
| 4c6ceaac59 | |||
|
|
9abc8a390c | ||
| d7d129cf24 | |||
|
|
2344b9a08a | ||
|
|
7f85f3fe94 | ||
| 1d55d3f241 |
@@ -2,12 +2,18 @@ import { Routes, Route } from 'react-router-dom';
|
||||
import Layout from './components/Layout';
|
||||
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>
|
||||
<Route element={<Layout />}>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/blog" element={<BlogPage />} />
|
||||
</Route>
|
||||
<Route path="/services/:slug" element={<ServiceDetailPage />} />
|
||||
<Route path="/machine-walkthrough" element={<MachineWalkthroughPage />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,11 @@ export default function Layout() {
|
||||
{
|
||||
"name": "Contact",
|
||||
"href": "#contact"
|
||||
}
|
||||
},
|
||||
{ name: "Blog", href: "/blog" },
|
||||
{ name: "Machine Walkthrough", href: "/machine-walkthrough" },
|
||||
|
||||
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
17
src/data/services.ts
Normal file
17
src/data/services.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface Service {
|
||||
slug: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
body: string;
|
||||
specs: Array<{ label: string; value: string }>;
|
||||
}
|
||||
|
||||
export const services: Service[] = [
|
||||
{"slug":"item-9f9u9","question":"Where is your primary service area?","answer":"We specialize in the Northern Cape mining and industrial regions, ensuring rapid local deployment.","body":"Our Item 1 service is designed to provide comprehensive strategic oversight for your most complex business challenges. We embed our senior consultants directly into your daily operations, ensuring a seamless integration of industry best practices with your unique company culture. This hands-on approach allows us to identify hidden bottlenecks and unlock new avenues for sustainable growth.\n\nBy leveraging proprietary analytics and decades of market experience, we transform raw data into actionable roadmaps. You will receive dedicated support throughout the entire implementation phase, minimizing downtime and maximizing your return on investment. Our team remains on call to refine these strategies as your market landscape evolves.","specs":[{"label":"Duration","value":"3-6 Months"},{"label":"Delivery Method","value":"Hybrid"},{"label":"Dedicated Experts","value":"2 Senior Consultants"},{"label":"Reporting","value":"Weekly Dashboards"}]},
|
||||
{"slug":"item-f6ct8","question":"Do you provide OEM maintenance?","answer":"Yes, we adhere strictly to OEM standards for all mechanical maintenance and repairs.","body":"Item 2 delivers robust, scalable technical solutions tailored to modern enterprise environments. We focus on building resilient infrastructure that grows alongside your business, utilizing the latest frameworks to guarantee optimal performance and security. Every deployment is rigorously tested against industry standards to ensure flawless operation from day one.\n\nOur development lifecycle prioritizes transparency and continuous feedback. You will have full visibility into our progress through regular milestone reviews and open communication channels. Once the initial build is complete, we provide comprehensive training sessions to empower your internal teams and facilitate a smooth handover.\n\nBeyond the initial launch, this service includes proactive monitoring and maintenance. We preemptively address potential vulnerabilities and apply critical updates, giving you total peace of mind and allowing your staff to focus on core business objectives.","specs":[{"label":"Tech Stack","value":"React & Node.js"},{"label":"Uptime Guarantee","value":"99.9%"},{"label":"Support SLA","value":"24/7 Priority"},{"label":"Training Included","value":"10 Hours"}]},
|
||||
{"slug":"item-tqcwk","question":"How do we request a project consultation?","answer":"Simply fill out our contact form below or call our office during business hours.","body":"Maximize your market penetration with Item 3, our fully managed growth and outreach service. We take a holistic view of your target audience, crafting highly personalized campaigns that resonate across multiple digital touchpoints. By aligning our creative assets with your core brand identity, we drive measurable engagement and foster long-term customer loyalty.\n\nContinuous optimization is at the heart of our methodology. We employ advanced split testing and real-time behavioral tracking to refine our messaging on the fly. This data-driven agility ensures that your budget is always allocated to the highest-performing channels, dramatically reducing acquisition costs over time.","specs":[{"label":"Campaign Types","value":"Omnichannel"},{"label":"Optimization","value":"Daily"},{"label":"Asset Creation","value":"Included"},{"label":"Contract Length","value":"Annual Minimum"},{"label":"Account Manager","value":"Dedicated"}]},
|
||||
];
|
||||
|
||||
export function findService(slug: string): Service | undefined {
|
||||
return services.find((x) => x.slug === slug);
|
||||
}
|
||||
19
src/pages/BlogPage.tsx
Normal file
19
src/pages/BlogPage.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import Button from "@/components/ui/Button";
|
||||
import HeroBackgroundSlot from "@/components/ui/HeroBackgroundSlot";
|
||||
import TextAnimation from "@/components/ui/TextAnimation";
|
||||
import ImageOrVideo from "@/components/ui/ImageOrVideo";
|
||||
import ScrollReveal from "@/components/ui/ScrollReveal";
|
||||
import AvatarGroup from "@/components/ui/AvatarGroup";
|
||||
import { ArrowUpRight, Loader2 } from "lucide-react";
|
||||
import GridOrCarousel from "@/components/ui/GridOrCarousel";
|
||||
import { useButtonClick } from "@/hooks/useButtonClick";
|
||||
import useBlogPosts from "@/hooks/useBlogPosts";
|
||||
|
||||
export default function BlogPage() {
|
||||
return (
|
||||
<>
|
||||
<div data-webild-section="HeroBillboard"><section aria-label="Hero section" className="relative pt-25 pb-20 md:pt-30"><HeroBackgroundSlot /><div className="flex flex-col gap-12 md:gap-15 w-content-width mx-auto"><div className="flex flex-col items-center gap-3 text-center"><AvatarGroup avatarsSrc={["https://img.freepik.com/free-photo/portrait-successful-man-having-stubble-posing-with-broad-smile-keeping-arms-folded_171337-1267.jpg","https://img.freepik.com/free-photo/young-beautiful-woman-pink-warm-sweater-natural-look-smiling-portrait-isolated-long-hair_285396-896.jpg","https://img.freepik.com/free-photo/handsome-corporate-man-real-estate-agent-assistant-smiling-holding-hands-together-how-can-i-help-smiling-camera-standing-white-background_176420-45257.jpg"]} label="Written by our experts" className="mb-1" /><TextAnimation text="Insights & Best Practices" variant="fade-blur" gradientText={true} tag="h1" className="md:max-w-8/10 text-7xl 2xl:text-8xl leading-[1.15] font-semibold text-center text-balance" /><TextAnimation text="Stay updated with the latest industry trends, technical updates, and expert insights from the team at Record Engineering." variant="fade-blur" gradientText={false} tag="p" className="md:max-w-7/10 text-lg md:text-xl leading-snug text-balance" /><div className="flex flex-wrap justify-center gap-3 mt-2 md:mt-3"><Button text="Read Latest" href="#latest-posts" variant="primary" /><Button text="Subscribe" href="#subscribe" variant="secondary" animationDelay={0.1} /></div></div><ScrollReveal variant="slide-up" delay={0.2} className="w-full p-2 xl:p-3 2xl:p-4 card rounded overflow-hidden"><ImageOrVideo imageSrc="https://img.freepik.com/free-photo/close-up-engineer-working-blueprints_1098-18833.jpg" className="aspect-4/5 md:aspect-video" /></ScrollReveal></div></section></div>
|
||||
<div data-webild-section="BlogMediaCards"><section aria-label="Blog section" className="py-20"><div className="w-content-width mx-auto flex justify-center"><Loader2 className="size-8 animate-spin text-foreground" strokeWidth={1.5} /></div></section></div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,9 +10,9 @@ export default function AboutSection(): React.JSX.Element {
|
||||
<div id="about" data-section="about">
|
||||
<SectionErrorBoundary name="about">
|
||||
<AboutText
|
||||
title="A proven partner in mining reliability. Record Engineering Pty Ltd combines local presence with technical precision to optimize lifecycle performance."
|
||||
textAnimation="fade-blur"
|
||||
/>
|
||||
title="With a track record of over 60 years as a support services company in the mining industry, we make sure our performance always exceeds expectations. We are an agile company that adapts to the changing and challenging needs of our clients. We foster a culture of discipline, detail and dynamic determination that attracts high-performing employees and results in masterful delivery. Over the years, Record Engineering has grown considerably and evolved into a key player in the mining sector, becoming a leading supplier to numerous Original Equipment Manufacturers (OEMs) and major mining operations. In Kathu and the Northern Cape we operate primarily as an engineering services provider for mining machinery and direct project management at mining plants. Our engineering service spans assembly, rebuilds and component change-outs across all mining machinery — including yellow machines and telehandlers — as well as equipment sales, maintenance and plant-design-based support. Additional support is available through our fleet and mining rental range. With a high-quality, mine-ready fleet, this is a cost-effective solution to meet our clients' fleet requirements during repair and rehabilitation."
|
||||
textAnimation="fade-blur"
|
||||
/>
|
||||
</SectionErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,59 +10,21 @@ export default function HeroSection(): React.JSX.Element {
|
||||
<div id="hero" data-section="hero">
|
||||
<SectionErrorBoundary name="hero">
|
||||
<HeroSplitVerticalMarquee
|
||||
tag="Since 2012"
|
||||
title="Engineering Excellence in the Northern Cape"
|
||||
description="Trusted project management, OEM maintenance, and lifecycle contracts for mining and heavy industry. Providing reliable regional engineering solutions."
|
||||
primaryButton={{
|
||||
tag="Record Engineering (Pty) Ltd"
|
||||
title="Engineering Excellence in the Heart of the Northern Cape"
|
||||
description="Over 60 years of trusted support services to the mining industry. Project Management • OEM Maintenance • Life Cycle Contracts"
|
||||
primaryButton={{
|
||||
text: "Explore Our Services",
|
||||
href: "#services",
|
||||
}}
|
||||
secondaryButton={{
|
||||
secondaryButton={{
|
||||
text: "Contact Us",
|
||||
href: "#contact",
|
||||
}}
|
||||
leftItems={[
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/old-weathered-house-with-mountain-range-background_53876-47060.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/computer-display-empty-facility-used-upgrading-solar-plant-machinery_482257-118310.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/aerial-view-front-end-loader-moving-soil-quarry_1308-190385.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/architecture-plan-blueprint-layout-work-concept_53876-124260.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/view-city-bridge-with-train-london_23-2149437440.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/businessman-working-desk_23-2147653318.jpg",
|
||||
},
|
||||
]}
|
||||
rightItems={[
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/male-jeweler-working-shop_23-2150914170.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/grey-concrete-building-with-multiple-windows_250224-252.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/technician-optimizing-energy-supply-solar-panel-manufacturing-plant_482257-120594.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/abstract-city-building-shadows_23-2149283224.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/business-plan-corporate-development-process_53876-124514.jpg",
|
||||
},
|
||||
{
|
||||
imageSrc: "http://img.b2bpic.net/free-photo/agricultural-silo_146671-19121.jpg",
|
||||
},
|
||||
]}
|
||||
textAnimation="slide-up"
|
||||
/>
|
||||
leftItems={[{"imageSrc":"https://images.unsplash.com/photo-1578593172074-a669ea40f372?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1531834685032-c34bf0d84c77?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1504307651254-35680f356dfd?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581092160562-40aa08e78837?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581091226825-a6a2a5aee158?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581092335397-9583eb92d232?q=80&w=2070&auto=format&fit=crop"}]}
|
||||
rightItems={[{"imageSrc":"https://images.unsplash.com/photo-1581092580497-e0d23cbdf1dc?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581092162384-8987c1d64718?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581091226033-d5c48150dbaa?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581092795360-fd1ca04f0952?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581092160607-ee22621dd758?q=80&w=2070&auto=format&fit=crop"},{"imageSrc":"https://images.unsplash.com/photo-1581092583537-20d51b4b4f1b?q=80&w=2070&auto=format&fit=crop"}]}
|
||||
textAnimation="slide-up"
|
||||
/>
|
||||
</SectionErrorBoundary>
|
||||
</div>
|
||||
);
|
||||
|
||||
241
src/pages/MachineWalkthroughPage.tsx
Normal file
241
src/pages/MachineWalkthroughPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
src/pages/ServiceDetailPage.tsx
Normal file
30
src/pages/ServiceDetailPage.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useParams, Link, Navigate } from 'react-router-dom';
|
||||
import { findService } from '@/data/services';
|
||||
|
||||
export default function ServiceDetailPage() {
|
||||
const { slug } = useParams<{ slug: string }>();
|
||||
const service = slug ? findService(slug) : undefined;
|
||||
if (!service) return <Navigate to="/services" replace />;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-screen bg-background text-foreground">
|
||||
<main className="flex-grow pt-24">
|
||||
<div className="w-content-width mx-auto py-16 flex flex-col gap-8">
|
||||
<Link to="/services" className="text-sm opacity-60 hover:opacity-100 w-fit">← Back to Services</Link>
|
||||
<div className="flex flex-col gap-4 max-w-3xl">
|
||||
<h1 className="text-5xl font-bold">{service.question}</h1>
|
||||
{service.body && <div className="text-base leading-relaxed opacity-80 whitespace-pre-line">{service.body}</div>}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{service.answer != null && (<div className="flex flex-col"><span className="text-xs uppercase tracking-wide opacity-50">Answer</span><span className="text-base">{String(service.answer)}</span></div>)}
|
||||
</div>
|
||||
{Array.isArray(service.specs) && service.specs.length > 0 && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 border-t border-foreground/10 pt-6">
|
||||
{service.specs.map((s, i) => (<div key={i} className="flex flex-col"><span className="text-xs uppercase tracking-wide opacity-50">{s.label}</span><span className="text-base">{s.value}</span></div>))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,4 +6,6 @@ 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' },
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user