- Add missing #map CSS styles to fix map container display - Fix auto theme detection by implementing proper system preference detection - Update applyTheme function in all JS files to handle 'auto' theme correctly - System theme changes are now properly detected and applied when in auto mode
729 lines
27 KiB
JavaScript
729 lines
27 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
let authToken = localStorage.getItem('adminToken');
|
|
let allLocations = [];
|
|
let editingRowId = null;
|
|
let sessionTimeout = null;
|
|
let warningTimeout = null;
|
|
|
|
// Session timeout settings (in milliseconds)
|
|
const SESSION_DURATION = 30 * 60 * 1000; // 30 minutes
|
|
const WARNING_TIME = 5 * 60 * 1000; // Show warning 5 minutes before expiry
|
|
|
|
const loginSection = document.getElementById('login-section');
|
|
const adminSection = document.getElementById('admin-section');
|
|
const loginForm = document.getElementById('login-form');
|
|
const loginMessage = document.getElementById('login-message');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
const refreshBtn = document.getElementById('refresh-btn');
|
|
const locationsTableBody = document.getElementById('locations-tbody');
|
|
|
|
// Check if already logged in and session is still valid
|
|
if (authToken) {
|
|
const loginTime = localStorage.getItem('adminLoginTime');
|
|
const now = Date.now();
|
|
|
|
if (loginTime && (now - parseInt(loginTime)) < SESSION_DURATION) {
|
|
showAdminSection();
|
|
loadLocations();
|
|
startSessionTimer();
|
|
} else {
|
|
// Session expired
|
|
logout('Session expired. Please log in again.');
|
|
}
|
|
}
|
|
|
|
// Login form handler
|
|
loginForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const password = document.getElementById('password').value;
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ password })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
authToken = data.token;
|
|
localStorage.setItem('adminToken', authToken);
|
|
localStorage.setItem('adminLoginTime', Date.now().toString());
|
|
showAdminSection();
|
|
loadLocations();
|
|
startSessionTimer();
|
|
} else {
|
|
showMessage(loginMessage, data.error, 'error');
|
|
}
|
|
} catch (error) {
|
|
showMessage(loginMessage, 'Login failed. Please try again.', 'error');
|
|
}
|
|
});
|
|
|
|
// Logout handler
|
|
logoutBtn.addEventListener('click', () => {
|
|
logout('Logged out successfully.');
|
|
});
|
|
|
|
// Refresh button handler
|
|
refreshBtn.addEventListener('click', () => {
|
|
loadLocations();
|
|
});
|
|
|
|
// Tab navigation logic
|
|
const tabs = document.querySelectorAll('.tab-btn');
|
|
const contents = document.querySelectorAll('.tab-content');
|
|
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', function () {
|
|
tabs.forEach(t => t.classList.remove('active'));
|
|
contents.forEach(c => c.classList.remove('active'));
|
|
this.classList.add('active');
|
|
document.querySelector(`#${this.dataset.tab}-tab`).classList.add('active');
|
|
|
|
// Load data for the active tab
|
|
if (this.dataset.tab === 'profanity') {
|
|
loadProfanityWords();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Profanity management handlers
|
|
const addProfanityForm = document.getElementById('add-profanity-form');
|
|
const testProfanityForm = document.getElementById('test-profanity-form');
|
|
const profanityTableBody = document.getElementById('profanity-tbody');
|
|
|
|
if (addProfanityForm) {
|
|
addProfanityForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const word = document.getElementById('new-word').value.trim();
|
|
const severity = document.getElementById('new-severity').value;
|
|
const category = document.getElementById('new-category').value.trim() || 'custom';
|
|
|
|
if (!word) return;
|
|
|
|
await addProfanityWord(word, severity, category);
|
|
});
|
|
}
|
|
|
|
if (testProfanityForm) {
|
|
testProfanityForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const text = document.getElementById('test-text').value.trim();
|
|
|
|
if (!text) return;
|
|
|
|
await testProfanityFilter(text);
|
|
});
|
|
}
|
|
|
|
function showLoginSection() {
|
|
loginSection.style.display = 'block';
|
|
adminSection.style.display = 'none';
|
|
document.getElementById('password').value = '';
|
|
hideMessage(loginMessage);
|
|
}
|
|
|
|
function showAdminSection() {
|
|
loginSection.style.display = 'none';
|
|
adminSection.style.display = 'block';
|
|
}
|
|
|
|
function showMessage(element, message, type) {
|
|
element.textContent = message;
|
|
element.className = `message ${type}`;
|
|
element.style.display = 'block';
|
|
setTimeout(() => hideMessage(element), 5000);
|
|
}
|
|
|
|
function hideMessage(element) {
|
|
element.style.display = 'none';
|
|
}
|
|
|
|
// Session management functions
|
|
function startSessionTimer() {
|
|
clearSessionTimers();
|
|
|
|
// Set warning timer (5 minutes before expiry)
|
|
warningTimeout = setTimeout(() => {
|
|
showSessionWarning();
|
|
}, SESSION_DURATION - WARNING_TIME);
|
|
|
|
// Set logout timer (30 minutes)
|
|
sessionTimeout = setTimeout(() => {
|
|
logout('Session expired due to inactivity.');
|
|
}, SESSION_DURATION);
|
|
}
|
|
|
|
function clearSessionTimers() {
|
|
if (sessionTimeout) {
|
|
clearTimeout(sessionTimeout);
|
|
sessionTimeout = null;
|
|
}
|
|
if (warningTimeout) {
|
|
clearTimeout(warningTimeout);
|
|
warningTimeout = null;
|
|
}
|
|
}
|
|
|
|
function showSessionWarning() {
|
|
const extend = confirm('Your admin session will expire in 5 minutes. Click OK to extend your session, or Cancel to log out now.');
|
|
if (extend) {
|
|
extendSession();
|
|
} else {
|
|
logout('Session ended by user.');
|
|
}
|
|
}
|
|
|
|
function extendSession() {
|
|
// Update login time and restart timers
|
|
localStorage.setItem('adminLoginTime', Date.now().toString());
|
|
startSessionTimer();
|
|
console.log('Admin session extended for 30 minutes');
|
|
}
|
|
|
|
function logout(message = 'Logged out.') {
|
|
clearSessionTimers();
|
|
authToken = null;
|
|
localStorage.removeItem('adminToken');
|
|
localStorage.removeItem('adminLoginTime');
|
|
showLoginSection();
|
|
if (message) {
|
|
showMessage(loginMessage, message, 'error');
|
|
}
|
|
}
|
|
|
|
// Reset session timer on user activity
|
|
function resetSessionTimer() {
|
|
if (authToken && adminSection.style.display !== 'none') {
|
|
const loginTime = localStorage.getItem('adminLoginTime');
|
|
const now = Date.now();
|
|
|
|
// Only reset if we're within the session duration
|
|
if (loginTime && (now - parseInt(loginTime)) < SESSION_DURATION) {
|
|
localStorage.setItem('adminLoginTime', now.toString());
|
|
startSessionTimer();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Listen for user activity to reset session timer
|
|
const activityEvents = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
|
|
let activityTimer = null;
|
|
|
|
activityEvents.forEach(event => {
|
|
document.addEventListener(event, () => {
|
|
// Throttle activity detection to once per minute
|
|
if (!activityTimer) {
|
|
activityTimer = setTimeout(() => {
|
|
resetSessionTimer();
|
|
activityTimer = null;
|
|
}, 60000); // 1 minute throttle
|
|
}
|
|
}, true);
|
|
});
|
|
|
|
// Check session validity periodically
|
|
setInterval(() => {
|
|
if (authToken) {
|
|
const loginTime = localStorage.getItem('adminLoginTime');
|
|
const now = Date.now();
|
|
|
|
if (!loginTime || (now - parseInt(loginTime)) >= SESSION_DURATION) {
|
|
logout('Session expired.');
|
|
}
|
|
}
|
|
}, 60000); // Check every minute
|
|
|
|
async function loadLocations() {
|
|
try {
|
|
const response = await fetch('/api/admin/locations', {
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
// Token expired or invalid
|
|
logout('Session expired. Please log in again.');
|
|
return;
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load locations');
|
|
}
|
|
|
|
allLocations = await response.json();
|
|
updateStats();
|
|
renderLocationsTable();
|
|
} catch (error) {
|
|
console.error('Error loading locations:', error);
|
|
locationsTableBody.innerHTML = '<tr><td colspan="7">Error loading locations</td></tr>';
|
|
}
|
|
}
|
|
|
|
function updateStats() {
|
|
const now = new Date();
|
|
const fortyEightHoursAgo = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
|
|
const activeLocations = allLocations.filter(location =>
|
|
new Date(location.created_at) > fortyEightHoursAgo
|
|
);
|
|
|
|
const persistentLocations = allLocations.filter(location => location.persistent);
|
|
|
|
document.getElementById('total-count').textContent = allLocations.length;
|
|
document.getElementById('active-count').textContent = activeLocations.length;
|
|
document.getElementById('expired-count').textContent = allLocations.length - activeLocations.length;
|
|
document.getElementById('persistent-count').textContent = persistentLocations.length;
|
|
}
|
|
|
|
function renderLocationsTable() {
|
|
if (allLocations.length === 0) {
|
|
locationsTableBody.innerHTML = '<tr><td colspan="7">No locations found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const now = new Date();
|
|
const fortyEightHoursAgo = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
|
|
locationsTableBody.innerHTML = allLocations.map(location => {
|
|
const isActive = new Date(location.created_at) > fortyEightHoursAgo;
|
|
const createdDate = new Date(location.created_at);
|
|
const formattedDate = createdDate.toLocaleString();
|
|
|
|
return `
|
|
<tr data-id="${location.id}" ${editingRowId === location.id ? 'class="edit-row"' : ''}>
|
|
<td>${location.id}</td>
|
|
<td>
|
|
<span class="status-indicator ${isActive ? 'status-active' : 'status-expired'}">
|
|
${isActive ? 'ACTIVE' : 'EXPIRED'}
|
|
</span>
|
|
</td>
|
|
<td class="address-cell" title="${location.address}">${location.address}</td>
|
|
<td>${location.description || ''}</td>
|
|
<td>
|
|
<button class="btn ${location.persistent ? 'btn-save' : 'btn-edit'}"
|
|
onclick="togglePersistent(${location.id})"
|
|
title="${location.persistent ? 'Remove persistent status' : 'Mark as persistent'}">
|
|
${location.persistent ? '🔒 Yes' : '🔓 No'}
|
|
</button>
|
|
</td>
|
|
<td title="${formattedDate}">${getTimeAgo(location.created_at)}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-edit" onclick="editLocation(${location.id})">Edit</button>
|
|
<button class="btn btn-delete" onclick="deleteLocation(${location.id})">Delete</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderEditRow(location) {
|
|
const row = document.querySelector(`tr[data-id="${location.id}"]`);
|
|
const now = new Date();
|
|
const fortyEightHoursAgo = new Date(now.getTime() - 48 * 60 * 60 * 1000);
|
|
const isActive = new Date(location.created_at) > fortyEightHoursAgo;
|
|
const createdDate = new Date(location.created_at);
|
|
const formattedDate = createdDate.toLocaleString();
|
|
|
|
row.innerHTML = `
|
|
<td>${location.id}</td>
|
|
<td>
|
|
<span class="status-indicator ${isActive ? 'status-active' : 'status-expired'}">
|
|
${isActive ? 'ACTIVE' : 'EXPIRED'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<input type="text" class="edit-input" id="edit-address-${location.id}"
|
|
value="${location.address}" placeholder="Address">
|
|
</td>
|
|
<td>
|
|
<input type="text" class="edit-input" id="edit-description-${location.id}"
|
|
value="${location.description || ''}" placeholder="Description">
|
|
</td>
|
|
<td>
|
|
<button class="btn ${location.persistent ? 'btn-save' : 'btn-edit'}"
|
|
onclick="togglePersistent(${location.id})"
|
|
title="${location.persistent ? 'Remove persistent status' : 'Mark as persistent'}">
|
|
${location.persistent ? '🔒 Yes' : '🔓 No'}
|
|
</button>
|
|
</td>
|
|
<td title="${formattedDate}">${getTimeAgo(location.created_at)}</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-save" onclick="saveLocation(${location.id})">Save</button>
|
|
<button class="btn btn-cancel" onclick="cancelEdit()">Cancel</button>
|
|
</div>
|
|
</td>
|
|
`;
|
|
row.className = 'edit-row';
|
|
}
|
|
|
|
window.editLocation = (id) => {
|
|
if (editingRowId) {
|
|
cancelEdit();
|
|
}
|
|
|
|
editingRowId = id;
|
|
const location = allLocations.find(loc => loc.id === id);
|
|
if (location) {
|
|
renderEditRow(location);
|
|
}
|
|
};
|
|
|
|
window.cancelEdit = () => {
|
|
editingRowId = null;
|
|
renderLocationsTable();
|
|
};
|
|
|
|
window.saveLocation = async (id) => {
|
|
const address = document.getElementById(`edit-address-${id}`).value.trim();
|
|
const description = document.getElementById(`edit-description-${id}`).value.trim();
|
|
|
|
if (!address) {
|
|
alert('Address is required');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Geocode the address if it was changed
|
|
let latitude = null;
|
|
let longitude = null;
|
|
|
|
const originalLocation = allLocations.find(loc => loc.id === id);
|
|
if (address !== originalLocation.address) {
|
|
try {
|
|
const geoResponse = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`);
|
|
const geoData = await geoResponse.json();
|
|
|
|
if (geoData && geoData.length > 0) {
|
|
latitude = parseFloat(geoData[0].lat);
|
|
longitude = parseFloat(geoData[0].lon);
|
|
}
|
|
} catch (geoError) {
|
|
console.warn('Geocoding failed, keeping original coordinates');
|
|
latitude = originalLocation.latitude;
|
|
longitude = originalLocation.longitude;
|
|
}
|
|
} else {
|
|
latitude = originalLocation.latitude;
|
|
longitude = originalLocation.longitude;
|
|
}
|
|
|
|
const response = await fetch(`/api/admin/locations/${id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${authToken}`
|
|
},
|
|
body: JSON.stringify({ address, latitude, longitude, description })
|
|
});
|
|
|
|
if (response.ok) {
|
|
editingRowId = null;
|
|
loadLocations(); // Reload to get updated data
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Error updating location: ${error.error}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving location:', error);
|
|
alert('Error saving location. Please try again.');
|
|
}
|
|
};
|
|
|
|
window.togglePersistent = async (id) => {
|
|
const location = allLocations.find(loc => loc.id === id);
|
|
if (!location) return;
|
|
|
|
const newPersistentStatus = !location.persistent;
|
|
const confirmMessage = newPersistentStatus
|
|
? 'Mark this report as persistent? It will not auto-expire after 48 hours.'
|
|
: 'Remove persistent status? This report will expire normally after 48 hours.';
|
|
|
|
if (!confirm(confirmMessage)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/locations/${id}/persistent`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${authToken}`
|
|
},
|
|
body: JSON.stringify({ persistent: newPersistentStatus })
|
|
});
|
|
|
|
if (response.ok) {
|
|
loadLocations(); // Reload to get updated data
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Error updating persistent status: ${error.error}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error toggling persistent status:', error);
|
|
alert('Error updating persistent status. Please try again.');
|
|
}
|
|
};
|
|
|
|
window.deleteLocation = async (id) => {
|
|
if (!confirm('Are you sure you want to delete this location report?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/locations/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
loadLocations(); // Reload the table
|
|
} else {
|
|
const error = await response.json();
|
|
alert(`Error deleting location: ${error.error}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting location:', error);
|
|
alert('Error deleting location. Please try again.');
|
|
}
|
|
};
|
|
|
|
// getTimeAgo and parseUTCDate functions are now available from utils.js
|
|
|
|
// Profanity management functions
|
|
async function loadProfanityWords() {
|
|
if (!profanityTableBody) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/profanity-words', {
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (response.status === 401) {
|
|
logout('Session expired. Please log in again.');
|
|
return;
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
displayProfanityWords(data || []);
|
|
} else {
|
|
console.error('Failed to load profanity words:', data.error);
|
|
profanityTableBody.innerHTML = '<tr><td colspan="6">Failed to load words</td></tr>';
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading profanity words:', error);
|
|
profanityTableBody.innerHTML = '<tr><td colspan="6">Error loading words</td></tr>';
|
|
}
|
|
}
|
|
|
|
function displayProfanityWords(words) {
|
|
if (!profanityTableBody) return;
|
|
|
|
if (words.length === 0) {
|
|
profanityTableBody.innerHTML = '<tr><td colspan="6">No custom words added yet</td></tr>';
|
|
return;
|
|
}
|
|
|
|
profanityTableBody.innerHTML = words.map(word => `
|
|
<tr>
|
|
<td>${word.id}</td>
|
|
<td><code>${escapeHtml(word.word)}</code></td>
|
|
<td><span class="severity-${word.severity}">${word.severity}</span></td>
|
|
<td>${word.category || 'N/A'}</td>
|
|
<td>${new Date(word.created_at).toLocaleDateString()}</td>
|
|
<td>
|
|
<button class="action-btn danger" onclick="deleteProfanityWord(${word.id})">
|
|
🗑️ Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
async function addProfanityWord(word, severity, category) {
|
|
try {
|
|
const response = await fetch('/api/admin/profanity-words', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${authToken}`
|
|
},
|
|
body: JSON.stringify({ word, severity, category })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
// Clear form
|
|
document.getElementById('new-word').value = '';
|
|
document.getElementById('new-category').value = 'custom';
|
|
document.getElementById('new-severity').value = 'medium';
|
|
|
|
// Reload words list
|
|
loadProfanityWords();
|
|
|
|
console.log('Word added successfully');
|
|
} else {
|
|
alert('Failed to add word: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error adding profanity word:', error);
|
|
alert('Error adding word. Please try again.');
|
|
}
|
|
}
|
|
|
|
async function testProfanityFilter(text) {
|
|
const resultsDiv = document.getElementById('test-results');
|
|
if (!resultsDiv) return;
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/test-profanity', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${authToken}`
|
|
},
|
|
body: JSON.stringify({ text })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (response.ok) {
|
|
if (data.analysis && data.analysis.hasProfanity) {
|
|
resultsDiv.className = 'test-results profane';
|
|
resultsDiv.innerHTML = `
|
|
<strong>⚠️ Profanity Detected!</strong><br>
|
|
Detected words: ${data.analysis.matches.map(m => `<code>${escapeHtml(m.word)}</code>`).join(', ')}<br>
|
|
Severity: ${data.analysis.severity}<br>
|
|
Filtered text: "${escapeHtml(data.filtered)}"
|
|
`;
|
|
} else {
|
|
resultsDiv.className = 'test-results clean';
|
|
resultsDiv.innerHTML = '<strong>✅ Text is clean!</strong><br>No profanity detected.';
|
|
}
|
|
} else {
|
|
resultsDiv.className = 'test-results empty';
|
|
resultsDiv.innerHTML = 'Error testing text: ' + data.error;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error testing profanity filter:', error);
|
|
resultsDiv.className = 'test-results empty';
|
|
resultsDiv.innerHTML = 'Error testing text. Please try again.';
|
|
}
|
|
}
|
|
|
|
// Make deleteProfanityWord available globally for onclick handlers
|
|
window.deleteProfanityWord = async function(wordId) {
|
|
if (!confirm('Are you sure you want to delete this word?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/admin/profanity-words/${wordId}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${authToken}` }
|
|
});
|
|
|
|
if (response.ok) {
|
|
loadProfanityWords();
|
|
console.log('Word deleted successfully');
|
|
} else {
|
|
const data = await response.json();
|
|
alert('Failed to delete word: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting profanity word:', error);
|
|
alert('Error deleting word. Please try again.');
|
|
}
|
|
};
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Initialize theme toggle
|
|
initializeTheme();
|
|
});
|
|
|
|
// Theme toggle functionality (shared with main page)
|
|
function initializeTheme() {
|
|
const themeToggle = document.getElementById('theme-toggle');
|
|
const themeIcon = document.querySelector('.theme-icon');
|
|
|
|
if (!themeToggle || !themeIcon) {
|
|
console.warn('Theme toggle elements not found');
|
|
return;
|
|
}
|
|
|
|
// Check for saved theme preference or default to auto (follows system)
|
|
const savedTheme = localStorage.getItem('theme') || 'auto';
|
|
applyTheme(savedTheme);
|
|
|
|
// Update icon based on current theme
|
|
updateThemeIcon(savedTheme, themeIcon);
|
|
|
|
// Add click listener for cycling through themes
|
|
themeToggle.addEventListener('click', () => {
|
|
const currentTheme = localStorage.getItem('theme') || 'auto';
|
|
let newTheme;
|
|
|
|
// Cycle: auto → light → dark → auto
|
|
switch(currentTheme) {
|
|
case 'auto':
|
|
newTheme = 'light';
|
|
break;
|
|
case 'light':
|
|
newTheme = 'dark';
|
|
break;
|
|
case 'dark':
|
|
newTheme = 'auto';
|
|
break;
|
|
default:
|
|
newTheme = 'auto';
|
|
}
|
|
|
|
localStorage.setItem('theme', newTheme);
|
|
applyTheme(newTheme);
|
|
updateThemeIcon(newTheme, themeIcon);
|
|
|
|
console.log(`Theme switched to: ${newTheme}`);
|
|
});
|
|
|
|
// Listen for system theme changes when in auto mode
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
const currentTheme = localStorage.getItem('theme') || 'auto';
|
|
if (currentTheme === 'auto') {
|
|
applyTheme('auto');
|
|
}
|
|
});
|
|
}
|
|
|
|
function applyTheme(theme) {
|
|
if (theme === 'auto') {
|
|
// Detect system preference and apply appropriate theme
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
document.documentElement.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
|
|
} else {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
}
|
|
}
|
|
|
|
function updateThemeIcon(theme, iconElement) {
|
|
switch(theme) {
|
|
case 'auto':
|
|
iconElement.textContent = '🌍'; // Globe (auto)
|
|
break;
|
|
case 'light':
|
|
iconElement.textContent = '☀️'; // Sun (light)
|
|
break;
|
|
case 'dark':
|
|
iconElement.textContent = '🌙'; // Moon (dark)
|
|
break;
|
|
}
|
|
}
|