fixed themes and ui added new features
This commit is contained in:
117
backend/index.js
117
backend/index.js
@@ -283,6 +283,123 @@ app.get('/api/servers/:guildId/commands', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// INVITES: create, list, delete
|
||||
app.get('/api/servers/:guildId/invites', async (req, res) => {
|
||||
try {
|
||||
const { guildId } = req.params;
|
||||
const db = readDb();
|
||||
const saved = (db[guildId] && db[guildId].invites) ? db[guildId].invites : [];
|
||||
|
||||
// try to enrich with live data where possible
|
||||
const guild = bot.client.guilds.cache.get(guildId);
|
||||
let liveInvites = [];
|
||||
if (guild) {
|
||||
try {
|
||||
const fetched = await guild.invites.fetch();
|
||||
liveInvites = Array.from(fetched.values());
|
||||
} catch (e) {
|
||||
// ignore fetch errors
|
||||
}
|
||||
}
|
||||
|
||||
const combined = saved.map(inv => {
|
||||
const live = liveInvites.find(li => li.code === inv.code);
|
||||
return {
|
||||
...inv,
|
||||
uses: live ? live.uses : inv.uses || 0,
|
||||
maxUses: inv.maxUses || (live ? live.maxUses : 0),
|
||||
maxAge: inv.maxAge || (live ? live.maxAge : 0),
|
||||
};
|
||||
});
|
||||
|
||||
res.json(combined);
|
||||
} catch (error) {
|
||||
console.error('Error listing invites:', error);
|
||||
res.status(500).json({ success: false, message: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/servers/:guildId/invites', async (req, res) => {
|
||||
try {
|
||||
const { guildId } = req.params;
|
||||
const { channelId, maxAge, maxUses, temporary } = req.body || {};
|
||||
const guild = bot.client.guilds.cache.get(guildId);
|
||||
if (!guild) return res.status(404).json({ success: false, message: 'Guild not found' });
|
||||
|
||||
let channel = null;
|
||||
if (channelId) {
|
||||
try { channel = await guild.channels.fetch(channelId); } catch (e) { channel = null; }
|
||||
}
|
||||
if (!channel) {
|
||||
// fall back to first text channel
|
||||
const channels = await guild.channels.fetch();
|
||||
channel = channels.find(c => c.type === 0) || channels.first();
|
||||
}
|
||||
if (!channel) return res.status(400).json({ success: false, message: 'No channel available to create invite' });
|
||||
|
||||
const inviteOptions = {
|
||||
maxAge: typeof maxAge === 'number' ? maxAge : 0,
|
||||
maxUses: typeof maxUses === 'number' ? maxUses : 0,
|
||||
temporary: !!temporary,
|
||||
unique: true,
|
||||
};
|
||||
|
||||
const invite = await channel.createInvite(inviteOptions);
|
||||
|
||||
const db = readDb();
|
||||
if (!db[guildId]) db[guildId] = {};
|
||||
if (!db[guildId].invites) db[guildId].invites = [];
|
||||
|
||||
const item = {
|
||||
code: invite.code,
|
||||
url: invite.url,
|
||||
channelId: channel.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
maxUses: invite.maxUses || inviteOptions.maxUses || 0,
|
||||
maxAge: invite.maxAge || inviteOptions.maxAge || 0,
|
||||
temporary: !!invite.temporary,
|
||||
};
|
||||
|
||||
db[guildId].invites.push(item);
|
||||
writeDb(db);
|
||||
|
||||
res.json({ success: true, invite: item });
|
||||
} catch (error) {
|
||||
console.error('Error creating invite:', error);
|
||||
res.status(500).json({ success: false, message: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/servers/:guildId/invites/:code', async (req, res) => {
|
||||
try {
|
||||
const { guildId, code } = req.params;
|
||||
const db = readDb();
|
||||
const guild = bot.client.guilds.cache.get(guildId);
|
||||
|
||||
// Try to delete on Discord if possible
|
||||
if (guild) {
|
||||
try {
|
||||
// fetch invites and delete matching code
|
||||
const fetched = await guild.invites.fetch();
|
||||
const inv = fetched.find(i => i.code === code);
|
||||
if (inv) await inv.delete();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (db[guildId] && db[guildId].invites) {
|
||||
db[guildId].invites = db[guildId].invites.filter(i => i.code !== code);
|
||||
writeDb(db);
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error deleting invite:', error);
|
||||
res.status(500).json({ success: false, message: 'Internal Server Error' });
|
||||
}
|
||||
});
|
||||
|
||||
const bot = require('../discord-bot');
|
||||
|
||||
bot.login();
|
||||
|
||||
15
checklist.md
15
checklist.md
@@ -40,6 +40,13 @@
|
||||
- [x] Improve NavBar layout and styling for clarity and compactness
|
||||
- [x] Implement single-hamburger NavBar (hamburger toggles to X; buttons hidden when collapsed)
|
||||
- [x] Commands List button added above Commands accordion in Server Settings
|
||||
- [ ] Add server invite management
|
||||
- [ ] Add UI to create invites: optional channel dropdown, maxAge dropdown, maxUses dropdown, temporary toggle, create button
|
||||
- [ ] Allow invite creation without selecting a channel (use default)
|
||||
- [ ] Persist created invites to backend encrypted DB
|
||||
- [ ] Add front-end list showing created invites with Copy and Delete actions and metadata (url, createdAt, uses, maxUses, maxAge, temporary)
|
||||
- [ ] Add `/create-invite` and `/list-invites` slash commands in the bot; ensure actions sync with backend
|
||||
- [ ] Add enable/disable toggles for these commands in Commands list
|
||||
- [x] Place 'Invite' button beside the server title on dashboard/server cards
|
||||
- Acceptance criteria: the invite button appears horizontally adjacent to the server title (to the right), remains visible and usable on tablet and desktop layouts, is keyboard-focusable, and has an accessible aria-label (e.g. "Invite bot to SERVER_NAME").
|
||||
- [x] Show the server name in a rounded "bubble" and render it bold
|
||||
@@ -60,6 +67,14 @@
|
||||
- [x] Redesign the login page to be more bubbly, centered, and eye-catching, with bigger text and responsive design.
|
||||
- [x] Make server settings panels collapsible for a cleaner mobile UI.
|
||||
|
||||
## Recent frontend tweaks
|
||||
|
||||
- [x] Commands list sorted alphabetically in Server Settings for easier scanning
|
||||
- [x] Invite creation form: labels added above dropdowns (Channel, Expiry, Max Uses, Temporary) and layout improved for mobile (stacked inputs)
|
||||
- [x] Theme persistence: theme changes now persist immediately (localStorage) and are not overwritten on page navigation; server-side preference is respected when different from local selection
|
||||
- [x] Theme preference behavior: UI now prefers an explicit user selection (localStorage) over defaults; default is used only on first visit when no prior selection exists
|
||||
|
||||
|
||||
## Discord Bot
|
||||
- [x] Create a basic Discord bot
|
||||
- [x] Add a feature with both slash and web commands
|
||||
|
||||
54
discord-bot/commands/create-invite.js
Normal file
54
discord-bot/commands/create-invite.js
Normal file
@@ -0,0 +1,54 @@
|
||||
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
|
||||
const { readDb, writeDb } = require('../../backend/db');
|
||||
|
||||
module.exports = {
|
||||
name: 'create-invite',
|
||||
description: 'Create a Discord invite with options (channel optional, maxAge seconds, maxUses, temporary).',
|
||||
enabled: true,
|
||||
builder: new SlashCommandBuilder()
|
||||
.setName('create-invite')
|
||||
.setDescription('Create a Discord invite with options (channel optional, maxAge seconds, maxUses, temporary).')
|
||||
.addChannelOption(opt => opt.setName('channel').setDescription('Channel to create invite in').setRequired(false))
|
||||
.addIntegerOption(opt => opt.setName('maxage').setDescription('Duration in seconds (0 means never expire)').setRequired(false))
|
||||
.addIntegerOption(opt => opt.setName('maxuses').setDescription('Number of uses allowed (0 means unlimited)').setRequired(false))
|
||||
.addBooleanOption(opt => opt.setName('temporary').setDescription('Temporary membership?').setRequired(false))
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||
async execute(interaction) {
|
||||
try {
|
||||
const channel = interaction.options.getChannel('channel');
|
||||
const maxAge = interaction.options.getInteger('maxage') || 0;
|
||||
const maxUses = interaction.options.getInteger('maxuses') || 0;
|
||||
const temporary = interaction.options.getBoolean('temporary') || false;
|
||||
|
||||
const targetChannel = channel || interaction.guild.channels.cache.find(c => c.type === 0);
|
||||
if (!targetChannel) {
|
||||
await interaction.reply({ content: 'No valid channel found to create an invite.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const invite = await targetChannel.createInvite({ maxAge, maxUses, temporary, unique: true });
|
||||
|
||||
const db = readDb();
|
||||
if (!db[interaction.guildId]) db[interaction.guildId] = {};
|
||||
if (!db[interaction.guildId].invites) db[interaction.guildId].invites = [];
|
||||
|
||||
const item = {
|
||||
code: invite.code,
|
||||
url: invite.url,
|
||||
channelId: targetChannel.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
maxUses: invite.maxUses || maxUses || 0,
|
||||
maxAge: invite.maxAge || maxAge || 0,
|
||||
temporary: !!invite.temporary,
|
||||
};
|
||||
|
||||
db[interaction.guildId].invites.push(item);
|
||||
writeDb(db);
|
||||
|
||||
await interaction.reply({ content: `Invite created: ${invite.url}`, ephemeral: true });
|
||||
} catch (error) {
|
||||
console.error('Error in create-invite:', error);
|
||||
await interaction.reply({ content: 'Failed to create invite.', ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
42
discord-bot/commands/list-invites.js
Normal file
42
discord-bot/commands/list-invites.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const { SlashCommandBuilder, PermissionFlagsBits, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
||||
const { readDb } = require('../../backend/db');
|
||||
|
||||
module.exports = {
|
||||
name: 'list-invites',
|
||||
description: 'List invites created by the bot for this guild',
|
||||
enabled: true,
|
||||
builder: new SlashCommandBuilder()
|
||||
.setName('list-invites')
|
||||
.setDescription('List invites created by the bot for this guild')
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||
async execute(interaction) {
|
||||
try {
|
||||
const db = readDb();
|
||||
const invites = (db[interaction.guildId] && db[interaction.guildId].invites) ? db[interaction.guildId].invites : [];
|
||||
|
||||
if (!invites.length) {
|
||||
await interaction.reply({ content: 'No invites created by the bot in this server.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Build a message with invite details and action buttons
|
||||
for (const inv of invites) {
|
||||
const created = inv.createdAt || 'Unknown';
|
||||
const uses = inv.uses || inv.maxUses || 0;
|
||||
const temporary = inv.temporary ? 'Yes' : 'No';
|
||||
const content = `Invite: ${inv.url}\nCreated: ${created}\nUses: ${uses}\nMax Uses: ${inv.maxUses || 0}\nMax Age (s): ${inv.maxAge || 0}\nTemporary: ${temporary}`;
|
||||
|
||||
const row = new ActionRowBuilder()
|
||||
.addComponents(
|
||||
new ButtonBuilder().setLabel('Copy Invite').setStyle(ButtonStyle.Secondary).setCustomId(`copy_inv_${inv.code}`),
|
||||
new ButtonBuilder().setLabel('Delete Invite').setStyle(ButtonStyle.Danger).setCustomId(`delete_inv_${inv.code}`),
|
||||
);
|
||||
|
||||
await interaction.reply({ content, components: [row], ephemeral: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in list-invites:', error);
|
||||
await interaction.reply({ content: 'Failed to list invites.', ephemeral: true });
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -15,6 +15,41 @@ commandHandler(client);
|
||||
eventHandler(client);
|
||||
|
||||
client.on('interactionCreate', async interaction => {
|
||||
// Handle button/component interactions for invites
|
||||
if (interaction.isButton && interaction.isButton()) {
|
||||
const id = interaction.customId || '';
|
||||
if (id.startsWith('copy_inv_')) {
|
||||
const code = id.replace('copy_inv_', '');
|
||||
const db = readDb();
|
||||
const invites = (db[interaction.guildId] && db[interaction.guildId].invites) ? db[interaction.guildId].invites : [];
|
||||
const inv = invites.find(i => i.code === code);
|
||||
if (inv) {
|
||||
await interaction.reply({ content: `Invite: ${inv.url}`, ephemeral: true });
|
||||
} else {
|
||||
await interaction.reply({ content: 'Invite not found.', ephemeral: true });
|
||||
}
|
||||
} else if (id.startsWith('delete_inv_')) {
|
||||
const code = id.replace('delete_inv_', '');
|
||||
// permission check: admin only
|
||||
const member = interaction.member;
|
||||
if (!member.permissions.has('Administrator')) {
|
||||
await interaction.reply({ content: 'You must be an administrator to delete invites.', ephemeral: true });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// call backend delete endpoint
|
||||
const fetch = require('node-fetch');
|
||||
const url = `http://localhost:${process.env.PORT || 3002}/api/servers/${interaction.guildId}/invites/${code}`;
|
||||
await fetch(url, { method: 'DELETE' });
|
||||
await interaction.reply({ content: 'Invite deleted.', ephemeral: true });
|
||||
} catch (e) {
|
||||
console.error('Error deleting invite via API:', e);
|
||||
await interaction.reply({ content: 'Failed to delete invite.', ephemeral: true });
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!interaction.isCommand()) return;
|
||||
|
||||
const command = client.commands.get(interaction.commandName);
|
||||
|
||||
@@ -6,12 +6,14 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
// UserSettings moved to NavBar
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
|
||||
const ServerSettings = () => {
|
||||
const { guildId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [settings, setSettings] = useState({ pingCommand: false });
|
||||
// settings state removed (not used) to avoid lint warnings
|
||||
const [isBotInServer, setIsBotInServer] = useState(false);
|
||||
const [clientId, setClientId] = useState(null);
|
||||
const [server, setServer] = useState(null);
|
||||
@@ -23,6 +25,8 @@ const ServerSettings = () => {
|
||||
roleId: '',
|
||||
});
|
||||
const [commandsList, setCommandsList] = useState([]);
|
||||
const [invites, setInvites] = useState([]);
|
||||
const [inviteForm, setInviteForm] = useState({ channelId: '', maxAge: 0, maxUses: 0, temporary: false });
|
||||
const [commandsExpanded, setCommandsExpanded] = useState(false);
|
||||
const [welcomeLeaveSettings, setWelcomeLeaveSettings] = useState({
|
||||
welcome: {
|
||||
@@ -54,11 +58,8 @@ const ServerSettings = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch settings
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/settings`)
|
||||
.then(response => {
|
||||
setSettings(response.data);
|
||||
});
|
||||
// Fetch settings (not used directly in this component)
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/settings`).catch(() => {});
|
||||
|
||||
// Check if bot is in server
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/bot-status`)
|
||||
@@ -107,6 +108,11 @@ const ServerSettings = () => {
|
||||
})
|
||||
.catch(() => setCommandsList([]));
|
||||
|
||||
// Fetch invites
|
||||
axios.get(`http://localhost:3002/api/servers/${guildId}/invites`)
|
||||
.then(resp => setInvites(resp.data || []))
|
||||
.catch(() => setInvites([]));
|
||||
|
||||
// Open commands accordion if navigated from Help back button
|
||||
if (location.state && location.state.openCommands) {
|
||||
setCommandsExpanded(true);
|
||||
@@ -188,16 +194,6 @@ const ServerSettings = () => {
|
||||
return 'custom';
|
||||
}
|
||||
|
||||
const togglePingCommand = () => {
|
||||
const newSettings = { ...settings, pingCommand: !settings.pingCommand };
|
||||
axios.post(`http://localhost:3002/api/servers/${guildId}/settings`, newSettings)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
setSettings(newSettings);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleInviteBot = () => {
|
||||
if (!clientId) return;
|
||||
const permissions = 8; // Administrator
|
||||
@@ -255,7 +251,7 @@ const ServerSettings = () => {
|
||||
<AccordionDetails>
|
||||
{!isBotInServer && <Typography>Invite the bot to enable commands.</Typography>}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: '10px' }}>
|
||||
{commandsList.map(cmd => (
|
||||
{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>
|
||||
@@ -288,6 +284,87 @@ const ServerSettings = () => {
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
{/* Invite creation and list */}
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">Invites</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
{!isBotInServer && <Typography>Invite features require the bot to be in the server.</Typography>}
|
||||
<Box sx={{ display: 'flex', gap: 2, flexDirection: { xs: 'column', sm: 'row' }, marginTop: 1 }}>
|
||||
<Box sx={{ width: { xs: '100%', sm: '40%' } }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Channel (optional)</Typography>
|
||||
<FormControl fullWidth>
|
||||
<Select value={inviteForm.channelId} onChange={(e) => setInviteForm(f => ({ ...f, channelId: e.target.value }))} displayEmpty>
|
||||
<MenuItem value="">(Any channel)</MenuItem>
|
||||
{channels.map(ch => (<MenuItem key={ch.id} value={ch.id}>{ch.name}</MenuItem>))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ width: { xs: '100%', sm: '20%' } }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Expiry</Typography>
|
||||
<FormControl fullWidth>
|
||||
<Select value={inviteForm.maxAge} onChange={(e) => setInviteForm(f => ({ ...f, maxAge: Number(e.target.value) }))}>
|
||||
<MenuItem value={0}>Never expire</MenuItem>
|
||||
<MenuItem value={3600}>1 hour</MenuItem>
|
||||
<MenuItem value={86400}>1 day</MenuItem>
|
||||
<MenuItem value={604800}>7 days</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ width: { xs: '100%', sm: '20%' } }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Max Uses</Typography>
|
||||
<FormControl fullWidth>
|
||||
<Select value={inviteForm.maxUses} onChange={(e) => setInviteForm(f => ({ ...f, maxUses: Number(e.target.value) }))}>
|
||||
<MenuItem value={0}>Unlimited</MenuItem>
|
||||
<MenuItem value={1}>1</MenuItem>
|
||||
<MenuItem value={5}>5</MenuItem>
|
||||
<MenuItem value={10}>10</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, width: { xs: '100%', sm: '20%' } }}>
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1 }}>Temporary</Typography>
|
||||
<FormControlLabel control={<Switch checked={inviteForm.temporary} onChange={(e) => setInviteForm(f => ({ ...f, temporary: e.target.checked }))} />} label="" />
|
||||
</Box>
|
||||
</Box>
|
||||
<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);
|
||||
if (resp.data && resp.data.success) {
|
||||
setInvites(prev => [...prev, resp.data.invite]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error creating invite:', err);
|
||||
}
|
||||
}} disabled={!isBotInServer}>Create Invite</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ marginTop: 2 }}>
|
||||
{invites.length === 0 && <Typography>No invites created by the bot.</Typography>}
|
||||
{invites.map(inv => (
|
||||
<Box key={inv.code} sx={{ border: '1px solid #eee', borderRadius: 1, padding: 1, marginTop: 1, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Box>
|
||||
<Typography>{inv.url}</Typography>
|
||||
<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 () => {
|
||||
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); }
|
||||
}}>Delete</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
{/* Help moved to dedicated Help page */}
|
||||
<Accordion sx={{ marginTop: '20px', opacity: isBotInServer ? 1 : 0.5 }}>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
|
||||
@@ -11,16 +11,25 @@ export const ThemeProvider = ({ children }) => {
|
||||
const [themeName, setThemeName] = useState(localStorage.getItem('themeName') || 'discord');
|
||||
|
||||
useEffect(() => {
|
||||
if (user && user.theme) {
|
||||
setThemeName(user.theme);
|
||||
} else {
|
||||
// Prefer an explicit user selection (stored in localStorage) over defaults or server values.
|
||||
// Behavior:
|
||||
// - If localStorage has a themeName, use that (user's explicit choice always wins).
|
||||
// - Else if the authenticated user has a server-side preference, adopt that and persist it locally.
|
||||
// - Else (first visit, no local choice and no server preference) use default 'discord'.
|
||||
const storedTheme = localStorage.getItem('themeName');
|
||||
if (storedTheme) {
|
||||
setThemeName(storedTheme);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (user && user.theme) {
|
||||
setThemeName(user.theme);
|
||||
localStorage.setItem('themeName', user.theme);
|
||||
return;
|
||||
}
|
||||
|
||||
// First-time visitor: fall back to default
|
||||
setThemeName('discord');
|
||||
}
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user