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')) ? (
<>