Spaces:
Runtime error
Runtime error
| "use client"; | |
| import React from "react"; | |
| import { useRouter } from "next/navigation"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import useUserStore from "@/stores/useUserStore"; | |
| interface ProfileModalProps { | |
| onClose: () => void; | |
| } | |
| export default function ProfileModal({ onClose }: ProfileModalProps) { | |
| const router = useRouter(); | |
| const { | |
| name, | |
| title, | |
| level, | |
| xp, | |
| longestStreak, | |
| coursesCompleted, | |
| preferences, | |
| setPreferences, | |
| logout, | |
| } = useUserStore(); | |
| const { soundOn, darkGlass, difficulty } = preferences; | |
| return ( | |
| <AnimatePresence> | |
| <motion.div | |
| className="fixed inset-0 z-[100] flex items-center justify-center" | |
| initial={{ opacity: 0 }} | |
| animate={{ opacity: 1 }} | |
| exit={{ opacity: 0 }} | |
| > | |
| {/* Backdrop */} | |
| <motion.div | |
| className="absolute inset-0 bg-black/30" | |
| onClick={onClose} | |
| initial={{ opacity: 0, backdropFilter: "blur(0px)" }} | |
| animate={{ opacity: 1, backdropFilter: "blur(6px)" }} | |
| exit={{ opacity: 0, backdropFilter: "blur(0px)" }} | |
| transition={{ duration: 0.6, ease: "easeOut" }} | |
| /> | |
| {/* Modal card */} | |
| <motion.div | |
| className="relative z-10 w-full max-w-sm mx-4" | |
| initial={{ scale: 0.9, opacity: 0, y: 30 }} | |
| animate={{ scale: 1, opacity: 1, y: 0 }} | |
| exit={{ scale: 0.9, opacity: 0, y: 30 }} | |
| transition={{ type: "spring", damping: 25, stiffness: 300 }} | |
| > | |
| {/* Floating avatar β overlaps top edge */} | |
| <div className="flex justify-center -mb-12 relative z-20"> | |
| <div className="relative"> | |
| <div className="h-24 w-24 rounded-full bg-[#e8ddd0] border-4 border-white shadow-lg flex items-center justify-center overflow-hidden"> | |
| <OwlAvatar /> | |
| </div> | |
| {/* Edit badge */} | |
| <button className="absolute bottom-0 right-0 h-7 w-7 rounded-full bg-white shadow-md border border-brand-gray-100 flex items-center justify-center hover:bg-brand-gray-50 transition"> | |
| <svg viewBox="0 0 16 16" className="h-3.5 w-3.5 text-brand-gray-500" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"> | |
| <path d="M11.5 1.5l3 3L5 14H2v-3L11.5 1.5z" /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Glass card body */} | |
| <div className="rounded-2xl border border-white/40 bg-white/80 backdrop-blur-xl shadow-2xl ring-1 ring-white/20 overflow-hidden pt-14 pb-5 px-6"> | |
| {/* Name & title */} | |
| <div className="text-center mb-5"> | |
| <h2 className="font-heading text-xl font-extrabold text-brand-gray-700"> | |
| Level {level} {title} | |
| </h2> | |
| <p className="text-sm text-brand-gray-400 mt-0.5"> | |
| {name} | |
| </p> | |
| </div> | |
| {/* ββ Stats Grid ββ */} | |
| <div className="mb-5"> | |
| <h3 className="font-heading font-bold text-brand-gray-600 text-sm mb-2.5"> | |
| Stats Grid | |
| </h3> | |
| <div className="grid grid-cols-3 gap-2.5"> | |
| <StatBox label="Total XP Earned" value={xp.toLocaleString()} /> | |
| <StatBox label="Longest Streak" value={`${longestStreak} Days`} /> | |
| <StatBox label="Courses Completed" value={String(coursesCompleted)} /> | |
| </div> | |
| </div> | |
| {/* ββ Preferences ββ */} | |
| <div className="mb-4"> | |
| <h3 className="font-heading font-bold text-brand-gray-600 text-sm mb-3"> | |
| Preferences | |
| </h3> | |
| <div className="space-y-3.5"> | |
| {/* Sound Effects */} | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-brand-gray-600">Sound Effects</span> | |
| <Toggle on={soundOn} onChange={() => setPreferences({ soundOn: !soundOn })} /> | |
| </div> | |
| {/* Dark/Light Theme */} | |
| <div className="flex items-center justify-between"> | |
| <span className="text-sm text-brand-gray-600">Dark/Light Theme</span> | |
| <div className="flex items-center gap-2"> | |
| <span className="text-xs text-brand-gray-400 font-medium">Dark/Glass</span> | |
| <Toggle on={darkGlass} onChange={() => setPreferences({ darkGlass: !darkGlass })} /> | |
| </div> | |
| </div> | |
| {/* Difficulty Scaling */} | |
| <div className="flex items-center justify-between gap-4"> | |
| <span className="text-sm text-brand-gray-600 shrink-0">Difficulty Scaling</span> | |
| <input | |
| type="range" | |
| min="0" | |
| max="100" | |
| value={difficulty} | |
| onChange={(e) => setPreferences({ difficulty: Number(e.target.value) })} | |
| className="w-full h-1.5 rounded-full appearance-none cursor-pointer accent-brand-teal bg-brand-gray-200" | |
| /> | |
| </div> | |
| </div> | |
| </div> | |
| {/* ββ Log Out ββ */} | |
| <div className="pt-2 border-t border-brand-gray-100"> | |
| <button | |
| onClick={() => { logout(); onClose(); router.push("/login"); }} | |
| className="w-full text-center text-sm text-brand-teal font-semibold hover:text-brand-teal/70 transition py-2" | |
| > | |
| Log Out | |
| </button> | |
| </div> | |
| </div> | |
| </motion.div> | |
| </motion.div> | |
| </AnimatePresence> | |
| ); | |
| } | |
| /* ββ Stat box ββ */ | |
| function StatBox({ label, value }: { label: string; value: string }) { | |
| return ( | |
| <div className="bg-gradient-to-b from-white/60 to-brand-gray-50 border border-brand-gray-100 rounded-xl px-2 py-3 text-center"> | |
| <p className="text-[10px] text-brand-gray-400 leading-tight mb-1">{label}</p> | |
| <p className="font-heading font-extrabold text-brand-gray-700 text-sm"> | |
| {value} | |
| </p> | |
| </div> | |
| ); | |
| } | |
| /* ββ Toggle switch ββ */ | |
| function Toggle({ on, onChange }: { on: boolean; onChange: () => void }) { | |
| return ( | |
| <button | |
| onClick={onChange} | |
| className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors duration-200 ${ | |
| on ? "bg-brand-teal" : "bg-brand-gray-200" | |
| }`} | |
| > | |
| <span | |
| className={`inline-block h-4.5 w-4.5 rounded-full bg-white shadow-sm transform transition-transform duration-200 ${ | |
| on ? "translate-x-5.5" : "translate-x-1" | |
| }`} | |
| style={{ | |
| width: "18px", | |
| height: "18px", | |
| transform: on ? "translateX(22px)" : "translateX(3px)", | |
| }} | |
| /> | |
| </button> | |
| ); | |
| } | |
| /* ββ Owl avatar (large) ββ */ | |
| function OwlAvatar() { | |
| return ( | |
| <svg viewBox="0 0 96 96" className="h-20 w-20" fill="none"> | |
| {/* Body */} | |
| <ellipse cx="48" cy="56" rx="22" ry="26" fill="#C4A882" /> | |
| <ellipse cx="48" cy="53" rx="17" ry="20" fill="#E8D5B7" /> | |
| {/* Eyes bg */} | |
| <circle cx="39" cy="44" r="8" fill="white" /> | |
| <circle cx="57" cy="44" r="8" fill="white" /> | |
| {/* Eye rims (glasses) */} | |
| <circle cx="39" cy="44" r="8.5" fill="none" stroke="#8B7355" strokeWidth="1.8" /> | |
| <circle cx="57" cy="44" r="8.5" fill="none" stroke="#8B7355" strokeWidth="1.8" /> | |
| <line x1="47.5" y1="44" x2="48.5" y2="44" stroke="#8B7355" strokeWidth="1.8" /> | |
| {/* Pupils */} | |
| <circle cx="40" cy="44" r="4" fill="#333" /> | |
| <circle cx="56" cy="44" r="4" fill="#333" /> | |
| {/* Highlights */} | |
| <circle cx="41.5" cy="42.5" r="1.5" fill="white" /> | |
| <circle cx="57.5" cy="42.5" r="1.5" fill="white" /> | |
| {/* Beak */} | |
| <polygon points="48,50 45,54 51,54" fill="#E8734A" /> | |
| {/* Ear tufts */} | |
| <polygon points="35,32 38,24 42,34" fill="#C4A882" /> | |
| <polygon points="61,32 58,24 54,34" fill="#C4A882" /> | |
| {/* Feet */} | |
| <ellipse cx="42" cy="80" rx="6" ry="2.5" fill="#E8734A" opacity="0.7" /> | |
| <ellipse cx="54" cy="80" rx="6" ry="2.5" fill="#E8734A" opacity="0.7" /> | |
| </svg> | |
| ); | |
| } | |