File size: 7,199 Bytes
f91a684 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 | import React, { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { X, Check } from 'lucide-react';
import { cn } from '@/lib/utils';
export type AvatarConfig = {
id: string;
hue: number;
saturate: number;
glowColor: string;
};
export const AURAS: (AvatarConfig & { label: string })[] = [
{ id: 'default', label: 'Classic', glowColor: 'transparent', hue: 0, saturate: 100 },
{ id: 'neon-cyber', label: 'Neon Cyber', glowColor: '#06b6d4', hue: 160, saturate: 150 },
{ id: 'crimson', label: 'Crimson', glowColor: '#e11d48', hue: 330, saturate: 130 },
{ id: 'matrix', label: 'Matrix', glowColor: '#22c55e', hue: 100, saturate: 200 },
{ id: 'golden', label: 'Golden', glowColor: '#fbbf24', hue: 45, saturate: 140 },
{ id: 'void', label: 'The Void', glowColor: '#a855f7', hue: 280, saturate: 180 },
];
export default function AvatarCustomizerModal({
isOpen,
onClose,
initialConfig,
onSave,
}: {
isOpen: boolean;
onClose: () => void;
initialConfig: AvatarConfig;
onSave: (config: AvatarConfig) => void;
}) {
const [activeTab, setActiveTab] = useState('aura');
const [selectedConfig, setSelectedConfig] = useState<AvatarConfig>(initialConfig);
useEffect(() => {
if (isOpen) setSelectedConfig(initialConfig);
}, [isOpen, initialConfig]);
return (
<AnimatePresence>
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center pointer-events-none p-4 md:p-12 font-sans font-medium">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 bg-black/60 backdrop-blur-md pointer-events-auto"
onClick={onClose}
/>
<motion.div
initial={{ scale: 0.95, opacity: 0, y: 30 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.95, opacity: 0, y: 30 }}
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
className="relative bg-[#0d0f1a] w-full max-w-4xl h-[85vh] md:h-[75vh] rounded-[2.5rem] border border-white/10 shadow-[0_0_80px_rgba(0,0,0,0.8)] overflow-hidden flex flex-col pointer-events-auto"
>
{/* Header */}
<div className="flex justify-between items-center p-6 border-b border-white/5 bg-[#121422]/50">
<h2 className="text-xl font-black text-white px-2">Customize Avatar</h2>
<button
onClick={onClose}
className="p-2 rounded-full hover:bg-white/10 text-slate-400 hover:text-white transition-colors"
>
<X size={24} />
</button>
</div>
{/* Content Split */}
<div className="flex flex-col md:flex-row flex-1 overflow-hidden">
{/* Avatar Stage */}
<div className="flex-1 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.05),transparent_70%)] relative flex items-center justify-center p-8 md:p-12 overflow-hidden border-b md:border-b-0 md:border-r border-white/5">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 25, repeat: Infinity, ease: "linear" }}
className="absolute md:-inset-1/2 -inset-[10%] opacity-60 mix-blend-screen transform-gpu"
style={{
background: selectedConfig.glowColor !== 'transparent' ? `conic-gradient(from 0deg, transparent 0 90deg, ${selectedConfig.glowColor}40 90deg 180deg, transparent 180deg 270deg, ${selectedConfig.glowColor}20 270deg 360deg)` : 'transparent'
}}
/>
<div
className="relative z-10 w-64 h-64 md:w-80 md:h-80 transition-all duration-500 ease-out"
style={{
filter: selectedConfig.glowColor !== 'transparent' ? `drop-shadow(0 0 50px ${selectedConfig.glowColor}80) hue-rotate(${selectedConfig.hue}deg) saturate(${selectedConfig.saturate}%)` : 'none'
}}
>
<img src="/avatar.png" alt="Avatar Preview" className="w-full h-full object-contain" />
</div>
</div>
{/* Drawer Menu */}
<div className="w-full md:w-[400px] flex flex-col bg-[#121422]/70 h-full">
<div className="flex gap-6 p-6 border-b border-white/5 overflow-x-auto no-scrollbar">
{['Aura'].map(tab => (
<button key={tab} onClick={() => setActiveTab(tab.toLowerCase())} className={cn("text-[11px] font-black uppercase tracking-[0.2em] pb-3 border-b-2 transition-colors", activeTab === tab.toLowerCase() ? "border-indigo-400 text-white" : "border-transparent text-slate-500 hover:text-slate-300")}>
{tab}
</button>
))}
</div>
<div className="flex-1 overflow-y-auto p-6 custom-scrollbar">
<div className="grid grid-cols-2 gap-4">
{AURAS.map(aura => (
<button
key={aura.id}
onClick={() => setSelectedConfig(aura)}
className={cn("flex flex-col items-center justify-center p-4 rounded-2xl border transition-all h-28 group", selectedConfig.id === aura.id ? "bg-white/10 border-white/20 shadow-[0_0_25px_rgba(255,255,255,0.05)] scale-[1.02]" : "bg-white/5 border-transparent hover:bg-white/10")}
>
<div
className="w-10 h-10 rounded-full mb-3 shadow-lg group-hover:scale-110 transition-transform"
style={{
background: aura.glowColor === 'transparent' ? 'linear-gradient(135deg, #475569, #1e293b)' : aura.glowColor,
boxShadow: aura.glowColor !== 'transparent' ? `0 0 15px ${aura.glowColor}` : 'none'
}}
/>
<span className={cn("text-xs font-bold transition-colors", selectedConfig.id === aura.id ? "text-white" : "text-slate-400 group-hover:text-slate-300")}>{aura.label}</span>
</button>
))}
</div>
</div>
<div className="p-6 border-t border-white/5 bg-[#0d0f1a]">
<button
onClick={() => { onSave(selectedConfig); onClose(); }}
className="w-full bg-white hover:bg-indigo-400 text-black hover:text-white py-3.5 rounded-full text-sm font-black tracking-wide shadow-[0_0_20px_rgba(255,255,255,0.1)] hover:shadow-[0_0_20px_rgba(99,102,241,0.4)] transition-all flex items-center justify-center gap-2"
>
<Check size={18} strokeWidth={3} /> Apply Appearance
</button>
</div>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
);
}
|