tweaked ui and updated invite command
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useLayoutEffect, useContext } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Grid, Card, CardContent, Typography, Box, CardMedia, IconButton, Snackbar, Alert } from '@mui/material';
|
||||
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 { UserContext } from '../contexts/UserContext';
|
||||
import PersonAddIcon from '@mui/icons-material/PersonAdd';
|
||||
import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
@@ -9,82 +9,79 @@ import axios from 'axios';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = useContext(UserContext);
|
||||
|
||||
const [guilds, setGuilds] = useState([]);
|
||||
const [botStatus, setBotStatus] = useState({});
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [selectedGuild, setSelectedGuild] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const userParam = urlParams.get('user');
|
||||
const guildsParam = urlParams.get('guilds');
|
||||
|
||||
if (userParam && guildsParam) {
|
||||
const parsedUser = JSON.parse(decodeURIComponent(userParam));
|
||||
const parsedGuilds = JSON.parse(decodeURIComponent(guildsParam));
|
||||
localStorage.setItem('user', JSON.stringify(parsedUser));
|
||||
localStorage.setItem('guilds', JSON.stringify(parsedGuilds));
|
||||
setUser(parsedUser);
|
||||
setGuilds(parsedGuilds);
|
||||
// Clean the URL
|
||||
window.history.replaceState({}, document.title, "/dashboard");
|
||||
const params = new URLSearchParams(location.search);
|
||||
const guildsParam = params.get('guilds');
|
||||
if (guildsParam) {
|
||||
try {
|
||||
const parsed = JSON.parse(decodeURIComponent(guildsParam));
|
||||
setGuilds(parsed || []);
|
||||
localStorage.setItem('guilds', JSON.stringify(parsed || []));
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
} else {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
const storedGuilds = localStorage.getItem('guilds');
|
||||
if (storedUser && storedGuilds) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
setGuilds(JSON.parse(storedGuilds));
|
||||
const stored = localStorage.getItem('guilds');
|
||||
if (stored) {
|
||||
try {
|
||||
setGuilds(JSON.parse(stored));
|
||||
} catch (err) {
|
||||
setGuilds([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [setUser]);
|
||||
}, [location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBotStatus = async () => {
|
||||
const statusPromises = guilds.map(async (guild) => {
|
||||
if (!guilds || guilds.length === 0) return;
|
||||
const fetchStatuses = async () => {
|
||||
const statuses = {};
|
||||
await Promise.all(guilds.map(async (g) => {
|
||||
try {
|
||||
const response = await axios.get(`http://localhost:3002/api/servers/${guild.id}/bot-status`);
|
||||
return { guildId: guild.id, isBotInServer: response.data.isBotInServer };
|
||||
} catch (error) {
|
||||
console.error(`Error fetching bot status for guild ${guild.id}:`, error);
|
||||
return { guildId: guild.id, isBotInServer: false };
|
||||
const resp = await axios.get(`${API_BASE}/api/servers/${g.id}/bot-status`);
|
||||
statuses[g.id] = resp.data.isBotInServer;
|
||||
} catch (err) {
|
||||
statuses[g.id] = false;
|
||||
}
|
||||
});
|
||||
const results = await Promise.all(statusPromises);
|
||||
const newBotStatus = results.reduce((acc, curr) => {
|
||||
acc[curr.guildId] = curr.isBotInServer;
|
||||
return acc;
|
||||
}, {});
|
||||
setBotStatus(newBotStatus);
|
||||
}));
|
||||
setBotStatus(statuses);
|
||||
};
|
||||
|
||||
if (guilds.length > 0) {
|
||||
fetchBotStatus();
|
||||
}
|
||||
}, [guilds]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const scrollPosition = sessionStorage.getItem('scrollPosition');
|
||||
if (scrollPosition) {
|
||||
window.scrollTo(0, parseInt(scrollPosition));
|
||||
sessionStorage.removeItem('scrollPosition');
|
||||
}
|
||||
}, []);
|
||||
fetchStatuses();
|
||||
}, [guilds, API_BASE]);
|
||||
|
||||
const handleCardClick = (guild) => {
|
||||
sessionStorage.setItem('scrollPosition', window.scrollY);
|
||||
navigate(`/server/${guild.id}`, { state: { guild } });
|
||||
};
|
||||
|
||||
const handleInviteBot = async (e, guild) => {
|
||||
const handleInviteBot = (e, guild) => {
|
||||
e.stopPropagation();
|
||||
const clientId = '1423377662055026840'; // Hardcoded client ID from user request
|
||||
const permissions = 8; // Administrator
|
||||
const inviteUrl = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=${permissions}&scope=bot%20applications.commands&guild_id=${guild.id}&disable_guild_select=true`;
|
||||
window.open(inviteUrl, '_blank', 'noopener,noreferrer');
|
||||
axios.get(`${API_BASE}/api/client-id`).then(resp => {
|
||||
const clientId = resp.data.clientId;
|
||||
if (!clientId) {
|
||||
setSnackbarMessage('No client ID available');
|
||||
setSnackbarOpen(true);
|
||||
return;
|
||||
}
|
||||
const permissions = 8; // admin
|
||||
const url = `https://discord.com/api/oauth2/authorize?client_id=${clientId}&permissions=${permissions}&scope=bot%20applications.commands&guild_id=${guild.id}&disable_guild_select=true`;
|
||||
window.open(url, '_blank');
|
||||
}).catch(() => {
|
||||
setSnackbarMessage('Failed to fetch client id');
|
||||
setSnackbarOpen(true);
|
||||
});
|
||||
};
|
||||
|
||||
const handleLeaveBot = (e, guild) => {
|
||||
@@ -96,114 +93,119 @@ const Dashboard = () => {
|
||||
const handleConfirmLeave = async () => {
|
||||
if (!selectedGuild) return;
|
||||
try {
|
||||
await axios.post(`http://localhost:3002/api/servers/${selectedGuild.id}/leave`);
|
||||
setBotStatus(prevStatus => ({ ...prevStatus, [selectedGuild.id]: false }));
|
||||
setSnackbarMessage('Bot has left the server.');
|
||||
await axios.post(`${API_BASE}/api/servers/${selectedGuild.id}/leave`);
|
||||
setBotStatus(prev => ({ ...prev, [selectedGuild.id]: false }));
|
||||
setSnackbarMessage('Bot left the server');
|
||||
setSnackbarOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Error leaving server:', error);
|
||||
setSnackbarMessage('Failed to make the bot leave the server.');
|
||||
} catch (err) {
|
||||
setSnackbarMessage('Failed to leave server');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
setDialogOpen(false);
|
||||
setSelectedGuild(null);
|
||||
};
|
||||
|
||||
const handleSnackbarClose = (event, reason) => {
|
||||
if (reason === 'clickaway') {
|
||||
return;
|
||||
}
|
||||
setSnackbarOpen(false);
|
||||
};
|
||||
const handleSnackbarClose = () => setSnackbarOpen(false);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
{/* UserSettings moved to NavBar */}
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Dashboard
|
||||
</Typography>
|
||||
{user && (
|
||||
<Typography variant="h5" gutterBottom>
|
||||
Welcome, {user.username}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Your Admin Servers:
|
||||
</Typography>
|
||||
<Grid container spacing={3}>
|
||||
<div style={{ padding: 20 }}>
|
||||
<Typography variant="h4" gutterBottom>Dashboard</Typography>
|
||||
{user && <Typography variant="h6" gutterBottom>Welcome, {user.username}</Typography>}
|
||||
<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}>
|
||||
<Card
|
||||
onClick={() => handleCardClick(guild)}
|
||||
sx={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: '20px',
|
||||
boxShadow: '0 8px 16px 0 rgba(0,0,0,0.2)',
|
||||
transition: 'transform 0.3s',
|
||||
height: '250px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.05)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CardMedia
|
||||
component="img"
|
||||
sx={{ height: '60%', objectFit: 'cover' }}
|
||||
image={guild.icon ? `https://cdn.discordapp.com/icons/${guild.id}/${guild.icon}.png` : 'https://cdn.discordapp.com/embed/avatars/0.png'}
|
||||
alt={guild.name}
|
||||
/>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1, flexDirection: { xs: 'column', sm: 'row' } }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: '100%', justifyContent: { xs: 'center', sm: 'flex-start' } }}>
|
||||
<Box
|
||||
title={guild.name}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
borderRadius: '999px',
|
||||
fontWeight: 'bold',
|
||||
bgcolor: 'rgba(0,0,0,0.06)',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
textAlign: { xs: 'center', sm: 'left' }
|
||||
}}
|
||||
>
|
||||
{guild.name}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<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 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
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] ? (
|
||||
<IconButton
|
||||
aria-label={`Make bot leave ${guild.name}`}
|
||||
size="small"
|
||||
onClick={(e) => handleLeaveBot(e, guild)}
|
||||
>
|
||||
<RemoveCircleOutlineIcon />
|
||||
</IconButton>
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
aria-label={`Invite bot to ${guild.name}`}
|
||||
size="small"
|
||||
onClick={(e) => handleInviteBot(e, guild)}
|
||||
>
|
||||
<PersonAddIcon />
|
||||
</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>
|
||||
</Card>
|
||||
|
||||
{/* 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 */}
|
||||
</Box>
|
||||
</CardContent>
|
||||
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Snackbar open={snackbarOpen} autoHideDuration={6000} onClose={handleSnackbarClose}>
|
||||
<Alert onClose={handleSnackbarClose} severity="info" sx={{ width: '100%' }}>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
|
||||
<ConfirmDialog
|
||||
open={dialogOpen}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
@@ -7,11 +7,11 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
const HelpPage = () => {
|
||||
const { guildId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [commands, setCommands] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/commands`)
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/commands`)
|
||||
.then(res => setCommands(res.data || []))
|
||||
.catch(() => setCommands([]));
|
||||
}, [guildId]);
|
||||
|
||||
@@ -13,7 +13,8 @@ const Login = () => {
|
||||
}, [navigate]);
|
||||
|
||||
const handleLogin = () => {
|
||||
window.location.href = 'http://localhost:3002/auth/discord';
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
window.location.href = `${API_BASE}/auth/discord`;
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { Button, Typography, Box, IconButton, Switch, Select, MenuItem, FormControl, FormControlLabel, Radio, RadioGroup, TextField, Accordion, AccordionSummary, AccordionDetails } from '@mui/material';
|
||||
import { Button, Typography, Box, IconButton, Switch, Select, MenuItem, FormControl, FormControlLabel, Radio, RadioGroup, TextField, Accordion, AccordionSummary, AccordionDetails, Snackbar, Alert } from '@mui/material';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
// UserSettings moved to NavBar
|
||||
@@ -26,6 +26,11 @@ const ServerSettings = () => {
|
||||
});
|
||||
const [commandsList, setCommandsList] = useState([]);
|
||||
const [invites, setInvites] = useState([]);
|
||||
const [deleting, setDeleting] = useState({});
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [pendingDeleteInvite, setPendingDeleteInvite] = useState(null);
|
||||
const [snackbarOpen, setSnackbarOpen] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
const [inviteForm, setInviteForm] = useState({ channelId: '', maxAge: 0, maxUses: 0, temporary: false });
|
||||
const [commandsExpanded, setCommandsExpanded] = useState(false);
|
||||
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
|
||||
@@ -58,29 +63,31 @@ const ServerSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
|
||||
// Fetch settings (not used directly in this component)
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/settings`).catch(() => {});
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/settings`).catch(() => {});
|
||||
|
||||
// Check if bot is in server
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/bot-status`)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/bot-status`)
|
||||
.then(response => {
|
||||
setIsBotInServer(response.data.isBotInServer);
|
||||
});
|
||||
|
||||
// Fetch client ID
|
||||
axios.get('http://localhost:3002/api/client-id')
|
||||
axios.get(`${API_BASE}/api/client-id`)
|
||||
.then(response => {
|
||||
setClientId(response.data.clientId);
|
||||
});
|
||||
|
||||
// Fetch channels
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/channels`)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/channels`)
|
||||
.then(response => {
|
||||
setChannels(response.data);
|
||||
});
|
||||
|
||||
// Fetch welcome/leave settings
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/welcome-leave-settings`)
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
setWelcomeLeaveSettings(response.data);
|
||||
@@ -88,13 +95,13 @@ const ServerSettings = () => {
|
||||
});
|
||||
|
||||
// Fetch roles
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/roles`)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/roles`)
|
||||
.then(response => {
|
||||
setRoles(response.data);
|
||||
});
|
||||
|
||||
// Fetch autorole settings
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/autorole-settings`)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/autorole-settings`)
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
setAutoroleSettings(response.data);
|
||||
@@ -102,14 +109,14 @@ const ServerSettings = () => {
|
||||
});
|
||||
|
||||
// Fetch commands/help list
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/commands`)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/commands`)
|
||||
.then(response => {
|
||||
setCommandsList(response.data || []);
|
||||
})
|
||||
.catch(() => setCommandsList([]));
|
||||
|
||||
// Fetch invites
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/invites`)
|
||||
axios.get(`${API_BASE}/api/servers/${guildId}/invites`)
|
||||
.then(resp => setInvites(resp.data || []))
|
||||
.catch(() => setInvites([]));
|
||||
|
||||
@@ -121,7 +128,7 @@ const ServerSettings = () => {
|
||||
}, [guildId, location.state]);
|
||||
|
||||
const handleAutoroleSettingUpdate = (newSettings) => {
|
||||
axios.post(`http://localhost:3002/api/servers/${guildId}/autorole-settings`, newSettings)
|
||||
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/autorole-settings`, newSettings)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
setAutoroleSettings(newSettings);
|
||||
@@ -140,7 +147,7 @@ const ServerSettings = () => {
|
||||
};
|
||||
|
||||
const handleSettingUpdate = (newSettings) => {
|
||||
axios.post(`http://localhost:3002/api/servers/${guildId}/welcome-leave-settings`, newSettings)
|
||||
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/welcome-leave-settings`, newSettings)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
setWelcomeLeaveSettings(newSettings);
|
||||
@@ -207,7 +214,7 @@ const ServerSettings = () => {
|
||||
|
||||
const handleConfirmLeave = async () => {
|
||||
try {
|
||||
await axios.post(`http://localhost:3002/api/servers/${guildId}/leave`);
|
||||
await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/leave`);
|
||||
setIsBotInServer(false);
|
||||
} catch (error) {
|
||||
console.error('Error leaving server:', error);
|
||||
@@ -251,36 +258,51 @@ const ServerSettings = () => {
|
||||
<AccordionDetails>
|
||||
{!isBotInServer && <Typography>Invite the bot to enable commands.</Typography>}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: '10px' }}>
|
||||
{commandsList && [...commandsList].sort((a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'})).map(cmd => (
|
||||
<Box key={cmd.name} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>{cmd.name}</Typography>
|
||||
<Typography variant="body2">{cmd.description}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{['help', 'manage-commands'].includes(cmd.name) ? (
|
||||
<FormControlLabel
|
||||
control={<Switch checked={true} disabled />}
|
||||
label="Locked"
|
||||
/>
|
||||
) : (
|
||||
<FormControlLabel
|
||||
control={<Switch checked={cmd.enabled} onChange={async (e) => {
|
||||
const newVal = e.target.checked;
|
||||
// optimistic update
|
||||
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: newVal } : c));
|
||||
try {
|
||||
await axios.post(`http://localhost:3002/api/servers/${guildId}/commands/${cmd.name}/toggle`, { enabled: newVal });
|
||||
} catch (err) {
|
||||
// revert on error
|
||||
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: cmd.enabled } : c));
|
||||
}
|
||||
}} disabled={!isBotInServer} label={cmd.enabled ? 'Enabled' : 'Disabled'} />}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
{/** Render protected commands first in a fixed order **/}
|
||||
{(() => {
|
||||
const protectedOrder = ['help', 'manage-commands'];
|
||||
const protectedCmds = protectedOrder.map(name => commandsList.find(c => c.name === name)).filter(Boolean);
|
||||
const otherCmds = (commandsList || []).filter(c => !protectedOrder.includes(c.name)).sort((a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}));
|
||||
return (
|
||||
<>
|
||||
{protectedCmds.map(cmd => (
|
||||
<Box key={cmd.name} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>{cmd.name}</Typography>
|
||||
<Typography variant="body2">{cmd.description}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FormControlLabel control={<Switch checked={true} disabled />} label="Locked" />
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{otherCmds.map(cmd => (
|
||||
<Box key={cmd.name} sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: 1, border: '1px solid #eee', borderRadius: 1 }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>{cmd.name}</Typography>
|
||||
<Typography variant="body2">{cmd.description}</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<FormControlLabel
|
||||
control={<Switch checked={cmd.enabled} onChange={async (e) => {
|
||||
const newVal = e.target.checked;
|
||||
// optimistic update
|
||||
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: newVal } : c));
|
||||
try {
|
||||
await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/commands/${cmd.name}/toggle`, { enabled: newVal });
|
||||
} catch (err) {
|
||||
// revert on error
|
||||
setCommandsList(prev => prev.map(c => c.name === cmd.name ? { ...c, enabled: cmd.enabled } : c));
|
||||
}
|
||||
}} disabled={!isBotInServer} label={cmd.enabled ? 'Enabled' : 'Disabled'} />}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
@@ -332,7 +354,7 @@ const ServerSettings = () => {
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end' }}>
|
||||
<Button variant="contained" onClick={async () => {
|
||||
try {
|
||||
const resp = await axios.post(`http://localhost:3002/api/servers/${guildId}/invites`, inviteForm);
|
||||
const resp = await axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/servers/${guildId}/invites`, inviteForm);
|
||||
if (resp.data && resp.data.success) {
|
||||
setInvites(prev => [...prev, resp.data.invite]);
|
||||
}
|
||||
@@ -352,12 +374,32 @@ const ServerSettings = () => {
|
||||
<Typography variant="caption">Created: {new Date(inv.createdAt).toLocaleString()} • Uses: {inv.uses || 0} • MaxUses: {inv.maxUses || 0} • MaxAge(s): {inv.maxAge || 0} • Temporary: {inv.temporary ? 'Yes' : 'No'}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Button startIcon={<ContentCopyIcon />} onClick={() => { navigator.clipboard.writeText(inv.url); }}>Copy</Button>
|
||||
<Button startIcon={<DeleteIcon />} color="error" onClick={async () => {
|
||||
<Button startIcon={<ContentCopyIcon />} onClick={async () => {
|
||||
// robust clipboard copy with fallback
|
||||
try {
|
||||
await axios.delete(`http://localhost:3002/api/servers/${guildId}/invites/${inv.code}`);
|
||||
setInvites(prev => prev.filter(i => i.code !== inv.code));
|
||||
} catch (err) { console.error('Error deleting invite:', err); }
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
await navigator.clipboard.writeText(inv.url);
|
||||
} else {
|
||||
// fallback for older browsers
|
||||
const input = document.createElement('input');
|
||||
input.value = inv.url;
|
||||
document.body.appendChild(input);
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(input);
|
||||
}
|
||||
setSnackbarMessage('Copied invite URL to clipboard');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
console.error('Clipboard copy failed:', err);
|
||||
setSnackbarMessage('Failed to copy — please copy manually');
|
||||
setSnackbarOpen(true);
|
||||
}
|
||||
}}>Copy</Button>
|
||||
<Button startIcon={<DeleteIcon />} color="error" disabled={!!deleting[inv.code]} onClick={() => {
|
||||
// open confirm dialog for this invite
|
||||
setPendingDeleteInvite(inv);
|
||||
setConfirmOpen(true);
|
||||
}}>Delete</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -497,6 +539,61 @@ const ServerSettings = () => {
|
||||
title="Confirm Leave"
|
||||
message={`Are you sure you want the bot to leave ${server?.name}?`}
|
||||
/>
|
||||
{/* Confirm dialog for invite deletion */}
|
||||
<ConfirmDialog
|
||||
open={confirmOpen}
|
||||
onClose={() => { setConfirmOpen(false); setPendingDeleteInvite(null); }}
|
||||
onConfirm={async () => {
|
||||
// perform deletion for pendingDeleteInvite
|
||||
if (!pendingDeleteInvite) {
|
||||
setConfirmOpen(false);
|
||||
return;
|
||||
}
|
||||
const code = pendingDeleteInvite.code;
|
||||
setConfirmOpen(false);
|
||||
setDeleting(prev => ({ ...prev, [code]: true }));
|
||||
try {
|
||||
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3002';
|
||||
// fetch token (one retry)
|
||||
let token = null;
|
||||
try {
|
||||
const tokenResp = await axios.get(`${API_BASE}/api/servers/${guildId}/invite-token`);
|
||||
token = tokenResp && tokenResp.data && tokenResp.data.token;
|
||||
} catch (tErr) {
|
||||
try {
|
||||
const tokenResp2 = await axios.get(`${API_BASE}/api/servers/${guildId}/invite-token`);
|
||||
token = tokenResp2 && tokenResp2.data && tokenResp2.data.token;
|
||||
} catch (tErr2) {
|
||||
throw new Error('Failed to obtain delete token from server');
|
||||
}
|
||||
}
|
||||
if (!token) throw new Error('No delete token received from server');
|
||||
await axios.delete(`${API_BASE}/api/servers/${guildId}/invites/${code}`, { headers: { 'x-invite-token': token } });
|
||||
setInvites(prev => prev.filter(i => i.code !== code));
|
||||
setSnackbarMessage('Invite deleted');
|
||||
setSnackbarOpen(true);
|
||||
} catch (err) {
|
||||
console.error('Error deleting invite:', err);
|
||||
const msg = (err && err.message) || (err && err.response && err.response.data && err.response.data.message) || 'Failed to delete invite';
|
||||
setSnackbarMessage(msg);
|
||||
setSnackbarOpen(true);
|
||||
} finally {
|
||||
setDeleting(prev => {
|
||||
const copy = { ...prev };
|
||||
delete copy[pendingDeleteInvite?.code];
|
||||
return copy;
|
||||
});
|
||||
setPendingDeleteInvite(null);
|
||||
}
|
||||
}}
|
||||
title="Delete Invite"
|
||||
message={`Are you sure you want to delete invite ${pendingDeleteInvite ? pendingDeleteInvite.url : ''}?`}
|
||||
/>
|
||||
<Snackbar open={snackbarOpen} autoHideDuration={4000} onClose={() => setSnackbarOpen(false)}>
|
||||
<Alert onClose={() => setSnackbarOpen(false)} severity="info" sx={{ width: '100%' }}>
|
||||
{snackbarMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ export const ThemeProvider = ({ children }) => {
|
||||
|
||||
const changeTheme = (name) => {
|
||||
if (user) {
|
||||
axios.post('http://localhost:3002/api/user/theme', { userId: user.id, theme: name });
|
||||
axios.post(`${process.env.REACT_APP_API_BASE || 'http://localhost:3002'}/api/user/theme`, { userId: user.id, theme: name });
|
||||
}
|
||||
localStorage.setItem('themeName', name);
|
||||
setThemeName(name);
|
||||
|
||||
Reference in New Issue
Block a user