tweaked ui and updated invite command

This commit is contained in:
2025-10-04 10:27:45 -04:00
parent 834e77a93e
commit 053ffe51f7
9 changed files with 496 additions and 343 deletions

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import axios from 'axios';
import { Button, Typography, Box, IconButton, Switch, Select, MenuItem, FormControl, FormControlLabel, Radio, RadioGroup, TextField, Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
import { Button, Typography, Box, IconButton, Switch, Select, MenuItem, FormControl, FormControlLabel, Radio, RadioGroup, TextField, Accordion, AccordionSummary, AccordionDetails, Snackbar, Alert } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
// UserSettings moved to NavBar
@@ -26,6 +26,11 @@ const ServerSettings = () => {
});
const [commandsList, setCommandsList] = useState([]);
const [invites, setInvites] = useState([]);
const [deleting, setDeleting] = useState({});
const [confirmOpen, setConfirmOpen] = useState(false);
const [pendingDeleteInvite, setPendingDeleteInvite] = useState(null);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [inviteForm, setInviteForm] = useState({ channelId: '', maxAge: 0, maxUses: 0, temporary: false });
const [commandsExpanded, setCommandsExpanded] = useState(false);
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
@@ -58,29 +63,31 @@ const ServerSettings = () => {
}
}
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
// Fetch settings (not used directly in this component)
axios.get(`http://localhost:3002/api/servers/${guildId}/settings`).catch(() => {});
axios.get(`${API_BASE}/api/servers/${guildId}/settings`).catch(() => {});
// Check if bot is in server
axios.get(`http://localhost:3002/api/servers/${guildId}/bot-status`)
axios.get(`${API_BASE}/api/servers/${guildId}/bot-status`)
.then(response => {
setIsBotInServer(response.data.isBotInServer);
});
// Fetch client ID
axios.get('http://localhost:3002/api/client-id')
axios.get(`${API_BASE}/api/client-id`)
.then(response => {
setClientId(response.data.clientId);
});
// Fetch channels
axios.get(`http://localhost:3002/api/servers/${guildId}/channels`)
axios.get(`${API_BASE}/api/servers/${guildId}/channels`)
.then(response => {
setChannels(response.data);
});
// Fetch welcome/leave settings
axios.get(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`)
axios.get(`${API_BASE}/api/servers/${guildId}/welcome-leave-settings`)
.then(response => {
if (response.data) {
setWelcomeLeaveSettings(response.data);
@@ -88,13 +95,13 @@ const ServerSettings = () => {
});
// Fetch roles
axios.get(`http://localhost:3002/api/servers/${guildId}/roles`)
axios.get(`${API_BASE}/api/servers/${guildId}/roles`)
.then(response => {
setRoles(response.data);
});
// Fetch autorole settings
axios.get(`http://localhost:3002/api/servers/${guildId}/autorole-settings`)
axios.get(`${API_BASE}/api/servers/${guildId}/autorole-settings`)
.then(response => {
if (response.data) {
setAutoroleSettings(response.data);
@@ -102,14 +109,14 @@ const ServerSettings = () => {
});
// Fetch commands/help list
axios.get(`http://localhost:3002/api/servers/${guildId}/commands`)
axios.get(`${API_BASE}/api/servers/${guildId}/commands`)
.then(response => {
setCommandsList(response.data || []);
})
.catch(() => setCommandsList([]));
// Fetch invites
axios.get(`http://localhost:3002/api/servers/${guildId}/invites`)
axios.get(`${API_BASE}/api/servers/${guildId}/invites`)
.then(resp => setInvites(resp.data || []))
.catch(() => setInvites([]));
@@ -121,7 +128,7 @@ const ServerSettings = () => {
}, [guildId, location.state]);
const handleAutoroleSettingUpdate = (newSettings) => {
axios.post(`http://localhost:3002/api/servers/${guildId}/autorole-settings`, newSettings)
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/autorole-settings`, newSettings)
.then(response => {
if (response.data.success) {
setAutoroleSettings(newSettings);
@@ -140,7 +147,7 @@ const ServerSettings = () => {
};
const handleSettingUpdate = (newSettings) => {
axios.post(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`, newSettings)
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/welcome-leave-settings`, newSettings)
.then(response => {
if (response.data.success) {
setWelcomeLeaveSettings(newSettings);
@@ -207,7 +214,7 @@ const ServerSettings = () => {
const handleConfirmLeave = async () => {
try {
await axios.post(`http://localhost:3002/api/servers/${guildId}/leave`);
await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/leave`);
setIsBotInServer(false);
} catch (error) {
console.error('Error leaving server:', error);
@@ -251,36 +258,51 @@ const ServerSettings = () => {
<AccordionDetails>
{!isBotInServer && <Typography>Invite the bot to enable commands.</Typography>}
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: '10px' }}>
{commandsList && [...commandsList].sort((a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'})).map(cmd => (
<Box key={cmd.name} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
<Box>
<Typography sx={{ fontWeight: 'bold' }}>{cmd.name}</Typography>
<Typography variant="body2">{cmd.description}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{['help', 'manage-commands'].includes(cmd.name) ? (
<FormControlLabel
control={<Switch checked={true} disabled />}
label="Locked"
/>
) : (
<FormControlLabel
control={<Switch checked={cmd.enabled} onChange={async (e) => {
const newVal = e.target.checked;
// optimistic update
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: newVal } : c));
try {
await axios.post(`http://localhost:3002/api/servers/${guildId}/commands/${cmd.name}/toggle`, { enabled: newVal });
} catch (err) {
// revert on error
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: cmd.enabled } : c));
}
}} disabled={!isBotInServer} label={cmd.enabled ? 'Enabled' : 'Disabled'} />}
/>
)}
</Box>
</Box>
))}
{/** Render protected commands first in a fixed order **/}
{(() => {
const protectedOrder = ['help', 'manage-commands'];
const protectedCmds = protectedOrder.map(name => commandsList.find(c => c.name === name)).filter(Boolean);
const otherCmds = (commandsList || []).filter(c => !protectedOrder.includes(c.name)).sort((a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}));
return (
<>
{protectedCmds.map(cmd => (
<Box key={cmd.name} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
<Box>
<Typography sx={{ fontWeight: 'bold' }}>{cmd.name}</Typography>
<Typography variant="body2">{cmd.description}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControlLabel control={<Switch checked={true} disabled />} label="Locked" />
</Box>
</Box>
))}
{otherCmds.map(cmd => (
<Box key={cmd.name} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
<Box>
<Typography sx={{ fontWeight: 'bold' }}>{cmd.name}</Typography>
<Typography variant="body2">{cmd.description}</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<FormControlLabel
control={<Switch checked={cmd.enabled} onChange={async (e) => {
const newVal = e.target.checked;
// optimistic update
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: newVal } : c));
try {
await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/commands/${cmd.name}/toggle`, { enabled: newVal });
} catch (err) {
// revert on error
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: cmd.enabled } : c));
}
}} disabled={!isBotInServer} label={cmd.enabled ? 'Enabled' : 'Disabled'} />}
/>
</Box>
</Box>
))}
</>
);
})()}
</Box>
</AccordionDetails>
</Accordion>
@@ -332,7 +354,7 @@ const ServerSettings = () => {
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
<Button variant="contained" onClick={async () => {
try {
const resp = await axios.post(`http://localhost:3002/api/servers/${guildId}/invites`, inviteForm);
const resp = await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/invites`, inviteForm);
if (resp.data && resp.data.success) {
setInvites(prev => [...prev, resp.data.invite]);
}
@@ -352,12 +374,32 @@ const ServerSettings = () => {
<Typography variant="caption">Created: {new Date(inv.createdAt).toLocaleString()} Uses: {inv.uses || 0} MaxUses: {inv.maxUses || 0} MaxAge(s): {inv.maxAge || 0} Temporary: {inv.temporary ? 'Yes' : 'No'}</Typography>
</Box>
<Box>
<Button startIcon={<ContentCopyIcon />} onClick={() => { navigator.clipboard.writeText(inv.url); }}>Copy</Button>
<Button startIcon={<DeleteIcon />} color="error" onClick={async () => {
<Button startIcon={<ContentCopyIcon />} onClick={async () => {
// robust clipboard copy with fallback
try {
await axios.delete(`http://localhost:3002/api/servers/${guildId}/invites/${inv.code}`);
setInvites(prev => prev.filter(i => i.code !== inv.code));
} catch (err) { console.error('Error deleting invite:', err); }
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(inv.url);
} else {
// fallback for older browsers
const input = document.createElement('input');
input.value = inv.url;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
}
setSnackbarMessage('Copied invite URL to clipboard');
setSnackbarOpen(true);
} catch (err) {
console.error('Clipboard copy failed:', err);
setSnackbarMessage('Failed to copy — please copy manually');
setSnackbarOpen(true);
}
}}>Copy</Button>
<Button startIcon={<DeleteIcon />} color="error" disabled={!!deleting[inv.code]} onClick={() => {
// open confirm dialog for this invite
setPendingDeleteInvite(inv);
setConfirmOpen(true);
}}>Delete</Button>
</Box>
</Box>
@@ -497,6 +539,61 @@ const ServerSettings = () => {
title="Confirm Leave"
message={`Are you sure you want the bot to leave ${server?.name}?`}
/>
{/* Confirm dialog for invite deletion */}
<ConfirmDialog
open={confirmOpen}
onClose={() => { setConfirmOpen(false); setPendingDeleteInvite(null); }}
onConfirm={async () => {
// perform deletion for pendingDeleteInvite
if (!pendingDeleteInvite) {
setConfirmOpen(false);
return;
}
const code = pendingDeleteInvite.code;
setConfirmOpen(false);
setDeleting(prev => ({ ...prev, [code]: true }));
try {
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
// fetch token (one retry)
let token = null;
try {
const tokenResp = await axios.get(`${API_BASE}/api/servers/${guildId}/invite-token`);
token = tokenResp && tokenResp.data && tokenResp.data.token;
} catch (tErr) {
try {
const tokenResp2 = await axios.get(`${API_BASE}/api/servers/${guildId}/invite-token`);
token = tokenResp2 && tokenResp2.data && tokenResp2.data.token;
} catch (tErr2) {
throw new Error('Failed to obtain delete token from server');
}
}
if (!token) throw new Error('No delete token received from server');
await axios.delete(`${API_BASE}/api/servers/${guildId}/invites/${code}`, { headers: { 'x-invite-token': token } });
setInvites(prev => prev.filter(i => i.code !== code));
setSnackbarMessage('Invite deleted');
setSnackbarOpen(true);
} catch (err) {
console.error('Error deleting invite:', err);
const msg = (err && err.message) || (err && err.response && err.response.data && err.response.data.message) || 'Failed to delete invite';
setSnackbarMessage(msg);
setSnackbarOpen(true);
} finally {
setDeleting(prev => {
const copy = { ...prev };
delete copy[pendingDeleteInvite?.code];
return copy;
});
setPendingDeleteInvite(null);
}
}}
title="Delete Invite"
message={`Are you sure you want to delete invite ${pendingDeleteInvite ? pendingDeleteInvite.url : ''}?`}
/>
<Snackbar open={snackbarOpen} autoHideDuration={4000} onClose={() => setSnackbarOpen(false)}>
<Alert onClose={() => setSnackbarOpen(false)} severity="info" sx={{ width: '100%' }}>
{snackbarMessage}
</Alert>
</Snackbar>
</div>
);
};