feat: add robotics components and data structures
CI/CD / test-and-build (push) Waiting to run
CI/CD / deploy (push) Blocked by required conditions

- 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:
Najjar\NajjarV02
2026-05-20 17:22:47 +04:00
parent 2eac53dac3
commit 92cf4aba3b
87 changed files with 6887 additions and 1430 deletions
+125
View File
@@ -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>
);
}
+226
View File
@@ -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>
);
}
+37
View File
@@ -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 />
</>
);
}