From 5aae999a6527eebd732ca97ea048084b4580cfe3 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 4 Apr 2026 14:07:54 +0200 Subject: [PATCH] feat: Add EmojiIconPicker component and integrate it into Admin pages for icon selection --- frontend/src/components/EmojiIconPicker.jsx | 206 ++++++++++++++++++ frontend/src/pages/AdminFocusAreasPage.jsx | 28 ++- frontend/src/pages/AdminGoalTypesPage.jsx | 14 +- frontend/src/pages/AdminTrainingTypesPage.jsx | 13 +- 4 files changed, 237 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/EmojiIconPicker.jsx diff --git a/frontend/src/components/EmojiIconPicker.jsx b/frontend/src/components/EmojiIconPicker.jsx new file mode 100644 index 0000000..8de5068 --- /dev/null +++ b/frontend/src/components/EmojiIconPicker.jsx @@ -0,0 +1,206 @@ +import { useState, useId } from 'react' +import { ChevronDown, ChevronUp } from 'lucide-react' + +/** + * Kuratierte Emoji-Gruppen für Sport, Körper, Ernährung usw. + * Kann bei Bedarf erweitert werden (z. B. prop `extraGroups`). + */ +export const EMOJI_ICON_GROUPS = [ + { + label: 'Training & Sport', + emojis: [ + '🏃', '🚴', '🏊', '🧘', '🏋️', '⛷️', '🤸', '🥊', '🎾', '⚽', '🏀', '🤺', + '🚶', '🧗', '⛰️', '🏄', '🤿', '🥋', '🤼', '⛹️', '🤾', '🏌️', '🎿', '🧗‍♂️' + ] + }, + { + label: 'Körper & Gesundheit', + emojis: [ + '💪', '❤️', '🫀', '🦵', '🧠', '👁️', '⚖️', '📏', '🩺', '💊', '🌡️', '🫁' + ] + }, + { + label: 'Ernährung', + emojis: [ + '🍎', '🥩', '🥗', '🍽️', '💧', '☕', '🥤', '🥛', '🍌', '🥑', '🍞', '🐟' + ] + }, + { + label: 'Schlaf & Erholung', + emojis: ['😴', '🌙', '🛌', '💤', '🧖', '☀️', '🌿'] + }, + { + label: 'Allgemein', + emojis: [ + '🎯', '📊', '🔥', '⭐', '✨', '🎵', '📌', '🧭', '📝', '✅', '💡', '🪄' + ] + } +] + +/** + * Wiederverwendbare Emoji-/Icon-Auswahl: Vorschau, Freitext (inkl. System-Emoji-Picker), + * optional ausklappbare Vorschläge. + * + * @param {string} value – aktueller Icon-String (meist ein Emoji) + * @param {(next: string) => void} onChange + * @param {string} [placeholder] + * @param {number} [maxLength=10] + * @param {boolean} [disabled] + * @param {string} [id] – optionale ID für das Textfeld (Label for=) + * @param {boolean} [defaultExpanded=false] – Vorschlags-Bereich initial offen + */ +export default function EmojiIconPicker({ + value, + onChange, + placeholder = '📝', + maxLength = 10, + disabled = false, + id: idProp, + defaultExpanded = false +}) { + const uid = useId() + const inputId = idProp || `emoji-icon-${uid}` + const [open, setOpen] = useState(defaultExpanded) + + const handleInput = (e) => { + onChange(e.target.value.slice(0, maxLength)) + } + + const pick = (em) => { + onChange(em.slice(0, maxLength)) + } + + return ( +
+
+ + {value || '·'} + + + + {!!value && !disabled && ( + + )} +
+ {open && ( +
+ {EMOJI_ICON_GROUPS.map((group) => ( +
+
+ {group.label} +
+
+ {group.emojis.map((em) => ( + + ))} +
+
+ ))} +

+ Du kannst auch direkt in das Feld tippen oder das Betriebssystem-Emoji-Menü nutzen + (z. B. Win + . unter Windows). +

+
+ )} +
+ ) +} diff --git a/frontend/src/pages/AdminFocusAreasPage.jsx b/frontend/src/pages/AdminFocusAreasPage.jsx index 0369314..f28df7e 100644 --- a/frontend/src/pages/AdminFocusAreasPage.jsx +++ b/frontend/src/pages/AdminFocusAreasPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' import { Plus, Pencil, Trash2, Save, X, Eye, EyeOff } from 'lucide-react' import { api } from '../utils/api' +import EmojiIconPicker from '../components/EmojiIconPicker' const CATEGORIES = [ { value: 'body_composition', label: 'Körperzusammensetzung' }, @@ -220,15 +221,18 @@ export default function AdminFocusAreasPage() {
-
@@ -332,14 +336,18 @@ export default function AdminFocusAreasPage() {
-
diff --git a/frontend/src/pages/AdminGoalTypesPage.jsx b/frontend/src/pages/AdminGoalTypesPage.jsx index 2a8855e..090cb53 100644 --- a/frontend/src/pages/AdminGoalTypesPage.jsx +++ b/frontend/src/pages/AdminGoalTypesPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' import { Settings, Plus, Pencil, Trash2, Database } from 'lucide-react' import { api } from '../utils/api' +import EmojiIconPicker from '../components/EmojiIconPicker' export default function AdminGoalTypesPage() { const [goalTypes, setGoalTypes] = useState([]) @@ -367,14 +368,15 @@ export default function AdminGoalTypesPage() { />
- - + Icon (Emoji) + + setFormData(f => ({ ...f, icon: e.target.value }))} + onChange={(icon) => setFormData((f) => ({ ...f, icon }))} placeholder="🧘" + maxLength={10} />
diff --git a/frontend/src/pages/AdminTrainingTypesPage.jsx b/frontend/src/pages/AdminTrainingTypesPage.jsx index 8b9ee1b..bec6d0d 100644 --- a/frontend/src/pages/AdminTrainingTypesPage.jsx +++ b/frontend/src/pages/AdminTrainingTypesPage.jsx @@ -3,6 +3,7 @@ import { useNavigate } from 'react-router-dom' import { Pencil, Trash2, Plus, Save, X, ArrowLeft, Settings } from 'lucide-react' import { api } from '../utils/api' import ProfileBuilder from '../components/ProfileBuilder' +import EmojiIconPicker from '../components/EmojiIconPicker' /** * AdminTrainingTypesPage - CRUD for training types @@ -254,13 +255,11 @@ export default function AdminTrainingTypesPage() {
Icon (Emoji)
- setFormData({ ...formData, icon: e.target.value })} + onChange={(icon) => setFormData({ ...formData, icon })} placeholder="🏃" maxLength={10} - style={{ width: '100%' }} />
@@ -495,13 +494,11 @@ export default function AdminTrainingTypesPage() {
Icon (Emoji)
- setFormData({ ...formData, icon: e.target.value })} + onChange={(icon) => setFormData({ ...formData, icon })} placeholder="🏃" maxLength={10} - style={{ width: '100%' }} />