import { useState, useEffect } from "react"; import { toast } from "sonner"; import { Loader2, Save, RefreshCw, Smartphone, Coins, Trophy, Shield, Users, Terminal, MessageSquare } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Card, CardContent } from "@/components/ui/card"; import { Switch } from "@/components/ui/switch"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; // Types matching the backend response interface RoleOption { id: string; name: string; color: string; } interface ChannelOption { id: string; name: string; type: number; } interface SettingsMeta { roles: RoleOption[]; channels: ChannelOption[]; commands: { name: string; category: string }[]; } import { type GameConfigType } from "@shared/lib/config"; // Recursive partial type for nested updates type DeepPartial = { [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; }; export function Settings() { // We use a DeepPartial type for the local form state to allow for safe updates const [config, setConfig] = useState | null>(null); const [meta, setMeta] = useState({ roles: [], channels: [], commands: [] }); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [activeTab, setActiveTab] = useState("general"); useEffect(() => { fetchData(); }, []); const fetchData = async () => { setLoading(true); try { const [configRes, metaRes] = await Promise.all([ fetch("/api/settings"), fetch("/api/settings/meta") ]); if (configRes.ok && metaRes.ok) { const configData = await configRes.json(); const metaData = await metaRes.json(); setConfig(configData); setMeta(metaData); } } catch (error) { console.error("Failed to fetch settings:", error); } finally { setLoading(false); } }; const handleSave = async () => { setSaving(true); const toastId = toast.loading("Saving configuration..."); try { const response = await fetch("/api/settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(config) }); if (!response.ok) throw new Error("Failed to save"); // Reload to satisfy any server-side transformations await fetchData(); toast.success("Settings saved successfully", { id: toastId }); } catch (error) { console.error("Failed to save settings:", error); toast.error("Failed to save settings", { id: toastId, description: "Please check your input and try again." }); } finally { setSaving(false); } }; // Helper to update nested state const updateConfig = (path: string, value: any) => { if (!path) return; setConfig((prev: any) => { const newConfig = { ...prev }; const parts = path.split('.'); let current = newConfig; for (let i = 0; i < parts.length - 1; i++) { const part = parts[i]; if (part === undefined) return prev; // Clone nested objects to ensure immutability if (!current[part]) current[part] = {}; current[part] = { ...current[part] }; current = current[part]; } const lastPart = parts[parts.length - 1]; if (lastPart !== undefined) { current[lastPart] = value; } return newConfig; }); }; if (loading || !config) { return (
); } const tabs = [ { id: "general", label: "General", icon: Smartphone }, { id: "economy", label: "Economy", icon: Coins }, { id: "leveling", label: "Leveling", icon: Trophy }, { id: "moderation", label: "Moderation", icon: Shield }, { id: "roles", label: "Roles", icon: Users }, { id: "system", label: "System", icon: Terminal }, ]; return (

Settings

Manage global bot configuration

{/* Tabs Navigation */}
{tabs.map((tab) => ( ))}
{activeTab === "general" && (
updateConfig("welcomeChannelId", v)} /> updateConfig("feedbackChannelId", v)} />
updateConfig("terminal.channelId", v)} /> updateConfig("terminal.messageId", v)} placeholder="ID of the pinned message" />
)} {activeTab === "economy" && (
updateConfig("economy.daily.amount", v)} /> updateConfig("economy.daily.streakBonus", v)} /> updateConfig("economy.daily.weeklyBonus", v)} />
updateConfig("economy.transfers.minAmount", v)} />
Allow Self Transfer updateConfig("economy.transfers.allowSelfTransfer", checked)} />
updateConfig("lootdrop.spawnChance", Number(v))} /> updateConfig("lootdrop.cooldownMs", Number(v))} /> updateConfig("lootdrop.minMessages", Number(v))} /> updateConfig("lootdrop.reward.min", Number(v))} /> updateConfig("lootdrop.reward.max", Number(v))} />
)} {activeTab === "leveling" && (
updateConfig("leveling.base", Number(v))} /> updateConfig("leveling.exponent", Number(v))} />
updateConfig("leveling.chat.minXp", Number(v))} /> updateConfig("leveling.chat.maxXp", Number(v))} /> updateConfig("leveling.chat.cooldownMs", Number(v))} />
)} {activeTab === "roles" && (
updateConfig("studentRole", v)} /> updateConfig("visitorRole", v)} />
{/* Multi-select for color roles is complex, simpler impl for now */}

Available Color Roles

{(config?.colorRoles || []).map((roleId: string | undefined) => { if (!roleId) return null; const role = meta.roles.find(r => r.id === roleId); return ( {role?.name || roleId} ); })}
)} {activeTab === "moderation" && (
updateConfig("moderation.prune.maxAmount", Number(v))} /> updateConfig("moderation.prune.confirmThreshold", Number(v))} />
)} {activeTab === "system" && (
{meta.commands.length === 0 ? (
No commands found in metadata.
) : ( Object.entries( meta.commands.reduce((acc, cmd) => { const cat = cmd.category || 'Uncategorized'; if (!acc[cat]) acc[cat] = []; acc[cat].push(cmd); return acc; }, {} as Record) ).sort(([a], [b]) => a.localeCompare(b)).map(([category, commands]) => (

{category}

{commands.map(cmd => (
/{cmd.name} {config?.commands?.[cmd.name] === false ? "Disabled" : "Enabled"}
updateConfig(`commands.${cmd.name}`, checked)} />
))}
)) )}
)}
); } // Sub-components for cleaner code function SectionTitle({ icon: Icon, title, description }: { icon: any, title: string, description: string }) { return (

{title}

{description}

); } function InputField({ label, value, onChange, type = "text", placeholder }: { label: string, value: any, onChange: (val: string) => void, type?: string, placeholder?: string }) { return (
onChange(e.target.value)} placeholder={placeholder} className="bg-black/20 border-white/10 text-white focus:border-primary/50" />
); } function SelectField({ label, value, options, onChange }: { label: string, value: string | undefined, options: any[], onChange: (val: string) => void }) { return (
); }