207 lines
5.9 KiB
React
207 lines
5.9 KiB
React
|
|
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 (
|
|||
|
|
<div className="emoji-icon-picker" style={{ width: '100%' }}>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
gap: 8,
|
|||
|
|
alignItems: 'center',
|
|||
|
|
flexWrap: 'wrap'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<span
|
|||
|
|
style={{
|
|||
|
|
fontSize: 28,
|
|||
|
|
lineHeight: 1,
|
|||
|
|
minWidth: 40,
|
|||
|
|
textAlign: 'center',
|
|||
|
|
opacity: value ? 1 : 0.35
|
|||
|
|
}}
|
|||
|
|
aria-hidden
|
|||
|
|
>
|
|||
|
|
{value || '·'}
|
|||
|
|
</span>
|
|||
|
|
<input
|
|||
|
|
id={inputId}
|
|||
|
|
type="text"
|
|||
|
|
className="form-input"
|
|||
|
|
style={{ flex: 1, minWidth: 120 }}
|
|||
|
|
value={value}
|
|||
|
|
onChange={handleInput}
|
|||
|
|
placeholder={placeholder}
|
|||
|
|
maxLength={maxLength}
|
|||
|
|
disabled={disabled}
|
|||
|
|
autoComplete="off"
|
|||
|
|
spellCheck={false}
|
|||
|
|
inputMode="text"
|
|||
|
|
/>
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="btn btn-secondary"
|
|||
|
|
disabled={disabled}
|
|||
|
|
onClick={() => setOpen((o) => !o)}
|
|||
|
|
style={{
|
|||
|
|
display: 'inline-flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 4,
|
|||
|
|
padding: '8px 12px',
|
|||
|
|
fontSize: 13
|
|||
|
|
}}
|
|||
|
|
aria-expanded={open}
|
|||
|
|
>
|
|||
|
|
{open ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
|
|||
|
|
Vorschläge
|
|||
|
|
</button>
|
|||
|
|
{!!value && !disabled && (
|
|||
|
|
<button
|
|||
|
|
type="button"
|
|||
|
|
className="btn btn-secondary"
|
|||
|
|
onClick={() => onChange('')}
|
|||
|
|
style={{ padding: '8px 12px', fontSize: 13 }}
|
|||
|
|
>
|
|||
|
|
Leeren
|
|||
|
|
</button>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
{open && (
|
|||
|
|
<div
|
|||
|
|
role="listbox"
|
|||
|
|
aria-label="Emoji-Vorschläge"
|
|||
|
|
style={{
|
|||
|
|
marginTop: 10,
|
|||
|
|
padding: 12,
|
|||
|
|
background: 'var(--surface2)',
|
|||
|
|
borderRadius: 12,
|
|||
|
|
border: '1px solid var(--border)',
|
|||
|
|
maxHeight: 280,
|
|||
|
|
overflowY: 'auto'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{EMOJI_ICON_GROUPS.map((group) => (
|
|||
|
|
<div key={group.label} style={{ marginBottom: 14 }}>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
fontSize: 11,
|
|||
|
|
fontWeight: 600,
|
|||
|
|
color: 'var(--text3)',
|
|||
|
|
marginBottom: 8,
|
|||
|
|
textTransform: 'uppercase',
|
|||
|
|
letterSpacing: '0.04em'
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{group.label}
|
|||
|
|
</div>
|
|||
|
|
<div
|
|||
|
|
style={{
|
|||
|
|
display: 'flex',
|
|||
|
|
flexWrap: 'wrap',
|
|||
|
|
gap: 6
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{group.emojis.map((em) => (
|
|||
|
|
<button
|
|||
|
|
key={`${group.label}-${em}`}
|
|||
|
|
type="button"
|
|||
|
|
onClick={() => pick(em)}
|
|||
|
|
disabled={disabled}
|
|||
|
|
title={em}
|
|||
|
|
aria-label={`Icon wählen: ${em}`}
|
|||
|
|
style={{
|
|||
|
|
fontSize: 22,
|
|||
|
|
lineHeight: 1,
|
|||
|
|
padding: '8px 10px',
|
|||
|
|
border: `1px solid ${value === em ? 'var(--accent)' : 'var(--border)'}`,
|
|||
|
|
borderRadius: 8,
|
|||
|
|
background:
|
|||
|
|
value === em ? 'var(--accent-light)' : 'var(--surface)',
|
|||
|
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
|||
|
|
opacity: disabled ? 0.5 : 1
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{em}
|
|||
|
|
</button>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
<p style={{ fontSize: 11, color: 'var(--text3)', margin: 0, lineHeight: 1.5 }}>
|
|||
|
|
Du kannst auch direkt in das Feld tippen oder das Betriebssystem-Emoji-Menü nutzen
|
|||
|
|
(z. B. Win + . unter Windows).
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|