![]() 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/ui/ |
import React, { useState, useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
interface EmojiPickerProps {
onEmojiSelect: (emoji: string) => void;
isOpen: boolean;
onClose: () => void;
position?: 'top' | 'bottom';
}
const EMOJI_CATEGORIES = {
'Smileys & People': ['๐', '๐', '๐', '๐', '๐', '๐
', '๐', '๐คฃ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐ฅฐ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐คช', '๐คจ', '๐ง', '๐ค', '๐', '๐คฉ', '๐ฅณ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', 'โน๏ธ', '๐ฃ', '๐', '๐ซ', '๐ฉ', '๐ฅบ', '๐ข', '๐ญ', '๐ค', '๐ ', '๐ก', '๐คฌ', '๐คฏ', '๐ณ', '๐ฅต', '๐ฅถ', '๐ฑ', '๐จ', '๐ฐ', '๐ฅ', '๐', '๐ค', '๐ค', '๐คญ', '๐คซ', '๐คฅ', '๐ถ', '๐', '๐', '๐ฌ', '๐', '๐ฏ', '๐ฆ', '๐ง', '๐ฎ', '๐ฒ', '๐ฅฑ', '๐ด', '๐คค', '๐ช', '๐ต', '๐ค', '๐ฅด', '๐คข', '๐คฎ', '๐คง', '๐ท', '๐ค', '๐ค'],
'Hearts & Love': ['โค๏ธ', '๐งก', '๐', '๐', '๐', '๐', '๐ค', '๐ค', '๐ค', '๐', 'โฃ๏ธ', '๐', '๐', '๐', '๐', '๐', '๐', '๐', '๐'],
'Gestures': ['๐', '๐', '๐', '๐ค', '๐ค', 'โ๏ธ', '๐ค', '๐ค', '๐ค', '๐ค', '๐', '๐', '๐', '๐', '๐', 'โ๏ธ', '๐', '๐', '๐', '๐คฒ', '๐ค', '๐'],
'Celebrations': ['๐', '๐', '๐ฅณ', '๐', '๐', '๐', '๐ฅ', '๐๏ธ', '๐
', 'โญ', '๐', 'โจ', '๐ฅ', '๐ฏ'],
'Objects': ['๐ฌ', '๐ญ', '๐ก', '๐', '๐', '๐ข', '๐ฃ', '๐ฏ', '๐ฏ', '๐ช', '๐ญ', '๐จ', '๐ฌ', '๐ท', '๐ฑ', '๐ป', 'โ', '๐', '๐', '๐', 'โ๏ธ', '๐๏ธ'],
'Nature': ['๐', '๐', 'โญ', '๐', 'โ๏ธ', 'โ
', '๐ค๏ธ', 'โ๏ธ', '๐ฆ๏ธ', '๐ง๏ธ', 'โ๏ธ', '๐ฉ๏ธ', 'โ๏ธ', '๐ฅ', '๐ง', '๐', '๐', '๐', '๐', '๐ธ', '๐บ', '๐ป', '๐ท', '๐น', '๐ด', '๐ฒ', '๐ณ']
};
const EmojiPicker: React.FC<EmojiPickerProps> = ({
onEmojiSelect,
isOpen,
onClose,
position = 'bottom'
}) => {
const [selectedCategory, setSelectedCategory] = useState('Smileys & People');
const [searchTerm, setSearchTerm] = useState('');
const pickerRef = useRef<HTMLDivElement>(null);
// Close picker when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (pickerRef.current && !pickerRef.current.contains(event.target as Node)) {
onClose();
}
};
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen, onClose]);
// Filter emojis based on search
const filteredEmojis = searchTerm
? Object.values(EMOJI_CATEGORIES).flat().filter(emoji =>
emoji.includes(searchTerm) ||
getEmojiName(emoji).toLowerCase().includes(searchTerm.toLowerCase())
)
: EMOJI_CATEGORIES[selectedCategory as keyof typeof EMOJI_CATEGORIES];
const handleEmojiClick = (emoji: string) => {
// Prevent event bubbling to avoid form submission
onEmojiSelect(emoji);
// Don't auto-close for better UX
};
return (
<AnimatePresence>
{isOpen && (
<motion.div
ref={pickerRef}
initial={{ opacity: 0, scale: 0.9, y: position === 'top' ? 10 : -10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: position === 'top' ? 10 : -10 }}
transition={{ duration: 0.15 }}
className={`absolute z-50 w-80 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl ${
position === 'top' ? 'bottom-full mb-2' : 'top-full mt-2'
}`}
>
{/* Header with search bar and close button */}
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<input
type="text"
placeholder="Search emojis..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="flex-1 px-3 py-2 bg-gray-100 dark:bg-gray-700 border-0 rounded-md text-sm focus:ring-2 focus:ring-blue-500 focus:outline-none"
autoFocus
/>
<button
type="button"
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
title="Close"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
{/* Category tabs */}
{!searchTerm && (
<div className="flex overflow-x-auto border-b border-gray-200 dark:border-gray-700">
{Object.keys(EMOJI_CATEGORIES).map((category) => (
<button
key={category}
onClick={() => setSelectedCategory(category)}
className={`px-3 py-2 text-xs font-medium whitespace-nowrap border-b-2 transition-colors ${
selectedCategory === category
? 'border-blue-500 text-blue-600 dark:text-blue-400'
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{category.split(' ')[0]}
</button>
))}
</div>
)}
{/* Emoji grid */}
<div className="p-2 max-h-64 overflow-y-auto">
<div className="grid grid-cols-8 gap-1">
{filteredEmojis.map((emoji, index) => (
<button
key={`${emoji}-${index}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEmojiClick(emoji);
}}
className="p-2 text-xl hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
title={getEmojiName(emoji)}
>
{emoji}
</button>
))}
</div>
{filteredEmojis.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
<p className="text-sm">No emojis found</p>
</div>
)}
</div>
{/* Recent emojis section */}
<div className="border-t border-gray-200 dark:border-gray-700 p-2">
<div className="flex items-center gap-1">
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">Recent:</span>
{getRecentEmojis().map((emoji, index) => (
<button
key={`recent-${emoji}-${index}`}
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleEmojiClick(emoji);
}}
className="p-1 text-lg hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
title={getEmojiName(emoji)}
>
{emoji}
</button>
))}
</div>
</div>
</motion.div>
)}
</AnimatePresence>
);
};
// Helper function to get emoji name (simplified)
function getEmojiName(emoji: string): string {
const emojiNames: { [key: string]: string } = {
'๐': 'grinning face',
'๐': 'grinning face with big eyes',
'๐': 'grinning face with smiling eyes',
'๐': 'beaming face with smiling eyes',
'๐': 'grinning squinting face',
'๐
': 'grinning face with sweat',
'๐': 'face with tears of joy',
'๐คฃ': 'rolling on the floor laughing',
'๐': 'smiling face with smiling eyes',
'๐': 'smiling face with heart-eyes',
'๐ฅฐ': 'smiling face with hearts',
'๐': 'face blowing a kiss',
'๐': 'thumbs up',
'๐': 'thumbs down',
'โค๏ธ': 'red heart',
'๐': 'party popper',
'๐ฅ': 'fire',
'๐ฏ': 'hundred points symbol',
// Add more as needed
};
return emojiNames[emoji] || emoji;
}
// Helper function to get recent emojis from localStorage
function getRecentEmojis(): string[] {
if (typeof window === 'undefined') return ['๐', 'โค๏ธ', '๐', '๐'];
try {
const recent = localStorage.getItem('recentEmojis');
return recent ? JSON.parse(recent) : ['๐', 'โค๏ธ', '๐', '๐'];
} catch {
return ['๐', 'โค๏ธ', '๐', '๐'];
}
}
// Helper function to save recent emoji
export function saveRecentEmoji(emoji: string) {
if (typeof window === 'undefined') return;
try {
const recent = getRecentEmojis();
const filtered = recent.filter(e => e !== emoji);
const updated = [emoji, ...filtered].slice(0, 8);
localStorage.setItem('recentEmojis', JSON.stringify(updated));
} catch {
// Ignore localStorage errors
}
}
export default EmojiPicker;