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 @@
+