Implement overdue giveaway recovery and enhance giveaway command handling

- Added functionality to recover overdue giveaways during bot startup, ensuring that giveaways that have passed their end time are processed correctly.
- Introduced `cancelGiveawayJob` to safely remove scheduled jobs, preventing issues with locked jobs.
- Updated giveaway command handling to trim IDs and validate existence within the guild context, improving error handling and user feedback.
- Enhanced the role picker component to normalize search queries and handle errors more gracefully, improving user experience.
- Implemented caching strategies for guild roles to avoid stale or empty responses, ensuring more reliable data retrieval.
This commit is contained in:
TheOnlyMace
2026-07-24 21:49:20 +02:00
parent c7fb3d7041
commit 57cdf847f5
10 changed files with 301 additions and 51 deletions

View File

@@ -26,6 +26,10 @@ function toOptions(roles: DiscordRoleOption[]) {
}));
}
function normalizeSearch(value: string): string {
return value.trim().toLocaleLowerCase();
}
interface RoleSelectBaseProps {
guildId: string;
disabled?: boolean;
@@ -48,14 +52,33 @@ export function DiscordRoleSelect({
allowClear = true
}: DiscordRoleSelectProps) {
const t = useTranslations();
const { roles, loading } = useGuildRoles(guildId);
const { roles, loading, error, reload } = useGuildRoles(guildId);
const [open, setOpen] = useState(false);
const [query, setQuery] = useState('');
const options = useMemo(() => toOptions(roles), [roles]);
const selected = options.find((option) => option.id === value);
const filtered = useMemo(() => {
const needle = normalizeSearch(query);
if (!needle) {
return options;
}
return options.filter((option) => {
const haystack = normalizeSearch(`${option.prefix}${option.name} ${option.id}`);
return haystack.includes(needle);
});
}, [options, query]);
return (
<Popover open={open} onOpenChange={setOpen}>
<Popover
open={open}
onOpenChange={(next) => {
setOpen(next);
if (!next) {
setQuery('');
}
}}
>
<PopoverTrigger asChild>
<Button
type="button"
@@ -74,10 +97,22 @@ export function DiscordRoleSelect({
</Button>
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder={placeholder ?? t('modulePages.commands.roleSearchPlaceholder')} />
<Command shouldFilter={false}>
<CommandInput
value={query}
onValueChange={setQuery}
placeholder={placeholder ?? t('modulePages.commands.roleSearchPlaceholder')}
/>
<CommandList>
<CommandEmpty>{t('common.noResults')}</CommandEmpty>
<CommandEmpty>
{error ? (
<button type="button" className="underline" onClick={() => reload()}>
{t('common.saveError')}
</button>
) : (
t('common.noResults')
)}
</CommandEmpty>
<CommandGroup>
{allowClear && value ? (
<CommandItem
@@ -91,10 +126,10 @@ export function DiscordRoleSelect({
<span className="text-muted-foreground">{t('common.clearSelection')}</span>
</CommandItem>
) : null}
{options.map((option) => (
{filtered.map((option) => (
<CommandItem
key={option.id}
value={`${option.prefix}${option.name} ${option.id}`}
value={`${option.name} ${option.id}`}
onSelect={() => {
onChange(option.id);
setOpen(false);