Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
83f85cd
html structure
Mar 18, 2025
cbe403f
ignore typechecking
Mar 18, 2025
a6ccbb1
fetch + function
Mar 18, 2025
a30f6d4
adjusted fetch function weather + localstorage
Mar 19, 2025
fe6ffea
updated localstorage
Mar 19, 2025
c7ee945
added styling to HTML
Mar 19, 2025
de82ce0
added searchabr, display weather, storage
Mar 19, 2025
5503506
adding missing html and styling
Mar 19, 2025
f76de5d
added images to assets
Mar 19, 2025
9d675a4
Merge branch 'main' of https://github.com/JasminHed/newjs-project-wea…
Mar 19, 2025
8352def
added html and css
Mar 19, 2025
7440a9c
removed extra side button
Mar 19, 2025
587c168
updated error bracket
Mar 19, 2025
1060ae5
commented out forecast function
Mar 19, 2025
baf6a38
removed comments and old code
Mar 19, 2025
ab46f76
updated js
Mar 20, 2025
0153fbe
new html
Mar 20, 2025
d2cde95
new css
Mar 20, 2025
3b593fe
tweaked js
Mar 20, 2025
e8e88c8
removed the new script file
Mar 20, 2025
dac19ae
created TS
Mar 20, 2025
c5b04e4
updated ts file
Mar 20, 2025
5a1e3a7
compiled ts+js and added slidebutton
Mar 21, 2025
fde6dc8
added eventlistener to enter key
Mar 22, 2025
16b9e8c
removed old ts code
Mar 22, 2025
b13c6c4
updated all functions to same
Mar 24, 2025
435a337
added functions for getting api icons in weather + forecast, changed css
Mar 24, 2025
b465cc5
removed old code and comments
Mar 24, 2025
dc21e55
Update README.md
JasminHed Apr 8, 2025
0f95aef
screen reader labeling + focus for tabbing
Apr 9, 2025
b6b659f
Merge branch 'main' of https://github.com/JasminHed/newjs-project-wea…
Apr 9, 2025
9e4e688
removed redundant focus input+button
Apr 9, 2025
0c5e797
Update README.md
JasminHed Apr 26, 2025
9de7171
updated readme
JasminHed Jun 30, 2025
3d40b68
resolved conflict
JasminHed Jun 30, 2025
5ef1d94
semantic html, page regions
JasminHed Aug 1, 2025
81c43a3
removed
JasminHed Aug 1, 2025
36528a2
updated html strucuture
JasminHed Aug 1, 2025
add6696
added scope column to table
JasminHed Aug 1, 2025
efdc9de
Update README with badges, features, Next Up
JasminHed Sep 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,78 @@
# js-project-weather-app
[![Frontend](https://img.shields.io/badge/Frontend-HTML%2CCSS%2CJS-orange?logo=javascript)](https://nordicweatherapp.netlify.app/)
[![API](https://img.shields.io/badge/API-Weather-brightblue?logo=openweathermap)](https://openweathermap.org/)
[![Deployment](https://img.shields.io/badge/Deployment-Netlify-brightgreen?logo=netlify)](https://nordicweatherapp.netlify.app/)
[![License](https://img.shields.io/badge/License-MIT-yellow)](LICENSE)

# JS Project: Weather App
**Live Demo:** [https://nordicweatherapp.netlify.app](https://nordicweatherapp.netlify.app)

A modern weather application that provides current conditions and 4-day forecasts for cities worldwide. This project challenged me to work with real-time weather data from external APIs and create an intuitive interface for checking weather conditions. Building this app taught me how to handle dynamic data, manage API calls efficiently, and present complex information in a user-friendly way.

## The Problem
I wanted to create a weather app that is both **functional and visually appealing**, while working with live API data. The challenge was managing asynchronous fetch requests, handling errors, and displaying complex data in a clear and responsive layout.

## View it Live
- **Live Demo:** [https://nordicweatherapp.netlify.app](https://nordicweatherapp.netlify.app)

## Installation / Setup Instructions

# Clone the repo
git clone [your-repo-url]
cd nordic-weather-app

# Open in browser
open index.html

# No installation needed - pure HTML, CSS, and JavaScript

## Technologies Used

Frontend: HTML5, CSS3, JavaScript (ES6+)

API: OpenWeatherMap / WeatherAPI

Layout: CSS Grid, Flexbox

Data Handling: Fetch API, JSON processing

Deployment: Netlify

Version Control: Git/GitHub

## Features

Search for weather in selected cities

Current weather conditions with temperature and description

4-day extended weather forecast

Responsive layout optimized for all devices

Real-time data updates from weather API

Error handling for invalid city searches

Loading states during API requests

## What I Learned

Working with third-party weather APIs and API keys

Handling asynchronous JavaScript and fetch requests

Creating intuitive search functionality

Managing API rate limits and error responses

## Next Up

Add hourly weather forecasts

Implement user geolocation for automatic city detection

Enhance UI with animations and dynamic icons

Add option to switch between Celsius and Fahrenheit

Improve accessibility and keyboard navigation
Binary file added assets/Brokenclouds.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Cloudy.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Fewclouds.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Group 13.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Group 15.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Group 16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Night.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Subtraction 2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Sunny.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Sunny.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Sunrise.jpg.avif
Binary file not shown.
Binary file added assets/Sunset.jpg.avif
Binary file not shown.
Binary file added assets/searchglass.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/sun.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
190 changes: 190 additions & 0 deletions dist/script.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
"use strict";
// @ts-nocheck
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
document.addEventListener('DOMContentLoaded', function () {
// API information
const API_KEY = '3bad52890d7306cc268371520cbaace6';
const BASE_URL = 'https://api.openweathermap.org/data/2.5/forecast';
// List of default cities
const cities = ['Stockholm', 'Gothenburg', 'Oslo'];
let weeklyForecast = {};
let currentCityIndex = 0;
function fetchWeather(city) {
return __awaiter(this, void 0, void 0, function* () {
try {
const response = yield fetch(`${BASE_URL}?q=${city}&units=metric&appid=${API_KEY}`);
const data = yield response.json();
if (data.cod !== "200") {
throw new Error(data.message || "Failed to fetch weather data.");
}
return data;
}
catch (error) {
console.error(`Error fetching weather data for ${city}:`, error);
return null;
}
});
}
function getDayName(dateString) {
const date = new Date(dateString);
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
return days[date.getDay()];
}
function fetchAndStoreWeather(city) {
return __awaiter(this, void 0, void 0, function* () {
const data = yield fetchWeather(city);
if (!data || !data.list) {
return;
}
const todayData = data.list.find(entry => entry.dt_txt.includes("12:00:00")) || data.list[0];
const todayDate = todayData.dt_txt.split(" ")[0];
const sunriseTime = new Date(data.city.sunrise * 1000).toLocaleTimeString("sv-SE", { hour: "2-digit", minute: "2-digit" });
const sunsetTime = new Date(data.city.sunset * 1000).toLocaleTimeString("sv-SE", { hour: "2-digit", minute: "2-digit" });
const todayForecast = {
city: data.city.name,
day: getDayName(todayDate),
weather: todayData.weather[0].description,
icon: todayData.weather[0].icon,
temp: todayData.main.temp,
wind: todayData.wind.speed,
sunrise: sunriseTime,
sunset: sunsetTime,
};
const dailyForecasts = {};
data.list.forEach(entry => {
const date = entry.dt_txt.split(' ')[0];
const hour = entry.dt_txt.split(' ')[1].split(':')[0];
if (hour === '12' && date !== todayDate) {
dailyForecasts[date] = {
date: date,
day: getDayName(date),
icon: entry.weather[0].icon,
weather: entry.weather[0].description,
temp: entry.main.temp,
wind: entry.wind.speed,
};
}
});
const upcomingForecast = Object.values(dailyForecasts).slice(0, 4);
weeklyForecast[city] = {
today: todayForecast,
upcoming: upcomingForecast
};
localStorage.setItem("weatherData", JSON.stringify(weeklyForecast));
displayTodaysWeather(todayForecast);
displayWeeklyWeather(upcomingForecast);
updateBackground(todayForecast.weather, todayForecast.icon);
});
}
function displayTodaysWeather(forecast) {
const weatherContent = document.getElementById('weather-content');
if (!weatherContent)
return;
weatherContent.innerHTML = `
<div class="weather-icon">
<img id="main-icon" src="https://openweathermap.org/img/wn/${forecast.icon}@2x.png" alt="${forecast.weather}">
</div>
<p id="temperature">${Math.round(forecast.temp)}°C</p>
<p id="city">${forecast.city}</p>
<p id="weather">${forecast.weather}</p>
<div class="sunrise-sunset">
<p id="sunrise">Sunrise: ${forecast.sunrise}</p>
<p id="sunset">Sunset: ${forecast.sunset}</p>
</div>
`;
}
function displayWeeklyWeather(forecastList) {
const forecastTable = document.querySelector("#weather-forecast table");
if (!forecastTable)
return;
const rows = forecastTable.getElementsByTagName("tr");
if (!rows || rows.length === 0)
return;
forecastList.forEach((forecast, index) => {
if (index < rows.length) {
const dayCell = rows[index].querySelector(`#day${index + 1}`);
if (dayCell)
dayCell.textContent = forecast.day;
const iconCell = rows[index].querySelector(`#iconday${index + 1}`);
if (iconCell) {
iconCell.innerHTML = `<img src="https://openweathermap.org/img/wn/${forecast.icon}.png" alt="${forecast.weather}">`;
}
const tempCell = rows[index].querySelector(`#tempday${index + 1}`);
if (tempCell)
tempCell.textContent = `${Math.round(forecast.temp)}°C`;
const windCell = rows[index].querySelector(`#windday${index + 1}`);
if (windCell)
windCell.textContent = `${forecast.wind} m/s`;
}
});
}
function updateBackground(weatherDescription, iconCode) {
const container = document.querySelector('.container');
if (!container)
return;
container.classList.remove('rainy', 'cloudy', 'clear', 'snowy', 'daytime', 'nighttime');
const isDaytime = iconCode.endsWith('d');
container.classList.add(isDaytime ? 'daytime' : 'nighttime');
if (weatherDescription.includes('rain') || weatherDescription.includes('drizzle')) {
container.classList.add('rainy');
}
else if (weatherDescription.includes('cloud')) {
container.classList.add('cloudy');
}
else if (weatherDescription.includes('clear')) {
container.classList.add('clear');
}
else if (weatherDescription.includes('snow')) {
container.classList.add('snowy');
}
}
function cycleCity() {
currentCityIndex = (currentCityIndex + 1) % cities.length;
const city = cities[currentCityIndex];
fetchAndStoreWeather(city);
}
function initializeEventListeners() {
const searchButton = document.getElementById("search-button");
const inputField = document.getElementById("input-field");
const nextSideButton = document.getElementById('next-side-button');
if (searchButton) {
searchButton.addEventListener("click", function () {
if (inputField && inputField.value.trim()) {
fetchAndStoreWeather(inputField.value.trim());
}
});
}
if (inputField) {
inputField.addEventListener("keydown", function (event) {
if (event.key === "Enter" && inputField.value.trim()) {
fetchAndStoreWeather(inputField.value.trim());
}
});
}
if (nextSideButton) {
nextSideButton.addEventListener('click', cycleCity);
}
else {
console.error("Could not find button with ID 'next-side-button'");
}
}
initializeEventListeners();
fetchAndStoreWeather("Stockholm");
const savedData = localStorage.getItem("weatherData");
if (savedData) {
try {
weeklyForecast = JSON.parse(savedData);
}
catch (e) {
console.error("Failed to parse saved weather data:", e);
}
}
});
114 changes: 114 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=, initial-scale=1.0"
>
<link
rel="stylesheet"
href="style.css"
>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
>
<title>Weather App</title>
</head>

<body>

<div class="container">
<header> <h1 class="sr-only">Weather App</h1> </header>

<main>
<div class="overlay"></div>
<div class="input-container">
<label
for="input-field"
class="sr-only"
>Search city</label>
<input
type="text"
id="input-field"
placeholder="Search city"
>
<button
id="search-button"
aria-label="Search"
>
<i
class="fa-solid fa-magnifying-glass"
aria-hidden="true"
></i>
</button>
</div>

<div>
<div
class="weather-content"
id="weather-content"
>
</div>


<button
id="next-side-button"
aria-label="Show next City"
><i
class="fa-solid fa-angle-right"
aria-hidden="true"
></i></button>

<div
class="weather-forecast"
id="weather-forecast"
>
<h2 class="sr-only">Weather Forecast</h2>

<table>
<caption class="sr-only">4-day weather forecast</caption>
<thead>
<tr>
<th scope="col" id="day1"></th>
<th scope="col" id="iconday1"></th>
<th scope="col" id="tempday1"></th>
<th scope="col" id="windday1"></th>
</tr>
</thead>
<tbody>
<tr>
<td id="day2"></td>
<td id="iconday2"></td>
<td id="tempday2"></td>
<td id="windday2"></td>
</tr>
<tr>
<td id="day3"></td>
<td id="iconday3"></td>
<td id="tempday3"></td>
<td id="windday3"></td>
</tr>
<tr>
<td id="day4"></td>
<td id="iconday4"></td>
<td id="tempday4"></td>
<td id="windday4"></td>
</tr>
</tbody>
</table>


</div>
</main>
</div>



<script src="script.js"></script>

</body>

</html>
Loading