422 lines
17 KiB
JavaScript
422 lines
17 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const darkModeToggle = document.getElementById('darkModeToggle');
|
|
const body = document.body;
|
|
const celsiusRadio = document.getElementById('celsius');
|
|
const fahrenheitRadio = document.getElementById('fahrenheit');
|
|
|
|
// Apply the cached theme on page load
|
|
if (localStorage.getItem('darkMode') === 'enabled') {
|
|
body.classList.add('dark-mode');
|
|
darkModeToggle.checked = true;
|
|
}
|
|
|
|
darkModeToggle.addEventListener('change', () => {
|
|
if (darkModeToggle.checked) {
|
|
body.classList.add('dark-mode');
|
|
localStorage.setItem('darkMode', 'enabled');
|
|
} else {
|
|
body.classList.remove('dark-mode');
|
|
localStorage.setItem('darkMode', 'disabled');
|
|
}
|
|
});
|
|
|
|
const weatherDisplay = document.getElementById('weather-display');
|
|
const citySearchForm = document.getElementById('city-search-form');
|
|
const cityInput = document.getElementById('city-input');
|
|
const favoriteLocationsList = document.getElementById('favorite-locations-list');
|
|
const searchHistoryContainer = document.getElementById('search-history');
|
|
|
|
const getUnit = () => localStorage.getItem('unit') || 'metric';
|
|
|
|
const getSearchHistory = () => JSON.parse(localStorage.getItem('searchHistory')) || [];
|
|
|
|
const saveSearchHistory = (city) => {
|
|
let history = getSearchHistory();
|
|
history = history.filter(item => item.toLowerCase() !== city.toLowerCase());
|
|
history.unshift(city);
|
|
if (history.length > 5) {
|
|
history.pop();
|
|
}
|
|
localStorage.setItem('searchHistory', JSON.stringify(history));
|
|
displaySearchHistory();
|
|
};
|
|
|
|
const displaySearchHistory = () => {
|
|
const history = getSearchHistory();
|
|
if (history.length > 0) {
|
|
let historyHtml = '<h6>Recent Searches:</h6><div class="d-flex flex-wrap">';
|
|
history.forEach(city => {
|
|
historyHtml += `<a href="#" class="btn btn-sm btn-outline-secondary me-2 mb-2 history-link" data-city="${city}">${city}</a>`;
|
|
});
|
|
historyHtml += '</div>';
|
|
searchHistoryContainer.innerHTML = historyHtml;
|
|
} else {
|
|
searchHistoryContainer.innerHTML = '';
|
|
}
|
|
};
|
|
|
|
const showLoading = () => {
|
|
weatherDisplay.innerHTML = '<div class="card-body text-center"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div><p class="mt-2">Loading weather...</p></div>';
|
|
};
|
|
|
|
const showError = (message) => {
|
|
weatherDisplay.innerHTML = `<div class="card-body text-center"><div class="alert alert-danger">${message}</div></div>`;
|
|
};
|
|
|
|
const getAqiString = (aqi) => {
|
|
switch (aqi) {
|
|
case 1: return 'Good';
|
|
case 2: return 'Fair';
|
|
case 3: return 'Moderate';
|
|
case 4: return 'Poor';
|
|
case 5: return 'Very Poor';
|
|
default: return 'Unknown';
|
|
}
|
|
};
|
|
|
|
const showWeather = (data) => {
|
|
if (!data || !data.name || data.cod === "404") {
|
|
showError(data.message || 'Could not display weather data.');
|
|
return;
|
|
}
|
|
const unit = getUnit();
|
|
const unitSymbol = unit === 'metric' ? '°C' : '°F';
|
|
const iconUrl = `https://openweathermap.org/img/wn/${data.weather[0].icon}@2x.png`;
|
|
|
|
let favoriteStar = '';
|
|
if (isUserLoggedIn) {
|
|
favoriteStar = ` <span id="add-to-favorites" class="favorite-star" style="cursor: pointer; color: orange; font-size: 1.2em;">☆</span>`; // Empty star
|
|
}
|
|
|
|
let aqiHtml = '';
|
|
if (data.aqi) {
|
|
aqiHtml = `<p>Air Quality: ${getAqiString(data.aqi)}</p>`;
|
|
}
|
|
|
|
let forecastHtml = '';
|
|
if (data.forecast && data.forecast.length > 0) {
|
|
forecastHtml += '<div class="mt-4"><h4>5-Day Forecast</h4><div class="row">';
|
|
data.forecast.forEach(day => {
|
|
const dayDate = new Date(day.dt * 1000);
|
|
const dayName = dayDate.toLocaleDateString('en-US', { weekday: 'short' });
|
|
const iconUrl = `https://openweathermap.org/img/wn/${day.icon}.png`;
|
|
forecastHtml += `
|
|
<div class="col text-center forecast-day">
|
|
<p>${dayName}</p>
|
|
<img src="${iconUrl}" alt="${day.description}" title="${day.description}">
|
|
<p>${day.temp.toFixed(1)}${unitSymbol}</p>
|
|
</div>
|
|
`;
|
|
});
|
|
forecastHtml += '</div></div>';
|
|
}
|
|
|
|
weatherDisplay.innerHTML = `
|
|
<div class="card-body text-center" data-lat="${data.coord.lat}" data-lon="${data.coord.lon}" data-city="${data.name}">
|
|
<h2 class="card-title">${data.name}${favoriteStar}</h2>
|
|
<img src="${iconUrl}" alt="Weather icon" class="weather-icon">
|
|
<h3>${data.main.temp}${unitSymbol}</h3>
|
|
<p class="text-muted">${data.weather[0].description}</p>
|
|
<p>Humidity: ${data.main.humidity}%</p>
|
|
${aqiHtml}
|
|
${forecastHtml}
|
|
</div>
|
|
<div id="weather-map" style="height: 400px; margin-top: 20px;"></div>
|
|
`;
|
|
|
|
initMap(data.coord.lat, data.coord.lon);
|
|
saveSearchHistory(data.name);
|
|
};
|
|
|
|
const initMap = (lat, lon) => {
|
|
const apiKey = '2350ecafed26b115b1557305d956630d';
|
|
|
|
const mapContainer = document.getElementById('weather-map');
|
|
if (!mapContainer || mapContainer._leaflet_id) {
|
|
// If map is already initialized, just update its view
|
|
if (mapContainer._leaflet_id) {
|
|
mapContainer._leaflet_map.setView([lat, lon], 10);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const map = L.map('weather-map').setView([lat, lon], 10);
|
|
mapContainer._leaflet_map = map; // Store map instance
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
|
}).addTo(map);
|
|
|
|
const tempLayer = L.tileLayer(`https://tile.openweathermap.org/map/temp_new/{z}/{x}/{y}.png?appid=${apiKey}`, {
|
|
attribution: 'Map data © <a href="https://openweathermap.org">OpenWeatherMap</a>'
|
|
});
|
|
|
|
const cloudsLayer = L.tileLayer(`https://tile.openweathermap.org/map/clouds_new/{z}/{x}/{y}.png?appid=${apiKey}`, {
|
|
attribution: 'Map data © <a href="https://openweathermap.org">OpenWeatherMap</a>'
|
|
});
|
|
|
|
const precipitationLayer = L.tileLayer(`https://tile.openweathermap.org/map/precipitation_new/{z}/{x}/{y}.png?appid=${apiKey}`, {
|
|
attribution: 'Map data © <a href="https://openweathermap.org">OpenWeatherMap</a>'
|
|
});
|
|
|
|
const baseMaps = {
|
|
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png')
|
|
};
|
|
|
|
const overlayMaps = {
|
|
"Temperature": tempLayer,
|
|
"Clouds": cloudsLayer,
|
|
"Precipitation": precipitationLayer
|
|
};
|
|
|
|
L.control.layers(baseMaps, overlayMaps).addTo(map);
|
|
overlayMaps.Temperature.addTo(map);
|
|
};
|
|
|
|
const fetchWeatherByCoords = (lat, lon) => {
|
|
const unit = getUnit();
|
|
showLoading();
|
|
fetch(`weather.php?lat=${lat}&lon=${lon}&units=${unit}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error) {
|
|
showError(data.error);
|
|
} else {
|
|
showWeather(data);
|
|
}
|
|
})
|
|
.catch(() => showError('Failed to fetch weather data.'));
|
|
};
|
|
|
|
const fetchWeatherByCity = (city) => {
|
|
const unit = getUnit();
|
|
showLoading();
|
|
fetch(`weather.php?city=${encodeURIComponent(city)}&units=${unit}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.error || data.cod === "404") {
|
|
showError(data.error || data.message);
|
|
} else {
|
|
showWeather(data);
|
|
}
|
|
})
|
|
.catch(() => showError('Failed to fetch weather data.'));
|
|
};
|
|
|
|
const saveLocation = () => {
|
|
const weatherCardBody = weatherDisplay.querySelector('.card-body');
|
|
if (!weatherCardBody) return;
|
|
|
|
const city = weatherCardBody.dataset.city;
|
|
const lat = weatherCardBody.dataset.lat;
|
|
const lon = weatherCardBody.dataset.lon;
|
|
|
|
if (!city || !lat || !lon) {
|
|
alert('Could not get location data to save.');
|
|
return;
|
|
}
|
|
|
|
fetch('save_location.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ city: city, lat: lat, lon: lon }),
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert('Location saved!');
|
|
location.reload(); // Reload to see the updated list
|
|
} else {
|
|
alert(data.message || 'Could not save location.');
|
|
}
|
|
})
|
|
.catch(() => alert('An error occurred while saving the location.'));
|
|
};
|
|
|
|
const deleteLocation = (locationId, listItemElement) => {
|
|
if (!confirm('Are you sure you want to delete this location?')) return;
|
|
|
|
fetch('delete_location.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({ location_id: locationId }),
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
listItemElement.remove();
|
|
alert('Location deleted!');
|
|
} else {
|
|
alert(data.message || 'Could not delete location.');
|
|
}
|
|
})
|
|
.catch(() => alert('An error occurred.'));
|
|
};
|
|
|
|
const handleSubscription = (locationId, time, subscriptionId, listItem) => {
|
|
const isSubscribing = !!locationId;
|
|
const url = isSubscribing ? 'subscribe_weather.php' : 'unsubscribe_weather.php';
|
|
const body = isSubscribing ? { location_id: locationId, time: time } : { subscription_id: subscriptionId };
|
|
|
|
fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body)
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert(data.message);
|
|
// Dynamically update the UI
|
|
const controlsDiv = listItem.querySelector('.subscription-controls');
|
|
if (isSubscribing) {
|
|
const newTime = new Date(`1970-01-01T${time}Z`);
|
|
const formattedTime = newTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZone: 'UTC' });
|
|
controlsDiv.innerHTML = `
|
|
<small>Subscribed for daily alerts at ${formattedTime}.</small>
|
|
<button class="btn btn-sm btn-warning unsubscribe-btn" data-id="${data.subscription_id}">Unsubscribe</button>
|
|
`;
|
|
} else {
|
|
controlsDiv.innerHTML = `
|
|
<div class="input-group input-group-sm">
|
|
<input type="time" class="form-control alert-time-input" value="08:00">
|
|
<button class="btn btn-sm btn-primary subscribe-btn">Subscribe</button>
|
|
</div>
|
|
`;
|
|
}
|
|
} else {
|
|
alert(data.message || 'An error occurred.');
|
|
}
|
|
})
|
|
.catch(() => alert('A network error occurred.'));
|
|
};
|
|
|
|
citySearchForm.addEventListener('submit', (e) => {
|
|
e.preventDefault();
|
|
const city = cityInput.value.trim();
|
|
if (city) {
|
|
fetchWeatherByCity(city);
|
|
}
|
|
});
|
|
|
|
weatherDisplay.addEventListener('click', (e) => {
|
|
if (e.target.id === 'add-to-favorites') {
|
|
saveLocation();
|
|
}
|
|
});
|
|
|
|
if (favoriteLocationsList) {
|
|
favoriteLocationsList.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const listItem = e.target.closest('li');
|
|
if (e.target.classList.contains('subscribe-btn')) {
|
|
const locationId = listItem.dataset.locationId;
|
|
const timeInput = listItem.querySelector('.alert-time-input');
|
|
handleSubscription(locationId, timeInput.value, null, listItem);
|
|
} else if (e.target.classList.contains('unsubscribe-btn')) {
|
|
const subscriptionId = e.target.dataset.id;
|
|
handleSubscription(null, null, subscriptionId, listItem);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (searchHistoryContainer) {
|
|
searchHistoryContainer.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
if (e.target.classList.contains('history-link')) {
|
|
const city = e.target.dataset.city;
|
|
fetchWeatherByCity(city);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
const getHistoryBtn = document.getElementById('get-history-btn');
|
|
if (getHistoryBtn) {
|
|
getHistoryBtn.addEventListener('click', () => {
|
|
const city = document.querySelector('#weather-display .card-title')?.textContent;
|
|
const date = document.getElementById('history-date').value;
|
|
|
|
if (!city) {
|
|
alert('Please search for a city first.');
|
|
return;
|
|
}
|
|
|
|
if (!date) {
|
|
alert('Please select a date.');
|
|
return;
|
|
}
|
|
|
|
fetch(`get_history.php?city=${encodeURIComponent(city)}&date=${date}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
const historyDisplay = document.getElementById('history-display');
|
|
if (data.error) {
|
|
historyDisplay.innerHTML = `<div class="alert alert-danger">${data.error}</div>`;
|
|
} else if (data.length === 0) {
|
|
historyDisplay.innerHTML = `<div class="alert alert-info">No historical data found for this date.</div>`;
|
|
} else {
|
|
let table = '<table class="table table-striped"><thead><tr><th>Time</th><th>Temp</th><th>Humidity</th><th>Description</th></tr></thead><tbody>';
|
|
data.forEach(record => {
|
|
const recordDate = new Date(record.timestamp + ' UTC');
|
|
table += `<tr>
|
|
<td>${recordDate.toLocaleTimeString()}</td>
|
|
<td>${record.temperature}°C</td>
|
|
<td>${record.humidity}%</td>
|
|
<td>${record.description}</td>
|
|
</tr>`;
|
|
});
|
|
table += '</tbody></table>';
|
|
historyDisplay.innerHTML = table;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
const historyDisplay = document.getElementById('history-display');
|
|
historyDisplay.innerHTML = `<div class="alert alert-danger">Failed to fetch historical data.</div>`;
|
|
});
|
|
});
|
|
}
|
|
|
|
// Set initial unit toggle state
|
|
if (getUnit() === 'imperial') {
|
|
fahrenheitRadio.checked = true;
|
|
} else {
|
|
celsiusRadio.checked = true;
|
|
}
|
|
|
|
// Add event listeners for unit switching
|
|
[celsiusRadio, fahrenheitRadio].forEach(radio => {
|
|
radio.addEventListener('change', () => {
|
|
const selectedUnit = fahrenheitRadio.checked ? 'imperial' : 'metric';
|
|
localStorage.setItem('unit', selectedUnit);
|
|
const currentCity = document.querySelector('#weather-display .card-title')?.textContent;
|
|
if (currentCity) {
|
|
fetchWeatherByCity(currentCity);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Initial weather fetch
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
position => {
|
|
fetchWeatherByCoords(position.coords.latitude, position.coords.longitude);
|
|
},
|
|
() => {
|
|
displaySearchHistory();
|
|
// Don't show an error, just let the user search.
|
|
}
|
|
);
|
|
} else {
|
|
displaySearchHistory();
|
|
}
|
|
|
|
// Initial display of search history
|
|
displaySearchHistory();
|
|
});
|