feat: implement 3D components and create a coming soon page with a memory match game

This commit is contained in:
sazzadulalambd
2026-06-22 15:52:44 +06:00
parent a831ea7a7c
commit 87d3421606
7 changed files with 969 additions and 34 deletions

View 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>
);
}