swapped to a new db locally hosted
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Grid, Card, CardContent, Typography, Box, IconButton, Snackbar, Alert } from '@mui/material';
|
||||
import { Grid, Card, CardContent, Typography, Box, IconButton, Snackbar, Alert, Menu, MenuItem, Button } from '@mui/material';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import { UserContext } from '../contexts/UserContext';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
@@ -11,7 +12,7 @@ import ConfirmDialog from './ConfirmDialog';
|
||||
const Dashboard = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = useContext(UserContext);
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
const [guilds, setGuilds] = useState([]);
|
||||
const [botStatus, setBotStatus] = useState({});
|
||||
@@ -19,31 +20,57 @@ const Dashboard = () => {
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedGuild, setSelectedGuild] = useState(null);
|
||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
||||
const [menuGuild, setMenuGuild] = useState(null);
|
||||
// Live notifications are managed on the Server Settings page; keep dashboard lightweight
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || '';
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const userParam = params.get('user');
|
||||
if (userParam) {
|
||||
try {
|
||||
const parsedUser = JSON.parse(decodeURIComponent(userParam));
|
||||
setUser(parsedUser);
|
||||
localStorage.setItem('user', JSON.stringify(parsedUser));
|
||||
} catch (err) {
|
||||
console.error("Failed to parse user from URL", err);
|
||||
}
|
||||
}
|
||||
|
||||
const guildsParam = params.get('guilds');
|
||||
if (guildsParam) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(guildsParam));
|
||||
setGuilds(parsed || []);
|
||||
localStorage.setItem('guilds', JSON.stringify(parsed || []));
|
||||
const parsedGuilds = JSON.parse(decodeURIComponent(guildsParam));
|
||||
setGuilds(parsedGuilds || []);
|
||||
localStorage.setItem('guilds', JSON.stringify(parsedGuilds || []));
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
const stored = localStorage.getItem('guilds');
|
||||
if (stored) {
|
||||
const storedGuilds = localStorage.getItem('guilds');
|
||||
if (storedGuilds) {
|
||||
try {
|
||||
setGuilds(JSON.parse(stored));
|
||||
setGuilds(JSON.parse(storedGuilds));
|
||||
} catch (err) {
|
||||
setGuilds([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [location.search]);
|
||||
}, [location.search, setUser]);
|
||||
|
||||
// Protect this route: if no user in context or localStorage, redirect to login
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
const stored = localStorage.getItem('user');
|
||||
if (!stored) {
|
||||
navigate('/');
|
||||
} else {
|
||||
try { setUser(JSON.parse(stored)); } catch (e) { navigate('/'); }
|
||||
}
|
||||
}
|
||||
}, [user, navigate, setUser]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!guilds || guilds.length === 0) return;
|
||||
@@ -62,6 +89,10 @@ const Dashboard = () => {
|
||||
fetchStatuses();
|
||||
}, [guilds, API_BASE]);
|
||||
|
||||
// Dashboard no longer loads live settings; that's on the server settings page
|
||||
|
||||
// Live notifications handlers were removed from Dashboard
|
||||
|
||||
const handleCardClick = (guild) => {
|
||||
navigate(`/server/${guild.id}`, { state: { guild } });
|
||||
};
|
||||
@@ -109,88 +140,63 @@ const Dashboard = () => {
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20 }}>
|
||||
<Typography variant="h4" gutterBottom>Dashboard</Typography>
|
||||
{user && <Typography variant="h6" gutterBottom>Welcome, {user.username}</Typography>}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Dashboard</Typography>
|
||||
{user && <Typography variant="h6" gutterBottom>Welcome, {user.username}</Typography>}
|
||||
</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}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3} key={guild.id} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||
<Card
|
||||
onClick={() => handleCardClick(guild)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: 2,
|
||||
boxShadow: '0 6px 12px rgba(0,0,0,0.10)',
|
||||
transition: 'transform 0.18s ease-in-out, box-shadow 0.18s',
|
||||
height: { xs: 320, sm: 260 },
|
||||
minHeight: { xs: 320, sm: 260 },
|
||||
maxHeight: { xs: 320, sm: 260 },
|
||||
width: { xs: '100%', sm: 260 },
|
||||
minWidth: { xs: '100%', sm: 260 },
|
||||
maxWidth: { xs: '100%', sm: 260 },
|
||||
borderRadius: '16px',
|
||||
boxShadow: '0 8px 16px rgba(0,0,0,0.2)',
|
||||
transition: 'transform 0.2s ease-in-out, box-shadow 0.2s',
|
||||
'&:hover': {
|
||||
transform: 'translateY(-5px)',
|
||||
boxShadow: '0 12px 24px rgba(0,0,0,0.3)',
|
||||
},
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
overflow: 'hidden',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
>
|
||||
{/* slightly larger image area for better visibility */}
|
||||
<Box sx={{ height: { xs: 196, sm: 168 }, width: '100%', bgcolor: '#fff', backgroundSize: 'cover', backgroundPosition: 'center', backgroundRepeat: 'no-repeat', backgroundImage: `url(${guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png` : 'https://cdn.discordapp.com/embed/avatars/0.png'})`, boxSizing: 'border-box' }} />
|
||||
|
||||
<Box sx={{ height: { xs: 72, sm: 56 }, display: 'flex', alignItems: 'center', justifyContent: 'center', px: 2, boxSizing: 'border-box' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 700, textAlign: 'center', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', textOverflow: 'ellipsis', lineHeight: '1.1rem', maxHeight: { xs: '2.2rem', sm: '2.2rem' } }}>{guild.name}</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ height: { xs: 64, sm: 48 }, display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: 1, px: 2, boxSizing: 'border-box' }}>
|
||||
{botStatus[guild.id] ? (
|
||||
<>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mr: 1 }}>Leave:</Typography>
|
||||
<IconButton aria-label={`Make bot leave ${guild.name}`} size="small" onClick={(e) => handleLeaveBot(e, guild)} sx={{ flexShrink: 0 }}>
|
||||
<RemoveCircleOutlineIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, mr: 1 }}>Invite:</Typography>
|
||||
<IconButton aria-label={`Invite bot to ${guild.name}`} size="small" onClick={(e) => handleInviteBot(e, guild)} sx={{ flexShrink: 0 }}>
|
||||
<PersonAddIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* CardContent reduced a bit to compensate for larger image */}
|
||||
<CardContent sx={{ height: { xs: '124px', sm: '92px' }, boxSizing: 'border-box', py: { xs: 1, sm: 1.5 }, px: { xs: 1.25, sm: 2 } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexDirection: { xs: 'column', sm: 'row' }, height: '100%', overflow: 'hidden' }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%', justifyContent: { xs: 'center', sm: 'flex-start' } }}>
|
||||
<Box
|
||||
title={guild.name}
|
||||
sx={{
|
||||
px: { xs: 1, sm: 2 },
|
||||
py: 0.5,
|
||||
borderRadius: '999px',
|
||||
fontWeight: 700,
|
||||
fontSize: { xs: '0.95rem', sm: '1rem' },
|
||||
bgcolor: 'rgba(0,0,0,0.04)',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
whiteSpace: 'normal',
|
||||
textAlign: { xs: 'center', sm: 'left' },
|
||||
lineHeight: '1.2rem',
|
||||
maxHeight: { xs: '2.4rem', sm: '2.4rem', md: '2.4rem' }
|
||||
}}
|
||||
>
|
||||
{guild.name}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Button removed from this location to avoid duplication; action is the labeled button above the CardContent */}
|
||||
<CardContent sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', p: 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,
|
||||
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 }}>
|
||||
{botStatus[guild.id] ? (
|
||||
<Button variant="contained" color="error" size="small" onClick={(e) => handleLeaveBot(e, guild)} startIcon={<RemoveCircleOutlineIcon />}>
|
||||
Leave
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="contained" color="success" size="small" onClick={(e) => handleInviteBot(e, guild)} startIcon={<PersonAddIcon />}>
|
||||
Invite
|
||||
</Button>
|
||||
)}
|
||||
<IconButton size="small" onClick={(e) => { e.stopPropagation(); setMenuAnchor(e.currentTarget); setMenuGuild(guild); }} aria-label="server menu">
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
@@ -200,6 +206,12 @@ 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>
|
||||
|
||||
{/* 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}
|
||||
@@ -217,4 +229,4 @@ const Dashboard = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
export default Dashboard;
|
||||
@@ -10,9 +10,15 @@ const HelpPage = () => {
|
||||
const [commands, setCommands] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || '';
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/commands`)
|
||||
.then(res => setCommands(res.data || []))
|
||||
.then(res => {
|
||||
const cmds = res.data || [];
|
||||
// sort: locked commands first (preserve relative order), then others alphabetically
|
||||
const locked = cmds.filter(c => c.locked);
|
||||
const others = cmds.filter(c => !c.locked).sort((a, b) => a.name.localeCompare(b.name));
|
||||
setCommands([...locked, ...others]);
|
||||
})
|
||||
.catch(() => setCommands([]));
|
||||
}, [guildId]);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const Login = () => {
|
||||
}, [navigate]);
|
||||
|
||||
const handleLogin = () => {
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || '';
|
||||
window.location.href = `${API_BASE}/auth/discord`;
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const NavBar = () => {
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 800 }} onClick={() => { navigate('/dashboard'); closeMenu(); }}>
|
||||
<Typography variant="h6" component="div" sx={{ fontWeight: 800, display: { xs: 'none', sm: 'block' } }} onClick={() => { navigate('/dashboard'); closeMenu(); }}>
|
||||
ECS - EHDCHADSWORTH
|
||||
</Typography>
|
||||
|
||||
@@ -58,4 +58,4 @@ const NavBar = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
export default NavBar;
|
||||
@@ -9,10 +9,15 @@ import ConfirmDialog from './ConfirmDialog';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
// 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.
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || '';
|
||||
|
||||
const ServerSettings = () => {
|
||||
const { guildId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// settings state removed (not used) to avoid lint warnings
|
||||
const [isBotInServer, setIsBotInServer] = useState(false);
|
||||
const [clientId, setClientId] = useState(null);
|
||||
@@ -31,7 +36,15 @@ const ServerSettings = () => {
|
||||
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);
|
||||
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 [watchedUsers, setWatchedUsers] = useState([]);
|
||||
const [liveStatus, setLiveStatus] = useState({});
|
||||
const [commandsExpanded, setCommandsExpanded] = useState(false);
|
||||
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
|
||||
welcome: {
|
||||
@@ -63,8 +76,6 @@ const ServerSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
|
||||
// Fetch settings (not used directly in this component)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/settings`).catch(() => {});
|
||||
|
||||
@@ -116,6 +127,14 @@ const ServerSettings = () => {
|
||||
.catch(() => setCommandsList([]));
|
||||
|
||||
// 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: '' };
|
||||
setLiveEnabled(!!s.enabled);
|
||||
setLiveChannelId(s.channelId || '');
|
||||
setLiveTwitchUser(s.twitchUser || '');
|
||||
}).catch(() => {});
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/twitch-users`).then(resp => setWatchedUsers(resp.data || [])).catch(() => setWatchedUsers([]));
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/invites`)
|
||||
.then(resp => setInvites(resp.data || []))
|
||||
.catch(() => setInvites([]));
|
||||
@@ -127,8 +146,92 @@ const ServerSettings = () => {
|
||||
|
||||
}, [guildId, location.state]);
|
||||
|
||||
// Subscribe to backend Server-Sent Events for real-time updates
|
||||
useEffect(() => {
|
||||
if (!guildId) return;
|
||||
if (typeof window === 'undefined' || typeof EventSource === 'undefined') return;
|
||||
const url = `${API_BASE}/api/events?guildId=${encodeURIComponent(guildId)}`;
|
||||
let es = null;
|
||||
try {
|
||||
es = new EventSource(url);
|
||||
} catch (err) {
|
||||
console.warn('EventSource not available or failed to connect', err);
|
||||
return;
|
||||
}
|
||||
es.addEventListener('connected', (e) => {
|
||||
setSnackbarMessage('Real-time updates connected');
|
||||
setSnackbarOpen(true);
|
||||
});
|
||||
es.addEventListener('commandToggle', (e) => {
|
||||
try {
|
||||
// refresh commands list to keep authoritative state
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/commands`).then(resp => setCommandsList(resp.data || [])).catch(() => {});
|
||||
} catch (err) {}
|
||||
});
|
||||
es.addEventListener('twitchUsersUpdate', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data || '{}');
|
||||
if (data && data.users) setWatchedUsers(data.users || []);
|
||||
// also refresh live status
|
||||
if (data && data.users && data.users.length > 0) {
|
||||
const usersCsv = data.users.join(',');
|
||||
axios.get(`${API_BASE}/api/twitch/streams?users=${encodeURIComponent(usersCsv)}`)
|
||||
.then(resp => {
|
||||
const arr = resp.data || [];
|
||||
const map = {};
|
||||
for (const s of arr) if (s && s.user_login) map[s.user_login.toLowerCase()] = s;
|
||||
setLiveStatus(map);
|
||||
})
|
||||
.catch(() => {});
|
||||
} else {
|
||||
setLiveStatus({});
|
||||
}
|
||||
} catch (err) {}
|
||||
});
|
||||
es.addEventListener('liveNotificationsUpdate', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data || '{}');
|
||||
if (typeof data.enabled !== 'undefined') setLiveEnabled(!!data.enabled);
|
||||
if (data.channelId) setLiveChannelId(data.channelId || '');
|
||||
if (data.twitchUser) setLiveTwitchUser(data.twitchUser || '');
|
||||
} catch (err) {}
|
||||
});
|
||||
es.onerror = (err) => {
|
||||
setSnackbarMessage('Real-time updates disconnected. Retrying...');
|
||||
setSnackbarOpen(true);
|
||||
// attempt reconnects handled by EventSource automatically
|
||||
};
|
||||
return () => { try { es && es.close(); } catch (e) {} };
|
||||
}, [guildId]);
|
||||
|
||||
const handleCloseSnackbar = () => {
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
|
||||
// Fetch live status for watched users
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
if (!watchedUsers || watchedUsers.length === 0) {
|
||||
setLiveStatus({});
|
||||
return;
|
||||
}
|
||||
const usersCsv = watchedUsers.join(',');
|
||||
axios.get(`${API_BASE}/api/twitch/streams?users=${encodeURIComponent(usersCsv)}`)
|
||||
.then(resp => {
|
||||
if (!mounted) return;
|
||||
const arr = resp.data || [];
|
||||
const map = {};
|
||||
for (const s of arr) {
|
||||
if (s && s.user_login) map[s.user_login.toLowerCase()] = s;
|
||||
}
|
||||
setLiveStatus(map);
|
||||
})
|
||||
.catch(() => setLiveStatus({}));
|
||||
return () => { mounted = false; };
|
||||
}, [watchedUsers]);
|
||||
|
||||
const handleAutoroleSettingUpdate = (newSettings) => {
|
||||
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/autorole-settings`, newSettings)
|
||||
axios.post(`${API_BASE}/api/servers/${guildId}/autorole-settings`, newSettings)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
setAutoroleSettings(newSettings);
|
||||
@@ -147,7 +250,7 @@ const ServerSettings = () => {
|
||||
};
|
||||
|
||||
const handleSettingUpdate = (newSettings) => {
|
||||
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/welcome-leave-settings`, newSettings)
|
||||
axios.post(`${API_BASE}/api/servers/${guildId}/welcome-leave-settings`, newSettings)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
setWelcomeLeaveSettings(newSettings);
|
||||
@@ -214,7 +317,7 @@ const ServerSettings = () => {
|
||||
|
||||
const handleConfirmLeave = async () => {
|
||||
try {
|
||||
await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/leave`);
|
||||
await axios.post(`${API_BASE}/api/servers/${guildId}/leave`);
|
||||
setIsBotInServer(false);
|
||||
} catch (error) {
|
||||
console.error('Error leaving server:', error);
|
||||
@@ -237,13 +340,17 @@ const ServerSettings = () => {
|
||||
{server ? `Server Settings for ${server.name}` : 'Loading...'}
|
||||
</Typography>
|
||||
{isBotInServer ? (
|
||||
<Button variant="contained" size="small" color="error" onClick={handleLeaveBot}>
|
||||
Leave Server
|
||||
</Button>
|
||||
<>
|
||||
<Button variant="contained" size="small" color="error" onClick={handleLeaveBot}>
|
||||
Leave Server
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button variant="contained" size="small" onClick={handleInviteBot} disabled={!clientId}>
|
||||
Invite Bot
|
||||
</Button>
|
||||
<>
|
||||
<Button variant="contained" size="small" onClick={handleInviteBot} disabled={!clientId}>
|
||||
Invite Bot
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
{/* UserSettings moved to NavBar */}
|
||||
@@ -285,17 +392,23 @@ const ServerSettings = () => {
|
||||
</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'} />}
|
||||
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(`${API_BASE}/api/servers/${guildId}/commands/${cmd.name}/toggle`, { enabled: newVal });
|
||||
// refresh authoritative state from backend
|
||||
const resp = await axios.get(`${API_BASE}/api/servers/${guildId}/commands`);
|
||||
setCommandsList(resp.data || []);
|
||||
} catch (err) {
|
||||
// revert on error and notify
|
||||
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: cmd.enabled } : c));
|
||||
setSnackbarMessage('Failed to update command toggle');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
}} disabled={!isBotInServer} />}
|
||||
label={cmd.enabled ? 'Enabled' : 'Disabled'}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -306,6 +419,8 @@ const ServerSettings = () => {
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
{/* Live Notifications dialog */}
|
||||
{/* header live dialog removed; Live Notifications is managed in its own accordion below */}
|
||||
{/* Invite creation and list */}
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
@@ -354,7 +469,7 @@ const ServerSettings = () => {
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<Button variant="contained" onClick={async () => {
|
||||
try {
|
||||
const resp = await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/invites`, inviteForm);
|
||||
const resp = await axios.post(`${API_BASE}/api/servers/${guildId}/invites`, inviteForm);
|
||||
if (resp.data && resp.data.success) {
|
||||
setInvites(prev => [...prev, resp.data.invite]);
|
||||
}
|
||||
@@ -498,6 +613,62 @@ const ServerSettings = () => {
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
{/* Live Notifications Accordion */}
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<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={{ 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) { console.error('Failed to add twitch user', err); }
|
||||
}} 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>
|
||||
</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) { console.error('Failed to save live settings', err); }
|
||||
}} disabled={!isBotInServer}>Save</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">Autorole</Typography>
|
||||
@@ -543,7 +714,7 @@ const ServerSettings = () => {
|
||||
<ConfirmDialog
|
||||
open={confirmOpen}
|
||||
onClose={() => { setConfirmOpen(false); setPendingDeleteInvite(null); }}
|
||||
onConfirm={async () => {
|
||||
onConfirm={async () => {
|
||||
// perform deletion for pendingDeleteInvite
|
||||
if (!pendingDeleteInvite) {
|
||||
setConfirmOpen(false);
|
||||
@@ -553,7 +724,6 @@ const ServerSettings = () => {
|
||||
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 {
|
||||
@@ -589,8 +759,31 @@ const ServerSettings = () => {
|
||||
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%' }}>
|
||||
{/* Confirm dialog for deleting a twitch user from watched list */}
|
||||
<ConfirmDialog
|
||||
open={confirmDeleteTwitch}
|
||||
onClose={() => { setConfirmDeleteTwitch(false); setPendingTwitchUser(null); }}
|
||||
onConfirm={async () => {
|
||||
if (!pendingTwitchUser) { setConfirmDeleteTwitch(false); return; }
|
||||
setConfirmDeleteTwitch(false);
|
||||
try {
|
||||
await axios.delete(`${API_BASE}/api/servers/${guildId}/twitch-users/${encodeURIComponent(pendingTwitchUser)}`);
|
||||
setWatchedUsers(prev => prev.filter(x => x !== pendingTwitchUser));
|
||||
setSnackbarMessage('Twitch user removed');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to delete twitch user', err);
|
||||
setSnackbarMessage('Failed to delete twitch user');
|
||||
setSnackbarOpen(true);
|
||||
} finally {
|
||||
setPendingTwitchUser(null);
|
||||
}
|
||||
}}
|
||||
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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Avatar, Menu, MenuItem, Button, Typography } from '@mui/material';
|
||||
import { UserContext } from '../contexts/UserContext';
|
||||
@@ -11,6 +11,17 @@ const UserSettings = () => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
const [themeMenuAnchorEl, setThemeMenuAnchorEl] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
try {
|
||||
setUser(JSON.parse(storedUser));
|
||||
} catch (error) {
|
||||
console.error("Failed to parse user from localStorage", error);
|
||||
}
|
||||
}
|
||||
}, [setUser]);
|
||||
|
||||
const handleMenu = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export const ThemeProvider = ({ children }) => {
|
||||
|
||||
const changeTheme = (name) => {
|
||||
if (user) {
|
||||
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/user/theme`, { userId: user.id, theme: name });
|
||||
axios.post(`${process.env.REACT_APP_API_BASE || ''}/api/user/theme`, { userId: user.id, theme: name });
|
||||
}
|
||||
localStorage.setItem('themeName', name);
|
||||
setThemeName(name);
|
||||
|
||||
Reference in New Issue
Block a user