Compare commits
10 Commits
refactorin
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6bb4ddf7e | ||
|
|
9506f6246c | ||
|
|
e64984b7b8 | ||
|
|
20187a06f0 | ||
|
|
464f37adda | ||
|
|
a1fda6fe87 | ||
|
|
aaa362ec0e | ||
|
|
3b62590df3 | ||
|
|
8373fdd685 | ||
| 6a74cd55a0 |
@@ -85,42 +85,42 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Dark Mode Palette - Modernized */
|
||||
--background: oklch(0.18 0 0); /* Slightly lighter dark charcoal */
|
||||
--foreground: oklch(0.88 0 0); /* Soft white/light gray */
|
||||
--card: oklch(0.22 0 0); /* Slightly lighter for cards than background */
|
||||
--card-foreground: oklch(0.88 0 0);
|
||||
--popover: oklch(0.22 0 0);
|
||||
--popover-foreground: oklch(0.88 0 0);
|
||||
--primary: oklch(0.65 0.18 260); /* Brighter, more vibrant indigo */
|
||||
--primary-foreground: oklch(0.18 0 0); /* Darker text on primary */
|
||||
--secondary: oklch(0.28 0 0); /* Medium dark gray */
|
||||
--secondary-foreground: oklch(0.75 0 0); /* Lighter gray */
|
||||
--muted: oklch(0.28 0 0);
|
||||
--muted-foreground: oklch(0.55 0 0); /* Mid-gray */
|
||||
--accent: oklch(0.28 0.04 260); /* Subtle dark hint of primary */
|
||||
--accent-foreground: oklch(0.7 0.06 260); /* Lighter desaturated primary */
|
||||
--destructive: oklch(0.65 0.16 20); /* Brighter red */
|
||||
--border: oklch(0.32 0 0); /* Medium dark gray */
|
||||
--input: oklch(0.28 0 0); /* Darker than border */
|
||||
--ring: oklch(0.5 0 0); /* Mid-dark gray */
|
||||
/* Dracula Dark Mode Palette */
|
||||
--background: oklch(0.17 0.005 270); /* Dracula background: #282a36 */
|
||||
--foreground: oklch(0.95 0.01 270); /* Dracula foreground: #f8f8f2 */
|
||||
--card: oklch(0.2 0.005 270); /* Slightly lighter for cards */
|
||||
--card-foreground: oklch(0.95 0.01 270);
|
||||
--popover: oklch(0.2 0.005 270);
|
||||
--popover-foreground: oklch(0.95 0.01 270);
|
||||
--primary: oklch(0.6 0.25 270); /* Dracula purple: #bd93f9 */
|
||||
--primary-foreground: oklch(0.17 0.005 270); /* Text on primary */
|
||||
--secondary: oklch(0.25 0.005 270); /* Dracula current line/selection: #44475a */
|
||||
--secondary-foreground: oklch(0.75 0.01 270); /* Lighter gray */
|
||||
--muted: oklch(0.25 0.005 270);
|
||||
--muted-foreground: oklch(0.55 0.005 270); /* Dracula comment: #6272a4 */
|
||||
--accent: oklch(0.3 0.05 270); /* Subtle hint of primary */
|
||||
--accent-foreground: oklch(0.7 0.05 270); /* Lighter desaturated primary */
|
||||
--destructive: oklch(0.7 0.2 0); /* Dracula red: #ff5555 */
|
||||
--border: oklch(0.3 0.005 270); /* Dracula border: #44475a (similar to secondary) */
|
||||
--input: oklch(0.25 0.005 270); /* Darker than border */
|
||||
--ring: oklch(0.5 0.005 270); /* Mid-dark gray */
|
||||
|
||||
/* Chart Colors (Dark Mode) - Brighter and distinct for visibility */
|
||||
--chart-1: oklch(0.75 0.2 270); /* Brighter purple-blue */
|
||||
--chart-2: oklch(0.8 0.18 180); /* Brighter teal */
|
||||
--chart-3: oklch(0.85 0.15 90); /* Brighter muted green-yellow */
|
||||
--chart-4: oklch(0.8 0.18 30); /* Brighter orange-brown */
|
||||
--chart-5: oklch(0.9 0.12 330); /* Brighter muted pink */
|
||||
/* Chart Colors (Dracula Dark Mode) - Vibrant and distinct */
|
||||
--chart-1: oklch(0.7 0.2 270); /* Dracula purple: #bd93f9 */
|
||||
--chart-2: oklch(0.7 0.2 120); /* Dracula green: #50fa7b */
|
||||
--chart-3: oklch(0.7 0.2 60); /* Dracula yellow: #f1fa8c */
|
||||
--chart-4: oklch(0.7 0.2 30); /* Dracula orange: #ffb86c */
|
||||
--chart-5: oklch(0.7 0.2 330); /* Dracula pink: #ff79c6 */
|
||||
|
||||
/* Sidebar Colors (Dark Mode) */
|
||||
--sidebar: oklch(0.22 0 0); /* Slightly lighter dark for sidebar */
|
||||
--sidebar-foreground: oklch(0.88 0 0);
|
||||
--sidebar-primary: oklch(0.65 0.18 260);
|
||||
--sidebar-primary-foreground: oklch(0.18 0 0);
|
||||
--sidebar-accent: oklch(0.28 0.04 260);
|
||||
--sidebar-accent-foreground: oklch(0.7 0.06 260);
|
||||
--sidebar-border: oklch(0.32 0 0);
|
||||
--sidebar-ring: oklch(0.5 0 0);
|
||||
/* Sidebar Colors (Dracula Dark Mode) */
|
||||
--sidebar: oklch(0.2 0.005 270); /* Slightly lighter dark for sidebar */
|
||||
--sidebar-foreground: oklch(0.95 0.01 270);
|
||||
--sidebar-primary: oklch(0.6 0.25 270);
|
||||
--sidebar-primary-foreground: oklch(0.17 0.005 270);
|
||||
--sidebar-accent: oklch(0.3 0.05 270);
|
||||
--sidebar-accent-foreground: oklch(0.7 0.05 270);
|
||||
--sidebar-border: oklch(0.3 0.005 270);
|
||||
--sidebar-ring: oklch(0.5 0.005 270);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -130,4 +130,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Discord Radio Bot Manager",
|
||||
description: "Manage the fleet of DRB nodes.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -46,7 +46,7 @@ export default function RootLayout({
|
||||
<footer>
|
||||
<div className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8 flex flex-col sm:flex-row justify-between items-center text-sm text-gray-500">
|
||||
<div className="mb-4 sm:mb-0 text-center sm:text-left">
|
||||
© 2025 Logan Cusano. All rights reserved.
|
||||
© 2015 - {new Date().getFullYear()} Logan Cusano. All rights reserved.
|
||||
</div>
|
||||
<div className="flex space-x-6">
|
||||
<a href="https://git.vpn.cusano.net/logan/drb-frontend/issues" target="_blank" rel="noopener noreferrer" className="hover:text-gray-700">Submit Issues</a>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { DRB_BASE_API_URL } from '@/constants/api';
|
||||
import { DiscordId, System, ErrorResponse } from '@/types';
|
||||
import { DiscordId, System, ErrorResponse, ActiveClient } from '@/types';
|
||||
import { authAwareFetch } from '@/utils/AuthAwareFetch';
|
||||
|
||||
interface BotsManagementProps {
|
||||
@@ -20,7 +20,7 @@ interface BotsManagementProps {
|
||||
}
|
||||
|
||||
const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) => {
|
||||
const [bots, setBots] = useState<string[]>([]);
|
||||
const [bots, setBots] = useState<ActiveClient[]>([]);
|
||||
const [discordIds, setDiscordIds] = useState<DiscordId[]>([]);
|
||||
const [systems, setSystems] = useState<System[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true); // Set to true initially
|
||||
@@ -61,7 +61,7 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
]);
|
||||
|
||||
if (nodesRes.ok) {
|
||||
const botsData: any = await nodesRes.json();
|
||||
const botsData: ActiveClient[] = await nodesRes.json();
|
||||
setBots(botsData);
|
||||
} else {
|
||||
let errorMsg = `Failed to fetch bots (${nodesRes.status})`;
|
||||
@@ -155,7 +155,7 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
if (response.ok) {
|
||||
fetchData();
|
||||
setIsAddIdDialogOpen(false);
|
||||
setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' });
|
||||
setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '', fixed_node: '' });
|
||||
setEditingId(null);
|
||||
} else {
|
||||
let errorMsg = `Failed to ${editingId ? 'update' : 'add'} Discord ID (${response.status})`;
|
||||
@@ -166,10 +166,10 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
setError(errorMsg);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
setError(`Network error during ${editingId ? 'update' : 'add'} Discord ID.`);
|
||||
console.error(err);
|
||||
}
|
||||
@@ -195,10 +195,10 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
setError(errorMsg);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
setError('Network error during delete Discord ID.');
|
||||
console.error(err);
|
||||
}
|
||||
@@ -207,8 +207,8 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
const handleAssignDismiss = async (): Promise<void> => {
|
||||
setError('');
|
||||
if (!selectedBotClientId || !selectedSystemId) {
|
||||
setError("Bot Client ID and System must be selected.");
|
||||
return;
|
||||
setError("Bot Client ID and System must be selected.");
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
setError('Authentication token is missing. Please log in.');
|
||||
@@ -236,10 +236,10 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
setError(errorMsg);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
setError(`Network error during ${assignDismissAction} bot.`);
|
||||
console.error(err);
|
||||
}
|
||||
@@ -270,14 +270,14 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{bots.length > 0 ? (
|
||||
bots.map((botClientId) => {
|
||||
bots.map((active_bot) => {
|
||||
return (
|
||||
<TableRow key={botClientId}>
|
||||
<TableRow key={active_bot.client_id}>
|
||||
<TableCell className="font-medium">
|
||||
<Link href={`/nodes/${botClientId}`} className="text-blue-600 hover:underline">
|
||||
{botClientId}
|
||||
<Link href={`/nodes/${active_bot.client_id}`} className="text-blue-600 hover:underline">
|
||||
{active_bot.nickname} ({active_bot.client_id})
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
@@ -292,7 +292,7 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-2">Manage Discord IDs</h3>
|
||||
<Button onClick={() => { setEditingId(null); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '' }); setIsAddIdDialogOpen(true);}} className="mb-4" disabled={!token}>Add New Discord ID</Button>
|
||||
<Button onClick={() => { setEditingId(null); setNewIdData({ discord_id: '', name: '', token: '', active: false, guild_ids: '', fixed_node: '' }); setIsAddIdDialogOpen(true); }} className="mb-4" disabled={!token}>Add New Discord ID</Button>
|
||||
<Button onClick={() => setIsAssignDismissDialogOpen(true)} className="mb-4 ml-2" disabled={!token}>Assign/Dismiss Bot System</Button>
|
||||
|
||||
<Table>
|
||||
@@ -303,6 +303,7 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
<TableHead>Token (Partial)</TableHead>
|
||||
<TableHead>Active</TableHead>
|
||||
<TableHead>Guilds</TableHead>
|
||||
<TableHead>Fixed Node ID</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -315,7 +316,14 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
<TableCell className="truncate max-w-xs">{dId.token ? dId.token.substring(0, 8) + '...' : 'N/A'}</TableCell>
|
||||
<TableCell>{dId.active ? 'Yes' : 'No'}</TableCell>
|
||||
<TableCell>{dId.guild_ids?.join(', ')}</TableCell>
|
||||
<TableCell>{dId?.fixed_node ? dId?.fixed_node : "None"}</TableCell>
|
||||
<TableCell>
|
||||
<a target="_blank" rel="noopener noreferrer" href={`https://discord.com/oauth2/authorize?client_id=${dId?.discord_id}&permissions=3146752&response_type=code&integration_type=0&scope=bot+rpc.voice.read+rpc.voice.write+rpc+voice+guilds.channels.read+guilds+guilds.join+gdm.join+guilds.members.read+connections`}>
|
||||
<Button variant="outline" size="sm" className="mr-2"
|
||||
disabled={!dId?.discord_id}>
|
||||
Invite
|
||||
</Button>
|
||||
</a>
|
||||
<Button variant="outline" size="sm" className="mr-2"
|
||||
onClick={() => {
|
||||
setEditingId(dId);
|
||||
@@ -325,6 +333,7 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
token: dId.token,
|
||||
active: dId.active,
|
||||
guild_ids: dId.guild_ids?.join(', '),
|
||||
fixed_node: dId.fixed_node,
|
||||
});
|
||||
setIsAddIdDialogOpen(true);
|
||||
}}
|
||||
@@ -346,120 +355,129 @@ const BotsManagement: React.FC<BotsManagementProps> = ({ token, logoutUser }) =>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<Dialog open={isAddIdDialogOpen} onOpenChange={setIsAddIdDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId ? 'Edit Discord ID' : 'Add New Discord ID'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newIdData.name}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, name: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
<Dialog open={isAddIdDialogOpen} onOpenChange={setIsAddIdDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingId ? 'Edit Discord ID' : 'Add New Discord ID'}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newIdData.name}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, name: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="discord_id" className="text-right">Discord ID</Label>
|
||||
<Input
|
||||
id="discord_id"
|
||||
value={newIdData.discord_id}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, discord_id: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="token" className="text-right">Token</Label>
|
||||
<Input
|
||||
id="token"
|
||||
value={newIdData.token}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, token: e.target.value })}
|
||||
className="col-span-3"
|
||||
placeholder={editingId ? "Token hidden for security, re-enter to change" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="guild_ids" className="text-right">Guild IDs (comma-separated)</Label>
|
||||
<Input
|
||||
id="guild_ids"
|
||||
value={newIdData.guild_ids}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, guild_ids: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="guild_ids" className="text-right">Fixed Node</Label>
|
||||
<Input
|
||||
id="fixed_node"
|
||||
value={newIdData?.fixed_node}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, fixed_node: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="active" className="text-right">Active</Label>
|
||||
<Checkbox
|
||||
id="active"
|
||||
checked={newIdData.active}
|
||||
onCheckedChange={(checked) => setNewIdData({ ...newIdData, active: checked === true })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="discord_id" className="text-right">Discord ID</Label>
|
||||
<Input
|
||||
id="discord_id"
|
||||
value={newIdData.discord_id}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, discord_id: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="token" className="text-right">Token</Label>
|
||||
<Input
|
||||
id="token"
|
||||
value={newIdData.token}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, token: e.target.value })}
|
||||
className="col-span-3"
|
||||
placeholder={editingId ? "Token hidden for security, re-enter to change" : ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="guild_ids" className="text-right">Guild IDs (comma-separated)</Label>
|
||||
<Input
|
||||
id="guild_ids"
|
||||
value={newIdData.guild_ids}
|
||||
onChange={(e) => setNewIdData({ ...newIdData, guild_ids: e.target.value })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="active" className="text-right">Active</Label>
|
||||
<Checkbox
|
||||
id="active"
|
||||
checked={newIdData.active}
|
||||
onCheckedChange={(checked) => setNewIdData({ ...newIdData, active: checked === true })}
|
||||
className="col-span-3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddId} disabled={!token}>
|
||||
{editingId ? 'Save Changes' : 'Add Discord ID'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAddId} disabled={!token}>
|
||||
{editingId ? 'Save Changes' : 'Add Discord ID'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isAssignDismissDialogOpen} onOpenChange={setIsAssignDismissDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign/Dismiss Bot to System</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="bot-client-id" className="text-right">Bot Client ID</Label>
|
||||
<Select onValueChange={setSelectedBotClientId} value={selectedBotClientId}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select a bot client ID" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{bots.map((botId) => (
|
||||
<SelectItem key={botId} value={botId}>{botId}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Dialog open={isAssignDismissDialogOpen} onOpenChange={setIsAssignDismissDialogOpen}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Assign/Dismiss Bot to System</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="bot-client-id" className="text-right">Bot Client ID</Label>
|
||||
<Select onValueChange={setSelectedBotClientId} value={selectedBotClientId}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select a bot client ID" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{bots.map((active_bot) => (
|
||||
<SelectItem key={active_bot.client_id} value={active_bot.client_id}>{active_bot.client_id}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="system-id" className="text-right">System</Label>
|
||||
<Select onValueChange={setSelectedSystemId} value={selectedSystemId}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select a system" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{systems.map((system) => (
|
||||
<SelectItem key={system._id} value={system._id}>{system.name} ({system._id})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="action" className="text-right">Action</Label>
|
||||
<Select
|
||||
onValueChange={(value: string) => setAssignDismissAction(value as 'assign' | 'dismiss')}
|
||||
value={assignDismissAction}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select action" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="assign">Assign</SelectItem>
|
||||
<SelectItem value="dismiss">Dismiss</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="system-id" className="text-right">System</Label>
|
||||
<Select onValueChange={setSelectedSystemId} value={selectedSystemId}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select a system" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{systems.map((system) => (
|
||||
<SelectItem key={system._id} value={system._id}>{system.name} ({system._id})</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="action" className="text-right">Action</Label>
|
||||
<Select
|
||||
onValueChange={(value: string) => setAssignDismissAction(value as 'assign' | 'dismiss')}
|
||||
value={assignDismissAction}
|
||||
>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Select action" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="assign">Assign</SelectItem>
|
||||
<SelectItem value="dismiss">Dismiss</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAssignDismiss} disabled={!selectedBotClientId || !selectedSystemId || !token}>Perform Action</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleAssignDismiss} disabled={!selectedBotClientId || !selectedSystemId || !token}>Perform Action</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card';
|
||||
import { ErrorResponse, NodeStatusResponse, System } from '@/types';
|
||||
import { ActiveClient, ErrorResponse, NodeStatusResponse, System } from '@/types';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -37,6 +37,9 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
const [isLeaveDiscordDialogOpen, setIsLeaveDiscordDialogOpen] = useState<boolean>(false);
|
||||
const [leaveGuildId, setLeaveGuildId] = useState<string>('');
|
||||
|
||||
// Active Client States
|
||||
const [activeClient, setActiveClient] = useState<ActiveClient | null>(null);
|
||||
|
||||
|
||||
const fetchClientStatus = async () => {
|
||||
if (!token) {
|
||||
@@ -51,6 +54,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
if (response.ok) {
|
||||
const data: NodeStatusResponse = await response.json();
|
||||
console.log(data)
|
||||
setActiveClient(data.active_client);
|
||||
setCurrentClientDiscordStatus(data.status.discord_status);
|
||||
setCurrentClientOp25Status(data.status.op25_status)
|
||||
} else {
|
||||
@@ -58,10 +62,10 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
setCurrentClientOp25Status('Failed to fetch status');
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
setCurrentClientDiscordStatus('Error fetching status');
|
||||
setCurrentClientOp25Status('Error fetching status');
|
||||
} finally {
|
||||
@@ -92,14 +96,14 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
setError(errorMsg);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
setError('Failed to fetch systems. Check server connection or console.');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -109,8 +113,8 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
fetchClientStatus();
|
||||
fetchSystems();
|
||||
} else {
|
||||
setLoading(false);
|
||||
setError('Please log in to manage this client.');
|
||||
setLoading(false);
|
||||
setError('Please log in to manage this client.');
|
||||
}
|
||||
}, [clientId, token, logoutUser]); // Added logoutUser to dependencies to avoid lint warnings
|
||||
|
||||
@@ -146,14 +150,15 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
"system_id": selectedSystem
|
||||
});
|
||||
} else if (action === "join") {
|
||||
if (!payloadData?.server_id || !payloadData?.channel_id) {
|
||||
if (!payloadData?.server_id || !payloadData?.channel_id || !selectedSystem) {
|
||||
setError("Please provide both Discord Server ID and Channel ID.");
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
httpOptions.body = JSON.stringify({
|
||||
"guild_id": payloadData.server_id,
|
||||
"channel_id": payloadData.channel_id
|
||||
"channel_id": payloadData.channel_id,
|
||||
"system_id": selectedSystem
|
||||
});
|
||||
} else if (action === "leave") {
|
||||
if (!payloadData?.guild_id) {
|
||||
@@ -181,10 +186,10 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
setError(`Failed to ${action} client "${clientId}": ${errorData.message || (typeof errorData.detail === 'string' ? errorData.detail : JSON.stringify(errorData.detail)) || response.statusText}`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
if (err.message === 'Unauthorized: Session expired or invalid token.') {
|
||||
setLoading(false); // already handled by authAwareFetch
|
||||
return;
|
||||
}
|
||||
setError(`Network error during ${action} client: ${err.message}`);
|
||||
console.error(err);
|
||||
} finally {
|
||||
@@ -215,7 +220,7 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
return (
|
||||
<Card className="w-full max-w-md mx-auto mt-8">
|
||||
<CardHeader>
|
||||
<CardTitle>Manage Client: {clientId}</CardTitle>
|
||||
<CardTitle>Manage Client: {activeClient?.nickname} - {clientId}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
@@ -331,9 +336,24 @@ const IndividualClientPage: React.FC<IndividualClientPageProps> = ({ clientId, t
|
||||
placeholder="Enter Discord Channel ID"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="system-select" className="text-right">Select System</Label>
|
||||
<Select onValueChange={setSelectedSystem} value={selectedSystem || ''}>
|
||||
<SelectTrigger className="col-span-3">
|
||||
<SelectValue placeholder="Choose a system" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableSystems.map((system) => (
|
||||
<SelectItem key={system._id} value={system._id}>
|
||||
{system.name} ({system._id})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={handleConfirmJoin} disabled={!discordServerId || !discordChannelId || loading || !token}>
|
||||
<Button onClick={handleConfirmJoin} disabled={!discordServerId || !discordChannelId || !selectedSystem || loading || !token}>
|
||||
{loading ? 'Joining...' : 'Confirm Join'}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => setIsJoinDiscordDialogOpen(false)}>
|
||||
|
||||
@@ -3,19 +3,46 @@
|
||||
import * as React from "react";
|
||||
import { useTheme } from "@/components/ThemeProvider"; // Adjust path as needed
|
||||
import { Button } from "@/components/ui/button"; // Assuming you have a Shadcn UI Button component
|
||||
import { Sun, Moon } from "lucide-react"; // Assuming you have lucide-react for icons
|
||||
import { Sun, Moon, Laptop } from "lucide-react"; // Import Laptop icon
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
// Function to cycle through themes: light -> dark -> system -> light
|
||||
const toggleTheme = () => {
|
||||
if (theme === "light") {
|
||||
setTheme("dark");
|
||||
} else if (theme === "dark") {
|
||||
setTheme("system");
|
||||
} else {
|
||||
setTheme("light");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost" // Use your preferred button variant
|
||||
size="icon"
|
||||
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
|
||||
onClick={toggleTheme} // Call the new toggleTheme function
|
||||
>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
{/* Icon for Light theme */}
|
||||
<Sun
|
||||
className={`h-[1.2rem] w-[1.2rem] transition-all
|
||||
${theme === "light" ? "rotate-0 scale-100" : "-rotate-90 scale-0"}
|
||||
`}
|
||||
/>
|
||||
{/* Icon for Dark theme */}
|
||||
<Moon
|
||||
className={`absolute h-[1.2rem] w-[1.2rem] transition-all
|
||||
${theme === "dark" ? "rotate-0 scale-100" : "rotate-90 scale-0"}
|
||||
`}
|
||||
/>
|
||||
{/* Icon for System theme */}
|
||||
<Laptop
|
||||
className={`absolute h-[1.2rem] w-[1.2rem] transition-all
|
||||
${theme === "system" ? "rotate-0 scale-100" : "rotate-90 scale-0"}
|
||||
`}
|
||||
/>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ export enum DemodTypes {
|
||||
token: string;
|
||||
active: boolean;
|
||||
guild_ids: string[];
|
||||
fixed_node?: string;
|
||||
}
|
||||
|
||||
export interface System {
|
||||
@@ -48,10 +49,17 @@ export enum DemodTypes {
|
||||
}
|
||||
|
||||
export interface NodeStatusResponse {
|
||||
active_client: ActiveClient;
|
||||
status: {
|
||||
op25_status: string;
|
||||
discord_status: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface ActiveClient {
|
||||
active_token: DiscordId;
|
||||
nickname: string;
|
||||
client_id: string;
|
||||
}
|
||||
|
||||
// Auth Context Types
|
||||
|
||||
Reference in New Issue
Block a user