README / components /shared /ProfileModal.tsx
kaigiii's picture
Deploy Learn8 Demo Space
5c920e9
"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>
);
}