From 3c750e6446f521c7aba3537d0fb12dacfeff4218 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Tue, 11 Feb 2025 15:00:39 -0500 Subject: [PATCH 1/9] initial commit --- index.html | 24 ++++++++++++++++++++++++ main.js | 1 + style.css | 7 +++++++ 3 files changed, 32 insertions(+) create mode 100644 index.html create mode 100644 main.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 00000000..511d6077 --- /dev/null +++ b/index.html @@ -0,0 +1,24 @@ + + + + + + My Weather Project + + + + + + +
+

Weather Project

+
+ + + + + + diff --git a/main.js b/main.js new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/main.js @@ -0,0 +1 @@ + diff --git a/style.css b/style.css new file mode 100644 index 00000000..49cc16a4 --- /dev/null +++ b/style.css @@ -0,0 +1,7 @@ +body { + background-color: #f8f9fa; +} + +.container { + padding: 20px; +} From db66c5fc5319739751542417f2b883cd86a892a8 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Tue, 11 Feb 2025 15:11:46 -0500 Subject: [PATCH 2/9] add form structure and submit logic --- index.html | 6 ++++++ main.js | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/index.html b/index.html index 511d6077..a115d066 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,12 @@

Weather Project

+
+
+ + +
+
diff --git a/main.js b/main.js index 8b137891..1d1480b0 100644 --- a/main.js +++ b/main.js @@ -1 +1,7 @@ +const form = document.getElementsByTagName("form")[0]; +form.addEventListener("submit", (e) => { + e.preventDefault(); + const input = document.querySelector(".input-control").value; + document.querySelector(".input-control").value = ""; +}); From 1009f4ab6f68e35a93dde24dcf3bddf194a25117 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Tue, 11 Feb 2025 20:03:49 -0500 Subject: [PATCH 3/9] add renderWeather and getWeatherData functionality, first pass getForecastData --- index.html | 1 + main.js | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/index.html b/index.html index a115d066..1f84bb1a 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,7 @@

Weather Project

+
diff --git a/main.js b/main.js index 1d1480b0..59eb8db7 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,56 @@ const form = document.getElementsByTagName("form")[0]; +const apiKey = `c8d0a8706ef69da2623e093b018f765f`; +let weatherData; +let forecastData; -form.addEventListener("submit", (e) => { +const getWeatherData = async (query) => { + try { + const response = await fetch( + `https://api.openweathermap.org/data/2.5/weather?q=${query}&appid=${apiKey}&units=imperial` + ); + if (!response.ok) { + console.log("Error"); + } else { + const data = await response.json(); + return { + name: data.name, + temp: data.main.temp, + weather: data.weather[0], + }; + } + } catch (error) { + console.log(error); + } +}; + +const getForecastData = async (query) => { + try { + const response = await fetch( + `https://api.openweathermap.org/data/2.5/forecast?q=${query}&appid=${apiKey}` + ); + if (!response.ok) { + console.log("Error"); + } else { + const data = await response.json(); + return data.list; + } + } catch (error) { + console.log(error); + } +}; + +const renderWeather = () => { + const weatherDiv = document.querySelector(".weather-container"); + weatherDiv.replaceChildren(); + const template = `

${weatherData.name}

${weatherData.temp}

${weatherData.weather.description}

`; + weatherDiv.innerHTML = template; +}; + +form.addEventListener("submit", async (e) => { e.preventDefault(); const input = document.querySelector(".input-control").value; + weatherData = await getWeatherData(input); + forecastData = await getForecastData(input); + renderWeather(); document.querySelector(".input-control").value = ""; }); From b2ae2ce75a5e0ad049cdc5683fa41ce601601881 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Wed, 12 Feb 2025 20:11:00 -0500 Subject: [PATCH 4/9] format forecastData --- main.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/main.js b/main.js index 59eb8db7..28ed8d90 100644 --- a/main.js +++ b/main.js @@ -26,13 +26,26 @@ const getWeatherData = async (query) => { const getForecastData = async (query) => { try { const response = await fetch( - `https://api.openweathermap.org/data/2.5/forecast?q=${query}&appid=${apiKey}` + `https://api.openweathermap.org/data/2.5/forecast?q=${query}&appid=${apiKey}&units=imperial` ); if (!response.ok) { console.log("Error"); } else { const data = await response.json(); - return data.list; + console.log(data.list); + const compiledForecast = data.list.reduce((acc, curr, index) => { + if (index % 8 === 0) { + acc.push({ + date_info: { dt: curr.dt, dt_txt: curr.dt_txt }, + temp: curr.main.temp, + weather: curr.weather[0], + }); + return acc; + } else { + return acc; + } + }, []); + return compiledForecast; } } catch (error) { console.log(error); From 4f495f9c8fc35b9b6d0342dc78d66781fb0c9ab6 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Thu, 13 Feb 2025 10:42:44 -0500 Subject: [PATCH 5/9] add renderForecast func, format Date --- index.html | 3 +++ main.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/index.html b/index.html index 1f84bb1a..1402ea74 100644 --- a/index.html +++ b/index.html @@ -20,7 +20,10 @@

Weather Project

+ +
+
diff --git a/main.js b/main.js index 28ed8d90..67858720 100644 --- a/main.js +++ b/main.js @@ -37,6 +37,9 @@ const getForecastData = async (query) => { if (index % 8 === 0) { acc.push({ date_info: { dt: curr.dt, dt_txt: curr.dt_txt }, + day: new Date(curr.dt_txt).toLocaleDateString("en-US", { + weekday: "long", + }), temp: curr.main.temp, weather: curr.weather[0], }); @@ -59,11 +62,41 @@ const renderWeather = () => { weatherDiv.innerHTML = template; }; +function renderForecast() { + const forecastsDiv = document.querySelector(".forecast-container"); + forecastsDiv.replaceChildren(); + + forecastData.forEach((forecast) => { + const forecastDiv = document.createElement("div"); + const dateP = document.createElement("p"); + const weatherImg = document.createElement("img"); + const tempP = document.createElement("p"); + const weatherP = document.createElement("p"); + + weatherImg.setAttribute( + "src", + `https://openweathermap.org/img/wn/${forecast.weather.icon}@2x.png` + ); + + dateP.textContent = `${forecast.day}`; + tempP.textContent = `${forecast.temp}`; + weatherP.textContent = `${forecast.weather.description}`; + + forecastDiv.appendChild(dateP); + forecastDiv.appendChild(weatherImg); + forecastDiv.appendChild(tempP); + forecastDiv.appendChild(weatherP); + + forecastsDiv.appendChild(forecastDiv); + }); +} + form.addEventListener("submit", async (e) => { e.preventDefault(); const input = document.querySelector(".input-control").value; weatherData = await getWeatherData(input); forecastData = await getForecastData(input); renderWeather(); + renderForecast(); document.querySelector(".input-control").value = ""; }); From c33dba9fe7293f6e00b4f1ded16ffee140a94cab Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Thu, 13 Feb 2025 10:58:16 -0500 Subject: [PATCH 6/9] style weather and forecast containers --- index.html | 2 +- main.js | 67 +++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 60 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index 1402ea74..d06d2e09 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,7 @@ -
+

Weather Project

diff --git a/main.js b/main.js index 67858720..39478f5f 100644 --- a/main.js +++ b/main.js @@ -32,7 +32,6 @@ const getForecastData = async (query) => { console.log("Error"); } else { const data = await response.json(); - console.log(data.list); const compiledForecast = data.list.reduce((acc, curr, index) => { if (index % 8 === 0) { acc.push({ @@ -58,20 +57,71 @@ const getForecastData = async (query) => { const renderWeather = () => { const weatherDiv = document.querySelector(".weather-container"); weatherDiv.replaceChildren(); - const template = `

${weatherData.name}

${weatherData.temp}

${weatherData.weather.description}

`; - weatherDiv.innerHTML = template; + weatherDiv.classList.add("d-flex", "justify-content-center", "mt-4"); + + const card = document.createElement("div"); + card.classList.add("card", "text-center", "col-12", "col-md-4", "p-0"); + + const cardBody = document.createElement("div"); + cardBody.classList.add("card-body"); + + const cityName = document.createElement("h2"); + cityName.classList.add("card-title", "mb-3"); + cityName.textContent = weatherData.name; + + const weatherImg = document.createElement("img"); + weatherImg.setAttribute( + "src", + `https://openweathermap.org/img/wn/${weatherData.weather.icon}@2x.png` + ); + weatherImg.classList.add("card-img"); + + const tempP = document.createElement("p"); + tempP.classList.add("card-text", "fs-2", "fw-bold", "mb-2"); + tempP.textContent = `${weatherData.temp}°F`; + + const weatherP = document.createElement("p"); + weatherP.classList.add("card-text", "text-muted"); + weatherP.textContent = weatherData.weather.description; + + cardBody.appendChild(cityName); + cardBody.appendChild(weatherImg); + cardBody.appendChild(tempP); + cardBody.appendChild(weatherP); + + card.appendChild(cardBody); + weatherDiv.appendChild(card); }; function renderForecast() { const forecastsDiv = document.querySelector(".forecast-container"); forecastsDiv.replaceChildren(); + forecastsDiv.classList.add("row", "gap-3", "justify-content-center", "mt-4"); forecastData.forEach((forecast) => { const forecastDiv = document.createElement("div"); + forecastDiv.classList.add( + "card", + "col-12", + "col-md-2", + "text-center", + "p-0" + ); + + const cardBody = document.createElement("div"); + cardBody.classList.add("card-body"); + const dateP = document.createElement("p"); + dateP.classList.add("card-title", "fw-bold"); + const weatherImg = document.createElement("img"); + weatherImg.classList.add("card-img"); + const tempP = document.createElement("p"); + tempP.classList.add("card-text", "fs-4"); + const weatherP = document.createElement("p"); + weatherP.classList.add("card-text", "text-muted"); weatherImg.setAttribute( "src", @@ -79,14 +129,15 @@ function renderForecast() { ); dateP.textContent = `${forecast.day}`; - tempP.textContent = `${forecast.temp}`; + tempP.textContent = `${forecast.temp}°F`; weatherP.textContent = `${forecast.weather.description}`; - forecastDiv.appendChild(dateP); - forecastDiv.appendChild(weatherImg); - forecastDiv.appendChild(tempP); - forecastDiv.appendChild(weatherP); + cardBody.appendChild(dateP); + cardBody.appendChild(weatherImg); + cardBody.appendChild(tempP); + cardBody.appendChild(weatherP); + forecastDiv.appendChild(cardBody); forecastsDiv.appendChild(forecastDiv); }); } From 6ca9ca1ab03914afb5fad9e98b9786042b0db1d4 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Thu, 13 Feb 2025 11:43:48 -0500 Subject: [PATCH 7/9] add error handling and form validation --- index.html | 9 ++++++++- main.js | 33 +++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index d06d2e09..1b30df35 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,14 @@

Weather Project

- +
+ +
Please enter a valid city name!
+
diff --git a/main.js b/main.js index 39478f5f..19279029 100644 --- a/main.js +++ b/main.js @@ -1,4 +1,5 @@ const form = document.getElementsByTagName("form")[0]; +const inputField = document.querySelector(".input-control"); const apiKey = `c8d0a8706ef69da2623e093b018f765f`; let weatherData; let forecastData; @@ -9,7 +10,9 @@ const getWeatherData = async (query) => { `https://api.openweathermap.org/data/2.5/weather?q=${query}&appid=${apiKey}&units=imperial` ); if (!response.ok) { - console.log("Error"); + throw new Error( + `Failed to find city: ${query} (Status: ${response.status})` + ); } else { const data = await response.json(); return { @@ -19,7 +22,8 @@ const getWeatherData = async (query) => { }; } } catch (error) { - console.log(error); + console.error(error); + return { error: error.message }; } }; @@ -29,7 +33,9 @@ const getForecastData = async (query) => { `https://api.openweathermap.org/data/2.5/forecast?q=${query}&appid=${apiKey}&units=imperial` ); if (!response.ok) { - console.log("Error"); + throw new Error( + `Failed to find city: ${query} (Status: ${response.status})` + ); } else { const data = await response.json(); const compiledForecast = data.list.reduce((acc, curr, index) => { @@ -50,7 +56,8 @@ const getForecastData = async (query) => { return compiledForecast; } } catch (error) { - console.log(error); + console.error(error); + return { error: error.message }; } }; @@ -144,10 +151,20 @@ function renderForecast() { form.addEventListener("submit", async (e) => { e.preventDefault(); - const input = document.querySelector(".input-control").value; - weatherData = await getWeatherData(input); - forecastData = await getForecastData(input); + weatherData = await getWeatherData(inputField.value); + forecastData = await getForecastData(inputField.value); + + if (weatherData.error || forecastData.error) { + alert(`Could not fetch weather data on this city, please try another one!`); + inputField.classList.add("is-invalid"); + inputField.focus(); + return; + } renderWeather(); renderForecast(); - document.querySelector(".input-control").value = ""; + inputField.value = ""; +}); + +inputField.addEventListener("input", () => { + inputField.classList.remove("is-invalid"); }); From 1e7ce4851e60cdbaa9477fb1fcfbe77da8c761d6 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Thu, 13 Feb 2025 13:50:35 -0500 Subject: [PATCH 8/9] add default city button --- main.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/main.js b/main.js index 19279029..fdda47d0 100644 --- a/main.js +++ b/main.js @@ -98,6 +98,19 @@ const renderWeather = () => { card.appendChild(cardBody); weatherDiv.appendChild(card); + + const defaultBtn = document.createElement("button"); + defaultBtn.classList.add("btn", "btn-secondary"); + defaultBtn.setAttribute("type", "button"); + defaultBtn.innerText = "Set City as Default"; + + defaultBtn.addEventListener("click", () => { + localStorage.setItem("weatherData", JSON.stringify(weatherData)); + localStorage.setItem("forecastData", JSON.stringify(forecastData)); + }); + if (!document.querySelector(".btn-secondary")) { + form.appendChild(defaultBtn); + } }; function renderForecast() { @@ -168,3 +181,15 @@ form.addEventListener("submit", async (e) => { inputField.addEventListener("input", () => { inputField.classList.remove("is-invalid"); }); + +document.addEventListener("DOMContentLoaded", () => { + if ( + localStorage.getItem("weatherData") && + localStorage.getItem("forecastData") + ) { + weatherData = JSON.parse(localStorage.getItem("weatherData")); + forecastData = JSON.parse(localStorage.getItem("forecastData")); + renderWeather(); + renderForecast(); + } +}); From d4d2e4428cd93800df95119387a6736d0489fd76 Mon Sep 17 00:00:00 2001 From: Christopher d'Arcy Date: Thu, 13 Feb 2025 14:10:58 -0500 Subject: [PATCH 9/9] add comments --- main.js | 47 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/main.js b/main.js index fdda47d0..7182ef70 100644 --- a/main.js +++ b/main.js @@ -1,9 +1,16 @@ +// Global Variables +// =================================== const form = document.getElementsByTagName("form")[0]; const inputField = document.querySelector(".input-control"); const apiKey = `c8d0a8706ef69da2623e093b018f765f`; let weatherData; let forecastData; +/** + * Fetches weather data for a given city + * @param {string} query - The city name to search for + * @returns {Promise} Weather data object or error object + */ const getWeatherData = async (query) => { try { const response = await fetch( @@ -27,6 +34,11 @@ const getWeatherData = async (query) => { } }; +/** + * Fetches forecast data for a given city + * @param {string} query - The city name to search for + * @returns {Promise} Weather data object or error object + */ const getForecastData = async (query) => { try { const response = await fetch( @@ -38,10 +50,10 @@ const getForecastData = async (query) => { ); } else { const data = await response.json(); + //reduces data array to 5-day forecast by taking every 8th forecast const compiledForecast = data.list.reduce((acc, curr, index) => { if (index % 8 === 0) { acc.push({ - date_info: { dt: curr.dt, dt_txt: curr.dt_txt }, day: new Date(curr.dt_txt).toLocaleDateString("en-US", { weekday: "long", }), @@ -61,6 +73,12 @@ const getForecastData = async (query) => { } }; +/** + * Renders the current weather data to the DOM + * Creates a card displaying city name, weather icon, temperature, and description + * Also creates a "Set City as Default" button if it doesn't exist + * @returns {void} + */ const renderWeather = () => { const weatherDiv = document.querySelector(".weather-container"); weatherDiv.replaceChildren(); @@ -99,20 +117,26 @@ const renderWeather = () => { card.appendChild(cardBody); weatherDiv.appendChild(card); - const defaultBtn = document.createElement("button"); - defaultBtn.classList.add("btn", "btn-secondary"); - defaultBtn.setAttribute("type", "button"); - defaultBtn.innerText = "Set City as Default"; - - defaultBtn.addEventListener("click", () => { - localStorage.setItem("weatherData", JSON.stringify(weatherData)); - localStorage.setItem("forecastData", JSON.stringify(forecastData)); - }); if (!document.querySelector(".btn-secondary")) { + const defaultBtn = document.createElement("button"); + defaultBtn.classList.add("btn", "btn-secondary"); + defaultBtn.setAttribute("type", "button"); + defaultBtn.innerText = "Set City as Default"; + + defaultBtn.addEventListener("click", () => { + localStorage.setItem("weatherData", JSON.stringify(weatherData)); + localStorage.setItem("forecastData", JSON.stringify(forecastData)); + }); form.appendChild(defaultBtn); } }; +/** + * Renders the 5-day forecast data to the DOM + * Creates a list of cards displaying day of week, weather icon, temperature, and description + * Each card represents one day's forecast + * @returns {void} + */ function renderForecast() { const forecastsDiv = document.querySelector(".forecast-container"); forecastsDiv.replaceChildren(); @@ -167,6 +191,7 @@ form.addEventListener("submit", async (e) => { weatherData = await getWeatherData(inputField.value); forecastData = await getForecastData(inputField.value); + //checks if data fetching returned an error, if so alert user and highlight input if (weatherData.error || forecastData.error) { alert(`Could not fetch weather data on this city, please try another one!`); inputField.classList.add("is-invalid"); @@ -178,10 +203,12 @@ form.addEventListener("submit", async (e) => { inputField.value = ""; }); +//removes invalid class from input upon change inputField.addEventListener("input", () => { inputField.classList.remove("is-invalid"); }); +//on DOM load checks if local storage contains default city weather info, if so renders it document.addEventListener("DOMContentLoaded", () => { if ( localStorage.getItem("weatherData") &&