Feature-Voting, Giveaways und Contribution-Heatmap
- /wunsch: Voting-Embed mit 👍, Reaction-Tracking (add/remove) in SQLite, Top-20-Rangliste öffentlich auf der Roadmap-Seite - /giveaway (Admin): Preis/Dauer/Gewinnerzahl, 🎉-Teilnahme-Button (toggle), Minuten-Scheduler zieht Gewinner, schließt das Embed ab und pingt sie - Contribution-Heatmap: 52-Wochen-Grid in Brand-Gelb aus dem Commit-Archiv (/api/heatmap) auf der Roadmap-Seite - Setting voting_channel_id auf der Setup-Seite Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
// /giveaway — Verlosung starten (Admin): 🎉-Button zum Teilnehmen, automatische Ziehung
|
||||
import {
|
||||
SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
|
||||
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
|
||||
} from 'discord.js';
|
||||
import { saveGiveaway } from '../../db.js';
|
||||
|
||||
/** "30m" / "2h" / "1d" → Millisekunden (null bei Unsinn) */
|
||||
export function parseDuration(input) {
|
||||
const m = String(input).trim().match(/^(\d+)\s*(m|h|d)$/i);
|
||||
if (!m) return null;
|
||||
const factor = { m: 60_000, h: 3_600_000, d: 86_400_000 }[m[2].toLowerCase()];
|
||||
const ms = Number(m[1]) * factor;
|
||||
return ms >= 60_000 && ms <= 30 * 86_400_000 ? ms : null;
|
||||
}
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('giveaway')
|
||||
.setDescription('Verlosung in diesem Kanal starten')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
|
||||
.addStringOption((o) => o.setName('preis').setDescription('Was gibt es zu gewinnen?').setRequired(true).setMaxLength(200))
|
||||
.addStringOption((o) => o.setName('dauer').setDescription('z. B. 30m, 2h, 1d').setRequired(true))
|
||||
.addIntegerOption((o) => o.setName('gewinner').setDescription('Anzahl Gewinner (Default 1)').setMinValue(1).setMaxValue(20));
|
||||
|
||||
export async function execute(interaction) {
|
||||
const duration = parseDuration(interaction.options.getString('dauer'));
|
||||
if (!duration) {
|
||||
await interaction.reply({
|
||||
content: '❌ Dauer bitte als `30m`, `2h` oder `1d` (1 Minute bis 30 Tage).',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const prize = interaction.options.getString('preis');
|
||||
const winners = interaction.options.getInteger('gewinner') ?? 1;
|
||||
const endsAt = new Date(Date.now() + duration);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setColor(0xf5c518)
|
||||
.setTitle(`🎉 Giveaway: ${prize}`)
|
||||
.setDescription(
|
||||
`Klick auf den Button zum Teilnehmen!\n\n` +
|
||||
`🏆 **${winners}** Gewinner · ⏰ Ziehung <t:${Math.floor(endsAt.getTime() / 1000)}:R>`
|
||||
)
|
||||
.setFooter({ text: 'D4RKST3R // GIVEAWAY' })
|
||||
.setTimestamp(endsAt);
|
||||
|
||||
const row = new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder().setCustomId('giveaway_enter').setStyle(ButtonStyle.Primary).setLabel('Teilnehmen').setEmoji('🎉')
|
||||
);
|
||||
|
||||
const message = await interaction.channel.send({ embeds: [embed], components: [row] });
|
||||
saveGiveaway({
|
||||
message_id: message.id,
|
||||
channel_id: message.channelId,
|
||||
prize,
|
||||
winners,
|
||||
// SQLite vergleicht mit datetime('now') im UTC-Format
|
||||
ends_at: endsAt.toISOString().slice(0, 19).replace('T', ' '),
|
||||
});
|
||||
await interaction.reply({ content: '✅ Giveaway gestartet!', flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
Reference in New Issue
Block a user