Emojis zum Anklicken, und Wuensche als Forums-Beitraege mit Tags
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

Ueberall, wo ein Emoji nach Discord geht — Rollen-Menues, Ticket-Anliegen,
Wunsch-Bereiche, Portal-Kacheln — stand ein leeres Textfeld, in das man mit
Win+. hineintippen musste. Jetzt sitzt daneben ein Knopf mit einer Tafel:
sieben Gruppen, deutsche Suchbegriffe ("auto", "geld", "warnung"), ein Klick
schreibt ins Feld. Das Feld bleibt daneben stehen, denn Server-Emojis
(<:name:123>) kennt nur Discord.

Bewusst eine eigene Liste statt einer Bibliothek: die vollstaendige
Unicode-Tabelle waere ein halbes Megabyte fuer eine Handvoll Symbole, und die
Suchbegriffe duerfen deutsch sein.

## Forum-Tags

Ist der Voting-Kanal ein Forum, wird jeder Wunsch jetzt ein Forums-Beitrag
statt Nachricht-plus-Thread — und bekommt Tags fuer Bereich und Stand. Damit
laesst sich auch in Discord filtern, wofuer Tags ja da sind. Beim Wechsel des
Stands zieht das Tag mit: der alte fliegt raus, der neue kommt rein, der
Bereich bleibt stehen.

Zugeordnet wird ueber den Namen. Wer im Forum ein Tag "Fahrzeuge" anlegt,
bekommt es automatisch gesetzt; wer keines anlegt, verliert nichts. Bewusst
keine ID-Zuordnung im Panel — das waere eine zweite Liste, die man pflegen
muss und die beim ersten Umbenennen auseinanderlaeuft.

Dabei aufgefallen und mitgefixt: im Forum liegt die Startnachricht im Beitrag
selbst und nicht im Kanal. Der Status-Aktualisierer hat sie vorher im Kanal
gesucht und nicht gefunden — die Begruendung waere im Forum also nie im Post
gelandet. Und der Titel wird jetzt nur um den Stand ergaenzt statt
ueberschrieben, damit "🚗 Fahrzeuge" stehen bleibt.

Geprueft: tagsFuer gegen 20 Faelle — kein Forum, Medien-Kanal, fehlende Tags,
Gross/Kleinschreibung, Deckel bei fuenf (mehr nimmt Discord nicht), und alle
fuenf Staende einzeln. Im Browser die Emoji-Tafel an allen vier Stellen:
Suche, Escape, Klick daneben, Leeren, Uebernehmen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 23:45:18 +02:00
co-authored by Claude Opus 5
parent edf8fa14c5
commit cc7a51f02e
7 changed files with 366 additions and 26 deletions
+167
View File
@@ -0,0 +1,167 @@
// Emoji-Auswahl fürs Panel.
//
// Überall, wo ein Emoji nach Discord geht — Rollen-Menüs, Ticket-Anliegen,
// Wunsch-Bereiche, Portal-Kacheln — stand bisher ein Textfeld, in das man mit
// Win+. hineintippen musste. Das hier ist derselbe Wert, nur klickbar.
//
// Bewusst eine eigene Liste statt einer Bibliothek: die vollständige
// Unicode-Tabelle wäre ein halbes Megabyte für eine Handvoll Symbole, und die
// Suchbegriffe dürfen deutsch sein.
//
// Server-Emojis (<:name:id>) tippt man weiter ins Feld — die kennt nur Discord.
import { useEffect, useRef, useState } from 'react';
import { IconSearch, IconX } from '../icons.jsx';
const GRUPPEN = [
['Häufig', '⭐', [
['💡', 'idee gluehbirne wunsch'], ['🎫', 'ticket'], ['🐛', 'bug fehler kaefer'],
['🎮', 'spiel gaming controller'], ['🎨', 'design farbe kunst'], ['🔧', 'technik werkzeug'],
['📢', 'ankuendigung megafon'], ['💬', 'chat reden nachricht'], ['⭐', 'stern favorit'],
['✅', 'haken fertig ja'], ['❌', 'kreuz nein fehler'], ['⚠️', 'warnung achtung'],
['🔒', 'schloss privat gesperrt'], ['🚀', 'rakete release start'], ['🔥', 'feuer heiss'],
['❤️', 'herz liebe'], ['👍', 'daumen gut ja'], ['🎉', 'party feier'],
]],
['Spiel', '🎮', [
['🚗', 'auto fahrzeug'], ['🏎️', 'rennwagen'], ['🚓', 'polizei streifenwagen'],
['🚑', 'krankenwagen rettung'], ['🚒', 'feuerwehr'], ['🏍️', 'motorrad'],
['🚁', 'hubschrauber heli'], ['✈️', 'flugzeug'], ['🚤', 'boot schiff'],
['⛵', 'segelboot'], ['🗺️', 'karte map'], ['🧭', 'kompass navigation'],
['⚔️', 'kampf waffen pvp'], ['🛡️', 'schild verteidigung'], ['🏹', 'bogen jagd'],
['💣', 'bombe'], ['🎯', 'ziel treffer'], ['🏆', 'pokal gewinner'],
['🥇', 'gold erster'], ['🎲', 'wuerfel zufall'], ['🕹️', 'joystick arcade'],
['🧪', 'test playtester labor'], ['⚙️', 'zahnrad einstellung'], ['🔫', 'waffe'],
]],
['Wirtschaft', '💰', [
['💰', 'geld sack wirtschaft'], ['💵', 'schein dollar'], ['💳', 'karte bezahlen'],
['🏦', 'bank'], ['📈', 'steigend gewinn'], ['📉', 'fallend verlust'],
['🛒', 'einkauf shop'], ['🏪', 'laden geschaeft'], ['📦', 'paket lieferung'],
['🏭', 'fabrik produktion'], ['⛏️', 'spitzhacke abbau'], ['🌾', 'landwirtschaft ernte'],
['🔨', 'hammer bauen'], ['🧰', 'werkzeugkasten'], ['💎', 'diamant edelstein'],
['🪙', 'muenze'],
]],
['Orte', '🏠', [
['🏠', 'haus zuhause'], ['🏢', 'buero gebaeude'], ['🏥', 'krankenhaus'],
['🏫', 'schule'], ['🏛️', 'amt behoerde'], ['⛽', 'tankstelle'],
['🌳', 'baum natur'], ['🏝️', 'insel'], ['🏔️', 'berg'],
['🌊', 'welle wasser meer'], ['🌆', 'stadt'], ['🛣️', 'strasse'],
['🚧', 'baustelle'], ['🎪', 'zelt event'], ['🎬', 'film kino'],
['🍔', 'essen restaurant'], ['☕', 'kaffee'], ['🍺', 'bier bar'],
]],
['Team', '👥', [
['👥', 'team gruppe'], ['👤', 'person nutzer'], ['👋', 'winken willkommen'],
['🙋', 'melden frage'], ['👮', 'polizist'], ['🧑‍⚕️', 'arzt sanitaeter'],
['🧑‍🔧', 'mechaniker'], ['🧑‍🌾', 'bauer'], ['🧑‍💻', 'entwickler'],
['🎭', 'rollen maske'], ['🤝', 'handschlag zusammen'], ['🗣️', 'sprechen stimme'],
['📣', 'ruf laut'], ['🎤', 'mikrofon stream'], ['🎧', 'kopfhoerer voice'],
]],
['Technik', '🖥️', [
['🖥️', 'rechner server'], ['💾', 'diskette speichern backup'], ['🗄️', 'archiv ablage'],
['📡', 'antenne verbindung'], ['🔌', 'stecker strom'], ['🔋', 'akku batterie'],
['📊', 'diagramm statistik'], ['📋', 'liste protokoll'], ['📝', 'notiz schreiben'],
['📅', 'kalender termin'], ['⏰', 'wecker zeit'], ['⏳', 'sanduhr warten'],
['🔍', 'lupe suche'], ['🔗', 'link verbindung'], ['🏷️', 'schild tag'],
['🗂️', 'ordner dienst'], ['📁', 'ordner'], ['🧹', 'besen aufraeumen'],
]],
['Zeichen', '🔵', [
['🟢', 'gruen online ok'], ['🟡', 'gelb warnung'], ['🔴', 'rot aus fehler'],
['🔵', 'blau'], ['🟣', 'lila'], ['⚫', 'schwarz'],
['⚪', 'weiss'], ['', 'plus hinzufuegen'], ['', 'minus entfernen'],
['❓', 'frage hilfe'], ['❗', 'ausruf wichtig'], ['🔔', 'glocke benachrichtigung'],
['🔕', 'stumm'], ['🚫', 'verboten nein'], ['♻️', 'recycling wiederholen'],
['🆕', 'neu'], ['🆓', 'gratis frei'], ['🔝', 'oben top'],
]],
];
export default function EmojiPick({ value, onChange, placeholder = '🙂' }) {
const [offen, setOffen] = useState(false);
const [suche, setSuche] = useState('');
const wrap = useRef(null);
// Klick daneben oder Escape schließt — ohne das bleibt das Feld offen
// stehen, sobald man woanders weiterarbeitet
useEffect(() => {
if (!offen) return undefined;
const aus = (e) => { if (!wrap.current?.contains(e.target)) setOffen(false); };
const taste = (e) => { if (e.key === 'Escape') setOffen(false); };
document.addEventListener('mousedown', aus);
document.addEventListener('keydown', taste);
return () => {
document.removeEventListener('mousedown', aus);
document.removeEventListener('keydown', taste);
};
}, [offen]);
const q = suche.trim().toLowerCase();
const gefiltert = GRUPPEN
.map(([name, symbol, liste]) => [
name,
symbol,
q ? liste.filter(([e, worte]) => worte.includes(q) || e === q) : liste,
])
.filter(([, , liste]) => liste.length > 0);
return (
<div className="ep-wrap" ref={wrap}>
<div className="ep-feld">
<button
type="button"
className="ep-knopf"
onClick={() => setOffen((o) => !o)}
title="Emoji wählen"
>
{value || <span className="ep-leer">{placeholder}</span>}
</button>
{/* Das Feld bleibt: Server-Emojis wie <:name:123> gibt es hier nicht */}
<input
type="text"
className="ep-eingabe"
maxLength={32}
placeholder={placeholder}
value={value ?? ''}
onChange={(e) => onChange(e.target.value)}
/>
{value && (
<button type="button" className="ep-weg" onClick={() => onChange('')} title="Leeren">
<IconX size={13} />
</button>
)}
</div>
{offen && (
<div className="ep-tafel">
<div className="ep-suche">
<IconSearch size={13} />
<input
type="text"
placeholder="Suchen — z. B. auto, geld, warnung"
value={suche}
onChange={(e) => setSuche(e.target.value)}
autoFocus
/>
</div>
<div className="ep-liste">
{gefiltert.length === 0 && <p className="ep-nix">Nichts gefunden.</p>}
{gefiltert.map(([name, symbol, liste]) => (
<div key={name}>
<div className="ep-gruppe">{symbol} {name}</div>
<div className="ep-raster">
{liste.map(([e, worte]) => (
<button
type="button"
key={e}
className={`ep-zelle${value === e ? ' aktiv' : ''}`}
title={worte.split(' ')[0]}
onClick={() => { onChange(e); setOffen(false); setSuche(''); }}
>
{e}
</button>
))}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
+27 -7
View File
@@ -12,6 +12,7 @@ import { moduleIcon } from '../module-icons.jsx';
import ModulePage from '../components/ModulePage.jsx';
import UptimeTrack from '../components/UptimeTrack.jsx';
import Dialog from '../components/Dialog.jsx';
import EmojiPick from '../components/EmojiPick.jsx';
import { findSettings, SETTING_INDEX } from '../setting-index.js';
import { Markdown } from '../markdown.jsx';
@@ -1878,6 +1879,14 @@ export default function Settings({ me }) {
der Aufruf-Post in Discord fragt beim Einreichen nach dem Bereich. Nach dem
Ändern <strong>/wunsch-setup</strong> neu ausführen.
</p>
<p className="section-intro">
Ist der Voting-Kanal ein <strong>Forum</strong>, wird jeder Wunsch ein
Forums-Beitrag und bekommt Tags. Leg dort Tags mit denselben Namen an wie hier
die Bereiche, dazu <em>Wird geprüft</em>, <em>Geplant</em>, <em>In Arbeit</em>,
<em>Umgesetzt</em> und <em>Nicht geplant</em> dann kann man auch in Discord
danach filtern, und der Stand zieht automatisch mit. Fehlt ein Tag, bleibt es
einfach ohne.
</p>
{bereiche.length === 0 && <p className="notice">Noch keine Bereiche.</p>}
@@ -1906,10 +1915,10 @@ export default function Settings({ me }) {
)}
<div className="field-row">
<input
type="text" maxLength={32} placeholder="🚗" style={{ maxWidth: '5rem' }}
<EmojiPick
value={bereichDraft.emoji}
onChange={(e) => setBereichDraft({ ...bereichDraft, emoji: e.target.value })}
onChange={(v) => setBereichDraft({ ...bereichDraft, emoji: v })}
placeholder="🚗"
/>
<input
type="text" maxLength={40} placeholder="Name, z. B. Fahrzeuge"
@@ -2101,7 +2110,11 @@ export default function Settings({ me }) {
<label>Rollen <span className="field-hint">Emoji optional (auch Custom als &lt;:name:id&gt;), Label = Button-Text</span></label>
{menuDraft.entries.map((entry, i) => (
<div className="rr-row" key={i}>
<input type="text" className="rr-emoji" placeholder="🎮" value={entry.emoji} onChange={(e) => setMenuEntry(i, { emoji: e.target.value })} />
<EmojiPick
value={entry.emoji}
onChange={(v) => setMenuEntry(i, { emoji: v })}
placeholder="🎮"
/>
<input type="text" placeholder="Button-Label, z. B. FiveM" value={entry.label} onChange={(e) => setMenuEntry(i, { label: e.target.value })} />
<PickSelect
items={roleItems}
@@ -2306,8 +2319,11 @@ export default function Settings({ me }) {
</div>
<div className="field">
<label>Emoji <span className="field-hint">ein Zeichen oder ein Server-Emoji wie &lt;:name:123&gt; — unpassendes wird weggelassen</span></label>
<input type="text" maxLength={32} placeholder="🐛" value={anliegenDraft.emoji}
onChange={(e) => setAnliegenDraft({ ...anliegenDraft, emoji: e.target.value })} />
<EmojiPick
value={anliegenDraft.emoji}
onChange={(v) => setAnliegenDraft({ ...anliegenDraft, emoji: v })}
placeholder="🐛"
/>
</div>
<div className="field">
<label>Kurzbeschreibung <span className="field-hint">graue Zeile unter dem Namen im Menü — höchstens 100 Zeichen</span></label>
@@ -2896,7 +2912,11 @@ export default function Settings({ me }) {
<div className="field">
<label>{serviceDraft.id ? `Dienst #${serviceDraft.id} bearbeiten` : 'Neuer Dienst'}</label>
<div className="field-row">
<input type="text" style={{ flex: 'none', width: '4rem' }} placeholder="🗂️" value={serviceDraft.icon} onChange={(e) => setServiceDraft({ ...serviceDraft, icon: e.target.value })} />
<EmojiPick
value={serviceDraft.icon}
onChange={(v) => setServiceDraft({ ...serviceDraft, icon: v })}
placeholder="🗂️"
/>
<input type="text" style={{ flex: 'none', width: '10rem' }} placeholder="Kanban" value={serviceDraft.name} onChange={(e) => setServiceDraft({ ...serviceDraft, name: e.target.value })} />
<input type="text" placeholder="https://kanban.d4rkst3r.de" value={serviceDraft.url} onChange={(e) => setServiceDraft({ ...serviceDraft, url: e.target.value })} />
<input type="number" style={{ flex: 'none', width: '5rem' }} placeholder="Pos." value={serviceDraft.sort} onChange={(e) => setServiceDraft({ ...serviceDraft, sort: e.target.value })} />
+49
View File
@@ -1963,6 +1963,55 @@ label.toggle {
border-left: 2px solid var(--border); padding-left: .6rem;
}
/* ── EMOJI-AUSWAHL ─────────────────────────────────── */
.ep-wrap { position: relative; display: inline-block; min-width: 0; }
.ep-feld { display: flex; align-items: stretch; gap: .3rem; }
.ep-knopf {
flex: 0 0 auto; width: 2.4rem;
background: var(--bg3); border: 1px solid var(--border); color: var(--text);
font-size: 1.05rem; line-height: 1; cursor: pointer;
display: grid; place-items: center;
}
.ep-knopf:hover { border-color: var(--neon); }
.ep-leer { opacity: .35; }
.ep-eingabe { width: 5.5rem; }
.ep-weg {
flex: 0 0 auto; background: none; border: none; color: var(--muted2);
cursor: pointer; padding: 0 .2rem;
}
.ep-weg:hover { color: #f23f43; }
.ep-tafel {
position: absolute; z-index: 60; top: calc(100% + .35rem); left: 0;
width: min(21rem, 78vw);
background: var(--bg2); border: 1px solid var(--border); border-radius: 6px;
box-shadow: 0 12px 30px rgba(0, 0, 0, .55);
animation: riseIn .15s ease both;
}
.ep-suche {
display: flex; align-items: center; gap: .45rem;
padding: .5rem .7rem; border-bottom: 1px solid var(--border); color: var(--muted2);
}
.ep-suche input {
flex: 1; min-width: 0; background: none; border: none; padding: 0;
color: var(--text); font: inherit; font-size: .84rem;
}
.ep-suche input:focus { outline: none; }
.ep-liste { max-height: 17rem; overflow-y: auto; padding: .5rem .7rem .7rem; }
.ep-gruppe {
font-family: var(--mono); font-size: .58rem; letter-spacing: .12em;
text-transform: uppercase; color: var(--muted2); margin: .5rem 0 .3rem;
}
.ep-raster { display: grid; grid-template-columns: repeat(auto-fill, 2rem); gap: .15rem; }
.ep-zelle {
width: 2rem; height: 2rem; display: grid; place-items: center;
background: none; border: 1px solid transparent; border-radius: 4px;
font-size: 1.05rem; line-height: 1; cursor: pointer;
}
.ep-zelle:hover { background: var(--bg3); border-color: var(--neon); }
.ep-zelle.aktiv { border-color: var(--neon); background: color-mix(in srgb, var(--neon) 12%, transparent); }
.ep-nix { color: var(--muted); font-size: .84rem; margin: .6rem 0; }
/* ── DIALOG ────────────────────────────────────────── */
.dlg-backdrop {
position: fixed; inset: 0; z-index: 1200;