diff --git a/public/css/styles.min.css b/public/css/styles.min.css index 51e252d..1042e03 100644 --- a/public/css/styles.min.css +++ b/public/css/styles.min.css @@ -42,6 +42,8 @@ --spacing: 0.25rem; --container-md: 28rem; --container-xl: 36rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); --text-sm: 0.875rem; --text-sm--line-height: calc(1.25 / 0.875); --text-lg: 1.125rem; @@ -224,6 +226,9 @@ .inset-0 { inset: calc(var(--spacing) * 0); } + .z-50 { + z-index: 50; + } .container { width: 100%; @media (width >= 40rem) { @@ -251,6 +256,9 @@ .mt-4 { margin-top: calc(var(--spacing) * 4); } + .mr-2 { + margin-right: calc(var(--spacing) * 2); + } .mb-1 { margin-bottom: calc(var(--spacing) * 1); } @@ -278,6 +286,9 @@ .inline-block { display: inline-block; } + .table { + display: table; + } .h-24 { height: calc(var(--spacing) * 24); } @@ -296,9 +307,15 @@ .min-h-full { min-height: 100%; } + .w-1 { + width: calc(var(--spacing) * 1); + } .w-1\/3 { width: calc(1/3 * 100%); } + .w-2 { + width: calc(var(--spacing) * 2); + } .w-2\/3 { width: calc(2/3 * 100%); } @@ -320,9 +337,15 @@ .flex-1 { flex: 1; } + .flex-shrink { + flex-shrink: 1; + } .flex-grow { flex-grow: 1; } + .border-collapse { + border-collapse: collapse; + } .transform { transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); } @@ -518,6 +541,10 @@ font-size: var(--text-xl); line-height: var(--tw-leading, var(--text-xl--line-height)); } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } .leading-relaxed { --tw-leading: var(--leading-relaxed); line-height: var(--leading-relaxed); @@ -552,6 +579,9 @@ .text-white { color: var(--color-white); } + .underline { + text-decoration-line: underline; + } .opacity-50 { opacity: 50%; } @@ -559,6 +589,10 @@ --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); } + .outline { + outline-style: var(--tw-outline-style); + outline-width: 1px; + } .filter { filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); } @@ -634,6 +668,13 @@ } } } + .hover\:text-red-700 { + &:hover { + @media (hover: hover) { + color: var(--color-red-700); + } + } + } .sm\:w-\[90\%\] { @media (width >= 40rem) { width: 90%; @@ -1188,6 +1229,11 @@ inherits: false; initial-value: 0 0 #0000; } +@property --tw-outline-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} @property --tw-blur { syntax: "*"; inherits: false; @@ -1296,6 +1342,7 @@ --tw-ring-offset-width: 0px; --tw-ring-offset-color: #fff; --tw-ring-offset-shadow: 0 0 #0000; + --tw-outline-style: solid; --tw-blur: initial; --tw-brightness: initial; --tw-contrast: initial; diff --git a/public/js/app.js b/public/js/app.js index e3c23a0..be1d0b4 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -1412,260 +1412,384 @@ document.addEventListener('DOMContentLoaded', () => { let displayProperties = {}; // Store original properties for filtering - function parseServerProperties(content) { - const properties = {}; - const lines = content.split('\n'); - lines.forEach(line => { - if (line.trim() && !line.trim().startsWith('#')) { - const [key, value] = line.split('=', 2).map(part => part.trim()); - if (key && value !== undefined) { - properties[key] = value; - } +function parseServerProperties(content) { + const properties = {}; + const lines = content.split('\n'); + lines.forEach(line => { + if (line.trim() && !line.trim().startsWith('#')) { + const [key, value] = line.split('=', 2).map(part => part.trim()); + if (key && value !== undefined) { + properties[key] = value; } + } + }); + return properties; +} + +function renderPropertiesFields(properties, filter = '') { + const fieldsContainer = elements.propertiesFields; + + // Create search box only if not already present + let searchContainer = fieldsContainer.querySelector('#searchContainer'); + if (!searchContainer) { + searchContainer = document.createElement('div'); + searchContainer.id = 'searchContainer'; + searchContainer.className = 'mb-4'; + searchContainer.style.display = 'block'; + + const searchLabel = document.createElement('label'); + searchLabel.textContent = 'Search Properties'; + searchLabel.className = 'block text-sm font-medium mb-1 text-white'; + searchLabel.setAttribute('for', 'propertiesSearch'); + + const searchInput = document.createElement('input'); + searchInput.type = 'text'; + searchInput.id = 'propertiesSearch'; + searchInput.placeholder = 'Search properties...'; + searchInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full'; + searchInput.setAttribute('aria-label', 'Search server properties'); + + searchContainer.appendChild(searchLabel); + searchContainer.appendChild(searchInput); + + // Add custom property button (mini style) + const addButton = document.createElement('button'); + addButton.textContent = 'Add Property'; + addButton.type = 'button'; + addButton.className = 'mt-2 bg-blue-600 hover:bg-blue-700 text-white px-2 py-1 rounded text-xs'; + addButton.addEventListener('click', showCustomPropertyForm); + searchContainer.appendChild(addButton); + + fieldsContainer.appendChild(searchContainer); + + // Add input event listener + searchInput.addEventListener('input', (e) => { + renderPropertiesList(displayProperties, e.target.value); }); - return properties; + + // Store reference to search input + elements.searchInput = searchInput; } - function renderPropertiesFields(properties, filter = '') { - const fieldsContainer = elements.propertiesFields; - - // Create search box only if not already present - let searchContainer = fieldsContainer.querySelector('#searchContainer'); - if (!searchContainer) { - searchContainer = document.createElement('div'); - searchContainer.id = 'searchContainer'; - searchContainer.className = 'mb-4'; - searchContainer.style.display = 'block'; - - const searchLabel = document.createElement('label'); - searchLabel.textContent = 'Search Properties'; - searchLabel.className = 'block text-sm font-medium mb-1 text-white'; - searchLabel.setAttribute('for', 'propertiesSearch'); - - const searchInput = document.createElement('input'); - searchInput.type = 'text'; - searchInput.id = 'propertiesSearch'; - searchInput.placeholder = 'Search properties...'; - searchInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full'; - searchInput.setAttribute('aria-label', 'Search server properties'); - - searchContainer.appendChild(searchLabel); - searchContainer.appendChild(searchInput); - fieldsContainer.appendChild(searchContainer); - - // Add input event listener - searchInput.addEventListener('input', (e) => { - renderPropertiesList(displayProperties, e.target.value); - }); - - // Store reference to search input - elements.searchInput = searchInput; - } - - // Create or update properties list container - let propertiesList = fieldsContainer.querySelector('#propertiesList'); - if (!propertiesList) { - propertiesList = document.createElement('div'); - propertiesList.id = 'propertiesList'; - propertiesList.className = 'space-y-2'; - fieldsContainer.appendChild(propertiesList); - } - - renderPropertiesList(properties, filter); - - // Add modal close handler - const closeButton = elements.editPropertiesModal.querySelector('.close-button'); - if (closeButton && !closeButton.dataset.closeHandlerAdded) { - closeButton.addEventListener('click', () => { - elements.searchInput.value = ''; - renderPropertiesList(displayProperties, ''); - elements.editPropertiesModal.classList.add('hidden'); - }); - closeButton.dataset.closeHandlerAdded = 'true'; - } + // Create or update properties list container + let propertiesList = fieldsContainer.querySelector('#propertiesList'); + if (!propertiesList) { + propertiesList = document.createElement('div'); + propertiesList.id = 'propertiesList'; + propertiesList.className = 'space-y-2'; + fieldsContainer.appendChild(propertiesList); } - function renderPropertiesList(properties, filter = '') { - const propertiesList = elements.propertiesFields.querySelector('#propertiesList'); - propertiesList.innerHTML = ''; + renderPropertiesList(properties, filter); - // Filter properties based on search input - const filteredProperties = Object.entries(properties).filter(([key]) => - key.toLowerCase().includes(filter.toLowerCase()) - ); - - // Render filtered properties - filteredProperties.forEach(([key, value]) => { - if (filteredSettings.includes(key)) { - return; - } - - console.log(`Rendering field for ${key}: ${value}`); // Debug log - - const fieldDiv = document.createElement('div'); - fieldDiv.className = 'flex items-center space-x-2'; - fieldDiv.style.display = 'flex'; - - let inputType = 'text'; - let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false'; - if (isBoolean) { - inputType = 'switch'; - } else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) { - inputType = 'number'; - } - - const label = document.createElement('label'); - label.textContent = key; - label.className = 'w-1/3 text-sm font-medium text-white'; - label.setAttribute('for', `prop-${key}`); - - if (inputType === 'switch') { - // Hidden text input for accessibility and form association - const hiddenInput = document.createElement('input'); - hiddenInput.type = 'text'; - hiddenInput.id = `prop-${key}`; - hiddenInput.name = key; - hiddenInput.value = value.toLowerCase(); - hiddenInput.style.display = 'none'; - - const switchContainer = document.createElement('div'); - switchContainer.className = 'relative inline-block'; - switchContainer.setAttribute('role', 'switch'); - switchContainer.setAttribute('aria-checked', value.toLowerCase()); - switchContainer.setAttribute('tabindex', '0'); - switchContainer.dataset.name = key; - switchContainer.style.width = '40px'; - switchContainer.style.height = '24px'; - switchContainer.style.display = 'inline-block'; - switchContainer.style.position = 'relative'; - switchContainer.style.cursor = 'pointer'; - switchContainer.style.zIndex = '10'; - - const switchTrack = document.createElement('div'); - switchTrack.className = 'block w-full h-full rounded-full'; - switchTrack.style.backgroundColor = value.toLowerCase() === 'true' ? '#10B981' : '#4B5563'; - switchTrack.style.transition = 'background-color 0.2s ease-in-out'; - - const switchHandle = document.createElement('div'); - switchHandle.className = 'absolute rounded-full bg-white'; - switchHandle.style.width = '16px'; - switchHandle.style.height = '16px'; - switchHandle.style.top = '4px'; - switchHandle.style.left = value.toLowerCase() === 'true' ? '20px' : '4px'; - switchHandle.style.transition = 'left 0.2s ease-in-out'; - switchHandle.style.position = 'absolute'; - switchHandle.style.zIndex = '11'; - - // Handle click and keyboard events - const toggleSwitch = () => { - const currentValue = hiddenInput.value === 'true'; - const newValue = !currentValue; - hiddenInput.value = newValue.toString(); - switchContainer.setAttribute('aria-checked', newValue.toString()); - switchTrack.style.backgroundColor = newValue ? '#10B981' : '#4B5563'; - switchHandle.style.left = newValue ? '20px' : '4px'; - }; - - switchContainer.addEventListener('click', toggleSwitch); - switchContainer.addEventListener('keydown', (e) => { - if (e.key === 'Enter' || e.key === ' ') { - e.preventDefault(); - toggleSwitch(); - } - }); - - switchContainer.appendChild(switchTrack); - switchContainer.appendChild(switchHandle); - fieldDiv.appendChild(label); - fieldDiv.appendChild(hiddenInput); - fieldDiv.appendChild(switchContainer); - } else { - const input = document.createElement('input'); - input.id = `prop-${key}`; - input.name = key; - input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3'; - input.type = inputType; - input.value = value; - if (inputType === 'number') { - input.min = '0'; - } - fieldDiv.appendChild(label); - fieldDiv.appendChild(input); - } - - propertiesList.appendChild(fieldDiv); - }); - } - - function propertiesToString(properties) { - let header = `#Minecraft server properties\n#${new Date().toUTCString()}\n`; - return header + Object.entries(properties) - .map(([key, value]) => `${key}=${value}`) - .join('\n'); - } - - async function fetchServerProperties() { - try { - const key = `action-fetch-properties`; - const notification = showNotification('Loading server properties...', 'loading', key); - const response = await wsRequest('/server-properties', 'GET'); - if (response.error) { - updateNotification(notification, `Failed to load server properties: ${response.error}`, 'error', key); - return; - } - if (response.content && response.content.length > 4000) { - updateNotification(notification, `Server properties file is too large to edit (${response.content.length} characters, max 4000)`, 'error', key); - return; - } - allProperties = parseServerProperties(response.content || ''); - displayProperties = Object.fromEntries( - Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key)) - ); - renderPropertiesFields(displayProperties); - elements.editPropertiesModal.classList.remove('hidden'); - updateNotification(notification, 'Server properties loaded successfully', 'success', key); - } catch (error) { - console.error('Fetch server properties error:', error); - showNotification(`Failed to load server properties: ${error.message}`, 'error', 'fetch-properties-error'); - } - } - - async function saveServerProperties() { - try { - const key = `action-save-properties`; - const notification = showNotification('Saving server properties...', 'loading', key); - const properties = {}; - const inputs = elements.propertiesFields.querySelectorAll('input:not(#propertiesSearch)'); - - // Collect modified properties - inputs.forEach(input => { - const key = input.name; - let value = input.value.trim(); - if (value !== '') { - properties[key] = value; - } - }); - - // Merge with allProperties to include all properties, even if not displayed - const fullProperties = { ...allProperties, ...properties }; - const content = propertiesToString(fullProperties); - const response = await wsRequest('/server-properties', 'POST', { content }); - if (response.error) { - updateNotification(notification, `Failed to save server properties: ${response.error}`, 'error', key); - return; - } - // Clear search input and reset properties list - if (elements.searchInput) { - elements.searchInput.value = ''; - renderPropertiesList(displayProperties, ''); - } + // Add modal close handler + const closeButton = elements.editPropertiesModal.querySelector('.close-button'); + if (closeButton && !closeButton.dataset.closeHandlerAdded) { + closeButton.addEventListener('click', () => { + elements.searchInput.value = ''; + renderPropertiesList(displayProperties, ''); elements.editPropertiesModal.classList.add('hidden'); - updateNotification(notification, 'Server properties saved successfully', 'success', key); - } catch (error) { - console.error('Save server properties error:', error); - showNotification(`Failed to save server properties: ${error.message}`, 'error', 'save-properties-error'); - } + hideCustomPropertyForm(); + }); + closeButton.dataset.closeHandlerAdded = 'true'; } +} + +function showCustomPropertyForm() { + let formContainer = elements.propertiesFields.querySelector('#customPropertyForm'); + if (formContainer) { + formContainer.remove(); + } + + formContainer = document.createElement('div'); + formContainer.id = 'customPropertyForm'; + formContainer.className = 'mb-4 p-4 bg-gray-800 rounded'; + + const form = document.createElement('form'); + form.addEventListener('submit', (e) => e.preventDefault()); + + const keyInput = document.createElement('input'); + keyInput.type = 'text'; + keyInput.placeholder = 'Property name'; + keyInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2'; + + const valueInput = document.createElement('input'); + valueInput.type = 'text'; + valueInput.placeholder = 'Property value'; + valueInput.className = 'bg-gray-700 px-4 py-2 rounded text-white w-full mb-2'; + + const addButton = document.createElement('button'); + addButton.type = 'button'; + addButton.textContent = 'Add'; + addButton.className = 'bg-green-600 hover:bg-green-700 text-white px-2 py-1 rounded text-xs mr-2'; + addButton.addEventListener('click', () => { + const key = keyInput.value.trim(); + const value = valueInput.value.trim(); + if (key && value) { + displayProperties[key] = value; + allProperties[key] = value; + renderPropertiesList(displayProperties, elements.searchInput.value); + formContainer.remove(); + } else { + showNotification('Please enter both property name and value', 'error', 'custom-property-error'); + } + }); + + const cancelButton = document.createElement('button'); + cancelButton.type = 'button'; + cancelButton.textContent = 'Cancel'; + cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-2 py-1 rounded text-xs'; + cancelButton.addEventListener('click', hideCustomPropertyForm); + + form.appendChild(keyInput); + form.appendChild(valueInput); + form.appendChild(addButton); + form.appendChild(cancelButton); + formContainer.appendChild(form); + + elements.propertiesFields.insertBefore(formContainer, elements.propertiesFields.querySelector('#propertiesList')); +} + +function hideCustomPropertyForm() { + const formContainer = elements.propertiesFields.querySelector('#customPropertyForm'); + if (formContainer) { + formContainer.remove(); + } +} + +function showDeleteConfirmationModal(key) { + const modal = document.createElement('div'); + modal.id = 'deleteConfirmationModal'; + modal.className = 'absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'; + + const modalContent = document.createElement('div'); + modalContent.className = 'bg-gray-800 p-6 rounded-lg max-w-md w-full'; + + const header = document.createElement('h3'); + header.className = 'text-lg font-medium text-white mb-4'; + header.textContent = 'Confirm Deletion'; + + const message = document.createElement('p'); + message.className = 'text-white mb-6'; + message.textContent = `Are you sure you want to delete the property "${key}"? This action cannot be undone.`; + + const buttonContainer = document.createElement('div'); + buttonContainer.className = 'flex justify-end space-x-2'; + + const cancelButton = document.createElement('button'); + cancelButton.type = 'button'; + cancelButton.className = 'bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded'; + cancelButton.textContent = 'Cancel'; + cancelButton.addEventListener('click', () => modal.remove()); + + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded'; + deleteButton.textContent = 'Delete'; + deleteButton.addEventListener('click', () => { + delete displayProperties[key]; + delete allProperties[key]; + renderPropertiesList(displayProperties, elements.searchInput.value); + modal.remove(); + showNotification(`Property "${key}" deleted`, 'success', 'delete-property-success'); + }); + + buttonContainer.appendChild(cancelButton); + buttonContainer.appendChild(deleteButton); + modalContent.appendChild(header); + modalContent.appendChild(message); + modalContent.appendChild(buttonContainer); + modal.appendChild(modalContent); + + elements.editPropertiesModal.appendChild(modal); +} + +function renderPropertiesList(properties, filter = '') { + const propertiesList = elements.propertiesFields.querySelector('#propertiesList'); + propertiesList.innerHTML = ''; + + const filteredProperties = Object.entries(properties).filter(([key]) => + key.toLowerCase().includes(filter.toLowerCase()) + ); + + filteredProperties.forEach(([key, value]) => { + if (filteredSettings.includes(key)) { + return; + } + + console.log(`Rendering field for ${key}: ${value}`); + + const fieldDiv = document.createElement('div'); + fieldDiv.className = 'flex items-center space-x-2'; + fieldDiv.style.display = 'flex'; + + const deleteButton = document.createElement('button'); + deleteButton.type = 'button'; + deleteButton.className = 'text-red-500 hover:text-red-700'; + deleteButton.innerHTML = '✕'; + deleteButton.title = `Delete ${key}`; + deleteButton.addEventListener('click', () => showDeleteConfirmationModal(key)); + + let inputType = 'text'; + let isBoolean = value.toLowerCase() === 'true' || value.toLowerCase() === 'false'; + if (isBoolean) { + inputType = 'switch'; + } else if (/^\d+$/.test(value) && !isNaN(parseInt(value))) { + inputType = 'number'; + } + + const label = document.createElement('label'); + label.textContent = key; + label.className = 'w-1/3 text-sm font-medium text-white'; + label.setAttribute('for', `prop-${key}`); + + if (inputType === 'switch') { + const hiddenInput = document.createElement('input'); + hiddenInput.type = 'text'; + hiddenInput.id = `prop-${key}`; + hiddenInput.name = key; + hiddenInput.value = value.toLowerCase(); + hiddenInput.style.display = 'none'; + + const switchContainer = document.createElement('div'); + switchContainer.className = 'relative inline-block'; + switchContainer.setAttribute('role', 'switch'); + switchContainer.setAttribute('aria-checked', value.toLowerCase()); + switchContainer.setAttribute('tabindex', '0'); + switchContainer.dataset.name = key; + switchContainer.style.width = '40px'; + switchContainer.style.height = '24px'; + switchContainer.style.display = 'inline-block'; + switchContainer.style.position = 'relative'; + switchContainer.style.cursor = 'pointer'; + switchContainer.style.zIndex = '10'; + + const switchTrack = document.createElement('div'); + switchTrack.className = 'block w-full h-full rounded-full'; + switchTrack.style.backgroundColor = value.toLowerCase() === 'true' ? '#10B981' : '#4B5563'; + switchTrack.style.transition = 'background-color 0.2s ease-in-out'; + + const switchHandle = document.createElement('div'); + switchHandle.className = 'absolute rounded-full bg-white'; + switchHandle.style.width = '16px'; + switchHandle.style.height = '16px'; + switchHandle.style.top = '4px'; + switchHandle.style.left = value.toLowerCase() === 'true' ? '20px' : '4px'; + switchHandle.style.transition = 'left 0.2s ease-in-out'; + switchHandle.style.position = 'absolute'; + switchHandle.style.zIndex = '11'; + + const toggleSwitch = () => { + const currentValue = hiddenInput.value === 'true'; + const newValue = !currentValue; + hiddenInput.value = newValue.toString(); + switchContainer.setAttribute('aria-checked', newValue.toString()); + switchTrack.style.backgroundColor = newValue ? '#10B981' : '#4B5563'; + switchHandle.style.left = newValue ? '20px' : '4px'; + }; + + switchContainer.addEventListener('click', toggleSwitch); + switchContainer.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + toggleSwitch(); + } + }); + + switchContainer.appendChild(switchTrack); + switchContainer.appendChild(switchHandle); + fieldDiv.appendChild(deleteButton); + fieldDiv.appendChild(label); + fieldDiv.appendChild(hiddenInput); + fieldDiv.appendChild(switchContainer); + } else { + const input = document.createElement('input'); + input.id = `prop-${key}`; + input.name = key; + input.className = 'bg-gray-700 px-4 py-2 rounded text-white w-2/3'; + input.type = 'number'; + input.value = value; + if (inputType === 'number') { + input.min = '0'; + } + fieldDiv.appendChild(deleteButton); + fieldDiv.appendChild(label); + fieldDiv.appendChild(input); + } + + propertiesList.appendChild(fieldDiv); + }); +} + +function propertiesToString(properties) { + let header = `#Minecraft server properties\n#${new Date().toUTCString()}\n`; + return header + Object.entries(properties) + .map(([key, value]) => `${key}=${value}`) + .join('\n'); +} + +async function fetchServerProperties() { + try { + const key = `action-fetch-properties`; + const notification = showNotification('Loading server properties...', 'loading', key); + const response = await wsRequest('/server-properties', 'GET'); + if (response.error) { + updateNotification(notification, `Failed to load server properties: ${response.error}`, 'error', key); + return; + } + if (response.content && response.content.length > 4000) { + updateNotification(notification, `Server properties file is too large to edit (${response.content.length} characters, max 4000)`, 'error', key); + return; + } + allProperties = parseServerProperties(response.content || ''); + displayProperties = Object.fromEntries( + Object.entries(allProperties).filter(([key]) => !filteredSettings.includes(key)) + ); + renderPropertiesFields(displayProperties); + elements.editPropertiesModal.classList.remove('hidden'); + updateNotification(notification, 'Server properties loaded successfully', 'success', key); + } catch (error) { + console.error('Fetch server properties error:', error); + showNotification(`Failed to load server properties: ${error.message}`, 'error', 'fetch-properties-error'); + } +} + +async function saveServerProperties() { + try { + const key = `action-save-properties`; + const notification = showNotification('Saving server properties...', 'loading', key); + const properties = {}; + const inputs = elements.propertiesFields.querySelectorAll('input:not(#propertiesSearch):not([id="customPropertyKey"]):not([id="customPropertyValue"])'); + + // Collect modified properties + inputs.forEach(input => { + const key = input.name; + let value = input.value.trim(); + if (value !== '') { + properties[key] = value; + } + }); + + // Merge with allProperties to include all properties, even if not displayed + const fullProperties = { ...allProperties, ...properties }; + const content = propertiesToString(fullProperties); + const response = await wsRequest('/server-properties', 'POST', { content }); + if (response.error) { + updateNotification(notification, `Failed to save server properties: ${response.error}`, 'error', key); + return; + } + // Clear search input and reset properties list + if (elements.searchInput) { + elements.searchInput.value = ''; + renderPropertiesList(displayProperties, ''); + } + elements.editPropertiesModal.classList.add('hidden'); + updateNotification(notification, 'Server properties saved successfully', 'success', key); + } catch (error) { + console.error('Save server properties error:', error); + showNotification(`Failed to save server properties: ${error.message}`, 'error', 'save-properties-error'); + } +} async function updateMods() { try {