- Introduced FounderSection component to highlight leadership and vision. - Created ServicesGrid component to display various robotics services offered. - Developed RoboticsScrollShowcase for showcasing robots with interactive elements. - Implemented RoboticsSplineShowcase featuring a 3D Spline scene for enhanced user experience. - Added reusable Card component for consistent styling across sections. - Integrated ContainerScroll for animated scrolling effects in the showcase. - Built SplineScene component for lazy loading Spline 3D scenes. - Added Spotlight component for interactive hover effects. - Created utility function for class name merging to streamline styling.
336 lines
14 KiB
TypeScript
336 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useScroll, useTransform, motion } from 'framer-motion';
|
|
import { ReactNode, useEffect, useState } from 'react';
|
|
import Link from 'next/link';
|
|
|
|
interface SectionProps {
|
|
children: ReactNode;
|
|
progress: any;
|
|
startAt: number;
|
|
peakAt: number;
|
|
endAt: number;
|
|
align: 'center' | 'left' | 'right';
|
|
verticalAlign?: 'top' | 'center' | 'bottom';
|
|
offsetY?: number;
|
|
className?: string;
|
|
}
|
|
|
|
function OverlaySection({
|
|
children,
|
|
progress,
|
|
startAt,
|
|
peakAt,
|
|
endAt,
|
|
align,
|
|
verticalAlign = 'center',
|
|
offsetY = 50,
|
|
className = '',
|
|
}: SectionProps) {
|
|
// Define a wide "plateau" zone where the text is fully readable and static.
|
|
// We use 30% of the travel distance for fading in, 30% for fading out.
|
|
const diffIn = peakAt - startAt;
|
|
const diffOut = endAt - peakAt;
|
|
|
|
const inStart = startAt;
|
|
const inEnd = startAt + diffIn * 0.4; // Fades in quickly
|
|
const outStart = endAt - diffOut * 0.6; // Holds for a long time
|
|
const outEnd = endAt;
|
|
|
|
// Smooth fade in and out mapping over a lengthy plateau
|
|
const opacity = useTransform(
|
|
progress,
|
|
[inStart, inEnd, outStart, outEnd],
|
|
[0, 1, 1, 0]
|
|
);
|
|
|
|
// Translate Y to slide in, pause entirely while reading, slide out
|
|
const y = useTransform(
|
|
progress,
|
|
[inStart, inEnd, outStart, outEnd],
|
|
[offsetY, 0, 0, -offsetY]
|
|
);
|
|
|
|
// Scale stays at 1.0 during reading mode
|
|
const scale = useTransform(
|
|
progress,
|
|
[inStart, inEnd, outStart, outEnd],
|
|
[0.95, 1, 1, 1.05]
|
|
);
|
|
|
|
const alignStyle: React.CSSProperties =
|
|
align === 'left'
|
|
? { alignItems: 'flex-start' }
|
|
: align === 'right'
|
|
? { alignItems: 'flex-end' }
|
|
: { left: '50%', transform: 'translateX(-50%)', alignItems: 'center' };
|
|
|
|
const alignClass =
|
|
align === 'left'
|
|
? 'overlay-section-left'
|
|
: align === 'right'
|
|
? 'overlay-section-right'
|
|
: 'overlay-section-center';
|
|
|
|
const verticalStyle: React.CSSProperties =
|
|
verticalAlign === 'top'
|
|
? { top: '15vh' }
|
|
: verticalAlign === 'bottom'
|
|
? { bottom: '15vh', top: 'auto' }
|
|
: { top: '50%' };
|
|
|
|
// Glass panel appearance for side text to not clash with the robot
|
|
const isCenter = align === 'center';
|
|
const panelStyle: React.CSSProperties = isCenter
|
|
? { textAlign: 'center' }
|
|
: {
|
|
background: 'rgba(255, 255, 255, 0.92)',
|
|
backdropFilter: 'blur(20px)',
|
|
WebkitBackdropFilter: 'blur(20px)',
|
|
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.08)',
|
|
border: '1px solid rgba(255, 255, 255, 0.6)',
|
|
textAlign: align === 'left' ? 'left' : 'right',
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
className={`${alignClass} ${className}`.trim()}
|
|
style={{
|
|
position: 'absolute',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
opacity,
|
|
pointerEvents: 'none',
|
|
...verticalStyle,
|
|
...alignStyle,
|
|
}}
|
|
>
|
|
<motion.div
|
|
style={{
|
|
y,
|
|
scale,
|
|
...panelStyle,
|
|
}}
|
|
className={`will-change-transform ${isCenter ? '' : 'overlay-panel'}`}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
</motion.div>
|
|
);
|
|
}
|
|
|
|
// Section timing definitions to match ScrollScene camera moves
|
|
const SECTION_CONFIGS = [
|
|
{ id: 'brand', startAt: 0, peakAt: 0.05, endAt: 0.15, align: 'center' as const, verticalAlign: 'top' as const },
|
|
{ id: 'hero', startAt: 0.10, peakAt: 0.22, endAt: 0.35, align: 'left' as const, verticalAlign: 'center' as const },
|
|
{ id: 'headReveal', startAt: 0.35, peakAt: 0.46, endAt: 0.53, align: 'right' as const, verticalAlign: 'center' as const },
|
|
{ id: 'customization', startAt: 0.55, peakAt: 0.66, endAt: 0.77, align: 'left' as const, verticalAlign: 'center' as const },
|
|
{ id: 'mobility', startAt: 0.75, peakAt: 0.82, endAt: 0.90, align: 'right' as const, verticalAlign: 'center' as const },
|
|
];
|
|
|
|
export function ScrollOverlays() {
|
|
const { scrollYProgress } = useScroll();
|
|
|
|
// Dynamically load personas from the pricing API so any admin-added attire shows here
|
|
const [attireItems, setAttireItems] = useState<{ label: string; id: string }[]>([
|
|
{ label: 'Kandura', id: 'emarati-kandura' },
|
|
{ label: 'Vest', id: 'industrial-vest' },
|
|
{ label: 'Suit', id: 'business-suit' },
|
|
]);
|
|
|
|
useEffect(() => {
|
|
fetch('/api/admin/pricing/')
|
|
.then((r) => r.json())
|
|
.then((data) => {
|
|
const excluded = new Set(['base', 'custom-color', 'emarati-kandura', 'industrial-vest', 'business-suit']);
|
|
const extras: { label: string; id: string }[] = (data.items ?? [])
|
|
.filter((item: { id: string; label: string; modelPath?: string | null }) =>
|
|
!excluded.has(item.id) && item.modelPath
|
|
)
|
|
.map((item: { id: string; label: string }) => ({ label: item.label, id: item.id }));
|
|
setAttireItems([
|
|
{ label: 'Kandura', id: 'emarati-kandura' },
|
|
{ label: 'Vest', id: 'industrial-vest' },
|
|
{ label: 'Suit', id: 'business-suit' },
|
|
...extras,
|
|
]);
|
|
})
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
position: 'fixed',
|
|
inset: 0,
|
|
zIndex: 10,
|
|
pointerEvents: 'none',
|
|
overflow: 'hidden',
|
|
}}
|
|
>
|
|
{/* 1. Brand Intro */}
|
|
<OverlaySection progress={scrollYProgress} {...SECTION_CONFIGS[0]} className="overlay-brand">
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '1rem', marginBottom: '1rem' }}>
|
|
<div style={{ width: '30px', height: '1px', background: 'linear-gradient(90deg, transparent, #273F94)' }} />
|
|
<span style={{ fontSize: '0.75rem', fontWeight: 600, color: '#273F94', letterSpacing: '0.4em', textTransform: 'uppercase' }}>
|
|
YS Lootah Robotics
|
|
</span>
|
|
<div style={{ width: '30px', height: '1px', background: 'linear-gradient(90deg, #273F94, transparent)' }} />
|
|
</div>
|
|
<p style={{ fontSize: '0.9rem', color: '#6a73a5', 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: '#0a0a0c', lineHeight: 1.0, letterSpacing: '-0.04em', margin: 0 }}>
|
|
The Future
|
|
</motion.h1>
|
|
<motion.h1 className="overlay-hero-heading" style={{ fontWeight: 200, color: '#0a0a0c', lineHeight: 1.0, letterSpacing: '-0.04em', margin: '0.1em 0 0' }}>
|
|
of <span style={{ color: '#273F94', fontWeight: 400 }}>Robotics</span>
|
|
</motion.h1>
|
|
<p style={{ fontSize: '1rem', color: '#475569', lineHeight: 1.7, margin: '1.5rem 0 0', fontWeight: 300 }}>
|
|
Meet the G1 Humanoid Robot. Fully customizable, enterprise-ready, designed for the world of tomorrow.
|
|
</p>
|
|
</OverlaySection>
|
|
|
|
{/* 3. Head Reveal */}
|
|
<OverlaySection progress={scrollYProgress} {...SECTION_CONFIGS[2]}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
|
<div style={{ width: '50px', height: '2px', background: '#273F94', marginBottom: '1.5rem' }} />
|
|
<div style={{ fontSize: '0.65rem', fontWeight: 600, color: '#273F94', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
|
Intelligent by Design
|
|
</div>
|
|
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#0a0a0c', 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 }}>
|
|
Advanced computer vision and neural processing. The G1 sees, interprets, and responds to the world in real-time.
|
|
</p>
|
|
<div style={{ display: 'flex', gap: '2.5rem', marginTop: '2rem' }}>
|
|
<div style={{ textAlign: 'center' }}>
|
|
<div className="overlay-stat" style={{ fontWeight: 300, color: '#0a0a0c', fontFamily: 'monospace' }}>360°</div>
|
|
<div style={{ fontSize: '0.65rem', color: '#6a73a5', 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: '#0a0a0c', fontFamily: 'monospace' }}>{'<'}50ms</div>
|
|
<div style={{ fontSize: '0.65rem', color: '#6a73a5', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Response Time</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</OverlaySection>
|
|
|
|
{/* 4. Customization */}
|
|
<OverlaySection progress={scrollYProgress} {...SECTION_CONFIGS[3]}>
|
|
<div style={{ width: '50px', height: '2px', background: '#273F94', marginBottom: '1.5rem' }} />
|
|
<div style={{ fontSize: '0.65rem', fontWeight: 600, color: '#273F94', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
|
Your Identity
|
|
</div>
|
|
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#0a0a0c', 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 }}>
|
|
From traditional Emarati Kandura to industrial safety gear and professional business attire. Configure every detail to match your brand.
|
|
</p>
|
|
<div style={{ display: 'flex', gap: '1rem', marginTop: '2rem', flexWrap: 'wrap' }}>
|
|
{attireItems.map((item) => (
|
|
<div
|
|
key={item.id}
|
|
onClick={() => {
|
|
import('@/store/useConfigStore').then(({ configStore }) => {
|
|
configStore.getState().setPersonaAttire(item.id);
|
|
});
|
|
}}
|
|
onMouseEnter={() => {
|
|
import('@/store/useConfigStore').then(({ configStore }) => {
|
|
configStore.getState().setPersonaAttire(item.id);
|
|
});
|
|
}}
|
|
style={{
|
|
padding: '0.6rem 1.25rem',
|
|
borderRadius: '2rem',
|
|
background: 'rgba(255, 255, 255, 0.5)',
|
|
border: '1px solid rgba(39, 63, 148, 0.3)',
|
|
fontSize: '0.75rem',
|
|
color: '#0a0a0c',
|
|
letterSpacing: '0.1em',
|
|
fontWeight: 600,
|
|
cursor: 'pointer',
|
|
transition: 'all 0.3s ease',
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.05)',
|
|
pointerEvents: 'auto',
|
|
userSelect: 'none',
|
|
WebkitTapHighlightColor: 'transparent',
|
|
}}
|
|
onMouseOver={(e) => {
|
|
e.currentTarget.style.background = 'var(--color-gold)';
|
|
e.currentTarget.style.color = '#ffffff';
|
|
e.currentTarget.style.transform = 'translateY(-2px)';
|
|
e.currentTarget.style.boxShadow = '0 6px 16px rgba(39, 63, 148, 0.3)';
|
|
}}
|
|
onMouseOut={(e) => {
|
|
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.5)';
|
|
e.currentTarget.style.color = '#0a0a0c';
|
|
e.currentTarget.style.transform = 'translateY(0)';
|
|
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.05)';
|
|
}}
|
|
>
|
|
{item.label}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</OverlaySection>
|
|
|
|
{/* 5. Mobility */}
|
|
<OverlaySection progress={scrollYProgress} {...SECTION_CONFIGS[4]}>
|
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end' }}>
|
|
<div style={{ width: '50px', height: '2px', background: '#273F94', marginBottom: '1.5rem' }} />
|
|
<div style={{ fontSize: '0.65rem', fontWeight: 600, color: '#273F94', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
|
Advanced Mobility
|
|
</div>
|
|
<h2 className="overlay-heading" style={{ fontWeight: 300, color: '#0a0a0c', 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 }}>
|
|
State-of-the-art locomotion enabling natural human-like movement, balance, and agility across any terrain.
|
|
</p>
|
|
<div style={{ display: 'flex', gap: '2.5rem', marginTop: '2rem' }}>
|
|
<div style={{ textAlign: 'center' }}>
|
|
<div className="overlay-stat" style={{ fontWeight: 300, color: '#0a0a0c', fontFamily: 'monospace' }}>2m/s</div>
|
|
<div style={{ fontSize: '0.65rem', color: '#6a73a5', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Max Speed</div>
|
|
</div>
|
|
<div style={{ textAlign: 'center' }}>
|
|
<div className="overlay-stat" style={{ fontWeight: 300, color: '#0a0a0c', fontFamily: 'monospace' }}>127kg</div>
|
|
<div style={{ fontSize: '0.65rem', color: '#6a73a5', letterSpacing: '0.15em', textTransform: 'uppercase', marginTop: '0.5rem' }}>Payload</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</OverlaySection>
|
|
|
|
|
|
|
|
{/* Scroll indicator mapped to vanish rapidly when scrolled */}
|
|
<motion.div
|
|
className="overlay-scroll-hint"
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: '2.5rem',
|
|
left: '50%',
|
|
x: '-50%',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
alignItems: 'center',
|
|
opacity: useTransform(scrollYProgress, [0, 0.05], [1, 0]),
|
|
}}
|
|
>
|
|
<span style={{ fontSize: '0.65rem', fontWeight: 500, color: '#6a73a5', letterSpacing: '0.3em', textTransform: 'uppercase', marginBottom: '1rem' }}>
|
|
Scroll to Explore
|
|
</span>
|
|
<div className="scroll-indicator" />
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|