Skip to content

Commit 6862b82

Browse files
committed
feat(engagement): configurable activity badges via config + dashboard editor
1 parent a7c855e commit 6862b82

4 files changed

Lines changed: 120 additions & 8 deletions

File tree

config.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,13 @@
185185
"engagement": {
186186
"enabled": false,
187187
"trackMessages": true,
188-
"trackReactions": true
188+
"trackReactions": true,
189+
"activityBadges": [
190+
{ "days": 90, "label": "👑 Legend" },
191+
{ "days": 30, "label": "🌳 Veteran" },
192+
{ "days": 7, "label": "🌿 Regular" },
193+
{ "days": 0, "label": "🌱 Newcomer" }
194+
]
189195
},
190196
"reputation": {
191197
"enabled": false,

src/commands/profile.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,27 @@ import { error as logError } from '../logger.js';
1111
import { getConfig } from '../modules/config.js';
1212
import { safeEditReply } from '../utils/safeSend.js';
1313

14+
/** Default activity badge tiers (threshold in days → emoji + label). */
15+
const DEFAULT_BADGES = [
16+
{ days: 90, label: '👑 Legend' },
17+
{ days: 30, label: '🌳 Veteran' },
18+
{ days: 7, label: '🌿 Regular' },
19+
{ days: 0, label: '🌱 Newcomer' },
20+
];
21+
1422
/**
15-
* Return an activity badge based on days_active.
23+
* Return an activity badge based on days_active and config.
1624
*
1725
* @param {number} daysActive
26+
* @param {Array<{days: number, label: string}>} [badges] - Custom badge tiers from config, sorted descending by days.
1827
* @returns {string}
1928
*/
20-
export function getActivityBadge(daysActive) {
21-
if (daysActive >= 90) return '👑 Legend';
22-
if (daysActive >= 30) return '🌳 Veteran';
23-
if (daysActive >= 7) return '🌿 Regular';
24-
return '🌱 Newcomer';
29+
export function getActivityBadge(daysActive, badges) {
30+
const tiers = badges?.length ? [...badges].sort((a, b) => b.days - a.days) : DEFAULT_BADGES;
31+
for (const tier of tiers) {
32+
if (daysActive >= tier.days) return tier.label;
33+
}
34+
return tiers[tiers.length - 1]?.label ?? '🌱 Newcomer';
2535
}
2636

2737
export const data = new SlashCommandBuilder()
@@ -70,7 +80,7 @@ export async function execute(interaction) {
7080
last_active: null,
7181
};
7282

73-
const badge = getActivityBadge(stats.days_active);
83+
const badge = getActivityBadge(stats.days_active, config.engagement?.activityBadges);
7484

7585
const formatDate = (d) =>
7686
d ? new Date(d).toLocaleDateString('en-US', { dateStyle: 'medium' }) : 'Never';

tests/commands/profile.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,30 @@ describe('getActivityBadge', () => {
111111
expect(getActivityBadge(90)).toBe('👑 Legend');
112112
expect(getActivityBadge(200)).toBe('👑 Legend');
113113
});
114+
115+
it('uses custom badges from config', () => {
116+
const custom = [
117+
{ days: 365, label: '🏆 OG' },
118+
{ days: 14, label: '⭐ Active' },
119+
{ days: 0, label: '🆕 Fresh' },
120+
];
121+
expect(getActivityBadge(400, custom)).toBe('🏆 OG');
122+
expect(getActivityBadge(14, custom)).toBe('⭐ Active');
123+
expect(getActivityBadge(5, custom)).toBe('🆕 Fresh');
124+
});
125+
126+
it('handles unsorted custom badges', () => {
127+
const unsorted = [
128+
{ days: 0, label: '🆕 New' },
129+
{ days: 50, label: '🔥 Hot' },
130+
];
131+
expect(getActivityBadge(50, unsorted)).toBe('🔥 Hot');
132+
expect(getActivityBadge(3, unsorted)).toBe('🆕 New');
133+
});
134+
135+
it('falls back to defaults when custom badges is empty', () => {
136+
expect(getActivityBadge(90, [])).toBe('👑 Legend');
137+
});
114138
});
115139

116140
describe('/profile execute', () => {

web/src/components/dashboard/config-editor.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1225,6 +1225,78 @@ export function ConfigEditor() {
12251225
</CardContent>
12261226
</Card>
12271227

1228+
{/* ═══ Engagement / Activity Badges ═══ */}
1229+
<Card>
1230+
<CardContent className="space-y-4 pt-6">
1231+
<CardTitle className="text-base">Activity Badges</CardTitle>
1232+
<p className="text-xs text-muted-foreground">Configure the badge tiers shown on /profile. Each badge requires a minimum number of active days.</p>
1233+
{(draftConfig.engagement?.activityBadges ?? [
1234+
{ days: 90, label: "👑 Legend" },
1235+
{ days: 30, label: "🌳 Veteran" },
1236+
{ days: 7, label: "🌿 Regular" },
1237+
{ days: 0, label: "🌱 Newcomer" },
1238+
]).map((badge: { days: number; label: string }, i: number) => (
1239+
<div key={i} className="flex items-center gap-2">
1240+
<Input
1241+
className="w-20"
1242+
type="number"
1243+
min={0}
1244+
value={badge.days}
1245+
onChange={(e) => {
1246+
const badges = [...(draftConfig.engagement?.activityBadges ?? [
1247+
{ days: 90, label: "👑 Legend" },
1248+
{ days: 30, label: "🌳 Veteran" },
1249+
{ days: 7, label: "🌿 Regular" },
1250+
{ days: 0, label: "🌱 Newcomer" },
1251+
])];
1252+
badges[i] = { ...badges[i], days: Math.max(0, parseInt(e.target.value, 10) || 0) };
1253+
setDraftConfig((prev) => ({ ...prev, engagement: { ...prev.engagement, activityBadges: badges } }));
1254+
}}
1255+
disabled={saving}
1256+
/>
1257+
<span className="text-xs text-muted-foreground">days →</span>
1258+
<Input
1259+
className="flex-1"
1260+
value={badge.label}
1261+
onChange={(e) => {
1262+
const badges = [...(draftConfig.engagement?.activityBadges ?? [
1263+
{ days: 90, label: "👑 Legend" },
1264+
{ days: 30, label: "🌳 Veteran" },
1265+
{ days: 7, label: "🌿 Regular" },
1266+
{ days: 0, label: "🌱 Newcomer" },
1267+
])];
1268+
badges[i] = { ...badges[i], label: e.target.value };
1269+
setDraftConfig((prev) => ({ ...prev, engagement: { ...prev.engagement, activityBadges: badges } }));
1270+
}}
1271+
disabled={saving}
1272+
/>
1273+
<Button
1274+
variant="ghost"
1275+
size="sm"
1276+
onClick={() => {
1277+
const badges = [...(draftConfig.engagement?.activityBadges ?? [])].filter((_, idx) => idx !== i);
1278+
setDraftConfig((prev) => ({ ...prev, engagement: { ...prev.engagement, activityBadges: badges } }));
1279+
}}
1280+
disabled={saving || (draftConfig.engagement?.activityBadges ?? []).length <= 1}
1281+
>
1282+
1283+
</Button>
1284+
</div>
1285+
))}
1286+
<Button
1287+
variant="outline"
1288+
size="sm"
1289+
onClick={() => {
1290+
const badges = [...(draftConfig.engagement?.activityBadges ?? []), { days: 0, label: "🌟 New Badge" }];
1291+
setDraftConfig((prev) => ({ ...prev, engagement: { ...prev.engagement, activityBadges: badges } }));
1292+
}}
1293+
disabled={saving}
1294+
>
1295+
+ Add Badge
1296+
</Button>
1297+
</CardContent>
1298+
</Card>
1299+
12281300
{/* ═══ Reputation / XP Settings ═══ */}
12291301
<Card>
12301302
<CardContent className="space-y-4 pt-6">

0 commit comments

Comments
 (0)