feat: implement 3D components and create a coming soon page with a memory match game
This commit is contained in:
169
src/app/coming-soon/page.tsx
Normal file
169
src/app/coming-soon/page.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, Trophy, Bike, Zap, Battery, Shield } from 'lucide-react';
|
||||
|
||||
const ICONS = [Bike, Zap, Battery, Shield, Bike, Zap, Battery, Shield];
|
||||
|
||||
export default function ComingSoonPage() {
|
||||
const router = useRouter();
|
||||
|
||||
// Mini Game State (Memory Match)
|
||||
const [cards, setCards] = useState<{ id: number; Icon: any; isFlipped: boolean; isMatched: boolean }[]>([]);
|
||||
const [flippedIndices, setFlippedIndices] = useState<number[]>([]);
|
||||
const [moves, setMoves] = useState(0);
|
||||
const [matches, setMatches] = useState(0);
|
||||
const [won, setWon] = useState(false);
|
||||
|
||||
// Countdown State
|
||||
const [timeLeft, setTimeLeft] = useState({ days: 0, hours: 0, minutes: 0, seconds: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
startNewGame();
|
||||
|
||||
const targetDate = new Date('2026-08-01T00:00:00').getTime();
|
||||
|
||||
const updateCountdown = () => {
|
||||
const now = new Date().getTime();
|
||||
const distance = targetDate - now;
|
||||
|
||||
if (distance > 0) {
|
||||
setTimeLeft({
|
||||
days: Math.floor(distance / (1000 * 60 * 60 * 24)),
|
||||
hours: Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
|
||||
minutes: Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60)),
|
||||
seconds: Math.floor((distance % (1000 * 60)) / 1000),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
updateCountdown();
|
||||
const interval = setInterval(updateCountdown, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const startNewGame = () => {
|
||||
const shuffled = [...ICONS]
|
||||
.sort(() => Math.random() - 0.5)
|
||||
.map((Icon, index) => ({ id: index, Icon, isFlipped: false, isMatched: false }));
|
||||
setCards(shuffled);
|
||||
setFlippedIndices([]);
|
||||
setMoves(0);
|
||||
setMatches(0);
|
||||
setWon(false);
|
||||
};
|
||||
|
||||
const handleCardClick = (index: number) => {
|
||||
if (flippedIndices.length === 2) return;
|
||||
if (cards[index].isFlipped || cards[index].isMatched) return;
|
||||
|
||||
const newCards = [...cards];
|
||||
newCards[index].isFlipped = true;
|
||||
setCards(newCards);
|
||||
|
||||
const newFlippedIndices = [...flippedIndices, index];
|
||||
setFlippedIndices(newFlippedIndices);
|
||||
|
||||
if (newFlippedIndices.length === 2) {
|
||||
setMoves(moves + 1);
|
||||
const [firstIndex, secondIndex] = newFlippedIndices;
|
||||
if (cards[firstIndex].Icon === cards[secondIndex].Icon) {
|
||||
setTimeout(() => {
|
||||
const matchedCards = [...newCards];
|
||||
matchedCards[firstIndex].isMatched = true;
|
||||
matchedCards[secondIndex].isMatched = true;
|
||||
setCards(matchedCards);
|
||||
setFlippedIndices([]);
|
||||
setMatches(matches + 1);
|
||||
if (matches + 1 === ICONS.length / 2) {
|
||||
setWon(true);
|
||||
}
|
||||
}, 500);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
const resetCards = [...newCards];
|
||||
resetCards[firstIndex].isFlipped = false;
|
||||
resetCards[secondIndex].isFlipped = false;
|
||||
setCards(resetCards);
|
||||
setFlippedIndices([]);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-900 flex flex-col items-center justify-center p-4 text-center">
|
||||
<div className="absolute top-6 left-6">
|
||||
<button
|
||||
onClick={() => router.push('/')}
|
||||
className="text-slate-400 hover:text-white flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5" /> Back Home
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="max-w-md w-full">
|
||||
<h1 className="text-4xl sm:text-6xl font-black text-transparent bg-clip-text bg-gradient-to-r from-accent to-emerald-400 mb-6 animate-pulse">
|
||||
COMING SOON
|
||||
</h1>
|
||||
|
||||
{/* Countdown Timer */}
|
||||
<div className="flex justify-center gap-3 sm:gap-4 mb-8">
|
||||
{[
|
||||
{ label: 'Days', value: timeLeft.days },
|
||||
{ label: 'Hours', value: timeLeft.hours },
|
||||
{ label: 'Mins', value: timeLeft.minutes },
|
||||
{ label: 'Secs', value: timeLeft.seconds },
|
||||
].map((item) => (
|
||||
<div key={item.label} className="bg-slate-800 p-3 sm:p-4 rounded-2xl border border-slate-700 shadow-xl flex flex-col items-center min-w-[70px] sm:min-w-[80px]">
|
||||
<span className="text-2xl sm:text-3xl font-black text-white">{item.value.toString().padStart(2, '0')}</span>
|
||||
<span className="text-xs sm:text-sm text-slate-400 font-medium uppercase tracking-wider">{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-slate-400 text-lg mb-10">
|
||||
We are working hard to bring this feature to you. In the meantime, play a quick game!
|
||||
</p>
|
||||
|
||||
{/* Mini Game Container */}
|
||||
<div className="bg-slate-800 p-6 rounded-3xl border border-slate-700 shadow-2xl">
|
||||
<div className="flex justify-between items-center mb-6 text-slate-300">
|
||||
<span className="font-semibold">Moves: <span className="text-accent">{moves}</span></span>
|
||||
{won && <span className="text-yellow-400 font-bold flex items-center gap-1"><Trophy className="w-5 h-5" /> You Won!</span>}
|
||||
<button onClick={startNewGame} className="text-sm px-3 py-1 bg-slate-700 hover:bg-slate-600 rounded-full transition-colors">
|
||||
Restart
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{cards.map((card, index) => {
|
||||
const Icon = card.Icon;
|
||||
const isVisible = card.isFlipped || card.isMatched;
|
||||
return (
|
||||
<button
|
||||
key={card.id}
|
||||
onClick={() => handleCardClick(index)}
|
||||
className={`aspect-square rounded-xl flex items-center justify-center text-2xl transition-all duration-300 transform ${
|
||||
isVisible
|
||||
? 'bg-accent/20 border-2 border-accent text-accent scale-100 rotate-0'
|
||||
: 'bg-slate-700 border-2 border-slate-600 hover:bg-slate-600 scale-95'
|
||||
} ${card.isMatched ? 'opacity-50' : ''}`}
|
||||
>
|
||||
{isVisible ? <Icon className="w-8 h-8" /> : '?'}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{won && (
|
||||
<div className="mt-6 animate-bounce text-emerald-400 font-bold text-xl">
|
||||
Great memory! The feature will be here soon.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Image from 'next/image';
|
||||
import HeroBackground3D from '@/components/ui/3d/HeroBackground3D';
|
||||
import CardIcon3D from '@/components/ui/3d/CardIcon3D';
|
||||
import { users } from '@/data/mockData';
|
||||
import { Zap, ArrowRight, Bike, Wallet, Leaf, ShieldCheck, MapPin, Truck, ChevronRight, BarChart3, BatteryCharging, Clock, Activity } from 'lucide-react';
|
||||
|
||||
@@ -22,13 +24,13 @@ export default function LandingPage() {
|
||||
const handleLogin = async (email: string) => {
|
||||
setLoading(true);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
|
||||
const user = users.find(u => u.email === email);
|
||||
if (user) {
|
||||
sessionStorage.setItem('authToken', 'demo-token');
|
||||
sessionStorage.setItem('userRole', user.role);
|
||||
sessionStorage.setItem('userName', user.name);
|
||||
|
||||
|
||||
switch (user.role) {
|
||||
case 'super_admin':
|
||||
case 'admin_manager':
|
||||
@@ -72,11 +74,10 @@ export default function LandingPage() {
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => router.push('/login')}
|
||||
className={`px-5 py-2.5 rounded-full font-semibold transition-all flex items-center gap-2 ${
|
||||
scrolled
|
||||
? 'bg-accent text-white hover:bg-accent-dark shadow-md hover:shadow-lg'
|
||||
: 'bg-white text-accent hover:bg-slate-100 shadow-xl'
|
||||
}`}
|
||||
className={`px-5 py-2.5 rounded-full font-semibold transition-all flex items-center gap-2 ${scrolled
|
||||
? 'bg-accent text-white hover:bg-accent-dark shadow-md hover:shadow-lg'
|
||||
: 'bg-white text-accent hover:bg-slate-100 shadow-xl'
|
||||
}`}
|
||||
>
|
||||
Sign In <ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -88,6 +89,7 @@ export default function LandingPage() {
|
||||
<section className="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden bg-slate-900">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-slate-900 via-slate-800 to-accent/20 z-0" />
|
||||
<div className="absolute inset-0 bg-[url('/noise.png')] opacity-20 mix-blend-overlay z-0"></div>
|
||||
<HeroBackground3D />
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10 text-center">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 bg-white/10 backdrop-blur-md border border-white/20 rounded-full text-white text-sm font-medium mb-8">
|
||||
<Leaf className="w-4 h-4 text-emerald-400" /> Bangladesh's Leading EV Platform
|
||||
@@ -101,14 +103,14 @@ export default function LandingPage() {
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<button
|
||||
onClick={() => router.push('/login')}
|
||||
onClick={() => router.push('/coming-soon')}
|
||||
className="w-full sm:w-auto px-8 py-4 bg-accent text-white rounded-full font-bold text-lg hover:bg-accent-dark transition-all transform hover:scale-105 shadow-xl shadow-accent/30 flex items-center justify-center gap-2"
|
||||
>
|
||||
Get Started Now <ArrowRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Decorative background elements */}
|
||||
<div className="absolute -bottom-24 -left-24 w-96 h-96 bg-accent/30 rounded-full blur-3xl mix-blend-screen pointer-events-none"></div>
|
||||
<div className="absolute top-24 -right-24 w-96 h-96 bg-emerald-500/20 rounded-full blur-3xl mix-blend-screen pointer-events-none"></div>
|
||||
@@ -155,7 +157,7 @@ export default function LandingPage() {
|
||||
<h4 className="text-2xl font-bold text-slate-900 mb-3 relative z-10">Biker Rentals</h4>
|
||||
<p className="text-slate-600 mb-8 flex-grow relative z-10">Save on petrol costs and rent a high-performance e-scooter for your daily commute or delivery gigs.</p>
|
||||
<button
|
||||
onClick={() => handleLogin('rahim@email.com')}
|
||||
onClick={() => router.push('/coming-soon')}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center gap-2 text-biker font-bold group-hover:translate-x-2 transition-transform relative z-10"
|
||||
>
|
||||
@@ -171,13 +173,13 @@ export default function LandingPage() {
|
||||
</div>
|
||||
<h4 className="text-2xl font-bold text-slate-900 mb-3 relative z-10">FICO Investment</h4>
|
||||
<p className="text-slate-600 mb-8 flex-grow relative z-10">Invest in fractional EV ownership. Let us manage the fleet while you enjoy guaranteed monthly returns.</p>
|
||||
<button
|
||||
{/* <button
|
||||
onClick={() => handleLogin('investor@email.com')}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center gap-2 text-investor font-bold group-hover:translate-x-2 transition-transform relative z-10"
|
||||
>
|
||||
Login as Investor <ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</button> */}
|
||||
</div>
|
||||
|
||||
{/* Service 3 */}
|
||||
@@ -189,14 +191,14 @@ export default function LandingPage() {
|
||||
<h4 className="text-2xl font-bold text-slate-900 mb-3 relative z-10">Swap Stations</h4>
|
||||
<p className="text-slate-600 mb-8 flex-grow relative z-10">Never wait to charge. Swap your depleted battery for a fully charged one in less than 2 minutes.</p>
|
||||
<button
|
||||
onClick={() => handleLogin('swap@jaiben.com')}
|
||||
onClick={() => router.push('/coming-soon')}
|
||||
disabled={loading}
|
||||
className="inline-flex items-center gap-2 text-purple-600 font-bold group-hover:translate-x-2 transition-transform relative z-10"
|
||||
>
|
||||
Login as Swap Station <ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Service 4 (Merchant/Rent to own) */}
|
||||
<div className="group bg-white rounded-3xl p-8 shadow-sm hover:shadow-2xl transition-all duration-300 border border-slate-100 relative overflow-hidden flex flex-col h-full md:col-span-2 lg:col-span-3 lg:w-2/3 lg:mx-auto">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-accent/10 rounded-bl-full -mr-8 -mt-8 transition-transform group-hover:scale-110"></div>
|
||||
@@ -206,9 +208,9 @@ export default function LandingPage() {
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-2xl font-bold text-slate-900 mb-3">Merchants & Deliveries</h4>
|
||||
<p className="text-slate-600 mb-6 max-w-xl">Are you a business looking for emission-free delivery solutions? Partner with us for dedicated EV riders or rent-to-own schemes for your fleet.</p>
|
||||
<p className="text-slate-600 mb-6 max-w-xl">Are you a delivery partner or company? Request any amount of riders for your delivery needs. Partner with us for dedicated EV riders.</p>
|
||||
<button
|
||||
onClick={() => router.push('/login')}
|
||||
onClick={() => router.push('/coming-soon')}
|
||||
className="inline-flex items-center gap-2 text-accent font-bold group-hover:translate-x-2 transition-transform"
|
||||
>
|
||||
Partner With Us <ChevronRight className="w-5 h-5" />
|
||||
@@ -237,7 +239,7 @@ export default function LandingPage() {
|
||||
<h4 className="text-xl font-bold text-slate-900 mb-3">IoT Enabled</h4>
|
||||
<p className="text-slate-600 text-sm">Get real-time tracking, remote locking, and smart analytics for every vehicle.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-slate-50 rounded-2xl p-8 text-center hover:-translate-y-2 transition-transform duration-300 border border-slate-100 hover:shadow-lg">
|
||||
<div className="w-16 h-16 mx-auto bg-white rounded-full shadow-sm flex items-center justify-center mb-6">
|
||||
<BatteryCharging className="w-8 h-8 text-accent" />
|
||||
@@ -307,7 +309,7 @@ export default function LandingPage() {
|
||||
|
||||
<div className="mt-16 text-center">
|
||||
<button
|
||||
onClick={() => router.push('/login')}
|
||||
onClick={() => router.push('/coming-soon')}
|
||||
className="px-10 py-4 bg-white text-slate-900 rounded-full font-bold text-lg hover:bg-slate-100 transition-all transform hover:scale-105 shadow-xl flex items-center justify-center gap-2 mx-auto"
|
||||
>
|
||||
Get The App <ArrowRight className="w-5 h-5" />
|
||||
@@ -338,14 +340,14 @@ export default function LandingPage() {
|
||||
<div className="w-10 h-10 rounded-full bg-slate-800 flex items-center justify-center text-slate-400 hover:bg-accent hover:text-white transition-colors cursor-pointer"><MapPin className="w-5 h-5" /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<h4 className="text-white font-bold mb-6">Services</h4>
|
||||
<ul className="space-y-3 text-slate-400">
|
||||
<li><button onClick={() => router.push('/login')} className="hover:text-accent transition-colors">EV for Deliveries</button></li>
|
||||
<li><button onClick={() => router.push('/login')} className="hover:text-accent transition-colors">Rent to Own</button></li>
|
||||
<li><button onClick={() => router.push('/login')} className="hover:text-accent transition-colors">Investor Platform</button></li>
|
||||
<li><button onClick={() => router.push('/login')} className="hover:text-accent transition-colors">Swap Stations</button></li>
|
||||
<li><button onClick={() => router.push('/coming-soon')} className="hover:text-accent transition-colors">EV for Deliveries</button></li>
|
||||
<li><button onClick={() => router.push('/coming-soon')} className="hover:text-accent transition-colors">Rent to Own</button></li>
|
||||
<li><button onClick={() => router.push('/coming-soon')} className="hover:text-accent transition-colors">Investor Platform</button></li>
|
||||
<li><button onClick={() => router.push('/coming-soon')} className="hover:text-accent transition-colors">Swap Stations</button></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -359,7 +361,7 @@ export default function LandingPage() {
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="border-t border-slate-800 pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-slate-500 text-sm">
|
||||
© {new Date().getFullYear()} JAIBEN Mobility Ltd. All rights reserved.
|
||||
|
||||
@@ -9,7 +9,7 @@ interface LayoutContentProps {
|
||||
|
||||
export default function LayoutContent({ children }: LayoutContentProps) {
|
||||
const pathname = usePathname();
|
||||
const showSidebar = pathname !== "/" && pathname !== "/login";
|
||||
const showSidebar = pathname !== "/" && pathname !== "/login" && pathname !== "/coming-soon";
|
||||
return (
|
||||
<>
|
||||
{showSidebar && <Sidebar />}
|
||||
|
||||
97
src/components/ui/3d/CardIcon3D.tsx
Normal file
97
src/components/ui/3d/CardIcon3D.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client';
|
||||
import { useRef } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { Float, Torus, Cylinder, Box, Octahedron, MeshTransmissionMaterial } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
|
||||
function BikeIcon() {
|
||||
const mesh = useRef<THREE.Mesh>(null);
|
||||
useFrame((_, delta) => {
|
||||
if (mesh.current) {
|
||||
mesh.current.rotation.y += delta;
|
||||
mesh.current.rotation.x += delta * 0.5;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Float floatIntensity={2} speed={2}>
|
||||
<Torus ref={mesh} args={[1.2, 0.4, 16, 32]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<meshStandardMaterial color="#f97316" roughness={0.1} metalness={0.8} />
|
||||
</Torus>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
function InvestorIcon() {
|
||||
const mesh = useRef<THREE.Mesh>(null);
|
||||
useFrame((_, delta) => {
|
||||
if (mesh.current) {
|
||||
mesh.current.rotation.y += delta * 1.5;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Float floatIntensity={2} speed={2}>
|
||||
<Cylinder ref={mesh} args={[1.2, 1.2, 0.4, 32]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<meshStandardMaterial color="#eab308" roughness={0.2} metalness={0.9} />
|
||||
</Cylinder>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
function SwapIcon() {
|
||||
const mesh = useRef<THREE.Mesh>(null);
|
||||
useFrame((_, delta) => {
|
||||
if (mesh.current) {
|
||||
mesh.current.rotation.x += delta;
|
||||
mesh.current.rotation.y += delta * 1.2;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Float floatIntensity={2} speed={2}>
|
||||
<Box ref={mesh} args={[1.5, 1.5, 1.5]}>
|
||||
<MeshTransmissionMaterial
|
||||
color="#a855f7"
|
||||
thickness={0.5}
|
||||
roughness={0.1}
|
||||
transmission={0.9}
|
||||
ior={1.5}
|
||||
/>
|
||||
</Box>
|
||||
<Box args={[0.8, 0.8, 0.8]}>
|
||||
<meshStandardMaterial color="#d8b4fe" emissive="#a855f7" emissiveIntensity={0.5} />
|
||||
</Box>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
function MerchantIcon() {
|
||||
const mesh = useRef<THREE.Mesh>(null);
|
||||
useFrame((_, delta) => {
|
||||
if (mesh.current) {
|
||||
mesh.current.rotation.x += delta * 0.5;
|
||||
mesh.current.rotation.y += delta;
|
||||
}
|
||||
});
|
||||
return (
|
||||
<Float floatIntensity={2} speed={2}>
|
||||
<Octahedron ref={mesh} args={[1.5]}>
|
||||
<meshStandardMaterial color="#00bc84" roughness={0.1} metalness={0.7} />
|
||||
</Octahedron>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CardIcon3D({ type }: { type: 'bike' | 'investor' | 'swap' | 'merchant' }) {
|
||||
return (
|
||||
<div className="w-full h-full relative z-10 transition-transform duration-300 group-hover:scale-125">
|
||||
<Canvas camera={{ position: [0, 0, 4], fov: 50 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[5, 5, 5]} intensity={1} />
|
||||
<pointLight position={[-5, -5, -5]} intensity={0.5} />
|
||||
{type === 'bike' && <BikeIcon />}
|
||||
{type === 'investor' && <InvestorIcon />}
|
||||
{type === 'swap' && <SwapIcon />}
|
||||
{type === 'merchant' && <MerchantIcon />}
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
65
src/components/ui/3d/HeroBackground3D.tsx
Normal file
65
src/components/ui/3d/HeroBackground3D.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
import { useRef } from 'react';
|
||||
import { Canvas, useFrame } from '@react-three/fiber';
|
||||
import { Float, Box, Cylinder } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
|
||||
function StylizedScooter() {
|
||||
const group = useRef<THREE.Group>(null);
|
||||
|
||||
useFrame((state, delta) => {
|
||||
if (group.current) {
|
||||
group.current.rotation.y += delta * 0.2;
|
||||
group.current.rotation.z = Math.sin(state.clock.elapsedTime * 0.5) * 0.05;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Float speed={2} rotationIntensity={0.5} floatIntensity={1.5}>
|
||||
<group ref={group} position={[2, 0, -2]} scale={1.2} rotation={[0.2, -0.5, 0]}>
|
||||
{/* Main Deck */}
|
||||
<Box args={[4, 0.3, 1]} position={[0, 0, 0]}>
|
||||
<meshStandardMaterial color="#334155" roughness={0.4} metalness={0.6} />
|
||||
</Box>
|
||||
|
||||
{/* Front Wheel */}
|
||||
<Cylinder args={[0.8, 0.8, 0.4, 32]} rotation={[Math.PI / 2, 0, 0]} position={[2, 0, 0]}>
|
||||
<meshStandardMaterial color="#f97316" roughness={0.2} metalness={0.8} />
|
||||
</Cylinder>
|
||||
|
||||
{/* Rear Wheel */}
|
||||
<Cylinder args={[0.8, 0.8, 0.4, 32]} rotation={[Math.PI / 2, 0, 0]} position={[-2, 0, 0]}>
|
||||
<meshStandardMaterial color="#f97316" roughness={0.2} metalness={0.8} />
|
||||
</Cylinder>
|
||||
|
||||
{/* Steering Column */}
|
||||
<Cylinder args={[0.1, 0.1, 3, 16]} position={[1.5, 1.5, 0]} rotation={[0, 0, -0.2]}>
|
||||
<meshStandardMaterial color="#cbd5e1" roughness={0.3} metalness={0.9} />
|
||||
</Cylinder>
|
||||
|
||||
{/* Handlebars */}
|
||||
<Box args={[0.2, 1.5, 0.2]} position={[1.2, 3, 0]} rotation={[Math.PI / 2, 0, 0]}>
|
||||
<meshStandardMaterial color="#0f172a" roughness={0.8} />
|
||||
</Box>
|
||||
|
||||
{/* Accent / Battery block */}
|
||||
<Box args={[1.5, 0.6, 1.1]} position={[-0.5, 0.4, 0]}>
|
||||
<meshStandardMaterial color="#00bc84" emissive="#00bc84" emissiveIntensity={0.5} roughness={0.2} />
|
||||
</Box>
|
||||
</group>
|
||||
</Float>
|
||||
);
|
||||
}
|
||||
|
||||
export default function HeroBackground3D() {
|
||||
return (
|
||||
<div className="absolute inset-0 pointer-events-none z-0 overflow-hidden">
|
||||
<Canvas camera={{ position: [0, 0, 8], fov: 45 }}>
|
||||
<ambientLight intensity={0.5} />
|
||||
<directionalLight position={[10, 10, 5]} intensity={1} />
|
||||
<directionalLight position={[-10, -10, -5]} intensity={0.5} color="#00bc84" />
|
||||
<StylizedScooter />
|
||||
</Canvas>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user