forked from Ahmedooode/weather-app
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjavascript.js
More file actions
345 lines (299 loc) · 13.7 KB
/
javascript.js
File metadata and controls
345 lines (299 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
// --- DOM Element Selection ---
const searchBox = document.querySelector(".search input.cityName");
const searchBtn = document.querySelector(".search button");
const body = document.body;
const cloudsContainer = document.querySelector('.clouds-bg');
const rainContainer = document.querySelector('.rain-bg');
const loader = document.querySelector('.loader');
// Main weather display
const weatherDisplay = document.querySelector(".weather-main .weather");
const tempElement = document.querySelector(".temp");
const cityElement = document.querySelector(".city");
const dateTimeElement = document.querySelector(".date-time");
const weatherIcon = document.querySelector(".weather-icon");
// Details panel
const detailsList = document.querySelector(".details-list");
const feelsLikeElement = document.querySelector(".feels-like");
const humidityElement = document.querySelector(".humidity");
const windElement = document.querySelector(".wind");
const uvIndexElement = document.querySelector(".uv-index");
const visibilityElement = document.querySelector(".visibility");
const sunriseElement = document.querySelector(".sunrise");
const sunsetElement = document.querySelector(".sunset");
// Forecast panel
const forecastSection = document.querySelector(".forecast-list");
const forecastListContainer = document.querySelector(".forecast-list ul");
const errorContainer = document.querySelector(".error");
// --- Configuration ---
// IMPORTANT: Your API key from Visual Crossing
// 1. Get your own free API key from https://www.visualcrossing.com/weather-api
// NOTE: Do not commit your real API key to a public repository.
const apiKey = "P3BPWX6G9ESRZ6TV76DVUY6WA";
const apiHost = "https://weather.visualcrossing.com";
// Map API icons to our local image files
// --- State Management ---
let isInitialLoad = true;
const iconMap = {
'partly-cloudy-day': 'images/clouds.png',
'partly-cloudy-night': 'images/clouds.png',
'cloudy': 'images/clouds.png',
'clear-day': 'images/clear.png',
'clear-night': 'images/clear.png', // Could have a moon icon
'rain': 'images/rain.png',
'snow': 'images/snow.png',
'wind': 'images/wind.png',
'fog': 'images/mist.png'
};
// --- Core Functions ---
/**
* Fetches and displays weather data for a given city.
* @param {string} location The name of the city, "lat,lon", or "auto".
* @param {string} [locationName] The display name for the location, to override API's resolvedAddress.
*/
async function checkWeather(location, locationName) { // Returns true on success, false on failure
// Check for API key.
if (apiKey === "YOUR_API_KEY_HERE" || !apiKey) {
showError("API key is missing. Please add it to javascript.js");
loader.style.display = 'none';
return false;
}
// 1. Set up loading state
weatherDisplay.classList.remove("visible");
detailsList.classList.remove("visible");
forecastSection.classList.remove("visible");
hideError();
loader.style.display = 'flex';
let loadingText = `Fetching weather for ${locationName || location}...`;
if (location === 'auto') {
loadingText = "Fetching weather for your location...";
}
loader.querySelector('p').textContent = loadingText;
const apiUrl = `${apiHost}/VisualCrossingWebServices/rest/services/timeline/${encodeURIComponent(location)}?unitGroup=metric&key=${apiKey}&contentType=json`;
try {
const response = await fetch(apiUrl);
if (!response.ok) {
// The API returns plain text errors for non-200 responses.
// This is crucial for debugging issues like an invalid API key.
const errorText = await response.text();
console.error("Visual Crossing API Error:", errorText);
// Show a more specific error to the user.
showError(`Could not fetch weather. Reason: ${errorText}`);
return false;
}
const data = await response.json();
updateWeatherUI(data, locationName);
return true;
} catch (error) {
console.error("Fetch API Error:", error);
if (isInitialLoad && error instanceof TypeError) {
// On initial load, a network error is likely due to an ad-blocker.
// Instead of a harsh error, we'll just invite the user to search manually.
console.warn("Initial weather fetch failed, likely due to a network block. The app is ready for manual search.");
loader.querySelector('p').textContent = "Auto-detection failed. Please use the search bar.";
} else {
// For manual searches or other types of errors, show the full error message.
let message = "Unable to connect. Please check your network.";
if (error instanceof TypeError) {
message = "Network request failed. This may be due to an ad-blocker, firewall, or a network issue.";
}
showError(message);
}
return false;
} finally {
// 3. Hide loader regardless of outcome
loader.style.display = 'none';
isInitialLoad = false; // The first attempt is over.
}
}
/**
* Updates the entire UI with the fetched weather data.
* @param {object} data The weather data object from the API.
* @param {string} [locationName] The display name for the location.
*/
function updateWeatherUI(data, locationName) {
const { currentConditions, timezone, resolvedAddress } = data;
// Get the current date and time in the location's timezone.
// The API's `currentConditions.datetime` is the observation time, not the real-time clock time.
const now = new Date();
const timeStr = new Intl.DateTimeFormat('en-GB', { hour: '2-digit', minute: '2-digit', timeZone: timezone }).format(now);
const dateStr = formatDate(now.getTime() / 1000, timezone);
// Update main display
tempElement.innerHTML = `${Math.round(currentConditions.temp)}°`;
// For simplicity, show the main city name from the resolved address.
// This is cleaner but less specific than showing the full address.
// e.g., shows "New York" instead of "New York, NY, United States".
cityElement.innerHTML = locationName || resolvedAddress.split(',')[0];
dateTimeElement.innerHTML = `${timeStr} - ${dateStr}`;
// Update details panel
feelsLikeElement.innerHTML = `${Math.round(currentConditions.feelslike)}°`;
humidityElement.innerHTML = `${currentConditions.humidity}%`;
windElement.innerHTML = `${currentConditions.windspeed} km/h`;
uvIndexElement.innerHTML = currentConditions.uvindex;
visibilityElement.innerHTML = `${currentConditions.visibility} km`;
// Use pre-formatted time strings from API and slice to HH:MM
sunriseElement.innerHTML = currentConditions.sunrise.slice(0, 5);
sunsetElement.innerHTML = currentConditions.sunset.slice(0, 5);
// Update icon and background
updateAppearance(currentConditions);
// Update the 5-day forecast
updateForecastUI(data.days);
// 2. Show the weather info
body.classList.add('weather-active');
weatherDisplay.classList.add("visible");
detailsList.classList.add("visible");
forecastSection.classList.add("visible");
}
/**
* Updates the background, icon, and star effect based on weather.
* @param {object} conditions The currentConditions object from the API.
*/
function updateAppearance(conditions) {
// Reset background effects
cloudsContainer.classList.remove("visible");
rainContainer.classList.remove("visible");
const conditionIcon = conditions.icon; // e.g., "partly-cloudy-day", "clear-night", "rain"
// Set day/night theme based on API response (icon name)
// Force night theme if icon indicates night or if UV index is 0 (a reliable indicator of no sun)
if (conditionIcon.includes('night') || conditions.uvindex === 0) {
body.classList.add('night');
} else {
body.classList.remove('night');
}
// Set dynamic background effects
if (cloudsContainer && conditionIcon.includes('cloudy')) {
cloudsContainer.classList.add('visible');
}
if (rainContainer && conditionIcon.includes('rain')) {
rainContainer.classList.add('visible');
}
// Use a default if the specific icon isn't in our map
weatherIcon.src = iconMap[conditionIcon] || 'images/clear.png';
}
/**
* Formats a UNIX epoch timestamp into a human-readable date string for a specific timezone.
* @param {number} epochSeconds The timestamp in seconds.
* @param {string} timeZone The IANA timezone name (e.g., "America/New_York").
* @returns {string} The formatted date string (e.g., "Monday, 1 Sep").
*/
function formatDate(epochSeconds, timeZone) {
const date = new Date(epochSeconds * 1000);
const dateOptions = { weekday: 'long', day: 'numeric', month: 'short', timeZone };
return new Intl.DateTimeFormat('en-US', dateOptions).format(date);
}
/**
* Populates the 5-day forecast section.
* @param {Array} days The array of forecast days from the API.
*/
function updateForecastUI(days) {
if (!forecastListContainer) return;
// Clear previous forecast
forecastListContainer.innerHTML = '';
// Loop through the next 5 days (skip today, which is index 0)
for (let i = 1; i < 6 && i < days.length; i++) {
const day = days[i];
const dayName = new Date(day.datetimeEpoch * 1000).toLocaleDateString('en-US', { weekday: 'short' });
const iconSrc = iconMap[day.icon] || 'images/clear.png';
const highTemp = `${Math.round(day.tempmax)}°`;
const lowTemp = `${Math.round(day.tempmin)}°`;
const forecastItemHTML = `
<li>
<span class="day-name">${dayName}</span>
<img src="${iconSrc}" alt="${day.conditions}" class="forecast-icon">
<span class="day-temp">${highTemp} / ${lowTemp}</span>
</li>
`;
forecastListContainer.insertAdjacentHTML('beforeend', forecastItemHTML);
}
}
/**
* Shows an error message.
* @param {string} message The error message to display.
*/
function showError(message) {
errorContainer.querySelector('p').textContent = message;
errorContainer.style.display = "block";
weatherDisplay.classList.remove("visible");
detailsList.classList.remove("visible");
forecastSection.classList.remove("visible");
}
/**
* Hides the error message.
*/
function hideError() {
errorContainer.style.display = "none";
}
/**
* Sets the initial day/night theme based on the user's local time.
* This determines the main background color before the first API call.
*/
function setInitialTheme() {
const currentHour = new Date().getHours();
// Consider night time from 6 PM (18:00) to 6 AM (05:59)
if (currentHour >= 18 || currentHour < 6) {
body.classList.add('night');
} else {
body.classList.remove('night');
}
}
/**
* Gets the user's approximate location by parsing their browser's timezone.
* This is requested once on page load to show local weather.
*/
async function getInitialWeather() {
try {
// Intl.DateTimeFormat().resolvedOptions().timeZone gives the IANA timezone name (e.g., "America/New_York")
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
console.log(`User's timezone is: ${userTimezone}`);
const parts = userTimezone.split('/');
// Take the last part of the timezone string (e.g., "Khartoum" from "Africa/Khartoum")
const potentialCity = parts[parts.length - 1].replace(/_/g, ' ');
// Check if the extracted name is a reasonable city name and not something generic like 'UTC' or 'GMT'
if (parts.length > 1 && potentialCity) {
console.log(`Attempting to get weather for extracted city: "${potentialCity}"`);
const success = await checkWeather(potentialCity);
// If the extracted city name fails (e.g., it's not in the API's database), fall back to 'auto'
if (!success) {
console.warn(`Failed to get weather for "${potentialCity}". Falling back to 'auto' detection.`);
hideError(); // Hide the previous error before retrying
await checkWeather('auto');
}
} else {
// If timezone is simple (e.g., "UTC"), use 'auto' directly.
console.log("Timezone is not in Region/City format. Using 'auto' detection.");
await checkWeather('auto');
}
} catch (error) {
// This might fail if Intl API is not supported, though it's highly unlikely in modern browsers.
console.warn("Could not determine timezone, falling back to weather API's 'auto' feature.", error);
await checkWeather('auto');
}
}
/**
* Creates the star elements for the background effect.
* This should only run once on page load.
*/
function initializeStars(count) {
const starsContainer = document.querySelector('.stars');
if (!starsContainer) return;
for (let i = 0; i < count; i++) {
const star = document.createElement('div');
star.className = 'star';
star.style.left = `${Math.random() * 100}%`;
star.style.top = `${Math.random() * 100}%`;
const size = Math.random() * 2 + 1;
star.style.width = `${size}px`;
star.style.height = `${size}px`;
star.style.animationDelay = `${Math.random() * 3}s`;
star.style.animationDuration = `${Math.random() * 2 + 1.5}s`;
starsContainer.appendChild(star);
}
}
// --- Event Listeners ---
searchBtn.closest('form').addEventListener("submit", (e) => {
e.preventDefault(); // Prevent page reload
if (searchBox.value) checkWeather(searchBox.value.trim());
});
// --- Initial Load ---
setInitialTheme(); // Set theme based on user's local time for the initial view.
initializeStars(150); // Create stars once when the page loads.
getInitialWeather(); // Get weather for user's location on start.