feat: add robotics components and data structures
- Introduced RobotProductCard component for displaying robot details. - Added WhyUs component highlighting key reasons for choosing our robotics solutions. - Implemented CursorSpotlight for enhanced user interaction. - Created GlassPanel for a stylish UI element. - Developed MotionSection for animated section visibility. - Added PremiumButton for versatile button options. - Established data structures for industries and robots, including detailed specifications and use cases. - Included utility functions for retrieving robots by slug and category.
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useMemo, useState } from 'react';
|
||||
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
||||
import { ROBOTS, CATEGORY_LABELS, BRANDS, type RobotCategory, type RobotBrand } from '@/data/robots';
|
||||
import { RobotProductCard } from '@/components/robotics/RobotProductCard';
|
||||
import { ProductFilterTabs, type Tab } from '@/components/robotics/ProductFilterTabs';
|
||||
|
||||
type Filter = { brand: RobotBrand | 'all'; category: RobotCategory | 'all' };
|
||||
|
||||
function CatalogInner() {
|
||||
const params = useSearchParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const initialBrand = (params.get('brand') as RobotBrand) || 'all';
|
||||
const initialCategory = (params.get('category') as RobotCategory) || 'all';
|
||||
|
||||
const [filter, setFilter] = useState<Filter>({ brand: initialBrand, category: initialCategory });
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return ROBOTS.filter((r) => {
|
||||
if (filter.brand !== 'all' && r.brand !== filter.brand) return false;
|
||||
if (filter.category !== 'all' && !r.categories.includes(filter.category)) return false;
|
||||
return true;
|
||||
});
|
||||
}, [filter]);
|
||||
|
||||
const updateFilter = (next: Partial<Filter>) => {
|
||||
const newFilter = { ...filter, ...next };
|
||||
setFilter(newFilter);
|
||||
const sp = new URLSearchParams();
|
||||
if (newFilter.brand !== 'all') sp.set('brand', newFilter.brand);
|
||||
if (newFilter.category !== 'all') sp.set('category', newFilter.category);
|
||||
const qs = sp.toString();
|
||||
router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false });
|
||||
};
|
||||
|
||||
const brandTabs: Tab[] = [
|
||||
{ id: 'all', label: 'All brands' },
|
||||
...(Object.keys(BRANDS) as RobotBrand[]).map((b) => ({
|
||||
id: b,
|
||||
label: BRANDS[b].name.split(' ')[0],
|
||||
accent: BRANDS[b].accent,
|
||||
})),
|
||||
];
|
||||
|
||||
const categoryTabs: Tab[] = [
|
||||
{ id: 'all', label: 'All' },
|
||||
...(Object.keys(CATEGORY_LABELS) as RobotCategory[]).map((c) => ({
|
||||
id: c,
|
||||
label: CATEGORY_LABELS[c],
|
||||
})),
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<div className="glass" style={{ padding: '1rem 1.25rem', borderRadius: '1.25rem', display: 'flex', flexWrap: 'wrap', gap: '1.5rem' }}>
|
||||
<ProductFilterTabs
|
||||
title="Brand"
|
||||
tabs={brandTabs}
|
||||
activeId={filter.brand}
|
||||
onChange={(id) => updateFilter({ brand: id as RobotBrand | 'all' })}
|
||||
/>
|
||||
<ProductFilterTabs
|
||||
title="Category"
|
||||
tabs={categoryTabs}
|
||||
activeId={filter.category}
|
||||
onChange={(id) => updateFilter({ category: id as RobotCategory | 'all' })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
<span style={{ color: '#94908a', fontSize: '0.85rem' }}>
|
||||
{filtered.length} robot{filtered.length === 1 ? '' : 's'} found
|
||||
</span>
|
||||
{(filter.brand !== 'all' || filter.category !== 'all') && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateFilter({ brand: 'all', category: 'all' })}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
color: '#e0c896',
|
||||
fontSize: '0.78rem',
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="card" style={{ padding: '2rem', textAlign: 'center', color: '#cbc4b3' }}>
|
||||
No robots match these filters yet. Try a different brand or category, or{' '}
|
||||
<a href="/contact/" style={{ color: '#e0c896', textDecoration: 'none' }}>contact us</a> for tailored options.
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1.25rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{filtered.map((r) => (
|
||||
<RobotProductCard key={r.id} robot={r} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CatalogClient() {
|
||||
return (
|
||||
<Suspense fallback={<div className="card" style={{ padding: '2rem', color: '#cbc4b3' }}>Loading catalog…</div>}>
|
||||
<CatalogInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { InquiryForm } from '@/components/robotics/InquiryForm';
|
||||
import { RobotProductCard } from '@/components/robotics/RobotProductCard';
|
||||
import { DemoCTA } from '@/components/robotics/DemoCTA';
|
||||
import { ConfigureCTA } from '@/components/robotics/ConfigureCTA';
|
||||
import { ProductSpecTable } from '@/components/robotics/ProductSpecTable';
|
||||
import { ProductGallery } from '@/components/robotics/ProductGallery';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { ROBOTS, getRobotBySlug, BRANDS, CATEGORY_LABELS } from '@/data/robots';
|
||||
|
||||
type Params = { slug: string };
|
||||
|
||||
export function generateStaticParams() {
|
||||
return ROBOTS.map((r) => ({ slug: r.slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const robot = getRobotBySlug(slug);
|
||||
if (!robot) return { title: 'Robot not found — YS Lootah Robotics' };
|
||||
return {
|
||||
title: `${robot.brandLabel} ${robot.name} — Available in Dubai | YS Lootah Robotics`,
|
||||
description: `${robot.shortDescription} Available through YS Lootah Robotics in Dubai — request a price or book a live demo.`,
|
||||
keywords: [robot.brandLabel, robot.name, 'Dubai', 'UAE', CATEGORY_LABELS[robot.category], 'robotics'],
|
||||
};
|
||||
}
|
||||
|
||||
export default async function RobotDetailPage({ params }: { params: Promise<Params> }) {
|
||||
const { slug } = await params;
|
||||
const robot = getRobotBySlug(slug);
|
||||
if (!robot) notFound();
|
||||
|
||||
const brand = BRANDS[robot.brand];
|
||||
const related = ROBOTS.filter((r) => r.id !== robot.id && (r.brand === robot.brand || r.category === robot.category)).slice(0, 3);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
<main style={{ paddingTop: 'clamp(6rem, 10vw, 8rem)', paddingBottom: 'clamp(4rem, 8vw, 6rem)' }}>
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: 'clamp(3rem, 6vw, 5rem)' }}>
|
||||
<nav style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem', fontSize: '0.78rem', letterSpacing: '0.14em', textTransform: 'uppercase', color: '#94908a' }}>
|
||||
<Link href="/" style={{ color: '#94908a', textDecoration: 'none' }}>Home</Link>
|
||||
<span>/</span>
|
||||
<Link href="/robots/" style={{ color: '#94908a', textDecoration: 'none' }}>Robots</Link>
|
||||
<span>/</span>
|
||||
<span style={{ color: '#cbc4b3' }}>{robot.name}</span>
|
||||
</nav>
|
||||
|
||||
<section
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: 'clamp(2rem, 5vw, 3rem)',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(320px, 100%), 1fr))',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<ProductGallery
|
||||
images={[{ src: robot.image, alt: `${robot.brandLabel} ${robot.name}` }]}
|
||||
accent={robot.accent}
|
||||
/>
|
||||
{robot.imageType === 'placeholder' && (
|
||||
<span
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '0.75rem',
|
||||
left: '0.75rem',
|
||||
padding: '0.3rem 0.55rem',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(5, 5, 5,0.6)',
|
||||
border: '1px solid rgba(196, 162, 101,0.2)',
|
||||
color: '#94908a',
|
||||
fontSize: '0.62rem',
|
||||
letterSpacing: '0.2em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Brand visual placeholder
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<span className="eyebrow" style={{ color: robot.accent }}>
|
||||
<span style={{ width: 6, height: 6, borderRadius: 999, background: robot.accent }} /> {brand.name} · {CATEGORY_LABELS[robot.category]}
|
||||
</span>
|
||||
<h1 style={{ margin: 0, fontSize: 'clamp(2rem, 5vw, 3.4rem)', fontWeight: 300, lineHeight: 1.05, letterSpacing: '-0.03em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>
|
||||
{robot.name}
|
||||
</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#e0c896', fontSize: '1.05rem', lineHeight: 1.5 }}>{robot.tagline}</p>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: '1rem', lineHeight: 1.7 }}>{robot.longDescription}</p>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.625rem' }}>
|
||||
<a className="btn btn-primary" href="#inquire">Request quotation</a>
|
||||
<a className="btn btn-ghost" href="/book-demo/">Book a live demo</a>
|
||||
{robot.configureSlug && (
|
||||
<a className="btn btn-outline" href={robot.configureSlug}>Configure for your business</a>
|
||||
)}
|
||||
{robot.officialUrl && (
|
||||
<a className="btn btn-ghost" href={robot.officialUrl} target="_blank" rel="noopener noreferrer">
|
||||
Brand site
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<MotionSection>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
<Block title="Key features">
|
||||
<ul style={{ margin: 0, paddingLeft: '1.1rem', display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
|
||||
{robot.features.map((f) => (
|
||||
<li key={f} style={{ color: '#cbc4b3', lineHeight: 1.6 }}>{f}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Block>
|
||||
<Block title="Use cases">
|
||||
<ul style={{ margin: 0, paddingLeft: '1.1rem', display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
|
||||
{robot.useCases.map((u) => (
|
||||
<li key={u} style={{ color: '#cbc4b3', lineHeight: 1.6 }}>{u}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Block>
|
||||
<Block title="Technical specs">
|
||||
<ProductSpecTable specs={robot.specs} />
|
||||
</Block>
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection id="inquire">
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1.5rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(300px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
<div className="card" style={{ padding: 'clamp(1.5rem, 4vw, 2.25rem)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginBottom: '1.5rem' }}>
|
||||
<span className="eyebrow">Inquiry</span>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.5rem, 3vw, 2rem)', fontWeight: 400, letterSpacing: '-0.02em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>
|
||||
Request a quotation for {robot.name}.
|
||||
</span>
|
||||
</h2>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', lineHeight: 1.7 }}>
|
||||
Tell us about your venue, timeline, and use case. We will respond with availability, configuration options, and pricing for the UAE.
|
||||
</p>
|
||||
</div>
|
||||
<InquiryForm defaultRobot={`${robot.brandLabel} ${robot.name}`} defaultCategory={robot.category} intent="quotation" />
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: 'clamp(1.5rem, 4vw, 2.25rem)', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<span className="eyebrow">Talk to an advisor</span>
|
||||
<h3 style={{ margin: 0, fontSize: '1.25rem', fontWeight: 600 }}>Prefer a quick conversation?</h3>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', lineHeight: 1.7 }}>
|
||||
Call our Dubai team or message us on WhatsApp — we will share availability and demo slots for {robot.name}.
|
||||
</p>
|
||||
<a className="btn btn-ghost" href="tel:+971559482728">Call +971 55 948 2728</a>
|
||||
<a className="btn btn-outline" href="https://wa.me/971559482728" target="_blank" rel="noopener noreferrer">WhatsApp us</a>
|
||||
<a className="btn btn-ghost" href="mailto:info@yslootahtech.com">Email info@yslootahtech.com</a>
|
||||
</div>
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection>
|
||||
<ConfigureCTA
|
||||
title={`Configure ${robot.name} for your business.`}
|
||||
description="Choose persona, attire, color, and accessories — visualize your deployment before requesting a quotation."
|
||||
/>
|
||||
</MotionSection>
|
||||
|
||||
{related.length > 0 && (
|
||||
<MotionSection>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<div>
|
||||
<span className="eyebrow">Related robots</span>
|
||||
<h2 style={{ margin: '0.5rem 0 0', fontSize: 'clamp(1.5rem, 3vw, 2rem)', fontWeight: 400 }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>You might also consider…</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1.25rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{related.map((r) => (
|
||||
<RobotProductCard key={r.id} robot={r} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</MotionSection>
|
||||
)}
|
||||
|
||||
<DemoCTA title="Ready to bring this robot to your venue?" />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Block({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="card" style={{ padding: '1.5rem' }}>
|
||||
<div className="eyebrow" style={{ marginBottom: '1rem' }}>{title}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { CatalogClient } from './CatalogClient';
|
||||
import { ROBOTS } from '@/data/robots';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Robots Catalog — YS Lootah Robotics Dubai',
|
||||
description:
|
||||
'Explore selected humanoid, quadruped, service, delivery, hospitality, and cleaning robots from Unitree and Pudu — available exclusively in the UAE through YS Lootah Robotics.',
|
||||
};
|
||||
|
||||
export default function RobotsPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
<main style={{ paddingTop: 'clamp(6rem, 10vw, 8rem)', paddingBottom: 'clamp(4rem, 8vw, 6rem)' }}>
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2.5rem' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: 760 }}>
|
||||
<span className="eyebrow">Robot Catalog · Dubai · UAE</span>
|
||||
<h1 style={{ margin: 0, fontSize: 'clamp(2rem, 5vw, 3.4rem)', fontWeight: 300, lineHeight: 1.05, letterSpacing: '-0.03em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>
|
||||
Robots ready to deploy across the UAE.
|
||||
</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
Browse {ROBOTS.length}+ models across our portfolio. Filter by brand or category, then request a price or book a live demo at our Dubai showroom.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CatalogClient />
|
||||
</div>
|
||||
</main>
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user