feat(ui): refactor gamification UI for role awareness and remove floaters

This commit is contained in:
Satyam
2026-02-22 01:24:26 +05:30
parent 058b5bb217
commit b8a20144d3
5 changed files with 668 additions and 96 deletions
@@ -0,0 +1,77 @@
import React, { useEffect, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { useGamification } from '../../../context/GamificationContext';
const FloatingXPManager = () => {
const { recentAction, setRecentAction, missionComplete } = useGamification();
const [popups, setPopups] = useState([]);
useEffect(() => {
if (recentAction) {
setPopups(prev => [...prev, recentAction]);
// The context requires us to clear the recentAction immediately so same actions can fire again
setRecentAction(null);
}
}, [recentAction, setRecentAction]);
useEffect(() => {
if (popups.length > 0) {
const timer = setTimeout(() => {
setPopups(prev => prev.slice(1));
}, 1500); // Remove after animation finishes
return () => clearTimeout(timer);
}
}, [popups]);
return (
<>
<div className="absolute inset-0 pointer-events-none z-[60]">
<AnimatePresence>
{popups.map(popup => (
<motion.div
key={popup.id}
initial={{ opacity: 0, y: popup.y, x: popup.x, scale: 0.5 }}
animate={{ opacity: 1, y: popup.y - 120, x: popup.x, scale: 1.2 }}
exit={{ opacity: 0, y: popup.y - 150, scale: 0.8 }}
transition={{ duration: 1.2, ease: "easeOut" }}
className={`absolute transform -translate-x-1/2 pointer-events-none drop-shadow-2xl font-black text-2xl
${popup.type === 'crit'
? 'text-transparent bg-clip-text bg-gradient-to-r from-green-400 to-emerald-600 drop-shadow-[0_0_15px_rgba(52,211,153,0.8)]'
: 'text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.8)]'}`}
style={{ padding: '4px' }}
>
{popup.text}
<div className="text-[10px] text-center font-bold text-slate-200 uppercase tracking-widest mt-1 drop-shadow-md">
{popup.actionLabel}
</div>
</motion.div>
))}
</AnimatePresence>
</div>
{/* Mission Complete Banner Banner */}
<AnimatePresence>
{missionComplete && (
<div className="absolute top-24 left-1/2 transform -translate-x-1/2 pointer-events-none z-[70] flex justify-center w-full">
<motion.div
initial={{ y: -100, opacity: 0, scale: 0.8 }}
animate={{ y: 0, opacity: 1, scale: 1 }}
exit={{ y: -50, opacity: 0, scale: 0.9 }}
transition={{ type: "spring", stiffness: 200, damping: 20 }}
className="bg-gradient-to-r from-emerald-500 to-teal-600 px-8 py-4 rounded-b-3xl shadow-[0_0_40px_rgba(16,185,129,0.5)] border-b-2 border-x-2 border-emerald-400"
>
<div className="text-emerald-100 text-sm font-bold uppercase tracking-widest text-center mb-1">
Mission Complete
</div>
<div className="text-white text-2xl font-black text-center whitespace-nowrap">
{missionComplete.message}
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</>
);
};
export default FloatingXPManager;
@@ -0,0 +1,90 @@
import React from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Star, Trophy, Zap } from 'lucide-react';
import { useGamification } from '../../../context/GamificationContext';
const LevelUpModal = () => {
const { levelUpData } = useGamification();
return (
<AnimatePresence>
{levelUpData && (
<div className="fixed inset-0 z-[100] flex items-center justify-center pointer-events-none">
{/* Backdrop glow */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm pointer-events-auto"
/>
<motion.div
initial={{ scale: 0.5, opacity: 0, y: 50 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.8, opacity: 0, y: -50 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
className="relative z-10 w-full max-w-sm"
>
{/* Celebration Ring */}
<motion.div
className="absolute inset-x-0 -top-12 h-40 bg-gradient-to-b from-yellow-400/20 to-transparent blur-2xl rounded-t-full"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ repeat: Infinity, duration: 2 }}
/>
<div className="bg-slate-900 border-2 border-yellow-500/50 rounded-3xl p-8 shadow-[0_0_50px_rgba(234,179,8,0.3)] relative overflow-hidden pointer-events-auto text-center">
{/* Rotating background ray effect */}
<motion.div
className="absolute inset-0 z-0 opacity-20"
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 20, ease: "linear" }}
style={{
background: 'repeating-conic-gradient(from 0deg, transparent 0deg 15deg, #eab308 15deg 30deg)'
}}
/>
<div className="relative z-10 flex flex-col items-center">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1, rotate: [0, -10, 10, -10, 0] }}
transition={{ delay: 0.2, type: "spring", stiffness: 200, damping: 10 }}
className="w-24 h-24 bg-gradient-to-br from-yellow-300 to-yellow-600 rounded-full flex items-center justify-center shadow-[0_0_30px_rgba(234,179,8,0.8)] mb-6 border-4 border-slate-900"
>
<Star className="w-12 h-12 text-slate-900 fill-slate-900" />
</motion.div>
<h2 className="text-4xl font-black text-transparent bg-clip-text bg-gradient-to-r from-white to-slate-300 mb-2 uppercase tracking-wide">
Level Up!
</h2>
<div className="text-xl font-bold text-yellow-400 mb-6 flex justify-center items-center gap-2">
<Zap className="w-5 h-5 fill-current" />
Level {levelUpData.newLevel} Reached
<Zap className="w-5 h-5 fill-current" />
</div>
{levelUpData.titleChanged && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
className="bg-slate-800/80 border border-slate-700 rounded-xl p-4 w-full shadow-inner"
>
<div className="text-xs text-slate-400 uppercase tracking-widest font-bold mb-1">New Rank Unlocked</div>
<div className="flex items-center justify-center gap-2 text-2xl font-black text-white">
<Trophy className="w-6 h-6 text-yellow-500" />
{levelUpData.newTitle}
</div>
</motion.div>
)}
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
};
export default LevelUpModal;