diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4154551..c0f45f8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.2.0", "dependencies": { "@types/prismjs": "^1.26.6", + "emoji-picker-react": "^4.12.0", "framer-motion": "^11.15.0", "lucide-react": "^0.468.0", "prismjs": "^1.30.0", @@ -2101,6 +2102,21 @@ "dev": true, "license": "ISC" }, + "node_modules/emoji-picker-react": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.0.tgz", + "integrity": "sha512-q2c8UcZH0eRIMj41bj0k1akTjk69tsu+E7EzkW7giN66iltF6H9LQvQvw6ugscsxdC+1lmt3WZpQkkY65J95tg==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -2302,6 +2318,12 @@ "node": ">=8" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 935837f..2d22bc8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@types/prismjs": "^1.26.6", + "emoji-picker-react": "4.12.0", "framer-motion": "^11.15.0", "lucide-react": "^0.468.0", "prismjs": "^1.30.0", diff --git a/frontend/src/features/workspace/DocumentEditor.tsx b/frontend/src/features/workspace/DocumentEditor.tsx index b88c4e8..19d97ae 100644 --- a/frontend/src/features/workspace/DocumentEditor.tsx +++ b/frontend/src/features/workspace/DocumentEditor.tsx @@ -44,63 +44,7 @@ export interface DocumentEditorProps { onSaveStatusChange?: (status: DocSaveStatus, errorMsg?: string) => void; } -interface EmojiItem { - emoji: string; - category: string; - keywords: string; -} - -const EMOJI_CATALOG: EmojiItem[] = [ - // Notizen & Dokumente - { emoji: '📝', category: 'Notizen', keywords: 'notiz schreiben memo text note' }, - { emoji: '📄', category: 'Notizen', keywords: 'dokument seite page doc' }, - { emoji: '📋', category: 'Notizen', keywords: 'clipboard klemmbrett liste' }, - { emoji: '📂', category: 'Notizen', keywords: 'ordner mappe folder datei' }, - { emoji: '📁', category: 'Notizen', keywords: 'ordner folder directory' }, - { emoji: '📌', category: 'Notizen', keywords: 'pin anheften bookmark wichtig' }, - { emoji: '📍', category: 'Notizen', keywords: 'standort ort pin location' }, - { emoji: '🏷️', category: 'Notizen', keywords: 'tag etikett label markierung' }, - { emoji: '📚', category: 'Notizen', keywords: 'buch bibliothek lesen studium' }, - { emoji: '📖', category: 'Notizen', keywords: 'buch open book wissen' }, - - // Ideen & Fokus - { emoji: '🧠', category: 'Ideen', keywords: 'gehirn brain denken wissen thought mind' }, - { emoji: '💡', category: 'Ideen', keywords: 'idee inspiration lampe glühbirne idea' }, - { emoji: '🎯', category: 'Ideen', keywords: 'ziel target fokus mission okr' }, - { emoji: '🚀', category: 'Ideen', keywords: 'rakete start launch projekt rocket' }, - { emoji: '⚡', category: 'Ideen', keywords: 'blitz energie schnell quick power' }, - { emoji: '🔥', category: 'Ideen', keywords: 'feuer flame priorität hot wichtig' }, - { emoji: '⭐', category: 'Ideen', keywords: 'stern favoriten star top' }, - { emoji: '✨', category: 'Ideen', keywords: 'funkeln magie neu neuigkeit sparkles' }, - { emoji: '🧩', category: 'Ideen', keywords: 'puzzle teil lösung problem' }, - { emoji: '🔬', category: 'Ideen', keywords: 'mikroskop forschung analyse test' }, - - // Tech & Dev - { emoji: '💻', category: 'Tech', keywords: 'laptop computer dev code programming' }, - { emoji: '🖥️', category: 'Tech', keywords: 'desktop monitor bildschirm workstation' }, - { emoji: '🛠️', category: 'Tech', keywords: 'werkzeug tools optionen settings einstellungen' }, - { emoji: '⚙️', category: 'Tech', keywords: 'zahnrad config settings system' }, - { emoji: '📊', category: 'Tech', keywords: 'diagramm chart daten statistik dashboard' }, - { emoji: '📈', category: 'Tech', keywords: 'wachstum trend kurve business' }, - { emoji: '🔒', category: 'Tech', keywords: 'schloss sicherheit geheim privat secure' }, - { emoji: '🔑', category: 'Tech', keywords: 'schlüssel key passwort access' }, - { emoji: '🌐', category: 'Tech', keywords: 'web internet network welt globus' }, - { emoji: '📦', category: 'Tech', keywords: 'paket box release lieferung package' }, - - // Alltag & Organisation - { emoji: '💬', category: 'Alltag', keywords: 'chat nachricht kommentar call sprechen' }, - { emoji: '✉️', category: 'Alltag', keywords: 'mail email brief post nachricht' }, - { emoji: '📅', category: 'Alltag', keywords: 'kalender datum termin plan event' }, - { emoji: '⏰', category: 'Alltag', keywords: 'wecker uhr zeit deadline alarm' }, - { emoji: '🛒', category: 'Alltag', keywords: 'warenkorb einkauf shopping liste' }, - { emoji: '☕', category: 'Alltag', keywords: 'kaffee pause bistro morning' }, - { emoji: '🏠', category: 'Alltag', keywords: 'haus heim home wohnung' }, - { emoji: '🏢', category: 'Alltag', keywords: 'büro firma arbeit office work' }, - { emoji: '✈️', category: 'Alltag', keywords: 'flugzeug reise urlaub trip travel' }, - { emoji: '🎨', category: 'Alltag', keywords: 'kunst farbe design creative' }, - { emoji: '🏆', category: 'Alltag', keywords: 'pokal erfolg win trophy award' }, - { emoji: '✅', category: 'Alltag', keywords: 'haken check done erledigt' }, -]; +import { EmojiPickerModal } from './EmojiPickerModal'; function ensureNonEmptyBlocks(docId: string, blocks?: DocumentBlock[]): DocumentBlock[] { if (!blocks || blocks.length === 0) { @@ -250,21 +194,12 @@ export const DocumentEditor: React.FC = ({ const [loading, setLoading] = useState(!initialDoc); const [error, setError] = useState(null); const [showEmojiPicker, setShowEmojiPicker] = useState(false); - const [emojiSearch, setEmojiSearch] = useState(''); const [zoomedBlockId, setZoomedBlockId] = useState(null); // Sync engine (operation log): stores every change locally and syncs it with the server const engineRef = useRef(null); const [engineState, setEngineState] = useState(null); - const filteredEmojis = useMemo(() => { - const q = emojiSearch.trim().toLowerCase(); - if (!q) return EMOJI_CATALOG; - return EMOJI_CATALOG.filter( - (item) => item.keywords.includes(q) || item.emoji.includes(q) || item.category.toLowerCase().includes(q) - ); - }, [emojiSearch]); - const titleInputRef = useRef(null); const outlinerTreeRef = useRef(null); @@ -1044,72 +979,11 @@ export const DocumentEditor: React.FC = ({ {doc?.icon || '📝'} {showEmojiPicker && canEdit && ( -
e.stopPropagation()} - > -
- - setEmojiSearch(e.target.value)} - placeholder="Icon suchen oder Emoji eingeben…" - className="input-field w-full pl-7 pr-2 py-1 text-xs" - /> -
- {doc?.icon && ( - - )} - {emojiSearch.trim() && ( - - )} -
- {filteredEmojis.map((item) => ( - - ))} - {filteredEmojis.length === 0 && ( -
- Kein passendes Standard-Icon -
- )} -
-
+ setShowEmojiPicker(false)} + /> )} diff --git a/frontend/src/features/workspace/EmojiPickerModal.tsx b/frontend/src/features/workspace/EmojiPickerModal.tsx new file mode 100644 index 0000000..9646fbc --- /dev/null +++ b/frontend/src/features/workspace/EmojiPickerModal.tsx @@ -0,0 +1,226 @@ +import React, { Suspense, useEffect, useRef, useState } from 'react'; +import { X, Trash2, Loader2, Clock } from 'lucide-react'; +import type { EmojiClickData } from 'emoji-picker-react'; +import { EmojiStyle, Theme, SuggestionMode } from 'emoji-picker-react'; + +// Lazy-load emoji-picker-react so the main bundle size stays small +const EmojiPicker = React.lazy(() => import('emoji-picker-react')); + +const RECENT_EMOJIS_STORAGE_KEY = 'mindsift_recent_emojis'; +const MAX_RECENT_EMOJIS = 10; + +function getStoredRecentEmojis(): string[] { + try { + const raw = localStorage.getItem(RECENT_EMOJIS_STORAGE_KEY); + if (!raw) return ['📝', '💡', '🚀', '⭐', '🔥', '🎯', '📚', '💻']; + const parsed = JSON.parse(raw); + if (Array.isArray(parsed) && parsed.length > 0) { + return parsed.slice(0, MAX_RECENT_EMOJIS); + } + } catch { + // fallback + } + return ['📝', '💡', '🚀', '⭐', '🔥', '🎯', '📚', '💻']; +} + +function saveRecentEmoji(emoji: string): void { + try { + const current = getStoredRecentEmojis(); + const updated = [emoji, ...current.filter((e) => e !== emoji)].slice(0, MAX_RECENT_EMOJIS); + localStorage.setItem(RECENT_EMOJIS_STORAGE_KEY, JSON.stringify(updated)); + } catch { + // ignore + } +} + +interface EmojiPickerModalProps { + currentIcon?: string; + onSelectEmoji: (emoji: string) => void; + onClose: () => void; +} + +export const EmojiPickerModal: React.FC = ({ + currentIcon, + onSelectEmoji, + onClose, +}) => { + const [recentEmojis, setRecentEmojis] = useState(getStoredRecentEmojis); + const [isDark, setIsDark] = useState(() => + typeof document !== 'undefined' + ? document.documentElement.classList.contains('dark') + : true + ); + + const containerRef = useRef(null); + + // Sync theme changes + useEffect(() => { + const observer = new MutationObserver(() => { + setIsDark(document.documentElement.classList.contains('dark')); + }); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class', 'data-theme'], + }); + return () => observer.disconnect(); + }, []); + + // Close on Escape or click outside (desktop popover) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + } + }; + + const handleClickOutside = (e: MouseEvent) => { + if ( + containerRef.current && + !containerRef.current.contains(e.target as Node) + ) { + onClose(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + document.addEventListener('mousedown', handleClickOutside); + return () => { + window.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [onClose]); + + const handlePick = (emoji: string) => { + saveRecentEmoji(emoji); + setRecentEmojis(getStoredRecentEmojis()); + onSelectEmoji(emoji); + }; + + const handlePickerEmojiClick = (emojiData: EmojiClickData) => { + handlePick(emojiData.emoji); + }; + + return ( + <> + {/* Mobile Backdrop (< 640px) */} +