|
| 1 | +'use client'; |
| 2 | +import AnimatedSpan from '@/components/ui/animated-span'; |
| 3 | +import ProfilePicture from '@/components/ui/profile-picture'; |
| 4 | +import { UserRecord } from '@/types/User'; |
| 5 | +import { getUserDisplayName } from '@/utils/user'; |
| 6 | +import { motion } from 'framer-motion'; |
| 7 | +import { Crown } from 'lucide-react'; |
| 8 | + |
| 9 | +/** |
| 10 | + * This will show the top three users on TechBlitz in a podium style with a 3D isometric look. |
| 11 | + * |
| 12 | + * @returns |
| 13 | + */ |
| 14 | +export default function LeaderboardHero(opts: { |
| 15 | + topThreeUsers: UserRecord[] & |
| 16 | + { |
| 17 | + _count: { answers: number }; |
| 18 | + }[]; |
| 19 | +}) { |
| 20 | + const { topThreeUsers } = opts; |
| 21 | + |
| 22 | + const podiumOrder = [1, 0, 2]; // 2nd, 1st, 3rd |
| 23 | + |
| 24 | + return ( |
| 25 | + <section className="w-full py-8 md:pb-28 flex flex-col gap-y-4 justify-between items-center"> |
| 26 | + <div className="flex flex-col gap-y-3 md:mb-8 relative items-center"> |
| 27 | + <h1 className="text-3xl md:text-5xl text-wrap text-center font-inter max-w-2xl text-gradient from-white to-white/55 relative z-10"> |
| 28 | + Leaderboard |
| 29 | + </h1> |
| 30 | + <p className="text-sm text-gray-400 relative z-10 text-center"> |
| 31 | + See how you stack up against the rest of the community and become the |
| 32 | + best! |
| 33 | + </p> |
| 34 | + <div className="hidden md:flex"> |
| 35 | + <AnimatedSpan content="Top Users" /> |
| 36 | + </div> |
| 37 | + </div> |
| 38 | + <div className="hidden md:flex justify-center items-end perspective-1000 relative"> |
| 39 | + {podiumOrder.map((index) => { |
| 40 | + const user = topThreeUsers[index]; |
| 41 | + const position = index + 1; |
| 42 | + return ( |
| 43 | + <motion.div |
| 44 | + key={user.uid} |
| 45 | + initial={{ opacity: 0, y: 50 }} |
| 46 | + animate={{ opacity: 1, y: 0 }} |
| 47 | + transition={{ |
| 48 | + duration: 0.5, |
| 49 | + delay: position * 0.3, |
| 50 | + type: 'spring', |
| 51 | + stiffness: 100, |
| 52 | + }} |
| 53 | + className={`flex flex-col items-center ${ |
| 54 | + position === 1 |
| 55 | + ? 'order-2' |
| 56 | + : position === 2 |
| 57 | + ? 'order-1' |
| 58 | + : 'order-3' |
| 59 | + }`} |
| 60 | + > |
| 61 | + <div |
| 62 | + className={`relative ${ |
| 63 | + position === 1 |
| 64 | + ? 'size-28 md:size-64' |
| 65 | + : position === 2 |
| 66 | + ? 'size-24 md:h-48 md:w-64' |
| 67 | + : 'size-24 md:h-40 md:w-64' |
| 68 | + }`} |
| 69 | + > |
| 70 | + <motion.div |
| 71 | + className={`flex flex-col items-center mb-2 md:mb-4 z-20 relative ${ |
| 72 | + position === 3 ? '-top-4' : '' |
| 73 | + }`} |
| 74 | + initial={{ scale: 0.8, opacity: 0 }} |
| 75 | + animate={{ scale: 1, opacity: 1 }} |
| 76 | + transition={{ |
| 77 | + duration: 0.4, |
| 78 | + delay: position * 0.2, |
| 79 | + type: 'spring', |
| 80 | + bounce: 0.2, |
| 81 | + }} |
| 82 | + > |
| 83 | + <motion.div |
| 84 | + whileHover={{ scale: 1.05 }} |
| 85 | + whileTap={{ scale: 0.98 }} |
| 86 | + className="relative" |
| 87 | + > |
| 88 | + <ProfilePicture |
| 89 | + src={user.userProfilePicture} |
| 90 | + alt={`${user.username} profile picture`} |
| 91 | + className="size-8 md:size-12 rounded-full shadow-lg" |
| 92 | + /> |
| 93 | + {position === 1 && ( |
| 94 | + <motion.div |
| 95 | + className="absolute -top-3 left-4 size-4" |
| 96 | + initial={{ y: -5, opacity: 0 }} |
| 97 | + animate={{ y: 0, opacity: 1 }} |
| 98 | + transition={{ |
| 99 | + delay: 0.8, |
| 100 | + type: 'spring', |
| 101 | + stiffness: 200, |
| 102 | + damping: 15, |
| 103 | + }} |
| 104 | + > |
| 105 | + <Crown className="size-4 text-yellow-500 fill-yellow-400" /> |
| 106 | + </motion.div> |
| 107 | + )} |
| 108 | + {user.userLevel === 'PREMIUM' && ( |
| 109 | + <motion.div |
| 110 | + initial={{ x: -5, opacity: 0 }} |
| 111 | + animate={{ x: 0, opacity: 1 }} |
| 112 | + transition={{ delay: position * 0.2 + 0.3 }} |
| 113 | + className="relative -top-3 left-1.5 w-fit bg-accent text-xs flex items-center justify-center px-2 py-0.5 rounded-full" |
| 114 | + > |
| 115 | + <span className="text-[10px]">PRO</span> |
| 116 | + </motion.div> |
| 117 | + )} |
| 118 | + {user.userLevel === 'ADMIN' && ( |
| 119 | + <motion.div |
| 120 | + initial={{ x: -5, opacity: 0 }} |
| 121 | + animate={{ x: 0, opacity: 1 }} |
| 122 | + transition={{ delay: position * 0.2 + 0.3 }} |
| 123 | + className="relative -top-3 w-fit bg-accent text-xs flex items-center justify-center px-2 py-0.5 rounded-full" |
| 124 | + > |
| 125 | + <span className="text-[10px]">ADMIN</span> |
| 126 | + </motion.div> |
| 127 | + )} |
| 128 | + </motion.div> |
| 129 | + <motion.span |
| 130 | + initial={{ y: 5, opacity: 0 }} |
| 131 | + animate={{ y: 0, opacity: 1 }} |
| 132 | + transition={{ delay: position * 0.2 + 0.2 }} |
| 133 | + className="text-sm md:text-base font-semibold line-clamp-1" |
| 134 | + > |
| 135 | + {getUserDisplayName(user as unknown as UserRecord)} |
| 136 | + </motion.span> |
| 137 | + <motion.span |
| 138 | + initial={{ y: 5, opacity: 0 }} |
| 139 | + animate={{ y: 0, opacity: 1 }} |
| 140 | + transition={{ delay: position * 0.2 + 0.3 }} |
| 141 | + className="text-sm text-white" |
| 142 | + > |
| 143 | + {user._count.answers} answers |
| 144 | + </motion.span> |
| 145 | + </motion.div> |
| 146 | + {/** top of the podium */} |
| 147 | + <div |
| 148 | + className={`absolute inset-0 transform-3d rotate-x-55 rotate-y-45 ${ |
| 149 | + position === 1 |
| 150 | + ? 'bg-[#383737]' |
| 151 | + : position === 2 |
| 152 | + ? 'bg-black-100' |
| 153 | + : 'bg-black-100' |
| 154 | + }`} |
| 155 | + style={{ |
| 156 | + transform: `rotateX(81deg)`, |
| 157 | + }} |
| 158 | + > |
| 159 | + {/** bottom of the podium */} |
| 160 | + <div |
| 161 | + className={`absolute left-0 right-0 bottom-0 h-full origin-bottom rotate-x-90 ${ |
| 162 | + position === 1 |
| 163 | + ? 'bg-black-50' |
| 164 | + : position === 2 |
| 165 | + ? 'bg-black-200' |
| 166 | + : 'bg-black-200' |
| 167 | + }`} |
| 168 | + > |
| 169 | + <div |
| 170 | + className="absolute inset-0 flex items-center justify-center rotate-180" |
| 171 | + style={{ |
| 172 | + transform: `rotateX(180deg)`, |
| 173 | + }} |
| 174 | + > |
| 175 | + <span className="text-2xl md:text-5xl font-bold text-gradient from-white to-white/55 font-onest"> |
| 176 | + {position} |
| 177 | + {position === 1 ? 'st' : position === 2 ? 'nd' : 'rd'} |
| 178 | + </span> |
| 179 | + </div> |
| 180 | + {/** bottom fade */} |
| 181 | + <div className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-[#000] to-transparent rotate-180 -top-px"></div> |
| 182 | + </div> |
| 183 | + <div className="absolute bottom-0 left-0 right-0 h-1/2 bg-gradient-to-t from-[#000] to-transparent rotate-180 -top-px"></div> |
| 184 | + </div> |
| 185 | + </div> |
| 186 | + </motion.div> |
| 187 | + ); |
| 188 | + })} |
| 189 | + </div> |
| 190 | + </section> |
| 191 | + ); |
| 192 | +} |
0 commit comments