Weather Web Project Using HTML CSS and Javascript

Creating a weather web Project application using HTML, CSS, and JavaScript involves integrating a weather API to fetch and display weather information

Weather Web Application

A weather web application created using HTML, CSS, and JavaScript allows users to check the weather conditions for a specific location. This application uses the OpenWeatherMap API to fetch weather data and display it on a web page. Here’s a breakdown of the components and functionality:

Sign Up for OpenWeatherMap API:

  • Go to OpenWeatherMap and sign up for a free API key.
  • Once you have your API key, you can start building your weather web application.

CSS (style.css):

  • The CSS file is responsible for styling the web page. In this example, it provides basic formatting, such as centering text and setting margins.

JavaScript (script.js):

  • The JavaScript file contains the logic for fetching weather data and updating the web page with the retrieved information.
  • It defines a getWeather function that is called when the “Get Weather” button is clicked.
  • The function uses the fetch API to make a request to the OpenWeatherMap API with the user’s entered location.
  • Upon receiving a response, it parses the JSON data and updates the content on the web page with details like city name, temperature, and weather description.
  • Error handling is implemented to handle situations where the API request fails.

Weather Web Project for Index.html

 <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <!-- primary meta tags  -->
    <title>weatherio</title>
    <meta name ="title" content="weatherio">
    <meta name ="description" content="weatherio is a weather app made by codewithsadee">
    <!-- favicon -->
    <link rel="shortcut icon" href="./favicon.svg" type="image/svg+xml">

    <!--google font  -->

    <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;600&display=swap" rel="stylesheet">

<link rel="stylesheet" href="./assets/css/style.css">
<script src="./assets/js/route.js" type="module"></script>

</head>
<body>

    <header class="header">
        <div class="container">
           <a href="#" class="logo">
            <img src="./assets/images/logo.png"  width="364"   height ="58"  alt="logo">
           </a>
           <div class="search-view " data-search-view>

            <div class="search-wrapper">
                <input type="search" name="search"  placeholder="search city..." autocomplete="off" class="search-field " data-search-field>
                <span class="m-icon leading-icon">search</span>


                <button class ="icon-btn leading-icon has-state" aria-label="close search" data-search-toggler>
                <span class="m-icon">arrow_back</span>
                </button>
            </div>

                <div class="  search-result" data-search-result></div>
               </div>


           <div class="header-actions">
            <button class=" icon-btn has-state" aria-label="open search" data-search-toggler>
                <span class="m-icon icon">search</span>
            </button>
            <a href="#/current-location" class="btn-primary has-state" data-current-location-btn  >
                <span class="m-icon">my_location</span>
                <span class="span">Current Location</span>
            </a>
           </div>
        </div>
    </header>

  <main>
    <article class="container" data-container>
        <div class="content-left">

        <section class="section current-weather" aria-label="current weather" data-current-weather> </section>

<!-- section  -->

<section class="section forecast" aria-labelledby="forecast-lable" data-5-day-forecast></section>


        </div>
<!--    -->

        <div class="content-right">
<!--           HIGHLIGHTs -->

  <section class="section highlights" aria-labelledby="highlights-label" data-highlights></section>


  <!--  per houre forecast -->


  <section class="section hourly-forecast" aria-label="hourly forecast" data-hourly-forecast></section>



  <!--
    #FOOTER
     -->
  <!-- <div class="card card-lg"> -->

  <!-- </div> -->

  <footer class="footer">
    <p class="body-3">Copyright 2023 rkcoder.tech. All Right Reserved</p>
    <p class="body-3">
        Powered by <a href="https://openweathermap.org/api" rel="noopener" title="Free Openweather Api" target="_blank">
        <img src="./assets/images/openweather.png"  width="150"  height="30"  loading="lazy" alt="Openweather"  >
        </a>
    </p>
 </footer>

        </div>

        <div class="loading" data-loading></div>

    </article>

  </main>

  <!--
    #404
   -->
<section class="error-content" data-error-content>
    <h2 class="heading">404</h2>
    <p class="body-1">Page not found !</p>
     <a href="#/weather?lat=25.4381302&lon=81.8338005" class="btn-primary">
        <span class="span"> Go Home</span>
     </a>

</section>


</body>
</html>   

Weather Web Project

Weather Web Project for Api.js

'use strict';

const api_key = '7775811b5bc22ad000d8cbdc6e86bda0';

// fetch data from server
export const fetchData = function (URL, callback) {
    fetch(`${URL}&appid=${api_key}`)
        .then(res => res.json())
        .then(data => callback(data))
}


export const url = {
    currentWeather(lat, lon) {
        return `https://api.openweathermap.org/data/2.5/weather?${lat}&${lon}&units=metric`
    },
    forecast(lat, lon) {
        return `https://api.openweathermap.org/data/2.5/forecast?${lat}&${lon}&units=metric`
    },
    airPollution(lat, lon) {
        return `https://api.openweathermap.org/data/2.5/air_pollution?${lat}&${lon}`
    },
    reverseGeo(lat, lon) {
        return `https://api.openweathermap.org/geo/1.0/reverse?${lat}&${lon}&limit=5`
     },
    /**
    *  @param {string} query Search query eg.: 'London', 'New york'
    **/
     geo(query) {
        return `https://api.openweathermap.org/geo/1.0/direct?q=${query}&limit=5`
     }
}

App.js

"use strict";

import { fetchData, url } from "./api.js";
import * as module from "./module.js";

/**
 * Add event listener on multiple elements
 * @param {NodeList} elements Elements node Array
 * @param {string} eventType event type eg: "click", "mouseover"
 * @param {fucntion} callback callback function
 */
const addEventOnElements = (elements, eventType, callback) => {
	for (const element of elements)
		element.addEventListener(eventType, callback);
};

/**
 * Toggle search in mobile devices
 */
const searchView = document.querySelector("[data-search-view]");
const searchTogglers = document.querySelectorAll("[data-search-toggler]");

const toggleSearch = () => searchView.classList.toggle("active");
addEventOnElements(searchTogglers, "click", toggleSearch);

/**
 * SEARCH INTEGRATION
 */
const searchField = document.querySelector("[data-search-field]");
const searchResult = document.querySelector("[data-search-result]");

let searchTimeout = null;
const searchTimeoutDuration = 500;

searchField.addEventListener("input", function () {
	searchTimeout ?? clearTimeout(searchTimeout);

	if (!searchField.value) {
		searchResult.classList.remove("active");
		searchResult.innerHTML = "";
		searchField.classList.remove("searching");
	} else {
		searchField.classList.add("searching");
	}

	if (searchField.value) {
		searchTimeout = setTimeout(() => {
			fetchData(url.geo(searchField.value), (locations) => {
				searchField.classList.remove("searching");
				searchResult.classList.add("active");
				searchResult.innerHTML = `
                    <ul class="view-list" data-search-list></ul>
                `;

				const /** {NodeList} | [] */ items = [];

				for (const { name, lat, lon, country, state } of locations) {
					const searchItem = document.createElement("li");
					searchItem.classList.add("view-item");

					searchItem.innerHTML = `
                        <span class="m-icon">location_on</span>

                        <div>
                            <p class="item-title">${name}</p>
                            <p class="label-2 item-subtitle">${state || ""} ${country}</p>
                        </div>

                        <a href="#/weather?lat=${lat}&lon=${lon}" class="item-link has-state" aria-label="${name} weather" data-search-toggler></a>
                    `;

					searchResult.querySelector("[data-search-list]").appendChild(searchItem);
					items.push(searchItem.querySelector("[data-search-toggler]"));
				}

				addEventOnElements(items, "click", () => {
					toggleSearch();
					searchResult.classList.remove("active");
				})
			});
		}, searchTimeoutDuration);
	}
});



const  container = document.querySelector("[data-container]");
const  loading = document.querySelector("[data-loading]");
const currentLocationBtn = document.querySelector("[data-current-location-btn]");
const errorContainer = document.querySelector("[data-error-content]");

/**
 *
 * @param {number} lat  latitude
 * @param {number} lon  longitude
 */

export const updateWeather =function (lat ,lon ){
    // loading.style.display= "grid";
    // container.style.overflowY= "hidden";
    // container.classList.remove("fade-in");
    errorContainer.style.display= "none";

    const currentWeatherSection = document.querySelector("[data-current-weather]");
    const highlightSection = document.querySelector("[data-highlights]");
	const hourlySection = document.querySelector("[data-hourly-forecast]");
	const forecastSection = document.querySelector("[data-5-day-forecast]");

	currentWeatherSection.innerHTML = "";
	highlightSection.innerHTML = "";
	hourlySection.innerHTML = "";
	forecastSection.innerHTML = "";

    if (window.location.hash === "#/current-location") {
		currentLocationBtn.setAttribute("disabled", "");
	} else {
		currentLocationBtn.removeAttribute("disabled");
	}

     // CURRENT WEATHER SECTION
       fetchData(url.currentWeather(lat, lon), function(currentWeather){
        const {
            weather,
            dt:dateUnix,
            sys: {sunrise: sunriseUnixUTC, sunset: sunsetUnixUTC },
            main: { temp, feels_like, pressure, humidity },
			visibility,
			timezone
        }= currentWeather;
        const [{description,icon}] =weather;
        const card = document.createElement("div");
		card.classList.add("card", "card-lg", "current-weather-card");

        card.innerHTML = `

        <h2 class="title-2 card-title">Now</h2>
            <div class="weapper">
                <p class="heading">${parseInt(temp)}°c<sup></sup></p>
                <img src="./assets/images/weather_icons/${icon}.png" alt="${description}" width="64" height="64" class="weather-icon">
            </div>
            <p class="body-3">${description}</p>
            <ul class="meta-list">

                <li class="meta-item">
                    <span class="m-icon">calendar_today</span>
                       <p class="title-3 meta-text">${module.getDate(dateUnix,timezone)}</p>
                </li>
                <li class="meta-item">
                    <span class="m-icon">location_on</span>
                       <p class="title-3 meta-text" data-location></p>
                </li>
            </ul>

        `;

        fetchData(url.reverseGeo(lat, lon),  function([{ name, country }]) {
			card.querySelector("[data-location]").innerHTML = `${name}, ${country}`
		});

        currentWeatherSection.appendChild(card);

        // TODAY"S HIGHLIGHTS
		fetchData(url.airPollution(lat, lon), (airPollution) => {
			const [{
				main: {aqi},
				components: { no2, o3, so2, pm2_5}
			}] = airPollution.list;

            const card = document.createElement("div");
			card.classList.add("card", "card-lg");

            card.innerHTML = `
				<h2 class="title-2" id="highlights-label">Todays Highlights</h2>

				<div class="highlight-list">
					<div class="card card-sm highlight-card one">
						<h3 class="title-3">Air Quality Index</h3>

						<div class="wrapper">

							<span class="m-icon">air</span>

							<ul class="card-list">
								<li class="card-item">
									<p class="title-1">${pm2_5.toPrecision(3)}</p>

									<p class="label-1">PM<sub>2.5</sub></p>
								</li>

								<li class="card-item">
									<p class="title-1">${so2.toPrecision(3)}</p>

									<p class="label-1">SO<sub>2</sub></p>
								</li>

								<li class="card-item">
									<p class="title-1">${no2.toPrecision(3)}</p>

									<p class="label-1">NO<sub>2</sub></p>
								</li>

								<li class="card-item">
									<p class="title-1">${o3.toPrecision(3)}</p>

									<p class="label-1">O<sub>3</sub></p>
								</li>
							</ul>
						</div>

						<span class="badge aqi-${aqi} label-${aqi}" title="${module.aqiText[aqi].message}">
							${module.aqiText[aqi].level}
						</span>
					</div>

					<div class="card card-sm highlight-card two">
						<h3 class="title-3">Sunrise & Sunset</h3>

						<div class="card-list">
							<div class="card-item">
								<span class="m-icon">clear_day</span>

								<div>
									<p class="label-1">Sunrise</p>
									<p class="title-1">${module.getTime(sunriseUnixUTC, timezone)}</p>
								</div>
							</div>

							<div class="card-item">
								<span class="m-icon">clear_night</span>

								<div>
									<p class="label-1">Sunset</p>
									<p class="title-1">${module.getTime(sunsetUnixUTC, timezone)}</p>
								</div>
							</div>
						</div>
					</div>

					<div class="card card-sm highlight-card">
						<h3 class="title-3">Humidity</h3>

						<div class="wrapper">
							<span class="m-icon">humidity_percentage</span>

							<p class="title-1">${humidity}<sub>%</sub></p>
						</div>
					</div>

					<div class="card card-sm highlight-card">
						<h3 class="title-3">Pressure</h3>

						<div class="wrapper">
							<span class="m-icon">airwave</span>

							<p class="title-1">${pressure}<sub>hPa</sub></p>
						</div>
					</div>

					<div class="card card-sm highlight-card">
						<h3 class="title-3">Visibility</h3>

						<div class="wrapper">
							<span class="m-icon">visibility</span>

							<p class="title-1">${visibility / 1000}<sub>km</sub></p>
						</div>
					</div>

					<div class="card card-sm highlight-card">
						<h3 class="title-3">Feels Like</h3>

						<div class="wrapper">
							<span class="m-icon">thermostat</span>

							<p class="title-1">${parseInt(feels_like)}&deg;<sup>c</sup></p>
						</div>
					</div>
				</div>
			`;

			highlightSection.appendChild(card)



         });


		// 24H FORECAST
		fetchData(url.forecast(lat, lon), (forecast) => {
			const {
				list: forecastList,
				city : { timezone }
			} = forecast;

			hourlySection.innerHTML = `


				<h2 class="title-2">Today at</h2>

				<div class="slider-container">
					<ul class="slider-list" data-temp></ul>

					<ul class="slider-list" data-wind></ul>
				</div>
			`;


			for (const [index, data] of forecastList.entries()) {

				if (index > 7) break;

				const {
					dt: dateTimeUnix,
					main : { temp },
					weather,
					wind : { deg: windDirection, speed: windSpeed }
				} = data;
				const [{ icon, description }] = weather;

				const tempLi = document.createElement("li");
				tempLi.classList.add("slider-item");

                tempLi.innerHTML = `
					<div class="card card-sm slider-card">
						<p class="body-3">${module.getHours(dateTimeUnix, timezone)}</p>

						<img src="./assets/images/weather_icons/${icon}.png" alt="${description}" class="weather-icon"
						width="48" height="48" loading="lazy" title="${description}">

						<p class="body-3">${parseInt(temp)}&deg;</p>
					</div>
				`;

				hourlySection.querySelector("[data-temp]").appendChild(tempLi)

				const windLi = document.createElement("li");
				windLi.classList.add("slider-item");

				windLi.innerHTML = `
					<div class="card card-sm slider-card">
						<p class="body-3">${module.getHours(dateTimeUnix, timezone)}</p>

						<img src="./assets/images/weather_icons/direction.png" alt="direction" class="weather-icon"
							width="48" height="48" loading="lazy" title="" style="transform: rotate(${windDirection - 180}deg)">

						<p class="body-3">${parseInt(module.mps_to_kmh(windSpeed))} km/h</p>
					</div>
				`;

				hourlySection.querySelector("[data-wind]").appendChild(windLi)



            }


            // 5 DAY FORECAST
			forecastSection.innerHTML = `
            <h2 class="title-2" id="forecast-label">5 Days Forecast</h2>

            <div class="card card-lg forecast-card">
                <ul data-forecast-list></ul>
            </div>
        `;

        for (let i = 7, len = forecastList.length; i < len; i += 8) {
            const {
                main: { temp_max },
                weather,
                dt_txt
            } = forecastList[i];
            const [{ icon, description }] = weather;
            const date = new Date(dt_txt);

            const li = document.createElement("li");
            li.classList.add("card-item");

            li.innerHTML = `
                <div class="icon-wrapper">
                    <img src="./assets/images/weather_icons/${icon}.png" alt="${description}"
                        class="weather-icon" width="36" height="36" title="${description}">

                    <span class="span">
                        <p class="title-2">${parseInt(temp_max)}&deg;</p>
                    </span>
                </div>

                <p class="label-1">${date.getDate()} ${module.monthNames[date.getUTCMonth()]}</p>

                <p class="label-1">${module.weekDayNames[date.getUTCDay()]}</p>
            `;

            forecastSection.querySelector("[data-forecast-list]").appendChild(li);
        }



        loading.style.display = "none";
        container.style.overflowY = "overlay";
        container.classList.add("fade-in");





        });

});

}

export const error404 = () => errorContent.style.display = "flex";

Module.js


'use strict';

export const weekDayNames = [
    "Sunday",
    "Monday",
    "Tuesday",
    "Wednesday",
    "Thursday",
    "Friday",
    "Saturday"
]

export const monthNames = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec"
]

/**
 *
 * @param {number} dateUnix Unix date in seconds
 * @param {number} timezone timezone shift from UTC in seconds
 * @returns {string} Date string, format: 'Sunday 10, Jan'
 */
export const getDate = function (dateUnix, timezone) {
    const date = new Date((dateUnix + timezone) * 1000)
    const weekDayName = weekDayNames[date.getUTCDay()]
    const monthName = monthNames[date.getUTCMonth()]

    return `${weekDayName} ${date.getUTCDate()}, ${monthName}`
}

/**
 * @param {number} timeUnix Unix time in seconds
 * @param {number} timezone timezone shift from UTC in seconds
 * @returns {string} Time string, format: 'HH:MM AM?PM'
 */
export const getTime = function (timeUnix, timezone) {
    const date = new Date((timeUnix + timezone) * 1000)
    const hours = date.getUTCHours()
    const minutes = date.getUTCMinutes()
    const period = hours >= 12 ? "PM" : "AM"

    return `${hours % 12 || 12}:${minutes} ${period}`
}

/**
 * @param {number} timeUnix Unix time in seconds
 * @param {number} timezone timezone shift from UTC in seconds
 * @returns {string} Time string, format: 'HH AM/PM'
 */
export const getHours = function (timeUnix, timezone) {
    const date = new Date((timeUnix + timezone) * 1000)
    const hours = date.getUTCHours()
    const period = hours >= 12 ? "PM" : "AM"

    return `${hours % 12 || 12} ${period}`
}

/**
 * @param {number} mps Metre per second
 * @returns {number} kilometre per hour
 */
export const mps_to_kmh = mps => {
    // mph = mps * 3600

    return (mps * 3600) / 1000
}

export const aqiText = {
    1: {
        level: "Good",
        message: "Air quality is considered satisfactory, and air pollution poses little or no risk."
    },
    2: {
        level: "Fair",
        message: "Air quality is acceptable; however, for some pollutants there may be a moderate health concern for a very small number of people who are unusually sensitive to air pollution."
    },
    3: {
        level: "Moderate",
        message: "Members of sensitive groups may experience health effects. The general public is not likely to be affected"
    },
    4: {
        level: "Poor",
        message: "Everyone may begin to experience health effects; members of sensitive groups may experience more serious health effects."
    },
    5: {
        level: "Very Poor",
        message: "Health warnings of emergency conditions. The entire population is more likely to be affected."
    }
}

Route.js

‘use strict’;

// import { query } from ‘express’;
import {updateWeather, error404 } from ‘./app.js’;
const defaultLocation = “#/weather?lat=25.4381302&lon=81.8338005”

const currentLocation = function() {

window.navigator.geolocation.getCurrentPosition(res=> {
    const { latitude, longitude } = res.coords;
    updateWeather(`lat=${latitude}`,`lon=${longitude}`);
}, err => {
    window.location.hash = defaultLocation;
});

}

Route.js

'use strict';

// import { query } from 'express';
import {updateWeather, error404 } from './app.js';
 const defaultLocation = "#/weather?lat=25.4381302&lon=81.8338005"

 const currentLocation = function() {

    window.navigator.geolocation.getCurrentPosition(res=> {
        const { latitude, longitude } = res.coords;
        updateWeather(`lat=${latitude}`,`lon=${longitude}`);
    }, err => {
        window.location.hash = defaultLocation;
    });

 }

 /**
  *
  * @param {string} query  Searched query
  */

 const searcheLocation = query => updateWeather(...query.split('&'));

 const routes = new Map([
    ["/current-location", currentLocation],
    ["/weather", searcheLocation]
 ]);

 const checkHash = function () {
    const requestURL = window.location.hash.slice(1);

    const [route, query] = requestURL.includes ? requestURL.split('?') : [requestURL];

    routes.get(route) ? routes.get(route)(query) : error404();

 }

 window.addEventListener("hashchange", checkHash);

 window.addEventListener("load", function () {
    if(!window.location.hash) {
        window.location.hash = "#/current-location";
    } else {
        checkHash();
    }
 });
Weather Web Project for

Style.css

:root {
	/* COLOR */
	--primary: #b5a1e5;
	--on-primary: #100e17;
	--background: #131214;
	--on-background: #eae6f2;
	--surface: #1d1c1f;
	--on-surface: #dddae5;
	--on-surface-variant: #7b7980;
	--on-surface-variant-2: #b9b6bf;
	--outline: #3e3d40;
	--bg-aqi-1: #89e589;
	--on-bg-aqi-1: #1f331f;
	--bg-aqi-2: #e5dd89;
	--on-bg-aqi-2: #33311f;
	--bg-aqi-3: #e5c089;
	--on-bg-aqi-3: #332b1f;
	--bg-aqi-4: #e58989;
	--on-bg-aqi-4: #331f1f;
	--bg-aqi-5: #e589b7;
	--on-bg-aqi-5: #331f29;
	--white: hsl(0, 0%, 100%);
	--white-alpha-4: hsla(0, 0%, 100%, 0.04);
	--white-alpha-8: hsla(0, 0%, 100%, 0.08);
	--black-alpha-10: hsla(0, 0%, 0%, 0.1);



	/* gradient */
	--gradient-1: linear-gradient(
		180deg,
		hsla(270, 5%, 7%, 0) 0%,
		hsla(270, 5%, 7%, 0.8) 65%,
		hsl(270, 5%, 7%) 100%
	);
	--gradient-2: linear-gradient(
		180deg,
		hsla(260, 5%, 12%, 0) 0%,
		hsla(260, 5%, 12%, 0.8) 65%,
		hsl(260, 5%, 12%) 100%
	);



	/* TYPOGRAPHY */
  /* font family */
	--ff-nunito-sans: "Nunito Sans", sans-serif;

  /* font size */
	--heading: 5.6rem;
	--title-1: 2rem;
	--title-2: 1.8rem;
	--title-3: 1.6rem;
	--body-1: 2.2rem;
	--body-2: 2rem;
	--body-3: 1.6rem;
	--label-1: 1.4rem;
	--label-2: 1.2rem;

  /* font weight */
	--weight-regular: 400;
	--weight-semiBold: 600;



	/* BOX SHADOW */
	--shadow-1: 0px 1px 3px hsla(0, 0%, 0%, 0.5);
	--shadow-2: 0px 3px 6px hsla(0, 0%, 0%, 0.4);



	/* BORDER RADIUS */
	--radius-28: 28px;
	--radius-16: 16px;
	--radius-pill: 500px;
	--radius-circle: 50%;



	/* TRANSITION */
	--transition-short: 100ms ease;
}

/*-----------------------------------*\
  #RESET
\*-----------------------------------*/

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

li {list-style: none;}

a,
img,
span,
input,
button { display: block; }

a { color: inherit; text-decoration: none;}

img { height: auto;}

input,
button {
  background: none;
  border: none;
  color: inherit;
  font: inherit;
}

input { width: 100%; }

button { cursor: pointer; }

sub { vertical-align: baseline; }

sup { vertical-align: top; }

sub, sup { font-size: 0.75em;}

html {
  font-family: var(--ff-nunito-sans);
  font-size: 10px;
  scroll-behavior: smooth;
}

body {
  background-color: var(--background);
  color: var(--on-background);
  font-size: var(--body-3);
  overflow: hidden;
}

:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 2px;
}

::selection { background-color: var(--white-alpha-8);}

::-webkit-scrollbar {
  width: 6px;
  height: 6px;  /*for horizontal scrollbar*/
}

::-webkit-scrollbar-thumb {
  background-color: var(--white-alpha-8);
  border-radius: var(--radius-pill);
}

/*-----------------------------------*\
  #MATERIAL ICON
\*-----------------------------------*/

@font-face {
  font-family: 'Material Symbols Rounded';
  font-style: normal;
  font-weight: 400;
  src: url('../font/material-symbol-rounded.woff2') format('woff2');
}

.m-icon {
  font-family: 'Material Symbols Rounded';
  font-style: normal;
  font-weight: normal;
  font-size: 2.4rem;
  line-height: 1;
  letter-spacing: normal;
  text-transform: none;
  white-space: nowrap;
  word-wrap: normal;
  direction: ltr;
  font-feature-settings: 'liga';
  -webkit-font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
  height: 1em;
  width: 1em;
  overflow: hidden;
}







/*-----------------------------------*\
  #REUSED STYLE
\*-----------------------------------*/

.container {
  max-width: 1600px;
  width: 100%;
  margin-inline: auto;
  padding: 16px;
}

.icon-btn {
  background-color: var(--white-alpha-8);
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-circle);
}

.has-state { position: relative; }

.has-state:hover { box-shadow: var(--shadow-1); }

.has-state:is(:focus, :focus-visible) { box-shadow: none; }

.has-state::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  clip-path: circle(100% at 50% 50%);
  transition: var(--transition-short);
}

.has-state:hover::before { background-color: var(--white-alpha-4); }

.has-state:is(:focus, :focus-visible)::before {
  background-color: var(--white-alpha-8);
  animation: ripple 250ms ease forwards;
}

.btn-primary {
  background-color: var(--primary);
  color: var(--on-primary);
  height: 48px;
  line-height: 48px;
  max-width: max-content;
  display: flex;
  align-items: center;
  gap: 16px;
  padding-inline: 16px;
  border-radius: var(--radius-pill);
}

.btn-primary .span { font-weight: var(--weight-semiBold); }

.btn-primary[disabled] {
  background-color: var(--outline);
  color: var(--on-surface-variant);
  cursor: not-allowed;
}

.btn-primary[disabled]::before { display: none; }

.card {
  background-color: var(--surface);
  color: var(--on-surface);
}

.card-lg {
  border-radius: var(--radius-28);
  padding: 20px;
}

.card-sm {
  border-radius: var(--radius-16);
  padding: 16px;
}

.heading {
  color: var(--white);
  font-size: var(--heading);
  line-height: 1.1;
}

.title-1 { font-size: var(--title-1); }

.title-2 {
  font-size: var(--title-2);
  margin-block-end: 12px;
}

.title-3 {
  font-size: var(--title-3);
  font-weight: var(--weight-semiBold);
}

.body-1 { font-size: var(--body-1); }

.body-2 {
  font-size: var(--body-2);
  font-weight: var(--weight-semiBold);
}

.body-1 { font-size: var(--body-3); }

.label-1 { font-size: var(--label-1); }

.label-2 { font-size: var(--label-2); }
/*
.fade-in { animation: fade-in 25ms ease forwards; }


@keyframes fade-in{
  0%{ opacity:0; }
  100%{ opacity: 1;}

} */








/*-----------------------------------*\
  #HEADER
\*-----------------------------------*/

.header .btn-primary .span { display: none; }

.logo img { width: 158px; }

.header .container,
.header-actions {
  display: flex;
  align-items: center;
}

.header .container { justify-content: space-between; }

.header-actions { gap: 16px; }

.header .btn-primary { padding-inline: 12px; }

.search-view {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  height: 100svh;   /*for mobile browser*/
  background-color: var(--surface);
  color: var(--on-surface);
  clip-path: circle(4% at calc(100% - 102px) 5%);
  opacity: 0;
  visibility: hidden;
  z-index: 4;
  transition: clip-path 500ms ease;
}

.search-view.active {
  opacity: 1;
  visibility: visible;
  clip-path: circle(130% at 73% 5%);
}

.search-wrapper {
  position: relative;
  border-block-end: 1px solid var(--outline);
}

.search-wrapper::before {
  content: "";
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  right: 16px;
  width: 24px;
  height: 24px;
  border: 3px solid var(--on-surface);
  border-block-start-color: transparent;
  border-radius: var(--radius-circle);
  animation: loading 500ms linear infinite;
  display: none;
}

.search-wrapper:has(.searching)::before { display: block; }

.search-field {
  height: 80px;
  line-height: 80px;
  padding-inline: 56px 16px;
  outline: none;
}

.search-field::placeholder { color: var(--on-surface-variant-2); }

.search-field::-webkit-search-cancel-button { display: none; }

.search-wrapper .leading-icon {
  position: absolute;
  top: 50%;
  left: 28px;
  transform: translate(-50%, -50%);
}

.search-wrapper > .m-icon { display: none; }

.search-wrapper .icon-btn {
  background-color: transparent;
  box-shadow: none;
}

.search-view .view-list { padding-block: 8px 16px; }

.search-view .view-item {
  position: relative;
  height: 56px;
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 16px;
  padding-inline: 16px 24px;
}

.search-view .view-item :is(.m-icon, .item-subtitle) {
  color: var(--on-surface-variant);
}

.search-view .view-item .item-link {
  position: absolute;
  inset: 0;
  box-shadow: none;

}








/*-----------------------------------*\
  #MAIN
\*-----------------------------------*/

main {
  height: calc(100vh - 80px);
  height: calc(100svh - 80px);   /*for mobile browsers*/
  overflow: hidden;
}

article.container {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 20px;
  height: 100%;
  overflow-y: auto;  /*for firefox*/
  overflow-y: overlay;
}

article.container::-webkit-scrollbar-thumb { background-color: transparent; }

article.container:is(:hover, :focus-within)::-webkit-scrollbar-thumb {
  background-color: var(--white-alpha-8);
}

article.container::-webkit-scrollbar-button { height: 10px;}

article.container::before {
  content: "";
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 40px;
  background-image: var(--gradient-1);
  pointer-events: none;
  z-index: 1;
}

.section:not(:last-child) { margin-block-end: 16px;}








/*-----------------------------------*\
  #CURRENT WEATHER
\*-----------------------------------*/

.current-weather-card .wrapper {
  margin-block: 12px;
  display: flex;
  gap: 8px;
  align-items: center;
}

.current-weather-card .weather-icon { margin-inline: auto; }

.current-weather-card > .body-3 { text-transform: capitalize; }

.current-weather-card .meta-list {
  margin-block-start: 16px;
  padding-block-start: 16px;
  border-block-start: 1px solid var(--outline);
}

.current-weather-card .meta-item {
  display: flex;
  align-items: center;
  gap: 8px;
}

.current-weather-card .meta-item:not(:last-child) { margin-block-end: 12px; }

.current-weather-card .meta-text { color: var(--on-surface-variant);}








/*-----------------------------------*\
  #HIGHLIGHTS
\*-----------------------------------*/

.forecast-card .title-2 { margin-block-end: 0; }

.forecast-card :is(.card-item, .icon-wrapper) {
  display: flex;
  align-items: center;
}

.forecast-card .card-item:not(:last-child) {margin-block-end: 12px;}

.forecast-card .icon-wrapper { gap: 8px; }

.forecast-card .label-1 {
  color: var(--on-surface-variant);
  font-weight: var(--weight-semiBold);
}

.forecast-card .card-item > .label-1 {
  width: 100%;
  text-align: right;
}








/*-----------------------------------*\
  #HOURLY FORECAST
\*-----------------------------------*/

.highlights .m-icon { font-size: 3.2rem; }

.highlight-list {
  display: grid;
  gap: 20px;
}

.highlight-list .title-3 {
  color: var(--on-surface-variant);
  margin-block-end: 20px;
}

.highlights .card-sm {
  background-color: var(--black-alpha-10);
  position: relative;
}

.highlight-card :is(.wrapper, .card-list, .card-item) {
  display: flex;
  align-items: center;
}

.highlight-card .wrapper {
  justify-content: space-between;
  gap: 16px;
}

.highlight-card .card-list {
  flex-wrap: wrap;
  flex-grow: 1;
  row-gap: 8px;
}

.highlight-card .card-item {
  width: 50%;
  justify-content: flex-end;
  gap: 4px;
}

.highlight-card .label-1 { color: var(--on-surface-variant); }

.badge {
  position: absolute;
  top: 16px;
  right: 16px;
  padding: 2px 12px;
  border-radius: var(--radius-pill);
  font-weight: var(--weight-semiBold);
  cursor: help;
}

.badge.aqi-1 {
  background-color: var(--bg-aqi-1);
  color: var(--on-bg-aqi-1);
}

.badge.aqi-2 {
  background-color: var(--bg-aqi-2);
  color: var(--on-bg-aqi-2);
}

.badge.aqi-3 {
  background-color: var(--bg-aqi-3);
  color: var(--on-bg-aqi-3);
}

.badge.aqi-4 {
  background-color: var(--bg-aqi-4);
  color: var(--on-bg-aqi-4);
}

.badge.aqi-5 {
  background-color: var(--bg-aqi-5);
  color: var(--on-bg-aqi-5);
}

.highlight-card.two .card-item {
  justify-content: flex-start;
  flex-wrap: wrap;
  gap: 8px 16px;
}

.highlight-card.two .label-1 { margin-block-end: 4px;}








/*-----------------------------------*\
  #FORECAST
\*-----------------------------------*/

.slider-container {
  overflow-x: auto;
  margin-inline: -16px;
}

.slider-container::-webkit-scrollbar { display: none; }

.slider-list {
  display: flex;
  gap: 12px;
}

.slider-list:first-child { margin-block-end: 16px; }

.slider-list::before,
.slider-list::after {
  content: "";
  min-width: 4px;
}

.slider-item {
  min-width: 110px;
  flex: 1 1 100%;
}

.slider-card { text-align: center; }

.slider-item .weather-icon {
  margin-inline: auto;
  margin-block: 10px;
}








/*-----------------------------------*\
  #LOADING
\*-----------------------------------*/

.loading {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: var(--background);
  display: grid;
  place-items: center;
  z-index: 1;
  display: none;
}

.loading::before {
  content: "";
  width: 48px;
  height: 48px;
  border: 4px solid var(--on-background);
  border-block-start-color: transparent;
  border-radius: var(--radius-circle);
  animation: loading 500ms linear infinite;
}








/*-----------------------------------*\
  #ERROR SECTION
\*-----------------------------------*/

.error-content {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100vh;
  height: 100svh;  /* for mobile browsers*/
  background-color: var(--background);
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  display: none;
  z-index: 8;
}

.error-content .btn-primary { margin-block-start: 20px; }







/*-----------------------------------*\
  #FOOTER
\*-----------------------------------*/

.footer,
.footer .body-3:last-child {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
}

.footer {
  color: var(--on-surface-variant);
  text-align: center;
  gap: 12px 24px;
  margin-block-start: 20px;
  display: none;
}

.fade-in .footer { display: flex; }

.footer .body-3:last-child { gap: 6px; }








/*-----------------------------------*\
  #ANIMATION
\*-----------------------------------*/

@keyframes ripple {
  0% {
    clip-path: circle(0% at 50% 50%);
  }
  100% {
    clip-path: circle(100% at 50% 50%);
  }
}

@keyframes loading {
  0% {
    transform: translateY(-50%) rotate(0);
  }
  100% {
    transform: translateY(-50%) rotate(1turn);
  }
}

@keyframes fade-in {
  0% { opacity: 0; }
  100% { opacity: 1; }
}







/*-----------------------------------*\
  #MEDIA QUERIES
\*-----------------------------------*/

/* for >768px screens */
@media (min-width: 769px) {

  /* REUSED STYLE */
  .container { padding: 24px; }

  .title-1 {--title-1: 2.4rem}

  .section > .title-2 { margin-block-end: 16px; }

  .card-lg { padding: 24px; }

  .card-sm {
    padding: 20px;
    display: grid;
    grid-template-rows: min-content 1fr;
  }

  .badge {
    top: 20px;
    right: 20px;
  }



  /* HEADER */
  .header-actions { gap: 24px; }

  .header .btn-primary { padding-inline: 16px 24px; }

  .header .btn-primary .span { display: block; }

  .search-view { clip-path: circle(3% at calc(100% - 273px) 6%);}



  /* MAIN */
  main {
    height: calc(100vh - 96px);
    height: calc(100svh - 96px);
  }

  article.container {
    padding-block-start: 0;
    grid-template-columns: 280px minmax(0, 1fr);
    align-items: flex-start;
    gap: 24px;
  }

  .content-left {
    position: sticky;
    top: 0;
  }

  .section:not(:last-child) { margin-block: 20px; }

  .forecast-card .card-item:not(:last-child) { margin-block-end: 16px;}

  .highlight-list { grid-template-columns: 1fr 1fr;}

  .highlight-card:nth-child(-n+2) {
    grid-column: span 2;
    height: 160px;
  }

  .highlight-card:nth-child(n+3) { height: 120px; }

  .highlights .m-icon {font-size: 3.6rem; }

  .highlight-card.one .card-item {
    width: 25%;
    flex-direction: column-reverse;
    gap: 8px;
  }

  .slider-container {
    margin-inline: 0 -24px;
    border-bottom-left-radius: var(--radius-16);
    border-top-left-radius: var(--radius-16);
  }

  .slider-list::before { display: none; }

  .slider-list::after { min-width: 12px; }

  .hourly-forecast .card-sm { padding: 16px; }
}


/* for >1200px screens */
@media (min-width: 1200px) {
  /* CUSTOM PROPERTY */
  :root {

    /* font-size */
    --heading: 8rem;
    --title-2: 2rem;
  }


  /* REUSED STYLES */
  .container { padding: 40px; }

  .card-lg { padding: 36px; }

  .card-sm { padding: 24px; }

  .title-1 { --title-1: 3.6rem; }

  .highlight-card.two .card-item { column-gap: 24px; }



  /* HEADER */
  .header .icon-btn { display: none; }

  .logo img { width: 200px; }

  .header {
    position: relative;
    height: 120px;
    z-index: 4;
  }

  .header .container {
    padding-block: 0;
    height: 100%;
  }

  .search-view,
  .search-view.active {
    all: unset;
    display: block;
    position: relative;
    width: 500px;
    animation: none;
  }

  .search-wrapper { border-block-end: none; }

  .search-wrapper > .m-icon { display: block; }

  .search-field,
  .search-view .view-list { background-color: var(--surface);}

  .search-field {
    height: 56px;
    border-radius: var(--radius-28);
  }

  .search-result,
  .search-view:not(:focus-within) .search-result { display: none; }

  .search-view:focus-within .search-result.active { display: block; }

  .search-view:has(.search-result.active):focus-within .search-field {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
  }

  .search-view .view-list {
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    max-height: 360px;
    border-radius: 0 0 var(--radius-28) var(--radius-28);
    border-block-start: 1px solid var(--outline);
    overflow-y: auto;   /* for firefox */
    overflow-y: overlay;
  }

  .search-view .view-list:empty { min-height: 120px; }

  .search-view .view-list::-webkit-scrollbar-button { height: 20px; }

  .search-view :is(:hover, :has(.view-list):hover) {
    filter: drop-shadow(var(--shadow-1));
  }

  .search-view :is(:focus-within, :has(.view-list):focus-within) {
    filter: drop-shadow(var(--shadow-2));
  }


  /* MAIN */
  main {
    height: calc(100vh - 120px);
    height: calc(100svh - 120px);
  }

  article.container {
    grid-template-columns: 360px minmax(0, 1fr);
    gap: 40px;
  }

  .currrent-weather .weather-icon { width: 80px; }

  .forecast-card .title-2 { --title-2: 2.2rem; }

  .highlight-card:nth-child(-n+2) { height: 200px; }

  .highlight-card:nth-child(n+3) { height: 150px; }

  .highlight-card .m-icon { font-size: 4.8rem; }

  .slider-list { gap: 16px; }
}


/* for >1400px screens */
@media (min-width: 1400px) {
  .highlight-list { grid-template-columns: repeat(4, 1fr); }
}
80 / 100 SEO Score

2 thoughts on “Weather Web Project Using HTML CSS and Javascript”

  1. Fantastic web site. Lots of useful information here. I’m sending it to several friends ans also sharing in delicious. And of course, thanks for your effort!

  2. Excellent beat ! I wish to apprentice whilst you amend your web site, how could i subscribe for a weblog site? The account aided me a acceptable deal. I have been a little bit familiar of this your broadcast provided brilliant clear idea

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top