forked from syntaxbullet/aurorabot
188 lines
6.0 KiB
TypeScript
188 lines
6.0 KiB
TypeScript
import { useEffect, useState, useCallback } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { toast } from "sonner";
|
|
|
|
// Sentinel value for "none" selection
|
|
export const NONE_VALUE = "__none__";
|
|
|
|
// Schema definition matching backend config
|
|
const bigIntStringSchema = z.coerce.string()
|
|
.refine((val) => /^\d+$/.test(val), { message: "Must be a valid integer" });
|
|
|
|
export const formSchema = z.object({
|
|
leveling: z.object({
|
|
base: z.number(),
|
|
exponent: z.number(),
|
|
chat: z.object({
|
|
cooldownMs: z.number(),
|
|
minXp: z.number(),
|
|
maxXp: z.number(),
|
|
})
|
|
}),
|
|
economy: z.object({
|
|
daily: z.object({
|
|
amount: bigIntStringSchema,
|
|
streakBonus: bigIntStringSchema,
|
|
weeklyBonus: bigIntStringSchema,
|
|
cooldownMs: z.number(),
|
|
}),
|
|
transfers: z.object({
|
|
allowSelfTransfer: z.boolean(),
|
|
minAmount: bigIntStringSchema,
|
|
}),
|
|
exam: z.object({
|
|
multMin: z.number(),
|
|
multMax: z.number(),
|
|
})
|
|
}),
|
|
inventory: z.object({
|
|
maxStackSize: bigIntStringSchema,
|
|
maxSlots: z.number(),
|
|
}),
|
|
commands: z.record(z.string(), z.boolean()).optional(),
|
|
lootdrop: z.object({
|
|
activityWindowMs: z.number(),
|
|
minMessages: z.number(),
|
|
spawnChance: z.number(),
|
|
cooldownMs: z.number(),
|
|
reward: z.object({
|
|
min: z.number(),
|
|
max: z.number(),
|
|
currency: z.string(),
|
|
})
|
|
}),
|
|
studentRole: z.string().optional(),
|
|
visitorRole: z.string().optional(),
|
|
colorRoles: z.array(z.string()).default([]),
|
|
welcomeChannelId: z.string().optional(),
|
|
welcomeMessage: z.string().optional(),
|
|
feedbackChannelId: z.string().optional(),
|
|
terminal: z.object({
|
|
channelId: z.string(),
|
|
messageId: z.string()
|
|
}).optional(),
|
|
moderation: z.object({
|
|
prune: z.object({
|
|
maxAmount: z.number(),
|
|
confirmThreshold: z.number(),
|
|
batchSize: z.number(),
|
|
batchDelayMs: z.number(),
|
|
}),
|
|
cases: z.object({
|
|
dmOnWarn: z.boolean(),
|
|
logChannelId: z.string().optional(),
|
|
autoTimeoutThreshold: z.number().optional()
|
|
})
|
|
}),
|
|
trivia: z.object({
|
|
entryFee: bigIntStringSchema,
|
|
rewardMultiplier: z.number(),
|
|
timeoutSeconds: z.number(),
|
|
cooldownMs: z.number(),
|
|
categories: z.array(z.number()).default([]),
|
|
difficulty: z.enum(['easy', 'medium', 'hard', 'random']),
|
|
}).optional(),
|
|
system: z.record(z.string(), z.any()).optional(),
|
|
});
|
|
|
|
export type FormValues = z.infer<typeof formSchema>;
|
|
|
|
export interface ConfigMeta {
|
|
roles: { id: string, name: string, color: string }[];
|
|
channels: { id: string, name: string, type: number }[];
|
|
commands: { name: string, category: string }[];
|
|
}
|
|
|
|
export const toSelectValue = (v: string | undefined | null) => v || NONE_VALUE;
|
|
export const fromSelectValue = (v: string) => v === NONE_VALUE ? "" : v;
|
|
|
|
export function useSettings() {
|
|
const [meta, setMeta] = useState<ConfigMeta | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
const form = useForm<FormValues>({
|
|
resolver: zodResolver(formSchema) as any,
|
|
defaultValues: {
|
|
economy: {
|
|
daily: { amount: "0", streakBonus: "0", weeklyBonus: "0", cooldownMs: 0 },
|
|
transfers: { minAmount: "0", allowSelfTransfer: false },
|
|
exam: { multMin: 1, multMax: 1 }
|
|
},
|
|
leveling: { base: 100, exponent: 1.5, chat: { minXp: 10, maxXp: 20, cooldownMs: 60000 } },
|
|
inventory: { maxStackSize: "1", maxSlots: 10 },
|
|
moderation: {
|
|
prune: { maxAmount: 100, confirmThreshold: 50, batchSize: 100, batchDelayMs: 1000 },
|
|
cases: { dmOnWarn: true }
|
|
},
|
|
lootdrop: {
|
|
spawnChance: 0.05,
|
|
minMessages: 10,
|
|
cooldownMs: 300000,
|
|
activityWindowMs: 600000,
|
|
reward: { min: 100, max: 500, currency: "AU" }
|
|
}
|
|
}
|
|
});
|
|
|
|
const loadSettings = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [config, metaData] = await Promise.all([
|
|
fetch("/api/settings").then(res => res.json()),
|
|
fetch("/api/settings/meta").then(res => res.json())
|
|
]);
|
|
form.reset(config as any);
|
|
setMeta(metaData);
|
|
} catch (err) {
|
|
toast.error("Failed to load settings", {
|
|
description: "Unable to fetch bot configuration. Please try again."
|
|
});
|
|
console.error(err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [form]);
|
|
|
|
useEffect(() => {
|
|
loadSettings();
|
|
}, [loadSettings]);
|
|
|
|
const saveSettings = async (data: FormValues) => {
|
|
setIsSaving(true);
|
|
try {
|
|
const response = await fetch("/api/settings", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data)
|
|
});
|
|
|
|
if (!response.ok) throw new Error("Failed to save");
|
|
|
|
toast.success("Settings saved successfully", {
|
|
description: "Bot configuration has been updated and reloaded."
|
|
});
|
|
// Reload settings to ensure we have the latest state
|
|
await loadSettings();
|
|
} catch (error) {
|
|
toast.error("Failed to save settings", {
|
|
description: error instanceof Error ? error.message : "Unable to save changes. Please try again."
|
|
});
|
|
console.error(error);
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
};
|
|
|
|
return {
|
|
form,
|
|
meta,
|
|
loading,
|
|
isSaving,
|
|
saveSettings,
|
|
loadSettings
|
|
};
|
|
}
|