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.
This commit is contained in:
smueller
2026-07-23 12:41:13 +02:00
parent 3af989a83b
commit fba597a6f2
9 changed files with 480 additions and 183 deletions

View File

@@ -499,6 +499,13 @@ export const SETTINGS_SEARCH_FIELDS: SearchIndexEntry[] = [
href: 'leveling',
hash: 'field-leveling-stack'
},
{
id: 'setting-leveling-rewards',
kind: 'setting',
labelKey: 'modulePages.leveling.rewardsTitle',
href: 'leveling',
hash: 'field-leveling-rewards'
},
{
id: 'setting-leveling-rank-card',
kind: 'setting',

View File

@@ -1,4 +1,9 @@
import type { LevelingConfigDashboard, LevelingConfigDashboardPatch } from '@nexumi/shared';
import {
hasUniqueRewardLevels,
type LevelingConfigDashboard,
type LevelingConfigDashboardPatch,
type LevelRewardDashboard
} from '@nexumi/shared';
import type { Prisma } from '@prisma/client';
import { prisma } from '../prisma';
@@ -21,7 +26,10 @@ function toMultiplierMap(raw: unknown): Record<string, number> {
return Object.fromEntries(entries);
}
function toDashboard(config: Awaited<ReturnType<typeof ensureLevelingConfig>>): LevelingConfigDashboard {
function toDashboard(
config: Awaited<ReturnType<typeof ensureLevelingConfig>>,
rewards: LevelRewardDashboard[]
): LevelingConfigDashboard {
return {
enabled: config.enabled,
textXpMin: config.textXpMin,
@@ -36,14 +44,25 @@ function toDashboard(config: Awaited<ReturnType<typeof ensureLevelingConfig>>):
levelUpChannelId: config.levelUpChannelId ?? '',
levelUpMessage: config.levelUpMessage,
stackRewards: config.stackRewards,
rewards,
cardAccentColor: config.cardAccentColor,
cardBackgroundColor: config.cardBackgroundColor
};
}
async function loadRewards(guildId: string): Promise<LevelRewardDashboard[]> {
const rows = await prisma.levelReward.findMany({
where: { guildId },
orderBy: { level: 'asc' },
select: { level: true, roleId: true }
});
return rows.map((row) => ({ level: row.level, roleId: row.roleId }));
}
export async function getLevelingDashboard(guildId: string): Promise<LevelingConfigDashboard> {
const config = await ensureLevelingConfig(guildId);
return toDashboard(config);
const rewards = await loadRewards(guildId);
return toDashboard(config, rewards);
}
export async function updateLevelingDashboard(
@@ -52,7 +71,11 @@ export async function updateLevelingDashboard(
): Promise<LevelingConfigDashboard> {
await ensureLevelingConfig(guildId);
const { levelUpChannelId, roleMultipliers, channelMultipliers, ...rest } = patch;
if (patch.rewards !== undefined && !hasUniqueRewardLevels(patch.rewards)) {
throw new Error('Duplicate reward levels are not allowed');
}
const { levelUpChannelId, roleMultipliers, channelMultipliers, rewards, ...rest } = patch;
const data: Prisma.LevelingConfigUpdateInput = { ...rest };
if (levelUpChannelId !== undefined) {
data.levelUpChannelId = levelUpChannelId === '' ? null : levelUpChannelId;
@@ -64,6 +87,23 @@ export async function updateLevelingDashboard(
data.channelMultipliers = channelMultipliers as Prisma.InputJsonValue;
}
const updated = await prisma.levelingConfig.update({ where: { guildId }, data });
return toDashboard(updated);
await prisma.$transaction(async (tx) => {
if (Object.keys(data).length > 0) {
await tx.levelingConfig.update({ where: { guildId }, data });
}
if (rewards !== undefined) {
await tx.levelReward.deleteMany({ where: { guildId } });
if (rewards.length > 0) {
await tx.levelReward.createMany({
data: rewards.map((reward) => ({
guildId,
level: reward.level,
roleId: reward.roleId
}))
});
}
}
});
return getLevelingDashboard(guildId);
}