Compare commits

10 Commits

Author SHA1 Message Date
Logan Cusano
e6bb4ddf7e Updated theme toggle to go through all three options and update dark mode colors
All checks were successful
Lint Pull Request / lint (push) Successful in 40s
release-image / release-image (push) Successful in 14m30s
2025-07-06 20:53:15 -04:00
Logan Cusano
9506f6246c Fix copyright date 2025-07-06 20:52:44 -04:00
Logan Cusano
e64984b7b8 Include the system ID when starting the bot
Some checks failed
Lint Pull Request / lint (push) Successful in 1m0s
release-image / release-image (push) Has been cancelled
2025-07-06 19:35:00 -04:00
Logan Cusano
20187a06f0 Update invite link
All checks were successful
Lint Pull Request / lint (push) Successful in 30s
release-image / release-image (push) Successful in 10m39s
2025-06-29 22:49:41 -04:00
Logan Cusano
464f37adda Implement active client on status
Some checks failed
Lint Pull Request / lint (push) Successful in 20s
release-image / release-image (push) Has been cancelled
2025-06-29 22:42:20 -04:00
Logan Cusano
a1fda6fe87 Add invite button for the bot tokens
All checks were successful
Lint Pull Request / lint (push) Successful in 23s
release-image / release-image (push) Successful in 10m15s
2025-06-29 22:13:47 -04:00
Logan Cusano
aaa362ec0e Update metadata
All checks were successful
Lint Pull Request / lint (push) Successful in 22s
release-image / release-image (push) Successful in 10m16s
2025-06-29 22:00:34 -04:00
Logan Cusano
3b62590df3 Implement Fixed Node Management
All checks were successful
Lint Pull Request / lint (push) Successful in 23s
release-image / release-image (push) Successful in 11m52s
2025-06-29 19:38:45 -04:00
Logan Cusano
8373fdd685 Implement nickname for table
All checks were successful
Lint Pull Request / lint (push) Successful in 22s
release-image / release-image (push) Successful in 10m26s
2025-06-29 03:46:43 -04:00
6a74cd55a0 Merge pull request 'Refactored the header' (#2) from refactoring-header into main
All checks were successful
Lint Pull Request / lint (push) Successful in 20s
release-image / release-image (push) Successful in 10m23s
Reviewed-on: #2
2025-05-28 23:20:07 -04:00
6 changed files with 271 additions and 198 deletions

View File

@@ -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;
}
}
}

View File

@@ -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">
&copy; 2025 Logan Cusano. All rights reserved.
&copy; 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>

View File

@@ -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>
);

View File

@@ -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)}>

View File

@@ -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>
);

View File

@@ -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