Skip to content

Instantly share code, notes, and snippets.

@nickstamas
Last active February 17, 2025 21:53
Show Gist options
  • Select an option

  • Save nickstamas/41a61ab155c467d41e8d9a6640caec41 to your computer and use it in GitHub Desktop.

Select an option

Save nickstamas/41a61ab155c467d41e8d9a6640caec41 to your computer and use it in GitHub Desktop.
Onboarding Animation Reference
import React, { useState, useEffect } from 'react';
import { ArrowRight, Trophy } from 'lucide-react';
const OnboardingQuiz = () => {
const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const [selectedAnswer, setSelectedAnswer] = useState(null);
const [showXPAlert, setShowXPAlert] = useState(false);
const [displayedXP, setDisplayedXP] = useState(0);
const questions = [
{
question: "What brings you here today?",
answers: [
"I want to learn new skills",
"I'm looking for career growth",
"I'm exploring new interests",
"I need specific knowledge"
],
xp: 50
},
{
question: "How much time can you dedicate?",
answers: [
"Less than 1 hour per week",
"2-3 hours per week",
"4-6 hours per week",
"More than 6 hours per week"
],
xp: 75
},
{
question: "What's your preferred learning style?",
answers: [
"Visual learning",
"Reading and writing",
"Interactive exercises",
"Audio and video content"
],
xp: 100
}
];
const handleAnswerClick = (answerIndex) => {
if (isAnimating) return;
setSelectedAnswer(answerIndex);
setIsAnimating(true);
setShowXPAlert(true);
// Hide XP alert after delay
setTimeout(() => {
setShowXPAlert(false);
// Update XP value only after alert is hidden
setTimeout(() => {
if (currentQuestionIndex < questions.length - 1) {
setDisplayedXP(questions[currentQuestionIndex + 1].xp);
}
}, 300); // Wait for fade out animation
}, 2500);
// Move to next question after delay
setTimeout(() => {
if (currentQuestionIndex < questions.length - 1) {
setCurrentQuestionIndex(prev => prev + 1);
setSelectedAnswer(null);
}
setIsAnimating(false);
}, 1000);
};
// Set initial XP value
useEffect(() => {
setDisplayedXP(questions[0].xp);
}, []);
// Reset animations when question changes
useEffect(() => {
setIsAnimating(false);
}, [currentQuestionIndex]);
const currentQuestion = questions[currentQuestionIndex];
return (
<div className="min-h-screen bg-gradient-to-b from-blue-50 to-blue-100 p-6 flex items-center justify-center">
{/* XP Alert */}
<div
className={`
absolute top-12 left-1/2 -translate-x-1/2 z-50
transition-all duration-300
${showXPAlert ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-8'}
`}
>
<div className="bg-white text-gray-800 px-6 py-3 rounded-full shadow-lg flex items-center gap-3">
<div className="bg-green-100 p-1.5 rounded-full">
<Trophy size={16} className="text-green-600" />
</div>
<span className="font-medium pr-2">+{displayedXP} XP</span>
</div>
</div>
<div className="w-full max-w-md">
{/* Progress bar */}
<div className="h-1 bg-gray-200 rounded-full mb-8">
<div
className="h-1 bg-blue-500 rounded-full transition-all duration-300"
style={{ width: `${((currentQuestionIndex + 1) / questions.length) * 100}%` }}
/>
</div>
{/* Question */}
<div className="mb-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-2 animate-slide-up">
{currentQuestion.question}
</h2>
<p className="text-sm text-gray-500 animate-slide-up" style={{ animationDelay: '100ms' }}>
Question {currentQuestionIndex + 1} of {questions.length}
</p>
</div>
{/* Answers */}
<div className="space-y-3">
{currentQuestion.answers.map((answer, index) => {
const delay = (index + 1) * 100;
const isSelected = selectedAnswer === index;
return (
<button
key={index}
onClick={() => handleAnswerClick(index)}
disabled={isAnimating}
className={`
w-full p-4 rounded-lg border-2 text-left relative
transition-all duration-300 transform
${isAnimating && !isSelected ? 'opacity-0 translate-y-4' : 'opacity-100 translate-y-0'}
${isSelected
? 'border-blue-500 bg-blue-50 text-blue-700 animate-pulse-once'
: 'border-gray-200 hover:border-blue-200 hover:bg-blue-50'
}
`}
style={{
transitionDelay: isAnimating ? '0ms' : `${delay}ms`,
animation: !isAnimating && `slideUp 500ms ${delay}ms both`
}}
>
<span className="block text-sm sm:text-base">{answer}</span>
{isSelected && (
<ArrowRight className="absolute right-4 top-1/2 transform -translate-y-1/2 text-blue-500" size={20} />
)}
</button>
);
})}
</div>
</div>
<style>{`
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulseOnce {
0% { transform: scale(1); }
50% { transform: scale(1.02); }
100% { transform: scale(1); }
}
.animate-slide-up {
animation: slideUp 500ms ease-out forwards;
}
.animate-pulse-once {
animation: pulseOnce 300ms ease-in-out;
}
`}</style>
</div>
);
};
export default OnboardingQuiz;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment