Moderation Update
This commit is contained in:
@@ -3,7 +3,7 @@ import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-route
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { UserProvider } from './contexts/UserContext';
|
||||
import { BackendProvider, useBackend } from './contexts/BackendContext';
|
||||
import { CssBaseline } from '@mui/material';
|
||||
import { CssBaseline, Box } from '@mui/material';
|
||||
import Login from './components/Login';
|
||||
import Dashboard from './components/Dashboard';
|
||||
import ServerSettings from './components/server/ServerSettings';
|
||||
@@ -11,6 +11,7 @@ import NavBar from './components/NavBar';
|
||||
import HelpPage from './components/server/HelpPage';
|
||||
import DiscordPage from './components/DiscordPage';
|
||||
import MaintenancePage from './components/common/MaintenancePage';
|
||||
import Footer from './components/common/Footer';
|
||||
|
||||
function AppInner() {
|
||||
const { backendOnline, checking, forceCheck } = useBackend();
|
||||
@@ -23,23 +24,41 @@ function AppInner() {
|
||||
<UserProvider>
|
||||
<ThemeProvider>
|
||||
<CssBaseline />
|
||||
<Router>
|
||||
<TitleSetter />
|
||||
{!backendOnline ? (
|
||||
<MaintenancePage onRetry={handleRetry} checking={checking} />
|
||||
) : (
|
||||
<>
|
||||
<NavBar />
|
||||
<Routes>
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/server/:guildId" element={<ServerSettings />} />
|
||||
<Route path="/server/:guildId/help" element={<HelpPage />} />
|
||||
<Route path="/discord" element={<DiscordPage />} />
|
||||
</Routes>
|
||||
</>
|
||||
)}
|
||||
</Router>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minHeight: '100vh',
|
||||
}}
|
||||
>
|
||||
<Router>
|
||||
<TitleSetter />
|
||||
{!backendOnline ? (
|
||||
<MaintenancePage onRetry={handleRetry} checking={checking} />
|
||||
) : (
|
||||
<>
|
||||
<NavBar />
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Login />} />
|
||||
<Route path="/dashboard" element={<Dashboard />} />
|
||||
<Route path="/server/:guildId" element={<ServerSettings />} />
|
||||
<Route path="/server/:guildId/help" element={<HelpPage />} />
|
||||
<Route path="/discord" element={<DiscordPage />} />
|
||||
</Routes>
|
||||
</Box>
|
||||
<Footer />
|
||||
</>
|
||||
)}
|
||||
</Router>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
</UserProvider>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Grid, Card, CardContent, Typography, Box, IconButton, Snackbar, Alert, Menu, MenuItem, Button } from '@mui/material';
|
||||
import { Grid, Card, CardContent, Typography, Box, IconButton, Snackbar, Alert, Menu, MenuItem, Button, Container } from '@mui/material';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { UserContext } from '../contexts/UserContext';
|
||||
import { useBackend } from '../contexts/BackendContext';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import DashboardIcon from '@mui/icons-material/Dashboard';
|
||||
import WavingHandIcon from '@mui/icons-material/WavingHand';
|
||||
import { get, post } from '../lib/api';
|
||||
|
||||
import ConfirmDialog from './common/ConfirmDialog';
|
||||
@@ -13,6 +16,7 @@ const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
const { eventTarget } = useBackend();
|
||||
|
||||
const [guilds, setGuilds] = useState([]);
|
||||
const [botStatus, setBotStatus] = useState({});
|
||||
@@ -89,6 +93,25 @@ const Dashboard = () => {
|
||||
fetchStatuses();
|
||||
}, [guilds, API_BASE]);
|
||||
|
||||
// Listen for bot status updates
|
||||
useEffect(() => {
|
||||
if (!eventTarget) return;
|
||||
|
||||
const onBotStatusUpdate = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
setBotStatus(prev => ({
|
||||
...prev,
|
||||
[data.guildId]: data.isBotInServer
|
||||
}));
|
||||
};
|
||||
|
||||
eventTarget.addEventListener('botStatusUpdate', onBotStatusUpdate);
|
||||
|
||||
return () => {
|
||||
eventTarget.removeEventListener('botStatusUpdate', onBotStatusUpdate);
|
||||
};
|
||||
}, [eventTarget]);
|
||||
|
||||
// Dashboard no longer loads live settings; that's on the server settings page
|
||||
|
||||
// Live notifications handlers were removed from Dashboard
|
||||
@@ -139,18 +162,32 @@ const Dashboard = () => {
|
||||
const handleSnackbarClose = () => setSnackbarOpen(false);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Container maxWidth="lg" sx={{ padding: { xs: 2, sm: 3, md: 5 } }}>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Dashboard</Typography>
|
||||
{user && <Typography variant="h6" gutterBottom>Welcome, {user.username}</Typography>}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
|
||||
<DashboardIcon sx={{ mr: 1, fontSize: { xs: '2rem', sm: '2.5rem' } }} />
|
||||
<Typography variant={{ xs: 'h4', sm: 'h3' }}>Dashboard</Typography>
|
||||
</Box>
|
||||
{user && <Box sx={{ display: 'flex', alignItems: 'center', mt: { xs: 4, sm: 5 }, mb: { xs: 4, sm: 5 } }}>
|
||||
<WavingHandIcon sx={{ mr: 1, color: 'text.secondary' }} />
|
||||
<Typography
|
||||
variant={{ xs: 'h3', sm: 'h2' }}
|
||||
sx={{
|
||||
fontWeight: 300,
|
||||
color: 'text.secondary'
|
||||
}}
|
||||
>
|
||||
Welcome back, {user.username}
|
||||
</Typography>
|
||||
</Box>}
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="h6" gutterBottom>Your Admin Servers:</Typography>
|
||||
|
||||
<Grid container spacing={3} justifyContent="center">
|
||||
{guilds.map(guild => (
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={guild.id} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid item xs={12} sm={6} md={6} lg={4} key={guild.id} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||
<Card
|
||||
onClick={() => handleCardClick(guild)}
|
||||
@@ -163,6 +200,11 @@ const Dashboard = () => {
|
||||
transform: 'translateY(-5px)',
|
||||
boxShadow: '0 12px 24px rgba(0,0,0,0.3)',
|
||||
},
|
||||
'&:active': {
|
||||
transform: 'translateY(-2px) scale(0.98)',
|
||||
transition: 'transform 0.1s ease-in-out',
|
||||
boxShadow: '0 8px 16px rgba(0,0,0,0.4)',
|
||||
},
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
@@ -171,30 +213,66 @@ const Dashboard = () => {
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', p: 2 }}>
|
||||
<CardContent sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', p: { xs: 1.5, sm: 2 } }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png` : 'https://cdn.discordapp.com/embed/avatars/0.png'}
|
||||
sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
width: { xs: 60, sm: 80 },
|
||||
height: { xs: 60, sm: 80 },
|
||||
borderRadius: '50%',
|
||||
mb: 2,
|
||||
boxShadow: '0 4px 8px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" sx={{ fontWeight: 700, textAlign: 'center', mb: 1 }}>{guild.name}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
textAlign: 'center',
|
||||
mb: 1,
|
||||
fontSize: { xs: '1rem', sm: '1.25rem' },
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
lineHeight: 1.2,
|
||||
minHeight: { xs: '2.4rem', sm: '2.5rem' },
|
||||
}}
|
||||
>
|
||||
{guild.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||||
{botStatus[guild.id] ? (
|
||||
<Button variant="contained" color="error" size="small" onClick={(e) => handleLeaveBot(e, guild)} startIcon={<RemoveCircleOutlineIcon />}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={(e) => handleLeaveBot(e, guild)}
|
||||
startIcon={<RemoveCircleOutlineIcon />}
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
>
|
||||
Leave
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="contained" color="success" size="small" onClick={(e) => handleInviteBot(e, guild)} startIcon={<PersonAddIcon />}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
size="small"
|
||||
onClick={(e) => handleInviteBot(e, guild)}
|
||||
startIcon={<PersonAddIcon />}
|
||||
sx={{ fontSize: { xs: '0.75rem', sm: '0.875rem' } }}
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
)}
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); setMenuAnchor(e.currentTarget); setMenuGuild(guild); }} aria-label="server menu">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => { e.stopPropagation(); setMenuAnchor(e.currentTarget); setMenuGuild(guild); }}
|
||||
aria-label="server menu"
|
||||
sx={{ ml: { xs: 0, sm: 1 } }}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
@@ -206,26 +284,26 @@ const Dashboard = () => {
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Menu anchorEl={menuAnchor} open={!!menuAnchor} onClose={() => { setMenuAnchor(null); setMenuGuild(null); }}>
|
||||
<MenuItem onClick={() => { setMenuAnchor(null); if (menuGuild) window.location.href = `/server/${menuGuild.id}`; }}>Open Server Settings</MenuItem>
|
||||
</Menu>
|
||||
<Menu anchorEl={menuAnchor} open={!!menuAnchor} onClose={() => { setMenuAnchor(null); setMenuGuild(null); }}>
|
||||
<MenuItem onClick={() => { setMenuAnchor(null); if (menuGuild) window.location.href = `/server/${menuGuild.id}`; }}>Open Server Settings</MenuItem>
|
||||
</Menu>
|
||||
|
||||
{/* Live Notifications dialog removed from Dashboard — available on Server Settings page */}
|
||||
{/* Live Notifications dialog removed from Dashboard — available on Server Settings page */}
|
||||
|
||||
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleSnackbarClose}>
|
||||
<Alert onClose={handleSnackbarClose} severity="info" sx={{ width: '100%' }}>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleSnackbarClose}>
|
||||
<Alert onClose={handleSnackbarClose} severity="info" sx={{ width: '100%' }}>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<ConfirmDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onConfirm={handleConfirmLeave}
|
||||
title="Confirm Leave"
|
||||
message={`Are you sure you want the bot to leave ${selectedGuild?.name}?`}
|
||||
/>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
onConfirm={handleConfirmLeave}
|
||||
title="Confirm Leave"
|
||||
message={`Are you sure you want the bot to leave ${selectedGuild?.name}?`}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const NavBar = () => {
|
||||
const closeMenu = () => { setAnchorEl(null); setOpen(false); };
|
||||
|
||||
return (
|
||||
<AppBar position="static" color="transparent" elevation={0} sx={{ mb: 2, borderBottom: '1px solid rgba(0,0,0,0.06)' }}>
|
||||
<AppBar position="static" color="default" elevation={1} sx={{ mb: 2, borderBottom: '1px solid rgba(0,0,0,0.12)' }}>
|
||||
<Toolbar sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', px: { xs: 2, sm: 4 } }}>
|
||||
<Box>
|
||||
<IconButton onClick={toggleOpen} aria-label="menu" size="large" sx={{ bgcolor: open ? 'primary.main' : 'transparent', color: open ? 'white' : 'text.primary' }}>
|
||||
@@ -33,8 +33,12 @@ const NavBar = () => {
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 800, display: { xs: 'block', sm: 'none' } }} onClick={() => { navigate('/dashboard'); closeMenu(); }}>
|
||||
ECS
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 800, display: { xs: 'none', sm: 'block' } }} onClick={() => { navigate('/dashboard'); closeMenu(); }}>
|
||||
ECS - EHDCHADSWORTH
|
||||
EhChadServices
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
|
||||
32
frontend/src/components/common/Footer.js
Normal file
32
frontend/src/components/common/Footer.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
const Footer = () => {
|
||||
return (
|
||||
<Box
|
||||
component="footer"
|
||||
sx={{
|
||||
py: { xs: 1, sm: 2 },
|
||||
px: { xs: 1, sm: 2 },
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'light'
|
||||
? theme.palette.grey[200]
|
||||
: theme.palette.grey[800],
|
||||
textAlign: 'center',
|
||||
borderTop: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontSize: { xs: '0.75rem', sm: '0.875rem' },
|
||||
}}
|
||||
>
|
||||
© ehchadservices.com 2025
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
};;
|
||||
|
||||
export default Footer;
|
||||
@@ -26,10 +26,10 @@ const HelpPage = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<IconButton onClick={handleBack}><ArrowBackIcon /></IconButton>
|
||||
<Typography variant="h5">Commands List</Typography>
|
||||
<Box sx={{ padding: { xs: 2, sm: 3, md: 5 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 2 }, mb: 2 }}>
|
||||
<IconButton onClick={handleBack}><ArrowBackIcon /></IconButton>
|
||||
<Typography variant={{ xs: 'h5', sm: 'h5' }}>Commands List</Typography>
|
||||
</Box>
|
||||
<Box sx={{ marginTop: 2 }}>
|
||||
{commands.length === 0 && <Typography>No commands available.</Typography>}
|
||||
@@ -43,7 +43,7 @@ const HelpPage = () => {
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { useBackend } from '../../contexts/BackendContext';
|
||||
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, Tabs, Tab, Snackbar, Alert } from '@mui/material';
|
||||
import { Button, Typography, Box, IconButton, Switch, Select, MenuItem, FormControl, FormControlLabel, Radio, RadioGroup, TextField, Accordion, AccordionSummary, AccordionDetails, Tabs, Tab, InputLabel, Snackbar, Alert, Autocomplete } from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
// UserSettings moved to NavBar
|
||||
import ConfirmDialog from '../common/ConfirmDialog';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { UserContext } from '../../contexts/UserContext';
|
||||
|
||||
// Use a relative API base by default so the frontend talks to the same origin that served it.
|
||||
// In development you can set REACT_APP_API_BASE to a full URL if needed.
|
||||
@@ -18,6 +19,7 @@ const ServerSettings = () => {
|
||||
const { guildId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
// settings state removed (not used) to avoid lint warnings
|
||||
const [isBotInServer, setIsBotInServer] = useState(false);
|
||||
@@ -35,8 +37,6 @@ const ServerSettings = () => {
|
||||
const [deleting, setDeleting] = useState({});
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [pendingDeleteInvite, setPendingDeleteInvite] = useState(null);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
// SSE connection status (not currently displayed)
|
||||
const [confirmDeleteTwitch, setConfirmDeleteTwitch] = useState(false);
|
||||
const [pendingTwitchUser, setPendingTwitchUser] = useState(null);
|
||||
@@ -44,18 +44,6 @@ const ServerSettings = () => {
|
||||
const [pendingKickUser, setPendingKickUser] = useState(null);
|
||||
const [commandsExpanded, setCommandsExpanded] = useState(false);
|
||||
const [liveExpanded, setLiveExpanded] = useState(false);
|
||||
const [inviteForm, setInviteForm] = useState({ channelId: '', maxAge: 0, maxUses: 0, temporary: false });
|
||||
const [liveEnabled, setLiveEnabled] = useState(false);
|
||||
const [liveChannelId, setLiveChannelId] = useState('');
|
||||
const [liveTwitchUser, setLiveTwitchUser] = useState('');
|
||||
const [liveMessage, setLiveMessage] = useState('');
|
||||
const [liveCustomMessage, setLiveCustomMessage] = useState('');
|
||||
const [watchedUsers, setWatchedUsers] = useState([]);
|
||||
const [liveStatus, setLiveStatus] = useState({});
|
||||
const [liveTabValue, setLiveTabValue] = useState(0);
|
||||
const [kickUsers, setKickUsers] = useState([]);
|
||||
const [kickStatus, setKickStatus] = useState({});
|
||||
const [kickUser, setKickUser] = useState('');
|
||||
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
|
||||
welcome: {
|
||||
enabled: false,
|
||||
@@ -70,6 +58,39 @@ const ServerSettings = () => {
|
||||
customMessage: '',
|
||||
},
|
||||
});
|
||||
const [adminLogsSettings, setAdminLogsSettings] = useState({
|
||||
enabled: false,
|
||||
channelId: '',
|
||||
commands: { kick: true, ban: true, timeout: true }
|
||||
});
|
||||
const [adminLogs, setAdminLogs] = useState([]);
|
||||
const [selectedChannelId, setSelectedChannelId] = useState('');
|
||||
const [moderationTarget, setModerationTarget] = useState('');
|
||||
const [moderationReason, setModerationReason] = useState('');
|
||||
const [timeoutDuration, setTimeoutDuration] = useState('');
|
||||
const [serverMembers, setServerMembers] = useState([]);
|
||||
|
||||
// Add back missing state variables that are still used in the code
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [inviteForm, setInviteForm] = useState({
|
||||
maxUses: '',
|
||||
expiresAt: '',
|
||||
channelId: ''
|
||||
});
|
||||
const [liveTabValue, setLiveTabValue] = useState(0);
|
||||
const [liveEnabled, setLiveEnabled] = useState(false);
|
||||
const [liveChannelId, setLiveChannelId] = useState('');
|
||||
const [liveTwitchUser, setLiveTwitchUser] = useState('');
|
||||
const [liveMessage, setLiveMessage] = useState('Twitch user {user} is now live!');
|
||||
const [liveCustomMessage, setLiveCustomMessage] = useState('');
|
||||
const [watchedUsers, setWatchedUsers] = useState([]);
|
||||
const [kickUsers, setKickUsers] = useState([]);
|
||||
const [liveStatus, setLiveStatus] = useState({});
|
||||
|
||||
// Confirm dialog states for admin logs
|
||||
const [deleteLogDialog, setDeleteLogDialog] = useState({ open: false, logId: null, logAction: '' });
|
||||
const [deleteAllLogsDialog, setDeleteAllLogsDialog] = useState(false);
|
||||
|
||||
const defaultWelcomeMessages = ["Welcome to the server, {user}!", "Hey {user}, welcome!", "{user} has joined the party!"];
|
||||
const defaultLeaveMessages = ["{user} has left the server.", "Goodbye, {user}.", "We'll miss you, {user}."];
|
||||
@@ -103,13 +124,13 @@ const ServerSettings = () => {
|
||||
}).catch(() => {
|
||||
// ignore when offline
|
||||
});
|
||||
|
||||
// Fetch bot status
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/bot-status`).then(response => {
|
||||
setIsBotInServer(response.data.isBotInServer);
|
||||
}).catch(() => setIsBotInServer(false));
|
||||
|
||||
// Fetch channels
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/channels`).then(response => {
|
||||
setChannels(response.data);
|
||||
}).catch(() => {
|
||||
setChannels([]);
|
||||
});
|
||||
// Fetch channels - moved to separate useEffect to depend on bot status
|
||||
|
||||
// Fetch welcome/leave settings
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/welcome-leave-settings`).then(response => {
|
||||
@@ -144,18 +165,46 @@ const ServerSettings = () => {
|
||||
setLiveCustomMessage(s.customMessage || '');
|
||||
}).catch(() => {});
|
||||
|
||||
// Fetch admin logs settings
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/admin-logs-settings`).then(response => {
|
||||
if (response.data) {
|
||||
setAdminLogsSettings(response.data);
|
||||
setSelectedChannelId(response.data.channelId || '');
|
||||
}
|
||||
}).catch(() => {
|
||||
// ignore
|
||||
});
|
||||
|
||||
// Fetch admin logs
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/admin-logs`).then(response => {
|
||||
setAdminLogs(response.data || []);
|
||||
}).catch(() => setAdminLogs([]));
|
||||
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/twitch-users`).then(resp => setWatchedUsers(resp.data || [])).catch(() => setWatchedUsers([]));
|
||||
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/kick-users`).then(resp => setKickUsers(resp.data || [])).catch(() => setKickUsers([]));
|
||||
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/invites`).then(resp => setInvites(resp.data || [])).catch(() => setInvites([]));
|
||||
|
||||
// Fetch server members for moderation
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/members`).then(resp => setServerMembers(resp.data || [])).catch(() => setServerMembers([]));
|
||||
|
||||
// Open commands accordion if navigated from Help back button
|
||||
if (location.state && location.state.openCommands) {
|
||||
setCommandsExpanded(true);
|
||||
}
|
||||
|
||||
}, [guildId, location.state]);
|
||||
}, [guildId, location.state, isBotInServer]);
|
||||
|
||||
// Fetch channels only when bot is in server
|
||||
useEffect(() => {
|
||||
if (!guildId || !isBotInServer) return;
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/channels`).then(response => {
|
||||
setChannels(response.data);
|
||||
}).catch(() => {
|
||||
setChannels([]);
|
||||
});
|
||||
}, [guildId, isBotInServer]);
|
||||
|
||||
// Listen to backend events for live notifications and twitch user updates
|
||||
const { eventTarget } = useBackend();
|
||||
@@ -198,10 +247,35 @@ const ServerSettings = () => {
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/commands`).then(resp => setCommandsList(resp.data || [])).catch(() => {});
|
||||
};
|
||||
|
||||
const onAdminLogAdded = (e) => {
|
||||
const data = e.detail || {};
|
||||
if (!data) return;
|
||||
if (data.guildId && data.guildId !== guildId) return;
|
||||
// Add the new log to the beginning of the list
|
||||
setAdminLogs(prev => [data.log, ...prev.slice(0, 49)]); // Keep only latest 50
|
||||
};
|
||||
|
||||
const onAdminLogDeleted = (e) => {
|
||||
const data = e.detail || {};
|
||||
if (!data) return;
|
||||
if (data.guildId && data.guildId !== guildId) return;
|
||||
setAdminLogs(prev => prev.filter(log => log.id !== data.logId));
|
||||
};
|
||||
|
||||
const onAdminLogsCleared = (e) => {
|
||||
const data = e.detail || {};
|
||||
if (!data) return;
|
||||
if (data.guildId && data.guildId !== guildId) return;
|
||||
setAdminLogs([]);
|
||||
};
|
||||
|
||||
eventTarget.addEventListener('twitchUsersUpdate', onTwitchUsers);
|
||||
eventTarget.addEventListener('kickUsersUpdate', onKickUsers);
|
||||
eventTarget.addEventListener('liveNotificationsUpdate', onLiveNotifications);
|
||||
eventTarget.addEventListener('commandToggle', onCommandToggle);
|
||||
eventTarget.addEventListener('adminLogAdded', onAdminLogAdded);
|
||||
eventTarget.addEventListener('adminLogDeleted', onAdminLogDeleted);
|
||||
eventTarget.addEventListener('adminLogsCleared', onAdminLogsCleared);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
@@ -209,12 +283,15 @@ const ServerSettings = () => {
|
||||
eventTarget.removeEventListener('kickUsersUpdate', onKickUsers);
|
||||
eventTarget.removeEventListener('liveNotificationsUpdate', onLiveNotifications);
|
||||
eventTarget.removeEventListener('commandToggle', onCommandToggle);
|
||||
eventTarget.removeEventListener('adminLogAdded', onAdminLogAdded);
|
||||
eventTarget.removeEventListener('adminLogDeleted', onAdminLogDeleted);
|
||||
eventTarget.removeEventListener('adminLogsCleared', onAdminLogsCleared);
|
||||
} catch (err) {}
|
||||
};
|
||||
}, [eventTarget, guildId]);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate(-1);
|
||||
navigate('/dashboard');
|
||||
};
|
||||
|
||||
const handleToggleLive = async (e) => {
|
||||
@@ -229,10 +306,6 @@ const ServerSettings = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
const handleAutoroleSettingUpdate = (newSettings) => {
|
||||
axios.post(`${API_BASE}/api/servers/${guildId}/autorole-settings`, newSettings)
|
||||
.then(response => {
|
||||
@@ -330,6 +403,100 @@ const ServerSettings = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
const handleAdminLogsSettingChange = async (key, value) => {
|
||||
const newSettings = { ...adminLogsSettings, [key]: value };
|
||||
setAdminLogsSettings(newSettings);
|
||||
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/admin-logs-settings`, newSettings);
|
||||
} catch (error) {
|
||||
setSnackbarMessage('Failed to update admin logs settings.');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminLogsCommandChange = async (command, enabled) => {
|
||||
const newSettings = {
|
||||
...adminLogsSettings,
|
||||
commands: { ...adminLogsSettings.commands, [command]: enabled }
|
||||
};
|
||||
setAdminLogsSettings(newSettings);
|
||||
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/admin-logs-settings`, newSettings);
|
||||
} catch (error) {
|
||||
setSnackbarMessage('Failed to update admin logs settings.');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteLog = async (logId) => {
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/api/servers/${guildId}/admin-logs/${logId}`);
|
||||
setAdminLogs(prev => prev.filter(log => log.id !== logId));
|
||||
setSnackbarMessage('Log deleted successfully.');
|
||||
setSnackbarOpen(true);
|
||||
} catch (error) {
|
||||
setSnackbarMessage('Failed to delete log.');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
setDeleteLogDialog({ open: false, logId: null, logAction: '' });
|
||||
};
|
||||
|
||||
const handleDeleteAllLogs = async () => {
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/api/servers/${guildId}/admin-logs`);
|
||||
setAdminLogs([]);
|
||||
setSnackbarMessage('All logs deleted successfully.');
|
||||
setSnackbarOpen(true);
|
||||
} catch (error) {
|
||||
setSnackbarMessage('Failed to delete all logs.');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
setDeleteAllLogsDialog(false);
|
||||
};
|
||||
|
||||
const handleModerationAction = async (action) => {
|
||||
// Validate reason has at least 3 words
|
||||
const reasonWords = moderationReason.trim().split(/\s+/);
|
||||
if (reasonWords.length < 3) {
|
||||
setSnackbarMessage('Reason must be at least 3 words long.');
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
action,
|
||||
target: moderationTarget.trim(),
|
||||
reason: moderationReason.trim(),
|
||||
moderator: {
|
||||
id: user?.id,
|
||||
username: user?.username,
|
||||
global_name: user?.global_name,
|
||||
discriminator: user?.discriminator
|
||||
}
|
||||
};
|
||||
|
||||
if (action === 'timeout') {
|
||||
payload.duration = parseInt(timeoutDuration);
|
||||
}
|
||||
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/moderate`, payload);
|
||||
|
||||
setSnackbarMessage(`${action.charAt(0).toUpperCase() + action.slice(1)} action completed successfully!`);
|
||||
setSnackbarOpen(true);
|
||||
setModerationTarget('');
|
||||
setModerationReason('');
|
||||
if (action === 'timeout') {
|
||||
setTimeoutDuration('');
|
||||
}
|
||||
} catch (error) {
|
||||
setSnackbarMessage(`Failed to ${action} user.`);
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
// Poll Twitch live status for watched users (simple interval). Avoid spamming when list empty or feature disabled.
|
||||
useEffect(() => {
|
||||
let timer = null;
|
||||
@@ -368,7 +535,7 @@ const ServerSettings = () => {
|
||||
const login = (s.user_login || '').toLowerCase();
|
||||
map[login] = { is_live: s.is_live, url: s.url, viewer_count: s.viewer_count };
|
||||
}
|
||||
setKickStatus(map);
|
||||
// kickStatus not used since Kick functionality is disabled
|
||||
} catch (e) {
|
||||
// network errors ignored
|
||||
}
|
||||
@@ -379,13 +546,31 @@ const ServerSettings = () => {
|
||||
}, [kickUsers]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box sx={{ padding: { xs: 2, sm: 3, md: 5 } }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
gap: 1,
|
||||
mb: 2
|
||||
}}>
|
||||
<IconButton onClick={handleBack} sx={{ borderRadius: '50%', boxShadow: '0 8px 16px 0 rgba(0,0,0,0.2)' }}>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography variant="h4" component="h1" sx={{ margin: 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: { xs: 1, sm: 2 }, flexWrap: 'wrap' }}>
|
||||
<Typography
|
||||
variant={{ xs: 'h5', sm: 'h4' }}
|
||||
component="h1"
|
||||
sx={{
|
||||
margin: 0,
|
||||
textAlign: { xs: 'center', sm: 'left' },
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}}
|
||||
>
|
||||
{server ? `Server Settings for ${server.name}` : 'Loading...'}
|
||||
</Typography>
|
||||
{isBotInServer ? (
|
||||
@@ -418,7 +603,8 @@ const ServerSettings = () => {
|
||||
{(() => {
|
||||
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'}));
|
||||
const adminCommands = ['kick', 'ban', 'timeout'];
|
||||
const otherCmds = (commandsList || []).filter(c => !protectedOrder.includes(c.name) && !adminCommands.includes(c.name)).sort((a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}));
|
||||
return (
|
||||
<>
|
||||
{protectedCmds.map(cmd => (
|
||||
@@ -664,7 +850,7 @@ const ServerSettings = () => {
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
{/* Live Notifications Accordion */}
|
||||
<Accordion expanded={liveExpanded} onChange={() => setLiveExpanded(prev => !prev)} sx={{ marginTop: '20px' }}>
|
||||
<Accordion expanded={liveExpanded} onChange={() => setLiveExpanded(prev => !prev)} sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">Live Notifications</Typography>
|
||||
</AccordionSummary>
|
||||
@@ -834,7 +1020,282 @@ const ServerSettings = () => {
|
||||
<Typography variant="h6">Admin Commands</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography>Coming soon...</Typography>
|
||||
{!isBotInServer && <Typography sx={{ mb: 2 }}>Invite the bot to enable admin commands.</Typography>}
|
||||
<Accordion sx={{ marginTop: '10px' }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="subtitle1">Moderation Commands</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>/kick</Typography>
|
||||
<Typography variant="body2">Kick a user from the server</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Requires: Kick Members permission</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FormControlLabel control={<Switch checked={true} disabled />} label="Enabled" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>/ban</Typography>
|
||||
<Typography variant="body2">Ban a user from the server</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Requires: Ban Members permission</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FormControlLabel control={<Switch checked={true} disabled />} label="Enabled" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>/timeout</Typography>
|
||||
<Typography variant="body2">Timeout a user in the server</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Requires: Moderate Members permission</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FormControlLabel control={<Switch checked={true} disabled />} label="Enabled" />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">Admin Logs</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{!isBotInServer && <Typography sx={{ mb: 2 }}>Invite the bot to enable admin logs.</Typography>}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={adminLogsSettings?.enabled || false}
|
||||
onChange={(e) => handleAdminLogsSettingChange('enabled', e.target.checked)}
|
||||
disabled={!isBotInServer}
|
||||
/>
|
||||
}
|
||||
label="Enable Admin Logging"
|
||||
/>
|
||||
|
||||
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||
<InputLabel>Log Channel</InputLabel>
|
||||
<Select
|
||||
value={selectedChannelId}
|
||||
onChange={(e) => {
|
||||
setSelectedChannelId(e.target.value);
|
||||
handleAdminLogsSettingChange('channelId', e.target.value);
|
||||
}}
|
||||
disabled={!isBotInServer || !adminLogsSettings?.enabled}
|
||||
label="Log Channel"
|
||||
>
|
||||
{channels.filter(channel => channel.type === 0).map(channel => (
|
||||
<MenuItem key={channel.id} value={channel.id}>
|
||||
# {channel.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Typography variant="subtitle1" sx={{ mt: 2, mb: 1 }}>Log Commands:</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, pl: 2 }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={adminLogsSettings?.commands?.kick !== false}
|
||||
onChange={(e) => handleAdminLogsCommandChange('kick', e.target.checked)}
|
||||
disabled={!isBotInServer || !adminLogsSettings?.enabled}
|
||||
/>
|
||||
}
|
||||
label="Log Kick Actions"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={adminLogsSettings?.commands?.ban !== false}
|
||||
onChange={(e) => handleAdminLogsCommandChange('ban', e.target.checked)}
|
||||
disabled={!isBotInServer || !adminLogsSettings?.enabled}
|
||||
/>
|
||||
}
|
||||
label="Log Ban Actions"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={adminLogsSettings?.commands?.timeout !== false}
|
||||
onChange={(e) => handleAdminLogsCommandChange('timeout', e.target.checked)}
|
||||
disabled={!isBotInServer || !adminLogsSettings?.enabled}
|
||||
/>
|
||||
}
|
||||
label="Log Timeout Actions"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2, mb: 1 }}>
|
||||
<Typography variant="subtitle1">Recent Logs:</Typography>
|
||||
{adminLogs.length > 0 && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => setDeleteAllLogsDialog(true)}
|
||||
startIcon={<DeleteIcon />}
|
||||
>
|
||||
Delete All Logs
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ maxHeight: 300, overflowY: 'auto', border: '1px solid #ddd', borderRadius: 1, p: 1 }}>
|
||||
{adminLogs.length === 0 ? (
|
||||
<Typography>No logs available.</Typography>
|
||||
) : (
|
||||
adminLogs.map(log => (
|
||||
<Box key={log.id} sx={{ p: 1, border: '1px solid #eee', mb: 1, borderRadius: 1, bgcolor: 'background.paper' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start' }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
|
||||
{log.action.toUpperCase()} - {log.targetUsername || 'Unknown User'} by {log.moderatorUsername || 'Unknown User'}
|
||||
</Typography>
|
||||
<Typography variant="body2">Reason: {log.reason}</Typography>
|
||||
{log.duration && <Typography variant="body2">Duration: {log.duration}</Typography>}
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(log.timestamp).toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={() => setDeleteLogDialog({ open: true, logId: log.id, logAction: log.action })}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">Moderation Actions</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{!isBotInServer && <Typography sx={{ mb: 2 }}>Invite the bot to enable moderation actions.</Typography>}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Perform moderation actions directly from the web interface. You must have the appropriate Discord permissions.
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<Autocomplete
|
||||
options={serverMembers}
|
||||
getOptionLabel={(option) => {
|
||||
if (typeof option === 'string') return option;
|
||||
return option.globalName || option.username || option.displayName || option.id;
|
||||
}}
|
||||
value={serverMembers.find(member => member.id === moderationTarget) || null}
|
||||
onChange={(event, newValue) => {
|
||||
if (newValue) {
|
||||
setModerationTarget(newValue.id);
|
||||
} else {
|
||||
setModerationTarget('');
|
||||
}
|
||||
}}
|
||||
onInputChange={(event, newInputValue) => {
|
||||
// Allow manual input of user IDs
|
||||
if (event && event.type === 'change') {
|
||||
setModerationTarget(newInputValue);
|
||||
}
|
||||
}}
|
||||
inputValue={moderationTarget}
|
||||
freeSolo
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="User"
|
||||
placeholder="Select user or enter user ID"
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
renderOption={(props, option) => (
|
||||
<Box component="li" {...props}>
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>
|
||||
{option.globalName || option.username}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
@{option.username} • ID: {option.id}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
if (!inputValue) return options.slice(0, 50); // Limit to 50 for performance
|
||||
|
||||
const filtered = options.filter(option =>
|
||||
option.username.toLowerCase().includes(inputValue.toLowerCase()) ||
|
||||
(option.globalName && option.globalName.toLowerCase().includes(inputValue.toLowerCase())) ||
|
||||
option.displayName.toLowerCase().includes(inputValue.toLowerCase()) ||
|
||||
option.id.includes(inputValue)
|
||||
);
|
||||
|
||||
return filtered.slice(0, 50); // Limit results
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Reason (minimum 3 words)"
|
||||
value={moderationReason}
|
||||
onChange={(e) => setModerationReason(e.target.value)}
|
||||
placeholder="Enter reason for moderation action"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="warning"
|
||||
onClick={() => handleModerationAction('kick')}
|
||||
disabled={!isBotInServer || !moderationTarget.trim() || !moderationReason.trim()}
|
||||
>
|
||||
Kick User
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => handleModerationAction('ban')}
|
||||
disabled={!isBotInServer || !moderationTarget.trim() || !moderationReason.trim()}
|
||||
>
|
||||
Ban User
|
||||
</Button>
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
<TextField
|
||||
label="Duration (minutes)"
|
||||
type="number"
|
||||
value={timeoutDuration}
|
||||
onChange={(e) => setTimeoutDuration(e.target.value)}
|
||||
sx={{ width: 150 }}
|
||||
inputProps={{ min: 1, max: 40320 }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={() => handleModerationAction('timeout')}
|
||||
disabled={!isBotInServer || !moderationTarget.trim() || !moderationReason.trim() || !timeoutDuration}
|
||||
>
|
||||
Timeout User
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<ConfirmDialog
|
||||
@@ -936,7 +1397,28 @@ const ServerSettings = () => {
|
||||
title="Delete Kick User"
|
||||
message={`Are you sure you want to remove ${pendingKickUser || ''} from the watch list?`}
|
||||
/>
|
||||
</div>
|
||||
{/* Confirm dialog for deleting individual admin log */}
|
||||
<ConfirmDialog
|
||||
open={deleteLogDialog.open}
|
||||
onClose={() => setDeleteLogDialog({ open: false, logId: null, logAction: '' })}
|
||||
onConfirm={() => handleDeleteLog(deleteLogDialog.logId)}
|
||||
title="Delete Admin Log"
|
||||
message={`Are you sure you want to delete this ${deleteLogDialog.logAction} log? This action cannot be undone.`}
|
||||
/>
|
||||
{/* Confirm dialog for deleting all admin logs */}
|
||||
<ConfirmDialog
|
||||
open={deleteAllLogsDialog}
|
||||
onClose={() => setDeleteAllLogsDialog(false)}
|
||||
onConfirm={handleDeleteAllLogs}
|
||||
title="Delete All Admin Logs"
|
||||
message="Are you sure you want to delete all admin logs? This action cannot be undone."
|
||||
/>
|
||||
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={() => setSnackbarOpen(false)}>
|
||||
<Alert onClose={() => setSnackbarOpen(false)} severity="info" sx={{ width: '100%' }}>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -60,6 +60,9 @@ export function BackendProvider({ children }) {
|
||||
es.addEventListener('commandToggle', forward('commandToggle'));
|
||||
es.addEventListener('twitchUsersUpdate', forward('twitchUsersUpdate'));
|
||||
es.addEventListener('liveNotificationsUpdate', forward('liveNotificationsUpdate'));
|
||||
es.addEventListener('adminLogAdded', forward('adminLogAdded'));
|
||||
es.addEventListener('adminLogDeleted', forward('adminLogDeleted'));
|
||||
es.addEventListener('adminLogsCleared', forward('adminLogsCleared'));
|
||||
|
||||
es.onerror = () => {
|
||||
// Let consumers react to backendOnline state changes instead of surfacing connection errors
|
||||
|
||||
@@ -11,3 +11,28 @@ code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* Global responsive styles */
|
||||
#root {
|
||||
max-width: 100vw;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Ensure content doesn't overflow on ultra-wide screens */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Responsive typography adjustments */
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
/* Ultra-wide screen adjustments */
|
||||
.MuiContainer-root {
|
||||
max-width: 1200px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ import { createTheme } from '@mui/material/styles';
|
||||
export const lightTheme = createTheme({
|
||||
palette: {
|
||||
mode: 'light',
|
||||
background: {
|
||||
default: '#e8e8e8', // More greyish background, less bright white
|
||||
paper: '#ffffff',
|
||||
},
|
||||
primary: {
|
||||
main: '#1565c0', // Slightly darker blue for less brightness
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user