diff --git a/public/app.js b/public/app.js index 068c2aa..8d2d899 100644 --- a/public/app.js +++ b/public/app.js @@ -84,7 +84,15 @@ document.addEventListener('DOMContentLoaded', () => { backupBtn: document.getElementById('backupBtn'), modListSearch: document.getElementById('modListSearch'), clearModListSearch: document.getElementById('clearModListSearch'), - modListPagination: document.getElementById('modListPagination') + modListPagination: document.getElementById('modListPagination'), + teleportModal: document.getElementById('teleportModal'), + teleportPlayerName: document.getElementById('teleportPlayerName'), + teleportDestination: document.getElementById('teleportDestination'), + teleportForm: document.getElementById('teleportForm'), + effectModal: document.getElementById('effectModal'), + effectPlayerName: document.getElementById('effectPlayerName'), + effectSelect: document.getElementById('effectSelect'), + effectForm: document.getElementById('effectForm') }; const loadouts = { @@ -168,7 +176,8 @@ document.addEventListener('DOMContentLoaded', () => { geyserStatus: '', sftpStatus: '', activeNotifications: new Map(), - allMods: [] + allMods: [], + currentPlayers: [] }; function showNotification(message, type = 'loading', key = null) { @@ -646,6 +655,8 @@ document.addEventListener('DOMContentLoaded', () => { if (message.type === 'list-players' && message.data?.players) { const players = message.data.players || []; + state.currentPlayers = players; + const isSinglePlayer = players.length <= 1; const playerListHtml = players.length > 0 ? players.map(player => `
@@ -653,6 +664,8 @@ document.addEventListener('DOMContentLoaded', () => {
+ + @@ -664,6 +677,66 @@ document.addEventListener('DOMContentLoaded', () => { if (state.playerList !== playerListHtml && elements.playerList) { elements.playerList.innerHTML = playerListHtml; state.playerList = playerListHtml; + + document.querySelectorAll('.tell-player').forEach(button => { + button.addEventListener('click', () => { + const player = button.getAttribute('data-player').trim(); + if (!player) { + showNotification('Invalid player name.', 'error'); + return; + } + elements.tellPlayerName.textContent = player; + elements.tellMessage.value = ''; + elements.tellModal.classList.remove('hidden'); + }); + }); + + document.querySelectorAll('.give-player').forEach(button => { + button.addEventListener('click', () => { + const player = button.getAttribute('data-player').trim(); + if (!player) { + showNotification('Invalid player name.', 'error'); + return; + } + elements.givePlayerName.textContent = player; + elements.loadoutSelect.value = 'custom'; + elements.customGiveFields.classList.remove('hidden'); + resetItemFields(); + elements.giveModal.classList.remove('hidden'); + }); + }); + + document.querySelectorAll('.teleport-player').forEach(button => { + if (!button.disabled) { + button.addEventListener('click', () => { + const player = button.getAttribute('data-player').trim(); + if (!player) { + showNotification('Invalid player name.', 'error'); + return; + } + elements.teleportPlayerName.textContent = player; + elements.teleportDestination.innerHTML = state.currentPlayers + .filter(p => p !== player) + .map(p => ``) + .join(''); + elements.teleportModal.classList.remove('hidden'); + }); + } + }); + + document.querySelectorAll('.effect-player').forEach(button => { + button.addEventListener('click', () => { + const player = button.getAttribute('data-player').trim(); + if (!player) { + showNotification('Invalid player name.', 'error'); + return; + } + elements.effectPlayerName.textContent = player; + elements.effectSelect.value = 'speed:30:1'; + elements.effectModal.classList.remove('hidden'); + }); + }); + document.querySelectorAll('.kick-player').forEach(button => { button.addEventListener('click', () => { const player = button.getAttribute('data-player').trim(); @@ -791,34 +864,6 @@ document.addEventListener('DOMContentLoaded', () => { } }); }); - - document.querySelectorAll('.tell-player').forEach(button => { - button.addEventListener('click', () => { - const player = button.getAttribute('data-player').trim(); - if (!player) { - showNotification('Invalid player name.', 'error'); - return; - } - elements.tellPlayerName.textContent = player; - elements.tellMessage.value = ''; - elements.tellModal.classList.remove('hidden'); - }); - }); - - document.querySelectorAll('.give-player').forEach(button => { - button.addEventListener('click', () => { - const player = button.getAttribute('data-player').trim(); - if (!player) { - showNotification('Invalid player name.', 'error'); - return; - } - elements.givePlayerName.textContent = player; - elements.loadoutSelect.value = 'custom'; - elements.customGiveFields.classList.remove('hidden'); - resetItemFields(); - elements.giveModal.classList.remove('hidden'); - }); - }); } } @@ -1052,7 +1097,6 @@ document.addEventListener('DOMContentLoaded', () => { renderModList(); } - // Debounce function to limit the rate of search execution function debounce(func, wait) { let timeout; return function executedFunction(...args) { @@ -1204,6 +1248,49 @@ document.addEventListener('DOMContentLoaded', () => { } } + async function sendTeleportCommand() { + const player = elements.teleportPlayerName.textContent.trim(); + const destination = elements.teleportDestination.value.trim(); + if (!player || !destination) { + showNotification('Source and destination players are required.', 'error', 'teleport-error'); + return; + } + try { + const command = `tp ${player} ${destination}`; + const key = `action-teleport-player-${player}`; + const notification = showNotification(`Teleporting ${player} to ${destination}...`, 'loading', key); + const response = await wsRequest('/console', 'POST', { command }); + updateNotification(notification, `Teleported ${player} to ${destination} successfully`, 'success', key); + elements.teleportModal.classList.add('hidden'); + } catch (error) { + console.error(`Teleport ${player} error:`, error); + showNotification(`Failed to teleport ${player}: ${error.message}`, 'error', 'teleport-error'); + } + } + + async function sendEffectCommand() { + const player = elements.effectPlayerName.textContent.trim(); + const effectData = elements.effectSelect.value.split(':'); + const effect = effectData[0]; + const duration = parseInt(effectData[1], 10); + const amplifier = parseInt(effectData[2], 10); + if (!player || !effect) { + showNotification('Player name and effect are required.', 'error', 'effect-error'); + return; + } + try { + const command = `effect give ${player} minecraft:${effect} ${duration} ${amplifier}`; + const key = `action-effect-player-${player}`; + const notification = showNotification(`Applying ${effect} to ${player}...`, 'loading', key); + const response = await wsRequest('/console', 'POST', { command }); + updateNotification(notification, `Applied ${effect} to ${player} successfully`, 'success', key); + elements.effectModal.classList.add('hidden'); + } catch (error) { + console.error(`Apply effect to ${player} error:`, error); + showNotification(`Failed to apply effect to ${player}: ${error.message}`, 'error', 'effect-error'); + } + } + function addItemField() { const itemList = elements.itemList; const itemEntry = document.createElement('div'); @@ -1637,6 +1724,14 @@ document.addEventListener('DOMContentLoaded', () => { elements.giveModal.classList.add('hidden'); }); + elements.teleportModal.querySelector('.modal-close').addEventListener('click', () => { + elements.teleportModal.classList.add('hidden'); + }); + + elements.effectModal.querySelector('.modal-close').addEventListener('click', () => { + elements.effectModal.classList.add('hidden'); + }); + elements.loadoutSelect.addEventListener('change', () => { const isCustom = elements.loadoutSelect.value === 'custom'; elements.customGiveFields.classList.toggle('hidden', !isCustom); @@ -1659,6 +1754,16 @@ document.addEventListener('DOMContentLoaded', () => { } }); + elements.teleportForm.addEventListener('submit', (e) => { + e.preventDefault(); + sendTeleportCommand(); + }); + + elements.effectForm.addEventListener('submit', (e) => { + e.preventDefault(); + sendEffectCommand(); + }); + elements.tellForm.addEventListener('submit', (e) => { e.preventDefault(); sendTellMessage(); @@ -1684,7 +1789,6 @@ document.addEventListener('DOMContentLoaded', () => { elements.clearModListSearch.addEventListener('click', clearModListSearch); - // Debounced search for installed mods const debouncedModListSearch = debounce((query) => { modListSearchQuery = query.trim(); modListCurrentPage = 1; diff --git a/public/css/styles.css b/public/css/styles.css index 10bec08..f5ea643 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -128,6 +128,7 @@ min-width: 80px; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-family: 'Minecraft', sans-serif; /* Apply Minecraft font */ } .control-btn:hover:not(.disabled-btn) { @@ -138,6 +139,60 @@ transform: translateY(0); } + /* Existing player button styles */ + .tell-player { + @apply bg-blue-600 hover:bg-blue-700 px-2 py-1 rounded text-sm; + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + + .give-player { + @apply bg-green-600 hover:bg-green-700 px-2 py-1 rounded text-sm; + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + + .op-player, .deop-player { + @apply bg-purple-600 hover:bg-purple-700 px-2 py-1 rounded text-sm; + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + + .kick-player, .ban-player { + @apply bg-red-600 hover:bg-red-700 px-2 py-1 rounded text-sm; + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + + /* New player button styles */ + .teleport-player { + @apply bg-cyan-600 hover:bg-cyan-700 px-2 py-1 rounded text-sm; + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + + .teleport-player:hover:not(.disabled-btn) { + transform: translateY(-1px); + } + + .teleport-player:active:not(.disabled-btn) { + transform: translateY(0); + } + + .effect-player { + @apply bg-teal-600 hover:bg-teal-700 px-2 py-1 rounded text-sm; + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + + .effect-player:hover:not(.disabled-btn) { + transform: translateY(-1px); + } + + .effect-player:active:not(.disabled-btn) { + transform: translateY(0); + } + .modal { position: fixed; inset: 0; @@ -228,6 +283,20 @@ width: 100%; margin-top: 0.5rem; } + + /* Ensure player buttons stack nicely on mobile */ + .tell-player, + .give-player, + .teleport-player, + .effect-player, + .op-player, + .deop-player, + .kick-player, + .ban-player { + width: 100%; + text-align: center; + margin-top: 0.25rem; + } } /* Additional styles */ diff --git a/public/css/styles.min.css b/public/css/styles.min.css index 6ed11d3..b13ca9b 100644 --- a/public/css/styles.min.css +++ b/public/css/styles.min.css @@ -21,6 +21,10 @@ --color-yellow-700: oklch(55.4% 0.135 66.442); --color-green-600: oklch(62.7% 0.194 149.214); --color-green-700: oklch(52.7% 0.154 150.069); + --color-teal-600: oklch(60% 0.118 184.704); + --color-teal-700: oklch(51.1% 0.096 186.391); + --color-cyan-600: oklch(60.9% 0.126 221.723); + --color-cyan-700: oklch(52% 0.105 223.128); --color-blue-400: oklch(70.7% 0.165 254.624); --color-blue-500: oklch(62.3% 0.214 259.815); --color-blue-600: oklch(54.6% 0.245 262.881); @@ -401,6 +405,9 @@ .bg-blue-600 { background-color: var(--color-blue-600); } + .bg-cyan-600 { + background-color: var(--color-cyan-600); + } .bg-gray-600 { background-color: var(--color-gray-600); } @@ -422,6 +429,9 @@ .bg-red-600 { background-color: var(--color-red-600); } + .bg-teal-600 { + background-color: var(--color-teal-600); + } .bg-yellow-600 { background-color: var(--color-yellow-600); } @@ -511,6 +521,9 @@ .text-white { color: var(--color-white); } + .opacity-50 { + opacity: 50%; + } .shadow-lg { --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); @@ -522,6 +535,13 @@ } } } + .hover\:bg-cyan-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-cyan-700); + } + } + } .hover\:bg-gray-700 { &:hover { @media (hover: hover) { @@ -550,6 +570,13 @@ } } } + .hover\:bg-teal-700 { + &:hover { + @media (hover: hover) { + background-color: var(--color-teal-700); + } + } + } .hover\:bg-yellow-700 { &:hover { @media (hover: hover) { @@ -699,6 +726,7 @@ min-width: 80px; text-align: center; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + font-family: 'Minecraft', sans-serif; } .control-btn:hover:not(.disabled-btn) { transform: translateY(-1px); @@ -706,6 +734,108 @@ .control-btn:active:not(.disabled-btn) { transform: translateY(0); } + .tell-player { + border-radius: 0.25rem; + background-color: var(--color-blue-600); + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + &:hover { + @media (hover: hover) { + background-color: var(--color-blue-700); + } + } + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + .give-player { + border-radius: 0.25rem; + background-color: var(--color-green-600); + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + &:hover { + @media (hover: hover) { + background-color: var(--color-green-700); + } + } + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + .op-player, .deop-player { + border-radius: 0.25rem; + background-color: var(--color-purple-600); + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + &:hover { + @media (hover: hover) { + background-color: var(--color-purple-700); + } + } + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + .kick-player, .ban-player { + border-radius: 0.25rem; + background-color: var(--color-red-600); + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + &:hover { + @media (hover: hover) { + background-color: var(--color-red-700); + } + } + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + .teleport-player { + border-radius: 0.25rem; + background-color: var(--color-cyan-600); + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + &:hover { + @media (hover: hover) { + background-color: var(--color-cyan-700); + } + } + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + .teleport-player:hover:not(.disabled-btn) { + transform: translateY(-1px); + } + .teleport-player:active:not(.disabled-btn) { + transform: translateY(0); + } + .effect-player { + border-radius: 0.25rem; + background-color: var(--color-teal-600); + padding-inline: calc(var(--spacing) * 2); + padding-block: calc(var(--spacing) * 1); + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + &:hover { + @media (hover: hover) { + background-color: var(--color-teal-700); + } + } + font-family: 'Minecraft', sans-serif; + transition: all 0.2s ease; + } + .effect-player:hover:not(.disabled-btn) { + transform: translateY(-1px); + } + .effect-player:active:not(.disabled-btn) { + transform: translateY(0); + } .modal { position: fixed; inset: 0; @@ -779,6 +909,11 @@ width: 100%; margin-top: 0.5rem; } + .tell-player, .give-player, .teleport-player, .effect-player, .op-player, .deop-player, .kick-player, .ban-player { + width: 100%; + text-align: center; + margin-top: 0.25rem; + } } .bg-gray-800.p-6.rounded-lg.shadow-lg .grid { overflow-x: hidden; diff --git a/public/index.html b/public/index.html index d7c9bae..03adb18 100644 --- a/public/index.html +++ b/public/index.html @@ -75,6 +75,45 @@
+ + + +