diff --git a/gsm-frontend/src/components/ConfirmModal.jsx b/gsm-frontend/src/components/ConfirmModal.jsx new file mode 100644 index 0000000..529094a --- /dev/null +++ b/gsm-frontend/src/components/ConfirmModal.jsx @@ -0,0 +1,23 @@ +export default function ConfirmModal({ title, message, confirmText, cancelText, onConfirm, onCancel, variant = 'danger' }) { + const confirmBtnClass = variant === 'danger' ? 'btn btn-destructive' : 'btn btn-primary' + + return ( +
+
e.stopPropagation()}> +
+

{title}

+

{message}

+ +
+ + +
+
+
+
+ ) +} diff --git a/gsm-frontend/src/components/ServerCard.jsx b/gsm-frontend/src/components/ServerCard.jsx index 7914534..1d395d7 100644 --- a/gsm-frontend/src/components/ServerCard.jsx +++ b/gsm-frontend/src/components/ServerCard.jsx @@ -1,3 +1,7 @@ +import { useState } from 'react' +import { serverAction } from '../api' +import ConfirmModal from './ConfirmModal' + const serverInfo = { minecraft: { address: 'minecraft.zeasy.dev', @@ -64,8 +68,10 @@ const getServerInfo = (serverName) => { return null } -export default function ServerCard({ server, onClick, isAuthenticated, isGuest, displaySettings }) { +export default function ServerCard({ server, onClick, isAuthenticated, isGuest, displaySettings, isModerator, token, onServerAction }) { const defaultInfo = getServerInfo(server.name) + const [confirmAction, setConfirmAction] = useState(null) + const [actionLoading, setActionLoading] = useState(false) // Merge default info with database display settings (database takes priority) const info = defaultInfo ? { @@ -119,6 +125,30 @@ export default function ServerCard({ server, onClick, isAuthenticated, isGuest, const isClickable = !isUnreachable && !isGuest && onClick + const handleAction = async (action) => { + setActionLoading(true) + try { + await serverAction(token, server.id, action) + if (onServerAction) onServerAction() + } catch (err) { + console.error(err) + } finally { + setActionLoading(false) + setConfirmAction(null) + } + } + + const handleActionClick = (action, e) => { + e.stopPropagation() + if (action === 'start') { + handleAction(action) + } else { + setConfirmAction(action) + } + } + + const isActionDisabled = actionLoading || server.status === 'starting' || server.status === 'stopping' + return (
)} + + {/* Server Controls - only for moderators */} + {isModerator && !isUnreachable && ( +
+
+ {(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? ( + <> + + + + ) : ( + + )} +
+
+ )} + + {/* Confirmation Modal */} + {confirmAction && ( + handleAction(confirmAction)} + onCancel={() => setConfirmAction(null)} + /> + )} ) } diff --git a/gsm-frontend/src/index.css b/gsm-frontend/src/index.css index bafbc6f..641a0a6 100644 --- a/gsm-frontend/src/index.css +++ b/gsm-frontend/src/index.css @@ -52,6 +52,11 @@ button { cursor: not-allowed; } +.btn-sm { + font-size: 0.75rem; + padding: 0.375rem 0.75rem; +} + .btn-primary { background-color: #fafafa; color: #0a0a0a; diff --git a/gsm-frontend/src/pages/Dashboard.jsx b/gsm-frontend/src/pages/Dashboard.jsx index 65a1717..373f2cc 100644 --- a/gsm-frontend/src/pages/Dashboard.jsx +++ b/gsm-frontend/src/pages/Dashboard.jsx @@ -9,7 +9,7 @@ import LoginModal from '../components/LoginModal' export default function Dashboard({ onLogout }) { const navigate = useNavigate() - const { user, token, loading: userLoading, isSuperadmin, role } = useUser() + const { user, token, loading: userLoading, isSuperadmin, isModerator, role } = useUser() const [servers, setServers] = useState([]) const [displaySettings, setDisplaySettings] = useState({}) const [loading, setLoading] = useState(true) @@ -259,6 +259,9 @@ export default function Dashboard({ onLogout }) { isAuthenticated={isAuthenticated} isGuest={isGuest} displaySettings={displaySettings[server.id]} + isModerator={isModerator} + token={token} + onServerAction={fetchServers} /> ))} diff --git a/gsm-frontend/src/pages/ServerDetail.jsx b/gsm-frontend/src/pages/ServerDetail.jsx index d58d95c..6e24f85 100644 --- a/gsm-frontend/src/pages/ServerDetail.jsx +++ b/gsm-frontend/src/pages/ServerDetail.jsx @@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom' import { getServers, serverAction, sendRcon, getServerLogs, getWhitelist, getFactorioCurrentSave, getAutoShutdownSettings, setAutoShutdownSettings, getDisplaySettings, setDisplaySettings } from '../api' import { useUser } from '../context/UserContext' import MetricsChart from '../components/MetricsChart' +import ConfirmModal from '../components/ConfirmModal' import FactorioWorldManager from '../components/FactorioWorldManager' import PalworldConfigEditor from '../components/PalworldConfigEditor' import ZomboidConfigEditor from '../components/ZomboidConfigEditor' @@ -42,6 +43,9 @@ export default function ServerDetail() { // Auto-shutdown state const [autoShutdown, setAutoShutdown] = useState({ enabled: false, timeoutMinutes: 15, emptySinceMinutes: null }) + + // Confirmation modal state + const [confirmAction, setConfirmAction] = useState(null) const [autoShutdownLoading, setAutoShutdownLoading] = useState(false) // Display settings state (superadmin only) @@ -131,11 +135,26 @@ export default function ServerDetail() { await serverAction(token, server.id, action) setTimeout(() => { fetchServer() - + }, 2000) } catch (err) { console.error(err) - + + } + } + + const handleActionWithConfirm = (action) => { + if (action === 'start') { + handleAction(action) + } else { + setConfirmAction(action) + } + } + + const handleConfirmAction = () => { + if (confirmAction) { + handleAction(confirmAction) + setConfirmAction(null) } } @@ -443,14 +462,14 @@ const formatUptime = (seconds) => { {(server.status === 'online' || (server.running && server.status !== 'starting' && server.status !== 'stopping')) ? ( <>