import { useEffect, useMemo, useRef, useState } from 'react'; interface EmojiEntry { e: string; k: string[]; // search keywords (incl. name) } interface Category { label: string; entries: EmojiEntry[]; } // Curated emoji set โ€” small enough to stay fast without a dependency, wide // enough to cover everyday messaging. Keywords are the primary search // surface; the emoji character itself is also matched so a user typing โค๏ธ // literally finds it. const CATEGORIES: Category[] = [ { label: 'Smileys', entries: [ { e: '๐Ÿ˜€', k: ['grin', 'smile', 'happy'] }, { e: '๐Ÿ˜ƒ', k: ['smile', 'happy'] }, { e: '๐Ÿ˜„', k: ['smile', 'laugh'] }, { e: '๐Ÿ˜', k: ['grin', 'smile'] }, { e: '๐Ÿ˜†', k: ['laugh', 'lol'] }, { e: '๐Ÿ˜…', k: ['sweat', 'nervous', 'laugh'] }, { e: '๐Ÿคฃ', k: ['lol', 'rofl', 'laugh'] }, { e: '๐Ÿ˜‚', k: ['joy', 'laugh', 'tears'] }, { e: '๐Ÿ™‚', k: ['smile', 'slight'] }, { e: '๐Ÿ™ƒ', k: ['upside', 'irony'] }, { e: '๐Ÿ˜‰', k: ['wink'] }, { e: '๐Ÿ˜Š', k: ['blush', 'smile'] }, { e: '๐Ÿ˜‡', k: ['angel', 'innocent'] }, { e: '๐Ÿฅฐ', k: ['love', 'hearts'] }, { e: '๐Ÿ˜', k: ['love', 'heart eyes'] }, { e: '๐Ÿคฉ', k: ['star', 'excited'] }, { e: '๐Ÿ˜˜', k: ['kiss'] }, { e: '๐Ÿ˜—', k: ['kiss'] }, { e: '๐Ÿ˜š', k: ['kiss'] }, { e: '๐Ÿ˜™', k: ['kiss'] }, { e: '๐Ÿฅฒ', k: ['tear', 'smile'] }, { e: '๐Ÿ˜‹', k: ['yum', 'tasty'] }, { e: '๐Ÿ˜›', k: ['tongue'] }, { e: '๐Ÿ˜œ', k: ['tongue', 'wink'] }, { e: '๐Ÿคช', k: ['zany', 'silly'] }, { e: '๐Ÿ˜', k: ['tongue'] }, { e: '๐Ÿค‘', k: ['money'] }, { e: '๐Ÿค—', k: ['hug'] }, { e: '๐Ÿคญ', k: ['giggle', 'shy'] }, { e: '๐Ÿคซ', k: ['shush', 'quiet'] }, { e: '๐Ÿค”', k: ['think'] }, { e: '๐Ÿค', k: ['zip', 'quiet'] }, { e: '๐Ÿคจ', k: ['raise brow'] }, { e: '๐Ÿ˜', k: ['neutral'] }, { e: '๐Ÿ˜‘', k: ['expressionless'] }, { e: '๐Ÿ˜ถ', k: ['speechless'] }, { e: '๐Ÿ˜', k: ['smirk'] }, { e: '๐Ÿ˜’', k: ['unamused'] }, { e: '๐Ÿ™„', k: ['eye roll'] }, { e: '๐Ÿ˜ฌ', k: ['grimace', 'awkward'] }, { e: '๐Ÿคฅ', k: ['lying'] }, { e: '๐Ÿ˜”', k: ['sad', 'pensive'] }, { e: '๐Ÿ˜ช', k: ['sleepy'] }, { e: '๐Ÿ˜ด', k: ['sleep'] }, { e: '๐Ÿ˜ท', k: ['mask', 'sick'] }, { e: '๐Ÿค’', k: ['sick', 'fever'] }, { e: '๐Ÿค•', k: ['injured'] }, { e: '๐Ÿคข', k: ['nauseated'] }, { e: '๐Ÿคฎ', k: ['vomit'] }, { e: '๐Ÿคง', k: ['sneeze'] }, { e: '๐Ÿฅต', k: ['hot'] }, { e: '๐Ÿฅถ', k: ['cold'] }, { e: '๐Ÿฅด', k: ['dizzy', 'woozy'] }, { e: '๐Ÿ˜ต', k: ['dizzy'] }, { e: '๐Ÿคฏ', k: ['mind blown'] }, { e: '๐Ÿค ', k: ['cowboy'] }, { e: '๐Ÿฅณ', k: ['party'] }, { e: '๐Ÿ˜Ž', k: ['cool', 'sunglasses'] }, { e: '๐Ÿค“', k: ['nerd'] }, { e: '๐Ÿง', k: ['monocle'] }, { e: '๐Ÿ˜•', k: ['confused'] }, { e: '๐Ÿ˜Ÿ', k: ['worried'] }, { e: '๐Ÿ™', k: ['frown'] }, { e: 'โ˜น๏ธ', k: ['frown'] }, { e: '๐Ÿ˜ฎ', k: ['open mouth'] }, { e: '๐Ÿ˜ฏ', k: ['hushed'] }, { e: '๐Ÿ˜ฒ', k: ['astonished'] }, { e: '๐Ÿ˜ณ', k: ['flushed'] }, { e: '๐Ÿฅบ', k: ['pleading'] }, { e: '๐Ÿ˜ฆ', k: ['frowning'] }, { e: '๐Ÿ˜ง', k: ['anguished'] }, { e: '๐Ÿ˜จ', k: ['fear'] }, { e: '๐Ÿ˜ฐ', k: ['anxious', 'sweat'] }, { e: '๐Ÿ˜ฅ', k: ['sad', 'relieved'] }, { e: '๐Ÿ˜ข', k: ['cry'] }, { e: '๐Ÿ˜ญ', k: ['cry', 'loud'] }, { e: '๐Ÿ˜ฑ', k: ['scream', 'scared'] }, { e: '๐Ÿ˜–', k: ['confounded'] }, { e: '๐Ÿ˜ฃ', k: ['persevere'] }, { e: '๐Ÿ˜ž', k: ['disappointed'] }, { e: '๐Ÿ˜“', k: ['sweat'] }, { e: '๐Ÿ˜ฉ', k: ['weary'] }, { e: '๐Ÿ˜ซ', k: ['tired'] }, { e: '๐Ÿฅฑ', k: ['yawn'] }, { e: '๐Ÿ˜ค', k: ['triumph'] }, { e: '๐Ÿ˜ก', k: ['angry', 'rage'] }, { e: '๐Ÿ˜ ', k: ['angry'] }, { e: '๐Ÿคฌ', k: ['swear', 'curse'] }, { e: '๐Ÿ˜ˆ', k: ['devil'] }, { e: '๐Ÿ‘ฟ', k: ['imp'] }, { e: '๐Ÿ’€', k: ['skull', 'dead'] }, { e: '๐Ÿคก', k: ['clown'] }, { e: '๐Ÿ‘ป', k: ['ghost'] }, { e: '๐Ÿ‘ฝ', k: ['alien'] }, { e: '๐Ÿค–', k: ['robot'] }, { e: '๐Ÿ’ฉ', k: ['poop', 'shit'] }, ], }, { label: 'Gestures', entries: [ { e: '๐Ÿ‘‹', k: ['wave', 'hi'] }, { e: '๐Ÿคš', k: ['hand'] }, { e: '๐Ÿ–๏ธ', k: ['hand'] }, { e: 'โœ‹', k: ['stop', 'high five'] }, { e: '๐Ÿ––', k: ['spock'] }, { e: '๐Ÿ‘Œ', k: ['ok'] }, { e: '๐ŸคŒ', k: ['pinch'] }, { e: '๐Ÿค', k: ['small'] }, { e: 'โœŒ๏ธ', k: ['peace', 'victory'] }, { e: '๐Ÿคž', k: ['crossed fingers'] }, { e: '๐ŸคŸ', k: ['love you'] }, { e: '๐Ÿค˜', k: ['rock'] }, { e: '๐Ÿค™', k: ['call me'] }, { e: '๐Ÿ‘ˆ', k: ['point left'] }, { e: '๐Ÿ‘‰', k: ['point right'] }, { e: '๐Ÿ‘†', k: ['point up'] }, { e: '๐Ÿ–•', k: ['middle finger', 'fuck'] }, { e: '๐Ÿ‘‡', k: ['point down'] }, { e: 'โ˜๏ธ', k: ['point up'] }, { e: '๐Ÿ‘', k: ['thumbs up', 'like'] }, { e: '๐Ÿ‘Ž', k: ['thumbs down', 'dislike'] }, { e: 'โœŠ', k: ['fist'] }, { e: '๐Ÿ‘Š', k: ['punch'] }, { e: '๐Ÿค›', k: ['fist left'] }, { e: '๐Ÿคœ', k: ['fist right'] }, { e: '๐Ÿ‘', k: ['clap'] }, { e: '๐Ÿ™Œ', k: ['raised hands'] }, { e: '๐Ÿ‘', k: ['open hands'] }, { e: '๐Ÿคฒ', k: ['palms'] }, { e: '๐Ÿ™', k: ['pray', 'thanks'] }, { e: 'โœ๏ธ', k: ['write'] }, { e: '๐Ÿ’ช', k: ['flex', 'strong'] }, ], }, { label: 'Hearts', entries: [ { e: 'โค๏ธ', k: ['heart', 'love'] }, { e: '๐Ÿงก', k: ['orange heart'] }, { e: '๐Ÿ’›', k: ['yellow heart'] }, { e: '๐Ÿ’š', k: ['green heart'] }, { e: '๐Ÿ’™', k: ['blue heart'] }, { e: '๐Ÿ’œ', k: ['purple heart'] }, { e: '๐Ÿ–ค', k: ['black heart'] }, { e: '๐Ÿค', k: ['white heart'] }, { e: '๐ŸคŽ', k: ['brown heart'] }, { e: '๐Ÿ’”', k: ['broken heart'] }, { e: 'โฃ๏ธ', k: ['heart exclamation'] }, { e: '๐Ÿ’•', k: ['hearts'] }, { e: '๐Ÿ’ž', k: ['revolving hearts'] }, { e: '๐Ÿ’“', k: ['beating heart'] }, { e: '๐Ÿ’—', k: ['growing heart'] }, { e: '๐Ÿ’–', k: ['sparkle heart'] }, { e: '๐Ÿ’˜', k: ['cupid'] }, { e: '๐Ÿ’', k: ['heart gift'] }, ], }, { label: 'Animals & Food', entries: [ { e: '๐Ÿถ', k: ['dog'] }, { e: '๐Ÿฑ', k: ['cat'] }, { e: '๐Ÿญ', k: ['mouse'] }, { e: '๐Ÿน', k: ['hamster'] }, { e: '๐Ÿฐ', k: ['rabbit'] }, { e: '๐ŸฆŠ', k: ['fox'] }, { e: '๐Ÿป', k: ['bear'] }, { e: '๐Ÿผ', k: ['panda'] }, { e: '๐Ÿจ', k: ['koala'] }, { e: '๐Ÿฏ', k: ['tiger'] }, { e: '๐Ÿฆ', k: ['lion'] }, { e: '๐Ÿธ', k: ['frog'] }, { e: '๐Ÿต', k: ['monkey'] }, { e: '๐Ÿ”', k: ['chicken'] }, { e: '๐Ÿง', k: ['penguin'] }, { e: '๐Ÿฆ', k: ['bird'] }, { e: '๐Ÿฆ†', k: ['duck'] }, { e: '๐ŸŽ', k: ['apple'] }, { e: '๐ŸŒ', k: ['banana'] }, { e: '๐Ÿ•', k: ['pizza'] }, { e: '๐Ÿ”', k: ['burger'] }, { e: '๐ŸŸ', k: ['fries'] }, { e: '๐ŸŒญ', k: ['hotdog'] }, { e: '๐Ÿฟ', k: ['popcorn'] }, { e: '๐Ÿฃ', k: ['sushi'] }, { e: '๐Ÿฉ', k: ['donut'] }, { e: '๐Ÿช', k: ['cookie'] }, { e: '๐ŸŽ‚', k: ['cake', 'birthday'] }, { e: '๐Ÿฐ', k: ['cake'] }, { e: '๐Ÿซ', k: ['chocolate'] }, { e: '๐Ÿบ', k: ['beer'] }, { e: '๐Ÿท', k: ['wine'] }, { e: '๐Ÿฅ‚', k: ['cheers'] }, { e: 'โ˜•', k: ['coffee'] }, ], }, { label: 'Objects & Symbols', entries: [ { e: '๐Ÿ”ฅ', k: ['fire', 'lit'] }, { e: 'โœจ', k: ['sparkle'] }, { e: 'โญ', k: ['star'] }, { e: '๐ŸŒŸ', k: ['star glowing'] }, { e: '๐Ÿ’ซ', k: ['dizzy'] }, { e: '๐Ÿ’ฅ', k: ['boom', 'explosion'] }, { e: 'โšก', k: ['lightning'] }, { e: 'โ˜€๏ธ', k: ['sun'] }, { e: '๐ŸŒˆ', k: ['rainbow'] }, { e: 'โ˜๏ธ', k: ['cloud'] }, { e: '๐ŸŒง๏ธ', k: ['rain'] }, { e: 'โ„๏ธ', k: ['snow'] }, { e: '๐ŸŽ‰', k: ['party', 'tada'] }, { e: '๐ŸŽŠ', k: ['confetti'] }, { e: '๐ŸŽ', k: ['gift'] }, { e: '๐ŸŽˆ', k: ['balloon'] }, { e: '๐Ÿ’ฏ', k: ['100', 'perfect'] }, { e: 'โœ…', k: ['check'] }, { e: 'โŒ', k: ['x', 'no'] }, { e: 'โš ๏ธ', k: ['warning'] }, { e: 'โ“', k: ['question'] }, { e: 'โ—', k: ['exclamation'] }, { e: '๐Ÿ’ฌ', k: ['speech'] }, { e: '๐Ÿ’ญ', k: ['thought'] }, { e: '๐Ÿ‘€', k: ['eyes'] }, { e: '๐Ÿš€', k: ['rocket'] }, { e: '๐ŸŽต', k: ['music'] }, { e: '๐ŸŽถ', k: ['music'] }, { e: '๐Ÿ””', k: ['bell'] }, { e: '๐Ÿ’ก', k: ['idea', 'bulb'] }, ], }, ]; const RECENT_KEY = 'chat.emoji.recents.v1'; const RECENT_MAX = 24; function loadRecents(): string[] { try { const raw = localStorage.getItem(RECENT_KEY); if (!raw) return []; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed.filter((x): x is string => typeof x === 'string').slice(0, RECENT_MAX); } catch { return []; } } function saveRecents(list: string[]): void { try { localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, RECENT_MAX))); } catch { /* ignore quota */ } } interface Props { open: boolean; onPick: (emoji: string) => void; onClose: () => void; } export function EmojiPicker({ open, onPick, onClose }: Props) { const [query, setQuery] = useState(''); const [recents, setRecents] = useState(() => loadRecents()); const rootRef = useRef(null); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); onClose(); } }; const onDown = (e: MouseEvent) => { const t = e.target as HTMLElement | null; if (rootRef.current && t && !rootRef.current.contains(t) && !t.closest('[data-emoji-trigger]')) { onClose(); } }; window.addEventListener('keydown', onKey); window.addEventListener('mousedown', onDown); return () => { window.removeEventListener('keydown', onKey); window.removeEventListener('mousedown', onDown); }; }, [open, onClose]); useEffect(() => { if (open) setQuery(''); }, [open]); const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return CATEGORIES; return CATEGORIES.map((cat) => ({ label: cat.label, entries: cat.entries.filter( (entry) => entry.e.includes(q) || entry.k.some((k) => k.includes(q)) || cat.label.toLowerCase().includes(q), ), })).filter((cat) => cat.entries.length > 0); }, [query]); if (!open) return null; const handlePick = (emoji: string) => { onPick(emoji); const next = [emoji, ...recents.filter((e) => e !== emoji)].slice(0, RECENT_MAX); setRecents(next); saveRecents(next); }; return (
setQuery(e.target.value)} placeholder="Suchenโ€ฆ" className="w-full rounded-md border border-line bg-surface-3 px-2.5 py-1.5 text-sm text-fg placeholder-fg-muted outline-none focus:border-accent" />
{recents.length > 0 && !query && ( ({ e, k: [] }))} onPick={handlePick} /> )} {filtered.map((cat) => ( ))} {filtered.length === 0 && (

Keine Treffer

)}
); } function CategoryBlock({ label, entries, onPick, }: { label: string; entries: EmojiEntry[]; onPick: (emoji: string) => void; }) { return (

{label}

{entries.map((entry, idx) => ( ))}
); }