Items 10-15: ES modules, inline style cleanup, template modal, code modernization
- Item 10: Convert to ES modules with import/export, single module entry point - Item 11: Replace inline styles with CSS classes (background overlay, card animations, highlight effect, config modal form elements) - Item 12: Move ConfigManager modal HTML from JS template literal to <template> element in index.html - Item 13: Replace deprecated url.parse() with new URL() in server.js and update route handlers to use searchParams - Item 14: Replace JSON.parse/stringify deep clone with structuredClone() - Item 15: Remove dead JSON-fixing regex code from departures.js route Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,109 +83,39 @@ class ConfigManager {
|
||||
* Create the configuration modal
|
||||
*/
|
||||
createConfigModal() {
|
||||
const template = document.getElementById('config-modal-template');
|
||||
const modalContainer = document.createElement('div');
|
||||
modalContainer.id = this.options.configModalId;
|
||||
modalContainer.className = 'config-modal';
|
||||
modalContainer.style.display = 'none';
|
||||
|
||||
modalContainer.innerHTML = `
|
||||
<div class="config-modal-content">
|
||||
<div class="config-modal-header">
|
||||
<h2>Settings</h2>
|
||||
<span class="config-modal-close">×</span>
|
||||
</div>
|
||||
<div class="config-tabs">
|
||||
<button class="config-tab active" data-tab="display">Display</button>
|
||||
<button class="config-tab" data-tab="appearance">Appearance</button>
|
||||
<button class="config-tab" data-tab="content">Content</button>
|
||||
<button class="config-tab" data-tab="options">Options</button>
|
||||
</div>
|
||||
<div class="config-modal-body">
|
||||
<!-- Display Tab -->
|
||||
<div class="config-tab-content active" id="tab-display">
|
||||
<div class="config-option">
|
||||
<label for="orientation-select">Screen Orientation:</label>
|
||||
<select id="orientation-select">
|
||||
<option value="normal" ${this.config.orientation === 'normal' ? 'selected' : ''}>Normal (0°)</option>
|
||||
<option value="vertical" ${this.config.orientation === 'vertical' ? 'selected' : ''}>Vertical (90°)</option>
|
||||
<option value="upsidedown" ${this.config.orientation === 'upsidedown' ? 'selected' : ''}>Upside Down (180°)</option>
|
||||
<option value="vertical-reverse" ${this.config.orientation === 'vertical-reverse' ? 'selected' : ''}>Vertical Reverse (270°)</option>
|
||||
<option value="landscape" ${this.config.orientation === 'landscape' ? 'selected' : ''}>Landscape (2-column)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="config-option">
|
||||
<label for="dark-mode-select">Dark Mode:</label>
|
||||
<select id="dark-mode-select">
|
||||
<option value="auto" ${this.config.darkMode === 'auto' ? 'selected' : ''}>Automatic (Sunset/Sunrise)</option>
|
||||
<option value="on" ${this.config.darkMode === 'on' ? 'selected' : ''}>Always On</option>
|
||||
<option value="off" ${this.config.darkMode === 'off' ? 'selected' : ''}>Always Off</option>
|
||||
</select>
|
||||
<div class="sun-times" id="sun-times">
|
||||
<small>Sunrise: <span id="sunrise-time">--:--</span> | Sunset: <span id="sunset-time">--:--</span></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Appearance Tab -->
|
||||
<div class="config-tab-content" id="tab-appearance">
|
||||
<div class="config-option">
|
||||
<label for="background-image-url">Background Image:</label>
|
||||
<input type="text" id="background-image-url" placeholder="Enter image URL" value="${this.config.backgroundImage}">
|
||||
<div style="display: flex; gap: 10px; margin-top: 5px;">
|
||||
<button id="test-image-button" style="padding: 5px 10px;">Use Test Image</button>
|
||||
<label for="local-image-input" style="padding: 5px 10px; background-color: #ddd; border-radius: 4px; cursor: pointer;">
|
||||
Select Local Image
|
||||
</label>
|
||||
<input type="file" id="local-image-input" accept="image/*" style="display: none;">
|
||||
</div>
|
||||
<div class="background-preview" id="background-preview">
|
||||
${this.config.backgroundImage ? `<img src="${this.config.backgroundImage}" alt="Background preview">` : '<div class="no-image">No image selected</div>'}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-option">
|
||||
<label for="background-opacity">Background Opacity: <span id="opacity-value">${Math.round(this.config.backgroundOpacity * 100)}%</span></label>
|
||||
<input type="range" id="background-opacity" min="0" max="1" step="0.05" value="${this.config.backgroundOpacity}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Tab -->
|
||||
<div class="config-tab-content" id="tab-content">
|
||||
<div class="config-option">
|
||||
<label>Transit Sites:</label>
|
||||
<div style="margin-bottom: 15px;">
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<input type="text" id="site-search-input" placeholder="Search for transit stop..." style="flex: 1; padding: 8px; border: 1px solid #ddd; border-radius: 4px;">
|
||||
<button id="search-site-button" style="padding: 8px 15px; background-color: #0061a1; color: white; border: none; border-radius: 4px; cursor: pointer;">Search</button>
|
||||
<button id="select-from-map-button" style="padding: 8px 15px; background-color: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">Select from Map</button>
|
||||
</div>
|
||||
<div id="site-search-results" style="display: none; max-height: 200px; overflow-y: auto; border: 1px solid #ddd; border-radius: 4px; background: white; margin-top: 5px;"></div>
|
||||
</div>
|
||||
<div id="sites-container">
|
||||
${this.generateSitesHTML()}
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<button id="add-site-button" style="padding: 5px 10px;">Add Site Manually</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Options Tab -->
|
||||
<div class="config-tab-content" id="tab-options">
|
||||
<div class="config-option">
|
||||
<label for="combine-directions">
|
||||
<input type="checkbox" id="combine-directions" ${this.config.combineSameDirection ? 'checked' : ''}>
|
||||
Combine departures going in the same direction
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-modal-footer">
|
||||
<button id="config-save-button">Save</button>
|
||||
<button id="config-cancel-button">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
// Clone the template content into the modal
|
||||
modalContainer.appendChild(template.content.cloneNode(true));
|
||||
|
||||
// Set dynamic values from current config
|
||||
modalContainer.querySelector('#orientation-select').value = this.config.orientation;
|
||||
modalContainer.querySelector('#dark-mode-select').value = this.config.darkMode;
|
||||
modalContainer.querySelector('#background-image-url').value = this.config.backgroundImage || '';
|
||||
modalContainer.querySelector('#background-opacity').value = this.config.backgroundOpacity;
|
||||
modalContainer.querySelector('#opacity-value').textContent = `${Math.round(this.config.backgroundOpacity * 100)}%`;
|
||||
modalContainer.querySelector('#combine-directions').checked = this.config.combineSameDirection;
|
||||
|
||||
// Populate sites
|
||||
const sitesContainer = modalContainer.querySelector('#sites-container');
|
||||
if (sitesContainer) {
|
||||
sitesContainer.innerHTML = this.generateSitesHTML();
|
||||
}
|
||||
|
||||
// Update background preview
|
||||
const preview = modalContainer.querySelector('#background-preview');
|
||||
if (preview && this.config.backgroundImage) {
|
||||
const img = document.createElement('img');
|
||||
img.src = this.config.backgroundImage;
|
||||
img.alt = 'Background preview';
|
||||
preview.innerHTML = '';
|
||||
preview.appendChild(img);
|
||||
}
|
||||
|
||||
document.body.appendChild(modalContainer);
|
||||
|
||||
// Add tab switching functionality
|
||||
@@ -549,33 +479,10 @@ class ConfigManager {
|
||||
if (this.config.backgroundImage && this.config.backgroundImage.trim() !== '') {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'background-overlay';
|
||||
overlay.style.position = 'fixed';
|
||||
overlay.style.top = '0';
|
||||
overlay.style.left = '0';
|
||||
overlay.style.width = '100vw';
|
||||
overlay.style.height = '100vh';
|
||||
overlay.style.backgroundImage = `url(${this.config.backgroundImage})`;
|
||||
overlay.style.backgroundSize = 'cover';
|
||||
overlay.style.backgroundPosition = 'center';
|
||||
overlay.style.opacity = this.config.backgroundOpacity;
|
||||
overlay.style.zIndex = '-1';
|
||||
overlay.style.pointerEvents = 'none';
|
||||
|
||||
// Adjust background rotation based on orientation
|
||||
if (this.config.orientation === 'vertical') {
|
||||
overlay.style.transform = 'rotate(90deg) scale(2)';
|
||||
overlay.style.transformOrigin = 'center center';
|
||||
} else if (this.config.orientation === 'upsidedown') {
|
||||
overlay.style.transform = 'rotate(180deg) scale(1.5)';
|
||||
overlay.style.transformOrigin = 'center center';
|
||||
} else if (this.config.orientation === 'vertical-reverse') {
|
||||
overlay.style.transform = 'rotate(270deg) scale(2)';
|
||||
overlay.style.transformOrigin = 'center center';
|
||||
} else {
|
||||
overlay.style.transform = 'scale(1.2)';
|
||||
overlay.style.transformOrigin = 'center center';
|
||||
}
|
||||
|
||||
overlay.className = `orientation-${this.config.orientation}`;
|
||||
|
||||
// Insert as the first child of body
|
||||
document.body.insertBefore(overlay, document.body.firstChild);
|
||||
}
|
||||
@@ -614,14 +521,14 @@ class ConfigManager {
|
||||
|
||||
return this.config.sites.map((site, index) => `
|
||||
<div class="site-item" data-index="${index}">
|
||||
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||
<div class="config-site-flex">
|
||||
<input type="checkbox" class="site-enabled" ${site.enabled ? 'checked' : ''}>
|
||||
<input type="text" class="site-name" value="${site.name}" placeholder="Site Name" style="flex: 1; margin: 0 5px;">
|
||||
<button class="remove-site-button" style="padding: 2px 5px;">×</button>
|
||||
<input type="text" class="site-name config-site-name-input" value="${site.name}" placeholder="Site Name">
|
||||
<button class="remove-site-button config-btn-remove">×</button>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 5px;">ID:</span>
|
||||
<input type="text" class="site-id" value="${site.id}" placeholder="Site ID" style="width: 100px;">
|
||||
<div class="config-site-id-row">
|
||||
<span class="config-site-id-label">ID:</span>
|
||||
<input type="text" class="site-id config-site-id-input" value="${site.id}" placeholder="Site ID">
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
@@ -644,7 +551,7 @@ class ConfigManager {
|
||||
|
||||
try {
|
||||
resultsContainer.style.display = 'block';
|
||||
resultsContainer.innerHTML = '<div style="padding: 10px; text-align: center; color: #666;">Searching...</div>';
|
||||
resultsContainer.textContent = 'Searching...';
|
||||
|
||||
const response = await fetch(`/api/sites/search?q=${encodeURIComponent(query)}`);
|
||||
|
||||
@@ -656,35 +563,32 @@ class ConfigManager {
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.sites || data.sites.length === 0) {
|
||||
resultsContainer.innerHTML = '<div style="padding: 10px; text-align: center; color: #666;">No sites found. Try a different search term.</div>';
|
||||
resultsContainer.textContent = 'No sites found. Try a different search term.';
|
||||
return;
|
||||
}
|
||||
|
||||
resultsContainer.innerHTML = data.sites.map(site => `
|
||||
<div class="site-search-result" style="padding: 10px; border-bottom: 1px solid #eee; cursor: pointer; transition: background-color 0.2s;"
|
||||
data-site-id="${site.id}" data-site-name="${site.name}">
|
||||
<div style="font-weight: bold; color: #0061a1;">${site.name}</div>
|
||||
<div style="font-size: 0.85em; color: #666;">ID: ${site.id}</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add click handlers to search results
|
||||
resultsContainer.querySelectorAll('.site-search-result').forEach(result => {
|
||||
result.addEventListener('click', () => {
|
||||
const siteId = result.dataset.siteId;
|
||||
const siteName = result.dataset.siteName;
|
||||
this.addSiteFromSearch(siteId, siteName);
|
||||
resultsContainer.innerHTML = '';
|
||||
data.sites.forEach(site => {
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'site-search-result';
|
||||
resultDiv.dataset.siteId = site.id;
|
||||
resultDiv.dataset.siteName = site.name;
|
||||
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.textContent = site.name;
|
||||
const idDiv = document.createElement('div');
|
||||
idDiv.textContent = `ID: ${site.id}`;
|
||||
|
||||
resultDiv.appendChild(nameDiv);
|
||||
resultDiv.appendChild(idDiv);
|
||||
|
||||
resultDiv.addEventListener('click', () => {
|
||||
this.addSiteFromSearch(site.id, site.name);
|
||||
searchInput.value = '';
|
||||
resultsContainer.style.display = 'none';
|
||||
});
|
||||
|
||||
result.addEventListener('mouseenter', () => {
|
||||
result.style.backgroundColor = '#f5f5f5';
|
||||
});
|
||||
|
||||
result.addEventListener('mouseleave', () => {
|
||||
result.style.backgroundColor = 'white';
|
||||
});
|
||||
|
||||
resultsContainer.appendChild(resultDiv);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
@@ -698,7 +602,7 @@ class ConfigManager {
|
||||
errorMessage = `Server error: ${error.message}`;
|
||||
}
|
||||
|
||||
resultsContainer.innerHTML = `<div style="padding: 10px; text-align: center; color: #d32f2f;">Error: ${errorMessage}</div>`;
|
||||
resultsContainer.textContent = `Error: ${errorMessage}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1159,5 +1063,8 @@ class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Export the ConfigManager class for use in other modules
|
||||
// ES module export
|
||||
export { ConfigManager };
|
||||
|
||||
// Keep window reference for backward compatibility
|
||||
window.ConfigManager = ConfigManager;
|
||||
|
||||
Reference in New Issue
Block a user