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.
236
.claude/skills/premium-frontend-designer/COMPONENT_RULES.md
Normal file
@ -0,0 +1,236 @@
|
||||
# Component Rules
|
||||
|
||||
Per-element rules. Apply when designing or reviewing the corresponding component.
|
||||
|
||||
---
|
||||
|
||||
## Buttons
|
||||
|
||||
**Primary**
|
||||
- Gradient gold: `bg-gradient-to-r from-gold-light via-gold to-gold-deep`
|
||||
- Text: `text-obsidian` (dark on gold for contrast)
|
||||
- Shape: `rounded-full px-7 py-3.5 text-sm font-medium tracking-wide`
|
||||
- Shadow: `shadow-[0_10px_40px_-12px_rgba(212,164,55,0.6)]`
|
||||
- Hover shadow only: `hover:shadow-[0_18px_50px_-12px_rgba(242,194,91,0.8)]`
|
||||
- Optional `magnetic` wrapper for cursor pull effect on desktop.
|
||||
|
||||
**Outline**
|
||||
- `border border-bone/20 text-bone hover:border-gold/60 hover:text-gold-light`
|
||||
- Same shape as primary.
|
||||
|
||||
**Ghost**
|
||||
- `text-bone hover:text-gold-light` — no border, no fill.
|
||||
|
||||
**Forbidden**
|
||||
- Bright red CTA buttons (unless brand color is red).
|
||||
- Square corners on action buttons.
|
||||
- Solid `#000` or `#fff` backgrounds.
|
||||
- Loading text inside button that breaks layout — use a spinner icon + word swap with fixed min-width.
|
||||
- Success/error message **inside** button — render outside as a banner.
|
||||
|
||||
---
|
||||
|
||||
## Cards
|
||||
|
||||
**Standard card**
|
||||
```tsx
|
||||
<div className="group relative isolate h-full overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/30 hover:bg-graphite/60 md:p-7">
|
||||
{/* hover amber glow (decorative) */}
|
||||
{/* content */}
|
||||
{/* gold underline sweep at bottom */}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Premium / featured card** (used for cert card, sale card, hero CTA)
|
||||
- `border-gold/25`
|
||||
- `bg-gradient-to-br from-[#1a1407] via-[#0c0a06] to-[#1e1409]`
|
||||
- `shadow-[0_30px_120px_-30px_rgba(212,164,55,0.45)]`
|
||||
- Often has animated `shimmer` hairline on top edge.
|
||||
|
||||
**Grid layout**
|
||||
- `grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-N gap-4 md:gap-5`
|
||||
- Apply `cardGrid` + `cardCell` variants for staggered entry.
|
||||
|
||||
**Inside the card**
|
||||
- Top row: small icon disc (`size-11 glass-gold`) + optional mono micro-label.
|
||||
- Heading: `font-display text-2xl md:text-3xl`.
|
||||
- Body: `text-sm text-mist leading-relaxed`.
|
||||
- Bottom: optional "Learn more" link or gold underline sweep.
|
||||
|
||||
---
|
||||
|
||||
## Section heading
|
||||
|
||||
```tsx
|
||||
<div className="flex flex-col items-center gap-6 text-center"> {/* or items-start for left-aligned */}
|
||||
<SectionLabel index="01">Section name</SectionLabel>
|
||||
<Reveal>
|
||||
<h2 className="font-display text-5xl leading-[1.02] text-bone text-balance md:text-6xl">
|
||||
Plain words and a{" "}
|
||||
<span className="gold-text">gold accent phrase.</span>
|
||||
</h2>
|
||||
</Reveal>
|
||||
<Reveal delay={0.1}>
|
||||
<p className="max-w-2xl text-mist text-pretty md:text-lg">
|
||||
One refined sentence describing the section.
|
||||
</p>
|
||||
</Reveal>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Rules**
|
||||
- Always `text-balance` on h2 for clean wrapping.
|
||||
- `text-pretty` on subtitle.
|
||||
- Use SectionLabel with sequential index per page (01, 02, 03…). No mono font.
|
||||
- One gold-accent phrase max per heading — overdoing gold cheapens it.
|
||||
|
||||
---
|
||||
|
||||
## Navbar
|
||||
|
||||
- Fixed top: `fixed inset-x-0 top-0 z-50`.
|
||||
- Logo left, nav center/right, primary CTA + WhatsApp icon right.
|
||||
- Scrolled state: `backdrop-blur-xl bg-obsidian/70 border-b border-bone/5`.
|
||||
- Nav links: `text-sm font-medium tracking-[0.005em] text-bone/85 hover:text-gold-light`.
|
||||
- Active state via `usePathname()` → `text-gold-light` + full-width underline + `aria-current="page"`.
|
||||
- Mobile: hamburger right, menu drawer below, links close on click.
|
||||
|
||||
---
|
||||
|
||||
## Footer
|
||||
|
||||
**Layout — 4 columns, no duplication**
|
||||
|
||||
| Column 1 | Column 2 | Column 3 | Column 4 |
|
||||
|---|---|---|---|
|
||||
| Brand + contact | Navigation | Services | Legal & App |
|
||||
| Logo, tagline, phone, email, WhatsApp, Instagram | Home + all top-level routes | Specific service items | Privacy Policy + 2 store badges |
|
||||
|
||||
**Rules**
|
||||
- No page link appears in two columns.
|
||||
- Privacy Policy lives only under Legal.
|
||||
- App badges only in Legal & App column (footer) — official SVGs at 132 px width, 8 px gap.
|
||||
- External links open in new tab with rel attrs.
|
||||
- Background: `bg-obsidian` with subtle "LUXAM"-style watermark behind content (translate-y-[18%], opacity 0.025).
|
||||
- Bottom row: `© YYYY {brand}. All rights reserved.` + tagline + agency credit (subtle, gold-underlined on hover).
|
||||
|
||||
---
|
||||
|
||||
## Forms
|
||||
|
||||
- Field wrapper: `<label>` with small uppercase metadata label + input.
|
||||
- Input class:
|
||||
```
|
||||
peer w-full rounded-xl border border-bone/12 bg-obsidian/60
|
||||
px-4 py-3.5 text-sm text-bone placeholder-mist/40 outline-none
|
||||
transition-colors duration-300
|
||||
focus:border-gold/60 focus:bg-obsidian/80
|
||||
focus:shadow-[0_0_0_3px_rgba(212,164,55,0.18)]
|
||||
aria-[invalid=true]:border-[#a3261c]/60
|
||||
```
|
||||
- Honeypot field: hidden `<input name="website">` — silently succeed if filled.
|
||||
- Validation: server-side via `submitContact()` action; client gets `state.fieldErrors` + `state.message`.
|
||||
- Submission states: `useActionState` + `useFormStatus`. Spinner inside button, success/error banner **outside** button.
|
||||
|
||||
---
|
||||
|
||||
## Icons
|
||||
|
||||
- Source: `lucide-react` (already installed).
|
||||
- Size: `size-4` inline, `size-5` standalone, `size-[18px]` inside discs.
|
||||
- StrokeWidth: `1.4` for elegance (not the default 2).
|
||||
- Wrapper for emphasis: `glass-gold size-11 grid place-items-center rounded-full`.
|
||||
- **Forbidden**: oversized icons (>32 px) standalone, multi-color icon sets, cartoonish flat icons.
|
||||
|
||||
**No lucide WhatsApp glyph** — use custom inline SVG from `snippets/WhatsAppLink.tsx`.
|
||||
|
||||
**No lucide Instagram glyph in v1.16** — use custom inline SVG (see footer).
|
||||
|
||||
---
|
||||
|
||||
## App store badges
|
||||
|
||||
- Use **official SVG badges**, not custom-drawn pills with text.
|
||||
- Sources: svgrepo.com `/show/<id>/<slug>.svg` (downloads clean SVG).
|
||||
- Store locally in `public/images/app-store-badge.svg` + `public/images/google-play-badge.svg`.
|
||||
- Render via plain `<img>` (next/image blocks SVG by default in Next 16).
|
||||
- Width: `132 px` footer, `140-160 px` hero CTA section.
|
||||
- Hover: `opacity-85` + `-translate-y-0.5`. No gold circles around them.
|
||||
- Stack with `gap-2` (8 px) in footer.
|
||||
|
||||
```tsx
|
||||
<a href={app.appStore} target="_blank" rel="noopener noreferrer" aria-label="Download Luxam on the App Store"
|
||||
className="inline-flex transition duration-300 hover:-translate-y-0.5 hover:opacity-85">
|
||||
<img src="/images/app-store-badge.svg" alt="Download on the App Store"
|
||||
className="h-auto w-[132px] max-w-full" loading="lazy" />
|
||||
</a>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WhatsApp link
|
||||
|
||||
- URL form: `https://api.whatsapp.com/send/?phone=<digits-only>&text&type=phone_number&app_absent=0`
|
||||
- Phone in URL: country code + number, **no `+` or spaces**.
|
||||
- Display phone format: `+CC XX XXX XXXX`.
|
||||
- Anchor: `target="_blank" rel="noopener noreferrer" aria-label="Chat with [Brand] on WhatsApp"`.
|
||||
- Icon: inline SVG (lucide has no WhatsApp glyph). Refined gold tint, not bright green.
|
||||
- Two surfaces: nav (circular icon button) + footer (icon + display number).
|
||||
- Optional third surface: contact page row.
|
||||
|
||||
---
|
||||
|
||||
## Client logo section
|
||||
|
||||
- Horizontal marquee, `animation: marquee 50s linear infinite`.
|
||||
- Hover pauses via `onMouseEnter/Leave` setting `animationPlayState`.
|
||||
- Duplicate the data array `[...clients, ...clients]` for seamless loop.
|
||||
- Tiles: `h-24 w-44 sm:h-28 sm:w-52 rounded-2xl border border-bone/10 bg-graphite/40 backdrop-blur-sm`.
|
||||
- Logos: `object-contain max-h-14 sm:max-h-16`. Default `grayscale opacity-70`. Hover: `grayscale-0 opacity-100 scale-105`.
|
||||
- Edge fade via `mask-image: linear-gradient(90deg, transparent, black 8%, black 92%, transparent)`.
|
||||
- Reduced-motion fallback: render a static `grid-cols-2 sm:grid-cols-3 lg:grid-cols-6`.
|
||||
|
||||
---
|
||||
|
||||
## Certificate gallery
|
||||
|
||||
- 4-col on desktop, 2-col tablet, 1-col mobile.
|
||||
- Each tile: portrait `aspect-[3/4]` with `next/image fill object-cover object-top`.
|
||||
- Top-left badge: page indicator (`Page 1`, `Page 2 · Annex`).
|
||||
- Top-right: ArrowUpRight disc (links open in new tab).
|
||||
- Click anywhere on tile opens the PDF.
|
||||
- Hover: scale 1.04 (1.2s ease-out), gold ring border, amber radial glow, caption title → gold-light.
|
||||
- Caption below image: `font-mono` micro-label (document name in gold) + title + "Open PDF" CTA.
|
||||
- Optional "Need the full PDFs?" pill row below grid for direct downloads.
|
||||
|
||||
---
|
||||
|
||||
## Final CTA section
|
||||
|
||||
- Wraps inside a premium gold-bordered glass card.
|
||||
- Animated halo (`!reduce && motion.div opacity pulse`).
|
||||
- Grid: `lg:grid-cols-12` (8/4 split).
|
||||
- Left col: SectionLabel + h2 with one gold-accent phrase + paragraph.
|
||||
- Right col: 2 buttons (`primary` + `outline`).
|
||||
- Place at the end of every long page, before the footer.
|
||||
|
||||
---
|
||||
|
||||
## Privacy / Legal pages
|
||||
|
||||
- Single-column max-w-3xl reading layout.
|
||||
- Each section gets `id="..."` + `scroll-mt-28` for anchored navigation.
|
||||
- `font-display` h2 per section, `text-mist` body, `text-pretty` on every `<p>`.
|
||||
- Effective date + `metallic-divider` at top.
|
||||
- Bottom disclaimer: `text-sm text-mist/70` with mailto link.
|
||||
|
||||
---
|
||||
|
||||
## Don't
|
||||
|
||||
- Don't add hover effects without `transition-*` for smoothness.
|
||||
- Don't use `transform-origin` defaults on rotated icon discs — keep them small (8-12°).
|
||||
- Don't use `whileHover={{ scale: 1.1 }}` on cards — too theatrical. Use `-translate-y-1` instead.
|
||||
- Don't add box-shadows with multiple layers and bright colors. One subtle amber glow max.
|
||||
- Don't introduce a new font family per page — stick to display + sans.
|
||||
- Don't render placeholder images (`via.placeholder.com`, gray boxes) in production code. Always use real assets the user provided.
|
||||
@ -0,0 +1,45 @@
|
||||
# Component Snippets
|
||||
|
||||
Overview of the reusable TSX snippets in `snippets/`. Each is a copy-paste base for a new project — adapt brand tokens + content via your own `lib/content.ts`.
|
||||
|
||||
| File | Purpose | Depends on |
|
||||
|---|---|---|
|
||||
| `PremiumButton.tsx` | Gold-gradient primary + outline + ghost variants, optional magnetic wrapper | `lib/cn`, lucide ArrowUpRight |
|
||||
| `SectionHeading.tsx` | Premium section heading w/ eyebrow + serif h2 + sub paragraph | `lib/cn`, framer Reveal |
|
||||
| `GlassCard.tsx` | Dark glass card w/ hover lift, amber glow, gold underline sweep | lucide LucideIcon |
|
||||
| `Footer.tsx` | 4-column footer w/ no duplication, watermark, agency credit | brand contact data |
|
||||
| `AppBadges.tsx` | Official App Store + Google Play badge anchors (plain `<img>`) | `/public/images/*-badge.svg` |
|
||||
| `WhatsAppLink.tsx` | Refined gold WhatsApp icon button + label variant | inline SVG (no lucide glyph) |
|
||||
| `ClientLogoMarquee.tsx` | Auto-scroll logo marquee w/ hover pause + reduced-motion grid fallback | logos array, marquee keyframe in globals.css |
|
||||
| `CertificateGallery.tsx` | 4-image bento gallery linking to PDFs in new tab | `next/image`, certs array |
|
||||
|
||||
## How to use
|
||||
|
||||
1. Copy the file you need into your project's `src/components/` (or wherever your component layer lives).
|
||||
2. Update import paths (`@/lib/cn`, `@/components/ui/...`) to match your project alias.
|
||||
3. Wire data: the snippets accept props or read from a `lib/content.ts` you control.
|
||||
4. Confirm Tailwind v4 `@theme` tokens (`obsidian`, `gold`, `bone`, `mist`, etc.) exist — see `DESIGN_SYSTEM.md`.
|
||||
5. Confirm Framer Motion + lucide-react installed:
|
||||
```bash
|
||||
npm i framer-motion lucide-react
|
||||
```
|
||||
|
||||
## Adapt patterns
|
||||
|
||||
The snippets show the **shape**. Your brand may need:
|
||||
- Different palette (warm copper instead of gold? `--color-gold` → your token, keep the same usage rules).
|
||||
- Different copy hierarchy (more / fewer columns in footer).
|
||||
- Different surfaces (e.g., add a Resources column).
|
||||
|
||||
Keep the **interaction grammar** consistent:
|
||||
- Hover lift `-translate-y-1`.
|
||||
- Amber/gold glow bottom-right.
|
||||
- Underline sweep on cards.
|
||||
- 1.4 strokeWidth on icons.
|
||||
- Serif on h1/h2 only.
|
||||
|
||||
That's what makes any palette feel "Luxam-class" premium.
|
||||
|
||||
## Don't fork without reason
|
||||
|
||||
If a snippet does 95% of what you need, **wrap** it with project-specific props rather than forking. Forks drift.
|
||||
204
.claude/skills/premium-frontend-designer/DESIGN_SYSTEM.md
Normal file
@ -0,0 +1,204 @@
|
||||
# Design System
|
||||
|
||||
Reusable design tokens + scales extracted from the Luxam reference build. Drop into `globals.css` (Tailwind v4 `@theme`) of any new project to start with the same DNA.
|
||||
|
||||
---
|
||||
|
||||
## Color palette
|
||||
|
||||
```css
|
||||
@theme {
|
||||
/* Backgrounds — obsidian → graphite layering */
|
||||
--color-obsidian: #08080A; /* page background */
|
||||
--color-graphite: #15161A; /* dark glass card */
|
||||
--color-graphite-2: #1C1D22; /* slightly lifted card variant */
|
||||
|
||||
/* Foreground */
|
||||
--color-bone: #F4F0E6; /* primary text on dark */
|
||||
--color-mist: #C7C2B5; /* secondary/muted text */
|
||||
|
||||
/* Brand — gold spectrum (use sparingly + structurally) */
|
||||
--color-gold: #D4A437; /* core brand gold */
|
||||
--color-gold-light: #F2C25B; /* hover, highlight, gradient stops */
|
||||
--color-gold-deep: #7A4A12; /* gradient base */
|
||||
--color-ember: #B5731A; /* warm secondary */
|
||||
|
||||
/* Sustainability accent (use only where ecology theme applies) */
|
||||
--color-emerald: #1F8F6B;
|
||||
--color-emerald-glow: #3FD8A4;
|
||||
}
|
||||
```
|
||||
|
||||
**Usage rules**
|
||||
- **Background:** `bg-obsidian` body, `bg-graphite/40 backdrop-blur-sm` cards.
|
||||
- **Borders:** `border-bone/10` default, `border-gold/30` on hover, `border-gold/40` active.
|
||||
- **Text:** `text-bone` titles, `text-mist` body/secondary, `text-gold-light` brand accents.
|
||||
- **Never use** raw `#fff` or pure `#000`. Always `bone` or `obsidian`.
|
||||
- **Gradients:** single-axis brand gradient `from-gold-light via-gold to-gold-deep`. No rainbow.
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
```ts
|
||||
// app/layout.tsx
|
||||
import { Cormorant_Garamond, Geist, Geist_Mono } from "next/font/google";
|
||||
|
||||
const display = Cormorant_Garamond({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-display",
|
||||
weight: ["400", "500", "600"],
|
||||
display: "swap",
|
||||
});
|
||||
const sans = Geist({ subsets: ["latin"], variable: "--font-sans", display: "swap" });
|
||||
const mono = Geist_Mono({ subsets: ["latin"], variable: "--font-mono", display: "swap" });
|
||||
```
|
||||
|
||||
**Hierarchy**
|
||||
|
||||
| Role | Font | Size |
|
||||
|---|---|---|
|
||||
| Hero H1 | `font-display` (serif) | `text-[clamp(2.6rem,7vw,6.4rem)] leading-[1.0] tracking-[-0.02em]` |
|
||||
| Section H2 | `font-display` | `text-[clamp(2.4rem,5vw,4.6rem)] leading-[1.02] tracking-[-0.02em]` |
|
||||
| Card H3/H4 | `font-display` | `text-2xl md:text-3xl` |
|
||||
| Body | `font-sans` (inherited default) | `text-base md:text-lg leading-[1.7]` |
|
||||
| UI label | `font-sans` | `text-[10px] md:text-[11px] font-medium uppercase tracking-[0.22em]` |
|
||||
| Nav link | `font-sans` | `text-sm font-medium tracking-[0.005em]` |
|
||||
| Stats number | `font-display` | `text-5xl md:text-6xl` (responsive smaller on mobile) |
|
||||
|
||||
**Avoid**
|
||||
- `font-mono` on long values or body text.
|
||||
- `tracking-[0.3em]+` on small labels.
|
||||
- `leading-[0.95]` on serif headlines (clips ascenders).
|
||||
|
||||
**Helper class** for one-color brand text:
|
||||
```css
|
||||
.gold-text {
|
||||
background: linear-gradient(90deg, #FFEFC5, #F2C25B 40%, #D4A437 80%, #7A4A12);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spacing scale
|
||||
|
||||
Section padding lives **only** on the `Section` primitive:
|
||||
|
||||
```tsx
|
||||
// components/ui/Section.tsx
|
||||
className={cn("relative scroll-mt-24 py-16 md:scroll-mt-28 md:py-20 lg:py-24", className)}
|
||||
```
|
||||
|
||||
- Mobile: `py-16` (64 px)
|
||||
- Tablet: `md:py-20` (80 px)
|
||||
- Desktop: `lg:py-24` (96 px)
|
||||
- All anchored sections get `scroll-mt-24 md:scroll-mt-28` for sticky-nav clearance.
|
||||
|
||||
**Don't** override per-section unless there's a structural reason. Centralize.
|
||||
|
||||
Card padding: `p-6 md:p-7` standard, `p-7 md:p-10` for hero/feature cards.
|
||||
|
||||
Grid gaps: `gap-4 md:gap-5` standard, `gap-6 md:gap-8` for large cards.
|
||||
|
||||
---
|
||||
|
||||
## Border radius scale
|
||||
|
||||
```css
|
||||
--radius-xs: 4px;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 22px;
|
||||
--radius-xl: 32px;
|
||||
```
|
||||
|
||||
Tailwind shortcuts to match: `rounded-2xl` (cards), `rounded-3xl` (hero panels), `rounded-full` (pills + buttons).
|
||||
|
||||
---
|
||||
|
||||
## Motion
|
||||
|
||||
```ts
|
||||
// shared variants
|
||||
const cardGrid: Variants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.15 } },
|
||||
};
|
||||
const cardCell: Variants = {
|
||||
hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
|
||||
visible: {
|
||||
opacity: 1, y: 0, filter: "blur(0px)",
|
||||
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
- Entrance ease: `[0.16, 1, 0.3, 1]` (cubic-bezier "out-quart-ish").
|
||||
- Duration: hero 1.3s, cards 0.8s, hover 0.3-0.5s.
|
||||
- Hover lift: `-translate-y-1` max. No `-translate-y-2`+ (jumpy).
|
||||
- Halo pulse: `[0.5, 0.85, 0.5]` opacity, 6s loop. Wrap in `!reduce && (...)`.
|
||||
- `useReducedMotion()` must be read in any animated component.
|
||||
|
||||
**Forbidden**
|
||||
- Bouncy springs (`stiffness > 200`) on entrance.
|
||||
- Scale jumps > 1.05 on hover for cards.
|
||||
- Parallax > 200 px translate range.
|
||||
- Marquees without `mask-image` edge fade.
|
||||
|
||||
---
|
||||
|
||||
## Visual textures
|
||||
|
||||
| Class | Purpose |
|
||||
|---|---|
|
||||
| `noise-bg` | Subtle film-grain background overlay |
|
||||
| `grain` | Animated grain (use sparingly on cards) |
|
||||
| `metallic-divider` | Thin gold-tinted divider line |
|
||||
| `radial-gold` | Faint amber radial wash (positioned absolute, blur-3xl) |
|
||||
| `radial-emerald` | Same, emerald variant |
|
||||
| `hairline-t`, `hairline-b` | 1 px top/bottom hairline borders |
|
||||
| `mask-fade-y`, `mask-fade-x` | Edge fade masks |
|
||||
|
||||
Define these as utilities in `globals.css` per the Luxam reference.
|
||||
|
||||
---
|
||||
|
||||
## Hover state pattern
|
||||
|
||||
Every card/button uses the same hover vocabulary:
|
||||
|
||||
1. `-translate-y-1` lift (cards) or `-translate-y-0.5` (buttons).
|
||||
2. Border step up: `border-bone/10 → border-gold/30 (or /35 /40)`.
|
||||
3. Background step up: `bg-graphite/40 → bg-graphite/60`.
|
||||
4. Amber radial glow bottom-right: `radial-gradient(circle, rgba(212,164,55,0.28), transparent 70%)` blurred.
|
||||
5. Gold underline sweep (cards): `absolute bottom-0 left-0 h-px w-0 ... group-hover:w-full`.
|
||||
6. Optional: top hairline gold gradient fade-in.
|
||||
7. Icon disc gets `rotate-[8deg]` + amber halo shadow.
|
||||
|
||||
Apply via `group` + `group-hover:` Tailwind variants — never per-element JS listeners.
|
||||
|
||||
---
|
||||
|
||||
## Container widths
|
||||
|
||||
```tsx
|
||||
// components/ui/Container.tsx — three variants
|
||||
size="default" → max-w-7xl
|
||||
size="wide" → max-w-[1480px]
|
||||
size="narrow" → max-w-3xl
|
||||
```
|
||||
|
||||
All variants: `mx-auto px-6 md:px-10`.
|
||||
|
||||
---
|
||||
|
||||
## When in doubt
|
||||
|
||||
- More dark, less gold.
|
||||
- More space, less content.
|
||||
- Less motion, more restraint.
|
||||
- Real photography > illustration.
|
||||
- Editorial typography > corporate sans.
|
||||
274
.claude/skills/premium-frontend-designer/PROMPT_TEMPLATES.md
Normal file
@ -0,0 +1,274 @@
|
||||
# Prompt Templates
|
||||
|
||||
Copy-paste prompts for common tasks. Replace `{{placeholders}}` with project specifics.
|
||||
|
||||
---
|
||||
|
||||
## 1. Create homepage from scratch
|
||||
|
||||
```
|
||||
Act as a senior frontend architect.
|
||||
Build a premium, cinematic, dark/gold luxury homepage for {{BRAND}} ({{INDUSTRY}}) using the design DNA in .claude/skills/premium-frontend-designer/.
|
||||
|
||||
Stack: Next.js 16 App Router · TypeScript · Tailwind v4 · Framer Motion · lucide-react.
|
||||
|
||||
Composition (top → bottom):
|
||||
1. Nav (fixed, scroll-aware)
|
||||
2. Hero (min-h-100svh, parallax halos, 2 CTAs, eyebrow + serif h1 + sub + body)
|
||||
3. BrandStrip (marquee of capability keywords)
|
||||
4. About / Story (split 5/7, copy + visual)
|
||||
5. WhyUCO-style problem→solution comparison
|
||||
6. Services overview (6 cards)
|
||||
7. Trusted Clients marquee (use ONLY logos I provide)
|
||||
8. Stats / Impact metrics (animated counters)
|
||||
9. Cinematic reel (lazy video)
|
||||
10. Process / Closed loop (timeline)
|
||||
11. Industries served
|
||||
12. Certifications
|
||||
13. Gallery (4-image bento)
|
||||
14. Final CTA section
|
||||
15. Footer (4 columns, no duplication)
|
||||
|
||||
Brand details I will provide:
|
||||
{{BRAND_LEGAL_NAME, PHONE, EMAIL, WHATSAPP_URL, INSTAGRAM, APP_STORE, GOOGLE_PLAY, AGENCY_CREDIT}}
|
||||
|
||||
Do NOT invent links. Ask if missing.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Redesign an existing page
|
||||
|
||||
```
|
||||
Inspect {{PAGE_ROUTE}}. Keep the content + intent but rebuild it to match the design DNA in .claude/skills/premium-frontend-designer/.
|
||||
|
||||
Requirements:
|
||||
- Reuse existing primitives (Section, Container, Button, Reveal, SectionLabel, etc.) — don't duplicate.
|
||||
- Apply the cardGrid + cardCell motion variants for grid entrances.
|
||||
- Add a single gold-accent phrase per h2.
|
||||
- Centralize section padding via the Section primitive (no per-page overrides).
|
||||
- Run typecheck + grep for placeholder hrefs before reporting done.
|
||||
|
||||
Return a clear file change list with NEW / MODIFIED markers.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Add a polished footer
|
||||
|
||||
```
|
||||
Build a 4-column premium footer for {{BRAND}}.
|
||||
|
||||
Columns:
|
||||
1. Brand + contact (logo, tagline, phone, email, WhatsApp, Instagram)
|
||||
2. Navigation (top-level pages)
|
||||
3. Services (specific service items)
|
||||
4. Legal & App (Privacy Policy + 2 store badges)
|
||||
|
||||
No duplicate link labels across columns. External links open in new tab.
|
||||
|
||||
Footer bottom row: copyright + tagline + subtle agency credit (gold underline on hover).
|
||||
|
||||
Watermark: project name in serif, large, opacity-0.025, contained inside footer overflow.
|
||||
|
||||
Brand details:
|
||||
{{PHONE, EMAIL, WHATSAPP_URL, INSTAGRAM_URL, APP_STORE_URL, GOOGLE_PLAY_URL, AGENCY_CREDIT_URL}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Add WhatsApp button
|
||||
|
||||
```
|
||||
Add a refined WhatsApp button to:
|
||||
1. Footer contact stack (icon + label "WhatsApp · {{DISPLAY_NUMBER}}").
|
||||
2. Nav (circular icon-only button next to primary CTA, mobile menu pill).
|
||||
3. Contact page info card (full-width tile w/ icon disc).
|
||||
|
||||
URL: {{WHATSAPP_API_URL}}
|
||||
Display number: {{+CC XX XXX XXXX}}
|
||||
|
||||
Use inline SVG (lucide has no WhatsApp glyph). Refined gold tint, not bright green. Hover: gold border + soft glow + scale 1.04. aria-label="Chat with {{BRAND}} on WhatsApp". target="_blank" rel="noopener noreferrer".
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Add app store badges
|
||||
|
||||
```
|
||||
Add official App Store + Google Play badge SVGs.
|
||||
|
||||
URLs:
|
||||
- App Store: {{APP_STORE_URL}}
|
||||
- Google Play: {{GOOGLE_PLAY_URL}}
|
||||
|
||||
Tasks:
|
||||
1. Download from svgrepo.com (use /show/ path not /download/):
|
||||
- https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg
|
||||
- https://www.svgrepo.com/show/303139/google-play-badge-logo.svg
|
||||
2. Save to public/images/app-store-badge.svg + public/images/google-play-badge.svg.
|
||||
3. Render via plain <img> (next/image blocks SVG in Next 16).
|
||||
4. Width: 132 px footer / 140-160 px in hero CTA section.
|
||||
5. Hover: opacity-85 + -translate-y-0.5. No gold circles.
|
||||
6. Stack gap-2 (8 px) in footer.
|
||||
7. aria-label on every anchor. target="_blank" rel="noopener noreferrer".
|
||||
|
||||
Use the snippet from snippets/AppBadges.tsx as a starting point.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Add client logo section
|
||||
|
||||
```
|
||||
Add a premium "Trusted by leading {{INDUSTRY}} partners" client logo marquee to the homepage (after Services) and About page (after process).
|
||||
|
||||
Use ONLY these 12 logos I provide:
|
||||
{{LIST OF LOGO URLS}}
|
||||
|
||||
Tasks:
|
||||
1. Download each into public/images/clients/ with clean filenames.
|
||||
2. Create src/lib/clients.ts with typed array { src, alt }.
|
||||
3. Use recognisable brand names for alt text; for unidentified logos use "{{BRAND}} client logo".
|
||||
4. Marquee: 50s linear infinite, hover-pause, mask-image edge fade.
|
||||
5. Tile: h-24 w-44 mobile / h-28 w-52 tablet+. Default grayscale + opacity 70%, hover full color + scale 1.05.
|
||||
6. Reduced-motion fallback: render a static grid-cols-2 sm:grid-cols-3 lg:grid-cols-6.
|
||||
|
||||
Use snippets/ClientLogoMarquee.tsx as the base.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Add certificate gallery
|
||||
|
||||
```
|
||||
Replace the document-card list on /certifications with a 4-image gallery showing the actual certificate pages.
|
||||
|
||||
Use these exact images (download to public/images/certifications/):
|
||||
{{IMG1_URL, IMG2_URL, IMG3_URL, IMG4_URL}}
|
||||
|
||||
Each image links to the corresponding PDF (already in public/documents/certifications/).
|
||||
|
||||
Layout: sm:grid-cols-2 lg:grid-cols-4 with aspect-[3/4] portrait tiles.
|
||||
|
||||
Tile chrome:
|
||||
- Top-left page badge (Page 1 / Page 2 · Annex).
|
||||
- Top-right ArrowUpRight disc.
|
||||
- Bottom caption: mono category in gold + serif title + "Open PDF" CTA.
|
||||
- Hover: scale 1.04, gold ring border, amber radial glow, gold underline sweep.
|
||||
|
||||
External: target="_blank" rel="noopener noreferrer" on every anchor.
|
||||
|
||||
Optional: secondary "Need the full PDFs?" pill row below grid with direct download links.
|
||||
|
||||
Use snippets/CertificateGallery.tsx as a starting point.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Fix mobile responsiveness
|
||||
|
||||
```
|
||||
The {{PAGE}} layout breaks on mobile. Specific issues:
|
||||
{{ISSUES_LIST}}
|
||||
|
||||
Walk through the responsive checklist in .claude/skills/premium-frontend-designer/RESPONSIVE_CHECKLIST.md and fix every applicable item.
|
||||
|
||||
Test at 320 / 375 / 390 / 414 / 768 / 1024 viewports.
|
||||
|
||||
Don't redesign — only fix layout, spacing, overflow, clipping. Preserve the visual identity.
|
||||
|
||||
Return a table of issues + fixes (Before / After columns).
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Polish typography sitewide
|
||||
|
||||
```
|
||||
Audit and refine typography across all pages:
|
||||
|
||||
1. Hero h1: clamp(2.4rem, 7vw, 6.4rem), leading-[1.0], tracking-[-0.02em]. Add pt buffer for sticky-nav clearance.
|
||||
2. Two-font system: serif for h1/h2 only, clean sans (Geist/Inter) for everything else.
|
||||
3. Remove font-mono from long values + body text. Keep only on tiny brand-pattern eyebrow tags if at all.
|
||||
4. Tracking: reduce all tracking-[0.32em+] on small labels to tracking-[0.22em].
|
||||
5. Nav links: text-sm font-medium tracking-[0.005em] (slightly larger + medium weight, near-zero tracking).
|
||||
6. BrandStrip marquee: smaller (text-xs md:text-sm uppercase) so it doesn't compete with hero.
|
||||
7. Section labels via SectionLabel primitive — never inline.
|
||||
8. text-balance on h1/h2, text-pretty on body paragraphs.
|
||||
|
||||
Confirm:
|
||||
- typecheck clean
|
||||
- HTTP 200 on every route
|
||||
- zero tracking-[0.32em] remaining
|
||||
- no font-mono on user-facing body content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Add Privacy Policy
|
||||
|
||||
```
|
||||
Create /privacy-policy route matching the premium dark/gold style.
|
||||
|
||||
9 sections with id="..." anchors + scroll-mt-28:
|
||||
- introduction
|
||||
- information-we-collect
|
||||
- how-we-use-information
|
||||
- contact-form-data
|
||||
- cookies-and-analytics
|
||||
- data-protection
|
||||
- third-party-services
|
||||
- user-rights
|
||||
- contact-information
|
||||
|
||||
Use brand contact details from lib/content.ts.
|
||||
|
||||
Effective date stamp at top + metallic-divider. Bottom disclaimer with mailto link in gold underline.
|
||||
|
||||
Single column max-w-3xl. font-display h2 per section. text-pretty body.
|
||||
|
||||
Don't write fake legal claims. General professional language only. Tell users they can contact for privacy questions.
|
||||
|
||||
Add to footer Legal column.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Coolify deploy via Dockerfile
|
||||
|
||||
```
|
||||
Deploy {{PROJECT}} to Coolify with a Dockerfile build pack.
|
||||
|
||||
Tasks:
|
||||
1. Add `output: "standalone"` to next.config.ts.
|
||||
2. Create multi-stage Dockerfile (node:22-alpine, 3 stages: deps → build → run).
|
||||
3. Add .dockerignore (skip .git, node_modules, .next cache, env files, .claude).
|
||||
4. Run-stage as non-root user (UID 1001).
|
||||
5. EXPOSE 3000, CMD ["node", "server.js"] using standalone output.
|
||||
|
||||
Then via Coolify API:
|
||||
- PATCH application: build_pack="dockerfile", dockerfile_location="/Dockerfile", ports_exposes="3000", domains="https://{{DOMAIN}}"
|
||||
- POST /api/v1/deploy?uuid={{APP_UUID}}&force=true
|
||||
- Poll deployment until terminal status.
|
||||
|
||||
If "no available server" appears on the domain after deploy: verify DNS resolves to Coolify host IP, then redeploy to trigger Let's Encrypt cert issuance.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. General "make this premium" polish
|
||||
|
||||
```
|
||||
Inspect {{COMPONENT_OR_PAGE}}. Identify what doesn't match the premium dark/gold luxury direction in .claude/skills/premium-frontend-designer/DESIGN_SYSTEM.md.
|
||||
|
||||
Specifically check:
|
||||
- Are buttons using premium variants (gold gradient primary, outline secondary)?
|
||||
- Are cards using the hover lift + amber glow + gold underline sweep pattern?
|
||||
- Is there any cheap iconography or stock-photo placeholder?
|
||||
- Are headings using the serif font with one gold-accent phrase?
|
||||
- Is spacing centralized via Section primitive?
|
||||
- Is motion subtle (no bounces, no scale > 1.05 on hover)?
|
||||
|
||||
Apply only the changes needed — don't rewrite working sections. Return a list of refinements made.
|
||||
```
|
||||
108
.claude/skills/premium-frontend-designer/RESPONSIVE_CHECKLIST.md
Normal file
@ -0,0 +1,108 @@
|
||||
# Responsive Checklist
|
||||
|
||||
Run through every item before declaring a page done.
|
||||
|
||||
## Viewports to verify
|
||||
|
||||
- **320 px** (iPhone SE 1st gen, smallest practical)
|
||||
- **375 px** (iPhone SE 2/3, iPhone 13 mini)
|
||||
- **390 px** (iPhone 14/15/16 default)
|
||||
- **414 px** (iPhone Plus)
|
||||
- **768 px** (iPad portrait, `md:` breakpoint)
|
||||
- **1024 px** (iPad landscape / small laptop, `lg:` breakpoint)
|
||||
- **1440 px** (full laptop)
|
||||
|
||||
## Global guards
|
||||
|
||||
- [ ] `body { overflow-x: hidden }` in `globals.css`.
|
||||
- [ ] No `min-w-[...]` on root containers.
|
||||
- [ ] All decorative absolute elements live inside `relative isolate overflow-hidden` parents.
|
||||
- [ ] `max-w-*` containers always paired with `mx-auto px-6 md:px-10`.
|
||||
|
||||
## Header / Nav
|
||||
|
||||
- [ ] Logo left, menu/CTA right on mobile.
|
||||
- [ ] Hamburger button is `size-10` minimum (touch target).
|
||||
- [ ] Mobile menu drawer closes when a link is clicked.
|
||||
- [ ] No sticky-nav overlap on anchored sections — `scroll-mt-24 md:scroll-mt-28` on `<Section>`.
|
||||
- [ ] Logo height responsive: `h-11 sm:h-14 md:h-16`.
|
||||
|
||||
## Hero
|
||||
|
||||
- [ ] H1 clamp: `clamp(2.4rem, 7vw, 6.4rem)` — never larger upper bound.
|
||||
- [ ] `leading-[1.0]` on serif H1 (not `0.95` — clips ascenders).
|
||||
- [ ] Top padding generous: `pt-32 sm:pt-36 md:pt-44` (clears sticky nav + breathing room).
|
||||
- [ ] CTAs use `flex flex-wrap items-center gap-4` so they stack on narrow screens.
|
||||
- [ ] Stats panel: collapse 1-col-tall mobile layouts to a horizontal 3-col compact row.
|
||||
|
||||
## Cards & grids
|
||||
|
||||
- [ ] Mobile: `grid-cols-1`. Tablet: `sm:grid-cols-2`. Desktop: `lg:grid-cols-N` matched to card count.
|
||||
- [ ] Avoid `lg:grid-cols-3` for a 5-card list (leaves orphan) — use 5 or 2-row layout matched to count.
|
||||
- [ ] Aspect-ratio used over fixed heights: `aspect-[4/3]` or `aspect-[3/4]`.
|
||||
|
||||
## Typography
|
||||
|
||||
- [ ] No tracking > `0.22em` on small labels.
|
||||
- [ ] `text-balance` on h1/h2.
|
||||
- [ ] `text-pretty` on body paragraphs.
|
||||
- [ ] Long tokens (cert numbers, emails) use `break-all` or `break-words` to wrap.
|
||||
|
||||
## Images
|
||||
|
||||
- [ ] `next/image` with `sizes` attribute matched to layout.
|
||||
- [ ] Hero LCP image gets `priority`.
|
||||
- [ ] `object-cover` only when crop is visually intentional.
|
||||
- [ ] `object-contain` for logos.
|
||||
- [ ] Alt text meaningful — never "image" or filename.
|
||||
|
||||
## Forms
|
||||
|
||||
- [ ] Field grid: `grid gap-5 sm:grid-cols-2` for first/last + email/phone.
|
||||
- [ ] Inputs `text-base` (16 px+) on mobile to prevent iOS zoom-on-focus.
|
||||
- [ ] Submit button respects pending state w/ spinner.
|
||||
- [ ] Field errors render below field, not in a popup.
|
||||
- [ ] Success/error banner renders outside button.
|
||||
|
||||
## Motion
|
||||
|
||||
- [ ] `useReducedMotion()` checked in every component using motion props.
|
||||
- [ ] Heavy halos / parallax skipped when reduced-motion is on.
|
||||
- [ ] Marquee paused when reduced-motion is on (or replaced with grid).
|
||||
|
||||
## External links
|
||||
|
||||
- [ ] All `<a target="_blank">` carry `rel="noopener noreferrer"`.
|
||||
- [ ] WhatsApp / Instagram / store badges have `aria-label`.
|
||||
- [ ] App badges sized 132 px (footer) / 140-160 px (CTA).
|
||||
|
||||
## Footer
|
||||
|
||||
- [ ] No duplicated link labels across columns.
|
||||
- [ ] Privacy Policy only in Legal column.
|
||||
- [ ] Agency credit row sits below copyright, subtle.
|
||||
- [ ] Watermark contained via `overflow-hidden` on footer root.
|
||||
|
||||
## Accessibility
|
||||
|
||||
- [ ] Every interactive element keyboard-reachable.
|
||||
- [ ] `focus-visible:` rings on buttons, inputs, anchors.
|
||||
- [ ] Modal/lightbox: scroll-locked body + `Escape` closes + arrows navigate.
|
||||
- [ ] Color contrast checked (`text-bone` on obsidian = 16:1).
|
||||
|
||||
## Final smoke test
|
||||
|
||||
```bash
|
||||
npx tsc --noEmit # clean
|
||||
curl -s -o NUL -w "%{http_code}" http://localhost:3000/... # 200 on every route
|
||||
```
|
||||
|
||||
Grep for placeholder leaks:
|
||||
|
||||
```bash
|
||||
grep -rn 'href="#"' src/
|
||||
grep -rn 'href=""' src/
|
||||
grep -rn 'href="/#' src/
|
||||
```
|
||||
|
||||
Should all return zero (unless intentional same-page anchors with matching IDs).
|
||||
143
.claude/skills/premium-frontend-designer/SKILL.md
Normal file
@ -0,0 +1,143 @@
|
||||
---
|
||||
name: premium-frontend-designer
|
||||
description: Build, redesign, and polish premium dark/gold luxury websites with React/Next.js/Tailwind. Use when the user asks to create a website, redesign a page, polish a section, fix mobile responsiveness, add premium components (hero, footer, CTA, client logos, certificates, app badges, WhatsApp button), or apply the Luxam-style luxury design DNA to another project.
|
||||
---
|
||||
|
||||
# Premium Frontend Designer & Implementation Agent
|
||||
|
||||
You are a senior frontend architect + UI/UX designer + implementation engineer.
|
||||
|
||||
Your job: create production-ready, premium, cinematic, dark/gold luxury websites that feel high-end, trustworthy, and conversion-focused — the way the Luxam reference site does.
|
||||
|
||||
This skill captures the design DNA, component patterns, prompt templates, and snippets so any future project can match the same quality bar.
|
||||
|
||||
---
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Auto-trigger whenever the user:
|
||||
- asks for a "premium", "luxury", "cinematic", "dark/gold", or "Luxam-style" design
|
||||
- creates or redesigns a page (homepage, about, services, contact, gallery, certifications, purchase-sale)
|
||||
- polishes typography, spacing, or motion
|
||||
- fixes mobile responsiveness
|
||||
- adds: WhatsApp link, app store buttons, client logos, certificate gallery, premium hero, premium footer
|
||||
- imports brand contact details into a layout
|
||||
- wants reusable React/Tailwind components in the Luxam style
|
||||
|
||||
Refuse silently if the project explicitly uses a different design language (e.g. bright corporate, neobrutalist, retro 8-bit). In that case ask before applying this skill's DNA.
|
||||
|
||||
---
|
||||
|
||||
## Visual principles
|
||||
|
||||
1. **Dark cinematic background** — obsidian-class blacks (`#08080A`), graphite layering (`#15161A` / `#1C1D22`). Never pure white. Never bright primary colors as backgrounds.
|
||||
2. **Gold/amber accents** — restrained, structural, single-tone gradient. Use on numbers, labels, dividers, hover states, hero highlights. Never bright yellow. Never multi-color rainbow gradients.
|
||||
3. **Editorial typography pairing**
|
||||
- Serif (Cormorant Garamond / Playfair / Libre Baskerville) for hero H1 + section H2 only.
|
||||
- Sans (Geist / Inter / Manrope / Satoshi) for nav, body, buttons, labels, metadata.
|
||||
- No monospace on user-facing copy or long values.
|
||||
4. **Glass + grain** — semi-transparent `bg-bone/[0.03]` cards with `backdrop-blur`. Subtle noise/grain overlays to break flat surfaces.
|
||||
5. **Cinematic motion** — Framer Motion: slow, restrained. `cubic-bezier(0.16, 1, 0.3, 1)`, 0.8-1.2s entrance, `useReducedMotion()` respected.
|
||||
6. **Responsive from the start** — every layout works at 320 / 375 / 768 / 1024 / 1440 viewports.
|
||||
7. **No corporate basic** — no boxy stock layouts, no flat colorful cards, no generic stock-photo hero, no cheap icon dumps.
|
||||
|
||||
Read `DESIGN_SYSTEM.md` for the full token palette + typography clamps.
|
||||
|
||||
---
|
||||
|
||||
## Coding principles
|
||||
|
||||
1. **Inspect before changing.** Always read the existing files, lockfile, framework version, and existing primitives. Reuse — don't duplicate.
|
||||
2. **Preserve project identity.** Don't rewrite the whole brand for a small request.
|
||||
3. **Real content only.** Never invent links, social handles, certifications, addresses, phone numbers, or legal claims. Ask if missing.
|
||||
4. **External links** carry `target="_blank"` + `rel="noopener noreferrer"`.
|
||||
5. **Reusable primitives first.** Look for existing `Section`, `Container`, `Button`, `Reveal`, `SectionLabel`, `GlassCard` before writing new ones.
|
||||
6. **Clean imports.** Remove unused icons / helpers after edits.
|
||||
7. **One source of truth** for spacing — push to a shared `Section` primitive instead of overriding per page.
|
||||
8. **Typecheck after every change.** `npx tsc --noEmit` must pass.
|
||||
9. **No backwards-compat shims.** Don't leave `// removed` comments, dead aliases, or "for future" stubs.
|
||||
10. **No commented-out code** in committed work.
|
||||
11. **No emojis** in code or files unless user asks.
|
||||
|
||||
Read `COMPONENT_RULES.md` for per-element rules (buttons, cards, footer, forms, icons).
|
||||
|
||||
---
|
||||
|
||||
## Accessibility requirements
|
||||
|
||||
- All anchors have meaningful text or `aria-label`.
|
||||
- All interactive elements reachable by keyboard.
|
||||
- Focus-visible states: `focus-visible:border-gold/60 focus-visible:ring-2 focus-visible:ring-gold/40`.
|
||||
- Modal/lightbox: `role="dialog" aria-modal="true"`, `Escape` closes, scroll-lock on body.
|
||||
- Decorative SVGs: `aria-hidden`.
|
||||
- Color contrast: body text `text-bone` (≈ #F4F0E6) on obsidian = 16:1 ratio, well above WCAG AA.
|
||||
- `prefers-reduced-motion` respected via Framer's `useReducedMotion()` — disable transforms/parallax, keep opacity fades.
|
||||
|
||||
---
|
||||
|
||||
## Responsive requirements
|
||||
|
||||
Test mentally (or in browser) at: **320, 375, 390, 414, 768, 1024, 1440**.
|
||||
|
||||
- Hero h1 clamp: `clamp(2.4rem, 7vw, 6.4rem)` with `leading-[1.0]`.
|
||||
- Section padding: `py-16 md:py-20 lg:py-24` (uniform).
|
||||
- Anchored sections: `scroll-mt-24 md:scroll-mt-28` for sticky-nav clearance.
|
||||
- `body { overflow-x: hidden }` in `globals.css` as a global guard.
|
||||
- Sticky decorations (halos, watermarks, blurs) must not introduce horizontal scroll — use `overflow-hidden` on parent + `translate` instead of fixed widths.
|
||||
- Mobile cards stack `grid-cols-1 sm:grid-cols-2 lg:grid-cols-N`.
|
||||
|
||||
Read `RESPONSIVE_CHECKLIST.md` before shipping any page.
|
||||
|
||||
---
|
||||
|
||||
## Output requirements
|
||||
|
||||
Every meaningful change returns:
|
||||
|
||||
1. **Files changed** — explicit list with `NEW` / `MODIFIED` / `DELETED` markers.
|
||||
2. **What was kept** vs **what was replaced** when redesigning.
|
||||
3. **Receipts** — `HTTP 200`, typecheck clean, key content rendered (grep output).
|
||||
4. **Responsive breakdown** — desktop / tablet / mobile behavior.
|
||||
5. **Confirmations** — explicit yes/no on every checklist item the user asked.
|
||||
6. **No placeholder leaks** — confirm zero `href="#"`, `href=""`, `href="/#"` remain in modified files.
|
||||
|
||||
---
|
||||
|
||||
## QA checklist (run before reporting done)
|
||||
|
||||
```
|
||||
[ ] npx tsc --noEmit clean
|
||||
[ ] dev server HTTP 200 on every modified route
|
||||
[ ] grep zero placeholder hrefs in modified files
|
||||
[ ] grep zero hardcoded brand values that should be in lib/content.ts
|
||||
[ ] mobile (<640px) no horizontal overflow, hero not clipped, CTAs not cut
|
||||
[ ] tablet (≥640 <1024) grids transition cleanly to 2-col
|
||||
[ ] desktop (≥1024) spacing balanced, no orphan cards
|
||||
[ ] external links target="_blank" + rel="noopener noreferrer"
|
||||
[ ] focus-visible visible on every button/anchor
|
||||
[ ] reduced-motion heavy parallax/scale animations disabled
|
||||
[ ] no leftover console.log or commented-out code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Brand-specific config (Luxam reference)
|
||||
|
||||
`examples/luxam-config.md` holds the exact Luxam contact details, WhatsApp URL, app store links, agency credit, etc. **Do not copy these values into other projects** — use them only as the shape pattern. Each new project gets its own `lib/content.ts` populated from real values the user provides.
|
||||
|
||||
---
|
||||
|
||||
## File map of this skill
|
||||
|
||||
```
|
||||
SKILL.md — this file (entry point, principles, when to use)
|
||||
DESIGN_SYSTEM.md — color tokens, typography, spacing scale, motion
|
||||
COMPONENT_RULES.md — per-component rules (buttons, cards, footer, forms, icons)
|
||||
RESPONSIVE_CHECKLIST.md — pre-ship checklist for every viewport
|
||||
PROMPT_TEMPLATES.md — copy-paste prompts for common tasks
|
||||
COMPONENT_SNIPPETS.md — high-level component overview
|
||||
snippets/ — 8 reusable TSX component files
|
||||
examples/luxam-config.md — Luxam-specific values (reference, not boilerplate)
|
||||
```
|
||||
|
||||
Read the relevant section(s) for the task at hand. Don't dump the whole skill into the user's reply — surface only the rules + snippets relevant to the change being made.
|
||||
@ -0,0 +1,118 @@
|
||||
# Luxam Reference Config
|
||||
|
||||
Concrete values used in the Luxam project. **Reference only** — do not paste into other projects unless the user explicitly says these are also their values.
|
||||
|
||||
## `src/lib/content.ts`
|
||||
|
||||
```ts
|
||||
export const brand = {
|
||||
name: "LUXAM",
|
||||
legalName: "Luxam Oils Trading LLC",
|
||||
tagline: "Sustainable Energy. Refined.",
|
||||
promise: "Converting waste into the fuel of tomorrow.",
|
||||
phone: "04 262 3314",
|
||||
email: "info@luxam.ae",
|
||||
location: "United Arab Emirates",
|
||||
instagram: "@luxamtrading",
|
||||
};
|
||||
|
||||
export const nav = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "About us", href: "/about-us" },
|
||||
{ label: "Services", href: "/services" },
|
||||
{ label: "Purchase & Sale", href: "/purchase-sale" },
|
||||
{ label: "Gallery", href: "/gallery" },
|
||||
{ label: "Certifications", href: "/certifications" },
|
||||
{ label: "Contact us", href: "/contact-us" },
|
||||
];
|
||||
|
||||
export const socials = {
|
||||
instagram: "https://www.instagram.com/luxamtrading/",
|
||||
whatsapp:
|
||||
"https://api.whatsapp.com/send/?phone=971527312190&text&type=phone_number&app_absent=0",
|
||||
whatsappDisplay: "+971 52 731 2190",
|
||||
};
|
||||
|
||||
export const app = {
|
||||
googlePlay: "https://play.google.com/store/apps/details?id=com.lootah.luxam",
|
||||
appStore: "https://apps.apple.com/ae/app/luxam/id6740183015",
|
||||
};
|
||||
|
||||
export const credit = {
|
||||
agency: "YS Lootah Tech",
|
||||
url: "https://yslootahtech.com/",
|
||||
};
|
||||
```
|
||||
|
||||
## Footer link cluster
|
||||
|
||||
```ts
|
||||
const navigationLinks = [
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "About us", href: "/about-us" },
|
||||
{ label: "Services", href: "/services" },
|
||||
{ label: "Purchase & Sale", href: "/purchase-sale" },
|
||||
{ label: "Gallery", href: "/gallery" },
|
||||
{ label: "Certifications", href: "/certifications" },
|
||||
{ label: "Contact us", href: "/contact-us" },
|
||||
];
|
||||
|
||||
const serviceItems = [
|
||||
{ label: "UCO Collection", href: "/services" },
|
||||
{ label: "Logistics & Handling", href: "/services" },
|
||||
{ label: "Recycling & Feedstock", href: "/services" },
|
||||
{ label: "Compliance & Documentation", href: "/certifications" },
|
||||
{ label: "Partner with us", href: "/contact-us" },
|
||||
];
|
||||
|
||||
const legalLinks = [{ label: "Privacy Policy", href: "/privacy-policy" }];
|
||||
```
|
||||
|
||||
## Deployment (Coolify)
|
||||
|
||||
```
|
||||
Coolify project luxam (uuid h0840c84kgo0g4cgwg0os88c)
|
||||
env production uuid s4ow44cwgo08g88g04gwkkcs
|
||||
Application uuid zc4s4k4k0c44c8cs8oc8so4k
|
||||
build_pack dockerfile
|
||||
dockerfile_location /Dockerfile
|
||||
ports_exposes 3000
|
||||
fqdn https://luxamnew.yslootahtech.com
|
||||
DNS A record luxamnew → 51.255.193.254 (DNS only)
|
||||
```
|
||||
|
||||
Git remote:
|
||||
|
||||
```
|
||||
origin https://git.devxsupport.com/mohammad/luxam-web.git (main)
|
||||
```
|
||||
|
||||
Token policy: never commit. Use `COOLIFY_TOKEN` env var when scripting deploys.
|
||||
|
||||
## Dockerfile pattern
|
||||
|
||||
Multi-stage:
|
||||
1. `deps` (node:22-alpine, `apk add libc6-compat`, `npm ci`)
|
||||
2. `build` (`next build` w/ `output: "standalone"`)
|
||||
3. `run` (non-root UID 1001, copies `.next/standalone` + `.next/static` + `public`, `CMD ["node","server.js"]`)
|
||||
|
||||
`.dockerignore` skips `.git`, `node_modules`, `.next/cache`, `.env*`, `.claude`.
|
||||
|
||||
## ISCC certificate facts (Luxam-specific)
|
||||
|
||||
Use only as a precedent for how to surface certificate metadata. Do not copy into other projects.
|
||||
|
||||
```
|
||||
Certificate number EU-ISCC-Cert-DE100-27002125
|
||||
Standard ISCC EU
|
||||
Framework RED III (Directive (EU) 2023/2413)
|
||||
Issuer SGS Germany GmbH (Europa Allee 12, D-49685 Emstek, Germany)
|
||||
Certified entity Luxam Oils Trading LLC
|
||||
Site Al Ttay – Al Khawaneej, Dubai, UAE
|
||||
Scope Collecting point, warehouse
|
||||
Material UCO (entirely of vegetable origin)
|
||||
Valid from 11 August 2025
|
||||
Valid to 10 August 2026
|
||||
```
|
||||
|
||||
For any new project that surfaces compliance docs, follow the same data shape: number, standard, issuer, certified entity, site, scope, material, validity dates. Pull only from official documents — never invent.
|
||||
@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Official App Store + Google Play badge anchors.
|
||||
*
|
||||
* Setup steps:
|
||||
* 1. Download official SVGs (use the /show/ path which returns clean SVG):
|
||||
* https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg
|
||||
* https://www.svgrepo.com/show/303139/google-play-badge-logo.svg
|
||||
* 2. Save to public/images/app-store-badge.svg + public/images/google-play-badge.svg.
|
||||
* 3. Add to lib/content.ts:
|
||||
* export const app = {
|
||||
* googlePlay: "https://play.google.com/store/apps/details?id=...",
|
||||
* appStore: "https://apps.apple.com/ae/app/.../id...",
|
||||
* };
|
||||
*
|
||||
* next/image blocks raw SVG by default in Next 16 — keep plain <img>.
|
||||
*/
|
||||
import { app } from "@/lib/content";
|
||||
|
||||
type Props = {
|
||||
store: "appstore" | "googleplay";
|
||||
small?: boolean;
|
||||
};
|
||||
|
||||
export function AppBadge({ store, small }: Props) {
|
||||
const isApple = store === "appstore";
|
||||
const href = isApple ? app.appStore : app.googlePlay;
|
||||
const src = isApple
|
||||
? "/images/app-store-badge.svg"
|
||||
: "/images/google-play-badge.svg";
|
||||
const alt = isApple ? "Download on the App Store" : "Get it on Google Play";
|
||||
const label = isApple
|
||||
? "Download on the App Store"
|
||||
: "Get it on Google Play";
|
||||
const width = small ? 132 : 160;
|
||||
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={label}
|
||||
className="group inline-flex transition duration-300 hover:-translate-y-0.5 hover:opacity-85"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt}
|
||||
width={width}
|
||||
height={small ? 40 : 48}
|
||||
className={
|
||||
small
|
||||
? "h-auto w-[132px] max-w-full select-none"
|
||||
: "h-auto w-[140px] max-w-full select-none sm:w-[160px]"
|
||||
}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
draggable={false}
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
/** Both badges in a vertical stack. Use in footer Legal column. */
|
||||
export function AppBadgeStack({ small }: { small?: boolean }) {
|
||||
return (
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<AppBadge store="appstore" small={small} />
|
||||
<AppBadge store="googleplay" small={small} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* 4-image certificate gallery — each tile links to a PDF in a new tab.
|
||||
*
|
||||
* Expected data shape:
|
||||
* export type CertPage = {
|
||||
* src: string; // /images/certifications/page.jpg
|
||||
* alt: string;
|
||||
* title: string; // ISCC Certificate 2025
|
||||
* page: string; // Page 1 · Annex
|
||||
* document: string; // ISCC Certificate
|
||||
* href: string; // /documents/certifications/iscc.pdf OR upstream URL
|
||||
* };
|
||||
* export const certificatePages: CertPage[] = [ ... ];
|
||||
*/
|
||||
import Image from "next/image";
|
||||
import { ArrowUpRight, FileText } from "lucide-react";
|
||||
import { motion, type Variants } from "framer-motion";
|
||||
|
||||
export type CertPage = {
|
||||
src: string;
|
||||
alt: string;
|
||||
title: string;
|
||||
page: string;
|
||||
document: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
const grid: Variants = {
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.08, delayChildren: 0.15 } },
|
||||
};
|
||||
const cell: Variants = {
|
||||
hidden: { opacity: 0, y: 24, filter: "blur(6px)" },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: "blur(0px)",
|
||||
transition: { duration: 0.8, ease: [0.16, 1, 0.3, 1] },
|
||||
},
|
||||
};
|
||||
|
||||
export function CertificateGallery({ pages }: { pages: CertPage[] }) {
|
||||
return (
|
||||
<motion.div
|
||||
variants={grid}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.1 }}
|
||||
className="grid gap-5 sm:grid-cols-2 lg:grid-cols-4 lg:gap-6"
|
||||
>
|
||||
{pages.map((p) => (
|
||||
<motion.div key={p.src} variants={cell}>
|
||||
<CertCard {...p} />
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
function CertCard({ src, alt, title, page, document, href }: CertPage) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={`${title} — ${page}. Opens PDF in a new tab.`}
|
||||
className="group relative isolate flex h-full flex-col overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/35 hover:bg-graphite/60 focus:outline-none focus-visible:border-gold/60 focus-visible:ring-2 focus-visible:ring-gold/40"
|
||||
>
|
||||
<div className="relative aspect-[3/4] w-full overflow-hidden bg-obsidian">
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw"
|
||||
className="object-cover object-top transition-transform duration-[1200ms] ease-[cubic-bezier(0.16,1,0.3,1)] group-hover:scale-[1.04]"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 top-0 h-20 bg-gradient-to-b from-obsidian/70 via-obsidian/20 to-transparent"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-obsidian/80 via-obsidian/30 to-transparent"
|
||||
/>
|
||||
<span className="absolute left-3 top-3 inline-flex items-center gap-2 rounded-full border border-gold/30 bg-obsidian/70 px-3 py-1 font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light backdrop-blur">
|
||||
<span className="size-1 rounded-full bg-gold-light shadow-[0_0_8px_rgba(242,194,91,0.8)]" />
|
||||
{page}
|
||||
</span>
|
||||
<span className="absolute right-3 top-3 grid size-9 place-items-center rounded-full border border-bone/20 bg-obsidian/70 text-mist backdrop-blur transition-all duration-500 group-hover:-translate-y-0.5 group-hover:border-gold/50 group-hover:text-gold-light">
|
||||
<ArrowUpRight className="size-4" />
|
||||
</span>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(212,164,55,0.32), transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 rounded-t-2xl border border-transparent transition-colors duration-500 group-hover:border-gold/40"
|
||||
/>
|
||||
</div>
|
||||
<div className="relative flex flex-col gap-2 p-5 md:p-6">
|
||||
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-gold-light">
|
||||
{document}
|
||||
</span>
|
||||
<h4 className="font-display text-lg text-bone text-balance transition-colors duration-500 group-hover:text-gold-light md:text-xl">
|
||||
{title}
|
||||
</h4>
|
||||
<div className="mt-1 inline-flex items-center gap-2 text-xs text-mist">
|
||||
<FileText className="size-3.5 text-gold-light" />
|
||||
<span>Open PDF</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Premium horizontal logo marquee.
|
||||
*
|
||||
* Setup:
|
||||
* 1. globals.css must have:
|
||||
* @keyframes marquee {
|
||||
* 0% { transform: translateX(0); }
|
||||
* 100% { transform: translateX(-50%); }
|
||||
* }
|
||||
* 2. Pass clients prop or create lib/clients.ts:
|
||||
* export const clients = [
|
||||
* { src: "/images/clients/brand-a.png", alt: "Brand A" },
|
||||
* ...
|
||||
* ];
|
||||
*/
|
||||
import Image from "next/image";
|
||||
import { useReducedMotion } from "framer-motion";
|
||||
|
||||
export type Client = { src: string; alt: string };
|
||||
|
||||
export function ClientLogoMarquee({
|
||||
clients,
|
||||
eyebrow = "Our Clients",
|
||||
title,
|
||||
subtitle,
|
||||
}: {
|
||||
clients: Client[];
|
||||
eyebrow?: string;
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
}) {
|
||||
const reduce = useReducedMotion();
|
||||
const loop = [...clients, ...clients];
|
||||
|
||||
return (
|
||||
<section className="relative overflow-hidden py-16 md:py-20 lg:py-24">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -left-32 top-1/3 size-[480px] rounded-full radial-gold blur-3xl opacity-30"
|
||||
/>
|
||||
<div className="mx-auto max-w-7xl px-6 md:px-10">
|
||||
<div className="flex flex-col items-center gap-6 text-center">
|
||||
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
|
||||
<span className="h-px w-8 bg-gold/40" />
|
||||
<span>{eyebrow}</span>
|
||||
</div>
|
||||
<h2 className="font-display text-4xl leading-[1.04] text-bone text-balance md:text-5xl">
|
||||
{title}
|
||||
</h2>
|
||||
{subtitle ? (
|
||||
<p className="max-w-2xl text-pretty text-mist md:text-lg">{subtitle}</p>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="relative mt-12 md:mt-14">
|
||||
{/* edge fades */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 left-0 z-10 w-16 bg-gradient-to-r from-obsidian to-transparent md:w-32"
|
||||
/>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-y-0 right-0 z-10 w-16 bg-gradient-to-l from-obsidian to-transparent md:w-32"
|
||||
/>
|
||||
<div
|
||||
className="flex overflow-hidden"
|
||||
style={{
|
||||
maskImage:
|
||||
"linear-gradient(90deg, transparent, black 8%, black 92%, transparent)",
|
||||
}}
|
||||
>
|
||||
{reduce ? (
|
||||
<ClientGrid clients={clients} />
|
||||
) : (
|
||||
<div
|
||||
className="flex shrink-0 gap-4 pr-4 md:gap-6 md:pr-6"
|
||||
style={{
|
||||
animation: "marquee 50s linear infinite",
|
||||
animationPlayState: "running",
|
||||
}}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.animationPlayState = "paused")
|
||||
}
|
||||
onMouseLeave={(e) =>
|
||||
(e.currentTarget.style.animationPlayState = "running")
|
||||
}
|
||||
>
|
||||
{loop.map((c, i) => (
|
||||
<LogoTile key={`${c.src}-${i}`} client={c} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoTile({ client }: { client: Client }) {
|
||||
return (
|
||||
<div className="group/tile relative grid h-24 w-44 shrink-0 place-items-center overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 px-5 backdrop-blur-sm transition-all duration-500 hover:border-gold/30 hover:bg-graphite/60 md:h-28 md:w-52">
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-700 group-hover/tile:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(60% 80% at 50% 50%, rgba(212,164,55,0.18), transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
<Image
|
||||
src={client.src}
|
||||
alt={client.alt}
|
||||
width={200}
|
||||
height={100}
|
||||
className="relative max-h-14 w-auto object-contain opacity-70 brightness-[1.05] contrast-[0.9] grayscale transition-all duration-500 group-hover/tile:scale-105 group-hover/tile:opacity-100 group-hover/tile:grayscale-0 md:max-h-16"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ClientGrid({ clients }: { clients: Client[] }) {
|
||||
return (
|
||||
<div className="grid w-full grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6">
|
||||
{clients.map((c) => (
|
||||
<LogoTile key={c.src} client={c} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
188
.claude/skills/premium-frontend-designer/snippets/Footer.tsx
Normal file
@ -0,0 +1,188 @@
|
||||
/**
|
||||
* Premium 4-column footer w/ no duplicated nav links.
|
||||
*
|
||||
* Expected lib/content.ts shape:
|
||||
*
|
||||
* export const brand = {
|
||||
* name: "BRAND",
|
||||
* legalName: "Brand Legal LLC",
|
||||
* phone: "00 000 0000",
|
||||
* email: "info@example.com",
|
||||
* instagram: "@brand_handle",
|
||||
* };
|
||||
* export const socials = {
|
||||
* instagram: "https://www.instagram.com/brand_handle/",
|
||||
* whatsapp: "https://api.whatsapp.com/send/?phone=000000000000&text&type=phone_number&app_absent=0",
|
||||
* whatsappDisplay: "+00 00 000 0000",
|
||||
* };
|
||||
* export const credit = {
|
||||
* agency: "YS Lootah Tech",
|
||||
* url: "https://yslootahtech.com/",
|
||||
* };
|
||||
*/
|
||||
import { Mail, Phone } from "lucide-react";
|
||||
import { brand, socials, credit } from "@/lib/content";
|
||||
import { WhatsAppLink } from "./WhatsAppLink";
|
||||
import { AppBadge } from "./AppBadges";
|
||||
|
||||
function InstagramIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
aria-hidden
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="5" />
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<circle cx="17.5" cy="6.5" r="1.1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type Link = { label: string; href: string };
|
||||
|
||||
type Props = {
|
||||
navigationLinks: Link[];
|
||||
serviceItems: Link[];
|
||||
legalLinks: Link[];
|
||||
/** Brand logo node (provide your own LogoMark) */
|
||||
logo?: React.ReactNode;
|
||||
};
|
||||
|
||||
export function Footer({ navigationLinks, serviceItems, legalLinks, logo }: Props) {
|
||||
return (
|
||||
<footer className="relative overflow-hidden border-t border-bone/10 bg-obsidian pb-8 pt-11 md:pb-10 md:pt-14 lg:pb-12 lg:pt-[72px]">
|
||||
<div className="relative z-10 mx-auto max-w-[1480px] px-6 md:px-10">
|
||||
<div className="grid gap-10 lg:grid-cols-12 lg:gap-12">
|
||||
{/* Col 1 — Brand + contact */}
|
||||
<div className="space-y-5 lg:col-span-4">
|
||||
<a href="/" className="inline-flex items-center gap-4">
|
||||
{logo}
|
||||
<span className="hidden font-display text-2xl tracking-[0.22em] text-bone sm:inline">
|
||||
{brand.name}
|
||||
</span>
|
||||
</a>
|
||||
<p className="max-w-md text-sm text-mist text-pretty md:text-base">
|
||||
{brand.legalName}.
|
||||
</p>
|
||||
<div className="metallic-divider" />
|
||||
<div className="space-y-2 text-sm text-mist">
|
||||
<a
|
||||
href={`tel:${brand.phone.replace(/\s/g, "")}`}
|
||||
className="flex items-center gap-3 hover:text-gold-light"
|
||||
>
|
||||
<Phone className="size-4" />
|
||||
{brand.phone}
|
||||
</a>
|
||||
<a
|
||||
href={`mailto:${brand.email}`}
|
||||
className="flex items-center gap-3 hover:text-gold-light"
|
||||
>
|
||||
<Mail className="size-4" />
|
||||
{brand.email}
|
||||
</a>
|
||||
<WhatsAppLink
|
||||
href={socials.whatsapp}
|
||||
display={socials.whatsappDisplay}
|
||||
ariaLabel={`Chat with ${brand.name} on WhatsApp`}
|
||||
inline
|
||||
/>
|
||||
<a
|
||||
href={socials.instagram}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-3 hover:text-gold-light"
|
||||
>
|
||||
<InstagramIcon className="size-4" />
|
||||
Instagram · {brand.instagram}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Cols 2-4 */}
|
||||
<div className="grid gap-8 sm:grid-cols-3 lg:col-span-8 lg:gap-10">
|
||||
<FooterCol title="Navigation" links={navigationLinks} />
|
||||
<FooterCol title="Services" links={serviceItems} />
|
||||
<div>
|
||||
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
|
||||
Legal & App
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2.5">
|
||||
{legalLinks.map((l) => (
|
||||
<li key={l.label}>
|
||||
<a
|
||||
href={l.href}
|
||||
className="text-sm text-mist transition-colors hover:text-bone"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-5 flex flex-col items-start gap-2">
|
||||
<AppBadge store="appstore" small />
|
||||
<AppBadge store="googleplay" small />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-9 flex flex-col items-start justify-between gap-3 border-t border-bone/10 pt-5 text-xs uppercase tracking-[0.22em] text-mist md:mt-10 md:flex-row md:items-center md:gap-6">
|
||||
<span>
|
||||
© {new Date().getFullYear()} {brand.legalName}. All rights reserved.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-[11px] tracking-[0.04em] text-mist/60 md:text-xs">
|
||||
Website designed & developed by{" "}
|
||||
<a
|
||||
href={credit.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-mist/80 underline decoration-gold/30 decoration-1 underline-offset-4 transition-colors duration-500 hover:text-gold-light hover:decoration-gold/70"
|
||||
>
|
||||
{credit.agency}
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Watermark — contained inside footer */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-0 flex select-none justify-center overflow-hidden"
|
||||
>
|
||||
<span className="translate-y-[18%] font-display text-[14vw] leading-none tracking-[-0.02em] text-bone/[0.025] md:text-[12vw]">
|
||||
{brand.name}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterCol({ title, links }: { title: string; links: Link[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h4 className="text-[10px] font-medium uppercase tracking-[0.22em] text-gold-light">
|
||||
{title}
|
||||
</h4>
|
||||
<ul className="mt-4 space-y-2.5">
|
||||
{links.map((l) => (
|
||||
<li key={l.label}>
|
||||
<a
|
||||
href={l.href}
|
||||
className="text-sm text-mist transition-colors hover:text-bone"
|
||||
>
|
||||
{l.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
Icon?: LucideIcon;
|
||||
title: ReactNode;
|
||||
body?: ReactNode;
|
||||
href?: string;
|
||||
external?: boolean;
|
||||
className?: string;
|
||||
/** Append custom inner content below the body */
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function GlassCard({
|
||||
Icon,
|
||||
title,
|
||||
body,
|
||||
href,
|
||||
external,
|
||||
className,
|
||||
children,
|
||||
}: Props) {
|
||||
const inner = (
|
||||
<>
|
||||
{/* hover amber radial glow */}
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute -bottom-16 -right-16 size-48 rounded-full opacity-0 blur-3xl transition-opacity duration-700 group-hover:opacity-100"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(circle, rgba(212,164,55,0.28), transparent 70%)",
|
||||
}}
|
||||
/>
|
||||
{Icon ? (
|
||||
<div className="grid size-11 place-items-center rounded-full glass-gold transition-all duration-500 group-hover:rotate-[8deg] group-hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)]">
|
||||
<Icon className="size-[18px] text-gold-light" strokeWidth={1.4} />
|
||||
</div>
|
||||
) : null}
|
||||
<h4 className="mt-5 font-display text-xl text-bone transition-colors duration-500 group-hover:text-gold-light md:text-2xl">
|
||||
{title}
|
||||
</h4>
|
||||
{body ? (
|
||||
<p className="mt-2 text-sm text-mist text-pretty leading-relaxed">
|
||||
{body}
|
||||
</p>
|
||||
) : null}
|
||||
{children}
|
||||
{/* gold underline sweep */}
|
||||
<span
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute bottom-0 left-0 h-px w-0 bg-gradient-to-r from-gold via-gold-light to-transparent transition-all duration-700 group-hover:w-full"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const base = `group relative isolate h-full overflow-hidden rounded-2xl border border-bone/10 bg-graphite/40 p-6 backdrop-blur-sm transition-all duration-500 hover:-translate-y-1 hover:border-gold/30 hover:bg-graphite/60 md:p-7 ${className ?? ""}`;
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...(external
|
||||
? { target: "_blank", rel: "noopener noreferrer" }
|
||||
: {})}
|
||||
className={base}
|
||||
>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <div className={base}>{inner}</div>;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { ArrowUpRight } from "lucide-react";
|
||||
import type { ComponentPropsWithoutRef } from "react";
|
||||
|
||||
type Variant = "primary" | "ghost" | "outline";
|
||||
|
||||
type Props = ComponentPropsWithoutRef<"a"> & {
|
||||
variant?: Variant;
|
||||
arrow?: boolean;
|
||||
};
|
||||
|
||||
const base =
|
||||
"relative inline-flex items-center gap-2 rounded-full px-7 py-3.5 text-sm font-medium tracking-wide transition-all duration-500";
|
||||
|
||||
const variants: Record<Variant, string> = {
|
||||
primary:
|
||||
"bg-gradient-to-r from-gold-light via-gold to-gold-deep text-obsidian shadow-[0_10px_40px_-12px_rgba(212,164,55,0.6)] hover:shadow-[0_18px_50px_-12px_rgba(242,194,91,0.8)]",
|
||||
outline:
|
||||
"border border-bone/20 text-bone hover:border-gold/60 hover:text-gold-light",
|
||||
ghost: "text-bone hover:text-gold-light",
|
||||
};
|
||||
|
||||
export function PremiumButton({
|
||||
variant = "primary",
|
||||
className,
|
||||
children,
|
||||
arrow = true,
|
||||
...rest
|
||||
}: Props) {
|
||||
return (
|
||||
<a className={cn(base, variants[variant], className)} {...rest}>
|
||||
<span>{children}</span>
|
||||
{arrow ? (
|
||||
<ArrowUpRight className="size-4 transition-transform duration-500 group-hover:translate-x-0.5" />
|
||||
) : null}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/cn";
|
||||
import { motion, useReducedMotion, type Variants } from "framer-motion";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
type Props = {
|
||||
eyebrow?: string;
|
||||
index?: string;
|
||||
title: ReactNode;
|
||||
/** The gold-accent phrase rendered inside the title — pass as JSX or separate prop. */
|
||||
accent?: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
align?: "left" | "center";
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function SectionHeading({
|
||||
eyebrow,
|
||||
index,
|
||||
title,
|
||||
accent,
|
||||
subtitle,
|
||||
align = "left",
|
||||
className,
|
||||
}: Props) {
|
||||
const reduce = useReducedMotion();
|
||||
const v: Variants = {
|
||||
hidden: { opacity: 0, y: reduce ? 0 : 24, filter: reduce ? "blur(0px)" : "blur(6px)" },
|
||||
visible: {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
filter: "blur(0px)",
|
||||
transition: { duration: 0.9, ease: [0.16, 1, 0.3, 1] },
|
||||
},
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col gap-6",
|
||||
align === "center" ? "items-center text-center" : "items-start",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{eyebrow ? (
|
||||
<div className="flex items-center gap-3 text-[11px] font-medium uppercase tracking-[0.22em] text-mist">
|
||||
{index ? <span className="text-gold/85">{index}</span> : null}
|
||||
<span className="h-px w-8 bg-gold/40" />
|
||||
<span className="text-mist/85">{eyebrow}</span>
|
||||
</div>
|
||||
) : null}
|
||||
<motion.h2
|
||||
variants={v}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
className="font-display text-[clamp(2.4rem,5vw,4.6rem)] leading-[1.02] tracking-[-0.02em] text-bone text-balance"
|
||||
>
|
||||
{title}
|
||||
{accent ? <> <span className="gold-text">{accent}</span></> : null}
|
||||
</motion.h2>
|
||||
{subtitle ? (
|
||||
<motion.p
|
||||
variants={v}
|
||||
initial="hidden"
|
||||
whileInView="visible"
|
||||
viewport={{ once: true, amount: 0.3 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="max-w-2xl text-pretty text-mist md:text-lg"
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Refined gold WhatsApp link.
|
||||
*
|
||||
* Use 3 surfaces:
|
||||
* - <WhatsAppLink href={...} display="+CC XX XXX XXXX" ariaLabel="..." inline /> (footer row)
|
||||
* - <WhatsAppIconButton href={...} ariaLabel="..." /> (nav)
|
||||
* - <WhatsAppLink href={...} display={...} ariaLabel={...} /> (contact card)
|
||||
*
|
||||
* URL format expected: https://api.whatsapp.com/send/?phone=COUNTRYCODE+DIGITS&text&type=phone_number&app_absent=0
|
||||
* Phone in URL = digits only (no + or spaces). Display string = formatted "+CC XX XXX XXXX".
|
||||
*
|
||||
* lucide-react has no WhatsApp glyph → use this inline SVG.
|
||||
*/
|
||||
import type { SVGProps } from "react";
|
||||
|
||||
export function WhatsAppIcon({ className, ...rest }: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden
|
||||
className={className}
|
||||
{...rest}
|
||||
>
|
||||
<path d="M19.11 4.86A9.94 9.94 0 0 0 12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.33 4.95L2 22l5.27-1.38a9.93 9.93 0 0 0 4.76 1.21h.01c5.46 0 9.91-4.45 9.91-9.91 0-2.65-1.03-5.14-2.84-7.06ZM12.04 20.15c-1.5 0-2.96-.4-4.24-1.17l-.3-.18-3.13.82.83-3.05-.2-.31a8.21 8.21 0 0 1-1.26-4.35c0-4.53 3.69-8.22 8.22-8.22 2.2 0 4.27.86 5.82 2.41a8.16 8.16 0 0 1 2.41 5.81c0 4.53-3.69 8.24-8.15 8.24Zm4.72-6.16c-.26-.13-1.53-.75-1.76-.83-.24-.09-.41-.13-.58.13-.17.26-.66.83-.81 1-.15.17-.3.2-.55.07-.26-.13-1.1-.41-2.1-1.29-.78-.69-1.3-1.55-1.45-1.81-.15-.26-.02-.4.11-.53.11-.11.26-.3.39-.45.13-.15.17-.26.26-.43.09-.17.04-.32-.02-.45-.07-.13-.58-1.4-.79-1.92-.21-.5-.42-.43-.58-.44-.15-.01-.32-.01-.49-.01-.17 0-.45.06-.69.32-.24.26-.9.88-.9 2.15 0 1.27.92 2.5 1.05 2.67.13.17 1.82 2.78 4.42 3.9.62.27 1.1.43 1.48.55.62.2 1.18.17 1.63.1.5-.07 1.53-.62 1.74-1.23.21-.6.21-1.12.15-1.23-.06-.11-.23-.18-.49-.31Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
type LinkProps = {
|
||||
href: string;
|
||||
display: string;
|
||||
ariaLabel: string;
|
||||
inline?: boolean; // true = footer row; false = larger contact-card row
|
||||
};
|
||||
|
||||
export function WhatsAppLink({ href, display, ariaLabel, inline }: LinkProps) {
|
||||
if (inline) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
className="flex items-center gap-3 text-sm text-mist hover:text-gold-light"
|
||||
>
|
||||
<WhatsAppIcon className="size-4" />
|
||||
WhatsApp · {display}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
className="group flex items-center gap-3 rounded-xl border border-bone/8 bg-bone/[0.03] p-3.5 transition-colors duration-500 hover:border-gold/30"
|
||||
>
|
||||
<span className="grid size-9 shrink-0 place-items-center rounded-full border border-gold/30 bg-gold/[0.08] text-gold-light shadow-[0_0_14px_-4px_rgba(212,164,55,0.6)]">
|
||||
<WhatsAppIcon className="size-4" />
|
||||
</span>
|
||||
<span className="text-bone group-hover:text-gold-light">
|
||||
WhatsApp · {display}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
type ButtonProps = {
|
||||
href: string;
|
||||
ariaLabel: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function WhatsAppIconButton({ href, ariaLabel, className }: ButtonProps) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={ariaLabel}
|
||||
className={`group grid size-10 place-items-center rounded-full border border-bone/15 text-bone transition-all duration-500 hover:border-gold/40 hover:text-gold-light hover:shadow-[0_0_22px_-6px_rgba(212,164,55,0.6)] hover:scale-[1.04] ${className ?? ""}`}
|
||||
>
|
||||
<WhatsAppIcon className="size-[18px]" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
4
.gitignore
vendored
@ -37,3 +37,7 @@ next-env.d.ts
|
||||
.vscode
|
||||
|
||||
/src/generated/prisma
|
||||
|
||||
# Local design references — not part of project
|
||||
/references
|
||||
|
||||
|
||||
BIN
YS Lootah Robotics final-1.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
39
public/images/robots/pudu-bellabot.svg
Normal file
@ -0,0 +1,39 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu BellaBot delivery robot silhouette">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="45%" r="60%">
|
||||
<stop offset="0%" stop-color="#c4a265" stop-opacity="0.35"/>
|
||||
<stop offset="60%" stop-color="#0a0907" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f5f1e8"/>
|
||||
<stop offset="100%" stop-color="#94908a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="screen" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#1f1a10"/>
|
||||
<stop offset="100%" stop-color="#0a0907"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e0c896"/>
|
||||
<stop offset="100%" stop-color="#8b6f47"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="600" fill="url(#bg)"/>
|
||||
<g transform="translate(300 480)">
|
||||
<ellipse cx="0" cy="0" rx="160" ry="14" fill="#000" opacity="0.55"/>
|
||||
</g>
|
||||
<g transform="translate(300 100)">
|
||||
<rect x="-120" y="0" width="240" height="120" rx="36" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<rect x="-100" y="20" width="200" height="80" rx="22" fill="url(#screen)"/>
|
||||
<circle cx="-44" cy="60" r="14" fill="url(#accent)"/>
|
||||
<circle cx="44" cy="60" r="14" fill="url(#accent)"/>
|
||||
<path d="M -30 88 Q 0 78 30 88" stroke="url(#accent)" stroke-width="4" fill="none" stroke-linecap="round"/>
|
||||
<rect x="-140" y="140" width="280" height="14" rx="6" fill="#5a4a30"/>
|
||||
<rect x="-128" y="170" width="256" height="50" rx="12" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<rect x="-128" y="240" width="256" height="50" rx="12" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<rect x="-128" y="310" width="256" height="50" rx="12" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<rect x="-100" y="370" width="200" height="14" rx="6" fill="#3a2f1c"/>
|
||||
<circle cx="-72" cy="384" r="20" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
<circle cx="72" cy="384" r="20" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
</g>
|
||||
<text x="50%" y="96%" text-anchor="middle" fill="#94908a" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU BELLABOT</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.3 KiB |
31
public/images/robots/pudu-cc1.svg
Normal file
@ -0,0 +1,31 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu CC1 cleaning robot silhouette">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="55%" r="60%">
|
||||
<stop offset="0%" stop-color="#c4a265" stop-opacity="0.32"/>
|
||||
<stop offset="60%" stop-color="#0a0907" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f5f1e8"/>
|
||||
<stop offset="100%" stop-color="#94908a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e0c896"/>
|
||||
<stop offset="100%" stop-color="#8b6f47"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="600" fill="url(#bg)"/>
|
||||
<g transform="translate(300 360)">
|
||||
<ellipse cx="0" cy="120" rx="200" ry="20" fill="#000" opacity="0.55"/>
|
||||
<rect x="-180" y="-180" width="360" height="240" rx="40" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<rect x="-160" y="-160" width="160" height="100" rx="18" fill="#0a0907"/>
|
||||
<rect x="-150" y="-150" width="140" height="14" rx="6" fill="url(#accent)" opacity="0.9"/>
|
||||
<rect x="-150" y="-130" width="100" height="10" rx="4" fill="#5a4a30"/>
|
||||
<rect x="-150" y="-110" width="120" height="10" rx="4" fill="#5a4a30"/>
|
||||
<circle cx="120" cy="-110" r="34" fill="#1f1a10" stroke="#5a4a30" stroke-width="2"/>
|
||||
<circle cx="120" cy="-110" r="14" fill="url(#accent)"/>
|
||||
<rect x="-170" y="80" width="340" height="30" rx="8" fill="#0a0907"/>
|
||||
<circle cx="-130" cy="110" r="22" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
<circle cx="130" cy="110" r="22" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
</g>
|
||||
<text x="50%" y="96%" text-anchor="middle" fill="#94908a" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU CC1 CLEANING ROBOT</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
35
public/images/robots/pudu-kettybot.svg
Normal file
@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu KettyBot service robot silhouette">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="45%" r="60%">
|
||||
<stop offset="0%" stop-color="#c4a265" stop-opacity="0.32"/>
|
||||
<stop offset="60%" stop-color="#0a0907" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f5f1e8"/>
|
||||
<stop offset="100%" stop-color="#94908a"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="screen" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#0a0907"/>
|
||||
<stop offset="100%" stop-color="#3a2f1c"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e0c896"/>
|
||||
<stop offset="100%" stop-color="#c4a265"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="600" fill="url(#bg)"/>
|
||||
<g transform="translate(300 110)">
|
||||
<rect x="-110" y="0" width="220" height="280" rx="28" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<rect x="-92" y="22" width="184" height="240" rx="18" fill="url(#screen)"/>
|
||||
<g transform="translate(0 110)">
|
||||
<circle cx="-30" cy="0" r="12" fill="url(#accent)"/>
|
||||
<circle cx="30" cy="0" r="12" fill="url(#accent)"/>
|
||||
<path d="M -22 30 Q 0 18 22 30" stroke="url(#accent)" stroke-width="3" fill="none" stroke-linecap="round"/>
|
||||
</g>
|
||||
<rect x="-130" y="300" width="260" height="14" rx="6" fill="#5a4a30"/>
|
||||
<path d="M -120 320 L 120 320 L 100 460 L -100 460 Z" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<circle cx="-70" cy="460" r="22" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
<circle cx="70" cy="460" r="22" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
</g>
|
||||
<text x="50%" y="96%" text-anchor="middle" fill="#94908a" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU KETTYBOT</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
29
public/images/robots/pudu-pudubot.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 600" role="img" aria-label="Pudu D-Series industrial delivery robot silhouette">
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="50%" cy="55%" r="60%">
|
||||
<stop offset="0%" stop-color="#c4a265" stop-opacity="0.32"/>
|
||||
<stop offset="60%" stop-color="#0a0907" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#f5f1e8"/>
|
||||
<stop offset="100%" stop-color="#6b6862"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="accent" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#e0c896"/>
|
||||
<stop offset="100%" stop-color="#c4a265"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="600" height="600" fill="url(#bg)"/>
|
||||
<g transform="translate(300 380)">
|
||||
<ellipse cx="0" cy="120" rx="200" ry="18" fill="#000" opacity="0.55"/>
|
||||
<rect x="-180" y="-220" width="360" height="320" rx="36" fill="url(#body)" stroke="#3a2f1c" stroke-width="2"/>
|
||||
<rect x="-160" y="-200" width="320" height="40" rx="10" fill="#0a0907"/>
|
||||
<rect x="-160" y="-140" width="320" height="80" rx="12" fill="#1f1a10"/>
|
||||
<rect x="-160" y="-40" width="320" height="80" rx="12" fill="#1f1a10"/>
|
||||
<rect x="-160" y="60" width="320" height="30" rx="10" fill="url(#accent)" opacity="0.85"/>
|
||||
<rect x="-180" y="100" width="360" height="20" rx="8" fill="#0a0907"/>
|
||||
<circle cx="-130" cy="120" r="26" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
<circle cx="130" cy="120" r="26" fill="#0a0907" stroke="#5a4a30" stroke-width="2"/>
|
||||
</g>
|
||||
<text x="50%" y="96%" text-anchor="middle" fill="#94908a" font-family="Inter, sans-serif" font-size="20" letter-spacing="6">PUDU D-SERIES</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/images/robots/unitree-a2.png
Normal file
|
After Width: | Height: | Size: 512 KiB |
BIN
public/images/robots/unitree-as2.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/images/robots/unitree-b2.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
public/images/robots/unitree-g1.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/images/robots/unitree-go2.png
Normal file
|
After Width: | Height: | Size: 409 KiB |
BIN
public/images/robots/unitree-h2.png
Normal file
|
After Width: | Height: | Size: 918 KiB |
BIN
public/images/robots/unitree-r1.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
55
src/app/about/page.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { DemoCTA } from '@/components/robotics/DemoCTA';
|
||||
import { WhyUs } from '@/components/robotics/WhyUs';
|
||||
import { FloatingTechPanel } from '@/components/robotics/FloatingTechPanel';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'About YS Lootah Robotics — Exclusive UAE Access to Unitree & Pudu',
|
||||
description:
|
||||
'YS Lootah Robotics holds exclusive UAE sales rights for selected Unitree and Pudu Robotics solutions — with a Dubai-based team managing sales, demo, configuration, and deployment.',
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
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)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem', maxWidth: 820 }}>
|
||||
<span className="eyebrow">About · Exclusive UAE Access</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 }}>The UAE's dedicated destination for Unitree and Pudu Robotics.</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
YS Lootah Robotics is the robotics arm of the YS Lootah group — a UAE-based technology partner that holds exclusive sales rights in the UAE for selected Unitree and Pudu Robotics solutions. Our Dubai team manages sales, live demos, configuration, deployment, and ongoing service across the UAE.
|
||||
</p>
|
||||
<p style={{ margin: 0, color: '#94908a', fontSize: '0.92rem', lineHeight: 1.7 }}>
|
||||
Brand names and product trademarks are property of their respective owners. Available exclusively in the UAE through YS Lootah Robotics.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<MotionSection>
|
||||
<FloatingTechPanel />
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.5rem, 3vw, 2.2rem)', fontWeight: 400, letterSpacing: '-0.02em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>Why work with us</span>
|
||||
</h2>
|
||||
<WhyUs />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<DemoCTA />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,172 +1,172 @@
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/login/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
router.push('/admin/');
|
||||
router.refresh();
|
||||
} else {
|
||||
setError(data.error ?? 'Login failed');
|
||||
}
|
||||
} catch {
|
||||
setError('Network error. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={pageStyle}>
|
||||
<div style={cardStyle}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
|
||||
<div style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto 1rem',
|
||||
fontSize: '1.25rem',
|
||||
}}>
|
||||
🔐
|
||||
</div>
|
||||
<h1 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#1a1a2e', margin: 0, marginBottom: '0.25rem' }}>
|
||||
Admin Login
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.8rem', color: '#94a3b8', margin: 0 }}>
|
||||
Lootah Robotics — G1 Configurator
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div>
|
||||
<label style={labelStyle} htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={inputStyle}
|
||||
onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')}
|
||||
onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')}
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle} htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={inputStyle}
|
||||
onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')}
|
||||
onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '0.6rem 0.875rem',
|
||||
borderRadius: '0.375rem',
|
||||
background: 'rgba(239, 68, 68, 0.06)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.2)',
|
||||
color: '#dc2626',
|
||||
fontSize: '0.8rem',
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '0.7rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
background: loading ? 'rgba(148, 163, 184, 0.1)' : 'rgba(59, 130, 246, 0.08)',
|
||||
color: loading ? '#94a3b8' : '#2563eb',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
marginTop: '0.25rem',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const pageStyle: React.CSSProperties = {
|
||||
minHeight: '100vh',
|
||||
background: '#ffffff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '2rem',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
};
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
maxWidth: '380px',
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
borderRadius: '1rem',
|
||||
padding: '2rem',
|
||||
};
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
marginBottom: '0.375rem',
|
||||
letterSpacing: '0.02em',
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '0.6rem 0.875rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||
background: '#ffffff',
|
||||
color: '#1a1a2e',
|
||||
fontSize: '0.875rem',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s ease',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
'use client';
|
||||
|
||||
import { useState, FormEvent } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const router = useRouter();
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch('/api/admin/login/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
router.push('/admin/');
|
||||
router.refresh();
|
||||
} else {
|
||||
setError(data.error ?? 'Login failed');
|
||||
}
|
||||
} catch {
|
||||
setError('Network error. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={pageStyle}>
|
||||
<div style={cardStyle}>
|
||||
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
|
||||
<div style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
borderRadius: '12px',
|
||||
background: 'rgba(59, 130, 246, 0.08)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.2)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
margin: '0 auto 1rem',
|
||||
fontSize: '1.25rem',
|
||||
}}>
|
||||
🔐
|
||||
</div>
|
||||
<h1 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#0a0907', margin: 0, marginBottom: '0.25rem' }}>
|
||||
Admin Login
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.8rem', color: '#94908a', margin: 0 }}>
|
||||
Lootah Robotics — G1 Configurator
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div>
|
||||
<label style={labelStyle} htmlFor="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
style={inputStyle}
|
||||
onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')}
|
||||
onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')}
|
||||
placeholder="admin"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle} htmlFor="password">Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
style={inputStyle}
|
||||
onFocus={(e) => (e.currentTarget.style.borderColor = 'rgba(59, 130, 246, 0.5)')}
|
||||
onBlur={(e) => (e.currentTarget.style.borderColor = 'rgba(0, 0, 0, 0.1)')}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div style={{
|
||||
padding: '0.6rem 0.875rem',
|
||||
borderRadius: '0.375rem',
|
||||
background: 'rgba(239, 68, 68, 0.06)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.2)',
|
||||
color: '#dc2626',
|
||||
fontSize: '0.8rem',
|
||||
}}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
style={{
|
||||
padding: '0.7rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
background: loading ? 'rgba(196, 162, 101, 0.1)' : 'rgba(59, 130, 246, 0.08)',
|
||||
color: loading ? '#94908a' : '#2563eb',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
cursor: loading ? 'not-allowed' : 'pointer',
|
||||
transition: 'all 0.2s ease',
|
||||
marginTop: '0.25rem',
|
||||
}}
|
||||
>
|
||||
{loading ? 'Signing in…' : 'Sign In'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const pageStyle: React.CSSProperties = {
|
||||
minHeight: '100vh',
|
||||
background: '#ffffff',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '2rem',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
};
|
||||
|
||||
const cardStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
maxWidth: '380px',
|
||||
background: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.06)',
|
||||
borderRadius: '1rem',
|
||||
padding: '2rem',
|
||||
};
|
||||
|
||||
const labelStyle: React.CSSProperties = {
|
||||
display: 'block',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
color: '#374151',
|
||||
marginBottom: '0.375rem',
|
||||
letterSpacing: '0.02em',
|
||||
};
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '0.6rem 0.875rem',
|
||||
borderRadius: '0.5rem',
|
||||
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||
background: '#ffffff',
|
||||
color: '#0a0907',
|
||||
fontSize: '0.875rem',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s ease',
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
|
||||
@ -488,7 +488,7 @@ export default function AdminPage() {
|
||||
new Date(ts * 1000).toLocaleDateString('en-AE', { day: 'numeric', month: 'short', year: 'numeric' });
|
||||
|
||||
if (!isPricingHydrated) {
|
||||
return <div style={pageStyle}><p style={{ color: '#64748b' }}>Loading…</p></div>;
|
||||
return <div style={pageStyle}><p style={{ color: '#6b6862' }}>Loading…</p></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -497,10 +497,10 @@ export default function AdminPage() {
|
||||
{/* HEADER */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '1.5rem' }}>
|
||||
<div>
|
||||
<h1 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#1a1a2e', margin: 0, marginBottom: '0.2rem' }}>
|
||||
<h1 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#0a0907', margin: 0, marginBottom: '0.2rem' }}>
|
||||
Admin Dashboard
|
||||
</h1>
|
||||
<p style={{ fontSize: '0.75rem', color: '#94a3b8', margin: 0 }}>Lootah Robotics G1 Configurator</p>
|
||||
<p style={{ fontSize: '0.75rem', color: '#94908a', margin: 0 }}>Lootah Robotics G1 Configurator</p>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center' }}>
|
||||
<button onClick={() => setShowPwModal(true)} style={ghostBtnStyle}>Change Password</button>
|
||||
@ -527,7 +527,7 @@ export default function AdminPage() {
|
||||
borderRadius: '0.375rem',
|
||||
border: 'none',
|
||||
background: activeTab === t ? 'rgba(59,130,246,0.08)' : 'transparent',
|
||||
color: activeTab === t ? '#2563eb' : '#64748b',
|
||||
color: activeTab === t ? '#2563eb' : '#6b6862',
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: activeTab === t ? 600 : 400,
|
||||
cursor: 'pointer',
|
||||
@ -558,9 +558,9 @@ export default function AdminPage() {
|
||||
aria-label={`Label for ${item.label}`}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#94a3b8', fontFamily: 'monospace', paddingLeft: '0.25rem' }}>{item.id}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#94908a', fontFamily: 'monospace', paddingLeft: '0.25rem' }}>{item.id}</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<span style={{ fontSize: '0.7rem', color: '#94a3b8' }}>AED</span>
|
||||
<span style={{ fontSize: '0.7rem', color: '#94908a' }}>AED</span>
|
||||
<input
|
||||
type="text"
|
||||
value={formatPrice(editedPrices[item.id] ?? item.price)}
|
||||
@ -584,7 +584,7 @@ export default function AdminPage() {
|
||||
) : null}
|
||||
<label
|
||||
title="Upload / replace .glb"
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', cursor: rowGlbUploading[item.id] ? 'wait' : 'pointer', fontSize: '0.7rem', color: rowGlbUploading[item.id] ? '#2563eb' : '#94a3b8', background: rowGlbUploading[item.id] ? 'rgba(59,130,246,0.06)' : 'transparent', border: `1px dashed ${rowGlbUploading[item.id] ? 'rgba(59,130,246,0.3)' : 'rgba(0,0,0,0.12)'}`, borderRadius: '0.375rem', padding: '0.2rem 0.45rem', whiteSpace: 'nowrap' }}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: '0.25rem', cursor: rowGlbUploading[item.id] ? 'wait' : 'pointer', fontSize: '0.7rem', color: rowGlbUploading[item.id] ? '#2563eb' : '#94908a', background: rowGlbUploading[item.id] ? 'rgba(59,130,246,0.06)' : 'transparent', border: `1px dashed ${rowGlbUploading[item.id] ? 'rgba(59,130,246,0.3)' : 'rgba(0,0,0,0.12)'}`, borderRadius: '0.375rem', padding: '0.2rem 0.45rem', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<input type="file" accept=".glb" style={{ display: 'none' }} onChange={(e) => {
|
||||
const f = e.target.files?.[0] ?? null;
|
||||
@ -642,7 +642,7 @@ export default function AdminPage() {
|
||||
background: addItemGlb ? 'rgba(59,130,246,0.04)' : '#fff',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8rem',
|
||||
color: addItemGlb ? '#2563eb' : '#94a3b8',
|
||||
color: addItemGlb ? '#2563eb' : '#94908a',
|
||||
transition: 'all 0.2s',
|
||||
}}>
|
||||
<input
|
||||
@ -658,7 +658,7 @@ export default function AdminPage() {
|
||||
{addItemGlb && (
|
||||
<button
|
||||
onClick={(e) => { e.preventDefault(); setAddItemGlb(null); }}
|
||||
style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: '#94a3b8', fontSize: '0.75rem', padding: '0 2px' }}
|
||||
style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: '#94908a', fontSize: '0.75rem', padding: '0 2px' }}
|
||||
>✕</button>
|
||||
)}
|
||||
</label>
|
||||
@ -686,7 +686,7 @@ export default function AdminPage() {
|
||||
{activeTab === 'personas' && (
|
||||
<div>
|
||||
{!isPersonaHydrated ? (
|
||||
<p style={{ color: '#64748b', fontSize: '0.85rem' }}>Loading personas…</p>
|
||||
<p style={{ color: '#6b6862', fontSize: '0.85rem' }}>Loading personas…</p>
|
||||
) : (
|
||||
<TableCard>
|
||||
<TableHeader cols="1fr 80px 80px 56px" labels={['Persona', 'Torso', 'Legs', '']} />
|
||||
@ -694,15 +694,15 @@ export default function AdminPage() {
|
||||
<div key={p.id} style={{ display: 'grid', gridTemplateColumns: '1fr 80px 80px 56px', padding: '0.875rem 1.25rem', alignItems: 'center', borderBottom: i < personas.length - 1 ? '1px solid rgba(0,0,0,0.04)' : 'none' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.85rem', color: '#374151', fontWeight: 500 }}>{p.label}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>{p.description}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#94908a' }}>{p.description}</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: 4, backgroundColor: p.colors.torso, border: '1px solid rgba(0,0,0,0.1)', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '0.7rem', color: '#94a3b8', fontFamily: 'monospace' }}>{p.colors.torso}</span>
|
||||
<span style={{ fontSize: '0.7rem', color: '#94908a', fontFamily: 'monospace' }}>{p.colors.torso}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.375rem' }}>
|
||||
<div style={{ width: 20, height: 20, borderRadius: 4, backgroundColor: p.colors.legs, border: '1px solid rgba(0,0,0,0.1)', flexShrink: 0 }} />
|
||||
<span style={{ fontSize: '0.7rem', color: '#94a3b8', fontFamily: 'monospace' }}>{p.colors.legs}</span>
|
||||
<span style={{ fontSize: '0.7rem', color: '#94908a', fontFamily: 'monospace' }}>{p.colors.legs}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
{p.id !== 'none' && (
|
||||
@ -762,7 +762,7 @@ export default function AdminPage() {
|
||||
</div>
|
||||
{ordersError && <p style={errorTextStyle}>{ordersError}</p>}
|
||||
{!ordersLoading && orders.length === 0 && !ordersError && (
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.85rem', textAlign: 'center', padding: '2rem' }}>No orders found.</p>
|
||||
<p style={{ color: '#94908a', fontSize: '0.85rem', textAlign: 'center', padding: '2rem' }}>No orders found.</p>
|
||||
)}
|
||||
{orders.length > 0 && (
|
||||
<TableCard>
|
||||
@ -791,7 +791,7 @@ export default function AdminPage() {
|
||||
</div>
|
||||
{contactsError && <p style={errorTextStyle}>{contactsError}</p>}
|
||||
{!contactsLoading && contacts.length === 0 && !contactsError && (
|
||||
<p style={{ color: '#94a3b8', fontSize: '0.85rem', textAlign: 'center', padding: '2rem' }}>No contact inquiries yet.</p>
|
||||
<p style={{ color: '#94908a', fontSize: '0.85rem', textAlign: 'center', padding: '2rem' }}>No contact inquiries yet.</p>
|
||||
)}
|
||||
{contacts.length > 0 && (
|
||||
<TableCard>
|
||||
@ -799,10 +799,10 @@ export default function AdminPage() {
|
||||
{contacts.map((c, i) => (
|
||||
<div key={c.id} style={{ display: 'grid', gridTemplateColumns: '1.5fr 1fr 1fr 2.5fr 1fr', padding: '1rem 1.25rem', alignItems: 'flex-start', borderBottom: i < contacts.length - 1 ? '1px solid rgba(0,0,0,0.04)' : 'none', gap: '1rem' }}>
|
||||
<div style={{ fontSize: '0.85rem', fontWeight: 500, color: '#1e293b' }}>{c.name}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#64748b' }}><a href={`mailto:${c.email}`} style={{ color: '#3b82f6', textDecoration: 'none' }}>{c.email}</a></div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#64748b' }}>{c.phone ? <a href={`tel:${c.phone}`} style={{ color: '#64748b', textDecoration: 'none' }}>{c.phone}</a> : '-'}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#6b6862' }}><a href={`mailto:${c.email}`} style={{ color: '#3b82f6', textDecoration: 'none' }}>{c.email}</a></div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#6b6862' }}>{c.phone ? <a href={`tel:${c.phone}`} style={{ color: '#6b6862', textDecoration: 'none' }}>{c.phone}</a> : '-'}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#475569', whiteSpace: 'pre-wrap', lineHeight: 1.5 }}>{c.message}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#94a3b8' }}>{new Date(c.createdAt).toLocaleDateString('en-AE')}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#94908a' }}>{new Date(c.createdAt).toLocaleDateString('en-AE')}</div>
|
||||
</div>
|
||||
))}
|
||||
</TableCard>
|
||||
@ -813,15 +813,15 @@ export default function AdminPage() {
|
||||
{/* SETTINGS TAB */}
|
||||
{activeTab === 'settings' && (
|
||||
<div>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700, color: '#1a1a2e', margin: '0 0 1rem' }}>App Settings</h2>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700, color: '#0a0907', margin: '0 0 1rem' }}>App Settings</h2>
|
||||
{settingError && <p style={{ color: '#dc2626', fontSize: '0.8rem', marginBottom: '0.75rem' }}>{settingError}</p>}
|
||||
{settingsLoading ? (
|
||||
<p style={{ color: '#64748b', fontSize: '0.875rem' }}>Loading…</p>
|
||||
<p style={{ color: '#6b6862', fontSize: '0.875rem' }}>Loading…</p>
|
||||
) : (
|
||||
<TableCard>
|
||||
<TableHeader cols="1fr 1.5fr 110px 50px" labels={['Key', 'Value', '', '']} />
|
||||
{settings.length === 0 && (
|
||||
<p style={{ padding: '1rem', color: '#94a3b8', fontSize: '0.8rem' }}>No settings yet.</p>
|
||||
<p style={{ padding: '1rem', color: '#94908a', fontSize: '0.8rem' }}>No settings yet.</p>
|
||||
)}
|
||||
{settings.map((s, i) => (
|
||||
<SettingRow
|
||||
@ -837,8 +837,8 @@ export default function AdminPage() {
|
||||
)}
|
||||
|
||||
{/* Add new setting */}
|
||||
<div style={{ marginTop: '1.25rem', padding: '1rem', background: '#f8fafc', borderRadius: '0.75rem', border: '1px solid rgba(0,0,0,0.06)' }}>
|
||||
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, color: '#1a1a2e', margin: '0 0 0.75rem' }}>Add Setting</h3>
|
||||
<div style={{ marginTop: '1.25rem', padding: '1rem', background: '#f5f1e8', borderRadius: '0.75rem', border: '1px solid rgba(0,0,0,0.06)' }}>
|
||||
<h3 style={{ fontSize: '0.875rem', fontWeight: 600, color: '#0a0907', margin: '0 0 0.75rem' }}>Add Setting</h3>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.5fr auto', gap: '0.5rem', alignItems: 'flex-end' }}>
|
||||
<div>
|
||||
<label style={labelStyle}>Key</label>
|
||||
@ -869,7 +869,7 @@ export default function AdminPage() {
|
||||
{showPwModal && (
|
||||
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.3)', backdropFilter: 'blur(4px)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 50 }}>
|
||||
<div style={{ background: '#fff', borderRadius: '1rem', padding: '1.5rem', width: '100%', maxWidth: '380px', boxShadow: '0 20px 60px rgba(0,0,0,0.15)' }}>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700, color: '#1a1a2e', margin: '0 0 1.25rem' }}>Change Password</h2>
|
||||
<h2 style={{ fontSize: '1rem', fontWeight: 700, color: '#0a0907', margin: '0 0 1.25rem' }}>Change Password</h2>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{(['current', 'next', 'confirm'] as const).map((field) => (
|
||||
<div key={field}>
|
||||
@ -918,7 +918,7 @@ function SettingRow({
|
||||
useEffect(() => { setEditVal(setting.value); }, [setting.value]);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.5fr 110px 50px', gap: '0.75rem', padding: '0.625rem 1rem', alignItems: 'center', background: index % 2 === 0 ? '#fff' : '#f8fafc', borderTop: index > 0 ? '1px solid rgba(0,0,0,0.04)' : 'none' }}>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1.5fr 110px 50px', gap: '0.75rem', padding: '0.625rem 1rem', alignItems: 'center', background: index % 2 === 0 ? '#fff' : '#f5f1e8', borderTop: index > 0 ? '1px solid rgba(0,0,0,0.04)' : 'none' }}>
|
||||
<span style={{ fontSize: '0.8rem', fontFamily: 'monospace', color: '#334155' }}>{setting.key}</span>
|
||||
<input
|
||||
value={editVal}
|
||||
@ -979,7 +979,7 @@ function OrderRow({
|
||||
const fieldStyle: React.CSSProperties = {
|
||||
fontSize: '0.63rem',
|
||||
fontWeight: 600,
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.04em',
|
||||
};
|
||||
@ -996,8 +996,8 @@ function OrderRow({
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 130px 90px 100px 28px', padding: '0.75rem 1.25rem', alignItems: 'center' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.85rem', color: '#374151', fontWeight: 500 }}>{name}</div>
|
||||
{email && <div style={{ fontSize: '0.7rem', color: '#94a3b8' }}>{email}</div>}
|
||||
<div style={{ fontSize: '0.65rem', color: '#cbd5e1', fontFamily: 'monospace', marginTop: '0.1rem' }}>{order.id}</div>
|
||||
{email && <div style={{ fontSize: '0.7rem', color: '#94908a' }}>{email}</div>}
|
||||
<div style={{ fontSize: '0.65rem', color: '#cbc4b3', fontFamily: 'monospace', marginTop: '0.1rem' }}>{order.id}</div>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.85rem', color: '#374151', fontWeight: 500 }}>{formatAmount(order.amount, order.currency)}</div>
|
||||
<div>
|
||||
@ -1008,17 +1008,17 @@ function OrderRow({
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
textTransform: 'uppercase',
|
||||
background: order.status === 'succeeded' ? 'rgba(34,197,94,0.1)' : order.status === 'canceled' ? 'rgba(239,68,68,0.08)' : 'rgba(148,163,184,0.15)',
|
||||
color: order.status === 'succeeded' ? '#16a34a' : order.status === 'canceled' ? '#dc2626' : '#64748b',
|
||||
background: order.status === 'succeeded' ? 'rgba(34,197,94,0.1)' : order.status === 'canceled' ? 'rgba(239,68,68,0.08)' : 'rgba(196, 162, 101,0.15)',
|
||||
color: order.status === 'succeeded' ? '#16a34a' : order.status === 'canceled' ? '#dc2626' : '#6b6862',
|
||||
}}>
|
||||
{order.status}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#94a3b8' }}>{formatDate(order.created)}</div>
|
||||
<div style={{ fontSize: '0.75rem', color: '#94908a' }}>{formatDate(order.created)}</div>
|
||||
<button
|
||||
onClick={handleExpand}
|
||||
title={expanded ? 'Collapse' : 'Show details'}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#94a3b8', fontSize: '0.7rem', padding: '2px 4px', borderRadius: 4 }}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#94908a', fontSize: '0.7rem', padding: '2px 4px', borderRadius: 4 }}
|
||||
>
|
||||
{expanded ? '▲' : '▼'}
|
||||
</button>
|
||||
@ -1073,16 +1073,16 @@ function OrderRow({
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: '0.4rem', marginTop: '0.1rem', borderTop: '1px solid rgba(0,0,0,0.07)' }}>
|
||||
<span style={{ fontSize: '0.82rem', fontWeight: 700, color: '#1a1a2e' }}>Total</span>
|
||||
<span style={{ fontSize: '0.88rem', fontWeight: 700, color: '#1a1a2e', fontFamily: 'monospace' }}>
|
||||
<span style={{ fontSize: '0.82rem', fontWeight: 700, color: '#0a0907' }}>Total</span>
|
||||
<span style={{ fontSize: '0.88rem', fontWeight: 700, color: '#0a0907', fontFamily: 'monospace' }}>
|
||||
{formatAmount(order.amount, order.currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: '0.78rem', color: '#64748b' }}>Total (legacy order)</span>
|
||||
<span style={{ fontSize: '0.88rem', fontWeight: 700, color: '#1a1a2e', fontFamily: 'monospace' }}>
|
||||
<span style={{ fontSize: '0.78rem', color: '#6b6862' }}>Total (legacy order)</span>
|
||||
<span style={{ fontSize: '0.88rem', fontWeight: 700, color: '#0a0907', fontFamily: 'monospace' }}>
|
||||
{formatAmount(order.amount, order.currency)}
|
||||
</span>
|
||||
</div>
|
||||
@ -1090,16 +1090,16 @@ function OrderRow({
|
||||
</SectionBox>
|
||||
|
||||
{/* Payment ID */}
|
||||
<div style={{ fontSize: '0.65rem', color: '#cbd5e1', fontFamily: 'monospace' }}>
|
||||
<div style={{ fontSize: '0.65rem', color: '#cbc4b3', fontFamily: 'monospace' }}>
|
||||
Payment ID: {order.id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: snapshot */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.375rem' }}>
|
||||
<div style={{ fontSize: '0.63rem', fontWeight: 600, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Robot Snapshot</div>
|
||||
<div style={{ fontSize: '0.63rem', fontWeight: 600, color: '#94908a', textTransform: 'uppercase', letterSpacing: '0.04em' }}>Robot Snapshot</div>
|
||||
{snapshot === 'loading' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(248,248,246,0.8)', borderRadius: '0.5rem', minHeight: 120, fontSize: '0.75rem', color: '#94a3b8' }}>Loading…</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(248,248,246,0.8)', borderRadius: '0.5rem', minHeight: 120, fontSize: '0.75rem', color: '#94908a' }}>Loading…</div>
|
||||
)}
|
||||
{snapshot && snapshot !== 'loading' && snapshot !== 'none' && (
|
||||
/* eslint-disable-next-line @next/next/no-img-element */
|
||||
@ -1110,7 +1110,7 @@ function OrderRow({
|
||||
/>
|
||||
)}
|
||||
{snapshot === 'none' && (
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(248,248,246,0.8)', borderRadius: '0.5rem', minHeight: 80, fontSize: '0.72rem', color: '#cbd5e1' }}>No snapshot</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'rgba(248,248,246,0.8)', borderRadius: '0.5rem', minHeight: 80, fontSize: '0.72rem', color: '#cbc4b3' }}>No snapshot</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -1122,7 +1122,7 @@ function OrderRow({
|
||||
function SectionBox({ title, children }: { title: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ background: 'rgba(248,248,246,0.8)', borderRadius: '0.5rem', padding: '0.75rem 1rem' }}>
|
||||
<div style={{ fontSize: '0.6rem', fontWeight: 700, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: '0.5rem' }}>{title}</div>
|
||||
<div style={{ fontSize: '0.6rem', fontWeight: 700, color: '#94908a', textTransform: 'uppercase', letterSpacing: '0.06em', marginBottom: '0.5rem' }}>{title}</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@ -1138,7 +1138,7 @@ function InfoField({ label, value }: { label: string; value?: string | null }) {
|
||||
if (!value) return null;
|
||||
return (
|
||||
<div>
|
||||
<div style={{ fontSize: '0.63rem', fontWeight: 600, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.04em' }}>{label}</div>
|
||||
<div style={{ fontSize: '0.63rem', fontWeight: 600, color: '#94908a', textTransform: 'uppercase', letterSpacing: '0.04em' }}>{label}</div>
|
||||
<div style={{ fontSize: '0.8rem', color: '#374151', marginTop: '0.1rem', wordBreak: 'break-word' }}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
@ -1147,8 +1147,8 @@ function InfoField({ label, value }: { label: string; value?: string | null }) {
|
||||
function StatCard({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div style={{ background: 'rgba(255,255,255,0.95)', border: '1px solid rgba(0,0,0,0.06)', borderRadius: '0.75rem', padding: '1rem 1.25rem' }}>
|
||||
<div style={{ fontSize: '0.7rem', fontWeight: 600, color: '#94a3b8', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.25rem' }}>{label}</div>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: '#1a1a2e' }}>{value}</div>
|
||||
<div style={{ fontSize: '0.7rem', fontWeight: 600, color: '#94908a', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.25rem' }}>{label}</div>
|
||||
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: '#0a0907' }}>{value}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1165,7 +1165,7 @@ function TableHeader({ cols, labels }: { cols: string; labels: string[] }) {
|
||||
return (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: cols, padding: '0.6rem 1.25rem', borderBottom: '1px solid rgba(0,0,0,0.04)', background: 'rgba(248,248,246,0.5)' }}>
|
||||
{labels.map((l) => (
|
||||
<span key={l} style={{ fontSize: '0.65rem', fontWeight: 600, color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{l}</span>
|
||||
<span key={l} style={{ fontSize: '0.65rem', fontWeight: 600, color: '#6b6862', textTransform: 'uppercase', letterSpacing: '0.05em' }}>{l}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@ -1204,7 +1204,7 @@ const formInputStyle: React.CSSProperties = {
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
background: '#ffffff',
|
||||
color: '#1a1a2e',
|
||||
color: '#0a0907',
|
||||
fontSize: '0.8rem',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
@ -1216,7 +1216,7 @@ const tableInputStyle: React.CSSProperties = {
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid rgba(0,0,0,0.1)',
|
||||
background: '#ffffff',
|
||||
color: '#1a1a2e',
|
||||
color: '#0a0907',
|
||||
fontSize: '0.8rem',
|
||||
fontFamily: 'monospace',
|
||||
textAlign: 'right' as const,
|
||||
|
||||
56
src/app/book-demo/page.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { InquiryForm } from '@/components/robotics/InquiryForm';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Book a Robotics Demo in Dubai — YS Lootah Robotics',
|
||||
description:
|
||||
'Book a live robot demo at our Dubai showroom or at your venue. See selected Unitree and Pudu Robotics solutions in action — available exclusively in the UAE through YS Lootah Robotics.',
|
||||
};
|
||||
|
||||
export default function BookDemoPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
<main style={{ paddingTop: 'clamp(6rem, 10vw, 8rem)', paddingBottom: 'clamp(4rem, 8vw, 6rem)' }}>
|
||||
<div className="container-wide" style={{ display: 'grid', gap: 'clamp(2rem, 5vw, 3rem)', gridTemplateColumns: 'repeat(auto-fit, minmax(min(320px, 100%), 1fr))', alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<span className="eyebrow">Book a live demo · Dubai showroom</span>
|
||||
<h1 style={{ margin: 0, fontSize: 'clamp(2rem, 5vw, 3.2rem)', fontWeight: 300, lineHeight: 1.05, letterSpacing: '-0.03em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>See the future in person.</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
Book a live robot demo at our Dubai showroom or schedule an on-site demo at your venue. Our team will tailor the demo to your use case before you arrive.
|
||||
</p>
|
||||
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{[
|
||||
'Choose a humanoid, quadruped, or service robot to see live',
|
||||
'Designed around your industry — restaurant, mall, hotel, security, etc.',
|
||||
'Walk through configuration and deployment options',
|
||||
'Get UAE-specific pricing and availability',
|
||||
].map((b) => (
|
||||
<li key={b} style={{ display: 'flex', alignItems: 'flex-start', gap: '0.625rem', color: '#cbc4b3', lineHeight: 1.55 }}>
|
||||
<span style={{ width: 8, height: 8, marginTop: 8, borderRadius: 999, background: '#e0c896', flex: 'none' }} />
|
||||
{b}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: 'clamp(1.5rem, 4vw, 2.5rem)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginBottom: '1.5rem' }}>
|
||||
<span className="eyebrow">Demo request</span>
|
||||
<h2 style={{ margin: 0, fontSize: '1.4rem', fontWeight: 500 }}>Tell us when and where</h2>
|
||||
</div>
|
||||
<InquiryForm intent="demo" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
95
src/app/brands/page.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { BRANDS, ROBOTS, type RobotBrand } from '@/data/robots';
|
||||
import { RobotProductCard } from '@/components/robotics/RobotProductCard';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { DemoCTA } from '@/components/robotics/DemoCTA';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Robotics Brands — Unitree & Pudu | Exclusive UAE Access via YS Lootah Robotics',
|
||||
description:
|
||||
'Selected Unitree and Pudu Robotics solutions — available exclusively in the UAE through YS Lootah Robotics. Humanoid, quadruped, service, delivery, and cleaning robots.',
|
||||
};
|
||||
|
||||
const ORDER: RobotBrand[] = ['unitree', 'pudu'];
|
||||
|
||||
export default function BrandsPage() {
|
||||
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, 5vw, 5rem)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: 760 }}>
|
||||
<span className="eyebrow">Exclusive UAE Access · Dubai</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 }}>
|
||||
Selected Unitree and Pudu solutions — exclusively in the UAE.
|
||||
</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
YS Lootah Robotics holds exclusive UAE sales rights for selected Unitree and Pudu Robotics solutions — with on-the-ground sales, demo, and deployment support across Dubai and the UAE.
|
||||
</p>
|
||||
<p style={{ margin: 0, color: '#6b6862', fontSize: '0.82rem', lineHeight: 1.6 }}>
|
||||
Brand names and product trademarks are property of their respective owners. Available exclusively in the UAE through YS Lootah Robotics.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{ORDER.map((id) => {
|
||||
const brand = BRANDS[id];
|
||||
const robots = ROBOTS.filter((r) => r.brand === id);
|
||||
return (
|
||||
<MotionSection key={id} id={id}>
|
||||
<div
|
||||
className="card"
|
||||
style={{
|
||||
padding: 'clamp(1.5rem, 4vw, 2.5rem)',
|
||||
background:
|
||||
`radial-gradient(ellipse 80% 60% at 0% 0%, ${brand.accent}1A, transparent 60%), linear-gradient(180deg, rgba(15, 12, 8,0.65), rgba(5, 5, 5,0.95))`,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'baseline', gap: '1rem', marginBottom: '1.5rem' }}>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.6rem, 3.4vw, 2.4rem)', fontWeight: 600, letterSpacing: '-0.02em' }}>
|
||||
{brand.name.split(' ')[0]} <span style={{ color: brand.accent }}>{brand.name.split(' ').slice(1).join(' ')}</span>
|
||||
</h2>
|
||||
<span style={{ fontSize: '0.72rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: brand.accent }}>
|
||||
{robots.length} model{robots.length === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', lineHeight: 1.7, maxWidth: 800, marginBottom: '1.5rem' }}>
|
||||
{brand.description}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.625rem', marginBottom: '2rem' }}>
|
||||
<a className="btn btn-primary" href={`/robots/?brand=${id}`}>See all {brand.name.split(' ')[0]} robots</a>
|
||||
<a className="btn btn-ghost" href={brand.url} target="_blank" rel="noopener noreferrer">
|
||||
Visit brand site
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1.25rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{robots.map((r) => (
|
||||
<RobotProductCard key={r.id} robot={r} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</MotionSection>
|
||||
);
|
||||
})}
|
||||
|
||||
<DemoCTA
|
||||
title="Not sure which brand fits?"
|
||||
description="Tell us about your venue, timeline, and use case. We will recommend a brand and model — and book a live demo at our Dubai showroom."
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,20 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { ConfiguratorSection } from '@/components/ConfiguratorSection';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
|
||||
export default function ConfigurePage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
{/* Configurator section takes up full height minus navbar height roughly, or we just let it take its normal height */}
|
||||
<div style={{ minHeight: '100vh', paddingTop: '80px' }}>
|
||||
<ConfiguratorSection />
|
||||
</div>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
|
||||
import { ConfiguratorSection } from '@/components/ConfiguratorSection';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
|
||||
export default function ConfigurePage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
<header
|
||||
style={{
|
||||
position: 'relative',
|
||||
paddingTop: 'clamp(6rem, 10vw, 8rem)',
|
||||
paddingBottom: 'clamp(1.5rem, 3vw, 2.5rem)',
|
||||
paddingLeft: 'clamp(1rem, 4vw, 2rem)',
|
||||
paddingRight: 'clamp(1rem, 4vw, 2rem)',
|
||||
background:
|
||||
'radial-gradient(ellipse 60% 80% at 50% 0%, rgba(196, 162, 101,0.12), transparent 60%), linear-gradient(180deg, #050505 0%, #030303 100%)',
|
||||
borderBottom: '1px solid rgba(196, 162, 101,0.12)',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: 1320, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
<span className="eyebrow">Configurator · Unitree G1 humanoid · Dubai</span>
|
||||
<h1
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(1.6rem, 3.6vw, 2.4rem)',
|
||||
fontWeight: 300,
|
||||
letterSpacing: '-0.02em',
|
||||
lineHeight: 1.1,
|
||||
}}
|
||||
>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>Configure your robot.</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#94908a', fontSize: 'clamp(0.85rem, 1.6vw, 0.95rem)', lineHeight: 1.6, maxWidth: 720 }}>
|
||||
Choose persona, attire, colors, and accessories — visualize your Unitree G1 humanoid before you request a quotation from YS Lootah Robotics.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div style={{ minHeight: '100vh', background: '#ffffff' }}>
|
||||
<ConfiguratorSection />
|
||||
</div>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
72
src/app/contact/page.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { InquiryForm } from '@/components/robotics/InquiryForm';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Contact YS Lootah Robotics Dubai — Robotics Sales & Demo Inquiries',
|
||||
description:
|
||||
'Dubai robotics sales, support, and demo inquiries. Contact YS Lootah Robotics — the exclusive UAE destination for selected Unitree and Pudu Robotics solutions — by phone, email, or WhatsApp.',
|
||||
};
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
<main style={{ paddingTop: 'clamp(6rem, 10vw, 8rem)', paddingBottom: 'clamp(4rem, 8vw, 6rem)' }}>
|
||||
<div className="container-wide" style={{ display: 'grid', gap: 'clamp(2rem, 5vw, 3rem)', gridTemplateColumns: 'repeat(auto-fit, minmax(min(320px, 100%), 1fr))', alignItems: 'flex-start' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<span className="eyebrow">Contact · Dubai · UAE</span>
|
||||
<h1 style={{ margin: 0, fontSize: 'clamp(2rem, 5vw, 3.2rem)', fontWeight: 300, lineHeight: 1.05, letterSpacing: '-0.03em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>Talk to our Dubai robotics team.</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
Tell us about your venue, timeline, and use case. We will recommend a robot, share availability, and book a live demo at our Dubai showroom.
|
||||
</p>
|
||||
|
||||
<div className="card" style={{ padding: 'clamp(1.5rem, 4vw, 2rem)', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<ContactRow label="Phone" value="+971 55 948 2728" href="tel:+971559482728" />
|
||||
<ContactRow label="Office" value="+971 4 349 9319" href="tel:+97143499319" />
|
||||
<ContactRow label="Email" value="info@yslootahtech.com" href="mailto:info@yslootahtech.com" />
|
||||
<ContactRow label="WhatsApp" value="+971 55 948 2728" href="https://wa.me/971559482728" external />
|
||||
<ContactRow label="Address" value="Office 408, City Bay Business Center, Dubai, UAE" href="https://maps.google.com/?q=Office+408+City+Bay+Business+Center+Dubai" external />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ padding: 'clamp(1.5rem, 4vw, 2.5rem)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem', marginBottom: '1.5rem' }}>
|
||||
<span className="eyebrow">Inquiry form</span>
|
||||
<h2 style={{ margin: 0, fontSize: '1.4rem', fontWeight: 500 }}>Send us a message</h2>
|
||||
</div>
|
||||
<InquiryForm intent="inquiry" />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ContactRow({ label, value, href, external = false }: { label: string; value: string; href: string; external?: boolean }) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
{...(external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
padding: '0.75rem 0',
|
||||
borderBottom: '1px solid rgba(196, 162, 101,0.1)',
|
||||
textDecoration: 'none',
|
||||
color: '#f5f1e8',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '0.7rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#94908a' }}>{label}</span>
|
||||
<span style={{ fontSize: '0.95rem', color: '#f5f1e8', textAlign: 'right' }}>{value}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@ -1,24 +1,46 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
/* Light Mode Color Palette */
|
||||
--color-primary: #ffffff;
|
||||
--color-secondary: #f8f8f6;
|
||||
--color-accent: #3b82f6;
|
||||
--color-accent-hover: #2563eb;
|
||||
/* === Luxury robotics — Black + Gold === */
|
||||
--color-bg: #050505;
|
||||
--color-bg-2: #0a0a0c;
|
||||
--color-bg-3: #111114;
|
||||
--color-bg-4: #18181c;
|
||||
--color-surface: rgba(20, 18, 14, 0.7);
|
||||
--color-primary: #050505;
|
||||
--color-secondary: #0a0a0c;
|
||||
|
||||
/* Gold spectrum (matches YS Lootah brand) */
|
||||
--color-gold: #c4a265;
|
||||
--color-text-primary: #1a1a2e;
|
||||
--color-text-secondary: #64748b;
|
||||
--color-text-muted: #94a3b8;
|
||||
--color-border: #e2e8f0;
|
||||
--color-border-light: #cbd5e1;
|
||||
--color-gold-light: #e0c896;
|
||||
--color-gold-rich: #d4af6a;
|
||||
--color-gold-bronze: #8b6f47;
|
||||
--color-gold-deep: #6b5436;
|
||||
|
||||
/* Glassmorphism Colors - Light */
|
||||
--color-glass-bg: rgba(255, 255, 255, 0.85);
|
||||
--color-glass-border: rgba(0, 0, 0, 0.08);
|
||||
--color-glass-highlight: rgba(255, 255, 255, 0.5);
|
||||
/* Accent aliases */
|
||||
--color-accent: #c4a265;
|
||||
--color-accent-2: #d4af6a;
|
||||
--color-accent-3: #8b6f47;
|
||||
--color-accent-hover: #d4af6a;
|
||||
|
||||
/* Spacing & Sizing */
|
||||
/* Text */
|
||||
--color-text-primary: #f5f1e8;
|
||||
--color-text-secondary: #cbc4b3;
|
||||
--color-text-muted: #94908a;
|
||||
--color-text-dim: #6b6862;
|
||||
|
||||
/* Borders */
|
||||
--color-border: rgba(196, 162, 101, 0.18);
|
||||
--color-border-strong: rgba(196, 162, 101, 0.36);
|
||||
--color-border-light: rgba(196, 162, 101, 0.08);
|
||||
--color-border-neutral: rgba(245, 241, 232, 0.08);
|
||||
|
||||
/* Glass */
|
||||
--color-glass-bg: rgba(20, 18, 14, 0.55);
|
||||
--color-glass-border: rgba(196, 162, 101, 0.22);
|
||||
--color-glass-highlight: rgba(245, 241, 232, 0.05);
|
||||
|
||||
/* Spacing */
|
||||
--spacing-xs: 0.25rem;
|
||||
--spacing-sm: 0.5rem;
|
||||
--spacing-md: 1rem;
|
||||
@ -26,15 +48,17 @@
|
||||
--spacing-xl: 2rem;
|
||||
--spacing-2xl: 3rem;
|
||||
|
||||
/* Border Radius */
|
||||
/* Radius */
|
||||
--radius-sm: 0.375rem;
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
--radius-xl: 1rem;
|
||||
--radius-2xl: 1.5rem;
|
||||
|
||||
/* Shadows */
|
||||
--shadow-glow: 0 2px 20px rgba(0, 0, 0, 0.06);
|
||||
--shadow-glow-lg: 0 4px 40px rgba(0, 0, 0, 0.08);
|
||||
--shadow-glow: 0 0 30px rgba(196, 162, 101, 0.22);
|
||||
--shadow-glow-lg: 0 0 60px rgba(196, 162, 101, 0.28);
|
||||
--shadow-card: 0 14px 50px rgba(0, 0, 0, 0.55);
|
||||
|
||||
/* Transitions */
|
||||
--transition-fast: 150ms ease;
|
||||
@ -42,101 +66,165 @@
|
||||
--transition-slow: 300ms ease;
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100vh;
|
||||
background-color: var(--color-primary);
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text-primary);
|
||||
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(1200px 700px at 80% -10%, rgba(196, 162, 101, 0.10), transparent 60%),
|
||||
radial-gradient(900px 500px at -10% 30%, rgba(196, 162, 101, 0.06), transparent 60%),
|
||||
radial-gradient(800px 500px at 50% 110%, rgba(139, 111, 71, 0.10), transparent 60%),
|
||||
linear-gradient(180deg, #030303 0%, #050505 50%, #030303 100%);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* Scroll Snap */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
scroll-snap-type: y mandatory;
|
||||
}
|
||||
|
||||
/* Focus Styles for Accessibility */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline: 2px solid var(--color-gold);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Selection Styles */
|
||||
::selection {
|
||||
background-color: var(--color-accent);
|
||||
background-color: rgba(196, 162, 101, 0.4);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Glassmorphism Utilities - Light */
|
||||
/* === Glassmorphism === */
|
||||
.glass {
|
||||
background: var(--color-glass-bg);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
border: 1px solid var(--color-glass-border);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.glass-panel {
|
||||
background: var(--color-glass-bg);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid var(--color-glass-border);
|
||||
box-shadow: var(--shadow-glow);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.glass-panel-highlight {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--color-glass-highlight) 0%,
|
||||
transparent 50%
|
||||
);
|
||||
background: linear-gradient(135deg, var(--color-glass-highlight) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* Hero animated gradient - Light */
|
||||
/* === Typography gradients === */
|
||||
.text-gradient {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f5e9c8 40%, #d4af6a 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.text-gradient-accent {
|
||||
background: linear-gradient(135deg, #e0c896 0%, #c4a265 50%, #8b6f47 100%);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.text-gold {
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
/* Metallic gold border */
|
||||
.metallic-border {
|
||||
position: relative;
|
||||
}
|
||||
.metallic-border::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg, rgba(224, 200, 150, 0.6), rgba(196, 162, 101, 0.25) 45%, rgba(139, 111, 71, 0.0) 100%);
|
||||
-webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* === Hero animated gradient === */
|
||||
.hero-gradient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 80% 60% at 50% 40%, rgba(59, 130, 246, 0.04) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 50% at 30% 70%, rgba(196, 162, 101, 0.03) 0%, transparent 50%),
|
||||
linear-gradient(180deg, #f0f0ec 0%, #e8e8e4 50%, #f0f0ec 100%);
|
||||
animation: heroShift 12s ease-in-out infinite alternate;
|
||||
background:
|
||||
radial-gradient(ellipse 70% 60% at 60% 40%, rgba(196, 162, 101, 0.20) 0%, transparent 60%),
|
||||
radial-gradient(ellipse 60% 50% at 20% 70%, rgba(139, 111, 71, 0.18) 0%, transparent 55%),
|
||||
radial-gradient(ellipse 80% 70% at 80% 100%, rgba(224, 200, 150, 0.14) 0%, transparent 60%),
|
||||
linear-gradient(180deg, #050505 0%, #070605 50%, #030303 100%);
|
||||
animation: heroShift 16s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes heroShift {
|
||||
0% {
|
||||
background-position: 0% 0%, 0% 0%, 0% 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% 100%, 100% 0%, 0% 0%;
|
||||
opacity: 1;
|
||||
}
|
||||
0% { background-position: 0% 0%, 0% 0%, 0% 0%, 0% 0%; }
|
||||
100% { background-position: 100% 100%, 80% 0%, 0% 100%, 0% 0%; }
|
||||
}
|
||||
|
||||
/* Fade in up animation */
|
||||
.fade-in-up {
|
||||
animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||
/* Grid overlay */
|
||||
.grid-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(rgba(196, 162, 101, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(196, 162, 101, 0.04) 1px, transparent 1px);
|
||||
background-size: 48px 48px;
|
||||
mask-image: radial-gradient(ellipse 60% 50% at 50% 40%, #000 30%, transparent 80%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 60% 50% at 50% 40%, #000 30%, transparent 80%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* === Animations === */
|
||||
.fade-in-up { animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) both; }
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
from { opacity: 0; transform: translateY(24px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Scroll indicator - dark for light mode */
|
||||
.shimmer {
|
||||
background: linear-gradient(110deg, transparent 35%, rgba(224, 200, 150, 0.12) 50%, transparent 65%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 3.5s linear infinite;
|
||||
}
|
||||
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
|
||||
|
||||
.float-y { animation: floatY 6s ease-in-out infinite; }
|
||||
@keyframes floatY {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-12px); }
|
||||
}
|
||||
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
@keyframes pulseGlow {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(196, 162, 101, 0.55), 0 0 40px rgba(196, 162, 101, 0.25); }
|
||||
50% { box-shadow: 0 0 0 12px rgba(196, 162, 101, 0), 0 0 60px rgba(196, 162, 101, 0.45); }
|
||||
}
|
||||
|
||||
/* Scroll indicator */
|
||||
.scroll-indicator {
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.scroll-indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@ -144,58 +232,111 @@ html {
|
||||
left: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: linear-gradient(180deg, #c4a265, transparent);
|
||||
background: linear-gradient(180deg, var(--color-gold), transparent);
|
||||
animation: scrollPulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes scrollPulse {
|
||||
0%, 100% {
|
||||
transform: translateY(-100%);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
0%, 100% { transform: translateY(-100%); opacity: 0; }
|
||||
50% { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* Spin animation for loaders */
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
/* === Scrollbar === */
|
||||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||
::-webkit-scrollbar-track { background: var(--color-bg-2); }
|
||||
::-webkit-scrollbar-thumb { background: rgba(196, 162, 101, 0.25); border-radius: var(--radius-sm); }
|
||||
::-webkit-scrollbar-thumb:hover { background: rgba(196, 162, 101, 0.55); }
|
||||
|
||||
/* === Buttons === */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.875rem 1.75rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.25s cubic-bezier(0.16,1,0.3,1), box-shadow 0.25s ease, background 0.25s ease, color 0.25s ease;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn:hover { transform: translateY(-1px); }
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #e0c896 0%, #c4a265 55%, #8b6f47 100%);
|
||||
color: #0b0905;
|
||||
box-shadow: 0 8px 28px rgba(196, 162, 101, 0.35), inset 0 1px 0 rgba(255, 244, 220, 0.4);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
box-shadow: 0 12px 38px rgba(224, 200, 150, 0.55), inset 0 1px 0 rgba(255, 244, 220, 0.5);
|
||||
}
|
||||
|
||||
/* Scrollbar Styling - Light */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
.btn-ghost {
|
||||
background: rgba(245, 241, 232, 0.04);
|
||||
color: #f5f1e8;
|
||||
border-color: rgba(196, 162, 101, 0.28);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
background: rgba(196, 162, 101, 0.10);
|
||||
border-color: rgba(196, 162, 101, 0.55);
|
||||
color: #e0c896;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--color-secondary);
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
color: #e0c896;
|
||||
border-color: rgba(196, 162, 101, 0.55);
|
||||
}
|
||||
.btn-outline:hover {
|
||||
background: rgba(196, 162, 101, 0.10);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--color-border-light);
|
||||
border-radius: var(--radius-sm);
|
||||
/* === Layout helpers === */
|
||||
.container-wide { max-width: 1320px; margin: 0 auto; padding-left: clamp(1rem, 4vw, 2rem); padding-right: clamp(1rem, 4vw, 2rem); }
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.34em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
.eyebrow::before {
|
||||
content: '';
|
||||
width: 28px;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--color-gold));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-muted);
|
||||
/* === Cards === */
|
||||
.card {
|
||||
position: relative;
|
||||
background: rgba(10, 9, 7, 0.72);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-2xl);
|
||||
transition: transform 0.4s cubic-bezier(0.16,1,0.3,1), border-color 0.3s, box-shadow 0.3s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-6px);
|
||||
border-color: rgba(196, 162, 101, 0.55);
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.7), 0 0 40px rgba(196, 162, 101, 0.15);
|
||||
}
|
||||
|
||||
/* Snap children */
|
||||
.snap-section {
|
||||
scroll-snap-align: start;
|
||||
height: 100vh;
|
||||
}
|
||||
/* === Configurator (preserve previous theme rules) === */
|
||||
.snap-section { scroll-snap-align: start; }
|
||||
|
||||
/* Responsive Layout Styles */
|
||||
@media (max-width: 1024px) {
|
||||
.glass-panel-responsive {
|
||||
width: 360px !important;
|
||||
}
|
||||
.glass-panel-responsive { width: 360px !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@ -203,7 +344,6 @@ html {
|
||||
flex-direction: column-reverse !important;
|
||||
height: 100dvh !important;
|
||||
}
|
||||
|
||||
.glass-panel-responsive {
|
||||
order: unset !important;
|
||||
position: relative !important;
|
||||
@ -223,8 +363,6 @@ html {
|
||||
z-index: 50;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px) !important;
|
||||
}
|
||||
|
||||
/* Expanded (fullscreen) state overrides */
|
||||
.glass-panel-responsive.panel-expanded {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
@ -240,7 +378,6 @@ html {
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
height: 45dvh !important;
|
||||
max-height: 45dvh !important;
|
||||
@ -248,59 +385,32 @@ html {
|
||||
width: 100% !important;
|
||||
flex: none !important;
|
||||
}
|
||||
|
||||
#configurator {
|
||||
height: 100dvh !important;
|
||||
}
|
||||
|
||||
.mobile-handle {
|
||||
display: flex !important;
|
||||
}
|
||||
#configurator { height: 100dvh !important; }
|
||||
.mobile-handle { display: flex !important; }
|
||||
}
|
||||
|
||||
/* Small phones */
|
||||
@media (max-width: 480px) {
|
||||
.glass-panel-responsive {
|
||||
height: 58dvh !important;
|
||||
max-height: 58dvh !important;
|
||||
border-radius: 0.75rem 0.75rem 0 0 !important;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
height: 42dvh !important;
|
||||
max-height: 42dvh !important;
|
||||
min-height: 160px !important;
|
||||
}
|
||||
|
||||
.glass-panel-responsive header {
|
||||
padding: 0.75rem 1rem !important;
|
||||
}
|
||||
|
||||
.glass-panel-responsive > div[role="region"] {
|
||||
padding: 1rem !important;
|
||||
}
|
||||
.glass-panel-responsive header { padding: 0.75rem 1rem !important; }
|
||||
.glass-panel-responsive > div[role="region"] { padding: 1rem !important; }
|
||||
}
|
||||
|
||||
/* Very small phones (iPhone SE, etc.) */
|
||||
@media (max-width: 375px) {
|
||||
.glass-panel-responsive {
|
||||
height: 60dvh !important;
|
||||
max-height: 60dvh !important;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
height: 40dvh !important;
|
||||
max-height: 40dvh !important;
|
||||
min-height: 140px !important;
|
||||
}
|
||||
.glass-panel-responsive { height: 60dvh !important; max-height: 60dvh !important; }
|
||||
.canvas-area { height: 40dvh !important; max-height: 40dvh !important; min-height: 140px !important; }
|
||||
}
|
||||
|
||||
/* Landscape mobile */
|
||||
@media (max-height: 500px) and (orientation: landscape) {
|
||||
.layout-container {
|
||||
flex-direction: row !important;
|
||||
}
|
||||
|
||||
.layout-container { flex-direction: row !important; }
|
||||
.glass-panel-responsive {
|
||||
order: unset !important;
|
||||
position: relative !important;
|
||||
@ -313,35 +423,15 @@ html {
|
||||
border-left: 1px solid var(--color-border) !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.canvas-area {
|
||||
height: 100% !important;
|
||||
max-height: 100% !important;
|
||||
width: auto !important;
|
||||
flex: 1 !important;
|
||||
}
|
||||
|
||||
.mobile-handle {
|
||||
display: none !important;
|
||||
}
|
||||
.canvas-area { height: 100% !important; max-height: 100% !important; width: auto !important; flex: 1 !important; }
|
||||
.mobile-handle { display: none !important; }
|
||||
}
|
||||
|
||||
.mobile-handle {
|
||||
display: none;
|
||||
}
|
||||
.mobile-handle { display: none; }
|
||||
|
||||
/* Hide expand button on desktop */
|
||||
.panel-expand-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Show expand button only on mobile */
|
||||
.panel-expand-btn { display: none !important; }
|
||||
@media (max-width: 768px) {
|
||||
.panel-expand-btn {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Expanded (fullscreen) panel state – position handled by React inline styles */
|
||||
.panel-expand-btn { display: flex !important; }
|
||||
.layout-expanded .canvas-area {
|
||||
height: 0 !important;
|
||||
max-height: 0 !important;
|
||||
@ -351,193 +441,26 @@ html {
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.glass-panel-responsive.panel-expanded {
|
||||
height: 100dvh !important;
|
||||
max-height: 100dvh !important;
|
||||
}
|
||||
.glass-panel-responsive.panel-expanded { height: 100dvh !important; max-height: 100dvh !important; }
|
||||
}
|
||||
|
||||
@media (max-width: 375px) {
|
||||
.glass-panel-responsive.panel-expanded {
|
||||
height: 100dvh !important;
|
||||
max-height: 100dvh !important;
|
||||
}
|
||||
.glass-panel-responsive.panel-expanded { height: 100dvh !important; max-height: 100dvh !important; }
|
||||
}
|
||||
|
||||
/* ===== Scroll Overlay Responsive ===== */
|
||||
|
||||
/* Overlay glass panels (side sections) */
|
||||
.overlay-panel {
|
||||
max-width: 450px;
|
||||
padding: 2.5rem;
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
|
||||
/* Overlay section positioning */
|
||||
.overlay-section-left {
|
||||
left: clamp(2rem, 6vw, 6rem);
|
||||
}
|
||||
|
||||
.overlay-section-right {
|
||||
right: clamp(2rem, 6vw, 6rem);
|
||||
}
|
||||
|
||||
.overlay-heading {
|
||||
font-size: clamp(2rem, 3.5vw, 3rem);
|
||||
}
|
||||
|
||||
.overlay-hero-heading {
|
||||
font-size: clamp(2.5rem, 5vw, 4.5rem);
|
||||
}
|
||||
|
||||
.overlay-stat {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Tablet */
|
||||
@media (max-width: 1024px) {
|
||||
.overlay-panel {
|
||||
max-width: 380px;
|
||||
padding: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media (max-width: 768px) {
|
||||
.overlay-section-left,
|
||||
.overlay-section-right {
|
||||
left: 50% !important;
|
||||
right: auto !important;
|
||||
transform: translateX(-50%);
|
||||
width: 90vw;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.overlay-section-left {
|
||||
top: auto !important;
|
||||
bottom: 4vh !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.overlay-section-right {
|
||||
top: auto !important;
|
||||
bottom: 4vh !important;
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.overlay-panel {
|
||||
max-width: 100%;
|
||||
padding: 1.25rem;
|
||||
border-radius: 1rem;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.overlay-panel > div {
|
||||
align-items: center !important;
|
||||
}
|
||||
|
||||
.overlay-hero-heading {
|
||||
font-size: clamp(1.8rem, 8vw, 2.8rem);
|
||||
}
|
||||
|
||||
.overlay-heading {
|
||||
font-size: clamp(1.5rem, 6vw, 2rem);
|
||||
}
|
||||
|
||||
.overlay-stat {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.overlay-brand {
|
||||
top: 4vh !important;
|
||||
bottom: auto !important;
|
||||
left: 50% !important;
|
||||
transform: translateX(-50%);
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.overlay-brand span {
|
||||
font-size: 0.6rem !important;
|
||||
letter-spacing: 0.25em !important;
|
||||
}
|
||||
|
||||
.overlay-brand p {
|
||||
font-size: 0.75rem !important;
|
||||
}
|
||||
|
||||
.overlay-scroll-hint {
|
||||
bottom: 1.5rem !important;
|
||||
}
|
||||
|
||||
.overlay-cta-section {
|
||||
top: 12vh !important;
|
||||
bottom: auto !important;
|
||||
}
|
||||
|
||||
.overlay-cta-btn {
|
||||
padding: 1.2rem 3rem !important;
|
||||
font-size: 1.1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Hide overlay CTA on desktop since it takes over the screen */
|
||||
@media (min-width: 769px) {
|
||||
.overlay-cta-section {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Specific class to ensure configurator shows only on desktop */
|
||||
.desktop-configurator {
|
||||
display: block !important;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.desktop-configurator {
|
||||
display: none !important;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.001ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
.hero-gradient { animation: none !important; }
|
||||
}
|
||||
|
||||
/* Small phones */
|
||||
@media (max-width: 480px) {
|
||||
.overlay-section-left,
|
||||
.overlay-section-right {
|
||||
width: 92vw;
|
||||
bottom: 4vh !important;
|
||||
}
|
||||
|
||||
.overlay-panel {
|
||||
padding: 1.25rem;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.overlay-hero-heading {
|
||||
font-size: clamp(1.5rem, 7vw, 2.2rem);
|
||||
}
|
||||
|
||||
.overlay-heading {
|
||||
font-size: clamp(1.3rem, 5.5vw, 1.8rem);
|
||||
}
|
||||
|
||||
.overlay-stat {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small phones */
|
||||
@media (max-width: 375px) {
|
||||
.overlay-panel {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.overlay-hero-heading {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.overlay-heading {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
}
|
||||
html, body { overflow-x: hidden; max-width: 100vw; }
|
||||
|
||||
47
src/app/industries/page.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { IndustryUseCases } from '@/components/robotics/IndustryUseCases';
|
||||
import { DemoCTA } from '@/components/robotics/DemoCTA';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Robotics for UAE Industries — YS Lootah Robotics Dubai',
|
||||
description:
|
||||
'Robotics solutions for hospitality, restaurants, hotels, healthcare, education, security, warehouses, smart buildings, and government across the UAE.',
|
||||
};
|
||||
|
||||
export default function IndustriesPage() {
|
||||
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, 5vw, 5rem)' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: 760 }}>
|
||||
<span className="eyebrow">Industries served · 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 }}>
|
||||
Robotics solutions for UAE businesses.
|
||||
</span>
|
||||
</h1>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
We deploy humanoid, quadruped, and service robots across industries that are reshaping how the UAE operates — every venue is matched to the right robot.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<MotionSection>
|
||||
<IndustryUseCases />
|
||||
</MotionSection>
|
||||
|
||||
<DemoCTA
|
||||
title="Don't see your industry?"
|
||||
description="If your venue is unusual, complex, or one-of-a-kind — that's exactly when we like to talk. Let's design the robotics fit."
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -4,14 +4,48 @@ import { I18nProvider } from "@/components/I18nProvider";
|
||||
import "./globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Lootah Robotics | G1 Configurator",
|
||||
description: "3D Configurator for the G1 Robot by Lootah Robotics",
|
||||
metadataBase: new URL('https://yslootahrobotics.com'),
|
||||
title: {
|
||||
default: 'YS Lootah Robotics — Exclusive UAE Access to Unitree & Pudu Robotics',
|
||||
template: '%s | YS Lootah Robotics',
|
||||
},
|
||||
description:
|
||||
'YS Lootah Robotics is the exclusive UAE sales destination for selected Unitree and Pudu Robotics solutions. Explore, configure, book demos, and deploy advanced robots across Dubai and the UAE.',
|
||||
keywords: [
|
||||
'YS Lootah Robotics',
|
||||
'robotics Dubai',
|
||||
'robotics UAE',
|
||||
'exclusive robotics UAE',
|
||||
'Unitree robots UAE',
|
||||
'Pudu Robotics UAE',
|
||||
'humanoid robots Dubai',
|
||||
'quadruped robots UAE',
|
||||
'service robots UAE',
|
||||
'robot sales Dubai',
|
||||
'book robot demo UAE',
|
||||
'robotics showroom Dubai',
|
||||
'robotics configuration UAE',
|
||||
],
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
title: 'YS Lootah Robotics — Exclusive UAE Access to Unitree & Pudu Robotics',
|
||||
description:
|
||||
'Advanced robotics. Exclusive UAE access. Selected Unitree and Pudu solutions available through YS Lootah Robotics in Dubai.',
|
||||
locale: 'en_AE',
|
||||
siteName: 'YS Lootah Robotics',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'YS Lootah Robotics — Exclusive UAE Access to Unitree & Pudu Robotics',
|
||||
description:
|
||||
'The UAE’s dedicated destination for Unitree and Pudu Robotics sales, demo inquiries, and business deployment.',
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
width: "device-width",
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
themeColor: "#ffffff",
|
||||
themeColor: '#050505',
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@ -20,7 +54,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" dir="ltr">
|
||||
<html lang="en" dir="ltr" data-scroll-behavior="smooth">
|
||||
<head>
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
184
src/app/page.tsx
@ -1,41 +1,167 @@
|
||||
"use client";
|
||||
import type { Metadata } from 'next';
|
||||
import { Navbar } from '@/components/Navbar';
|
||||
import { FooterAndContact } from '@/components/FooterAndContact';
|
||||
import { Hero3DRobotics } from '@/components/robotics/Hero3DRobotics';
|
||||
import { BrandShowcase } from '@/components/robotics/BrandShowcase';
|
||||
import { RobotCategoryGrid } from '@/components/robotics/RobotCategoryGrid';
|
||||
import { RobotProductCard } from '@/components/robotics/RobotProductCard';
|
||||
import { IndustryUseCases } from '@/components/robotics/IndustryUseCases';
|
||||
import { DemoCTA } from '@/components/robotics/DemoCTA';
|
||||
import { ConfigureCTA } from '@/components/robotics/ConfigureCTA';
|
||||
import { ExclusiveAccessSection } from '@/components/robotics/ExclusiveAccessSection';
|
||||
import { MarqueeStrip } from '@/components/robotics/MarqueeStrip';
|
||||
import { BentoGrid } from '@/components/robotics/BentoGrid';
|
||||
import { HowItWorks } from '@/components/robotics/HowItWorks';
|
||||
import { MotionSection } from '@/components/ui/MotionSection';
|
||||
import { FEATURED_ROBOTS } from '@/data/robots';
|
||||
|
||||
import { useRef } from "react";
|
||||
import { ClientOnly } from "@/components/ClientOnly";
|
||||
import { ScrollScene } from "@/components/ScrollScene";
|
||||
import { ScrollOverlays } from "@/components/ScrollOverlays";
|
||||
import { FooterAndContact } from "@/components/FooterAndContact";
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
export const metadata: Metadata = {
|
||||
title: 'YS Lootah Robotics — Exclusive UAE Access to Unitree & Pudu Robotics',
|
||||
description:
|
||||
'YS Lootah Robotics is the exclusive UAE sales destination for selected Unitree and Pudu Robotics solutions. Explore, configure, book demos, and deploy advanced robots across Dubai and the UAE.',
|
||||
keywords: [
|
||||
'YS Lootah Robotics',
|
||||
'robotics Dubai',
|
||||
'robotics UAE',
|
||||
'exclusive robotics UAE',
|
||||
'Unitree robots UAE',
|
||||
'Pudu Robotics UAE',
|
||||
'humanoid robots Dubai',
|
||||
'quadruped robots UAE',
|
||||
'service robots UAE',
|
||||
'robotics showroom Dubai',
|
||||
'robotics configuration UAE',
|
||||
'robot sales and demo UAE',
|
||||
],
|
||||
};
|
||||
|
||||
export default function HomePage() {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar />
|
||||
|
||||
{/* Fixed 3D scene behind everything */}
|
||||
<ClientOnly>
|
||||
<ScrollScene scrollContainerRef={scrollContainerRef} />
|
||||
</ClientOnly>
|
||||
|
||||
{/* Text overlays that fade based on scroll */}
|
||||
<ScrollOverlays />
|
||||
<main>
|
||||
<Hero3DRobotics />
|
||||
|
||||
{/* Scroll spacer with snap sections -- 7 sections for the landing */}
|
||||
<div ref={scrollContainerRef} style={{ position: "relative", zIndex: 1, pointerEvents: "none" }}>
|
||||
<div className="snap-section" />
|
||||
<div className="snap-section" />
|
||||
<div className="snap-section" />
|
||||
<div className="snap-section" />
|
||||
<div className="snap-section" />
|
||||
<div className="snap-section" />
|
||||
<div className="snap-section" />
|
||||
</div>
|
||||
<MarqueeStrip />
|
||||
|
||||
<div className="snap-section" style={{ scrollSnapAlign: 'start' }}>
|
||||
<FooterAndContact />
|
||||
</div>
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="exclusive">
|
||||
<div className="container-wide">
|
||||
<ExclusiveAccessSection />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="brands">
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<SectionHeading
|
||||
eyebrow="Featured robotics brands"
|
||||
title="Unitree & Pudu — available exclusively in the UAE."
|
||||
description="We hold exclusive UAE sales rights for selected Unitree and Pudu Robotics solutions — curated for proven performance in the field and full local support across Dubai."
|
||||
/>
|
||||
<BrandShowcase />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="why">
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<SectionHeading
|
||||
eyebrow="Why YS Lootah Robotics"
|
||||
title="A premium robotics partner — not a marketplace."
|
||||
description="From inquiry to live deployment, we manage your robotics program end to end with a UAE-based team and exclusive access to Unitree and Pudu portfolios."
|
||||
/>
|
||||
<BentoGrid />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="categories">
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<SectionHeading
|
||||
eyebrow="Robot categories"
|
||||
title="From quadruped patrol to humanoid concierge."
|
||||
description="Find the right robot for your business — humanoid for events and education, quadruped for inspection and security, service and delivery for hospitality."
|
||||
/>
|
||||
<RobotCategoryGrid />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="featured">
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<SectionHeading
|
||||
eyebrow="Featured robots"
|
||||
title="Available exclusively in the UAE through YS Lootah Robotics."
|
||||
description="A live snapshot of robots ready to demo at our Dubai showroom or deploy at your venue. Request a UAE quotation or book a live demo."
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1.25rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{FEATURED_ROBOTS.map((r, idx) => (
|
||||
<RobotProductCard key={r.id} robot={r} priority={idx === 0} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(2rem, 5vw, 3.5rem) 0' }}>
|
||||
<div className="container-wide">
|
||||
<ConfigureCTA />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="how-it-works">
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<SectionHeading
|
||||
eyebrow="How we work"
|
||||
title="From inquiry to live deployment."
|
||||
description="Three steps from your first message to a robot running in your venue — fully supported by our Dubai team."
|
||||
/>
|
||||
<HowItWorks />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0' }} id="industries">
|
||||
<div className="container-wide" style={{ display: 'flex', flexDirection: 'column', gap: '2rem' }}>
|
||||
<SectionHeading
|
||||
eyebrow="Industries served"
|
||||
title="Robotics solutions for UAE businesses."
|
||||
description="From restaurants to security operators to government innovation programs — we deliver robotics fits for the venue."
|
||||
/>
|
||||
<IndustryUseCases limit={8} />
|
||||
</div>
|
||||
</MotionSection>
|
||||
|
||||
<MotionSection style={{ padding: 'clamp(3rem, 6vw, 5rem) 0 clamp(4rem, 8vw, 7rem)' }}>
|
||||
<div className="container-wide">
|
||||
<DemoCTA
|
||||
title="Talk to our robotics team."
|
||||
description="Book a live demo at our Dubai showroom or request a UAE quotation. Our local team responds within one business day."
|
||||
primaryLabel="Book a live demo"
|
||||
secondaryLabel="Talk to our team"
|
||||
/>
|
||||
</div>
|
||||
</MotionSection>
|
||||
</main>
|
||||
|
||||
<FooterAndContact />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeading({ eyebrow, title, description }: { eyebrow: string; title: string; description?: string }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', maxWidth: 760 }}>
|
||||
<span className="eyebrow">{eyebrow}</span>
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.8rem, 4.5vw, 3rem)', lineHeight: 1.1, fontWeight: 300, letterSpacing: '-0.03em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>{title}</span>
|
||||
</h2>
|
||||
{description && (
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,32 +9,32 @@ export default function PrivacyPolicyPage() {
|
||||
<div style={{ background: '#050508', minHeight: '100vh', color: '#ffffff', fontFamily: 'Inter, sans-serif' }}>
|
||||
<main style={{ maxWidth: '800px', margin: '0 auto', padding: '12rem 1.5rem 6rem', lineHeight: 1.8 }}>
|
||||
<h1 style={{ fontSize: '3rem', fontWeight: 200, marginBottom: '1rem', letterSpacing: '-0.03em' }}>Privacy <span style={{ color: 'var(--color-gold)', fontWeight: 500 }}>Policy</span></h1>
|
||||
<p style={{ color: '#94a3b8', fontSize: '1rem', marginBottom: '4rem' }}>Effective Date: {new Date().toLocaleDateString('en-AE')}</p>
|
||||
<p style={{ color: '#94908a', fontSize: '1rem', marginBottom: '4rem' }}>Effective Date: {new Date().toLocaleDateString('en-AE')}</p>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>1. Information We Collect</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>1. Information We Collect</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
At YS Lootah Robotics, we collect information you provide directly to us when you request information, use the G1 Customizer, or contact us. This includes your name, email address, phone number, and any other information you choose to provide in your message.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>2. How We Use Your Information</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>2. How We Use Your Information</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
We use the information we collect to respond to your inquiries, deliver our robotics enterprise solutions, maintain our dashboard, and communicate with you about your custom humanoid configurations.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>3. Data Security</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>3. Data Security</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
We implement robust security measures designed to protect your personal information. Your contact data is stored securely in our private databases strictly for administrative and operational purposes.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>4. Contact Us</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>4. Contact Us</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
If you have questions or concerns about this Privacy Policy, please reach out to us at:
|
||||
<br/><br/>
|
||||
<strong>YS Lootah Robotics</strong><br/>
|
||||
|
||||
125
src/app/robots/CatalogClient.tsx
Normal 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
src/app/robots/[slug]/page.tsx
Normal 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
src/app/robots/page.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,32 +1,29 @@
|
||||
import { MetadataRoute } from 'next';
|
||||
import { ROBOTS } from '@/data/robots';
|
||||
|
||||
const baseUrl = 'https://yslootahrobotics.com';
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = 'https://lootahrobotics.com'; // Adjust to your actual production domain
|
||||
|
||||
return [
|
||||
{
|
||||
url: `${baseUrl}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/configure`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/privacy-policy`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly',
|
||||
priority: 0.5,
|
||||
},
|
||||
{
|
||||
url: `${baseUrl}/terms-of-service`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly',
|
||||
priority: 0.5,
|
||||
},
|
||||
const now = new Date();
|
||||
const staticRoutes: MetadataRoute.Sitemap = [
|
||||
{ url: `${baseUrl}/`, lastModified: now, changeFrequency: 'weekly', priority: 1 },
|
||||
{ url: `${baseUrl}/robots/`, lastModified: now, changeFrequency: 'weekly', priority: 0.9 },
|
||||
{ url: `${baseUrl}/brands/`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 },
|
||||
{ url: `${baseUrl}/industries/`, lastModified: now, changeFrequency: 'weekly', priority: 0.8 },
|
||||
{ url: `${baseUrl}/about/`, lastModified: now, changeFrequency: 'monthly', priority: 0.6 },
|
||||
{ url: `${baseUrl}/contact/`, lastModified: now, changeFrequency: 'monthly', priority: 0.7 },
|
||||
{ url: `${baseUrl}/book-demo/`, lastModified: now, changeFrequency: 'monthly', priority: 0.8 },
|
||||
{ url: `${baseUrl}/configure/`, lastModified: now, changeFrequency: 'monthly', priority: 0.8 },
|
||||
{ url: `${baseUrl}/privacy-policy/`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
|
||||
{ url: `${baseUrl}/terms-of-service/`, lastModified: now, changeFrequency: 'yearly', priority: 0.3 },
|
||||
];
|
||||
|
||||
const robotRoutes: MetadataRoute.Sitemap = ROBOTS.map((r) => ({
|
||||
url: `${baseUrl}/robots/${r.slug}/`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
}));
|
||||
|
||||
return [...staticRoutes, ...robotRoutes];
|
||||
}
|
||||
|
||||
@ -9,32 +9,32 @@ export default function TermsOfServicePage() {
|
||||
<div style={{ background: '#050508', minHeight: '100vh', color: '#ffffff', fontFamily: 'Inter, sans-serif' }}>
|
||||
<main style={{ maxWidth: '800px', margin: '0 auto', padding: '12rem 1.5rem 6rem', lineHeight: 1.8 }}>
|
||||
<h1 style={{ fontSize: '3rem', fontWeight: 200, marginBottom: '1rem', letterSpacing: '-0.03em' }}>Terms of <span style={{ color: 'var(--color-gold)', fontWeight: 500 }}>Service</span></h1>
|
||||
<p style={{ color: '#94a3b8', fontSize: '1rem', marginBottom: '4rem' }}>Effective Date: {new Date().toLocaleDateString('en-AE')}</p>
|
||||
<p style={{ color: '#94908a', fontSize: '1rem', marginBottom: '4rem' }}>Effective Date: {new Date().toLocaleDateString('en-AE')}</p>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>1. Acceptance of Terms</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>1. Acceptance of Terms</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
By accessing and utilizing the YS Lootah Robotics web platform and the G1 Configurator, you accept and agree to be bound by the terms and provisions of this agreement.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>2. Use of the Site & Configurator</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>2. Use of the Site & Configurator</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
The 3D G1 Configurator is provided for informational and demonstrative purposes to showcase the capabilities of YS Lootah technologies. You agree to use this site strictly for lawful purposes resulting in enterprise robotics inquiries and configurations.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>3. Intellectual Property Rights</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>3. Intellectual Property Rights</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
All original content on this website, including but not limited to text, graphics, 3D models (GLB files), logos, and software, is the exclusive property of YS Lootah Robotics and is protected by United Arab Emirates and international copyright laws.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section style={{ marginBottom: '3rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e2e8f0', marginBottom: '1rem' }}>4. Disclaimer of Warranties</h2>
|
||||
<p style={{ color: '#cbd5e1', marginBottom: '1rem' }}>
|
||||
<h2 style={{ fontSize: '1.5rem', fontWeight: 500, color: '#e8e0cf', marginBottom: '1rem' }}>4. Disclaimer of Warranties</h2>
|
||||
<p style={{ color: '#cbc4b3', marginBottom: '1rem' }}>
|
||||
The materials on our platform are provided "as is". We make no warranties, expressed or implied, and hereby disclaim to the fullest extent permitted by law all warranties regarding the immediate enterprise availability of the rendered concepts displayed in the Configurator.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@ -103,7 +103,7 @@ export function CheckoutOverlay() {
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid rgba(0, 0, 0, 0.08)',
|
||||
background: 'transparent',
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.75rem',
|
||||
transition: 'all 0.2s ease',
|
||||
@ -111,7 +111,7 @@ export function CheckoutOverlay() {
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<h2 style={{ fontSize: '0.9rem', fontWeight: 600, color: '#1a1a2e', margin: 0 }}>
|
||||
<h2 style={{ fontSize: '0.9rem', fontWeight: 600, color: '#0a0907', margin: 0 }}>
|
||||
Checkout
|
||||
</h2>
|
||||
<button
|
||||
@ -122,7 +122,7 @@ export function CheckoutOverlay() {
|
||||
borderRadius: '50%',
|
||||
border: '1px solid rgba(0, 0, 0, 0.08)',
|
||||
background: 'transparent',
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
display: 'flex',
|
||||
@ -155,7 +155,7 @@ export function CheckoutOverlay() {
|
||||
}} />
|
||||
<span style={{
|
||||
fontSize: '0.65rem',
|
||||
color: isActive ? '#2563eb' : isComplete ? '#3b82f6' : '#64748b',
|
||||
color: isActive ? '#2563eb' : isComplete ? '#3b82f6' : '#6b6862',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
@ -216,7 +216,7 @@ export function CheckoutOverlay() {
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<span style={{ color: '#94a3b8' }}>Initializing secure payment...</span>
|
||||
<span style={{ color: '#94908a' }}>Initializing secure payment...</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -141,14 +141,14 @@ export function ConfigPanel() {
|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: isActive ? 600 : 400,
|
||||
color: isActive ? '#374151' : '#94a3b8',
|
||||
color: isActive ? '#374151' : '#94908a',
|
||||
marginBottom: '2px',
|
||||
}}>
|
||||
{persona.label}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.65rem',
|
||||
color: '#64748b',
|
||||
color: '#6b6862',
|
||||
lineHeight: 1.3,
|
||||
}}>
|
||||
{persona.description}
|
||||
@ -221,14 +221,14 @@ export function ConfigPanel() {
|
||||
<div style={{
|
||||
fontSize: '0.8rem',
|
||||
fontWeight: isActive ? 600 : 500,
|
||||
color: isActive ? '#374151' : '#64748b',
|
||||
color: isActive ? '#374151' : '#6b6862',
|
||||
marginBottom: '2px',
|
||||
}}>
|
||||
{opt.label}
|
||||
</div>
|
||||
<div style={{
|
||||
fontSize: '0.65rem',
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
lineHeight: 1.3,
|
||||
}}>
|
||||
{opt.description}
|
||||
@ -282,7 +282,7 @@ function ColorInput({ label, value, onChange }: { label: string; value: string;
|
||||
/>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: '0.8rem', color: '#374151', marginBottom: '2px' }}>{label}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#64748b', fontFamily: 'monospace' }}>{value}</div>
|
||||
<div style={{ fontSize: '0.7rem', color: '#6b6862', fontFamily: 'monospace' }}>{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -291,7 +291,7 @@ function ColorInput({ label, value, onChange }: { label: string; value: string;
|
||||
const sectionTitleStyle: React.CSSProperties = {
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
margin: 0,
|
||||
|
||||
@ -41,7 +41,7 @@ export function ConfiguratorSection() {
|
||||
background: 'rgba(255, 255, 255, 0.9)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
borderRight: '1px solid #e2e8f0',
|
||||
borderRight: '1px solid #e8e0cf',
|
||||
boxShadow: '4px 0 30px rgba(0, 0, 0, 0.04)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@ -74,7 +74,7 @@ export function ConfiguratorSection() {
|
||||
style={{
|
||||
width: '40px',
|
||||
height: '4px',
|
||||
backgroundColor: '#e2e8f0',
|
||||
backgroundColor: '#e8e0cf',
|
||||
borderRadius: '2px',
|
||||
transition: 'width 0.2s, background-color 0.2s',
|
||||
}}
|
||||
@ -85,7 +85,7 @@ export function ConfiguratorSection() {
|
||||
<header
|
||||
style={{
|
||||
padding: '1.25rem 1.5rem',
|
||||
borderBottom: '1px solid #e2e8f0',
|
||||
borderBottom: '1px solid #e8e0cf',
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
}}
|
||||
@ -94,7 +94,7 @@ export function ConfiguratorSection() {
|
||||
style={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 600,
|
||||
color: '#1a1a2e',
|
||||
color: '#0a0907',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
@ -150,13 +150,13 @@ export function ConfiguratorSection() {
|
||||
style={{
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
border: '3px solid #e2e8f0',
|
||||
border: '3px solid #e8e0cf',
|
||||
borderTopColor: '#3b82f6',
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
}}
|
||||
/>
|
||||
<p style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#94a3b8' }}>
|
||||
<p style={{ marginTop: '1rem', fontSize: '0.875rem', color: '#94908a' }}>
|
||||
{t('loading.configuration')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -1,378 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import PhoneInput from 'react-country-phone-input';
|
||||
import 'react-country-phone-input/lib/style.css';
|
||||
import { BRANDS } from '@/data/robots';
|
||||
|
||||
const CONTACT = {
|
||||
phonePrimary: '+971 55 948 2728',
|
||||
phoneSecondary: '+971 4 349 9319',
|
||||
email: 'info@yslootahtech.com',
|
||||
address: 'Office 408, City Bay Business Center, Dubai, UAE',
|
||||
whatsapp: 'https://wa.me/971559482728',
|
||||
maps: 'https://maps.google.com/?q=Office+408+City+Bay+Business+Center+Dubai',
|
||||
instagram: 'https://www.instagram.com/yslootahtech',
|
||||
linkedin: 'https://www.linkedin.com/company/ys-lootah-tech',
|
||||
facebook: 'https://www.facebook.com/yslootahtech',
|
||||
};
|
||||
|
||||
export function FooterAndContact() {
|
||||
const [formData, setFormData] = useState({ name: '', email: '', phone: '', message: '' });
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
});
|
||||
if (res.ok) {
|
||||
setStatus('success');
|
||||
setFormData({ name: '', email: '', phone: '', message: '' });
|
||||
setTimeout(() => setStatus('idle'), 4000);
|
||||
} else {
|
||||
setStatus('error');
|
||||
setTimeout(() => setStatus('idle'), 4000);
|
||||
}
|
||||
} catch {
|
||||
setStatus('error');
|
||||
setTimeout(() => setStatus('idle'), 4000);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', zIndex: 10, background: '#0a0a0f', color: '#ffffff', fontFamily: 'Inter, sans-serif' }}>
|
||||
|
||||
{/* Premium Desktop CTA section */}
|
||||
<div style={{
|
||||
padding: 'clamp(4rem, 8vw, 8rem) clamp(1.5rem, 5vw, 3rem)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
background: 'linear-gradient(180deg, #11111a 0%, #0a0a0f 100%)',
|
||||
<footer
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Subtle background glow */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 'clamp(300px, 80vw, 600px)',
|
||||
height: 'clamp(200px, 50vw, 400px)',
|
||||
background: 'radial-gradient(circle, rgba(196,162,101,0.08) 0%, rgba(0,0,0,0) 70%)',
|
||||
pointerEvents: 'none'
|
||||
}} />
|
||||
|
||||
<h2 style={{
|
||||
fontSize: 'clamp(2rem, 6vw, 3.5rem)',
|
||||
fontWeight: 200,
|
||||
color: '#ffffff',
|
||||
marginBottom: '1.5rem',
|
||||
letterSpacing: '-0.04em',
|
||||
position: 'relative'
|
||||
}}>
|
||||
Ready to Build Your <span style={{ color: 'var(--color-gold)', fontWeight: 500 }}>G1</span>?
|
||||
</h2>
|
||||
<p style={{
|
||||
fontSize: 'clamp(1rem, 2.5vw, 1.15rem)',
|
||||
color: '#94a3b8',
|
||||
maxWidth: '600px',
|
||||
textAlign: 'center',
|
||||
lineHeight: 1.7,
|
||||
marginBottom: '3.5rem',
|
||||
fontWeight: 300,
|
||||
position: 'relative'
|
||||
}}>
|
||||
Customize every detail. From intelligent locomotion to identity and purpose. Your enterprise-grade humanoid is just a few clicks away.
|
||||
</p>
|
||||
<Link
|
||||
href="/configure/"
|
||||
zIndex: 10,
|
||||
marginTop: 'clamp(4rem, 8vw, 6rem)',
|
||||
borderTop: '1px solid rgba(196, 162, 101,0.12)',
|
||||
background:
|
||||
'radial-gradient(ellipse 80% 60% at 50% 0%, rgba(196, 162, 101,0.05), transparent 60%), linear-gradient(180deg, #030303, #02040a)',
|
||||
color: '#cbc4b3',
|
||||
}}
|
||||
>
|
||||
<div className="container-wide" style={{ paddingTop: 'clamp(3rem, 6vw, 5rem)', paddingBottom: '2rem' }}>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
padding: 'clamp(0.8rem, 2vw, 1.25rem) clamp(1.5rem, 5vw, 3.5rem)',
|
||||
borderRadius: '4rem',
|
||||
background: 'transparent',
|
||||
color: 'var(--color-gold)',
|
||||
border: '1px solid var(--color-gold)',
|
||||
fontSize: 'clamp(0.85rem, 2vw, 1.1rem)',
|
||||
fontWeight: 500,
|
||||
letterSpacing: '0.1em',
|
||||
textDecoration: 'none',
|
||||
textTransform: 'uppercase',
|
||||
transition: 'all 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
e.currentTarget.style.background = 'var(--color-gold)';
|
||||
e.currentTarget.style.color = '#ffffff';
|
||||
e.currentTarget.style.boxShadow = '0 0 30px rgba(196, 162, 101, 0.4)';
|
||||
e.currentTarget.style.transform = 'translateY(-2px)';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.currentTarget.style.background = 'transparent';
|
||||
e.currentTarget.style.color = 'var(--color-gold)';
|
||||
e.currentTarget.style.boxShadow = 'none';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
display: 'grid',
|
||||
gap: 'clamp(2rem, 4vw, 3.5rem)',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(220px, 100%), 1fr))',
|
||||
paddingBottom: '2.5rem',
|
||||
}}
|
||||
>
|
||||
Configure Your G1
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Contact Section */}
|
||||
<section style={{ padding: 'clamp(4rem, 8vw, 6rem) 1.5rem', maxWidth: '1280px', margin: '0 auto', borderTop: '1px solid rgba(255,255,255,0.05)' }} id="contact">
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'clamp(2rem, 5vw, 4rem)', justifyContent: 'space-between' }}>
|
||||
|
||||
{/* Contact Info */}
|
||||
<div style={{ flex: '1 1 min(400px, 100%)' }}>
|
||||
<div style={{ width: '40px', height: '1px', background: 'var(--color-gold)', marginBottom: '2rem' }} />
|
||||
<h3 style={{ fontSize: '0.8rem', fontWeight: 500, color: 'var(--color-gold)', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
||||
Connect With Us
|
||||
</h3>
|
||||
<h2 style={{ fontSize: 'clamp(2rem, 5vw, 2.8rem)', fontWeight: 200, color: '#ffffff', lineHeight: 1.15, marginBottom: '2rem', letterSpacing: '-0.03em' }}>
|
||||
Start your<br /><span style={{ fontWeight: 500 }}>Robotics Journey</span>
|
||||
</h2>
|
||||
<p style={{ fontSize: '1.05rem', color: '#94a3b8', lineHeight: 1.7, marginBottom: '4rem', fontWeight: 300 }}>
|
||||
Whether you are looking to integrate the G1 into your enterprise workflows or have questions about custom developments, our team in the UAE is here to help.
|
||||
</p>
|
||||
|
||||
<div style={{ padding: 'clamp(1.5rem, 4vw, 2rem)', borderRadius: '1rem', border: '1px solid rgba(255,255,255,0.05)', background: '#11111a', marginTop: '2rem' }}>
|
||||
<h3 className="text-xl font-semibold text-white mb-6" style={{ letterSpacing: '0.05em' }}>Contact Information</h3>
|
||||
<div className="space-y-6">
|
||||
<a className="group flex items-start gap-4" href="tel:+971 55 948 2728" target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[#c4a265]/10 text-[#c4a265] transition-all duration-300 group-hover:bg-[#c4a265]/20 group-hover:scale-110">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-phone h-5 w-5" aria-hidden="true"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"></path></svg>
|
||||
</div>
|
||||
<div><div className="text-sm text-slate-400">Phone</div><div className="mt-1 font-medium text-white">+971 55 948 2728</div></div>
|
||||
</a>
|
||||
<a className="group flex items-start gap-4" href="tel:+971 4 349 9319" target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[#c4a265]/10 text-[#c4a265] transition-all duration-300 group-hover:bg-[#c4a265]/20 group-hover:scale-110">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-phone h-5 w-5" aria-hidden="true"><path d="M13.832 16.568a1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 6.392 6.384"></path></svg>
|
||||
</div>
|
||||
<div><div className="text-sm text-slate-400">Phone</div><div className="mt-1 font-medium text-white">+971 4 349 9319</div></div>
|
||||
</a>
|
||||
<a className="group flex items-start gap-4" href="mailto:info@yslootahtech.com" target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[#c4a265]/10 text-[#c4a265] transition-all duration-300 group-hover:bg-[#c4a265]/20 group-hover:scale-110">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-mail h-5 w-5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>
|
||||
</div>
|
||||
<div><div className="text-sm text-slate-400">Email</div><div className="mt-1 font-medium text-white">info@yslootahtech.com</div></div>
|
||||
</a>
|
||||
<a className="group flex items-start gap-4" href="https://maps.google.com/?q=Office+408+City+Bay+Business+Center+Dubai" target="_blank" rel="noopener noreferrer">
|
||||
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-[#c4a265]/10 text-[#c4a265] transition-all duration-300 group-hover:bg-[#c4a265]/20 group-hover:scale-110">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-map-pin h-5 w-5" aria-hidden="true"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"></path><circle cx="12" cy="10" r="3"></circle></svg>
|
||||
</div>
|
||||
<div><div className="text-sm text-slate-400">Address</div><div className="mt-1 font-medium text-white">Office 408, City Bay Business Center<br/>Dubai, UAE</div></div>
|
||||
</a>
|
||||
</div>
|
||||
<div className="mt-8 pt-6 border-t border-[rgba(255,255,255,0.05)]">
|
||||
<div className="text-sm text-slate-400 mb-4" style={{ letterSpacing: '0.05em' }}>Follow Us</div>
|
||||
<div className="flex gap-3">
|
||||
<a href="https://www.instagram.com/yslootahtech" target="_blank" rel="noopener noreferrer" className="group flex h-11 w-11 items-center justify-center rounded-xl border border-[rgba(255,255,255,0.05)] bg-[#11111a] transition-all duration-300 hover:border-[#c4a265]/50 hover:bg-[#c4a265]/10 group-hover:text-[#c4a265]" aria-label="Instagram"><svg className="h-5 w-5 text-slate-400 transition-colors group-hover:text-[#c4a265]" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"></path></svg></a>
|
||||
<a href="https://www.linkedin.com/company/ys-lootah-tech" target="_blank" rel="noopener noreferrer" className="group flex h-11 w-11 items-center justify-center rounded-xl border border-[rgba(255,255,255,0.05)] bg-[#11111a] transition-all duration-300 hover:border-[#c4a265]/50 hover:bg-[#c4a265]/10 group-hover:text-[#c4a265]" aria-label="Linkedin"><svg className="h-5 w-5 text-slate-400 transition-colors group-hover:text-[#c4a265]" fill="currentColor" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path></svg></a>
|
||||
<a href="https://www.facebook.com/yslootahtech" target="_blank" rel="noopener noreferrer" className="group flex h-11 w-11 items-center justify-center rounded-xl border border-[rgba(255,255,255,0.05)] bg-[#11111a] transition-all duration-300 hover:border-[#c4a265]/50 hover:bg-[#c4a265]/10 group-hover:text-[#c4a265]" aria-label="Facebook"><svg className="h-5 w-5 text-slate-400 transition-colors group-hover:text-[#c4a265]" fill="currentColor" viewBox="0 0 24 24"><path d="M9.101 24v-11.01h-3.427v-3.929h3.427v-2.897c0-3.411 2.083-5.268 5.123-5.268 1.455 0 2.707.108 3.07.157v3.56h-2.107c-1.654 0-1.974.786-1.974 1.938v2.51h3.942l-.513 3.929h-3.429V24H9.101z"></path></svg></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Contact Form */}
|
||||
<div style={{ flex: '1 1 min(450px, 100%)', padding: 'clamp(1.5rem, 5vw, 3.5rem)', borderRadius: '1rem', background: '#11111a', border: '1px solid rgba(255,255,255,0.05)', boxSizing: 'border-box' }}>
|
||||
<form style={{ display: 'flex', flexDirection: 'column', gap: '1.75rem' }} onSubmit={handleSubmit}>
|
||||
{status === 'success' && (
|
||||
<div style={{ background: 'rgba(34, 197, 94, 0.1)', color: '#22c55e', padding: '1rem', borderRadius: '0.5rem', border: '1px solid rgba(34, 197, 94, 0.2)', textAlign: 'center', fontSize: '0.9rem' }}>
|
||||
Thank you! Your message has been sent successfully.
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div style={{ background: 'rgba(239, 68, 68, 0.1)', color: '#ef4444', padding: '1rem', borderRadius: '0.5rem', border: '1px solid rgba(239, 68, 68, 0.2)', textAlign: 'center', fontSize: '0.9rem' }}>
|
||||
An error occurred. Please try again.
|
||||
</div>
|
||||
)}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<label style={{ fontSize: '0.8rem', fontWeight: 500, color: '#cbd5e1', letterSpacing: '0.05em' }}>Full Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
placeholder="John Doe"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({...formData, name: e.target.value})}
|
||||
style={{ width: '100%', padding: '1rem 0', border: 'none', borderBottom: '1px solid rgba(255,255,255,0.1)', fontSize: '1rem', background: 'transparent', color: '#ffffff', outline: 'none', transition: 'border-color 0.3s', boxSizing: 'border-box' }}
|
||||
onFocus={(e) => e.target.style.borderBottom = '1px solid var(--color-gold)'}
|
||||
onBlur={(e) => e.target.style.borderBottom = '1px solid rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<label style={{ fontSize: '0.8rem', fontWeight: 500, color: '#cbd5e1', letterSpacing: '0.05em' }}>Email Address</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="john@company.com"
|
||||
value={formData.email}
|
||||
onChange={(e) => setFormData({...formData, email: e.target.value})}
|
||||
style={{ width: '100%', padding: '1rem 0', border: 'none', borderBottom: '1px solid rgba(255,255,255,0.1)', fontSize: '1rem', background: 'transparent', color: '#ffffff', outline: 'none', transition: 'border-color 0.3s', boxSizing: 'border-box' }}
|
||||
onFocus={(e) => e.target.style.borderBottom = '1px solid var(--color-gold)'}
|
||||
onBlur={(e) => e.target.style.borderBottom = '1px solid rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Mobile Number with Country Code */}
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<label style={{ fontSize: '0.8rem', fontWeight: 500, color: '#cbd5e1', letterSpacing: '0.05em' }}>Mobile Number</label>
|
||||
<div style={{ display: 'flex', gap: '1rem', alignItems: 'flex-end' }}>
|
||||
<PhoneInput
|
||||
country={'ae'}
|
||||
value={formData.phone}
|
||||
onChange={(phone) => setFormData({...formData, phone})}
|
||||
containerStyle={{ width: '100%' }}
|
||||
inputStyle={{
|
||||
width: '100%',
|
||||
padding: '1rem 0 1rem 3.5rem',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
||||
fontSize: '1rem',
|
||||
background: 'transparent',
|
||||
color: '#ffffff',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.3s',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
buttonStyle={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.1)',
|
||||
padding: '0 0.5rem',
|
||||
}}
|
||||
dropdownStyle={{ background: '#11111a', color: '#fff', border: '1px solid rgba(255,255,255,0.1)' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', marginTop: '0.5rem' }}>
|
||||
<label style={{ fontSize: '0.8rem', fontWeight: 500, color: '#cbd5e1', letterSpacing: '0.05em' }}>Message</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
required
|
||||
placeholder="How can we help you?"
|
||||
value={formData.message}
|
||||
onChange={(e) => setFormData({...formData, message: e.target.value})}
|
||||
style={{ width: '100%', padding: '1rem 0', border: 'none', borderBottom: '1px solid rgba(255,255,255,0.1)', fontSize: '1rem', background: 'transparent', color: '#ffffff', outline: 'none', transition: 'border-color 0.3s', resize: 'none', boxSizing: 'border-box' }}
|
||||
onFocus={(e) => e.target.style.borderBottom = '1px solid var(--color-gold)'}
|
||||
onBlur={(e) => e.target.style.borderBottom = '1px solid rgba(255,255,255,0.1)'}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
disabled={status === 'loading'}
|
||||
style={{
|
||||
padding: '1.25rem 2.5rem',
|
||||
borderRadius: '3rem',
|
||||
background: status === 'loading' ? '#64748b' : '#ffffff',
|
||||
color: status === 'loading' ? '#ffffff' : '#0a0a0f',
|
||||
fontSize: '0.95rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.05em',
|
||||
textTransform: 'uppercase',
|
||||
marginTop: '1.5rem',
|
||||
border: 'none',
|
||||
cursor: status === 'loading' ? 'not-allowed' : 'pointer',
|
||||
transition: 'background-color 0.3s, transform 0.3s'
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem', marginBottom: '1.25rem' }}>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 10,
|
||||
background: 'linear-gradient(135deg, #e0c896, #c4a265 50%, #8b6f47)',
|
||||
boxShadow: '0 0 20px rgba(196, 162, 101,0.4)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 'none',
|
||||
}}
|
||||
onMouseOver={(e) => { if(status !== 'loading') e.currentTarget.style.background = 'var(--color-gold)' }}
|
||||
onMouseOut={(e) => { if(status !== 'loading') e.currentTarget.style.background = '#ffffff' }}
|
||||
>
|
||||
{status === 'loading' ? 'Sending...' : 'Send Message'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Redesigned Footer (Inspired by yslootahtech.com) */}
|
||||
<footer style={{ background: '#050508', borderTop: '1px solid rgba(255,255,255,0.05)', padding: 'clamp(3rem, 8vw, 6rem) 1.5rem 2rem' }}>
|
||||
<div style={{ maxWidth: '1280px', margin: '0 auto', display: 'flex', flexWrap: 'wrap', gap: 'clamp(2rem, 5vw, 5rem)', justifyContent: 'space-between', paddingBottom: 'clamp(2rem, 5vw, 5rem)' }}>
|
||||
|
||||
{/* Brand Column */}
|
||||
<div style={{ flex: '1 1 min(350px, 100%)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', marginBottom: '2rem' }}>
|
||||
<span style={{ fontSize: '1.2rem', fontWeight: 700, color: 'var(--color-gold)', letterSpacing: '0.25em', textTransform: 'uppercase' }}>
|
||||
YS Lootah
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#04111a" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="4" y="6" width="16" height="12" rx="3" />
|
||||
<path d="M9 12h6M12 2v4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span style={{ fontSize: '1.2rem', fontWeight: 300, color: '#ffffff', letterSpacing: '0.25em', textTransform: 'uppercase' }}>
|
||||
Robotics
|
||||
<span style={{ display: 'flex', flexDirection: 'column', lineHeight: 1 }}>
|
||||
<span style={{ fontSize: '1rem', fontWeight: 700, letterSpacing: '0.16em', color: '#ffffff', textTransform: 'uppercase' }}>
|
||||
YS Lootah
|
||||
</span>
|
||||
<span style={{ fontSize: '0.7rem', fontWeight: 500, letterSpacing: '0.32em', color: '#e0c896', textTransform: 'uppercase', marginTop: 4 }}>
|
||||
Robotics
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ fontSize: '1rem', color: '#94a3b8', lineHeight: 1.8, fontWeight: 300, maxWidth: '90%', marginBottom: '2rem' }}>
|
||||
Innovating today for a smarter tomorrow. We are more than an automation provider; we are your trusted technology partner delivering advanced enterprise humanoid robotics.
|
||||
<p style={{ margin: 0, color: '#94908a', lineHeight: 1.7, fontSize: '0.95rem' }}>
|
||||
Premium humanoid, quadruped, and service robotics — available through YS Lootah Robotics for businesses and innovators across Dubai and the UAE.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '1rem' }}>
|
||||
<a href="https://www.instagram.com/yslootahtech" target="_blank" rel="noopener noreferrer" style={{ color: '#64748b', transition: 'color 0.3s' }} onMouseOver={e=>e.currentTarget.style.color='var(--color-gold)'} onMouseOut={e=>e.currentTarget.style.color='#64748b'}>
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"></path></svg>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/ys-lootah-tech" target="_blank" rel="noopener noreferrer" style={{ color: '#64748b', transition: 'color 0.3s' }} onMouseOver={e=>e.currentTarget.style.color='var(--color-gold)'} onMouseOut={e=>e.currentTarget.style.color='#64748b'}>
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"></path></svg>
|
||||
</a>
|
||||
<a href="https://www.facebook.com/yslootahtech" target="_blank" rel="noopener noreferrer" style={{ color: '#64748b', transition: 'color 0.3s' }} onMouseOver={e=>e.currentTarget.style.color='var(--color-gold)'} onMouseOut={e=>e.currentTarget.style.color='#64748b'}>
|
||||
<svg className="h-5 w-5" fill="currentColor" viewBox="0 0 24 24"><path d="M9.101 24v-11.01h-3.427v-3.929h3.427v-2.897c0-3.411 2.083-5.268 5.123-5.268 1.455 0 2.707.108 3.07.157v3.56h-2.107c-1.654 0-1.974.786-1.974 1.938v2.51h3.942l-.513 3.929h-3.429V24H9.101z"></path></svg>
|
||||
</a>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.625rem', marginTop: '1.5rem' }}>
|
||||
<SocialLink href={CONTACT.instagram} label="Instagram" path="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z" />
|
||||
<SocialLink href={CONTACT.linkedin} label="LinkedIn" path="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
<SocialLink href={CONTACT.facebook} label="Facebook" path="M9.101 24v-11.01h-3.427v-3.929h3.427v-2.897c0-3.411 2.083-5.268 5.123-5.268 1.455 0 2.707.108 3.07.157v3.56h-2.107c-1.654 0-1.974.786-1.974 1.938v2.51h3.942l-.513 3.929h-3.429V24H9.101z" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '1 1 200px' }}>
|
||||
<h4 style={{ fontSize: '0.85rem', fontWeight: 600, color: '#ffffff', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '2rem' }}>Customization</h4>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
{[
|
||||
{ label: 'Emarati Kandura', href: '/configure/?config=eyJjIjp7InByaW1hcnkiOiIjOTZhMmI2Iiwic2Vjb25kYXJ5IjoiIzFlMjkzYiIsImFjY2VudCI6IiNmNTllMGIifSwicCI6ImVtYXJhdGkta2FuZHVyYSIsInkiOltdfQ%3D%3D' },
|
||||
{ label: 'Industrial Vest', href: '/configure/?config=eyJjIjp7InByaW1hcnkiOiIjOTZhMmI2Iiwic2Vjb25kYXJ5IjoiIzFlMjkzYiIsImFjY2VudCI6IiNmNTllMGIifSwicCI6ImluZHVzdHJpYWwtdmVzdCIsInkiOltdfQ%3D%3D' },
|
||||
{ label: 'Business Suit', href: '/configure/?config=eyJjIjp7InByaW1hcnkiOiIjOTZhMmI2Iiwic2Vjb25kYXJ5IjoiIzFlMjkzYiIsImFjY2VudCI6IiNmNTllMGIifSwicCI6ImJ1c2luZXNzLXN1aXQiLCJ5IjpbXX0%3D' },
|
||||
{ label: 'Robot Doctor', href: '/configure/?config=eyJjIjp7InByaW1hcnkiOiIjOTZhMmI2Iiwic2Vjb25kYXJ5IjoiIzFlMjkzYiIsImFjY2VudCI6IiNmNTllMGIifSwicCI6InJvYm90LWRvY3RvciIsInkiOltdfQ%3D%3D' },
|
||||
{ label: 'Security Guard', href: '/configure/?config=eyJjIjp7InByaW1hcnkiOiIjOTZhMmI2Iiwic2Vjb25kYXJ5IjoiIzFlMjkzYiIsImFjY2VudCI6IiNmNTllMGIifSwicCI6InNlY3VyaXR5LWd1YXJkIiwieSI6W119' }
|
||||
].map((item, i) => (
|
||||
<li key={i}>
|
||||
<Link href={item.href} style={{ color: '#94a3b8', textDecoration: 'none', fontSize: '0.95rem', fontWeight: 300, transition: 'color 0.3s, padding-left 0.3s' }}
|
||||
onMouseOver={e => { e.currentTarget.style.color = 'var(--color-gold)'; e.currentTarget.style.paddingLeft = '5px'; }}
|
||||
onMouseOut={e => { e.currentTarget.style.color = '#94a3b8'; e.currentTarget.style.paddingLeft = '0'; }}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<ColumnLinks
|
||||
title="Explore"
|
||||
items={[
|
||||
{ label: 'Robots', href: '/robots/' },
|
||||
{ label: 'Brands', href: '/brands/' },
|
||||
{ label: 'Industries', href: '/industries/' },
|
||||
{ label: 'About', href: '/about/' },
|
||||
{ label: 'Configure your robot', href: '/configure/' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<ColumnLinks
|
||||
title="Robot categories"
|
||||
items={[
|
||||
{ label: 'Humanoid', href: '/robots/?category=humanoid' },
|
||||
{ label: 'Quadruped', href: '/robots/?category=quadruped' },
|
||||
{ label: 'Service', href: '/robots/?category=service' },
|
||||
{ label: 'Delivery', href: '/robots/?category=delivery' },
|
||||
{ label: 'Hospitality', href: '/robots/?category=hospitality' },
|
||||
{ label: 'Cleaning', href: '/robots/?category=cleaning' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<ColumnLinks
|
||||
title="Brands"
|
||||
items={[
|
||||
{ label: BRANDS.unitree.name, href: '/brands/#unitree' },
|
||||
{ label: BRANDS.pudu.name, href: '/brands/#pudu' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<h4 style={{ margin: '0 0 1.25rem', fontSize: '0.72rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#94908a', fontWeight: 700 }}>
|
||||
Contact
|
||||
</h4>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
<li>
|
||||
<a href={`tel:${CONTACT.phonePrimary.replace(/\s/g, '')}`} style={footerLink}>{CONTACT.phonePrimary}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={`tel:${CONTACT.phoneSecondary.replace(/\s/g, '')}`} style={footerLink}>{CONTACT.phoneSecondary}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={`mailto:${CONTACT.email}`} style={footerLink}>{CONTACT.email}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={CONTACT.maps} target="_blank" rel="noopener noreferrer" style={{ ...footerLink, color: '#94908a' }}>
|
||||
{CONTACT.address}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href={CONTACT.whatsapp} target="_blank" rel="noopener noreferrer" style={{ ...footerLink, color: '#e0c896' }}>
|
||||
WhatsApp us
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: '1 1 200px' }}>
|
||||
<h4 style={{ fontSize: '0.85rem', fontWeight: 600, color: '#ffffff', letterSpacing: '0.15em', textTransform: 'uppercase', marginBottom: '2rem' }}>Company</h4>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
{[
|
||||
{ label: 'About YS Lootah', href: 'https://yslootahtech.com/#whyus' },
|
||||
{ label: 'Contact Us', href: '#contact' }
|
||||
].map((item, i) => (
|
||||
<li key={i}>
|
||||
<Link href={item.href} style={{ color: '#94a3b8', textDecoration: 'none', fontSize: '0.95rem', fontWeight: 300, transition: 'color 0.3s, padding-left 0.3s' }}
|
||||
onMouseOver={e => { e.currentTarget.style.color = 'var(--color-gold)'; e.currentTarget.style.paddingLeft = '5px'; }}
|
||||
onMouseOut={e => { e.currentTarget.style.color = '#94a3b8'; e.currentTarget.style.paddingLeft = '0'; }}
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Copyright */}
|
||||
<div style={{ maxWidth: '1280px', margin: '0 auto', paddingTop: '2.5rem', borderTop: '1px solid rgba(255,255,255,0.05)', display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', gap: '1rem' }}>
|
||||
<p style={{ fontSize: '0.85rem', color: '#64748b', margin: 0, fontWeight: 300 }}>
|
||||
© {new Date().getFullYear()} YS Lootah Robotics. All rights reserved.
|
||||
|
||||
<div style={{ borderTop: '1px solid rgba(196, 162, 101,0.1)', paddingTop: '1.75rem', display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', gap: '1rem' }}>
|
||||
<p style={{ margin: 0, fontSize: '0.82rem', color: '#6b6862' }}>
|
||||
© {new Date().getFullYear()} YS Lootah Robotics. All rights reserved.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '2rem' }}>
|
||||
<Link href="/privacy-policy" style={{ fontSize: '0.85rem', color: '#64748b', textDecoration: 'none', transition: 'color 0.3s' }} onMouseOver={e=>e.currentTarget.style.color='#fff'} onMouseOut={e=>e.currentTarget.style.color='#64748b'}>Privacy Policy</Link>
|
||||
<Link href="/terms-of-service" style={{ fontSize: '0.85rem', color: '#64748b', textDecoration: 'none', transition: 'color 0.3s' }} onMouseOver={e=>e.currentTarget.style.color='#fff'} onMouseOut={e=>e.currentTarget.style.color='#64748b'}>Terms of Service</Link>
|
||||
<p style={{ margin: 0, fontSize: '0.78rem', color: '#6b6862' }}>
|
||||
Website designed & developed by{' '}
|
||||
<a href="https://yslootahtech.com" target="_blank" rel="noopener noreferrer" style={{ color: '#94908a', textDecoration: 'none' }}>
|
||||
YS Lootah Tech
|
||||
</a>
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '1.5rem' }}>
|
||||
<Link href="/privacy-policy/" style={{ fontSize: '0.82rem', color: '#6b6862', textDecoration: 'none' }}>Privacy</Link>
|
||||
<Link href="/terms-of-service/" style={{ fontSize: '0.82rem', color: '#6b6862', textDecoration: 'none' }}>Terms</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
const footerLink: React.CSSProperties = {
|
||||
fontSize: '0.9rem',
|
||||
color: '#cbc4b3',
|
||||
textDecoration: 'none',
|
||||
transition: 'color 0.2s',
|
||||
};
|
||||
|
||||
function ColumnLinks({ title, items }: { title: string; items: { label: string; href: string }[] }) {
|
||||
return (
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<h4 style={{ margin: '0 0 1.25rem', fontSize: '0.72rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#94908a', fontWeight: 700 }}>
|
||||
{title}
|
||||
</h4>
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
|
||||
{items.map((it) => (
|
||||
<li key={it.label}>
|
||||
<Link href={it.href} style={footerLink}>{it.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SocialLink({ href, label, path }: { href: string; label: string; path: string }) {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label={label}
|
||||
style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 10,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(15, 12, 8,0.6)',
|
||||
border: '1px solid rgba(196, 162, 101,0.18)',
|
||||
color: '#94908a',
|
||||
transition: 'all 0.25s ease',
|
||||
}}
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d={path} />
|
||||
</svg>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@ -116,7 +116,7 @@ export function InteractiveHotspot({
|
||||
whiteSpace: 'nowrap',
|
||||
fontFamily: 'system-ui, -apple-system, sans-serif',
|
||||
fontSize: '12px',
|
||||
color: '#1a1a2e',
|
||||
color: '#0a0907',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
animation: 'fadeIn 0.15s ease-out',
|
||||
}}
|
||||
@ -125,7 +125,7 @@ export function InteractiveHotspot({
|
||||
<div
|
||||
style={{
|
||||
fontSize: '10px',
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
|
||||
@ -23,7 +23,7 @@ export function LanguageSwitcher() {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.04)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.08)',
|
||||
borderRadius: '0.5rem',
|
||||
color: '#1a1a2e',
|
||||
color: '#0a0907',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
|
||||
@ -1,32 +1,41 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Robots', href: '/robots/' },
|
||||
{ label: 'Brands', href: '/brands/' },
|
||||
{ label: 'Industries', href: '/industries/' },
|
||||
{ label: 'About', href: '/about/' },
|
||||
{ label: 'Contact', href: '/contact/' },
|
||||
{ label: 'Book Demo', href: '/book-demo/' },
|
||||
];
|
||||
|
||||
export function Navbar() {
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
const pathname = usePathname() || '/';
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setScrolled(window.scrollY > 20);
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
const onScroll = () => setScrolled(window.scrollY > 16);
|
||||
onScroll();
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
return () => window.removeEventListener('scroll', onScroll);
|
||||
}, []);
|
||||
|
||||
const navLinks = [
|
||||
{ label: 'Contact', href: '#contact' }
|
||||
];
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = mobileOpen ? 'hidden' : '';
|
||||
return () => {
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [mobileOpen]);
|
||||
|
||||
const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, link: any) => {
|
||||
if (link.progress !== undefined) {
|
||||
e.preventDefault();
|
||||
setMobileMenuOpen(false);
|
||||
// We have 7 snap-sections of 100vh each. Max scroll inside the scene is 600vh.
|
||||
const targetScroll = link.progress * (6 * window.innerHeight);
|
||||
window.scrollTo({ top: targetScroll, behavior: 'smooth' });
|
||||
}
|
||||
const isActive = (href: string) => {
|
||||
if (href === '/') return pathname === '/' || pathname === '';
|
||||
return pathname.startsWith(href.replace(/\/$/, ''));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -38,179 +47,188 @@ export function Navbar() {
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 100,
|
||||
background: scrolled ? 'rgba(255, 255, 255, 0.85)' : 'transparent',
|
||||
backdropFilter: scrolled ? 'blur(16px)' : 'none',
|
||||
WebkitBackdropFilter: scrolled ? 'blur(16px)' : 'none',
|
||||
borderBottom: scrolled ? '1px solid rgba(0,0,0,0.05)' : '1px solid transparent',
|
||||
transition: 'all 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
padding: scrolled ? '1rem 2rem' : '1.5rem 2rem',
|
||||
background: scrolled ? 'rgba(5, 5, 5, 0.78)' : 'linear-gradient(180deg, rgba(5, 5, 5,0.6), rgba(5, 5, 5,0))',
|
||||
backdropFilter: scrolled ? 'blur(18px)' : 'blur(8px)',
|
||||
WebkitBackdropFilter: scrolled ? 'blur(18px)' : 'blur(8px)',
|
||||
borderBottom: scrolled ? '1px solid rgba(196, 162, 101,0.14)' : '1px solid transparent',
|
||||
transition: 'all 0.35s cubic-bezier(0.16,1,0.3,1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ maxWidth: '1280px', margin: '0 auto', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
|
||||
{/* Logo */}
|
||||
<Link href="/" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
|
||||
<span style={{
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: 700,
|
||||
color: 'var(--color-gold)',
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase',
|
||||
textShadow: scrolled ? 'none' : '0 2px 10px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
YS Lootah
|
||||
<div
|
||||
style={{
|
||||
maxWidth: '1320px',
|
||||
margin: '0 auto',
|
||||
padding: scrolled ? '0.85rem clamp(1rem, 4vw, 2rem)' : '1.1rem clamp(1rem, 4vw, 2rem)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
transition: 'padding 0.3s ease',
|
||||
}}
|
||||
>
|
||||
<Link href="/" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: '0.5rem', minWidth: 0 }} onClick={() => setMobileOpen(false)}>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 8,
|
||||
background: 'linear-gradient(135deg, #e0c896, #c4a265 50%, #8b6f47)',
|
||||
boxShadow: '0 0 18px rgba(196, 162, 101,0.4)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: 'none',
|
||||
}}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#04111a" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="4" y="6" width="16" height="12" rx="3" />
|
||||
<path d="M9 12h6M12 2v4" />
|
||||
</svg>
|
||||
</span>
|
||||
<span style={{
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: 300,
|
||||
color: scrolled ? '#1a1a2e' : '#1a1a2e',
|
||||
letterSpacing: '0.15em',
|
||||
textTransform: 'uppercase'
|
||||
}}>
|
||||
Robotics
|
||||
<span style={{ display: 'flex', flexDirection: 'column', lineHeight: 1 }}>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 700, letterSpacing: '0.16em', color: '#ffffff', textTransform: 'uppercase' }}>
|
||||
YS Lootah
|
||||
</span>
|
||||
<span style={{ fontSize: '0.7rem', fontWeight: 500, letterSpacing: '0.32em', color: '#e0c896', textTransform: 'uppercase', marginTop: 3 }}>
|
||||
Robotics
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Links */}
|
||||
<div className="hidden md:flex" style={{ alignItems: 'center', gap: '3rem' }}>
|
||||
{navLinks.map((link) => (
|
||||
<div className="nav-desktop" style={{ alignItems: 'center', gap: '1.4rem' }}>
|
||||
{NAV_LINKS.map((l) => (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href || '#'}
|
||||
onClick={(e) => handleNavClick(e, link)}
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
style={{
|
||||
color: scrolled ? '#64748b' : '#1a1a2e',
|
||||
fontSize: '0.85rem',
|
||||
position: 'relative',
|
||||
color: isActive(l.href) ? '#e0c896' : '#cbc4b3',
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
letterSpacing: '0.16em',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
transition: 'color 0.2s',
|
||||
textShadow: scrolled ? 'none' : '0 2px 10px rgba(255,255,255,0.2)'
|
||||
paddingBottom: 4,
|
||||
borderBottom: isActive(l.href) ? '1px solid #e0c896' : '1px solid transparent',
|
||||
}}
|
||||
onMouseOver={(e) => e.currentTarget.style.color = 'var(--color-gold)'}
|
||||
onMouseOut={(e) => e.currentTarget.style.color = scrolled ? '#64748b' : '#1a1a2e'}
|
||||
>
|
||||
{link.label}
|
||||
{l.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action Button & Hamburger */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.625rem' }}>
|
||||
<Link
|
||||
href="/configure/"
|
||||
className="hidden md:flex"
|
||||
className="nav-desktop-cta btn btn-primary"
|
||||
style={{ padding: '0.7rem 1.2rem', fontSize: '0.78rem' }}
|
||||
>
|
||||
Configure
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="nav-mobile-toggle"
|
||||
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileOpen}
|
||||
onClick={() => setMobileOpen((s) => !s)}
|
||||
style={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 12,
|
||||
border: '1px solid rgba(196, 162, 101,0.2)',
|
||||
background: 'rgba(15, 12, 8,0.6)',
|
||||
color: '#f5f1e8',
|
||||
cursor: 'pointer',
|
||||
display: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0.75rem 1.5rem',
|
||||
borderRadius: '2rem',
|
||||
background: scrolled ? '#1a1a2e' : 'var(--color-gold)',
|
||||
color: '#ffffff',
|
||||
fontSize: '0.85rem',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
transition: 'all 0.3s',
|
||||
boxShadow: scrolled ? 'none' : '0 4px 14px rgba(196, 162, 101, 0.3)',
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(-1px)';
|
||||
e.currentTarget.style.boxShadow = scrolled ? '0 4px 14px rgba(26, 26, 46, 0.2)' : '0 6px 20px rgba(196, 162, 101, 0.4)';
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = scrolled ? 'none' : '0 4px 14px rgba(196, 162, 101, 0.3)';
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
Configure G1
|
||||
</Link>
|
||||
|
||||
{/* Mobile Menu Toggle */}
|
||||
<button
|
||||
className="md:hidden"
|
||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||
style={{
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: scrolled ? '#1a1a2e' : 'var(--color-gold)',
|
||||
padding: '0.5rem',
|
||||
}}
|
||||
>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
{mobileMenuOpen ? (
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
) : (
|
||||
<path d="M4 12h16M4 6h16M4 18h16" />
|
||||
)}
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
{mobileOpen ? <path d="M6 6l12 12M6 18L18 6" /> : <path d="M4 7h16M4 12h16M4 17h16" />}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Mobile Menu Dropdown */}
|
||||
<div
|
||||
className="md:hidden"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Mobile navigation"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: '#ffffff',
|
||||
zIndex: 90,
|
||||
display: mobileMenuOpen ? 'block' : 'none',
|
||||
paddingTop: '6rem',
|
||||
inset: 0,
|
||||
zIndex: 99,
|
||||
background: 'rgba(5, 5, 5,0.96)',
|
||||
backdropFilter: 'blur(20px)',
|
||||
WebkitBackdropFilter: 'blur(20px)',
|
||||
transform: mobileOpen ? 'translateY(0)' : 'translateY(-12px)',
|
||||
opacity: mobileOpen ? 1 : 0,
|
||||
pointerEvents: mobileOpen ? 'auto' : 'none',
|
||||
transition: 'opacity 0.25s ease, transform 0.25s ease',
|
||||
padding: 'clamp(5rem, 14vw, 7rem) clamp(1.5rem, 5vw, 2rem) clamp(2rem, 6vw, 3rem)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
overflowY: 'auto',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', padding: '0 2rem', gap: '2rem' }}>
|
||||
{navLinks.map((link) => (
|
||||
<Link
|
||||
key={link.label}
|
||||
href={link.href || '#'}
|
||||
onClick={(e) => handleNavClick(e, link)}
|
||||
style={{
|
||||
color: '#1a1a2e',
|
||||
fontSize: '1.4rem',
|
||||
fontWeight: 300,
|
||||
textDecoration: 'none',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
borderBottom: '1px solid #f1f5f9',
|
||||
paddingBottom: '1rem',
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
{NAV_LINKS.map((l) => (
|
||||
<Link
|
||||
href="/configure/"
|
||||
onClick={() => setMobileMenuOpen(false)}
|
||||
key={l.href}
|
||||
href={l.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '1.25rem',
|
||||
borderRadius: '3rem',
|
||||
background: 'var(--color-gold)',
|
||||
color: '#ffffff',
|
||||
fontSize: '1.1rem',
|
||||
fontWeight: 600,
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 500,
|
||||
color: isActive(l.href) ? '#e0c896' : '#f5f1e8',
|
||||
textDecoration: 'none',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.1em',
|
||||
marginTop: '1rem',
|
||||
boxShadow: '0 4px 14px rgba(196, 162, 101, 0.3)',
|
||||
letterSpacing: '-0.01em',
|
||||
padding: '0.75rem 0',
|
||||
borderBottom: '1px solid rgba(196, 162, 101,0.12)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
Configure Your G1
|
||||
<span>{l.label}</span>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<Link
|
||||
href="/configure/"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="btn btn-primary"
|
||||
style={{ marginTop: '1rem', justifyContent: 'center' }}
|
||||
>
|
||||
Configure your robot
|
||||
</Link>
|
||||
<Link
|
||||
href="/book-demo/"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="btn btn-ghost"
|
||||
style={{ justifyContent: 'center' }}
|
||||
>
|
||||
Book a demo
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
.nav-desktop, .nav-desktop-cta { display: none !important; }
|
||||
.nav-mobile-toggle { display: inline-flex !important; }
|
||||
@media (min-width: 1080px) {
|
||||
.nav-desktop { display: flex !important; }
|
||||
.nav-desktop-cta { display: inline-flex !important; }
|
||||
.nav-mobile-toggle { display: none !important; }
|
||||
}
|
||||
`}</style>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -94,7 +94,7 @@ function PayloadLoader({ modelPath }: { modelPath: string }) {
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.material) {
|
||||
child.material = new THREE.MeshStandardMaterial({
|
||||
color: '#64748b',
|
||||
color: '#6b6862',
|
||||
metalness: 0.6,
|
||||
roughness: 0.4,
|
||||
});
|
||||
|
||||
@ -70,7 +70,7 @@ export function PricingEngine() {
|
||||
<h3 style={{
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
margin: 0,
|
||||
@ -101,7 +101,7 @@ export function PricingEngine() {
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '0.8rem', fontWeight: 600, color: '#374151' }}>Total</span>
|
||||
<span style={{ fontSize: '0.9rem', fontWeight: 700, color: '#1a1a2e', fontFamily: 'monospace' }}>
|
||||
<span style={{ fontSize: '0.9rem', fontWeight: 700, color: '#0a0907', fontFamily: 'monospace' }}>
|
||||
AED {formatAED(total)}
|
||||
</span>
|
||||
</div>
|
||||
@ -136,7 +136,7 @@ function PriceLine({ label, price }: { label: string; price: number }) {
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<span style={{ fontSize: '0.75rem', color: '#94a3b8' }}>{label}</span>
|
||||
<span style={{ fontSize: '0.75rem', color: '#94908a' }}>{label}</span>
|
||||
<span style={{ fontSize: '0.75rem', color: '#374151', fontFamily: 'monospace' }}>
|
||||
AED {formatAED(price)}
|
||||
</span>
|
||||
|
||||
@ -29,7 +29,7 @@ function Loader() {
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
}} />
|
||||
<p style={{ fontSize: '0.875rem', color: '#64748b', fontFamily: 'system-ui, sans-serif' }}>
|
||||
<p style={{ fontSize: '0.875rem', color: '#6b6862', fontFamily: 'system-ui, sans-serif' }}>
|
||||
{progress.toFixed(0)}% loaded
|
||||
</p>
|
||||
</div>
|
||||
@ -53,7 +53,7 @@ function SceneContent({ onCapture }: { onCapture: (gl: WebGLRenderer, scene: Sce
|
||||
<ambientLight intensity={0.8} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={1.8} color="#ffffff" castShadow shadow-mapSize={[1024, 1024]} />
|
||||
<directionalLight position={[-4, 3, 2]} intensity={0.8} color="#e8f0ff" />
|
||||
<directionalLight position={[0, 3, -5]} intensity={0.8} color="#94a3b8" />
|
||||
<directionalLight position={[0, 3, -5]} intensity={0.8} color="#94908a" />
|
||||
<spotLight position={[0, 8, 0]} intensity={1.0} angle={0.6} penumbra={0.5} color="#ffffff" />
|
||||
<directionalLight position={[0, 2, 5]} intensity={0.7} color="#ffffff" />
|
||||
<RobotModel />
|
||||
@ -148,7 +148,7 @@ export function RobotCanvas() {
|
||||
<button
|
||||
onClick={handleSnapshot}
|
||||
disabled={isCapturing}
|
||||
style={{ ...btnBase, left: '1rem', backgroundColor: 'rgba(255,255,255,0.8)', border: '1px solid #e2e8f0', color: '#1a1a2e' }}
|
||||
style={{ ...btnBase, left: '1rem', backgroundColor: 'rgba(255,255,255,0.8)', border: '1px solid #e8e0cf', color: '#0a0907' }}
|
||||
aria-label="Capture 3D scene snapshot"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
@ -163,8 +163,8 @@ export function RobotCanvas() {
|
||||
...btnBase,
|
||||
left: '8.5rem',
|
||||
backgroundColor: shareStatus === 'copied' ? 'rgba(34,197,94,0.1)' : 'rgba(255,255,255,0.8)',
|
||||
border: `1px solid ${shareStatus === 'copied' ? '#86efac' : '#e2e8f0'}`,
|
||||
color: shareStatus === 'copied' ? '#16a34a' : '#1a1a2e',
|
||||
border: `1px solid ${shareStatus === 'copied' ? '#86efac' : '#e8e0cf'}`,
|
||||
color: shareStatus === 'copied' ? '#16a34a' : '#0a0907',
|
||||
}}
|
||||
aria-label="Share configuration link"
|
||||
>
|
||||
|
||||
@ -178,17 +178,17 @@ export function ScrollOverlays() {
|
||||
</span>
|
||||
<div style={{ width: '30px', height: '1px', background: 'linear-gradient(90deg, #c4a265, transparent)' }} />
|
||||
</div>
|
||||
<p style={{ fontSize: '0.9rem', color: '#64748b', fontWeight: 400, letterSpacing: '0.15em', margin: 0 }}>
|
||||
<p style={{ fontSize: '0.9rem', color: '#6b6862', fontWeight: 400, letterSpacing: '0.15em', margin: 0 }}>
|
||||
Pioneering Humanoid Robotics in the UAE
|
||||
</p>
|
||||
</OverlaySection>
|
||||
|
||||
{/* 2. Hero */}
|
||||
<OverlaySection progress={scrollYProgress} {...SECTION_CONFIGS[1]}>
|
||||
<motion.h1 className="overlay-hero-heading" style={{ fontWeight: 200, color: '#1a1a2e', lineHeight: 1.0, letterSpacing: '-0.04em', margin: 0 }}>
|
||||
<motion.h1 className="overlay-hero-heading" style={{ fontWeight: 200, color: '#0a0907', lineHeight: 1.0, letterSpacing: '-0.04em', margin: 0 }}>
|
||||
The Future
|
||||
</motion.h1>
|
||||
<motion.h1 className="overlay-hero-heading" style={{ fontWeight: 200, color: '#1a1a2e', lineHeight: 1.0, letterSpacing: '-0.04em', margin: '0.1em 0 0' }}>
|
||||
<motion.h1 className="overlay-hero-heading" style={{ fontWeight: 200, color: '#0a0907', lineHeight: 1.0, letterSpacing: '-0.04em', margin: '0.1em 0 0' }}>
|
||||
of <span style={{ color: '#c4a265', fontWeight: 400 }}>Robotics</span>
|
||||
</motion.h1>
|
||||
<p style={{ fontSize: '1rem', color: '#475569', lineHeight: 1.7, margin: '1.5rem 0 0', fontWeight: 300 }}>
|
||||
@ -203,7 +203,7 @@ export function ScrollOverlays() {
|
||||
<div style={{ fontSize: '0.65rem', fontWeight: 600, color: '#c4a265', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
||||
Intelligent by Design
|
||||
</div>
|
||||
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#1a1a2e', lineHeight: 1.1, margin: '0 0 1rem', letterSpacing: '-0.03em' }}>
|
||||
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#0a0907', lineHeight: 1.1, margin: '0 0 1rem', letterSpacing: '-0.03em' }}>
|
||||
Vision That<br />Understands
|
||||
</h2>
|
||||
<p style={{ fontSize: '0.95rem', color: '#475569', lineHeight: 1.6, margin: 0, fontWeight: 300 }}>
|
||||
@ -211,12 +211,12 @@ export function ScrollOverlays() {
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '2.5rem', marginTop: '2rem' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#1a1a2e', fontFamily: 'monospace' }}>360°</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#64748b', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Field of View</div>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#0a0907', fontFamily: 'monospace' }}>360°</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#6b6862', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Field of View</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#1a1a2e', fontFamily: 'monospace' }}>{'<'}50ms</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#64748b', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Response Time</div>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#0a0907', fontFamily: 'monospace' }}>{'<'}50ms</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#6b6862', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Response Time</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -228,7 +228,7 @@ export function ScrollOverlays() {
|
||||
<div style={{ fontSize: '0.65rem', fontWeight: 600, color: '#c4a265', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
||||
Your Identity
|
||||
</div>
|
||||
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#1a1a2e', lineHeight: 1.1, margin: '0 0 1rem', letterSpacing: '-0.03em' }}>
|
||||
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#0a0907', lineHeight: 1.1, margin: '0 0 1rem', letterSpacing: '-0.03em' }}>
|
||||
Dress for Any<br />Mission
|
||||
</h2>
|
||||
<p style={{ fontSize: '0.95rem', color: '#475569', lineHeight: 1.6, margin: 0, fontWeight: 300 }}>
|
||||
@ -254,7 +254,7 @@ export function ScrollOverlays() {
|
||||
background: 'rgba(255, 255, 255, 0.5)',
|
||||
border: '1px solid rgba(196, 162, 101, 0.3)',
|
||||
fontSize: '0.75rem',
|
||||
color: '#1a1a2e',
|
||||
color: '#0a0907',
|
||||
letterSpacing: '0.1em',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
@ -272,7 +272,7 @@ export function ScrollOverlays() {
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.5)';
|
||||
e.currentTarget.style.color = '#1a1a2e';
|
||||
e.currentTarget.style.color = '#0a0907';
|
||||
e.currentTarget.style.transform = 'translateY(0)';
|
||||
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)';
|
||||
}}
|
||||
@ -290,7 +290,7 @@ export function ScrollOverlays() {
|
||||
<div style={{ fontSize: '0.65rem', fontWeight: 600, color: '#c4a265', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
||||
Advanced Mobility
|
||||
</div>
|
||||
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#1a1a2e', lineHeight: 1.1, margin: '0 0 1rem', letterSpacing: '-0.03em' }}>
|
||||
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#0a0907', lineHeight: 1.1, margin: '0 0 1rem', letterSpacing: '-0.03em' }}>
|
||||
23 Degrees of<br />Freedom
|
||||
</h2>
|
||||
<p style={{ fontSize: '0.95rem', color: '#475569', lineHeight: 1.6, margin: 0, fontWeight: 300 }}>
|
||||
@ -298,12 +298,12 @@ export function ScrollOverlays() {
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '2.5rem', marginTop: '2rem' }}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#1a1a2e', fontFamily: 'monospace' }}>2m/s</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#64748b', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Max Speed</div>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#0a0907', fontFamily: 'monospace' }}>2m/s</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#6b6862', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Max Speed</div>
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#1a1a2e', fontFamily: 'monospace' }}>127kg</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#64748b', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Payload</div>
|
||||
<div className="overlay-stat" style={{ fontWeight: 300, color: '#0a0907', fontFamily: 'monospace' }}>127kg</div>
|
||||
<div style={{ fontSize: '0.65rem', color: '#6b6862', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Payload</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -325,7 +325,7 @@ export function ScrollOverlays() {
|
||||
opacity: useTransform(scrollYProgress, [0, 0.05], [1, 0]),
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: '0.65rem', fontWeight: 500, color: '#64748b', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
||||
<span style={{ fontSize: '0.65rem', fontWeight: 500, color: '#6b6862', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
||||
Scroll to Explore
|
||||
</span>
|
||||
<div className="scroll-indicator" />
|
||||
|
||||
@ -241,7 +241,7 @@ function Loader() {
|
||||
borderRadius: '50%',
|
||||
animation: 'spin 1s linear infinite',
|
||||
}} />
|
||||
<p style={{ fontSize: '0.8rem', color: '#64748b', fontFamily: 'Inter, system-ui, sans-serif' }}>
|
||||
<p style={{ fontSize: '0.8rem', color: '#6b6862', fontFamily: 'Inter, system-ui, sans-serif' }}>
|
||||
{progress.toFixed(0)}%
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -47,7 +47,7 @@ function SnapshotButtonInner({ gl, scene, camera, filename = 'g1-robot', onSnaps
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.3)',
|
||||
borderRadius: '0.5rem',
|
||||
color: '#f8fafc',
|
||||
color: '#f5f1e8',
|
||||
cursor: isCapturing ? 'wait' : 'pointer',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
|
||||
@ -39,10 +39,10 @@ export function ConfirmationStep() {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#1a1a2e', margin: 0, marginBottom: '0.5rem' }}>
|
||||
<h3 style={{ fontSize: '1.25rem', fontWeight: 700, color: '#0a0907', margin: 0, marginBottom: '0.5rem' }}>
|
||||
Order Confirmed!
|
||||
</h3>
|
||||
<p style={{ fontSize: '0.8rem', color: '#94a3b8', margin: 0 }}>
|
||||
<p style={{ fontSize: '0.8rem', color: '#94908a', margin: 0 }}>
|
||||
Thank you for your order. Your G1 Robot is being prepared.
|
||||
</p>
|
||||
</div>
|
||||
@ -54,7 +54,7 @@ export function ConfirmationStep() {
|
||||
background: 'rgba(59, 130, 246, 0.04)',
|
||||
border: '1px solid rgba(59, 130, 246, 0.2)',
|
||||
}}>
|
||||
<div style={{ fontSize: '0.65rem', color: '#64748b', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.25rem' }}>
|
||||
<div style={{ fontSize: '0.65rem', color: '#6b6862', textTransform: 'uppercase', letterSpacing: '0.05em', marginBottom: '0.25rem' }}>
|
||||
Order ID
|
||||
</div>
|
||||
<div style={{ fontSize: '1.1rem', fontWeight: 700, color: '#2563eb', fontFamily: 'monospace', letterSpacing: '0.05em' }}>
|
||||
@ -72,16 +72,16 @@ export function ConfirmationStep() {
|
||||
textAlign: 'left',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||
<span style={{ fontSize: '0.75rem', color: '#94a3b8' }}>Configuration</span>
|
||||
<span style={{ fontSize: '0.75rem', color: '#94908a' }}>Configuration</span>
|
||||
<span style={{ fontSize: '0.75rem', color: '#374151' }}>{personaSummary}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
|
||||
<span style={{ fontSize: '0.75rem', color: '#94a3b8' }}>Ship to</span>
|
||||
<span style={{ fontSize: '0.75rem', color: '#94908a' }}>Ship to</span>
|
||||
<span style={{ fontSize: '0.75rem', color: '#374151' }}>{shipping.name}, {shipping.city}</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', paddingTop: '0.5rem', borderTop: '1px solid rgba(0, 0, 0, 0.06)' }}>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: '#374151' }}>Total Paid</span>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 700, color: '#1a1a2e', fontFamily: 'monospace' }}>AED {formatAED(orderTotal)}</span>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 700, color: '#0a0907', fontFamily: 'monospace' }}>AED {formatAED(orderTotal)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -32,7 +32,7 @@ export function PaymentStep() {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: '#1a1a2e', margin: 0 }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: '#0a0907', margin: 0 }}>
|
||||
Payment Details
|
||||
</h3>
|
||||
|
||||
|
||||
@ -97,7 +97,7 @@ export function ReviewStep() {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: '#1a1a2e', margin: 0 }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: '#0a0907', margin: 0 }}>
|
||||
Review Your Order
|
||||
</h3>
|
||||
|
||||
@ -124,8 +124,8 @@ export function ReviewStep() {
|
||||
<div style={{ fontWeight: 500 }}>{shipping.name}</div>
|
||||
<div>{shipping.address}</div>
|
||||
<div>{shipping.city}, {shipping.country} {shipping.postalCode}</div>
|
||||
<div style={{ color: '#94a3b8', marginTop: '0.25rem' }}>{shipping.email}</div>
|
||||
<div style={{ color: '#94a3b8' }}>{shipping.phone}</div>
|
||||
<div style={{ color: '#94908a', marginTop: '0.25rem' }}>{shipping.email}</div>
|
||||
<div style={{ color: '#94908a' }}>{shipping.phone}</div>
|
||||
</div>
|
||||
</SummarySection>
|
||||
|
||||
@ -137,7 +137,7 @@ export function ReviewStep() {
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<span style={{ fontSize: '0.85rem', fontWeight: 600, color: '#374151' }}>Total</span>
|
||||
<span style={{ fontSize: '1.1rem', fontWeight: 700, color: '#1a1a2e', fontFamily: 'monospace' }}>
|
||||
<span style={{ fontSize: '1.1rem', fontWeight: 700, color: '#0a0907', fontFamily: 'monospace' }}>
|
||||
AED {formatAED(orderTotal)}
|
||||
</span>
|
||||
</div>
|
||||
@ -195,7 +195,7 @@ function SummarySection({ title, children }: { title: string; children: React.Re
|
||||
<div style={{
|
||||
fontSize: '0.65rem',
|
||||
fontWeight: 600,
|
||||
color: '#64748b',
|
||||
color: '#6b6862',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
marginBottom: '0.5rem',
|
||||
@ -215,7 +215,7 @@ function SummaryRow({ label, value }: { label: string; value: React.ReactNode })
|
||||
alignItems: 'center',
|
||||
padding: '0.25rem 0',
|
||||
}}>
|
||||
<span style={{ fontSize: '0.8rem', color: '#94a3b8' }}>{label}</span>
|
||||
<span style={{ fontSize: '0.8rem', color: '#94908a' }}>{label}</span>
|
||||
<span style={{ fontSize: '0.8rem', color: '#374151' }}>{value}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -9,7 +9,7 @@ const inputStyle: React.CSSProperties = {
|
||||
borderRadius: '0.375rem',
|
||||
border: '1px solid rgba(0, 0, 0, 0.08)',
|
||||
background: 'rgba(255, 255, 255, 0.8)',
|
||||
color: '#1a1a2e',
|
||||
color: '#0a0907',
|
||||
fontSize: '0.8rem',
|
||||
outline: 'none',
|
||||
transition: 'border-color 0.2s ease',
|
||||
@ -18,7 +18,7 @@ const inputStyle: React.CSSProperties = {
|
||||
const labelStyle: React.CSSProperties = {
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 500,
|
||||
color: '#94a3b8',
|
||||
color: '#94908a',
|
||||
marginBottom: '0.3rem',
|
||||
display: 'block',
|
||||
};
|
||||
@ -80,7 +80,7 @@ export function ShippingStep() {
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: '#1a1a2e', margin: 0 }}>
|
||||
<h3 style={{ fontSize: '1rem', fontWeight: 600, color: '#0a0907', margin: 0 }}>
|
||||
Shipping Information
|
||||
</h3>
|
||||
|
||||
|
||||
178
src/components/robotics/BentoGrid.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { FEATURED_ROBOTS } from '@/data/robots';
|
||||
|
||||
const SOC_PATHS = {
|
||||
brain: 'M9 3a4 4 0 0 0-4 4v1a3 3 0 0 0 0 6v3a3 3 0 0 0 4 2 3 3 0 0 0 6 0 3 3 0 0 0 4-2v-3a3 3 0 0 0 0-6V7a4 4 0 0 0-4-4 3 3 0 0 0-6 0Z',
|
||||
shield: 'M12 2 4 5v7c0 5 3.5 9 8 10 4.5-1 8-5 8-10V5l-8-3Z',
|
||||
bolt: 'M13 2 3 14h7l-1 8 10-12h-7l1-8Z',
|
||||
globe: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 2c2.2 0 4 3.6 4 8s-1.8 8-4 8-4-3.6-4-8 1.8-8 4-8ZM2 12c0-1 .5-2 1.5-3h17c1 1 1.5 2 1.5 3s-.5 2-1.5 3h-17c-1-1-1.5-2-1.5-3Z',
|
||||
};
|
||||
|
||||
export function BentoGrid() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
gridTemplateColumns: 'repeat(12, 1fr)',
|
||||
gridAutoRows: 'minmax(180px, auto)',
|
||||
}}
|
||||
>
|
||||
<Tile col="span 7" row="span 2" accent="#e0c896">
|
||||
<div style={{ position: 'absolute', inset: 0 }}>
|
||||
<Image
|
||||
src={FEATURED_ROBOTS[0].image}
|
||||
alt="Premium robotics in Dubai"
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 60vw"
|
||||
style={{ objectFit: 'contain', objectPosition: 'right center', padding: 'clamp(1.5rem, 4vw, 3rem)', opacity: 0.92 }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: '0.75rem', maxWidth: '50%' }}>
|
||||
<span className="eyebrow">Flagship · Dubai</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'clamp(1.4rem, 2.6vw, 2rem)', fontWeight: 500, letterSpacing: '-0.02em', lineHeight: 1.1 }}>
|
||||
<span className="text-gradient">Unitree G1 humanoid — live in our Dubai showroom.</span>
|
||||
</h3>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: '0.92rem', lineHeight: 1.6 }}>
|
||||
Configure persona, attire, and accessories. Then book a live demo.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginTop: '0.5rem' }}>
|
||||
<Link href="/robots/unitree-g1/" className="btn btn-primary" style={{ padding: '0.65rem 1.1rem', fontSize: '0.75rem' }}>View G1</Link>
|
||||
<Link href="/configure/" className="btn btn-ghost" style={{ padding: '0.65rem 1.1rem', fontSize: '0.75rem' }}>Configure</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Tile>
|
||||
|
||||
<Tile col="span 5" row="span 1" accent="#c4a265" icon={SOC_PATHS.bolt}>
|
||||
<span className="eyebrow">Sales & demos</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'clamp(1.2rem, 2vw, 1.5rem)', fontWeight: 500, letterSpacing: '-0.02em', lineHeight: 1.15 }}>
|
||||
<span className="text-gradient">Fast quotes for UAE businesses.</span>
|
||||
</h3>
|
||||
<p style={{ margin: 0, color: '#94908a', fontSize: '0.88rem', lineHeight: 1.55 }}>
|
||||
Tell us your use case — we respond within one business day with availability and pricing.
|
||||
</p>
|
||||
</Tile>
|
||||
|
||||
<Tile col="span 5" row="span 1" accent="#8b6f47" icon={SOC_PATHS.shield}>
|
||||
<span className="eyebrow">End-to-end</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'clamp(1.2rem, 2vw, 1.5rem)', fontWeight: 500, letterSpacing: '-0.02em', lineHeight: 1.15 }}>
|
||||
<span className="text-gradient">Procurement, deployment, support.</span>
|
||||
</h3>
|
||||
<p style={{ margin: 0, color: '#94908a', fontSize: '0.88rem', lineHeight: 1.55 }}>
|
||||
Local team handles import, setup, training, and ongoing service across the UAE.
|
||||
</p>
|
||||
</Tile>
|
||||
|
||||
<Tile col="span 4" row="span 1" accent="#e0c896" icon={SOC_PATHS.brain}>
|
||||
<span className="eyebrow">Exclusive UAE access</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'clamp(1.05rem, 1.8vw, 1.3rem)', fontWeight: 500, letterSpacing: '-0.01em', lineHeight: 1.2 }}>
|
||||
<span className="text-gradient">Unitree · Pudu — one Dubai team.</span>
|
||||
</h3>
|
||||
</Tile>
|
||||
|
||||
<Tile col="span 4" row="span 1" accent="#c4a265" icon={SOC_PATHS.globe}>
|
||||
<span className="eyebrow">10+ industries</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'clamp(1.05rem, 1.8vw, 1.3rem)', fontWeight: 500, letterSpacing: '-0.01em', lineHeight: 1.2 }}>
|
||||
<span className="text-gradient">Hospitality, security, healthcare, retail.</span>
|
||||
</h3>
|
||||
</Tile>
|
||||
|
||||
<Tile col="span 4" row="span 1" accent="#8b6f47">
|
||||
<span className="eyebrow">Configurator</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'clamp(1.05rem, 1.8vw, 1.3rem)', fontWeight: 500, letterSpacing: '-0.01em', lineHeight: 1.2 }}>
|
||||
<span className="text-gradient">3D persona builder for the G1.</span>
|
||||
</h3>
|
||||
<Link
|
||||
href="/configure/"
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.4rem',
|
||||
color: '#8b6f47',
|
||||
fontSize: '0.78rem',
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Start configuration
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
</Tile>
|
||||
|
||||
<style jsx global>{`
|
||||
@media (max-width: 900px) {
|
||||
.bento-tile-7 { grid-column: span 12 !important; }
|
||||
.bento-tile-5 { grid-column: span 12 !important; }
|
||||
.bento-tile-4 { grid-column: span 6 !important; }
|
||||
}
|
||||
@media (max-width: 540px) {
|
||||
.bento-tile-4 { grid-column: span 12 !important; }
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Tile({
|
||||
col,
|
||||
row,
|
||||
accent,
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
col: string;
|
||||
row: string;
|
||||
accent: string;
|
||||
icon?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const span = col.includes('7') ? 'bento-tile-7' : col.includes('5') ? 'bento-tile-5' : 'bento-tile-4';
|
||||
return (
|
||||
<div
|
||||
className={`card ${span}`}
|
||||
style={{
|
||||
gridColumn: col,
|
||||
gridRow: row,
|
||||
padding: 'clamp(1.25rem, 2.2vw, 1.75rem)',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
`radial-gradient(ellipse 80% 80% at 0% 100%, ${accent}1A, transparent 60%), linear-gradient(135deg, rgba(15, 12, 8,0.7), rgba(5, 5, 5,0.95))`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.6rem',
|
||||
minHeight: 180,
|
||||
}}
|
||||
>
|
||||
{icon && (
|
||||
<div
|
||||
style={{
|
||||
width: 38,
|
||||
height: 38,
|
||||
borderRadius: 10,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: `${accent}1A`,
|
||||
border: `1px solid ${accent}55`,
|
||||
marginBottom: '0.25rem',
|
||||
}}
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke={accent} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d={icon} />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
96
src/components/robotics/BrandShowcase.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { BRANDS, ROBOTS, type RobotBrand } from '@/data/robots';
|
||||
|
||||
export function BrandShowcase() {
|
||||
const brandIds = Object.keys(BRANDS) as RobotBrand[];
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1.25rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{brandIds.map((id) => {
|
||||
const brand = BRANDS[id];
|
||||
const count = ROBOTS.filter((r) => r.brand === id).length;
|
||||
return (
|
||||
<Link
|
||||
key={id}
|
||||
href={`/brands/#${id}`}
|
||||
className="card"
|
||||
style={{
|
||||
padding: '1.75rem',
|
||||
textDecoration: 'none',
|
||||
color: '#f5f1e8',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
minHeight: 220,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background: `radial-gradient(circle at 80% 0%, ${brand.accent}22, transparent 60%)`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: '1.45rem',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.04em',
|
||||
color: '#ffffff',
|
||||
}}
|
||||
>
|
||||
{brand.name.split(' ')[0]}
|
||||
<span style={{ color: brand.accent, marginLeft: 6 }}>
|
||||
{brand.name.split(' ').slice(1).join(' ')}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.2em',
|
||||
textTransform: 'uppercase',
|
||||
color: brand.accent,
|
||||
}}
|
||||
>
|
||||
{count} model{count === 1 ? '' : 's'}
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ position: 'relative', margin: 0, color: '#94908a', lineHeight: 1.6, fontSize: '0.95rem' }}>
|
||||
{brand.tagline}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
marginTop: 'auto',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
color: brand.accent,
|
||||
fontSize: '0.8rem',
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
Explore {brand.name.split(' ')[0]}
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/components/robotics/ConfigureCTA.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
href?: string;
|
||||
ctaLabel?: string;
|
||||
};
|
||||
|
||||
export function ConfigureCTA({
|
||||
title = 'Configure a robotics solution for your business.',
|
||||
description = 'Build a tailored robot — choose persona, attire, color, accessories, and accessories. Visualize before you request a quote.',
|
||||
href = '/configure/',
|
||||
ctaLabel = 'Start configuration',
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'grid',
|
||||
gap: 'clamp(1.5rem, 3vw, 2.5rem)',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
||||
alignItems: 'center',
|
||||
padding: 'clamp(1.75rem, 4vw, 2.75rem)',
|
||||
borderRadius: '1.5rem',
|
||||
background:
|
||||
'radial-gradient(ellipse 80% 80% at 100% 0%, rgba(196, 162, 101,0.16), transparent 60%), linear-gradient(135deg, rgba(15, 12, 8,0.85), rgba(5, 5, 5,0.95))',
|
||||
border: '1px solid rgba(196, 162, 101,0.28)',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 20px 80px rgba(0, 0, 0,0.55)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'linear-gradient(rgba(196, 162, 101,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(196, 162, 101,0.05) 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px',
|
||||
maskImage: 'radial-gradient(ellipse 60% 80% at 80% 30%, #000 30%, transparent 80%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 60% 80% at 80% 30%, #000 30%, transparent 80%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: '0.875rem' }}>
|
||||
<span className="eyebrow">3D Configurator · Unitree G1 · Dubai</span>
|
||||
<h3 style={{ margin: 0, fontSize: 'clamp(1.4rem, 3vw, 2rem)', lineHeight: 1.15, fontWeight: 400, letterSpacing: '-0.02em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>{title}</span>
|
||||
</h3>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: '0.95rem', lineHeight: 1.65 }}>{description}</p>
|
||||
</div>
|
||||
<div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<Link href={href} className="btn btn-primary">
|
||||
{ctaLabel}
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href="/book-demo/" className="btn btn-ghost">Book a demo</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/components/robotics/DemoCTA.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { PremiumButton } from '@/components/ui/PremiumButton';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
description?: string;
|
||||
primaryHref?: string;
|
||||
primaryLabel?: string;
|
||||
secondaryHref?: string;
|
||||
secondaryLabel?: string;
|
||||
};
|
||||
|
||||
export function DemoCTA({
|
||||
title = 'Bring premium robotics to your team.',
|
||||
description = 'Book a live demo at our Dubai showroom or talk to a robotics advisor about your project. We respond fast.',
|
||||
primaryHref = '/book-demo/',
|
||||
primaryLabel = 'Book a live demo',
|
||||
secondaryHref = '/contact/',
|
||||
secondaryLabel = 'Talk to an advisor',
|
||||
}: Props) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: '2rem',
|
||||
padding: 'clamp(2rem, 5vw, 4rem)',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(ellipse 60% 80% at 20% 20%, rgba(196, 162, 101,0.18), transparent 60%), radial-gradient(ellipse 70% 80% at 80% 100%, rgba(139, 111, 71,0.18), transparent 60%), linear-gradient(135deg, rgba(15, 12, 8,0.9), rgba(5, 5, 5,0.95))',
|
||||
border: '1px solid rgba(196, 162, 101,0.18)',
|
||||
boxShadow: '0 30px 100px rgba(0, 0, 0,0.6), inset 0 1px 0 rgba(255,255,255,0.04)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'linear-gradient(rgba(196, 162, 101,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(196, 162, 101,0.05) 1px, transparent 1px)',
|
||||
backgroundSize: '48px 48px',
|
||||
maskImage: 'radial-gradient(ellipse 60% 80% at 50% 30%, #000 30%, transparent 80%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 60% 80% at 50% 30%, #000 30%, transparent 80%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<div style={{ position: 'relative', display: 'flex', flexDirection: 'column', alignItems: 'center', textAlign: 'center', gap: '1.25rem' }}>
|
||||
<span className="eyebrow">Book a demo · Dubai showroom</span>
|
||||
<h2
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(1.8rem, 4.5vw, 3rem)',
|
||||
lineHeight: 1.1,
|
||||
fontWeight: 300,
|
||||
letterSpacing: '-0.03em',
|
||||
maxWidth: 760,
|
||||
}}
|
||||
>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>{title}</span>
|
||||
</h2>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', maxWidth: 620, lineHeight: 1.7, fontSize: 'clamp(0.95rem, 2vw, 1.05rem)' }}>
|
||||
{description}
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', justifyContent: 'center', marginTop: '0.5rem' }}>
|
||||
<PremiumButton href={primaryHref}>{primaryLabel}</PremiumButton>
|
||||
<PremiumButton href={secondaryHref} variant="ghost">{secondaryLabel}</PremiumButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
163
src/components/robotics/ExclusiveAccessSection.tsx
Normal file
@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { PremiumButton } from '@/components/ui/PremiumButton';
|
||||
|
||||
export function ExclusiveAccessSection() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
borderRadius: '2rem',
|
||||
padding: 'clamp(2rem, 5vw, 4rem)',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(ellipse 60% 80% at 0% 0%, rgba(224,200,150,0.18), transparent 60%), radial-gradient(ellipse 70% 80% at 100% 100%, rgba(139,111,71,0.22), transparent 60%), linear-gradient(135deg, rgba(20,17,11,0.92), rgba(5,5,5,0.97))',
|
||||
border: '1px solid rgba(196,162,101,0.32)',
|
||||
boxShadow: '0 30px 100px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,240,200,0.06)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'linear-gradient(rgba(196,162,101,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(196,162,101,0.05) 1px, transparent 1px)',
|
||||
backgroundSize: '48px 48px',
|
||||
maskImage: 'radial-gradient(ellipse 60% 80% at 50% 30%, #000 30%, transparent 80%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 60% 80% at 50% 30%, #000 30%, transparent 80%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ position: 'relative', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))', gap: 'clamp(2rem, 4vw, 3rem)', alignItems: 'center' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
|
||||
<span
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.6rem',
|
||||
padding: '0.5rem 0.9rem',
|
||||
borderRadius: 999,
|
||||
border: '1px solid rgba(196,162,101,0.4)',
|
||||
background: 'rgba(196,162,101,0.08)',
|
||||
color: '#e0c896',
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.24em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
<span aria-hidden style={{ width: 6, height: 6, borderRadius: 999, background: '#e0c896', boxShadow: '0 0 10px rgba(224,200,150,0.8)' }} />
|
||||
Exclusive UAE Access
|
||||
</span>
|
||||
|
||||
<h2 style={{ margin: 0, fontSize: 'clamp(1.8rem, 4.4vw, 2.8rem)', lineHeight: 1.1, fontWeight: 300, letterSpacing: '-0.03em' }}>
|
||||
<span className="text-gradient" style={{ fontWeight: 500 }}>
|
||||
The UAE's dedicated destination for selected Unitree and Pudu Robotics solutions.
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(0.95rem, 2vw, 1.05rem)', lineHeight: 1.7 }}>
|
||||
YS Lootah Robotics holds exclusive sales rights in the UAE for selected Unitree and Pudu Robotics solutions. We deliver advanced robots to businesses, venues, and innovators across Dubai and the UAE — from inquiry to live deployment.
|
||||
</p>
|
||||
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
|
||||
{[
|
||||
'Available exclusively in the UAE through YS Lootah Robotics.',
|
||||
'Local Dubai team handles sales, demos, configuration, and deployment.',
|
||||
'Curated portfolio across humanoid, quadruped, service, and delivery robots.',
|
||||
'End-to-end procurement, training, and ongoing support.',
|
||||
].map((b) => (
|
||||
<li key={b} style={{ display: 'flex', alignItems: 'flex-start', gap: '0.65rem', color: '#f5f1e8', fontSize: '0.95rem', lineHeight: 1.55 }}>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#e0c896" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={{ flex: 'none', marginTop: 3 }} aria-hidden="true">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
{b}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', marginTop: '0.5rem' }}>
|
||||
<PremiumButton href="/robots/">Explore exclusive robots</PremiumButton>
|
||||
<PremiumButton href="/contact/" variant="ghost">Request UAE quotation</PremiumButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '1rem' }}>
|
||||
<BrandTile name="Unitree" sub="Quadruped · Humanoid" href="/brands/#unitree" accent="#e0c896" />
|
||||
<BrandTile name="Pudu Robotics" sub="Service · Delivery · Cleaning" href="/brands/#pudu" accent="#8b6f47" />
|
||||
<Stat value="UAE" label="Exclusive sales territory" />
|
||||
<Stat value="Dubai" label="Showroom & demo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BrandTile({ name, sub, href, accent }: { name: string; sub: string; href: string; accent: string }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
gap: '0.6rem',
|
||||
padding: '1.25rem',
|
||||
borderRadius: 16,
|
||||
background: `linear-gradient(135deg, rgba(20,17,11,0.85), rgba(5,5,5,0.95))`,
|
||||
border: `1px solid ${accent}44`,
|
||||
color: '#f5f1e8',
|
||||
textDecoration: 'none',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
minHeight: 130,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-1rem',
|
||||
right: '-1rem',
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: '50%',
|
||||
background: `radial-gradient(circle, ${accent}33, transparent 70%)`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '1.05rem', fontWeight: 600, letterSpacing: '-0.01em' }}>{name}</span>
|
||||
<span style={{ fontSize: '0.72rem', color: '#94908a', letterSpacing: '0.18em', textTransform: 'uppercase' }}>{sub}</span>
|
||||
<span style={{ fontSize: '0.68rem', color: accent, letterSpacing: '0.22em', textTransform: 'uppercase', fontWeight: 700, marginTop: 'auto' }}>
|
||||
Available in UAE
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({ value, label }: { value: string; label: string }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding: '1.25rem',
|
||||
borderRadius: 16,
|
||||
background: 'linear-gradient(135deg, rgba(20,17,11,0.7), rgba(5,5,5,0.92))',
|
||||
border: '1px solid rgba(196,162,101,0.22)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.4rem',
|
||||
minHeight: 130,
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<span className="text-gradient-accent" style={{ fontSize: 'clamp(1.5rem, 2.6vw, 1.9rem)', fontWeight: 700, letterSpacing: '-0.02em' }}>
|
||||
{value}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.7rem', color: '#94908a', letterSpacing: '0.22em', textTransform: 'uppercase' }}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/components/robotics/FloatingTechPanel.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
type Stat = { label: string; value: string; sub?: string };
|
||||
|
||||
const STATS: Stat[] = [
|
||||
{ label: 'Exclusive territory', value: 'UAE', sub: 'YS Lootah Robotics sales rights' },
|
||||
{ label: 'Premier brands', value: '2', sub: 'Unitree · Pudu Robotics' },
|
||||
{ label: 'Robot models', value: '11+', sub: 'Humanoid, quadruped, service, delivery' },
|
||||
{ label: 'Based in', value: 'Dubai', sub: 'Showroom · demo · deployment' },
|
||||
];
|
||||
|
||||
export function FloatingTechPanel() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(180px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{STATS.map((s, i) => (
|
||||
<div
|
||||
key={s.label}
|
||||
className="card"
|
||||
style={{
|
||||
padding: '1.5rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.4rem',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-1rem',
|
||||
right: '-1rem',
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: '50%',
|
||||
background: `radial-gradient(circle, ${['#e0c896', '#c4a265', '#8b6f47', '#e0c896'][i % 4]}33, transparent 70%)`,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<span style={{ fontSize: '0.68rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#94908a' }}>{s.label}</span>
|
||||
<span className="text-gradient-accent" style={{ fontSize: 'clamp(1.7rem, 3vw, 2.2rem)', fontWeight: 700, letterSpacing: '-0.02em' }}>
|
||||
{s.value}
|
||||
</span>
|
||||
{s.sub && <span style={{ color: '#cbc4b3', fontSize: '0.82rem', lineHeight: 1.5 }}>{s.sub}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
361
src/components/robotics/Hero3DRobotics.tsx
Normal file
@ -0,0 +1,361 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { PremiumButton } from '@/components/ui/PremiumButton';
|
||||
import { CursorSpotlight } from '@/components/ui/CursorSpotlight';
|
||||
import { FEATURED_ROBOTS } from '@/data/robots';
|
||||
|
||||
const HERO_STATS = [
|
||||
{ value: 'Exclusive', label: 'UAE access' },
|
||||
{ value: '2', label: 'Premier brands' },
|
||||
{ value: 'Dubai', label: 'Showroom & demo' },
|
||||
];
|
||||
|
||||
type LabelPos = {
|
||||
text: string;
|
||||
delay: number;
|
||||
top?: string;
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
};
|
||||
|
||||
const FLOATING_LABELS: LabelPos[] = [
|
||||
{ text: 'Exclusive UAE Access', top: '10%', left: '5%', delay: 0 },
|
||||
{ text: 'Unitree Robotics', top: '22%', right: '4%', delay: 0.3 },
|
||||
{ text: 'Pudu Service Robots', bottom: '28%', left: '4%', delay: 0.6 },
|
||||
{ text: 'Demo & Sales · Dubai', bottom: '12%', right: '6%', delay: 0.9 },
|
||||
];
|
||||
|
||||
export function Hero3DRobotics() {
|
||||
const wrapRef = useRef<HTMLDivElement | null>(null);
|
||||
const [tilt, setTilt] = useState({ rx: 0, ry: 0 });
|
||||
const [activeIdx, setActiveIdx] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reduce) return;
|
||||
const el = wrapRef.current;
|
||||
if (!el) return;
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = (e.clientX - rect.left) / rect.width - 0.5;
|
||||
const y = (e.clientY - rect.top) / rect.height - 0.5;
|
||||
setTilt({ rx: -y * 9, ry: x * 14 });
|
||||
};
|
||||
const onLeave = () => setTilt({ rx: 0, ry: 0 });
|
||||
el.addEventListener('mousemove', onMove);
|
||||
el.addEventListener('mouseleave', onLeave);
|
||||
return () => {
|
||||
el.removeEventListener('mousemove', onMove);
|
||||
el.removeEventListener('mouseleave', onLeave);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setActiveIdx((i) => (i + 1) % FEATURED_ROBOTS.length), 5400);
|
||||
return () => clearInterval(t);
|
||||
}, []);
|
||||
|
||||
const robot = FEATURED_ROBOTS[activeIdx];
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{
|
||||
position: 'relative',
|
||||
minHeight: 'min(960px, 100svh)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
paddingTop: 'clamp(6rem, 12vw, 9rem)',
|
||||
paddingBottom: 'clamp(4rem, 8vw, 6rem)',
|
||||
}}
|
||||
>
|
||||
<div className="hero-gradient" />
|
||||
<div className="grid-overlay" />
|
||||
<CursorSpotlight color="rgba(224, 200, 150, 0.22)" size={620} />
|
||||
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'conic-gradient(from 220deg at 60% 50%, transparent 0deg, rgba(196,162,101,0.06) 25deg, transparent 50deg, rgba(224,200,150,0.05) 180deg, transparent 220deg, rgba(196,162,101,0.05) 320deg, transparent 360deg)',
|
||||
mixBlendMode: 'screen',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="container-wide" style={{ position: 'relative', display: 'grid', gridTemplateColumns: '1fr', gap: 'clamp(2rem, 5vw, 4rem)', alignItems: 'center', width: '100%' }}>
|
||||
<div className="hero-grid" style={{ display: 'grid', gap: 'clamp(2rem, 6vw, 4rem)', alignItems: 'center' }}>
|
||||
<div className="fade-in-up" style={{ display: 'flex', flexDirection: 'column', gap: '1.75rem' }}>
|
||||
<span
|
||||
className="eyebrow"
|
||||
style={{
|
||||
color: '#e0c896',
|
||||
border: '1px solid rgba(196,162,101,0.35)',
|
||||
background: 'rgba(196,162,101,0.06)',
|
||||
padding: '0.5rem 0.9rem',
|
||||
borderRadius: 999,
|
||||
alignSelf: 'flex-start',
|
||||
gap: '0.625rem',
|
||||
}}
|
||||
>
|
||||
<span aria-hidden style={{ width: 6, height: 6, borderRadius: 999, background: '#e0c896', boxShadow: '0 0 12px rgba(224,200,150,0.8)' }} />
|
||||
Exclusive UAE Access · Dubai
|
||||
</span>
|
||||
|
||||
<h1
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: 'clamp(2.6rem, 6.4vw, 4.6rem)',
|
||||
lineHeight: 1.02,
|
||||
letterSpacing: '-0.035em',
|
||||
fontWeight: 300,
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'block', color: '#f5f1e8', fontWeight: 200 }}>Advanced robotics.</span>
|
||||
<span className="text-gradient-accent" style={{ display: 'block', fontWeight: 600 }}>
|
||||
Exclusive UAE access.
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: 'clamp(1rem, 2vw, 1.18rem)', lineHeight: 1.7, maxWidth: 600, fontWeight: 300 }}>
|
||||
YS Lootah Robotics is the exclusive UAE sales destination for selected Unitree and Pudu Robotics solutions — helping businesses explore, configure, book demos, and deploy advanced robots across Dubai and the UAE.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', marginTop: '0.5rem' }}>
|
||||
<PremiumButton href="/robots/">Explore exclusive robots</PremiumButton>
|
||||
<PremiumButton href="/book-demo/" variant="ghost">Book a demo</PremiumButton>
|
||||
<PremiumButton href="/configure/" variant="outline">Configure your solution</PremiumButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
|
||||
gap: 'clamp(0.5rem, 2vw, 1.25rem)',
|
||||
paddingTop: '1.25rem',
|
||||
marginTop: '0.5rem',
|
||||
borderTop: '1px solid rgba(196,162,101,0.18)',
|
||||
}}
|
||||
>
|
||||
{HERO_STATS.map((s) => (
|
||||
<div key={s.label} style={{ display: 'flex', flexDirection: 'column', gap: '0.25rem' }}>
|
||||
<span className="text-gradient-accent" style={{ fontSize: 'clamp(1.3rem, 2.6vw, 1.8rem)', fontWeight: 700, letterSpacing: '-0.02em' }}>
|
||||
{s.value}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.7rem', color: '#94908a', letterSpacing: '0.22em', textTransform: 'uppercase' }}>
|
||||
{s.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={wrapRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
perspective: '1500px',
|
||||
minHeight: 'clamp(420px, 56vw, 620px)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: '-3rem',
|
||||
background:
|
||||
`radial-gradient(ellipse 60% 50% at 50% 50%, ${robot.accent}55 0%, transparent 65%)`,
|
||||
filter: 'blur(28px)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="float-y"
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minHeight: 'clamp(420px, 56vw, 620px)',
|
||||
borderRadius: '1.75rem',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(20,17,11,0.7) 0%, rgba(5,5,5,0.92) 100%)',
|
||||
border: '1px solid rgba(196,162,101,0.28)',
|
||||
backdropFilter: 'blur(18px)',
|
||||
boxShadow:
|
||||
'0 30px 100px rgba(0,0,0,0.75), inset 0 1px 0 rgba(255,240,200,0.08), 0 0 60px rgba(196,162,101,0.15)',
|
||||
transform: `rotateX(${tilt.rx}deg) rotateY(${tilt.ry}deg)`,
|
||||
transition: 'transform 0.25s ease-out',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'linear-gradient(rgba(196,162,101,0.06) 1px, transparent 1px), linear-gradient(90deg, rgba(196,162,101,0.06) 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px',
|
||||
maskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 30%, transparent 80%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 70% 70% at 50% 50%, #000 30%, transparent 80%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '-30%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
width: '70%',
|
||||
height: '60%',
|
||||
background:
|
||||
'radial-gradient(ellipse 50% 50% at 50% 30%, rgba(224,200,150,0.30) 0%, transparent 70%)',
|
||||
filter: 'blur(20px)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{FEATURED_ROBOTS.map((r, idx) => (
|
||||
<div
|
||||
key={r.id}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: 'clamp(1.5rem, 5vw, 3rem)',
|
||||
opacity: idx === activeIdx ? 1 : 0,
|
||||
transform: idx === activeIdx ? 'translateY(0) scale(1)' : 'translateY(14px) scale(0.97)',
|
||||
transition: 'opacity 0.9s ease, transform 0.9s ease',
|
||||
}}
|
||||
aria-hidden={idx !== activeIdx}
|
||||
>
|
||||
<Image
|
||||
src={r.image}
|
||||
alt={`${r.brandLabel} ${r.name}`}
|
||||
fill
|
||||
sizes="(max-width: 768px) 90vw, 620px"
|
||||
style={{ objectFit: 'contain', padding: 'clamp(1rem, 4vw, 2rem)' }}
|
||||
priority={idx === 0}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="hero-labels" aria-hidden>
|
||||
{FLOATING_LABELS.map((l) => (
|
||||
<span
|
||||
key={l.text}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: l.top,
|
||||
bottom: l.bottom,
|
||||
left: l.left,
|
||||
right: l.right,
|
||||
padding: '0.4rem 0.75rem',
|
||||
borderRadius: 999,
|
||||
background: 'rgba(10,9,7,0.7)',
|
||||
border: '1px solid rgba(196,162,101,0.3)',
|
||||
color: '#e0c896',
|
||||
fontSize: '0.66rem',
|
||||
letterSpacing: '0.22em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 600,
|
||||
backdropFilter: 'blur(8px)',
|
||||
animation: `floatY 6s ease-in-out ${l.delay}s infinite`,
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{l.text}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '1.25rem',
|
||||
left: '1.25rem',
|
||||
right: '1.25rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1rem',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={`/robots/${robot.slug}/`}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
padding: '0.65rem 1rem',
|
||||
borderRadius: 14,
|
||||
background: 'rgba(10,9,7,0.7)',
|
||||
border: '1px solid rgba(196,162,101,0.35)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
color: '#f5f1e8',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<span style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<span style={{ fontSize: '0.62rem', color: '#94908a', letterSpacing: '0.24em', textTransform: 'uppercase' }}>
|
||||
Featured · {robot.brandLabel}
|
||||
</span>
|
||||
<span style={{ fontSize: '0.95rem', fontWeight: 600 }}>{robot.name}</span>
|
||||
</span>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke={robot.accent} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
</Link>
|
||||
<div style={{ display: 'flex', gap: '0.4rem' }}>
|
||||
{FEATURED_ROBOTS.map((r, idx) => (
|
||||
<button
|
||||
key={r.id}
|
||||
type="button"
|
||||
onClick={() => setActiveIdx(idx)}
|
||||
aria-label={`Show ${r.name}`}
|
||||
style={{
|
||||
width: idx === activeIdx ? 24 : 8,
|
||||
height: 8,
|
||||
borderRadius: 999,
|
||||
border: 'none',
|
||||
background: idx === activeIdx ? r.accent : 'rgba(196,162,101,0.3)',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.4s ease',
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@media (min-width: 1024px) {
|
||||
.hero-grid {
|
||||
grid-template-columns: 1.05fr 1fr !important;
|
||||
}
|
||||
}
|
||||
.hero-labels { display: none; }
|
||||
@media (min-width: 768px) {
|
||||
.hero-labels { display: block; }
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
86
src/components/robotics/HowItWorks.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
'use client';
|
||||
|
||||
const STEPS = [
|
||||
{
|
||||
n: '01',
|
||||
title: 'Discover',
|
||||
body: 'Tell us about your venue and use case. We recommend a brand and model — humanoid, quadruped, service, delivery, cleaning, or commercial.',
|
||||
accent: '#e0c896',
|
||||
},
|
||||
{
|
||||
n: '02',
|
||||
title: 'Demo & configure',
|
||||
body: 'Book a live demo at our Dubai showroom or your venue. Configure your robot’s persona, attire, and accessories in our 3D configurator.',
|
||||
accent: '#c4a265',
|
||||
},
|
||||
{
|
||||
n: '03',
|
||||
title: 'Deploy & support',
|
||||
body: 'We handle procurement, setup, training, and ongoing service across the UAE — so your team can run, not maintain.',
|
||||
accent: '#8b6f47',
|
||||
},
|
||||
];
|
||||
|
||||
export function HowItWorks() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{STEPS.map((s, i) => (
|
||||
<div
|
||||
key={s.n}
|
||||
className="card"
|
||||
style={{
|
||||
position: 'relative',
|
||||
padding: 'clamp(1.5rem, 3vw, 2rem)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-1.5rem',
|
||||
right: '-1rem',
|
||||
fontSize: 'clamp(6rem, 12vw, 9rem)',
|
||||
fontWeight: 800,
|
||||
color: 'transparent',
|
||||
WebkitTextStroke: `1px ${s.accent}33`,
|
||||
letterSpacing: '-0.05em',
|
||||
lineHeight: 1,
|
||||
pointerEvents: 'none',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
{s.n}
|
||||
</span>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
color: s.accent,
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.22em',
|
||||
textTransform: 'uppercase',
|
||||
fontWeight: 700,
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 18, height: 1, background: s.accent }} />
|
||||
Step {i + 1}
|
||||
</div>
|
||||
<h3 style={{ margin: 0, fontSize: '1.35rem', fontWeight: 600, letterSpacing: '-0.01em' }}>{s.title}</h3>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', lineHeight: 1.65, fontSize: '0.95rem' }}>{s.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
73
src/components/robotics/IndustryUseCases.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
'use client';
|
||||
|
||||
import { INDUSTRIES, type Industry } from '@/data/industries';
|
||||
|
||||
const ICONS: Record<Industry['icon'], string> = {
|
||||
building: 'M3 21V8l9-5 9 5v13h-6v-6h-6v6H3Z',
|
||||
utensils: 'M4 2h2v9a2 2 0 1 1-4 0V2h2Zm9 0h2v7l2 2v11h-2v-9h-2Zm-3 0h2v9a2 2 0 1 1-4 0V2h2Z',
|
||||
hotel: 'M2 21V7h20v14h-2v-3H4v3H2Zm5-7a3 3 0 1 1 0-6 3 3 0 0 1 0 6Zm5 0V8h8v6h-8Z',
|
||||
'shopping-bag': 'M6 6V5a6 6 0 0 1 12 0v1h3v15H3V6h3Zm2 0h8V5a4 4 0 0 0-8 0v1Z',
|
||||
'heart-pulse': 'M12 21s-7-4.5-9-9a5 5 0 0 1 9-3 5 5 0 0 1 9 3c-2 4.5-9 9-9 9Zm-2-8 2-3 2 5 2-3h3',
|
||||
'graduation-cap': 'M12 3 2 8l10 5 10-5-10-5Zm-7 7v4l7 3 7-3v-4l-7 3-7-3Z',
|
||||
shield: 'M12 2 4 5v6c0 5 3.5 9 8 11 4.5-2 8-6 8-11V5l-8-3Z',
|
||||
warehouse: 'M3 21V9l9-5 9 5v12H3Zm5-9h8v8H8v-8Z',
|
||||
sparkles: 'M12 2l2 6 6 2-6 2-2 6-2-6-6-2 6-2 2-6Zm7 12l1 3 3 1-3 1-1 3-1-3-3-1 3-1 1-3Z',
|
||||
landmark: 'M2 21v-2h20v2H2Zm2-4v-8h2v8H4Zm4 0v-8h2v8H8Zm4 0v-8h2v8h-2Zm4 0v-8h2v8h-2ZM12 2l10 5H2l10-5Z',
|
||||
};
|
||||
|
||||
export function IndustryUseCases({ limit }: { limit?: number }) {
|
||||
const list = limit ? INDUSTRIES.slice(0, limit) : INDUSTRIES;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(280px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{list.map((i) => (
|
||||
<div key={i.id} className="card" style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.875rem',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: `${i.accent}1A`,
|
||||
border: `1px solid ${i.accent}55`,
|
||||
}}
|
||||
>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill={i.accent} aria-hidden="true">
|
||||
<path d={ICONS[i.icon]} />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 600 }}>{i.name}</h3>
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.68rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#6b6862', marginBottom: 4 }}>Problem</div>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: '0.92rem', lineHeight: 1.55 }}>{i.problem}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.68rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#6b6862', marginBottom: 4 }}>Solution</div>
|
||||
<p style={{ margin: 0, color: '#e8e0cf', fontSize: '0.92rem', lineHeight: 1.55 }}>{i.solution}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ fontSize: '0.68rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: i.accent, marginBottom: 4 }}>Benefit</div>
|
||||
<p style={{ margin: 0, color: '#ffffff', fontSize: '0.92rem', lineHeight: 1.55 }}>{i.benefit}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
src/components/robotics/InquiryForm.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { CATEGORY_LABELS, type RobotCategory } from '@/data/robots';
|
||||
|
||||
type Props = {
|
||||
defaultRobot?: string;
|
||||
defaultCategory?: RobotCategory | '';
|
||||
intent?: 'inquiry' | 'demo' | 'quotation';
|
||||
};
|
||||
|
||||
const INTENT_LABEL: Record<NonNullable<Props['intent']>, string> = {
|
||||
inquiry: 'Send inquiry',
|
||||
demo: 'Book live demo',
|
||||
quotation: 'Request quotation',
|
||||
};
|
||||
|
||||
export function InquiryForm({ defaultRobot = '', defaultCategory = '', intent = 'inquiry' }: Props) {
|
||||
const [data, setData] = useState({
|
||||
name: '',
|
||||
company: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
robotInterest: defaultRobot,
|
||||
category: defaultCategory as string,
|
||||
message: '',
|
||||
});
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||
|
||||
const onSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setStatus('loading');
|
||||
try {
|
||||
const res = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
phone: data.phone,
|
||||
message:
|
||||
`[${intent.toUpperCase()}] from ${data.company || 'Individual'}\n` +
|
||||
`Robot interest: ${data.robotInterest || 'N/A'}\n` +
|
||||
`Category: ${data.category || 'N/A'}\n\n` +
|
||||
data.message,
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
setStatus('success');
|
||||
setData({ name: '', company: '', email: '', phone: '', robotInterest: defaultRobot, category: defaultCategory as string, message: '' });
|
||||
setTimeout(() => setStatus('idle'), 4500);
|
||||
} else {
|
||||
setStatus('error');
|
||||
setTimeout(() => setStatus('idle'), 4500);
|
||||
}
|
||||
} catch {
|
||||
setStatus('error');
|
||||
setTimeout(() => setStatus('idle'), 4500);
|
||||
}
|
||||
};
|
||||
|
||||
const field: React.CSSProperties = {
|
||||
width: '100%',
|
||||
padding: '0.875rem 1rem',
|
||||
borderRadius: '0.75rem',
|
||||
border: '1px solid rgba(196, 162, 101,0.18)',
|
||||
background: 'rgba(15, 12, 8,0.55)',
|
||||
color: '#f5f1e8',
|
||||
fontSize: '0.95rem',
|
||||
outline: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'border-color 0.2s, background 0.2s',
|
||||
fontFamily: 'inherit',
|
||||
};
|
||||
|
||||
const label: React.CSSProperties = {
|
||||
fontSize: '0.72rem',
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#94908a',
|
||||
fontWeight: 600,
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmit} style={{ display: 'grid', gap: '1rem' }}>
|
||||
{status === 'success' && (
|
||||
<div style={{ padding: '0.875rem 1rem', borderRadius: 12, background: 'rgba(34,197,94,0.1)', border: '1px solid rgba(34,197,94,0.3)', color: '#86efac' }}>
|
||||
Thank you — your message was sent. Our team will reach out shortly.
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && (
|
||||
<div style={{ padding: '0.875rem 1rem', borderRadius: 12, background: 'rgba(239,68,68,0.1)', border: '1px solid rgba(239,68,68,0.3)', color: '#fca5a5' }}>
|
||||
Something went wrong. Please try again, or email info@yslootahtech.com.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(min(220px, 100%), 1fr))' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={label} htmlFor="i-name">Full name</label>
|
||||
<input id="i-name" required style={field} value={data.name} onChange={(e) => setData({ ...data, name: e.target.value })} placeholder="Your name" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={label} htmlFor="i-company">Company</label>
|
||||
<input id="i-company" style={field} value={data.company} onChange={(e) => setData({ ...data, company: e.target.value })} placeholder="Company or organization" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(min(220px, 100%), 1fr))' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={label} htmlFor="i-email">Email</label>
|
||||
<input id="i-email" type="email" required style={field} value={data.email} onChange={(e) => setData({ ...data, email: e.target.value })} placeholder="you@company.com" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={label} htmlFor="i-phone">Phone / WhatsApp</label>
|
||||
<input id="i-phone" style={field} value={data.phone} onChange={(e) => setData({ ...data, phone: e.target.value })} placeholder="+971 …" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(min(220px, 100%), 1fr))' }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={label} htmlFor="i-robot">Robot of interest</label>
|
||||
<input id="i-robot" style={field} value={data.robotInterest} onChange={(e) => setData({ ...data, robotInterest: e.target.value })} placeholder="e.g. Unitree Go2, BellaBot" />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={label} htmlFor="i-cat">Preferred category</label>
|
||||
<select id="i-cat" style={field} value={data.category} onChange={(e) => setData({ ...data, category: e.target.value })}>
|
||||
<option value="">Select a category</option>
|
||||
{(Object.keys(CATEGORY_LABELS) as RobotCategory[]).map((c) => (
|
||||
<option key={c} value={c}>{CATEGORY_LABELS[c]}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<label style={label} htmlFor="i-msg">Project details</label>
|
||||
<textarea id="i-msg" required rows={4} style={{ ...field, resize: 'vertical', fontFamily: 'inherit' }} value={data.message} onChange={(e) => setData({ ...data, message: e.target.value })} placeholder="Tell us about your use case, timeline, and venue." />
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.75rem', alignItems: 'center', marginTop: '0.25rem' }}>
|
||||
<button type="submit" disabled={status === 'loading'} className="btn btn-primary" style={{ opacity: status === 'loading' ? 0.6 : 1, cursor: status === 'loading' ? 'wait' : 'pointer' }}>
|
||||
{status === 'loading' ? 'Sending…' : INTENT_LABEL[intent]}
|
||||
</button>
|
||||
<a href="https://wa.me/971559482728" target="_blank" rel="noopener noreferrer" className="btn btn-ghost">
|
||||
WhatsApp us
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
109
src/components/robotics/MarqueeStrip.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Item = {
|
||||
label: string;
|
||||
accent?: string;
|
||||
sub?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_ITEMS: Item[] = [
|
||||
{ label: 'Exclusive UAE Access', sub: 'YS Lootah Robotics', accent: '#e0c896' },
|
||||
{ label: 'Unitree', sub: 'Quadruped · Humanoid', accent: '#c4a265' },
|
||||
{ label: 'Pudu Robotics', sub: 'Service · Delivery · Cleaning', accent: '#8b6f47' },
|
||||
{ label: 'Dubai · UAE', sub: 'Sales · Demo · Configure', accent: '#e0c896' },
|
||||
{ label: 'Showroom', sub: 'City Bay Business Center', accent: '#c4a265' },
|
||||
{ label: '10+ Industries', sub: 'Hospitality · Security · Healthcare', accent: '#8b6f47' },
|
||||
];
|
||||
|
||||
export function MarqueeStrip({ items = DEFAULT_ITEMS, speed = 38 }: { items?: Item[]; speed?: number }) {
|
||||
const loop = [...items, ...items];
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
maskImage: 'linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent)',
|
||||
WebkitMaskImage: 'linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent)',
|
||||
padding: '1.25rem 0',
|
||||
borderTop: '1px solid rgba(196, 162, 101,0.12)',
|
||||
borderBottom: '1px solid rgba(196, 162, 101,0.12)',
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(15, 12, 8,0.4), rgba(5, 5, 5,0.6))',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="marquee-track"
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: 'clamp(2rem, 4vw, 3.5rem)',
|
||||
width: 'max-content',
|
||||
animation: `marqueeScroll ${speed}s linear infinite`,
|
||||
}}
|
||||
>
|
||||
{loop.map((it, idx) => (
|
||||
<div
|
||||
key={`${it.label}-${idx}`}
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
flex: 'none',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={{
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 999,
|
||||
background: it.accent ?? '#e0c896',
|
||||
boxShadow: `0 0 12px ${it.accent ?? '#e0c896'}88`,
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 'clamp(1rem, 2.4vw, 1.35rem)',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '-0.01em',
|
||||
color: '#f0ebe0',
|
||||
}}
|
||||
>
|
||||
{it.label}
|
||||
</span>
|
||||
{it.sub && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.22em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#6b6862',
|
||||
}}
|
||||
>
|
||||
{it.sub}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes marqueeScroll {
|
||||
from { transform: translate3d(0, 0, 0); }
|
||||
to { transform: translate3d(-50%, 0, 0); }
|
||||
}
|
||||
.marquee-track:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.marquee-track {
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
src/components/robotics/ProductFilterTabs.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export type Tab = { id: string; label: string; accent?: string };
|
||||
|
||||
type Props = {
|
||||
tabs: Tab[];
|
||||
activeId: string;
|
||||
onChange: (id: string) => void;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
export function ProductFilterTabs({ tabs, activeId, onChange, title }: Props) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem', minWidth: 0 }}>
|
||||
{title && (
|
||||
<span style={{ fontSize: '0.68rem', letterSpacing: '0.22em', textTransform: 'uppercase', color: '#94908a', fontWeight: 700 }}>
|
||||
{title}
|
||||
</span>
|
||||
)}
|
||||
<div role="tablist" aria-label={title} style={{ display: 'flex', flexWrap: 'wrap', gap: '0.4rem' }}>
|
||||
{tabs.map((t) => {
|
||||
const active = t.id === activeId;
|
||||
return (
|
||||
<button
|
||||
key={t.id}
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
type="button"
|
||||
onClick={() => onChange(t.id)}
|
||||
style={{
|
||||
padding: '0.45rem 0.85rem',
|
||||
borderRadius: 999,
|
||||
fontSize: '0.78rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.06em',
|
||||
textTransform: 'uppercase',
|
||||
border: '1px solid',
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
background: active ? (t.accent ? `${t.accent}1F` : 'rgba(196, 162, 101,0.18)') : 'rgba(15, 12, 8,0.55)',
|
||||
borderColor: active ? (t.accent ?? '#e0c896') : 'rgba(196, 162, 101,0.2)',
|
||||
color: active ? (t.accent ?? '#e0c896') : '#cbc4b3',
|
||||
}}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
79
src/components/robotics/ProductGallery.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
images: { src: string; alt: string }[];
|
||||
accent: string;
|
||||
};
|
||||
|
||||
export function ProductGallery({ images, accent }: Props) {
|
||||
const [active, setActive] = useState(0);
|
||||
if (images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
aspectRatio: '4 / 5',
|
||||
borderRadius: '1.5rem',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
`radial-gradient(ellipse 70% 60% at 50% 50%, ${accent}33, transparent 60%), linear-gradient(180deg, rgba(15, 12, 8,0.7), rgba(5, 5, 5,0.95))`,
|
||||
border: '1px solid rgba(196, 162, 101,0.18)',
|
||||
boxShadow: '0 30px 100px rgba(0, 0, 0,0.6)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'linear-gradient(rgba(196, 162, 101,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(196, 162, 101,0.05) 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px',
|
||||
maskImage: 'radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 80%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 80%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<Image
|
||||
src={images[active].src}
|
||||
alt={images[active].alt}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
priority
|
||||
style={{ objectFit: 'contain', padding: 'clamp(1.5rem, 4vw, 3rem)' }}
|
||||
/>
|
||||
</div>
|
||||
{images.length > 1 && (
|
||||
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{images.map((img, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
type="button"
|
||||
onClick={() => setActive(idx)}
|
||||
aria-label={`Show image ${idx + 1}`}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
cursor: 'pointer',
|
||||
background: 'rgba(15, 12, 8,0.6)',
|
||||
border: `1px solid ${idx === active ? accent : 'rgba(196, 162, 101,0.2)'}`,
|
||||
padding: 4,
|
||||
transition: 'all 0.2s',
|
||||
}}
|
||||
>
|
||||
<Image src={img.src} alt={img.alt} fill sizes="72px" style={{ objectFit: 'contain', padding: 4 }} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/components/robotics/ProductSpecTable.tsx
Normal file
@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import type { RobotSpec } from '@/data/robots';
|
||||
|
||||
export function ProductSpecTable({ specs }: { specs: RobotSpec[] }) {
|
||||
return (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse', color: '#cbc4b3' }}>
|
||||
<tbody>
|
||||
{specs.map((s) => (
|
||||
<tr key={s.label} style={{ borderBottom: '1px solid rgba(196, 162, 101,0.1)' }}>
|
||||
<td
|
||||
style={{
|
||||
padding: '0.65rem 0',
|
||||
fontSize: '0.74rem',
|
||||
letterSpacing: '0.14em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#94908a',
|
||||
width: '46%',
|
||||
verticalAlign: 'top',
|
||||
}}
|
||||
>
|
||||
{s.label}
|
||||
</td>
|
||||
<td style={{ padding: '0.65rem 0', color: '#f5f1e8', fontSize: '0.92rem' }}>{s.value}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
113
src/components/robotics/RobotCategoryGrid.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { CATEGORY_LABELS, ROBOTS, type RobotCategory } from '@/data/robots';
|
||||
|
||||
const CATEGORY_META: { id: RobotCategory; description: string; accent: string; iconPath: string }[] = [
|
||||
{
|
||||
id: 'humanoid',
|
||||
description: 'Bipedal robots for events, education, hospitality, and research.',
|
||||
accent: '#e0c896',
|
||||
iconPath: 'M12 2a4 4 0 0 1 4 4v2h-8V6a4 4 0 0 1 4-4Zm-6 8h12v4l-2 2v6h-3v-5h-2v5H8v-6l-2-2v-4Z',
|
||||
},
|
||||
{
|
||||
id: 'quadruped',
|
||||
description: 'Four-legged robots for inspection, security, and field operations.',
|
||||
accent: '#c4a265',
|
||||
iconPath: 'M3 11l3-3h4l2-2 2 2h4l3 3v3l-2 1v3h-3v-3h-3l-1 2-1-2H8v3H5v-3l-2-1v-3Z',
|
||||
},
|
||||
{
|
||||
id: 'service',
|
||||
description: 'Customer-facing robots that greet, guide, and assist.',
|
||||
accent: '#8b6f47',
|
||||
iconPath: 'M12 2a5 5 0 0 1 5 5v3a5 5 0 0 1-10 0V7a5 5 0 0 1 5-5Zm-7 17a7 7 0 0 1 14 0v3H5v-3Z',
|
||||
},
|
||||
{
|
||||
id: 'delivery',
|
||||
description: 'Autonomous delivery for restaurants, hotels, and back-of-house.',
|
||||
accent: '#e0c896',
|
||||
iconPath: 'M3 6h11v9H3V6Zm14 3h3l2 3v3h-2a2 2 0 1 1-4 0h-1V9h2ZM6 18a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm12 0a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z',
|
||||
},
|
||||
{
|
||||
id: 'hospitality',
|
||||
description: 'Premium hospitality robotics for hotels, resorts, and venues.',
|
||||
accent: '#8b6f47',
|
||||
iconPath: 'M4 7h16v3H4V7Zm0 5h16v8h-3v-3H7v3H4v-8Z',
|
||||
},
|
||||
{
|
||||
id: 'cleaning',
|
||||
description: 'Commercial cleaning robots for malls, airports, and offices.',
|
||||
accent: '#c4a265',
|
||||
iconPath: 'M5 4h6v8H5V4Zm0 10h6v2H5v-2Zm-2 4h10v3H3v-3Zm12-14h3l3 9v9h-3v-7h-3V4Z',
|
||||
},
|
||||
{
|
||||
id: 'commercial',
|
||||
description: 'Commercial automation for warehouses, sites, and facilities.',
|
||||
accent: '#e0c896',
|
||||
iconPath: 'M3 21V8l9-5 9 5v13h-6v-6h-6v6H3Z',
|
||||
},
|
||||
{
|
||||
id: 'inspection',
|
||||
description: 'Inspection robots for energy, utilities, and industrial sites.',
|
||||
accent: '#c4a265',
|
||||
iconPath: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm0 4a6 6 0 1 1 0 12 6 6 0 0 1 0-12Zm0 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z',
|
||||
},
|
||||
];
|
||||
|
||||
export function RobotCategoryGrid() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(240px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{CATEGORY_META.map((cat) => {
|
||||
const count = ROBOTS.filter((r) => r.categories.includes(cat.id)).length;
|
||||
return (
|
||||
<Link
|
||||
key={cat.id}
|
||||
href={`/robots/?category=${cat.id}`}
|
||||
className="card"
|
||||
style={{
|
||||
padding: '1.5rem',
|
||||
textDecoration: 'none',
|
||||
color: '#f5f1e8',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0.875rem',
|
||||
minHeight: 200,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: `${cat.accent}1A`,
|
||||
border: `1px solid ${cat.accent}55`,
|
||||
}}
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill={cat.accent} aria-hidden="true">
|
||||
<path d={cat.iconPath} />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '1.05rem', fontWeight: 600 }}>{CATEGORY_LABELS[cat.id]}</h3>
|
||||
<span style={{ fontSize: '0.72rem', color: cat.accent, letterSpacing: '0.18em', textTransform: 'uppercase' }}>
|
||||
{count} models
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ margin: 0, fontSize: '0.88rem', color: '#94908a', lineHeight: 1.55 }}>
|
||||
{cat.description}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
216
src/components/robotics/RobotProductCard.tsx
Normal file
@ -0,0 +1,216 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { Robot, CATEGORY_LABELS } from '@/data/robots';
|
||||
|
||||
type Props = {
|
||||
robot: Robot;
|
||||
priority?: boolean;
|
||||
};
|
||||
|
||||
export function RobotProductCard({ robot, priority = false }: Props) {
|
||||
const detailHref = `/robots/${robot.slug}/`;
|
||||
|
||||
return (
|
||||
<article
|
||||
className="card"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100%',
|
||||
color: '#f5f1e8',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={detailHref}
|
||||
aria-label={`View details for ${robot.brandLabel} ${robot.name}`}
|
||||
style={{
|
||||
position: 'relative',
|
||||
display: 'block',
|
||||
aspectRatio: '1 / 1',
|
||||
background:
|
||||
`radial-gradient(ellipse 70% 50% at 50% 60%, ${robot.accent}26 0%, transparent 60%), linear-gradient(180deg, rgba(15, 12, 8,0.7), rgba(5, 5, 5,0.95))`,
|
||||
overflow: 'hidden',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
aria-hidden
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(196, 162, 101,0.05) 1px, transparent 1px), linear-gradient(90deg, rgba(196, 162, 101,0.05) 1px, transparent 1px)',
|
||||
backgroundSize: '32px 32px',
|
||||
maskImage: 'radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 80%)',
|
||||
WebkitMaskImage: 'radial-gradient(ellipse 60% 60% at 50% 50%, #000 30%, transparent 80%)',
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
<Image
|
||||
src={robot.image}
|
||||
alt={`${robot.brandLabel} ${robot.name}`}
|
||||
fill
|
||||
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
|
||||
style={{
|
||||
objectFit: 'contain',
|
||||
padding: '2rem',
|
||||
transition: 'transform 0.6s cubic-bezier(0.16,1,0.3,1)',
|
||||
}}
|
||||
priority={priority}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
left: '1rem',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
padding: '0.4rem 0.7rem',
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(5, 5, 5,0.7)',
|
||||
border: `1px solid ${robot.accent}55`,
|
||||
fontSize: '0.7rem',
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#e8e0cf',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
>
|
||||
<span style={{ width: 6, height: 6, borderRadius: 999, background: robot.accent }} />
|
||||
{robot.brandLabel}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '1rem',
|
||||
right: '1rem',
|
||||
padding: '0.35rem 0.7rem',
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(255,255,255,0.05)',
|
||||
border: '1px solid rgba(196, 162, 101,0.2)',
|
||||
fontSize: '0.66rem',
|
||||
letterSpacing: '0.18em',
|
||||
textTransform: 'uppercase',
|
||||
color: '#cbc4b3',
|
||||
backdropFilter: 'blur(8px)',
|
||||
}}
|
||||
>
|
||||
{CATEGORY_LABELS[robot.category]}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div style={{ padding: '1.5rem', display: 'flex', flexDirection: 'column', gap: '0.75rem', flex: 1 }}>
|
||||
<h3 style={{ margin: 0, fontSize: '1.2rem', fontWeight: 600, letterSpacing: '-0.01em' }}>
|
||||
<Link href={detailHref} style={{ color: '#f5f1e8', textDecoration: 'none' }}>
|
||||
{robot.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p style={{ margin: 0, color: '#94908a', fontSize: '0.88rem', lineHeight: 1.55, flex: 1 }}>
|
||||
{robot.shortDescription}
|
||||
</p>
|
||||
|
||||
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexWrap: 'wrap', gap: '0.35rem' }}>
|
||||
{robot.useCases.slice(0, 2).map((u) => (
|
||||
<li
|
||||
key={u}
|
||||
style={{
|
||||
fontSize: '0.7rem',
|
||||
color: '#94908a',
|
||||
padding: '0.2rem 0.5rem',
|
||||
borderRadius: 999,
|
||||
border: '1px solid rgba(196, 162, 101,0.18)',
|
||||
background: 'rgba(15, 12, 8,0.5)',
|
||||
letterSpacing: '0.04em',
|
||||
}}
|
||||
>
|
||||
{u}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: 'auto',
|
||||
paddingTop: '0.75rem',
|
||||
borderTop: '1px solid rgba(196, 162, 101,0.1)',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '0.4rem',
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
href={detailHref}
|
||||
className="card-cta card-cta-primary"
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.4rem',
|
||||
padding: '0.55rem 0.75rem',
|
||||
borderRadius: 10,
|
||||
background: `${robot.accent}1F`,
|
||||
border: `1px solid ${robot.accent}66`,
|
||||
color: robot.accent,
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
View details
|
||||
</Link>
|
||||
<Link
|
||||
href={`${detailHref}#inquire`}
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0.55rem 0.75rem',
|
||||
borderRadius: 10,
|
||||
background: 'transparent',
|
||||
border: '1px solid rgba(196, 162, 101,0.2)',
|
||||
color: '#cbc4b3',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Request price
|
||||
</Link>
|
||||
<Link
|
||||
href="/book-demo/"
|
||||
style={{
|
||||
flex: '1 1 auto',
|
||||
minWidth: 0,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '0.55rem 0.75rem',
|
||||
borderRadius: 10,
|
||||
background: 'transparent',
|
||||
border: '1px solid rgba(196, 162, 101,0.2)',
|
||||
color: '#cbc4b3',
|
||||
fontSize: '0.72rem',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Book demo
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
63
src/components/robotics/WhyUs.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
'use client';
|
||||
|
||||
const REASONS = [
|
||||
{
|
||||
title: 'Exclusive UAE portfolio',
|
||||
body: 'Selected Unitree and Pudu Robotics solutions — available exclusively in the UAE through YS Lootah Robotics for hospitality, security, education, and industry.',
|
||||
accent: '#e0c896',
|
||||
icon: 'M3 3h7v7H3V3Zm0 11h7v7H3v-7Zm11-11h7v7h-7V3Zm0 11h7v7h-7v-7Z',
|
||||
},
|
||||
{
|
||||
title: 'Dubai sales and support',
|
||||
body: 'A local Dubai team manages your inquiry, demo, deployment, and on-the-ground support — not just a website with a contact form.',
|
||||
accent: '#c4a265',
|
||||
icon: 'M12 2 2 7v6c0 5 4 9 10 11 6-2 10-6 10-11V7l-10-5Z',
|
||||
},
|
||||
{
|
||||
title: 'Custom configuration',
|
||||
body: 'Configure persona, attire, and accessories for humanoid robots — visualize your deployment before you order with our 3D configurator.',
|
||||
accent: '#8b6f47',
|
||||
icon: 'M12 4a4 4 0 0 1 4 4v1h5v6h-5v1a4 4 0 0 1-8 0v-1H3V9h5V8a4 4 0 0 1 4-4Z',
|
||||
},
|
||||
{
|
||||
title: 'End-to-end deployment',
|
||||
body: 'From procurement and configuration to delivery, installation, training, and ongoing support — we handle the full lifecycle.',
|
||||
accent: '#e0c896',
|
||||
icon: 'M3 12h4l3-7 4 14 3-7h4',
|
||||
},
|
||||
];
|
||||
|
||||
export function WhyUs() {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gap: '1rem',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(min(260px, 100%), 1fr))',
|
||||
}}
|
||||
>
|
||||
{REASONS.map((r) => (
|
||||
<div key={r.title} className="card" style={{ padding: '1.75rem', display: 'flex', flexDirection: 'column', gap: '0.875rem' }}>
|
||||
<div
|
||||
style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 14,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: `${r.accent}1A`,
|
||||
border: `1px solid ${r.accent}55`,
|
||||
}}
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke={r.accent} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<path d={r.icon} />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 style={{ margin: 0, fontSize: '1.1rem', fontWeight: 600 }}>{r.title}</h3>
|
||||
<p style={{ margin: 0, color: '#cbc4b3', fontSize: '0.92rem', lineHeight: 1.6 }}>{r.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
62
src/components/ui/CursorSpotlight.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
color?: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function CursorSpotlight({ color = 'rgba(196, 162, 101, 0.18)', size = 520, className = '' }: Props) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(null);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const finePointer = window.matchMedia('(pointer: fine)').matches;
|
||||
const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (!finePointer || reduced) return;
|
||||
setEnabled(true);
|
||||
|
||||
const el = ref.current?.parentElement;
|
||||
if (!el) return;
|
||||
let raf = 0;
|
||||
const onMove = (e: MouseEvent) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
cancelAnimationFrame(raf);
|
||||
raf = requestAnimationFrame(() => setPos({ x, y }));
|
||||
};
|
||||
const onLeave = () => setPos(null);
|
||||
el.addEventListener('mousemove', onMove);
|
||||
el.addEventListener('mouseleave', onLeave);
|
||||
return () => {
|
||||
el.removeEventListener('mousemove', onMove);
|
||||
el.removeEventListener('mouseleave', onLeave);
|
||||
cancelAnimationFrame(raf);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enabled) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
aria-hidden
|
||||
className={className}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
pointerEvents: 'none',
|
||||
opacity: pos ? 1 : 0,
|
||||
transition: 'opacity 0.4s ease',
|
||||
background: pos
|
||||
? `radial-gradient(${size}px ${size}px at ${pos.x}px ${pos.y}px, ${color}, transparent 70%)`
|
||||
: 'transparent',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
src/components/ui/GlassPanel.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
glow?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export function GlassPanel({ className = '', children, glow = false, style }: Props) {
|
||||
return (
|
||||
<div
|
||||
className={`glass metallic-border ${className}`}
|
||||
style={{
|
||||
borderRadius: '1.25rem',
|
||||
padding: '1.5rem',
|
||||
boxShadow: glow ? '0 12px 60px rgba(0, 0, 0,0.5), 0 0 60px rgba(196, 162, 101,0.18)' : undefined,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/components/ui/MotionSection.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
id?: string;
|
||||
delay?: number;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export function MotionSection({ children, className = '', id, delay = 0, style }: Props) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (reduce) {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
const obs = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const e of entries) {
|
||||
if (e.isIntersecting) {
|
||||
setVisible(true);
|
||||
obs.disconnect();
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.12 }
|
||||
);
|
||||
obs.observe(el);
|
||||
return () => obs.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section
|
||||
ref={ref}
|
||||
id={id}
|
||||
className={className}
|
||||
style={{
|
||||
opacity: visible ? 1 : 0,
|
||||
transform: visible ? 'translateY(0)' : 'translateY(28px)',
|
||||
transition: `opacity 0.9s cubic-bezier(0.16,1,0.3,1) ${delay}s, transform 0.9s cubic-bezier(0.16,1,0.3,1) ${delay}s`,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
55
src/components/ui/PremiumButton.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import React from 'react';
|
||||
|
||||
type Variant = 'primary' | 'ghost' | 'outline';
|
||||
|
||||
type CommonProps = {
|
||||
variant?: Variant;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
iconRight?: React.ReactNode;
|
||||
};
|
||||
|
||||
type AsButton = CommonProps & { href?: undefined } & React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
type AsLink = CommonProps & { href: string; target?: string; rel?: string };
|
||||
|
||||
export function PremiumButton(props: AsButton | AsLink) {
|
||||
const { variant = 'primary', className = '', children, iconRight, ...rest } = props as CommonProps & Record<string, unknown>;
|
||||
const cls = `btn btn-${variant} ${className}`.trim();
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
<span>{children}</span>
|
||||
{iconRight ?? (
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<polyline points="12 5 19 12 12 19" />
|
||||
</svg>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if ('href' in props && props.href) {
|
||||
const isExternal = /^https?:/.test(props.href);
|
||||
if (isExternal) {
|
||||
return (
|
||||
<a className={cls} href={props.href} target={(rest as { target?: string }).target ?? '_blank'} rel={(rest as { rel?: string }).rel ?? 'noopener noreferrer'}>
|
||||
{inner}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link className={cls} href={props.href}>
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={cls} {...(rest as React.ButtonHTMLAttributes<HTMLButtonElement>)}>
|
||||
{inner}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
141
src/data/industries.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import type { RobotCategory } from './robots';
|
||||
|
||||
export interface Industry {
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
problem: string;
|
||||
solution: string;
|
||||
benefit: string;
|
||||
matchCategories: RobotCategory[];
|
||||
accent: string;
|
||||
icon: 'building' | 'utensils' | 'hotel' | 'shopping-bag' | 'heart-pulse' | 'graduation-cap' | 'shield' | 'warehouse' | 'sparkles' | 'landmark';
|
||||
}
|
||||
|
||||
export const INDUSTRIES: Industry[] = [
|
||||
{
|
||||
id: 'hospitality',
|
||||
slug: 'hospitality',
|
||||
name: 'Hospitality',
|
||||
problem: 'Staff shortages and rising expectations stretch guest service teams thin.',
|
||||
solution: 'Service and delivery robots that greet, deliver, and assist around the clock.',
|
||||
benefit: 'Consistent guest experience, lower turnover impact, premium brand positioning.',
|
||||
matchCategories: ['hospitality', 'service', 'delivery'],
|
||||
accent: '#e0c896',
|
||||
icon: 'hotel',
|
||||
},
|
||||
{
|
||||
id: 'restaurants',
|
||||
slug: 'restaurants',
|
||||
name: 'Restaurants & Cafés',
|
||||
problem: 'Peak-hour service quality drops as servers shuttle trays across the floor.',
|
||||
solution: 'Delivery robots like BellaBot and KettyBot move food fast and reliably.',
|
||||
benefit: 'Faster table turns, happier staff, instagrammable brand moment.',
|
||||
matchCategories: ['delivery', 'service', 'hospitality'],
|
||||
accent: '#8b6f47',
|
||||
icon: 'utensils',
|
||||
},
|
||||
{
|
||||
id: 'hotels',
|
||||
slug: 'hotels',
|
||||
name: 'Hotels & Resorts',
|
||||
problem: 'Room service and back-of-house logistics burn operational hours.',
|
||||
solution: 'Multi-floor delivery robots and humanoid concierge experiences.',
|
||||
benefit: 'Faster service, lower overhead, signature guest moments.',
|
||||
matchCategories: ['delivery', 'hospitality', 'service'],
|
||||
accent: '#c4a265',
|
||||
icon: 'hotel',
|
||||
},
|
||||
{
|
||||
id: 'malls',
|
||||
slug: 'shopping-malls',
|
||||
name: 'Shopping Malls',
|
||||
problem: 'Cleaning at scale, customer wayfinding, and brand activations are hard to staff.',
|
||||
solution: 'Cleaning robots, service robots, and humanoid activations for retail floors.',
|
||||
benefit: 'Cleaner venues, memorable customer experiences, modern positioning.',
|
||||
matchCategories: ['cleaning', 'service', 'humanoid'],
|
||||
accent: '#e0c896',
|
||||
icon: 'shopping-bag',
|
||||
},
|
||||
{
|
||||
id: 'healthcare',
|
||||
slug: 'healthcare',
|
||||
name: 'Healthcare',
|
||||
problem: 'Clinical staff spend valuable time on non-clinical tasks like transport and delivery.',
|
||||
solution: 'Autonomous delivery robots for medications, meals, and supplies.',
|
||||
benefit: 'More clinician time at the bedside, lower contamination risk, predictable logistics.',
|
||||
matchCategories: ['delivery', 'service'],
|
||||
accent: '#c4a265',
|
||||
icon: 'heart-pulse',
|
||||
},
|
||||
{
|
||||
id: 'education',
|
||||
slug: 'education',
|
||||
name: 'Education',
|
||||
problem: 'Schools need engaging, hands-on tools to teach AI and robotics today.',
|
||||
solution: 'Unitree humanoids and quadrupeds for STEM labs and innovation programs.',
|
||||
benefit: 'Future-ready students, signature programs, and stronger STEM outcomes.',
|
||||
matchCategories: ['humanoid', 'service'],
|
||||
accent: '#8b6f47',
|
||||
icon: 'graduation-cap',
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
slug: 'security',
|
||||
name: 'Security & Surveillance',
|
||||
problem: 'Manual patrols are repetitive, costly, and miss anomalies after hours.',
|
||||
solution: 'Quadruped robots like Unitree Go2 and B2 patrol autonomously.',
|
||||
benefit: 'Continuous coverage, audit trails, and consistent perimeter monitoring.',
|
||||
matchCategories: ['quadruped', 'commercial'],
|
||||
accent: '#e0c896',
|
||||
icon: 'shield',
|
||||
},
|
||||
{
|
||||
id: 'warehouses',
|
||||
slug: 'warehouses',
|
||||
name: 'Warehouses & Logistics',
|
||||
problem: 'Inventory transport, picking, and inspection take human hours that scale poorly.',
|
||||
solution: 'Quadruped inspection robots and autonomous delivery platforms.',
|
||||
benefit: 'Higher throughput, fewer errors, and accurate facility monitoring.',
|
||||
matchCategories: ['quadruped', 'delivery', 'commercial'],
|
||||
accent: '#c4a265',
|
||||
icon: 'warehouse',
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
slug: 'events',
|
||||
name: 'Events & Brand Activations',
|
||||
problem: 'Brands need premium, share-worthy moments to stand out at events.',
|
||||
solution: 'Humanoid and quadruped robot showcases configured for your brand.',
|
||||
benefit: 'Press coverage, viral content, and unmistakable premium positioning.',
|
||||
matchCategories: ['humanoid', 'quadruped'],
|
||||
accent: '#8b6f47',
|
||||
icon: 'sparkles',
|
||||
},
|
||||
{
|
||||
id: 'smart-buildings',
|
||||
slug: 'smart-buildings',
|
||||
name: 'Smart Buildings',
|
||||
problem: 'Operations leaders struggle to unify cleaning, security, and logistics in modern buildings.',
|
||||
solution: 'A multi-robot fleet across cleaning, delivery, and patrol functions.',
|
||||
benefit: 'Single operational system, predictable costs, future-ready facility.',
|
||||
matchCategories: ['cleaning', 'commercial', 'delivery'],
|
||||
accent: '#e0c896',
|
||||
icon: 'building',
|
||||
},
|
||||
{
|
||||
id: 'government',
|
||||
slug: 'government',
|
||||
name: 'Government & Public Spaces',
|
||||
problem: 'Government innovation programs need flagship robotics that signal global ambition.',
|
||||
solution: 'A curated multi-brand robotics portfolio with full local support.',
|
||||
benefit: 'Visible innovation leadership and a UAE-supported deployment model.',
|
||||
matchCategories: ['humanoid', 'quadruped', 'service'],
|
||||
accent: '#c4a265',
|
||||
icon: 'landmark',
|
||||
},
|
||||
];
|
||||
|
||||
export function getIndustryBySlug(slug: string): Industry | undefined {
|
||||
return INDUSTRIES.find((i) => i.slug === slug);
|
||||
}
|
||||
418
src/data/robots.ts
Normal file
@ -0,0 +1,418 @@
|
||||
export type RobotBrand = 'unitree' | 'pudu';
|
||||
export type RobotCategory =
|
||||
| 'humanoid'
|
||||
| 'quadruped'
|
||||
| 'service'
|
||||
| 'delivery'
|
||||
| 'hospitality'
|
||||
| 'commercial'
|
||||
| 'cleaning'
|
||||
| 'inspection';
|
||||
|
||||
export interface RobotSpec {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Robot {
|
||||
id: string;
|
||||
slug: string;
|
||||
brand: RobotBrand;
|
||||
brandLabel: string;
|
||||
name: string;
|
||||
tagline: string;
|
||||
category: RobotCategory;
|
||||
categories: RobotCategory[];
|
||||
shortDescription: string;
|
||||
longDescription: string;
|
||||
features: string[];
|
||||
useCases: string[];
|
||||
specs: RobotSpec[];
|
||||
image: string;
|
||||
imageType: 'photo' | 'placeholder';
|
||||
accent: string;
|
||||
officialUrl?: string;
|
||||
configureSlug?: string;
|
||||
}
|
||||
|
||||
/* Gold spectrum for accents (no cyan/blue/violet) */
|
||||
const GOLD_CHAMPAGNE = '#e0c896';
|
||||
const GOLD_BRAND = '#c4a265';
|
||||
const GOLD_BRONZE = '#8b6f47';
|
||||
|
||||
export const BRANDS: Record<RobotBrand, { name: string; tagline: string; description: string; url: string; accent: string }> = {
|
||||
unitree: {
|
||||
name: 'Unitree Robotics',
|
||||
tagline: 'Quadrupeds and humanoids — available exclusively in the UAE through YS Lootah Robotics.',
|
||||
description:
|
||||
'High-performance bipedal and four-legged robots engineered for mobility, perception, and real-world deployment. Selected Unitree solutions are available exclusively in the UAE through YS Lootah Robotics — with on-the-ground sales, demo, and deployment support in Dubai.',
|
||||
url: 'https://www.unitree.com/',
|
||||
accent: GOLD_CHAMPAGNE,
|
||||
},
|
||||
pudu: {
|
||||
name: 'Pudu Robotics',
|
||||
tagline: 'Service, delivery, and cleaning robotics — available exclusively in the UAE through YS Lootah Robotics.',
|
||||
description:
|
||||
'A global leader in commercial service robotics. Selected Pudu Robotics solutions are available exclusively in the UAE through YS Lootah Robotics for restaurants, hotels, malls, hospitals, and offices.',
|
||||
url: 'https://www.pudurobotics.com/',
|
||||
accent: GOLD_BRONZE,
|
||||
},
|
||||
};
|
||||
|
||||
export const CATEGORY_LABELS: Record<RobotCategory, string> = {
|
||||
humanoid: 'Humanoid',
|
||||
quadruped: 'Quadruped',
|
||||
service: 'Service',
|
||||
delivery: 'Delivery',
|
||||
hospitality: 'Hospitality',
|
||||
commercial: 'Commercial Automation',
|
||||
cleaning: 'Cleaning',
|
||||
inspection: 'Inspection',
|
||||
};
|
||||
|
||||
const SPEC_PLACEHOLDER: RobotSpec = { label: 'Detailed Specs', value: 'Available upon request' };
|
||||
const SPEC_CONSULT: RobotSpec = { label: 'Configuration', value: 'Specifications confirmed during consultation' };
|
||||
|
||||
export const ROBOTS: Robot[] = [
|
||||
{
|
||||
id: 'unitree-g1',
|
||||
slug: 'unitree-g1',
|
||||
brand: 'unitree',
|
||||
brandLabel: 'Unitree',
|
||||
name: 'G1 Humanoid',
|
||||
tagline: 'Compact bipedal robot built for research, demos, and human-scale environments.',
|
||||
category: 'humanoid',
|
||||
categories: ['humanoid'],
|
||||
shortDescription:
|
||||
'A research-grade humanoid with agile locomotion, dexterous hands, and a developer-friendly platform — available exclusively in the UAE through YS Lootah Robotics.',
|
||||
longDescription:
|
||||
'The G1 is a next-generation bipedal humanoid designed for AI research, education, and real-world deployment. It can walk, run, balance, and manipulate objects. Through our exclusive UAE access, YS Lootah Robotics delivers fully tailored persona configurations and on-the-ground deployment support across Dubai and the UAE.',
|
||||
features: [
|
||||
'Compact bipedal form factor for indoor environments',
|
||||
'Dynamic balance and high-mobility locomotion',
|
||||
'Dexterous hand options for manipulation research',
|
||||
'Open developer SDK for custom behaviors',
|
||||
'Configurable persona — kandura, suit, vest, doctor, security guard',
|
||||
],
|
||||
useCases: [
|
||||
'Robotics R&D and AI training labs',
|
||||
'Brand activations and live events',
|
||||
'Education and university research',
|
||||
'Hospitality concierge and reception',
|
||||
'Government and innovation showrooms',
|
||||
],
|
||||
specs: [
|
||||
{ label: 'Type', value: 'Bipedal humanoid' },
|
||||
{ label: 'Configurator', value: 'Persona, attire, color, accessories' },
|
||||
SPEC_CONSULT,
|
||||
SPEC_PLACEHOLDER,
|
||||
],
|
||||
image: '/images/robots/unitree-g1.png',
|
||||
imageType: 'photo',
|
||||
accent: GOLD_CHAMPAGNE,
|
||||
officialUrl: 'https://www.unitree.com/g1',
|
||||
configureSlug: '/configure/',
|
||||
},
|
||||
{
|
||||
id: 'unitree-h2',
|
||||
slug: 'unitree-h2',
|
||||
brand: 'unitree',
|
||||
brandLabel: 'Unitree',
|
||||
name: 'H2 Humanoid',
|
||||
tagline: 'Full-size humanoid platform for advanced motion, perception, and field work.',
|
||||
category: 'humanoid',
|
||||
categories: ['humanoid'],
|
||||
shortDescription:
|
||||
'Full-size bipedal humanoid engineered for athletic motion, heavy-duty research, and demanding deployment scenarios — available exclusively in the UAE through YS Lootah Robotics.',
|
||||
longDescription:
|
||||
'The Unitree H2 is built for organizations that need a robust, full-scale humanoid — combining powerful actuators with high-bandwidth perception. Ideal for advanced labs and industrial pilots across the UAE, with full local support from YS Lootah Robotics.',
|
||||
features: [
|
||||
'Full adult-scale humanoid frame',
|
||||
'High-torque actuators for dynamic motion',
|
||||
'Advanced perception and stability stack',
|
||||
'Targeted at research, industry, and pilots',
|
||||
],
|
||||
useCases: ['Advanced robotics research', 'Industrial automation pilots', 'Innovation labs', 'Public showcases'],
|
||||
specs: [
|
||||
{ label: 'Type', value: 'Full-size bipedal humanoid' },
|
||||
SPEC_CONSULT,
|
||||
SPEC_PLACEHOLDER,
|
||||
],
|
||||
image: '/images/robots/unitree-h2.png',
|
||||
imageType: 'photo',
|
||||
accent: GOLD_CHAMPAGNE,
|
||||
officialUrl: 'https://www.unitree.com/h2',
|
||||
},
|
||||
{
|
||||
id: 'unitree-r1',
|
||||
slug: 'unitree-r1',
|
||||
brand: 'unitree',
|
||||
brandLabel: 'Unitree',
|
||||
name: 'R1 Humanoid',
|
||||
tagline: 'Next-generation humanoid for accessible robotics research.',
|
||||
category: 'humanoid',
|
||||
categories: ['humanoid'],
|
||||
shortDescription:
|
||||
'A purpose-built humanoid that opens advanced robotics to startups, educators, and innovators — available exclusively in the UAE through YS Lootah Robotics.',
|
||||
longDescription:
|
||||
'Engineered for accessibility without compromising capability, the R1 brings advanced humanoid robotics within reach of startups, educators, and innovators across the UAE.',
|
||||
features: ['Compact form factor', 'Mobility-focused design', 'Developer-friendly stack', 'Open integration paths'],
|
||||
useCases: ['Startup R&D', 'Education', 'Public demonstrations'],
|
||||
specs: [SPEC_CONSULT, SPEC_PLACEHOLDER],
|
||||
image: '/images/robots/unitree-r1.png',
|
||||
imageType: 'photo',
|
||||
accent: GOLD_BRAND,
|
||||
officialUrl: 'https://www.unitree.com/',
|
||||
},
|
||||
{
|
||||
id: 'unitree-go2',
|
||||
slug: 'unitree-go2',
|
||||
brand: 'unitree',
|
||||
brandLabel: 'Unitree',
|
||||
name: 'Go2 Quadruped',
|
||||
tagline: 'Agile four-legged robot for inspection, security, and mobile AI.',
|
||||
category: 'quadruped',
|
||||
categories: ['quadruped', 'commercial', 'inspection'],
|
||||
shortDescription:
|
||||
'A versatile quadruped platform with high-bandwidth perception — available exclusively in the UAE through YS Lootah Robotics for inspection, security, and mobile AI deployments.',
|
||||
longDescription:
|
||||
'Unitree Go2 is a compact quadruped designed for mobility-first applications. With a powerful AI stack, LiDAR options, and developer SDK, it is the most widely deployed legged robot in commercial robotics worldwide.',
|
||||
features: [
|
||||
'Stable, agile quadruped locomotion',
|
||||
'Optional 4D LiDAR perception',
|
||||
'Open SDK for autonomy stacks',
|
||||
'Long battery life for field operations',
|
||||
],
|
||||
useCases: [
|
||||
'Facility and infrastructure inspection',
|
||||
'Security and surveillance patrol',
|
||||
'Robotics research and education',
|
||||
'Live entertainment and brand activations',
|
||||
],
|
||||
specs: [
|
||||
{ label: 'Type', value: 'Quadruped' },
|
||||
{ label: 'Payload Options', value: 'Cameras, LiDAR, sensors' },
|
||||
SPEC_CONSULT,
|
||||
SPEC_PLACEHOLDER,
|
||||
],
|
||||
image: '/images/robots/unitree-go2.png',
|
||||
imageType: 'photo',
|
||||
accent: GOLD_CHAMPAGNE,
|
||||
officialUrl: 'https://www.unitree.com/go2',
|
||||
},
|
||||
{
|
||||
id: 'unitree-b2',
|
||||
slug: 'unitree-b2',
|
||||
brand: 'unitree',
|
||||
brandLabel: 'Unitree',
|
||||
name: 'B2 Industrial Quadruped',
|
||||
tagline: 'Heavy-duty quadruped engineered for harsh industrial environments.',
|
||||
category: 'quadruped',
|
||||
categories: ['quadruped', 'commercial', 'inspection'],
|
||||
shortDescription:
|
||||
'A rugged industrial-grade quadruped — available exclusively in the UAE through YS Lootah Robotics for energy, utilities, and infrastructure operators.',
|
||||
longDescription:
|
||||
'The Unitree B2 brings industrial reliability to legged robotics. With higher payload capacity and IP-rated build quality, it suits energy, utilities, and infrastructure operators across the UAE.',
|
||||
features: [
|
||||
'Industrial payload capacity',
|
||||
'IP-rated rugged design',
|
||||
'Long-range autonomous patrol',
|
||||
'Modular sensor stack',
|
||||
],
|
||||
useCases: [
|
||||
'Energy and utilities inspection',
|
||||
'Infrastructure monitoring',
|
||||
'Security in industrial sites',
|
||||
'Asset and facility management',
|
||||
],
|
||||
specs: [
|
||||
{ label: 'Type', value: 'Industrial quadruped' },
|
||||
SPEC_CONSULT,
|
||||
SPEC_PLACEHOLDER,
|
||||
],
|
||||
image: '/images/robots/unitree-b2.png',
|
||||
imageType: 'photo',
|
||||
accent: GOLD_BRAND,
|
||||
officialUrl: 'https://www.unitree.com/b2',
|
||||
},
|
||||
{
|
||||
id: 'unitree-a2',
|
||||
slug: 'unitree-a2',
|
||||
brand: 'unitree',
|
||||
brandLabel: 'Unitree',
|
||||
name: 'A2 Quadruped',
|
||||
tagline: 'Agile mid-size quadruped with extended autonomy.',
|
||||
category: 'quadruped',
|
||||
categories: ['quadruped', 'inspection'],
|
||||
shortDescription:
|
||||
'A balanced quadruped platform — available exclusively in the UAE through YS Lootah Robotics for organizations that need a step up in capability.',
|
||||
longDescription:
|
||||
'The A2 sits between Go2 and B2 — combining longer endurance and a richer sensor payload with a manageable size for UAE deployments.',
|
||||
features: ['Extended autonomy', 'Sensor-rich payload bay', 'Robust outdoor capability'],
|
||||
useCases: ['Site monitoring', 'Inspection at scale', 'Research deployments'],
|
||||
specs: [SPEC_CONSULT, SPEC_PLACEHOLDER],
|
||||
image: '/images/robots/unitree-a2.png',
|
||||
imageType: 'photo',
|
||||
accent: GOLD_CHAMPAGNE,
|
||||
officialUrl: 'https://www.unitree.com/',
|
||||
},
|
||||
{
|
||||
id: 'unitree-as2',
|
||||
slug: 'unitree-as2',
|
||||
brand: 'unitree',
|
||||
brandLabel: 'Unitree',
|
||||
name: 'AS2 Autonomous System',
|
||||
tagline: 'Advanced perception platform for autonomous mobility research.',
|
||||
category: 'commercial',
|
||||
categories: ['commercial', 'inspection'],
|
||||
shortDescription:
|
||||
'A perception-first robotics platform combining LiDAR, vision, and compute — available exclusively in the UAE through YS Lootah Robotics for autonomous mobility research.',
|
||||
longDescription:
|
||||
'AS2 brings together the sensor and compute stack Unitree pairs with its robots — useful for research teams building autonomy on top of legged or wheeled platforms.',
|
||||
features: ['LiDAR + vision fusion', 'On-board compute', 'Modular integration'],
|
||||
useCases: ['Autonomy research', 'Inspection autonomy stacks', 'Mobile robotics labs'],
|
||||
specs: [SPEC_CONSULT, SPEC_PLACEHOLDER],
|
||||
image: '/images/robots/unitree-as2.png',
|
||||
imageType: 'photo',
|
||||
accent: GOLD_BRONZE,
|
||||
officialUrl: 'https://www.unitree.com/',
|
||||
},
|
||||
{
|
||||
id: 'pudu-bellabot',
|
||||
slug: 'pudu-bellabot',
|
||||
brand: 'pudu',
|
||||
brandLabel: 'Pudu',
|
||||
name: 'BellaBot',
|
||||
tagline: 'Premium delivery robot built for restaurants, hotels, and hospitality venues.',
|
||||
category: 'delivery',
|
||||
categories: ['delivery', 'hospitality', 'service'],
|
||||
shortDescription:
|
||||
'A multi-tray delivery robot with an expressive interface — available exclusively in the UAE through YS Lootah Robotics for hospitality venues across Dubai.',
|
||||
longDescription:
|
||||
'BellaBot delivers food, beverages, and goods with charm, efficiency, and remarkable safety performance. Deploy across restaurants, hotels, and venues in Dubai and the UAE with exclusive local support from YS Lootah Robotics.',
|
||||
features: [
|
||||
'Multi-tray delivery configuration',
|
||||
'Expressive animated face interface',
|
||||
'Advanced obstacle avoidance',
|
||||
'Hot-swappable battery for full-day operation',
|
||||
'Fleet management integration',
|
||||
],
|
||||
useCases: [
|
||||
'Restaurants and casual dining',
|
||||
'Hotels and resort room service',
|
||||
'Hospital ward delivery',
|
||||
'Office building catering',
|
||||
],
|
||||
specs: [
|
||||
{ label: 'Type', value: 'Multi-tray delivery robot' },
|
||||
{ label: 'Navigation', value: 'SLAM + vision + LiDAR' },
|
||||
SPEC_CONSULT,
|
||||
SPEC_PLACEHOLDER,
|
||||
],
|
||||
image: '/images/robots/pudu-bellabot.svg',
|
||||
imageType: 'placeholder',
|
||||
accent: GOLD_BRONZE,
|
||||
officialUrl: 'https://www.pudurobotics.com/en',
|
||||
},
|
||||
{
|
||||
id: 'pudu-kettybot',
|
||||
slug: 'pudu-kettybot',
|
||||
brand: 'pudu',
|
||||
brandLabel: 'Pudu',
|
||||
name: 'KettyBot',
|
||||
tagline: 'Compact greeter, advertiser, and delivery robot for retail and dining.',
|
||||
category: 'service',
|
||||
categories: ['service', 'delivery', 'hospitality'],
|
||||
shortDescription:
|
||||
'A slim, billboard-equipped service robot — available exclusively in the UAE through YS Lootah Robotics for high-traffic retail and dining spaces.',
|
||||
longDescription:
|
||||
'KettyBot blends marketing and service. A large display lets venues advertise menus and promotions while the robot greets customers, guides them, and delivers orders — perfect for high-traffic UAE retail and dining spaces.',
|
||||
features: [
|
||||
'Built-in advertising display',
|
||||
'Compact footprint for narrow aisles',
|
||||
'Customer greeting and guiding',
|
||||
'Light delivery tray',
|
||||
],
|
||||
useCases: ['Restaurants', 'Retail showrooms', 'Cafés and quick-service venues', 'Shopping mall activations'],
|
||||
specs: [
|
||||
{ label: 'Type', value: 'Service + delivery robot' },
|
||||
SPEC_CONSULT,
|
||||
SPEC_PLACEHOLDER,
|
||||
],
|
||||
image: '/images/robots/pudu-kettybot.svg',
|
||||
imageType: 'placeholder',
|
||||
accent: GOLD_BRAND,
|
||||
officialUrl: 'https://www.pudurobotics.com/en',
|
||||
},
|
||||
{
|
||||
id: 'pudu-cc1',
|
||||
slug: 'pudu-cc1',
|
||||
brand: 'pudu',
|
||||
brandLabel: 'Pudu',
|
||||
name: 'CC1 Cleaning Robot',
|
||||
tagline: 'Commercial cleaning robot for malls, hotels, and offices.',
|
||||
category: 'cleaning',
|
||||
categories: ['cleaning', 'commercial'],
|
||||
shortDescription:
|
||||
'An autonomous commercial cleaning platform — available exclusively in the UAE through YS Lootah Robotics for malls, airports, hotels, and offices.',
|
||||
longDescription:
|
||||
'CC1 is a multi-mode cleaning robot built for large commercial environments. Deploy across shopping malls, airports, hotels, and offices in the UAE — fully supported by your local Pudu team.',
|
||||
features: [
|
||||
'Multi-mode cleaning: sweep, scrub, mop, vacuum',
|
||||
'Autonomous mapping and routing',
|
||||
'Fleet management for multi-robot ops',
|
||||
'Safety-first obstacle handling',
|
||||
],
|
||||
useCases: ['Shopping malls', 'Airports and transport hubs', 'Hotels and resorts', 'Corporate offices'],
|
||||
specs: [
|
||||
{ label: 'Modes', value: 'Sweep, scrub, mop, vacuum' },
|
||||
SPEC_CONSULT,
|
||||
SPEC_PLACEHOLDER,
|
||||
],
|
||||
image: '/images/robots/pudu-cc1.svg',
|
||||
imageType: 'placeholder',
|
||||
accent: GOLD_BRONZE,
|
||||
officialUrl: 'https://www.pudurobotics.com/en',
|
||||
},
|
||||
{
|
||||
id: 'pudu-d-series',
|
||||
slug: 'pudu-d-series',
|
||||
brand: 'pudu',
|
||||
brandLabel: 'Pudu',
|
||||
name: 'PUDU D-Series',
|
||||
tagline: 'Industrial delivery robots for warehouses and back-of-house logistics.',
|
||||
category: 'delivery',
|
||||
categories: ['delivery', 'commercial'],
|
||||
shortDescription:
|
||||
'Heavy-payload autonomous delivery robots — available exclusively in the UAE through YS Lootah Robotics for warehouses, factories, and back-of-house operations.',
|
||||
longDescription:
|
||||
'The PUDU D-Series powers industrial delivery flows — moving goods, parts, and trays across kitchens, hospitals, and warehouses with autonomous navigation and fleet coordination.',
|
||||
features: [
|
||||
'High payload capacity',
|
||||
'Multi-floor and elevator integration',
|
||||
'Fleet coordination and routing',
|
||||
'Industrial reliability',
|
||||
],
|
||||
useCases: ['Warehouses', 'Hospital logistics', 'Hotel back-of-house', 'Industrial kitchens'],
|
||||
specs: [SPEC_CONSULT, SPEC_PLACEHOLDER],
|
||||
image: '/images/robots/pudu-pudubot.svg',
|
||||
imageType: 'placeholder',
|
||||
accent: GOLD_CHAMPAGNE,
|
||||
officialUrl: 'https://www.pudurobotics.com/en',
|
||||
},
|
||||
];
|
||||
|
||||
export function getRobotBySlug(slug: string): Robot | undefined {
|
||||
return ROBOTS.find((r) => r.slug === slug);
|
||||
}
|
||||
|
||||
export function getRobotsByBrand(brand: RobotBrand): Robot[] {
|
||||
return ROBOTS.filter((r) => r.brand === brand);
|
||||
}
|
||||
|
||||
export function getRobotsByCategory(category: RobotCategory): Robot[] {
|
||||
return ROBOTS.filter((r) => r.categories.includes(category));
|
||||
}
|
||||
|
||||
export const FEATURED_ROBOT_SLUGS = ['unitree-g1', 'unitree-go2', 'pudu-bellabot', 'pudu-kettybot'];
|
||||
export const FEATURED_ROBOTS = FEATURED_ROBOT_SLUGS.map((s) => getRobotBySlug(s)!).filter(Boolean);
|
||||
@ -1,209 +1,209 @@
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
export interface PersonaOption {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
colors: { torso: string; legs: string };
|
||||
modelPath?: string;
|
||||
}
|
||||
|
||||
export interface PersonaState {
|
||||
personas: PersonaOption[];
|
||||
isHydrated: boolean;
|
||||
}
|
||||
|
||||
export interface PersonaActions {
|
||||
addPersona: (persona: Omit<PersonaOption, 'id'> & { id?: string }) => void;
|
||||
removePersona: (id: string) => void;
|
||||
updatePersona: (id: string, updates: Partial<Omit<PersonaOption, 'id'>>) => void;
|
||||
resetPersonas: () => void;
|
||||
hydrate: () => void;
|
||||
}
|
||||
|
||||
export type PersonaStore = PersonaState & PersonaActions;
|
||||
|
||||
export const DEFAULT_PERSONAS: PersonaOption[] = [
|
||||
{
|
||||
id: 'none',
|
||||
label: 'Default',
|
||||
description: 'Original robot appearance',
|
||||
colors: { torso: '#3b82f6', legs: '#3b82f6' },
|
||||
},
|
||||
{
|
||||
id: 'emarati-kandura',
|
||||
label: 'Emarati Kandura',
|
||||
description: 'Traditional white robe attire',
|
||||
colors: { torso: '#f8fafc', legs: '#f8fafc' },
|
||||
},
|
||||
{
|
||||
id: 'industrial-vest',
|
||||
label: 'Industrial Vest',
|
||||
description: 'High-visibility safety vest',
|
||||
colors: { torso: '#f59e0b', legs: '#3b82f6' },
|
||||
},
|
||||
{
|
||||
id: 'business-suit',
|
||||
label: 'Business Suit',
|
||||
description: 'Professional navy suit',
|
||||
colors: { torso: '#1e293b', legs: '#1e293b' },
|
||||
},
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'lootah-personas';
|
||||
|
||||
function loadFromStorage(): PersonaOption[] | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveToStorage(personas: PersonaOption[]) {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(personas));
|
||||
} catch {
|
||||
// Storage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return `persona-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
|
||||
export const personaStore = createStore<PersonaStore>((set, get) => ({
|
||||
personas: DEFAULT_PERSONAS,
|
||||
isHydrated: false,
|
||||
|
||||
addPersona: (persona) => {
|
||||
const newPersona: PersonaOption = {
|
||||
id: persona.id ?? generateId(),
|
||||
label: persona.label,
|
||||
description: persona.description,
|
||||
colors: persona.colors,
|
||||
...(persona.modelPath ? { modelPath: persona.modelPath } : {}),
|
||||
};
|
||||
set((state) => {
|
||||
const updated = [...state.personas, newPersona];
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
},
|
||||
|
||||
removePersona: (id) => {
|
||||
// Prevent removing the default 'none' persona
|
||||
if (id === 'none') return;
|
||||
set((state) => {
|
||||
const updated = state.personas.filter((p) => p.id !== id);
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
},
|
||||
|
||||
updatePersona: (id, updates) => {
|
||||
set((state) => {
|
||||
const updated = state.personas.map((p) =>
|
||||
p.id === id ? { ...p, ...updates } : p
|
||||
);
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
},
|
||||
|
||||
resetPersonas: () => {
|
||||
saveToStorage(DEFAULT_PERSONAS);
|
||||
set({ personas: [...DEFAULT_PERSONAS] });
|
||||
},
|
||||
|
||||
hydrate: () => {
|
||||
// Guard: only hydrate once — prevents race condition duplicates when
|
||||
// called from multiple components at the same time.
|
||||
if (get().isHydrated) return;
|
||||
set({ isHydrated: true });
|
||||
|
||||
const raw = loadFromStorage();
|
||||
// Deduplicate stored personas (keep last occurrence of each id)
|
||||
const deduped = raw
|
||||
? [...new Map(raw.map((p) => [p.id, p])).values()]
|
||||
: null;
|
||||
|
||||
if (deduped && deduped.length > 0) {
|
||||
// Only re-inject truly built-in personas (those still in DEFAULT_PERSONAS) if missing.
|
||||
// Dynamic/uploaded personas that were deleted via the dashboard must NOT be re-added.
|
||||
const storedIds = new Set(deduped.map((s) => s.id));
|
||||
const missing = DEFAULT_PERSONAS.filter((d) => !storedIds.has(d.id));
|
||||
set({ personas: [...deduped, ...missing] });
|
||||
} else {
|
||||
set({ personas: [...DEFAULT_PERSONAS] });
|
||||
}
|
||||
|
||||
// Fetch pricing items from server DB and auto-register personas for all attire items
|
||||
if (typeof window !== 'undefined') {
|
||||
fetch('/api/admin/pricing/')
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const serverItems: { id: string; label: string; modelPath: string | null }[] = data.items ?? [];
|
||||
const current = get().personas;
|
||||
const currentIds = new Set(current.map((p) => p.id));
|
||||
const newPersonas: PersonaOption[] = [];
|
||||
// Items that should not appear as selectable personas
|
||||
const excludeIds = new Set(['base', 'custom-color']);
|
||||
|
||||
serverItems.forEach(({ id, label, modelPath }) => {
|
||||
if (excludeIds.has(id)) return;
|
||||
if (currentIds.has(id)) {
|
||||
// Update modelPath if it changed
|
||||
const existing = current.find((p) => p.id === id);
|
||||
if (existing && modelPath && existing.modelPath !== modelPath) {
|
||||
set((state) => ({
|
||||
personas: state.personas.map((p) =>
|
||||
p.id === id ? { ...p, modelPath } : p
|
||||
),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Auto-create a persona entry for every pricing item
|
||||
newPersonas.push({
|
||||
id,
|
||||
label,
|
||||
description: label,
|
||||
colors: { torso: '#3b82f6', legs: '#3b82f6' },
|
||||
...(modelPath ? { modelPath } : {}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (newPersonas.length > 0) {
|
||||
set((state) => {
|
||||
// Deduplicate: merge by id, new personas take precedence for modelPath
|
||||
const merged = new Map(state.personas.map((p) => [p.id, p]));
|
||||
newPersonas.forEach((p) => { if (!merged.has(p.id)) merged.set(p.id, p); });
|
||||
const updated = [...merged.values()];
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
} else {
|
||||
// Save current state to localStorage so it persists
|
||||
saveToStorage(get().personas);
|
||||
}
|
||||
})
|
||||
.catch(() => {}); // silent — use local data as fallback
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export const usePersonaStore = <T>(selector: (state: PersonaStore) => T): T => {
|
||||
return useSyncExternalStore(
|
||||
personaStore.subscribe,
|
||||
() => selector(personaStore.getState()),
|
||||
() => selector(personaStore.getState())
|
||||
);
|
||||
};
|
||||
import { createStore } from 'zustand/vanilla';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
|
||||
export interface PersonaOption {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
colors: { torso: string; legs: string };
|
||||
modelPath?: string;
|
||||
}
|
||||
|
||||
export interface PersonaState {
|
||||
personas: PersonaOption[];
|
||||
isHydrated: boolean;
|
||||
}
|
||||
|
||||
export interface PersonaActions {
|
||||
addPersona: (persona: Omit<PersonaOption, 'id'> & { id?: string }) => void;
|
||||
removePersona: (id: string) => void;
|
||||
updatePersona: (id: string, updates: Partial<Omit<PersonaOption, 'id'>>) => void;
|
||||
resetPersonas: () => void;
|
||||
hydrate: () => void;
|
||||
}
|
||||
|
||||
export type PersonaStore = PersonaState & PersonaActions;
|
||||
|
||||
export const DEFAULT_PERSONAS: PersonaOption[] = [
|
||||
{
|
||||
id: 'none',
|
||||
label: 'Default',
|
||||
description: 'Original robot appearance',
|
||||
colors: { torso: '#3b82f6', legs: '#3b82f6' },
|
||||
},
|
||||
{
|
||||
id: 'emarati-kandura',
|
||||
label: 'Emarati Kandura',
|
||||
description: 'Traditional white robe attire',
|
||||
colors: { torso: '#f5f1e8', legs: '#f5f1e8' },
|
||||
},
|
||||
{
|
||||
id: 'industrial-vest',
|
||||
label: 'Industrial Vest',
|
||||
description: 'High-visibility safety vest',
|
||||
colors: { torso: '#f59e0b', legs: '#3b82f6' },
|
||||
},
|
||||
{
|
||||
id: 'business-suit',
|
||||
label: 'Business Suit',
|
||||
description: 'Professional navy suit',
|
||||
colors: { torso: '#1e293b', legs: '#1e293b' },
|
||||
},
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'lootah-personas';
|
||||
|
||||
function loadFromStorage(): PersonaOption[] | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
return parsed;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveToStorage(personas: PersonaOption[]) {
|
||||
if (typeof window === 'undefined') return;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(personas));
|
||||
} catch {
|
||||
// Storage unavailable
|
||||
}
|
||||
}
|
||||
|
||||
function generateId(): string {
|
||||
return `persona-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
||||
}
|
||||
|
||||
export const personaStore = createStore<PersonaStore>((set, get) => ({
|
||||
personas: DEFAULT_PERSONAS,
|
||||
isHydrated: false,
|
||||
|
||||
addPersona: (persona) => {
|
||||
const newPersona: PersonaOption = {
|
||||
id: persona.id ?? generateId(),
|
||||
label: persona.label,
|
||||
description: persona.description,
|
||||
colors: persona.colors,
|
||||
...(persona.modelPath ? { modelPath: persona.modelPath } : {}),
|
||||
};
|
||||
set((state) => {
|
||||
const updated = [...state.personas, newPersona];
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
},
|
||||
|
||||
removePersona: (id) => {
|
||||
// Prevent removing the default 'none' persona
|
||||
if (id === 'none') return;
|
||||
set((state) => {
|
||||
const updated = state.personas.filter((p) => p.id !== id);
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
},
|
||||
|
||||
updatePersona: (id, updates) => {
|
||||
set((state) => {
|
||||
const updated = state.personas.map((p) =>
|
||||
p.id === id ? { ...p, ...updates } : p
|
||||
);
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
},
|
||||
|
||||
resetPersonas: () => {
|
||||
saveToStorage(DEFAULT_PERSONAS);
|
||||
set({ personas: [...DEFAULT_PERSONAS] });
|
||||
},
|
||||
|
||||
hydrate: () => {
|
||||
// Guard: only hydrate once — prevents race condition duplicates when
|
||||
// called from multiple components at the same time.
|
||||
if (get().isHydrated) return;
|
||||
set({ isHydrated: true });
|
||||
|
||||
const raw = loadFromStorage();
|
||||
// Deduplicate stored personas (keep last occurrence of each id)
|
||||
const deduped = raw
|
||||
? [...new Map(raw.map((p) => [p.id, p])).values()]
|
||||
: null;
|
||||
|
||||
if (deduped && deduped.length > 0) {
|
||||
// Only re-inject truly built-in personas (those still in DEFAULT_PERSONAS) if missing.
|
||||
// Dynamic/uploaded personas that were deleted via the dashboard must NOT be re-added.
|
||||
const storedIds = new Set(deduped.map((s) => s.id));
|
||||
const missing = DEFAULT_PERSONAS.filter((d) => !storedIds.has(d.id));
|
||||
set({ personas: [...deduped, ...missing] });
|
||||
} else {
|
||||
set({ personas: [...DEFAULT_PERSONAS] });
|
||||
}
|
||||
|
||||
// Fetch pricing items from server DB and auto-register personas for all attire items
|
||||
if (typeof window !== 'undefined') {
|
||||
fetch('/api/admin/pricing/')
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
const serverItems: { id: string; label: string; modelPath: string | null }[] = data.items ?? [];
|
||||
const current = get().personas;
|
||||
const currentIds = new Set(current.map((p) => p.id));
|
||||
const newPersonas: PersonaOption[] = [];
|
||||
// Items that should not appear as selectable personas
|
||||
const excludeIds = new Set(['base', 'custom-color']);
|
||||
|
||||
serverItems.forEach(({ id, label, modelPath }) => {
|
||||
if (excludeIds.has(id)) return;
|
||||
if (currentIds.has(id)) {
|
||||
// Update modelPath if it changed
|
||||
const existing = current.find((p) => p.id === id);
|
||||
if (existing && modelPath && existing.modelPath !== modelPath) {
|
||||
set((state) => ({
|
||||
personas: state.personas.map((p) =>
|
||||
p.id === id ? { ...p, modelPath } : p
|
||||
),
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
// Auto-create a persona entry for every pricing item
|
||||
newPersonas.push({
|
||||
id,
|
||||
label,
|
||||
description: label,
|
||||
colors: { torso: '#3b82f6', legs: '#3b82f6' },
|
||||
...(modelPath ? { modelPath } : {}),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (newPersonas.length > 0) {
|
||||
set((state) => {
|
||||
// Deduplicate: merge by id, new personas take precedence for modelPath
|
||||
const merged = new Map(state.personas.map((p) => [p.id, p]));
|
||||
newPersonas.forEach((p) => { if (!merged.has(p.id)) merged.set(p.id, p); });
|
||||
const updated = [...merged.values()];
|
||||
saveToStorage(updated);
|
||||
return { personas: updated };
|
||||
});
|
||||
} else {
|
||||
// Save current state to localStorage so it persists
|
||||
saveToStorage(get().personas);
|
||||
}
|
||||
})
|
||||
.catch(() => {}); // silent — use local data as fallback
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
export const usePersonaStore = <T>(selector: (state: PersonaStore) => T): T => {
|
||||
return useSyncExternalStore(
|
||||
personaStore.subscribe,
|
||||
() => selector(personaStore.getState()),
|
||||
() => selector(personaStore.getState())
|
||||
);
|
||||
};
|
||||
|
||||
@ -36,6 +36,7 @@
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
"node_modules",
|
||||
"references"
|
||||
]
|
||||
}
|
||||
|
||||