fix(admin): repair management views and harden plugin handling
Build and publish Docker image / build-and-publish (push) Successful in 14s
Build and publish Docker image / build-and-publish (push) Successful in 14s
This commit is contained in:
1 parent
d8862c9611
commit
b7b36b1355
7 files changed
+92
-50
No files matched your search
@@ -145,7 +145,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
demo_servers_enabled: Boolean(data.settings.demo_servers_enabled),
|
||||
new_user_credits: parseInt(data.settings.new_user_credits, 10) || 0
|
||||
});
|
||||
showAlert('Success', 'Panel settings updated successfully', 'success');
|
||||
showAlert('Settings saved', 'The panel settings are now active.', 'success');
|
||||
} else {
|
||||
showAlert('Error', data.error || 'Failed to update panel settings', 'error');
|
||||
}
|
||||
@@ -216,7 +216,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
}
|
||||
}
|
||||
|
||||
showAlert('Success', `Pricing updated successfully${postNotice ? ' and announcement posted' : ''}`, 'success');
|
||||
showAlert('Pricing saved', `New prices are active${postNotice ? ', and the announcement is posted.' : '.'}`, 'success');
|
||||
setPostNotice(false); // Reset checkbox
|
||||
} else {
|
||||
showAlert('Error', data.error || 'Failed to update pricing', 'error');
|
||||
@@ -529,7 +529,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
if (selectedUser?.id === userId) {
|
||||
setSelectedUser(prev => ({ ...prev, role: isBanned ? 'user' : 'banned' }));
|
||||
}
|
||||
showAlert('Success', `User ${isBanned ? 'unbanned' : 'banned'} successfully`, 'success');
|
||||
showAlert('User access updated', `This account is ${isBanned ? 'active' : 'suspended'}.`, 'success');
|
||||
} else {
|
||||
showAlert('Error', data.error || 'Operation failed', 'error');
|
||||
}
|
||||
@@ -551,7 +551,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showAlert('Success', 'User deleted successfully', 'success');
|
||||
showAlert('User deleted', 'The account has been removed.', 'success');
|
||||
fetchUsers();
|
||||
setSelectedUser(null);
|
||||
} else {
|
||||
@@ -579,7 +579,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showAlert('Success', 'Password reset successfully', 'success');
|
||||
showAlert('Password reset', 'The new password is ready to use.', 'success');
|
||||
setNewPassword('');
|
||||
} else {
|
||||
showAlert('Error', data.error || 'Failed to reset password', 'error');
|
||||
@@ -604,7 +604,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showAlert('Success', '2FA disabled successfully', 'success');
|
||||
showAlert('Two-factor authentication disabled', 'The user can sign in without a verification code.', 'success');
|
||||
// Update local state to reflect change (optional, depends if we get data back)
|
||||
} else {
|
||||
showAlert('Error', data.error || 'Failed to disable 2FA', 'error');
|
||||
@@ -639,7 +639,7 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
));
|
||||
setSelectedUser(prev => ({ ...prev, credits: data.new_credits }));
|
||||
setCreditAmount('');
|
||||
showAlert('Success', 'Credits updated successfully', 'success');
|
||||
showAlert('Credits updated', 'The new balance is available to the user.', 'success');
|
||||
fetchUserHistory(selectedUser.id); // Refresh history
|
||||
} else {
|
||||
showAlert('Error', data.error || 'Failed to update credits', 'error');
|
||||
@@ -832,20 +832,25 @@ const AdminSection = ({ user, activeTab: parentActiveTab, showAlert }) => {
|
||||
</ActionButton>
|
||||
</div>
|
||||
|
||||
<p className="mb-5 max-w-3xl text-sm leading-relaxed text-dash-textMuted">
|
||||
Review every server across user accounts. Select a server to open its controls, or refresh the list to check for recent changes.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
{isLoadingAllServers ? (
|
||||
<DashboardLoadingSpinner label="Loading servers..." />
|
||||
) : allServers.length === 0 ? (
|
||||
<div className="text-center py-20 rounded-2xl border border-dashed border-dash-border bg-white/[0.01]">
|
||||
<div className="text-center py-20 border-2 border-dashed border-dash-border bg-dash-surfaceElevated">
|
||||
<Server className="w-12 h-12 mx-auto mb-4 opacity-20 text-dash-textMuted" />
|
||||
<p className="font-medium text-dash-textMuted">No servers found in the system.</p>
|
||||
<p className="font-bold text-dash-text">No servers to manage</p>
|
||||
<p className="mt-2 text-sm text-dash-textMuted">Servers appear here after an account creates one.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||
{allServers.map(server => (
|
||||
<div
|
||||
key={server.id}
|
||||
className="group relative rounded-2xl border border-dash-border bg-dash-surfaceElevated/30 p-5 transition-all duration-300 hover:border-primary/40 hover:bg-dash-surfaceElevated/50"
|
||||
className="group relative border-2 border-dash-border bg-dash-surfaceElevated p-5 shadow-[5px_5px_0_0_rgba(0,0,0,0.65)] transition-colors hover:border-primary"
|
||||
>
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="min-w-0">
|
||||
|
||||
@@ -26,7 +26,6 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const outputRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const token = localStorage.getItem('anchorhost_token');
|
||||
|
||||
const {
|
||||
@@ -113,35 +112,47 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (!isConnected) return;
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\r');
|
||||
} else if (e.key === 'Backspace') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\b');
|
||||
} else if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\t');
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x1b');
|
||||
} else if (e.ctrlKey && e.key === 'c') {
|
||||
} else if (e.ctrlKey && e.key.toLowerCase() === 'c') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x03');
|
||||
} else if (e.ctrlKey && e.key === 'd') {
|
||||
} else if (e.ctrlKey && e.key.toLowerCase() === 'd') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x04');
|
||||
} else if (e.ctrlKey && e.key === 'l') {
|
||||
} else if (e.ctrlKey && e.key.toLowerCase() === 'l') {
|
||||
e.preventDefault();
|
||||
setTerminalOutput('');
|
||||
handleTerminalInput('\x0c');
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x1b[A');
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x1b[B');
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x1b[C');
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x1b[D');
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (!isConnected) return;
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
} else if (e.key === 'Home') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x1b[H');
|
||||
} else if (e.key === 'End') {
|
||||
e.preventDefault();
|
||||
handleTerminalInput('\x1b[F');
|
||||
} else if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
handleTerminalInput(e.key);
|
||||
}
|
||||
};
|
||||
@@ -199,24 +210,24 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
};
|
||||
|
||||
const modalContent = (
|
||||
<div className="fixed inset-0 bg-black/80 z-[9999] flex items-center justify-center p-2 sm:p-4 md:p-6 backdrop-blur-md animate-in fade-in duration-200">
|
||||
<div className="fixed inset-0 bg-black/90 z-[9999] flex items-center justify-center p-2 sm:p-4 md:p-6 animate-in fade-in duration-200">
|
||||
<div
|
||||
className={`bg-[#0d1117] border border-dash-border/80 rounded-2xl shadow-2xl flex flex-col overflow-hidden transition-all duration-300 ${
|
||||
className={`bg-dash-surface border-2 border-dash-border shadow-[6px_6px_0_0_rgba(0,0,0,0.8)] flex flex-col overflow-hidden transition-all duration-300 ${
|
||||
isFullscreen ? 'w-full h-full rounded-none' : 'w-full max-w-5xl h-[85vh]'
|
||||
}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b border-dash-border/40 bg-dash-surfaceElevated/60 backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between px-5 py-3.5 border-b-2 border-dash-border bg-dash-surfaceElevated">
|
||||
<div className="flex items-center space-x-3 min-w-0">
|
||||
<div className="p-2 rounded-xl bg-primary/10 border border-primary/20 text-primary">
|
||||
<div className="p-2 bg-primary/10 border-2 border-primary/30 text-primary">
|
||||
<TerminalIcon className="w-5 h-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-bold text-sm md:text-base text-dash-text truncate">
|
||||
Node Shell: {node.name}
|
||||
Host terminal · {node.name}
|
||||
</h3>
|
||||
<span className={`px-2 py-0.5 rounded-full text-[10px] font-bold uppercase tracking-wider border ${
|
||||
<span className={`px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider border-2 ${
|
||||
node.role?.includes('Control')
|
||||
? 'bg-brightMagenta/10 border-brightMagenta/20 text-brightMagenta'
|
||||
: 'bg-primary/10 border-primary/20 text-primary'
|
||||
@@ -252,7 +263,7 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
<button
|
||||
onClick={connectTerminal}
|
||||
disabled={isLoading}
|
||||
className="px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all bg-primary hover:bg-primary/90 text-white flex items-center space-x-1.5 shadow-sm disabled:opacity-50"
|
||||
className="px-3.5 py-1.5 text-xs font-bold transition-all bg-primary hover:bg-primary/90 text-white flex items-center space-x-1.5 border-2 border-dash-border shadow-[3px_3px_0_0_rgba(0,0,0,0.6)] disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Power className="w-3.5 h-3.5" />}
|
||||
<span>{isLoading ? 'Connecting' : 'Connect'}</span>
|
||||
@@ -260,7 +271,7 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
) : (
|
||||
<button
|
||||
onClick={disconnectTerminal}
|
||||
className="px-3.5 py-1.5 rounded-xl text-xs font-bold transition-all bg-brightRed/10 hover:bg-brightRed/20 border border-brightRed/30 text-brightRed flex items-center space-x-1.5"
|
||||
className="px-3.5 py-1.5 text-xs font-bold transition-all bg-brightRed/10 hover:bg-brightRed/20 border-2 border-brightRed/50 text-brightRed flex items-center space-x-1.5"
|
||||
>
|
||||
<Square className="w-3.5 h-3.5 fill-current" />
|
||||
<span>Disconnect</span>
|
||||
@@ -304,11 +315,15 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
{/* Terminal Screen */}
|
||||
<div
|
||||
ref={outputRef}
|
||||
onClick={() => inputRef.current?.focus()}
|
||||
className="flex-1 p-4 md:p-6 font-mono text-xs md:text-[13px] overflow-y-auto custom-scrollbar bg-black/90 text-gray-200 cursor-text select-text whitespace-pre-wrap break-all leading-relaxed"
|
||||
tabIndex={0}
|
||||
onClick={() => outputRef.current?.focus()}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyPress={handleKeyPress}
|
||||
aria-label={`Interactive root terminal for node ${node.name}. Click here and type commands.`}
|
||||
role="textbox"
|
||||
aria-multiline="true"
|
||||
aria-readonly="false"
|
||||
className="flex-1 p-4 md:p-6 font-mono text-xs md:text-[13px] overflow-y-auto custom-scrollbar bg-black text-gray-200 cursor-text select-none whitespace-pre-wrap break-all leading-relaxed outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
>
|
||||
{terminalOutput ? parseAnsi(terminalOutput) : (
|
||||
<div className="text-dash-textMuted/60 italic">
|
||||
@@ -321,10 +336,10 @@ const NodeTerminalModal = ({ node, onClose }) => {
|
||||
</div>
|
||||
|
||||
{/* Footer info banner */}
|
||||
<div className="px-5 py-2.5 border-t border-dash-border/30 bg-dash-surfaceElevated/40 text-[11px] text-dash-textMuted/60 flex items-center justify-between">
|
||||
<div className="px-5 py-2.5 border-t-2 border-dash-border bg-dash-surfaceElevated text-[11px] text-dash-textMuted flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="font-mono">root@{node.name}:~#</span>
|
||||
<span className="opacity-40">| Host namespace privileged session</span>
|
||||
<span className="text-brightYellow">| Privileged host shell</span>
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center space-x-4">
|
||||
<span>Ctrl+C: Cancel</span>
|
||||
|
||||
@@ -279,11 +279,14 @@ function PluginDetails() {
|
||||
{plugin.description && (
|
||||
<div className="rounded-xl border p-6 mb-6" style={{ backgroundColor: colors.backgroundSecondary, borderColor: colors.brightBlack }}>
|
||||
<h2 className="text-xl font-semibold mb-4" style={{ color: colors.brightWhite }}>Description</h2>
|
||||
<div
|
||||
className="prose prose-invert max-w-none"
|
||||
<p
|
||||
className="max-w-none whitespace-pre-wrap leading-relaxed"
|
||||
style={{ color: colors.foreground }}
|
||||
dangerouslySetInnerHTML={{ __html: plugin.description }}
|
||||
/>
|
||||
>
|
||||
{typeof plugin.description === 'string'
|
||||
? new DOMParser().parseFromString(plugin.description, 'text/html').body.textContent
|
||||
: ''}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -32,7 +32,19 @@ import {
|
||||
MinecraftSettings
|
||||
} from '../../components/DashboardComponents';
|
||||
|
||||
const normalizeServerPorts = (ports) => {
|
||||
if (Array.isArray(ports)) return ports.filter((port) => port && typeof port === 'object');
|
||||
if (typeof ports !== 'string' || !ports.trim()) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(ports);
|
||||
return Array.isArray(parsed) ? parsed.filter((port) => port && typeof port === 'object') : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
|
||||
const serverPorts = normalizeServerPorts(server?.ports);
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [isTerminalFullscreen, setIsTerminalFullscreen] = useState(false);
|
||||
const [serverMetrics, setServerMetrics] = useState({ cpu: { display: '0m', value: 0 }, memory: { display: '0 Mi', value: 0 }, cpu_percentage: 0, memory_percentage: 0 });
|
||||
@@ -674,12 +686,12 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<p className="text-base font-mono font-bold tracking-tight text-white truncate">
|
||||
{server.ip || '85.237.179.121'}:{server.ports?.[0]?.external || '---'}
|
||||
{server.ip || '85.237.179.121'}:{serverPorts[0]?.external || '---'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${server.ip || '85.237.179.121'}:${server.ports?.[0]?.external || ''}`);
|
||||
navigator.clipboard.writeText(`${server.ip || '85.237.179.121'}:${serverPorts[0]?.external || ''}`);
|
||||
showAlert('Copied', 'Primary address copied', 'success');
|
||||
}}
|
||||
className="px-3 py-1.5 rounded-lg bg-primary text-white text-[10px] font-black uppercase tracking-widest hover:brightness-110 active:scale-95 transition-all shadow-lg shadow-primary/20"
|
||||
@@ -698,7 +710,7 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2 max-h-[140px] overflow-y-auto pr-1 custom-scrollbar">
|
||||
{server.ports && server.ports.map((port, idx) => (
|
||||
{serverPorts.map((port, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between p-3 rounded-xl bg-white/[0.03] border border-white/5 hover:border-white/10 transition-all group">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-1.5 rounded-lg bg-white/5 text-[9px] font-black text-dash-textMuted border border-white/5">
|
||||
@@ -953,7 +965,7 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-white/5">
|
||||
{server.ports && server.ports.map((port, idx) => (
|
||||
{serverPorts.map((port, idx) => (
|
||||
<tr key={idx} className="group hover:bg-white/[0.01] transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -1000,7 +1012,7 @@ const ServerDetailsModal = ({ server, onClose, onServerAction, showAlert }) => {
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{(!server.ports || server.ports.length === 0) && (
|
||||
{serverPorts.length === 0 && (
|
||||
<div className="py-20 text-center">
|
||||
<NetworkIcon className="w-12 h-12 mx-auto mb-4 opacity-10" />
|
||||
<p className="text-sm font-bold opacity-20 uppercase tracking-[0.2em]">No network mappings active</p>
|
||||
|
||||
@@ -453,8 +453,15 @@ body > div:not(#root) [class*="rounded-"] { border-radius: 2px !important; }
|
||||
body > div:not(#root) :not(button):not(a)[class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
|
||||
body > div:not(#root) :is(button, a)[class*="shadow-"] { box-shadow: none !important; transform: none !important; }
|
||||
body > div:not(#root) [class*="backdrop-blur"] { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
|
||||
body > div:not(#root) [class*="bg-dash-surfaceElevated/"] { background-color: var(--ah-panel) !important; }
|
||||
body > div:not(#root) :is(button, a, input, textarea, select):focus-visible { outline: 2px solid var(--ah-accent); outline-offset: 2px; }
|
||||
.dashboard-app, .admin-app { background: var(--ah-background) !important; color: var(--ah-text); }
|
||||
.admin-app [class*="backdrop-blur"] { -webkit-backdrop-filter: none !important; backdrop-filter: none !important; }
|
||||
.admin-app [class*="rounded-"] { border-radius: 2px !important; }
|
||||
.admin-app :not(button):not(a)[class*="shadow-"]:not(.shadow-none) { box-shadow: var(--ah-shadow) !important; }
|
||||
.admin-app [class*="hover:scale"] { transform: none !important; }
|
||||
.admin-app :is(button, a, input, textarea, select) { border-radius: 2px; }
|
||||
.admin-app :is(button, a, input, textarea, select):focus-visible { outline: 2px solid var(--ah-accent); outline-offset: 2px; }
|
||||
.dashboard-app .bg-dash-surface, .admin-app .bg-dash-surface { background-color: var(--ah-surface); }
|
||||
.dashboard-app .bg-dash-surfaceElevated, .admin-app .bg-dash-surfaceElevated { background-color: var(--ah-panel); }
|
||||
.dashboard-app .border-dash-border, .admin-app .border-dash-border { border-color: var(--ah-border); }
|
||||
|
||||
@@ -123,7 +123,7 @@ const AdminPage = () => {
|
||||
<div className="admin-app flex h-screen overflow-hidden bg-dash-surface font-sans selection:bg-primary/30 text-dash-textMuted antialiased">
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated/50 backdrop-blur-md pt-[env(safe-area-inset-top,0px)] ${
|
||||
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-transform duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated pt-[env(safe-area-inset-top,0px)] ${
|
||||
mobileMenuOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
@@ -257,7 +257,7 @@ const AdminPage = () => {
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 flex flex-col min-w-0 overflow-hidden relative">
|
||||
{/* Mobile Header */}
|
||||
<header className="md:hidden flex items-center justify-between p-4 border-b border-dash-border bg-dash-surfaceElevated/80 backdrop-blur-md pt-[calc(1rem+env(safe-area-inset-top,0px))]">
|
||||
<header className="md:hidden flex items-center justify-between p-4 border-b border-dash-border bg-dash-surfaceElevated pt-[calc(1rem+env(safe-area-inset-top,0px))]">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="p-2 rounded-lg bg-brightRed/10 border border-brightRed/20 shadow-sm">
|
||||
<Shield className="w-5 h-5 text-brightRed" />
|
||||
@@ -283,9 +283,9 @@ const AdminPage = () => {
|
||||
|
||||
{/* Alert Modal */}
|
||||
{alertModal.open && (
|
||||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-[9999] p-4 backdrop-blur-sm animate-in fade-in duration-300">
|
||||
<div className="rounded-2xl w-full max-w-md border border-dash-border bg-dash-surfaceElevated shadow-2xl overflow-hidden animate-in zoom-in-95 duration-200">
|
||||
<div className={`p-6 border-b flex justify-between items-center ${
|
||||
<div className="fixed inset-0 bg-black/90 flex items-center justify-center z-[9999] p-4 animate-in fade-in duration-200">
|
||||
<div className="w-full max-w-md border-2 border-dash-border bg-dash-surfaceElevated shadow-[6px_6px_0_0_rgba(0,0,0,0.8)] overflow-hidden animate-in zoom-in-95 duration-200">
|
||||
<div className={`p-6 border-b-2 flex justify-between items-center ${
|
||||
alertModal.type === 'error' ? 'border-brightRed/20' :
|
||||
alertModal.type === 'success' ? 'border-brightGreen/20' : 'border-dash-border'
|
||||
}`}>
|
||||
@@ -316,7 +316,7 @@ const AdminPage = () => {
|
||||
onClick={() => setAlertModal({ ...alertModal, open: false })}
|
||||
className="w-full sm:w-auto font-bold"
|
||||
>
|
||||
Confirm Action
|
||||
Close
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1907,7 +1907,7 @@ const handleWebSocketMessage = useCallback((data) => {
|
||||
className="dashboard-app flex h-screen overflow-hidden bg-dash-surface font-sans selection:bg-primary/30 text-dash-text antialiased"
|
||||
>
|
||||
<aside
|
||||
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-all duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated/50 backdrop-blur-xl pt-[env(safe-area-inset-top,0px)] ${
|
||||
className={`fixed inset-y-0 left-0 z-50 w-72 transform transition-all duration-300 ease-in-out md:relative md:translate-x-0 border-r border-dash-border bg-dash-surfaceElevated pt-[env(safe-area-inset-top,0px)] ${
|
||||
isMobileSidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
}`}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user