Create Discord Management admin component
cgen-df0d81d8fe084790bbc3ad2f339764aa
This commit is contained in:
parent
2ef9b59fe9
commit
1cfe2ee740
1 changed files with 318 additions and 0 deletions
318
client/components/admin/AdminDiscordManagement.tsx
Normal file
318
client/components/admin/AdminDiscordManagement.tsx
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from "../ui/card";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Badge } from "../ui/badge";
|
||||||
|
import {
|
||||||
|
AlertDialog,
|
||||||
|
AlertDialogAction,
|
||||||
|
AlertDialogCancel,
|
||||||
|
AlertDialogContent,
|
||||||
|
AlertDialogDescription,
|
||||||
|
AlertDialogHeader,
|
||||||
|
AlertDialogTitle,
|
||||||
|
} from "../ui/alert-dialog";
|
||||||
|
|
||||||
|
interface RoleMapping {
|
||||||
|
id: string;
|
||||||
|
arm: string;
|
||||||
|
user_type?: string;
|
||||||
|
discord_role: string;
|
||||||
|
server_id?: string;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ARMS = [
|
||||||
|
{ value: "labs", label: "Labs", color: "bg-yellow-500/20" },
|
||||||
|
{ value: "gameforge", label: "GameForge", color: "bg-green-500/20" },
|
||||||
|
{ value: "corp", label: "Corp", color: "bg-blue-500/20" },
|
||||||
|
{ value: "foundation", label: "Foundation", color: "bg-red-500/20" },
|
||||||
|
{ value: "devlink", label: "Dev-Link", color: "bg-cyan-500/20" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function AdminDiscordManagement() {
|
||||||
|
const [mappings, setMappings] = useState<RoleMapping[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [newMapping, setNewMapping] = useState({
|
||||||
|
arm: "labs",
|
||||||
|
discord_role: "",
|
||||||
|
server_id: "",
|
||||||
|
});
|
||||||
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
const [deleteId, setDeleteId] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchMappings();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchMappings = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch("/api/discord/role-mappings");
|
||||||
|
if (!response.ok) throw new Error("Failed to fetch mappings");
|
||||||
|
const data = await response.json();
|
||||||
|
setMappings(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error fetching mappings:", err);
|
||||||
|
setError("Failed to load role mappings");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateMapping = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newMapping.discord_role) {
|
||||||
|
setError("Discord role is required");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/discord/role-mappings", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(newMapping),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Failed to create mapping");
|
||||||
|
const data = await response.json();
|
||||||
|
setMappings([...mappings, data]);
|
||||||
|
setNewMapping({ arm: "labs", discord_role: "", server_id: "" });
|
||||||
|
setSuccess("Role mapping created successfully!");
|
||||||
|
setTimeout(() => setSuccess(null), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error creating mapping:", err);
|
||||||
|
setError("Failed to create role mapping");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteMapping = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/discord/role-mappings?id=${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error("Failed to delete mapping");
|
||||||
|
setMappings(mappings.filter((m) => m.id !== id));
|
||||||
|
setSuccess("Role mapping deleted successfully!");
|
||||||
|
setTimeout(() => setSuccess(null), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error deleting mapping:", err);
|
||||||
|
setError("Failed to delete role mapping");
|
||||||
|
} finally {
|
||||||
|
setDeleteId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getArmLabel = (armValue: string) => {
|
||||||
|
return ARMS.find((a) => a.value === armValue)?.label || armValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Bot Status */}
|
||||||
|
<Card className="border-purple-500/30 bg-purple-950/20">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Discord Bot Status</CardTitle>
|
||||||
|
<CardDescription>Real-time bot configuration</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<div className="bg-gradient-to-br from-green-500/10 to-green-600/5 p-4 rounded-lg border border-green-500/20">
|
||||||
|
<p className="text-sm text-green-300">Bot Status</p>
|
||||||
|
<p className="text-lg font-bold text-green-400 mt-1">Online</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gradient-to-br from-blue-500/10 to-blue-600/5 p-4 rounded-lg border border-blue-500/20">
|
||||||
|
<p className="text-sm text-blue-300">Linked Accounts</p>
|
||||||
|
<p className="text-lg font-bold text-blue-400 mt-1">
|
||||||
|
{mappings.length > 0 ? "Active" : "0"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gradient-to-br from-purple-500/10 to-purple-600/5 p-4 rounded-lg border border-purple-500/20">
|
||||||
|
<p className="text-sm text-purple-300">Role Mappings</p>
|
||||||
|
<p className="text-lg font-bold text-purple-400 mt-1">
|
||||||
|
{mappings.length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gradient-to-br from-pink-500/10 to-pink-600/5 p-4 rounded-lg border border-pink-500/20">
|
||||||
|
<p className="text-sm text-pink-300">Servers</p>
|
||||||
|
<p className="text-lg font-bold text-pink-400 mt-1">6</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Role Mappings */}
|
||||||
|
<Card className="border-purple-500/30 bg-purple-950/20">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Discord Role Mappings</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Configure which Discord roles are assigned for each arm
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
{error && (
|
||||||
|
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded text-red-300 text-sm">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<div className="p-3 bg-green-500/10 border border-green-500/30 rounded text-green-300 text-sm">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add New Mapping Form */}
|
||||||
|
<div className="bg-gradient-to-br from-purple-900/20 to-blue-900/20 p-4 rounded-lg border border-purple-500/20">
|
||||||
|
<h4 className="font-semibold mb-4">Add New Role Mapping</h4>
|
||||||
|
<form onSubmit={handleCreateMapping} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-300">Arm</label>
|
||||||
|
<select
|
||||||
|
value={newMapping.arm}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewMapping({ ...newMapping, arm: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full mt-1 bg-gray-800 border border-purple-500/30 rounded px-3 py-2 text-white focus:outline-none focus:border-purple-500"
|
||||||
|
>
|
||||||
|
{ARMS.map((arm) => (
|
||||||
|
<option key={arm.value} value={arm.value}>
|
||||||
|
{arm.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-300">
|
||||||
|
Discord Role Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g., Labs Member"
|
||||||
|
value={newMapping.discord_role}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewMapping({
|
||||||
|
...newMapping,
|
||||||
|
discord_role: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full mt-1 bg-gray-800 border border-purple-500/30 rounded px-3 py-2 text-white focus:outline-none focus:border-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-300">
|
||||||
|
Server ID (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Discord Server ID"
|
||||||
|
value={newMapping.server_id}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewMapping({ ...newMapping, server_id: e.target.value })
|
||||||
|
}
|
||||||
|
className="w-full mt-1 bg-gray-800 border border-purple-500/30 rounded px-3 py-2 text-white focus:outline-none focus:border-purple-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full bg-purple-600 hover:bg-purple-700">
|
||||||
|
Add Mapping
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mappings Table */}
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
Loading mappings...
|
||||||
|
</div>
|
||||||
|
) : mappings.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-gray-400">
|
||||||
|
No role mappings configured yet. Add one above!
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="border-b border-purple-500/30">
|
||||||
|
<tr>
|
||||||
|
<th className="text-left py-2 px-3 text-purple-300">Arm</th>
|
||||||
|
<th className="text-left py-2 px-3 text-purple-300">
|
||||||
|
Discord Role
|
||||||
|
</th>
|
||||||
|
<th className="text-left py-2 px-3 text-purple-300">
|
||||||
|
Server ID
|
||||||
|
</th>
|
||||||
|
<th className="text-left py-2 px-3 text-purple-300">
|
||||||
|
Actions
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{mappings.map((mapping) => (
|
||||||
|
<tr
|
||||||
|
key={mapping.id}
|
||||||
|
className="border-b border-purple-500/10 hover:bg-purple-500/5"
|
||||||
|
>
|
||||||
|
<td className="py-3 px-3">
|
||||||
|
<Badge variant="outline">
|
||||||
|
{getArmLabel(mapping.arm)}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-3 text-gray-300">
|
||||||
|
{mapping.discord_role}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-3 text-gray-400 text-xs">
|
||||||
|
{mapping.server_id || "Global"}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||||
|
onClick={() => setDeleteId(mapping.id)}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Delete Confirmation */}
|
||||||
|
<AlertDialog open={!!deleteId} onOpenChange={(open) => !open && setDeleteId(null)}>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Delete Role Mapping?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>
|
||||||
|
This role mapping will be permanently deleted. Users will no longer
|
||||||
|
receive this role when setting this arm.
|
||||||
|
</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
|
<AlertDialogAction
|
||||||
|
onClick={() => deleteId && handleDeleteMapping(deleteId)}
|
||||||
|
className="bg-red-600 hover:bg-red-700"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AlertDialogAction>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue