Update backend, DB, Commands, Live Reloading
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { BrowserRouter as Router, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { UserProvider } from './contexts/UserContext';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } 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, Snackbar, Alert } from '@mui/material';
|
||||
import { Button, Typography, Box, IconButton, Switch, Select, MenuItem, FormControl, FormControlLabel, Radio, RadioGroup, TextField, Accordion, AccordionSummary, AccordionDetails, Tabs, Tab, Snackbar, Alert } from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
// UserSettings moved to NavBar
|
||||
@@ -40,13 +40,22 @@ const ServerSettings = () => {
|
||||
// SSE connection status (not currently displayed)
|
||||
const [confirmDeleteTwitch, setConfirmDeleteTwitch] = useState(false);
|
||||
const [pendingTwitchUser, setPendingTwitchUser] = useState(null);
|
||||
const [confirmDeleteKick, setConfirmDeleteKick] = useState(false);
|
||||
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 [commandsExpanded, setCommandsExpanded] = useState(false);
|
||||
const [liveTabValue, setLiveTabValue] = useState(0);
|
||||
const [kickUsers, setKickUsers] = useState([]);
|
||||
const [kickStatus, setKickStatus] = useState({});
|
||||
const [kickUser, setKickUser] = useState('');
|
||||
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
|
||||
welcome: {
|
||||
enabled: false,
|
||||
@@ -127,14 +136,18 @@ const ServerSettings = () => {
|
||||
// Fetch invites
|
||||
// Fetch live notifications settings and watched users
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/live-notifications`).then(resp => {
|
||||
const s = resp.data || { enabled: false, twitchUser: '', channelId: '' };
|
||||
const s = resp.data || { enabled: false, twitchUser: '', channelId: '', message: '', customMessage: '' };
|
||||
setLiveEnabled(!!s.enabled);
|
||||
setLiveChannelId(s.channelId || '');
|
||||
setLiveTwitchUser(s.twitchUser || '');
|
||||
setLiveMessage(s.message || '');
|
||||
setLiveCustomMessage(s.customMessage || '');
|
||||
}).catch(() => {});
|
||||
|
||||
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([]));
|
||||
|
||||
// Open commands accordion if navigated from Help back button
|
||||
@@ -165,6 +178,16 @@ const ServerSettings = () => {
|
||||
setLiveEnabled(!!data.enabled);
|
||||
setLiveChannelId(data.channelId || '');
|
||||
setLiveTwitchUser(data.twitchUser || '');
|
||||
setLiveMessage(data.message || '');
|
||||
setLiveCustomMessage(data.customMessage || '');
|
||||
};
|
||||
|
||||
const onKickUsers = (e) => {
|
||||
const data = e.detail || {};
|
||||
// payload is { users: [...], guildId }
|
||||
if (!data) return;
|
||||
if (data.guildId && data.guildId !== guildId) return; // ignore other guilds
|
||||
setKickUsers(data.users || []);
|
||||
};
|
||||
|
||||
const onCommandToggle = (e) => {
|
||||
@@ -176,12 +199,14 @@ const ServerSettings = () => {
|
||||
};
|
||||
|
||||
eventTarget.addEventListener('twitchUsersUpdate', onTwitchUsers);
|
||||
eventTarget.addEventListener('kickUsersUpdate', onKickUsers);
|
||||
eventTarget.addEventListener('liveNotificationsUpdate', onLiveNotifications);
|
||||
eventTarget.addEventListener('commandToggle', onCommandToggle);
|
||||
|
||||
return () => {
|
||||
try {
|
||||
eventTarget.removeEventListener('twitchUsersUpdate', onTwitchUsers);
|
||||
eventTarget.removeEventListener('kickUsersUpdate', onKickUsers);
|
||||
eventTarget.removeEventListener('liveNotificationsUpdate', onLiveNotifications);
|
||||
eventTarget.removeEventListener('commandToggle', onCommandToggle);
|
||||
} catch (err) {}
|
||||
@@ -192,77 +217,18 @@ const ServerSettings = () => {
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleCopy = (code) => {
|
||||
try {
|
||||
navigator.clipboard.writeText(code);
|
||||
setSnackbarMessage('Copied to clipboard');
|
||||
setSnackbarOpen(true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteInvite = async (inviteId) => {
|
||||
setDeleting(prev => ({ ...prev, [inviteId]: true }));
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/api/servers/${guildId}/invites/${inviteId}`);
|
||||
setInvites(invites.filter(i => i.id !== inviteId));
|
||||
} catch (e) {
|
||||
// ignore
|
||||
} finally {
|
||||
setDeleting(prev => ({ ...prev, [inviteId]: false }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateInvite = async () => {
|
||||
try {
|
||||
const resp = await axios.post(`${API_BASE}/api/servers/${guildId}/invites`, inviteForm);
|
||||
setInvites([...(invites || []), resp.data]);
|
||||
setInviteForm({ channelId: '', maxAge: 0, maxUses: 0, temporary: false });
|
||||
setSnackbarMessage('Invite created');
|
||||
setSnackbarOpen(true);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleLive = async (e) => {
|
||||
const enabled = e.target.checked;
|
||||
setLiveEnabled(enabled);
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/live-notifications`, { enabled, channelId: liveChannelId, twitchUser: liveTwitchUser });
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/live-notifications`, { enabled, channelId: liveChannelId, twitchUser: liveTwitchUser, message: liveMessage, customMessage: liveCustomMessage });
|
||||
setSnackbarMessage('Live notifications updated');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
// revert on error
|
||||
setLiveEnabled(!enabled);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTwitchUser = async () => {
|
||||
if (!liveTwitchUser) return;
|
||||
try {
|
||||
const resp = await axios.post(`${API_BASE}/api/servers/${guildId}/twitch-users`, { username: liveTwitchUser });
|
||||
setWatchedUsers([...watchedUsers, resp.data]);
|
||||
setLiveTwitchUser('');
|
||||
setSnackbarMessage('Twitch user added');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTwitchUser = async (username) => {
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/api/servers/${guildId}/twitch-users/${encodeURIComponent(username)}`);
|
||||
setWatchedUsers(watchedUsers.filter(u => u !== username));
|
||||
setSnackbarMessage('Twitch user removed');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
@@ -364,6 +330,54 @@ const ServerSettings = () => {
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
// Poll Twitch live status for watched users (simple interval). Avoid spamming when list empty or feature disabled.
|
||||
useEffect(() => {
|
||||
let timer = null;
|
||||
const poll = async () => {
|
||||
if (!watchedUsers || watchedUsers.length === 0) return;
|
||||
try {
|
||||
const csv = watchedUsers.join(',');
|
||||
const resp = await axios.get(`${API_BASE}/api/twitch/streams?users=${encodeURIComponent(csv)}`);
|
||||
const arr = resp.data || [];
|
||||
const map = {};
|
||||
for (const s of arr) {
|
||||
const login = (s.user_login || '').toLowerCase();
|
||||
map[login] = { is_live: s.is_live, url: s.url, viewer_count: s.viewer_count };
|
||||
}
|
||||
setLiveStatus(map);
|
||||
} catch (e) {
|
||||
// network errors ignored
|
||||
}
|
||||
};
|
||||
poll();
|
||||
timer = setInterval(poll, 15000); // 15s interval
|
||||
return () => { if (timer) clearInterval(timer); };
|
||||
}, [watchedUsers]);
|
||||
|
||||
// Poll Kick live status for watched users (simple interval). Avoid spamming when list empty or feature disabled.
|
||||
useEffect(() => {
|
||||
let timer = null;
|
||||
const poll = async () => {
|
||||
if (!kickUsers || kickUsers.length === 0) return;
|
||||
try {
|
||||
const csv = kickUsers.join(',');
|
||||
const resp = await axios.get(`${API_BASE}/api/kick/streams?users=${encodeURIComponent(csv)}`);
|
||||
const arr = resp.data || [];
|
||||
const map = {};
|
||||
for (const s of arr) {
|
||||
const login = (s.user_login || '').toLowerCase();
|
||||
map[login] = { is_live: s.is_live, url: s.url, viewer_count: s.viewer_count };
|
||||
}
|
||||
setKickStatus(map);
|
||||
} catch (e) {
|
||||
// network errors ignored
|
||||
}
|
||||
};
|
||||
poll();
|
||||
timer = setInterval(poll, 15000); // 15s interval
|
||||
return () => { if (timer) clearInterval(timer); };
|
||||
}, [kickUsers]);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
@@ -650,59 +664,143 @@ const ServerSettings = () => {
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
{/* Live Notifications Accordion */}
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<Accordion expanded={liveExpanded} onChange={() => setLiveExpanded(prev => !prev)} sx={{ marginTop: '20px' }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">Live Notifications</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{!isBotInServer && <Typography>Invite the bot to enable this feature.</Typography>}
|
||||
<Box sx={{ marginTop: '10px' }}>
|
||||
<FormControl fullWidth disabled={!isBotInServer}>
|
||||
<Select value={liveChannelId} onChange={(e) => setLiveChannelId(e.target.value)} displayEmpty>
|
||||
<MenuItem value="">(Select channel)</MenuItem>
|
||||
{channels.map(ch => (<MenuItem key={ch.id} value={ch.id}>{ch.name}</MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box sx={{ opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
{!isBotInServer && <Typography sx={{ mb: 2 }}>Invite the bot to enable this feature.</Typography>}
|
||||
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
||||
<Tabs value={liveTabValue} onChange={(e, newValue) => {
|
||||
// Prevent switching to Kick tab (index 1) since it's disabled
|
||||
if (newValue !== 1) {
|
||||
setLiveTabValue(newValue);
|
||||
}
|
||||
}} sx={{ borderBottom: 1, borderColor: 'divider', '& .MuiTabs-indicator': { backgroundColor: 'primary.main' } }}>
|
||||
<Tab label="Twitch" sx={{ textTransform: 'none', fontWeight: 'medium' }} />
|
||||
<Tab label="Kick (Disabled)" sx={{ textTransform: 'none', fontWeight: 'medium', opacity: 0.5, cursor: 'not-allowed' }} disabled />
|
||||
</Tabs>
|
||||
{liveTabValue === 0 && (
|
||||
<Box sx={{ p: 3 }}>
|
||||
<FormControlLabel control={<Switch checked={liveEnabled} onChange={handleToggleLive} />} label="Enabled" sx={{ mb: 2 }} />
|
||||
<FormControl fullWidth sx={{ mb: 2 }} disabled={!isBotInServer}>
|
||||
<Select value={liveChannelId} onChange={(e) => setLiveChannelId(e.target.value)} displayEmpty>
|
||||
<MenuItem value="">(Select channel)</MenuItem>
|
||||
{channels.map(ch => (<MenuItem key={ch.id} value={ch.id}>{ch.name}</MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
|
||||
<TextField label="Twitch username" value={liveTwitchUser} onChange={(e) => setLiveTwitchUser(e.target.value)} fullWidth disabled={!isBotInServer} />
|
||||
<Button variant="contained" onClick={async () => {
|
||||
if (!liveTwitchUser) return;
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/twitch-users`, { username: liveTwitchUser });
|
||||
setWatchedUsers(prev => [...prev.filter(u => u !== liveTwitchUser.toLowerCase()), liveTwitchUser.toLowerCase()]);
|
||||
setLiveTwitchUser('');
|
||||
} catch (err) { setSnackbarMessage('Failed to add Twitch user (backend offline?)'); setSnackbarOpen(true); }
|
||||
}} disabled={!isBotInServer}>Add</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 1, mt: 2 }}>
|
||||
<TextField label="Twitch username" value={liveTwitchUser} onChange={(e) => setLiveTwitchUser(e.target.value)} fullWidth disabled={!isBotInServer} />
|
||||
<Button variant="contained" onClick={async () => {
|
||||
if (!liveTwitchUser) return;
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/twitch-users`, { username: liveTwitchUser });
|
||||
setWatchedUsers(prev => [...prev.filter(u => u !== liveTwitchUser.toLowerCase()), liveTwitchUser.toLowerCase()]);
|
||||
setLiveTwitchUser('');
|
||||
} catch (err) { setSnackbarMessage('Failed to add Twitch user (backend offline?)'); setSnackbarOpen(true); }
|
||||
}} disabled={!isBotInServer}>Add</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="subtitle2">Watched Users</Typography>
|
||||
{watchedUsers.length === 0 && <Typography>No users added</Typography>}
|
||||
{watchedUsers.map(u => (
|
||||
<Box key={u} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography>{u}</Typography>
|
||||
{liveStatus[u] && liveStatus[u].is_live && (
|
||||
<Button size="small" color="error" href={liveStatus[u].url} target="_blank" rel="noopener">Watch Live</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Button size="small" onClick={() => { setPendingTwitchUser(u); setConfirmDeleteTwitch(true); }}>Delete</Button>
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Watched Users</Typography>
|
||||
{watchedUsers.length === 0 && <Typography>No users added</Typography>}
|
||||
{watchedUsers.map(u => (
|
||||
<Box key={u} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 1, p: 1, border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography>{u}</Typography>
|
||||
{liveStatus[u] && liveStatus[u].is_live && (
|
||||
<Button size="small" color="error" href={liveStatus[u].url} target="_blank" rel="noopener">Watch Live</Button>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
<Button size="small" onClick={() => { setPendingTwitchUser(u); setConfirmDeleteTwitch(true); }}>Delete</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3 }}>
|
||||
<Typography variant="subtitle2">Notification Message Mode</Typography>
|
||||
<FormControl component="fieldset" sx={{ mt: 1 }} disabled={!isBotInServer || !liveEnabled}>
|
||||
<RadioGroup
|
||||
row
|
||||
value={liveCustomMessage ? 'custom' : 'default'}
|
||||
onChange={(e) => {
|
||||
const mode = e.target.value;
|
||||
if (mode === 'default') {
|
||||
setLiveCustomMessage('');
|
||||
if (!liveMessage) setLiveMessage('🔴 {user} is now live!');
|
||||
} else {
|
||||
setLiveCustomMessage(liveCustomMessage || liveMessage || '🔴 {user} is now live!');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormControlLabel value="default" control={<Radio />} label="Default" />
|
||||
<FormControlLabel value="custom" control={<Radio />} label="Custom" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
{liveCustomMessage ? (
|
||||
<TextField
|
||||
label="Custom Message"
|
||||
value={liveCustomMessage}
|
||||
onChange={(e) => setLiveCustomMessage(e.target.value)}
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
placeholder="Your custom announcement text"
|
||||
disabled={!isBotInServer || !liveEnabled}
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2" sx={{ mt: 2 }}>
|
||||
Using default message: <strong>{liveMessage || '🔴 {user} is now live!'}</strong>
|
||||
</Typography>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2, gap: 1 }}>
|
||||
{liveCustomMessage && (
|
||||
<Button variant="text" size="small" onClick={() => setLiveCustomMessage('')} disabled={!isBotInServer || !liveEnabled}>Use Default</Button>
|
||||
)}
|
||||
<Button variant="outlined" size="small" onClick={async () => {
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/live-notifications`, {
|
||||
enabled: liveEnabled,
|
||||
channelId: liveChannelId,
|
||||
twitchUser: '',
|
||||
message: liveMessage || '🔴 {user} is now live!',
|
||||
customMessage: liveCustomMessage
|
||||
});
|
||||
setSnackbarMessage('Notification message updated');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
setSnackbarMessage('Failed to update message');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
}} disabled={!isBotInServer || !liveEnabled}>Apply</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 3 }}>
|
||||
<Button variant="contained" onClick={async () => {
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/live-notifications`, { enabled: liveEnabled, twitchUser: '', channelId: liveChannelId, message: liveMessage, customMessage: liveCustomMessage });
|
||||
setSnackbarMessage('Live notification settings saved');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) { setSnackbarMessage('Failed to save live settings (backend offline?)'); setSnackbarOpen(true); }
|
||||
}} disabled={!isBotInServer}>Save</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<FormControlLabel control={<Switch checked={liveEnabled} onChange={(e) => setLiveEnabled(e.target.checked)} />} label="Enabled" sx={{ mt: 2 }} />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
|
||||
<Button variant="contained" onClick={async () => {
|
||||
try {
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/live-notifications`, { enabled: liveEnabled, twitchUser: '', channelId: liveChannelId });
|
||||
} catch (err) { setSnackbarMessage('Failed to save live settings (backend offline?)'); setSnackbarOpen(true); }
|
||||
}} disabled={!isBotInServer}>Save</Button>
|
||||
)}
|
||||
{liveTabValue === 1 && (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="h6" sx={{ mb: 2, color: 'text.secondary' }}>
|
||||
Kick Live Notifications (Disabled)
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||||
Kick live notifications are temporarily disabled. This feature will be re-enabled in a future update.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
@@ -816,11 +914,28 @@ const ServerSettings = () => {
|
||||
title="Delete Twitch User"
|
||||
message={`Are you sure you want to remove ${pendingTwitchUser || ''} from the watch list?`}
|
||||
/>
|
||||
<Snackbar open={snackbarOpen} autoHideDuration={4000} onClose={handleCloseSnackbar}>
|
||||
<Alert onClose={handleCloseSnackbar} severity="info" sx={{ width: '100%' }}>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
{/* Confirm dialog for deleting a kick user from watched list */}
|
||||
<ConfirmDialog
|
||||
open={confirmDeleteKick}
|
||||
onClose={() => { setConfirmDeleteKick(false); setPendingKickUser(null); }}
|
||||
onConfirm={async () => {
|
||||
if (!pendingKickUser) { setConfirmDeleteKick(false); return; }
|
||||
setConfirmDeleteKick(false);
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/api/servers/${guildId}/kick-users/${encodeURIComponent(pendingKickUser)}`);
|
||||
setKickUsers(prev => prev.filter(x => x !== pendingKickUser));
|
||||
setSnackbarMessage('Kick user removed');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
setSnackbarMessage('Failed to delete kick user');
|
||||
setSnackbarOpen(true);
|
||||
} finally {
|
||||
setPendingKickUser(null);
|
||||
}
|
||||
}}
|
||||
title="Delete Kick User"
|
||||
message={`Are you sure you want to remove ${pendingKickUser || ''} from the watch list?`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -66,7 +66,7 @@ export function BackendProvider({ children }) {
|
||||
};
|
||||
|
||||
return () => { try { es && es.close(); } catch (e) {} };
|
||||
}, [process.env.REACT_APP_API_BASE]);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const forceCheck = async () => {
|
||||
const API_BASE2 = process.env.REACT_APP_API_BASE || '';
|
||||
|
||||
Reference in New Issue
Block a user