15 Commits

Author SHA1 Message Date
56a1d63906 Merge version_8_1783098560292 into main
Merge version_8_1783098560292 into main
2026-07-03 17:14:27 +00:00
kudinDmitriyUp
067aa9c650 Bob AI: Populate the newly-created page at src/pages/MachineWalkthro 2026-07-03 17:13:45 +00:00
kudinDmitriyUp
8d22a47359 Bob AI: Add machine-walkthrough page 2026-07-03 17:10:24 +00:00
f075b114bc Merge version_7_1783075702690 into main
Merge version_7_1783075702690 into main
2026-07-03 10:49:01 +00:00
kudinDmitriyUp
8c5952d938 Bob AI: Add service detail pages (dynamic route /services/:slug) 2026-07-03 10:48:55 +00:00
fb0c1893fc Merge version_6_1783075595252 into main
Merge version_6_1783075595252 into main
2026-07-03 10:48:05 +00:00
kudinDmitriyUp
ec986ba755 Bob AI: Fix broken image link in hero marquee 2026-07-03 10:47:20 +00:00
14058137c7 Merge version_5_1783075537741 into main
Merge version_5_1783075537741 into main
2026-07-03 10:46:10 +00:00
kudinDmitriyUp
70dbf05d84 Bob AI: Add service detail pages (dynamic route /services/:slug) 2026-07-03 10:46:07 +00:00
4c6ceaac59 Merge version_4_1783075093266 into main
Merge version_4_1783075093266 into main
2026-07-03 10:40:40 +00:00
kudinDmitriyUp
9abc8a390c Bob AI: Update the website with the following industrial-modern desi 2026-07-03 10:40:02 +00:00
d7d129cf24 Merge version_3_1783062879841 into main
Merge version_3_1783062879841 into main
2026-07-03 07:16:47 +00:00
kudinDmitriyUp
2344b9a08a Bob AI: Populate src/pages/BlogPage.tsx (snippet builder, 2 sections) 2026-07-03 07:16:08 +00:00
kudinDmitriyUp
7f85f3fe94 Bob AI: Add blog page 2026-07-03 07:15:32 +00:00
1d55d3f241 Merge version_2_1783062361385 into main
Merge version_2_1783062361385 into main
2026-07-03 07:08:54 +00:00
9 changed files with 332 additions and 51 deletions

View File

@@ -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>
);
}

View File

@@ -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
View 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
View 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>
</>
);
}

View File

@@ -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>
);

View File

@@ -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>
);

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

@@ -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>
);
}

View File

@@ -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' },
];