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; 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(null); const [loading, setLoading] = useState(true); const [isSaving, setIsSaving] = useState(false); const form = useForm({ 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 }; }