![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/components/ |
import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Trophy,
Star,
Award,
Target,
TrendingUp,
Users,
MessageSquare,
ThumbsUp,
Eye,
Calendar,
Zap,
Crown,
Medal,
Shield,
Heart
} from 'lucide-react';
interface Achievement {
id: string;
title: string;
description: string;
icon: string;
category: 'social' | 'professional' | 'engagement' | 'milestone';
rarity: 'common' | 'rare' | 'epic' | 'legendary';
progress: number;
maxProgress: number;
unlocked: boolean;
unlockedAt?: Date;
xpReward: number;
}
interface UserLevel {
level: number;
currentXP: number;
xpToNext: number;
totalXP: number;
title: string;
badge: string;
}
interface AchievementSystemProps {
userId: string;
stats?: {
followers: number;
following: number;
cases: number;
endorsements: number;
profileViews: number;
messages: number;
daysActive: number;
};
}
const AchievementSystem: React.FC<AchievementSystemProps> = ({ userId, stats }) => {
const [achievements, setAchievements] = useState<Achievement[]>([]);
const [userLevel, setUserLevel] = useState<UserLevel>({
level: 1,
currentXP: 0,
xpToNext: 100,
totalXP: 0,
title: 'Novice',
badge: '🌱'
});
const [showUnlockAnimation, setShowUnlockAnimation] = useState(false);
const [recentUnlock, setRecentUnlock] = useState<Achievement | null>(null);
// Provide default stats if undefined
const defaultStats = {
followers: 0,
following: 0,
cases: 0,
endorsements: 0,
profileViews: 0,
messages: 0,
daysActive: 0,
...(stats || {})
};
console.log('AchievementSystem render:', { userId, stats, defaultStats });
useEffect(() => {
console.log('AchievementSystem useEffect triggered:', { defaultStats });
generateAchievements();
calculateUserLevel();
}, [defaultStats]);
const generateAchievements = () => {
console.log('generateAchievements called with defaultStats:', defaultStats);
const baseAchievements: Achievement[] = [
// Social Achievements
{
id: 'first_follower',
title: 'First Steps',
description: 'Gain your first follower',
icon: '👥',
category: 'social',
rarity: 'common',
progress: Math.min(defaultStats.followers, 1),
maxProgress: 1,
unlocked: defaultStats.followers >= 1,
xpReward: 50
},
{
id: 'social_butterfly',
title: 'Social Butterfly',
description: 'Reach 10 followers',
icon: '🦋',
category: 'social',
rarity: 'common',
progress: Math.min(defaultStats.followers, 10),
maxProgress: 10,
unlocked: defaultStats.followers >= 10,
xpReward: 100
},
{
id: 'influencer',
title: 'Influencer',
description: 'Reach 100 followers',
icon: '⭐',
category: 'social',
rarity: 'rare',
progress: Math.min(defaultStats.followers, 100),
maxProgress: 100,
unlocked: defaultStats.followers >= 100,
xpReward: 500
},
{
id: 'celebrity',
title: 'Celebrity',
description: 'Reach 1000 followers',
icon: '👑',
category: 'social',
rarity: 'epic',
progress: Math.min(defaultStats.followers, 1000),
maxProgress: 1000,
unlocked: defaultStats.followers >= 1000,
xpReward: 2000
},
// Professional Achievements
{
id: 'first_case',
title: 'Case Handler',
description: 'Participate in your first case',
icon: '📋',
category: 'professional',
rarity: 'common',
progress: Math.min(defaultStats.cases, 1),
maxProgress: 1,
unlocked: defaultStats.cases >= 1,
xpReward: 100
},
{
id: 'case_expert',
title: 'Case Expert',
description: 'Participate in 10 cases',
icon: '🎯',
category: 'professional',
rarity: 'rare',
progress: Math.min(defaultStats.cases, 10),
maxProgress: 10,
unlocked: defaultStats.cases >= 10,
xpReward: 300
},
{
id: 'legal_legend',
title: 'Legal Legend',
description: 'Participate in 50 cases',
icon: '⚖️',
category: 'professional',
rarity: 'epic',
progress: Math.min(defaultStats.cases, 50),
maxProgress: 50,
unlocked: defaultStats.cases >= 50,
xpReward: 1000
},
// Engagement Achievements
{
id: 'first_endorsement',
title: 'Endorsed',
description: 'Receive your first endorsement',
icon: '👍',
category: 'engagement',
rarity: 'common',
progress: Math.min(defaultStats.endorsements, 1),
maxProgress: 1,
unlocked: defaultStats.endorsements >= 1,
xpReward: 75
},
{
id: 'highly_endorsed',
title: 'Highly Endorsed',
description: 'Receive 25 endorsements',
icon: '🏆',
category: 'engagement',
rarity: 'rare',
progress: Math.min(defaultStats.endorsements, 25),
maxProgress: 25,
unlocked: defaultStats.endorsements >= 25,
xpReward: 400
},
{
id: 'trusted_professional',
title: 'Trusted Professional',
description: 'Receive 100 endorsements',
icon: '🛡️',
category: 'engagement',
rarity: 'epic',
progress: Math.min(defaultStats.endorsements, 100),
maxProgress: 100,
unlocked: defaultStats.endorsements >= 100,
xpReward: 1500
},
// Milestone Achievements
{
id: 'first_view',
title: 'Noticed',
description: 'Receive your first profile view',
icon: '👁️',
category: 'milestone',
rarity: 'common',
progress: Math.min(defaultStats.profileViews, 1),
maxProgress: 1,
unlocked: defaultStats.profileViews >= 1,
xpReward: 25
},
{
id: 'popular',
title: 'Popular',
description: 'Reach 100 profile views',
icon: '🔥',
category: 'milestone',
rarity: 'common',
progress: Math.min(defaultStats.profileViews, 100),
maxProgress: 100,
unlocked: defaultStats.profileViews >= 100,
xpReward: 200
},
{
id: 'viral',
title: 'Viral',
description: 'Reach 1000 profile views',
icon: '🚀',
category: 'milestone',
rarity: 'rare',
progress: Math.min(defaultStats.profileViews, 1000),
maxProgress: 1000,
unlocked: defaultStats.profileViews >= 1000,
xpReward: 800
},
{
id: 'week_warrior',
title: 'Week Warrior',
description: 'Stay active for 7 days',
icon: '📅',
category: 'milestone',
rarity: 'common',
progress: Math.min(defaultStats.daysActive, 7),
maxProgress: 7,
unlocked: defaultStats.daysActive >= 7,
xpReward: 150
},
{
id: 'month_master',
title: 'Month Master',
description: 'Stay active for 30 days',
icon: '📆',
category: 'milestone',
rarity: 'rare',
progress: Math.min(defaultStats.daysActive, 30),
maxProgress: 30,
unlocked: defaultStats.daysActive >= 30,
xpReward: 600
}
];
setAchievements(baseAchievements);
};
const calculateUserLevel = () => {
const totalXP = achievements
.filter(a => a.unlocked)
.reduce((sum, a) => sum + a.xpReward, 0);
const level = Math.floor(totalXP / 1000) + 1;
const currentXP = totalXP % 1000;
const xpToNext = 1000 - currentXP;
const titles = [
'Novice', 'Apprentice', 'Practitioner', 'Specialist', 'Expert',
'Master', 'Grandmaster', 'Legend', 'Mythic', 'Divine'
];
const badges = ['🌱', '🌿', '🌳', '🌲', '⭐', '🌟', '💫', '✨', '👑', '🏆'];
setUserLevel({
level: Math.min(level, 10),
currentXP,
xpToNext,
totalXP,
title: titles[Math.min(level - 1, 9)],
badge: badges[Math.min(level - 1, 9)]
});
};
const getRarityColor = (rarity: string) => {
switch (rarity) {
case 'common': return 'text-gray-600 bg-gray-100';
case 'rare': return 'text-blue-600 bg-blue-100';
case 'epic': return 'text-purple-600 bg-purple-100';
case 'legendary': return 'text-yellow-600 bg-yellow-100';
default: return 'text-gray-600 bg-gray-100';
}
};
const getCategoryIcon = (category: string) => {
switch (category) {
case 'social': return <Users className="h-4 w-4" />;
case 'professional': return <Target className="h-4 w-4" />;
case 'engagement': return <ThumbsUp className="h-4 w-4" />;
case 'milestone': return <TrendingUp className="h-4 w-4" />;
default: return <Star className="h-4 w-4" />;
}
};
const unlockedCount = achievements.filter(a => a.unlocked).length;
const totalCount = achievements.length;
return (
<div className="bg-white rounded-xl shadow p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold flex items-center gap-2">
<Trophy className="h-6 w-6 text-yellow-500" />
Achievements & Level
</h2>
<div className="text-sm text-gray-500">
{unlockedCount}/{totalCount} unlocked
</div>
</div>
{/* Level Progress */}
<div className="bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg p-4 mb-6 text-white">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-2xl">{userLevel.badge}</span>
<div>
<div className="font-semibold">Level {userLevel.level}</div>
<div className="text-sm opacity-90">{userLevel.title}</div>
</div>
</div>
<div className="text-right">
<div className="font-semibold">{userLevel.totalXP} XP</div>
<div className="text-sm opacity-90">Total Experience</div>
</div>
</div>
<div className="w-full bg-white/20 rounded-full h-2 mb-2">
<div
className="bg-white h-2 rounded-full transition-all duration-500"
style={{ width: `${(userLevel.currentXP / 1000) * 100}%` }}
></div>
</div>
<div className="text-sm opacity-90">
{userLevel.currentXP} / 1000 XP to next level
</div>
</div>
{/* Achievement Categories */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{['social', 'professional', 'engagement', 'milestone'].map(category => {
const categoryAchievements = achievements.filter(a => a.category === category);
const unlocked = categoryAchievements.filter(a => a.unlocked).length;
const total = categoryAchievements.length;
return (
<div key={category} className="bg-gray-50 rounded-lg p-3">
<div className="flex items-center gap-2 mb-2">
{getCategoryIcon(category)}
<span className="font-medium capitalize">{category}</span>
</div>
<div className="text-sm text-gray-600">
{unlocked}/{total} completed
</div>
<div className="w-full bg-gray-200 rounded-full h-1 mt-1">
<div
className="bg-blue-500 h-1 rounded-full"
style={{ width: `${(unlocked / total) * 100}%` }}
></div>
</div>
</div>
);
})}
</div>
{/* Achievements Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{achievements.map(achievement => (
<motion.div
key={achievement.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className={`border rounded-lg p-4 transition-all duration-300 ${
achievement.unlocked
? 'border-green-200 bg-green-50'
: 'border-gray-200 bg-gray-50'
}`}
>
<div className="flex items-start justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-2xl">{achievement.icon}</span>
<div>
<h3 className="font-semibold text-sm">{achievement.title}</h3>
<p className="text-xs text-gray-600">{achievement.description}</p>
</div>
</div>
{achievement.unlocked && (
<div className="text-green-500">
<Award className="h-4 w-4" />
</div>
)}
</div>
<div className="flex items-center justify-between mb-2">
<span className={`text-xs px-2 py-1 rounded-full ${getRarityColor(achievement.rarity)}`}>
{achievement.rarity}
</span>
<span className="text-xs text-gray-500">
+{achievement.xpReward} XP
</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-1">
<div
className={`h-1 rounded-full transition-all duration-500 ${
achievement.unlocked ? 'bg-green-500' : 'bg-blue-500'
}`}
style={{ width: `${(achievement.progress / achievement.maxProgress) * 100}%` }}
></div>
</div>
<div className="text-xs text-gray-500 mt-1">
{achievement.progress}/{achievement.maxProgress}
</div>
</motion.div>
))}
</div>
{/* Unlock Animation */}
<AnimatePresence>
{showUnlockAnimation && recentUnlock && (
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
className="fixed inset-0 flex items-center justify-center z-50 bg-black/50"
onClick={() => setShowUnlockAnimation(false)}
>
<motion.div
initial={{ y: 50 }}
animate={{ y: 0 }}
className="bg-white rounded-xl p-6 max-w-sm mx-4 text-center"
>
<div className="text-6xl mb-4">🎉</div>
<h3 className="text-xl font-bold mb-2">Achievement Unlocked!</h3>
<div className="text-3xl mb-2">{recentUnlock.icon}</div>
<h4 className="font-semibold mb-1">{recentUnlock.title}</h4>
<p className="text-gray-600 text-sm mb-4">{recentUnlock.description}</p>
<div className="text-green-600 font-semibold">+{recentUnlock.xpReward} XP</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
};
export default AchievementSystem;