From fba597a6f24026c275d576e12789ff386d31de60 Mon Sep 17 00:00:00 2001 From: smueller Date: Thu, 23 Jul 2026 12:41:13 +0200 Subject: [PATCH] Implement leveling rewards feature with UI and backend support - Added functionality to manage role rewards based on user levels in the leveling system. - Updated the LevelingForm component to include fields for defining rewards, including validation for unique levels. - Enhanced backend logic to handle reward data storage and retrieval, ensuring proper integration with the existing leveling configuration. - Introduced localization keys for rewards in both English and German, improving user experience. - Updated dashboard search index to include rewards settings for easier access. --- apps/bot/src/modules/leveling/service.ts | 2 +- .../src/components/modules/leveling-form.tsx | 428 +++++++++++------- .../src/components/ui/level-reward-editor.tsx | 119 +++++ apps/webui/src/lib/dashboard-search-index.ts | 7 + apps/webui/src/lib/module-configs/leveling.ts | 52 ++- apps/webui/src/messages/de.json | 11 +- apps/webui/src/messages/en.json | 11 +- docs/PHASE-TRACKING.md | 14 + packages/shared/src/dashboard.ts | 19 + 9 files changed, 480 insertions(+), 183 deletions(-) create mode 100644 apps/webui/src/components/ui/level-reward-editor.tsx diff --git a/apps/bot/src/modules/leveling/service.ts b/apps/bot/src/modules/leveling/service.ts index 960e52a..b18cf91 100644 --- a/apps/bot/src/modules/leveling/service.ts +++ b/apps/bot/src/modules/leveling/service.ts @@ -273,7 +273,7 @@ export async function applyLevelRewards( } } } else { - const reward = rewards.find((entry) => entry.level === newLevel); + const reward = [...rewards].reverse().find((entry) => entry.level <= newLevel); const removable = rewards .map((entry) => entry.roleId) .filter((roleId) => member.roles.cache.has(roleId) && rewardRoleIds.has(roleId)); diff --git a/apps/webui/src/components/modules/leveling-form.tsx b/apps/webui/src/components/modules/leveling-form.tsx index 31c6e23..f50b88e 100644 --- a/apps/webui/src/components/modules/leveling-form.tsx +++ b/apps/webui/src/components/modules/leveling-form.tsx @@ -12,6 +12,12 @@ import { type MultiplierEntry } from '@/components/ui/discord-multiplier-editor'; import { DiscordRoleMultiSelect } from '@/components/ui/discord-role-select'; +import { + entriesToRewards, + LevelRewardEditor, + rewardsToEntries, + type LevelRewardEntry +} from '@/components/ui/level-reward-editor'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { PlaceholderHelp } from '@/components/ui/placeholder-help'; @@ -22,21 +28,27 @@ import type { SettingsSaveResult } from '@/components/settings/settings-form'; import { SettingsForm } from '@/components/settings/settings-form'; interface LocalLevelingValue - extends Omit { + extends Omit { roleMultiplierEntries: MultiplierEntry[]; channelMultiplierEntries: MultiplierEntry[]; + rewardEntries: LevelRewardEntry[]; } function toLocal(config: LevelingConfigDashboard): LocalLevelingValue { - const { roleMultipliers, channelMultipliers, ...rest } = config; + const { roleMultipliers, channelMultipliers, rewards, ...rest } = config; return { ...rest, roleMultiplierEntries: mapToMultiplierEntries(roleMultipliers), - channelMultiplierEntries: mapToMultiplierEntries(channelMultipliers) + channelMultiplierEntries: mapToMultiplierEntries(channelMultipliers), + rewardEntries: rewardsToEntries(rewards) }; } -async function saveLeveling(guildId: string, value: LocalLevelingValue, t: (key: string) => string): Promise { +async function saveLeveling( + guildId: string, + value: LocalLevelingValue, + t: (key: string) => string +): Promise { const roleMultipliers = multiplierEntriesToMap(value.roleMultiplierEntries); if (roleMultipliers.error) { return { ok: false, error: t(`modulePages.leveling.errors.${roleMultipliers.error}`) }; @@ -45,17 +57,23 @@ async function saveLeveling(guildId: string, value: LocalLevelingValue, t: (key: if (channelMultipliers.error) { return { ok: false, error: t(`modulePages.leveling.errors.${channelMultipliers.error}`) }; } + const rewards = entriesToRewards(value.rewardEntries); + if (rewards.error) { + return { ok: false, error: t(`modulePages.leveling.errors.${rewards.error}`) }; + } const { roleMultiplierEntries: _roleMultiplierEntries, channelMultiplierEntries: _channelMultiplierEntries, + rewardEntries: _rewardEntries, ...rest } = value; const payload: LevelingConfigDashboard = { ...rest, roleMultipliers: roleMultipliers.value, - channelMultipliers: channelMultipliers.value + channelMultipliers: channelMultipliers.value, + rewards: rewards.value }; try { @@ -96,214 +114,276 @@ export function LevelingForm({ guildId, initialValue }: LevelingFormProps) { -
- - setValue((prev) => ({ ...prev, enabled: checked }))} - /> -
+
+ + setValue((prev) => ({ ...prev, enabled: checked }))} + /> +
-
- - setValue((prev) => ({ ...prev, textXpMin: Number(event.target.value) || 1 }))} - /> -
+
+ + + setValue((prev) => ({ ...prev, textXpMin: Number(event.target.value) || 1 })) + } + /> +
-
- - setValue((prev) => ({ ...prev, textXpMax: Number(event.target.value) || 1 }))} - /> -
+
+ + + setValue((prev) => ({ ...prev, textXpMax: Number(event.target.value) || 1 })) + } + /> +
-
- - - setValue((prev) => ({ ...prev, textCooldownSeconds: Number(event.target.value) || 1 })) - } - /> -
+
+ + + setValue((prev) => ({ + ...prev, + textCooldownSeconds: Number(event.target.value) || 1 + })) + } + /> +
-
- - - setValue((prev) => ({ ...prev, voiceXpPerMinute: Number(event.target.value) || 0 })) - } - /> -
+
+ + + setValue((prev) => ({ + ...prev, + voiceXpPerMinute: Number(event.target.value) || 0 + })) + } + /> +
-
- - setValue((prev) => ({ ...prev, noXpChannelIds }))} - /> -
+
+ + setValue((prev) => ({ ...prev, noXpChannelIds }))} + /> +
-
- - setValue((prev) => ({ ...prev, noXpRoleIds }))} - /> -
+
+ + setValue((prev) => ({ ...prev, noXpRoleIds }))} + /> +
-
- - setValue((prev) => ({ ...prev, roleMultiplierEntries }))} - /> -

{t('modulePages.leveling.multiplierHint')}

-
+
+ + + setValue((prev) => ({ ...prev, roleMultiplierEntries })) + } + /> +

+ {t('modulePages.leveling.multiplierHint')} +

+
-
- - - setValue((prev) => ({ ...prev, channelMultiplierEntries })) - } - /> -

{t('modulePages.leveling.multiplierHint')}

-
+
+ + + setValue((prev) => ({ ...prev, channelMultiplierEntries })) + } + /> +

+ {t('modulePages.leveling.multiplierHint')} +

+
- - - {t('modulePages.leveling.levelUpTitle')} - - -
-
- - + + + {t('modulePages.leveling.levelUpTitle')} + + +
+
+ + +
+
+ + + setValue((prev) => ({ ...prev, levelUpChannelId })) + } + /> +
- - {t('modulePages.leveling.levelUpMessage')} +