Weather App with API Integration – Complete Project IN HTML CSS AND JAVASCRIPT WITH PHP AND MY SQL

Introduction to the Project

The Weather App with API Integration is a dynamic, real-time web application that provides users with current weather information and forecasts for locations worldwide. This project demonstrates how to integrate third-party APIs, handle asynchronous data fetching, and create responsive, user-friendly interfaces. The application fetches live weather data from the OpenWeatherMap API and presents it in an intuitive dashboard with dynamic theming based on weather conditions .

This full-stack application separates frontend presentation from backend API handling, ensuring security for API keys and providing robust error handling. Whether you're a beginner learning API integration or an experienced developer looking for a reference implementation, this weather app offers valuable insights into modern web development practices .

Key Features

Core Features

  • Real-Time Weather Data: Fetch current weather conditions including temperature, humidity, wind speed, and weather descriptions
  • Location Search: Search for weather information by city name worldwide
  • Geolocation Support: Automatically detect and display weather for user's current location
  • 5-Day Weather Forecast: View future weather predictions with min/max temperatures
  • Dynamic Weather Icons: Visual representation of weather conditions with appropriate icons
  • Responsive Design: Mobile-friendly interface that works on all devices

Advanced Features

  • Temperature Unit Toggle: Switch between Celsius and Fahrenheit
  • Search History: Recently searched cities stored in browser's localStorage
  • Dynamic Backgrounds: Background color and theme changes based on weather conditions (sunny, rainy, cloudy, etc.)
  • Error Handling: User-friendly error messages for invalid cities or API failures
  • Loading States: Visual indicators during data fetching
  • Last Updated Timestamp: Shows when weather data was last refreshed

Technical Highlights

  • Secure API Key Storage: Backend proxy to hide API keys from client-side
  • Asynchronous Programming: Using async/await and Promises for API calls
  • DOM Manipulation: Dynamic updating of UI elements based on API responses
  • Form Validation: Input validation and error prevention
  • Caching: API response caching to reduce request frequency

Technology Stack

  • Frontend: HTML5, CSS3, JavaScript (ES6+)
  • Backend: PHP 8.0+ (for API proxy and security)
  • API: OpenWeatherMap API (Current Weather Data and 5-Day Forecast)
  • Additional Libraries:
  • cURL for PHP API requests
  • Font Awesome for icons
  • Dotenv for environment variables

Project File Structure

weather-app/
β”‚
β”œβ”€β”€ assets/
β”‚   β”œβ”€β”€ css/
β”‚   β”‚   β”œβ”€β”€ style.css
β”‚   β”‚   └── responsive.css
β”‚   β”œβ”€β”€ js/
β”‚   β”‚   β”œβ”€β”€ main.js
β”‚   β”‚   β”œβ”€β”€ weather.js
β”‚   β”‚   └── geolocation.js
β”‚   └── images/
β”‚       β”œβ”€β”€ icons/
β”‚       β”‚   β”œβ”€β”€ clear.png
β”‚       β”‚   β”œβ”€β”€ clouds.png
β”‚       β”‚   β”œβ”€β”€ rain.png
β”‚       β”‚   β”œβ”€β”€ snow.png
β”‚       β”‚   β”œβ”€β”€ mist.png
β”‚       β”‚   β”œβ”€β”€ drizzle.png
β”‚       β”‚   └── default.png
β”‚       └── backgrounds/
β”‚
β”œβ”€β”€ api/
β”‚   └── weather.php
β”‚
β”œβ”€β”€ includes/
β”‚   β”œβ”€β”€ config.php
β”‚   β”œβ”€β”€ functions.php
β”‚   └── WeatherService.php
β”‚
β”œβ”€β”€ .env
β”œβ”€β”€ .gitignore
β”œβ”€β”€ index.php
β”œβ”€β”€ composer.json
└── README.md

Database Schema (Optional - for Search History)

If you want to store user search history persistently across sessions, you can use this simple database schema. However, the main version uses browser's localStorage for client-side history.

File: sql/database.sql (Optional)

CREATE DATABASE IF NOT EXISTS `weather_app`;
USE `weather_app`;
-- Search History Table (Optional - for persistent storage)
CREATE TABLE `search_history` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`city_name` VARCHAR(100) NOT NULL,
`country_code` VARCHAR(2),
`search_count` INT DEFAULT 1,
`last_searched` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_city` (`city_name`, `country_code`)
);
-- Sample data
INSERT INTO `search_history` (`city_name`, `country_code`, `search_count`) VALUES
('London', 'GB', 5),
('New York', 'US', 3),
('Tokyo', 'JP', 2);

Environment Configuration

File: .env

# OpenWeatherMap API Configuration
OPENWEATHER_API_KEY=your_api_key_here
# API Endpoints
API_URL_CURRENT=https://api.openweathermap.org/data/2.5/weather
API_URL_FORECAST=https://api.openweathermap.org/data/2.5/forecast
# Application Settings
DEBUG=false
DEFAULT_UNITS=metric  # metric for Celsius, imperial for Fahrenheit
DEFAULT_CITY=London
CACHE_TIME=600  # Cache time in seconds (10 minutes)
# Server Configuration
TIMEZONE=America/New_York

File: .gitignore

# Environment variables
.env
# Dependencies
/vendor/
node_modules/
# IDE files
.vscode/
.idea/
# OS files
.DS_Store
Thumbs.db
# Logs
*.log

Core PHP Files

Configuration File

File: includes/config.php

<?php
/**
* Configuration File for Weather App
* Loads environment variables and sets up constants
*/
// Load environment variables from .env file
function loadEnv($path) {
if (!file_exists($path)) {
return false;
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
list($name, $value) = explode('=', $line, 2);
$name = trim($name);
$value = trim($value);
if (!array_key_exists($name, $_ENV)) {
$_ENV[$name] = $value;
putenv(sprintf('%s=%s', $name, $value));
}
}
return true;
}
// Load environment variables
loadEnv(__DIR__ . '/../.env');
// Define constants
define('API_KEY', getenv('OPENWEATHER_API_KEY') ?: '');
define('API_URL_CURRENT', getenv('API_URL_CURRENT') ?: 'https://api.openweathermap.org/data/2.5/weather');
define('API_URL_FORECAST', getenv('API_URL_FORECAST') ?: 'https://api.openweathermap.org/data/2.5/forecast');
define('DEFAULT_UNITS', getenv('DEFAULT_UNITS') ?: 'metric');
define('DEFAULT_CITY', getenv('DEFAULT_CITY') ?: 'London');
define('CACHE_TIME', getenv('CACHE_TIME') ?: 600);
define('DEBUG', getenv('DEBUG') === 'true');
// Error reporting
if (DEBUG) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
} else {
error_reporting(0);
ini_set('display_errors', 0);
}
// Set timezone
date_default_timezone_set(getenv('TIMEZONE') ?: 'America/New_York');
// Start session if not started
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
// Initialize search history in session if not exists
if (!isset($_SESSION['search_history'])) {
$_SESSION['search_history'] = [];
}
// Check if API key is set
if (empty(API_KEY)) {
die('API Key not configured. Please set OPENWEATHER_API_KEY in .env file.');
}
?>

Helper Functions

File: includes/functions.php

<?php
/**
* Helper Functions for Weather App
*/
/**
* Make API request using cURL
* 
* @param string $url API endpoint URL
* @return array|false Response data or false on failure
*/
function makeApiRequest($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Weather-App/1.0');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("HTTP Error: " . $httpCode . " - URL: " . $url);
return false;
}
return json_decode($response, true);
}
/**
* Get weather data for a city
* 
* @param string $city City name
* @param string $units Units (metric/imperial)
* @return array|false Weather data or false on failure
*/
function getWeatherData($city, $units = 'metric') {
$cacheKey = 'weather_' . md5($city . $units);
$cachedData = getFromCache($cacheKey);
if ($cachedData) {
return $cachedData;
}
$url = API_URL_CURRENT . "?q=" . urlencode($city) . "&appid=" . API_KEY . "&units=" . $units;
$data = makeApiRequest($url);
if ($data && isset($data['cod']) && $data['cod'] == 200) {
saveToCache($cacheKey, $data, CACHE_TIME);
return $data;
}
return false;
}
/**
* Get 5-day forecast for a city
* 
* @param string $city City name
* @param string $units Units (metric/imperial)
* @return array|false Forecast data or false on failure
*/
function getForecastData($city, $units = 'metric') {
$cacheKey = 'forecast_' . md5($city . $units);
$cachedData = getFromCache($cacheKey);
if ($cachedData) {
return $cachedData;
}
$url = API_URL_FORECAST . "?q=" . urlencode($city) . "&appid=" . API_KEY . "&units=" . $units;
$data = makeApiRequest($url);
if ($data && isset($data['cod']) && $data['cod'] == '200') {
saveToCache($cacheKey, $data, CACHE_TIME);
return $data;
}
return false;
}
/**
* Simple file-based caching
* 
* @param string $key Cache key
* @return mixed|false Cached data or false if not found/expired
*/
function getFromCache($key) {
$cacheDir = __DIR__ . '/../cache/';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$cacheFile = $cacheDir . $key . '.cache';
if (file_exists($cacheFile)) {
$content = file_get_contents($cacheFile);
$data = unserialize($content);
if ($data['expires'] > time()) {
return $data['data'];
}
}
return false;
}
/**
* Save data to cache
* 
* @param string $key Cache key
* @param mixed $data Data to cache
* @param int $ttl Time to live in seconds
* @return bool Success status
*/
function saveToCache($key, $data, $ttl = 600) {
$cacheDir = __DIR__ . '/../cache/';
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
$cacheFile = $cacheDir . $key . '.cache';
$cacheData = [
'expires' => time() + $ttl,
'data' => $data
];
return file_put_contents($cacheFile, serialize($cacheData)) !== false;
}
/**
* Format temperature for display
* 
* @param float $temp Temperature value
* @param string $units Units (metric/imperial)
* @return string Formatted temperature
*/
function formatTemperature($temp, $units = 'metric') {
$symbol = ($units === 'metric') ? 'Β°C' : 'Β°F';
return round($temp) . $symbol;
}
/**
* Get weather icon URL from OpenWeatherMap
* 
* @param string $iconCode Icon code from API
* @return string Icon URL
*/
function getWeatherIconUrl($iconCode) {
return "https://openweathermap.org/img/wn/{$iconCode}@2x.png";
}
/**
* Get CSS class based on weather condition
* 
* @param string $weatherMain Main weather condition
* @return string CSS class name
*/
function getWeatherThemeClass($weatherMain) {
$themes = [
'Clear' => 'weather-sunny',
'Clouds' => 'weather-cloudy',
'Rain' => 'weather-rainy',
'Drizzle' => 'weather-drizzle',
'Thunderstorm' => 'weather-storm',
'Snow' => 'weather-snowy',
'Mist' => 'weather-misty',
'Fog' => 'weather-foggy',
'Haze' => 'weather-hazy'
];
return $themes[$weatherMain] ?? 'weather-default';
}
/**
* Add city to search history
* 
* @param string $city City name
* @param string $country Country code
*/
function addToSearchHistory($city, $country = '') {
if (!isset($_SESSION['search_history'])) {
$_SESSION['search_history'] = [];
}
$entry = [
'city' => $city,
'country' => $country,
'timestamp' => time()
];
// Remove if already exists
foreach ($_SESSION['search_history'] as $key => $item) {
if ($item['city'] === $city && $item['country'] === $country) {
unset($_SESSION['search_history'][$key]);
}
}
// Add to beginning
array_unshift($_SESSION['search_history'], $entry);
// Keep only last 10 searches
$_SESSION['search_history'] = array_slice($_SESSION['search_history'], 0, 10);
}
/**
* Get search history
* 
* @return array Search history
*/
function getSearchHistory() {
return $_SESSION['search_history'] ?? [];
}
/**
* Sanitize input
* 
* @param string $input Raw input
* @return string Sanitized input
*/
function sanitize($input) {
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
/**
* Log error message
* 
* @param string $message Error message
* @param array $context Additional context
*/
function logError($message, $context = []) {
$logFile = __DIR__ . '/../logs/error.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? ' ' . json_encode($context) : '';
$logMessage = "[{$timestamp}] {$message}{$contextStr}\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
}
?>

Weather Service Class

File: includes/WeatherService.php

<?php
/**
* Weather Service Class
* Handles all weather-related operations
*/
class WeatherService {
private $apiKey;
private $apiUrlCurrent;
private $apiUrlForecast;
private $units;
/**
* Constructor
* 
* @param string $apiKey OpenWeatherMap API key
* @param string $units Units (metric/imperial)
*/
public function __construct($apiKey, $units = 'metric') {
$this->apiKey = $apiKey;
$this->apiUrlCurrent = 'https://api.openweathermap.org/data/2.5/weather';
$this->apiUrlForecast = 'https://api.openweathermap.org/data/2.5/forecast';
$this->units = $units;
}
/**
* Get current weather by city name
* 
* @param string $city City name
* @return array Weather data
* @throws Exception If API request fails
*/
public function getCurrentWeatherByCity($city) {
$url = $this->apiUrlCurrent . "?q=" . urlencode($city) . 
"&appid=" . $this->apiKey . "&units=" . $this->units;
return $this->makeRequest($url);
}
/**
* Get current weather by coordinates
* 
* @param float $lat Latitude
* @param float $lon Longitude
* @return array Weather data
* @throws Exception If API request fails
*/
public function getCurrentWeatherByCoords($lat, $lon) {
$url = $this->apiUrlCurrent . "?lat=" . $lat . "&lon=" . $lon . 
"&appid=" . $this->apiKey . "&units=" . $this->units;
return $this->makeRequest($url);
}
/**
* Get 5-day forecast by city name
* 
* @param string $city City name
* @return array Forecast data
* @throws Exception If API request fails
*/
public function getForecastByCity($city) {
$url = $this->apiUrlForecast . "?q=" . urlencode($city) . 
"&appid=" . $this->apiKey . "&units=" . $this->units;
return $this->makeRequest($url);
}
/**
* Make API request
* 
* @param string $url API URL
* @return array Decoded JSON response
* @throws Exception If request fails
*/
private function makeRequest($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Weather-App/1.0');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
throw new Exception("cURL Error: " . $error);
}
if ($httpCode !== 200) {
$data = json_decode($response, true);
$message = isset($data['message']) ? $data['message'] : 'Unknown error';
throw new Exception("API Error: " . $message, $httpCode);
}
return json_decode($response, true);
}
/**
* Process weather data for display
* 
* @param array $data Raw weather data
* @return array Processed weather data
*/
public function processWeatherData($data) {
return [
'city' => $data['name'],
'country' => $data['sys']['country'],
'temperature' => round($data['main']['temp']),
'feels_like' => round($data['main']['feels_like']),
'temp_min' => round($data['main']['temp_min']),
'temp_max' => round($data['main']['temp_max']),
'pressure' => $data['main']['pressure'],
'humidity' => $data['main']['humidity'],
'wind_speed' => $data['wind']['speed'],
'wind_deg' => $data['wind']['deg'],
'clouds' => $data['clouds']['all'],
'weather_main' => $data['weather'][0]['main'],
'weather_description' => $data['weather'][0]['description'],
'weather_icon' => $data['weather'][0]['icon'],
'icon_url' => "https://openweathermap.org/img/wn/" . $data['weather'][0]['icon'] . "@2x.png",
'timestamp' => $data['dt'],
'timezone' => $data['timezone']
];
}
/**
* Process forecast data for display
* 
* @param array $data Raw forecast data
* @return array Processed forecast data
*/
public function processForecastData($data) {
$processed = [];
$daily = [];
// Group by day
foreach ($data['list'] as $item) {
$date = date('Y-m-d', $item['dt']);
if (!isset($daily[$date])) {
$daily[$date] = [
'temps' => [],
'weather' => [],
'icons' => []
];
}
$daily[$date]['temps'][] = $item['main']['temp'];
$daily[$date]['weather'][] = $item['weather'][0]['main'];
$daily[$date]['icons'][] = $item['weather'][0]['icon'];
$daily[$date]['humidity'][] = $item['main']['humidity'];
$daily[$date]['wind'][] = $item['wind']['speed'];
}
// Process each day
foreach ($daily as $date => $dayData) {
$weatherCounts = array_count_values($dayData['weather']);
$dominantWeather = array_search(max($weatherCounts), $weatherCounts);
$iconCounts = array_count_values($dayData['icons']);
$dominantIcon = array_search(max($iconCounts), $iconCounts);
$processed[] = [
'date' => $date,
'day_name' => date('l', strtotime($date)),
'temp_min' => round(min($dayData['temps'])),
'temp_max' => round(max($dayData['temps'])),
'weather_main' => $dominantWeather,
'weather_icon' => $dominantIcon,
'icon_url' => "https://openweathermap.org/img/wn/" . $dominantIcon . ".png",
'humidity_avg' => round(array_sum($dayData['humidity']) / count($dayData['humidity'])),
'wind_avg' => round(array_sum($dayData['wind']) / count($dayData['wind']), 1)
];
}
// Return first 5 days
return array_slice($processed, 0, 5);
}
/**
* Set units
* 
* @param string $units Units (metric/imperial)
*/
public function setUnits($units) {
$this->units = $units;
}
/**
* Get temperature symbol
* 
* @return string Temperature symbol
*/
public function getTempSymbol() {
return $this->units === 'metric' ? 'Β°C' : 'Β°F';
}
/**
* Get wind speed unit
* 
* @return string Wind speed unit
*/
public function getWindUnit() {
return $this->units === 'metric' ? 'm/s' : 'mph';
}
}
?>

API Endpoint

File: api/weather.php

<?php
/**
* Weather API Endpoint
* Handles all weather-related API requests from frontend
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST');
header('Access-Control-Allow-Headers: Content-Type');
require_once '../includes/config.php';
require_once '../includes/WeatherService.php';
require_once '../includes/functions.php';
// Initialize response
$response = [
'success' => false,
'data' => null,
'error' => null
];
try {
// Get request parameters
$action = $_GET['action'] ?? '';
$city = isset($_GET['city']) ? sanitize($_GET['city']) : '';
$lat = isset($_GET['lat']) ? floatval($_GET['lat']) : null;
$lon = isset($_GET['lon']) ? floatval($_GET['lon']) : null;
$units = isset($_GET['units']) && $_GET['units'] === 'imperial' ? 'imperial' : 'metric';
// Validate API key
if (empty(API_KEY)) {
throw new Exception('API key not configured');
}
// Initialize weather service
$weatherService = new WeatherService(API_KEY, $units);
switch ($action) {
case 'current':
if ($city) {
// Get weather by city name
$data = $weatherService->getCurrentWeatherByCity($city);
$processed = $weatherService->processWeatherData($data);
// Add to search history
addToSearchHistory($processed['city'], $processed['country']);
$response['success'] = true;
$response['data'] = $processed;
$response['units'] = [
'temp' => $weatherService->getTempSymbol(),
'wind' => $weatherService->getWindUnit()
];
} elseif ($lat && $lon) {
// Get weather by coordinates
$data = $weatherService->getCurrentWeatherByCoords($lat, $lon);
$processed = $weatherService->processWeatherData($data);
$response['success'] = true;
$response['data'] = $processed;
$response['units'] = [
'temp' => $weatherService->getTempSymbol(),
'wind' => $weatherService->getWindUnit()
];
} else {
throw new Exception('City or coordinates required');
}
break;
case 'forecast':
if (!$city) {
throw new Exception('City name required for forecast');
}
$data = $weatherService->getForecastByCity($city);
$processed = $weatherService->processForecastData($data);
$response['success'] = true;
$response['data'] = $processed;
$response['units'] = [
'temp' => $weatherService->getTempSymbol(),
'wind' => $weatherService->getWindUnit()
];
break;
case 'history':
$response['success'] = true;
$response['data'] = getSearchHistory();
break;
default:
throw new Exception('Invalid action');
}
} catch (Exception $e) {
$response['error'] = $e->getMessage();
logError($e->getMessage(), ['action' => $action, 'city' => $city]);
}
echo json_encode($response);
?>

Frontend Files

Main HTML Page

File: index.php

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather App - Real-Time Weather & Forecast</title>
<!-- CSS -->
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="assets/css/responsive.css">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap" rel="stylesheet">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="header-content">
<h1>
<i class="fas fa-cloud-sun"></i>
Weather App
</h1>
<p class="subtitle">Real-time weather information powered by OpenWeatherMap</p>
</div>
<!-- Unit Toggle -->
<div class="unit-toggle">
<button id="celsiusBtn" class="unit-btn active">Β°C</button>
<button id="fahrenheitBtn" class="unit-btn">Β°F</button>
</div>
</header>
<!-- Search Section -->
<section class="search-section">
<div class="search-container">
<div class="search-box">
<i class="fas fa-search search-icon"></i>
<input 
type="text" 
id="cityInput" 
placeholder="Enter city name (e.g., London, New York, Tokyo)"
autocomplete="off"
>
<button id="searchBtn" class="search-btn">
<i class="fas fa-magnifying-glass"></i>
Search
</button>
</div>
<button id="locationBtn" class="location-btn" title="Use my current location">
<i class="fas fa-location-dot"></i>
<span>Current Location</span>
</button>
</div>
<!-- Error Message -->
<div id="errorMessage" class="error-message" style="display: none;">
<i class="fas fa-exclamation-circle"></i>
<span></span>
</div>
</section>
<!-- Loading Indicator -->
<div id="loading" class="loading" style="display: none;">
<div class="spinner"></div>
<p>Fetching weather data...</p>
</div>
<!-- Weather Display -->
<section id="weatherDisplay" class="weather-display" style="display: none;">
<!-- Current Weather Card -->
<div class="current-weather">
<div class="weather-header">
<div class="location-info">
<h2 id="cityName">London, GB</h2>
<p id="dateTime" class="date-time"></p>
</div>
<div class="weather-main">
<img id="weatherIcon" src="" alt="Weather Icon" class="weather-icon-large">
<div class="temperature">
<span id="temperature">22</span>
<span class="temp-unit">Β°C</span>
</div>
<p id="weatherDescription" class="weather-description">Clear Sky</p>
</div>
</div>
<div class="weather-details">
<div class="detail-item">
<i class="fas fa-temperature-low"></i>
<div class="detail-info">
<span class="detail-label">Feels Like</span>
<span id="feelsLike" class="detail-value">22Β°C</span>
</div>
</div>
<div class="detail-item">
<i class="fas fa-droplet"></i>
<div class="detail-info">
<span class="detail-label">Humidity</span>
<span id="humidity" class="detail-value">65%</span>
</div>
</div>
<div class="detail-item">
<i class="fas fa-wind"></i>
<div class="detail-info">
<span class="detail-label">Wind Speed</span>
<span id="windSpeed" class="detail-value">5 m/s</span>
</div>
</div>
<div class="detail-item">
<i class="fas fa-gauge-high"></i>
<div class="detail-info">
<span class="detail-label">Pressure</span>
<span id="pressure" class="detail-value">1012 hPa</span>
</div>
</div>
<div class="detail-item">
<i class="fas fa-eye"></i>
<div class="detail-info">
<span class="detail-label">Min Temp</span>
<span id="tempMin" class="detail-value">18Β°C</span>
</div>
</div>
<div class="detail-item">
<i class="fas fa-eye"></i>
<div class="detail-info">
<span class="detail-label">Max Temp</span>
<span id="tempMax" class="detail-value">25Β°C</span>
</div>
</div>
</div>
</div>
<!-- 5-Day Forecast -->
<div class="forecast-section">
<h3><i class="fas fa-calendar"></i> 5-Day Forecast</h3>
<div id="forecastContainer" class="forecast-container">
<!-- Forecast cards will be inserted here by JavaScript -->
</div>
</div>
<!-- Search History -->
<div id="historySection" class="history-section" style="display: none;">
<h3><i class="fas fa-history"></i> Recent Searches</h3>
<div id="historyList" class="history-list">
<!-- History items will be inserted here by JavaScript -->
</div>
</div>
</section>
<!-- Default/Welcome Message -->
<section id="welcomeMessage" class="welcome-message">
<i class="fas fa-cloud-sun-rain"></i>
<h2>Check Weather Anywhere</h2>
<p>Search for a city or use your current location to get real-time weather information and 5-day forecast.</p>
</section>
</div>
<!-- JavaScript -->
<script src="assets/js/weather.js"></script>
<script src="assets/js/geolocation.js"></script>
<script src="assets/js/main.js"></script>
</body>
</html>

CSS Styling

File: assets/css/style.css

/* Global Styles */
:root {
--primary-color: #4a90e2;
--primary-dark: #3a7bc8;
--secondary-color: #50c878;
--danger-color: #e74c3c;
--warning-color: #f39c12;
--dark-bg: #2c3e50;
--light-bg: #f5f7fa;
--text-primary: #333333;
--text-secondary: #666666;
--text-light: #888888;
--white: #ffffff;
--shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
--border-radius: 12px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: var(--text-primary);
}
.container {
max-width: 1200px;
margin: 0 auto;
}
/* Header Styles */
.header {
background: rgba(255, 255, 255, 0.95);
border-radius: var(--border-radius);
padding: 30px;
margin-bottom: 30px;
box-shadow: var(--shadow);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.header-content h1 {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 15px;
}
.header-content h1 i {
color: var(--warning-color);
}
.subtitle {
color: var(--text-secondary);
font-size: 1rem;
}
/* Unit Toggle */
.unit-toggle {
display: flex;
gap: 10px;
background: var(--light-bg);
padding: 5px;
border-radius: 50px;
}
.unit-btn {
padding: 8px 20px;
border: none;
background: transparent;
border-radius: 50px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.unit-btn.active {
background: var(--primary-color);
color: var(--white);
}
/* Search Section */
.search-section {
margin-bottom: 30px;
}
.search-container {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.search-box {
flex: 1;
display: flex;
background: var(--white);
border-radius: 50px;
overflow: hidden;
box-shadow: var(--shadow);
min-width: 300px;
}
.search-icon {
display: flex;
align-items: center;
padding-left: 20px;
color: var(--text-light);
}
.search-box input {
flex: 1;
padding: 15px;
border: none;
font-size: 1rem;
font-family: inherit;
}
.search-box input:focus {
outline: none;
}
.search-btn {
padding: 15px 30px;
background: var(--primary-color);
color: var(--white);
border: none;
cursor: pointer;
font-weight: 600;
transition: background 0.3s ease;
display: flex;
align-items: center;
gap: 10px;
}
.search-btn:hover {
background: var(--primary-dark);
}
.location-btn {
padding: 15px 25px;
background: var(--secondary-color);
color: var(--white);
border: none;
border-radius: 50px;
cursor: pointer;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
transition: background 0.3s ease;
box-shadow: var(--shadow);
}
.location-btn:hover {
background: #45b069;
}
/* Error Message */
.error-message {
background: #fee;
color: var(--danger-color);
padding: 15px 20px;
border-radius: var(--border-radius);
margin-top: 15px;
display: flex;
align-items: center;
gap: 10px;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Loading Indicator */
.loading {
text-align: center;
padding: 50px;
background: var(--white);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.spinner {
border: 4px solid var(--light-bg);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Current Weather Card */
.current-weather {
background: var(--white);
border-radius: var(--border-radius);
padding: 30px;
margin-bottom: 30px;
box-shadow: var(--shadow);
}
.weather-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 30px;
margin-bottom: 30px;
}
.location-info h2 {
font-size: 2rem;
margin-bottom: 10px;
}
.date-time {
color: var(--text-secondary);
font-size: 1rem;
}
.weather-main {
text-align: right;
}
.weather-icon-large {
width: 100px;
height: 100px;
}
.temperature {
font-size: 4rem;
font-weight: 700;
color: var(--primary-color);
line-height: 1;
}
.temp-unit {
font-size: 2rem;
color: var(--text-light);
margin-left: 5px;
}
.weather-description {
font-size: 1.2rem;
color: var(--text-secondary);
text-transform: capitalize;
}
/* Weather Details Grid */
.weather-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 30px;
}
.detail-item {
display: flex;
align-items: center;
gap: 15px;
padding: 20px;
background: var(--light-bg);
border-radius: 10px;
transition: transform 0.3s ease;
}
.detail-item:hover {
transform: translateY(-5px);
}
.detail-item i {
font-size: 2rem;
color: var(--primary-color);
}
.detail-info {
flex: 1;
}
.detail-label {
display: block;
color: var(--text-secondary);
font-size: 0.9rem;
margin-bottom: 5px;
}
.detail-value {
font-size: 1.3rem;
font-weight: 600;
color: var(--text-primary);
}
/* Forecast Section */
.forecast-section {
background: var(--white);
border-radius: var(--border-radius);
padding: 30px;
margin-bottom: 30px;
box-shadow: var(--shadow);
}
.forecast-section h3 {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
color: var(--text-primary);
}
.forecast-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 15px;
}
.forecast-card {
background: var(--light-bg);
padding: 20px;
border-radius: 10px;
text-align: center;
transition: transform 0.3s ease;
}
.forecast-card:hover {
transform: translateY(-5px);
}
.forecast-day {
font-weight: 600;
color: var(--text-primary);
margin-bottom: 10px;
}
.forecast-icon {
width: 50px;
height: 50px;
margin: 10px auto;
}
.forecast-temp {
display: flex;
justify-content: center;
gap: 10px;
margin: 10px 0;
}
.forecast-max {
font-weight: 600;
color: var(--danger-color);
}
.forecast-min {
color: var(--primary-color);
}
.forecast-desc {
color: var(--text-secondary);
font-size: 0.9rem;
text-transform: capitalize;
margin-top: 5px;
}
.forecast-humidity,
.forecast-wind {
font-size: 0.85rem;
color: var(--text-light);
margin-top: 5px;
}
.forecast-humidity i,
.forecast-wind i {
margin-right: 5px;
color: var(--primary-color);
}
/* History Section */
.history-section {
background: var(--white);
border-radius: var(--border-radius);
padding: 30px;
box-shadow: var(--shadow);
}
.history-section h3 {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.history-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.history-item {
background: var(--light-bg);
padding: 8px 15px;
border-radius: 50px;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.3s ease;
}
.history-item:hover {
background: var(--primary-color);
color: var(--white);
}
.history-item i {
margin-right: 5px;
font-size: 0.8rem;
}
/* Welcome Message */
.welcome-message {
text-align: center;
padding: 80px 20px;
background: var(--white);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
}
.welcome-message i {
font-size: 5rem;
color: var(--primary-color);
margin-bottom: 20px;
}
.welcome-message h2 {
font-size: 2rem;
margin-bottom: 15px;
}
.welcome-message p {
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
}
/* Theme Classes */
.weather-sunny {
background: linear-gradient(135deg, #f6d365 0%, #fda085 100%);
}
.weather-cloudy {
background: linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%);
}
.weather-rainy {
background: linear-gradient(135deg, #4b79a1 0%, #283e51 100%);
}
.weather-snowy {
background: linear-gradient(135deg, #e6e9f0 0%, #eef2f3 100%);
}
.weather-storm {
background: linear-gradient(135deg, #373b44 0%, #4286f4 100%);
}
/* Utility Classes */
.hidden {
display: none !important;
}

Responsive CSS

File: assets/css/responsive.css

/* Tablet Styles */
@media screen and (max-width: 768px) {
.header {
flex-direction: column;
text-align: center;
}
.header-content h1 {
font-size: 2rem;
justify-content: center;
}
.search-container {
flex-direction: column;
}
.search-box {
width: 100%;
}
.location-btn {
width: 100%;
justify-content: center;
}
.weather-header {
flex-direction: column;
text-align: center;
}
.weather-main {
text-align: center;
}
.temperature {
font-size: 3rem;
}
.weather-details {
grid-template-columns: 1fr;
}
.forecast-container {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}
.welcome-message {
padding: 50px 20px;
}
.welcome-message h2 {
font-size: 1.5rem;
}
}
/* Mobile Styles */
@media screen and (max-width: 480px) {
body {
padding: 10px;
}
.header {
padding: 20px;
}
.header-content h1 {
font-size: 1.5rem;
}
.unit-btn {
padding: 6px 15px;
}
.search-btn {
padding: 12px 20px;
}
.search-btn span {
display: none;
}
.location-btn span {
display: none;
}
.current-weather {
padding: 20px;
}
.temperature {
font-size: 2.5rem;
}
.weather-icon-large {
width: 70px;
height: 70px;
}
.detail-item {
padding: 15px;
}
.detail-value {
font-size: 1.1rem;
}
.forecast-container {
grid-template-columns: 1fr;
}
.history-item {
font-size: 0.8rem;
padding: 6px 12px;
}
.welcome-message i {
font-size: 3rem;
}
.welcome-message h2 {
font-size: 1.2rem;
}
.welcome-message p {
font-size: 0.9rem;
}
}
/* Landscape Mode */
@media screen and (max-height: 600px) and (orientation: landscape) {
.weather-header {
flex-direction: row;
}
.weather-details {
grid-template-columns: repeat(3, 1fr);
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
:root {
--light-bg: #2c3e50;
--white: #34495e;
--text-primary: #ecf0f1;
--text-secondary: #bdc3c7;
--text-light: #95a5a6;
}
body {
background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
}
.search-box input {
background: var(--white);
color: var(--text-primary);
}
.search-box input::placeholder {
color: var(--text-light);
}
.history-item {
color: var(--text-primary);
}
.detail-item {
background: #2c3e50;
}
}
/* High Resolution Screens */
@media screen and (min-width: 1400px) {
.container {
max-width: 1400px;
}
.weather-details {
grid-template-columns: repeat(6, 1fr);
}
.forecast-container {
grid-template-columns: repeat(5, 1fr);
}
}
/* Print Styles */
@media print {
.search-section,
.location-btn,
.unit-toggle,
.history-section {
display: none !important;
}
.current-weather,
.forecast-section {
break-inside: avoid;
box-shadow: none;
border: 1px solid #ddd;
}
}

Main JavaScript

File: assets/js/main.js

/**
* Main JavaScript for Weather App
* Handles UI interactions and coordinates weather data fetching
*/
class WeatherApp {
constructor() {
this.apiBaseUrl = 'api/weather.php';
this.currentUnits = 'metric'; // metric or imperial
this.currentCity = null;
this.weatherData = null;
this.forecastData = null;
// DOM Elements
this.cityInput = document.getElementById('cityInput');
this.searchBtn = document.getElementById('searchBtn');
this.locationBtn = document.getElementById('locationBtn');
this.celsiusBtn = document.getElementById('celsiusBtn');
this.fahrenheitBtn = document.getElementById('fahrenheitBtn');
this.weatherDisplay = document.getElementById('weatherDisplay');
this.welcomeMessage = document.getElementById('welcomeMessage');
this.loading = document.getElementById('loading');
this.errorMessage = document.getElementById('errorMessage');
// Initialize event listeners
this.initEventListeners();
// Load last searched city from localStorage
this.loadLastCity();
}
initEventListeners() {
// Search button click
this.searchBtn.addEventListener('click', () => this.handleSearch());
// Enter key in input
this.cityInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.handleSearch();
}
});
// Location button click
this.locationBtn.addEventListener('click', () => this.handleGeolocation());
// Unit toggle
this.celsiusBtn.addEventListener('click', () => this.toggleUnits('metric'));
this.fahrenheitBtn.addEventListener('click', () => this.toggleUnits('imperial'));
// Input focus
this.cityInput.addEventListener('focus', () => {
this.cityInput.classList.add('focused');
});
this.cityInput.addEventListener('blur', () => {
this.cityInput.classList.remove('focused');
});
}
async handleSearch() {
const city = this.cityInput.value.trim();
if (!city) {
this.showError('Please enter a city name');
return;
}
this.currentCity = city;
await this.fetchWeatherData(city);
}
async handleGeolocation() {
if (!navigator.geolocation) {
this.showError('Geolocation is not supported by your browser');
return;
}
this.showLoading();
navigator.geolocation.getCurrentPosition(
(position) => this.fetchWeatherByCoords(position.coords.latitude, position.coords.longitude),
(error) => {
this.hideLoading();
switch(error.code) {
case error.PERMISSION_DENIED:
this.showError('Please allow location access to use this feature');
break;
case error.POSITION_UNAVAILABLE:
this.showError('Location information is unavailable');
break;
case error.TIMEOUT:
this.showError('Location request timed out');
break;
default:
this.showError('An unknown error occurred');
}
}
);
}
async fetchWeatherData(city) {
this.showLoading();
this.hideError();
try {
// Fetch current weather
const weatherResponse = await fetch(
`${this.apiBaseUrl}?action=current&city=${encodeURIComponent(city)}&units=${this.currentUnits}`
);
const weatherResult = await weatherResponse.json();
if (!weatherResult.success) {
throw new Error(weatherResult.error || 'Failed to fetch weather data');
}
this.weatherData = weatherResult.data;
// Fetch forecast
const forecastResponse = await fetch(
`${this.apiBaseUrl}?action=forecast&city=${encodeURIComponent(city)}&units=${this.currentUnits}`
);
const forecastResult = await forecastResponse.json();
if (forecastResult.success) {
this.forecastData = forecastResult.data;
}
// Update UI
this.updateUI();
this.saveLastCity(city);
this.hideLoading();
} catch (error) {
console.error('Error fetching weather:', error);
this.showError(error.message || 'Failed to fetch weather data. Please try again.');
this.hideLoading();
}
}
async fetchWeatherByCoords(lat, lon) {
this.showLoading();
this.hideError();
try {
const response = await fetch(
`${this.apiBaseUrl}?action=current&lat=${lat}&lon=${lon}&units=${this.currentUnits}`
);
const result = await response.json();
if (!result.success) {
throw new Error(result.error || 'Failed to fetch weather data');
}
this.weatherData = result.data;
this.currentCity = this.weatherData.city;
// Fetch forecast for this city
const forecastResponse = await fetch(
`${this.apiBaseUrl}?action=forecast&city=${encodeURIComponent(this.currentCity)}&units=${this.currentUnits}`
);
const forecastResult = await forecastResponse.json();
if (forecastResult.success) {
this.forecastData = forecastResult.data;
}
// Update UI
this.updateUI();
this.cityInput.value = this.currentCity;
this.saveLastCity(this.currentCity);
this.hideLoading();
} catch (error) {
console.error('Error fetching weather:', error);
this.showError(error.message || 'Failed to fetch weather data. Please try again.');
this.hideLoading();
}
}
updateUI() {
if (!this.weatherData) return;
// Hide welcome message, show weather display
this.welcomeMessage.style.display = 'none';
this.weatherDisplay.style.display = 'block';
// Update current weather
this.updateCurrentWeather();
// Update forecast
if (this.forecastData) {
this.updateForecast();
}
// Update theme based on weather
this.updateTheme();
// Load and display search history
this.loadSearchHistory();
}
updateCurrentWeather() {
const data = this.weatherData;
const tempSymbol = this.currentUnits === 'metric' ? 'Β°C' : 'Β°F';
const windUnit = this.currentUnits === 'metric' ? 'm/s' : 'mph';
document.getElementById('cityName').textContent = `${data.city}, ${data.country}`;
document.getElementById('dateTime').textContent = this.formatDate(data.timestamp, data.timezone);
document.getElementById('weatherIcon').src = data.icon_url;
document.getElementById('weatherIcon').alt = data.weather_description;
document.getElementById('temperature').textContent = data.temperature;
document.getElementById('tempUnit').textContent = tempSymbol;
document.getElementById('weatherDescription').textContent = data.weather_description;
document.getElementById('feelsLike').textContent = `${data.feels_like}${tempSymbol}`;
document.getElementById('humidity').textContent = `${data.humidity}%`;
document.getElementById('windSpeed').textContent = `${data.wind_speed} ${windUnit}`;
document.getElementById('pressure').textContent = `${data.pressure} hPa`;
document.getElementById('tempMin').textContent = `${data.temp_min}${tempSymbol}`;
document.getElementById('tempMax').textContent = `${data.temp_max}${tempSymbol}`;
}
updateForecast() {
const container = document.getElementById('forecastContainer');
container.innerHTML = '';
this.forecastData.forEach(day => {
const card = this.createForecastCard(day);
container.appendChild(card);
});
}
createForecastCard(day) {
const tempSymbol = this.currentUnits === 'metric' ? 'Β°C' : 'Β°F';
const windUnit = this.currentUnits === 'metric' ? 'm/s' : 'mph';
const card = document.createElement('div');
card.className = 'forecast-card';
card.innerHTML = `
<div class="forecast-day">${day.day_name}</div>
<div class="forecast-date">${this.formatDateShort(day.date)}</div>
<img class="forecast-icon" src="${day.icon_url}" alt="${day.weather_main}">
<div class="forecast-temp">
<span class="forecast-max">${day.temp_max}${tempSymbol}</span>
<span class="forecast-min">${day.temp_min}${tempSymbol}</span>
</div>
<div class="forecast-desc">${day.weather_main}</div>
<div class="forecast-humidity">
<i class="fas fa-droplet"></i> ${day.humidity_avg}%
</div>
<div class="forecast-wind">
<i class="fas fa-wind"></i> ${day.wind_avg} ${windUnit}
</div>
`;
return card;
}
updateTheme() {
const weatherMain = this.weatherData.weather_main;
const themeClass = this.getWeatherThemeClass(weatherMain);
// Remove existing theme classes
document.body.classList.remove(
'weather-sunny', 'weather-cloudy', 'weather-rainy',
'weather-snowy', 'weather-storm'
);
// Add new theme class
if (themeClass) {
document.body.classList.add(themeClass);
}
}
getWeatherThemeClass(weather) {
const themes = {
'Clear': 'weather-sunny',
'Clouds': 'weather-cloudy',
'Rain': 'weather-rainy',
'Drizzle': 'weather-rainy',
'Thunderstorm': 'weather-storm',
'Snow': 'weather-snowy',
'Mist': 'weather-cloudy',
'Fog': 'weather-cloudy',
'Haze': 'weather-cloudy'
};
return themes[weather] || '';
}
async loadSearchHistory() {
try {
const response = await fetch(`${this.apiBaseUrl}?action=history`);
const result = await response.json();
if (result.success && result.data.length > 0) {
const historySection = document.getElementById('historySection');
const historyList = document.getElementById('historyList');
historyList.innerHTML = '';
result.data.forEach(item => {
const historyItem = document.createElement('span');
historyItem.className = 'history-item';
historyItem.innerHTML = `<i class="fas fa-history"></i> ${item.city}`;
historyItem.addEventListener('click', () => {
this.cityInput.value = item.city;
this.handleSearch();
});
historyList.appendChild(historyItem);
});
historySection.style.display = 'block';
}
} catch (error) {
console.error('Error loading history:', error);
}
}
toggleUnits(units) {
if (this.currentUnits === units) return;
this.currentUnits = units;
// Update active button
this.celsiusBtn.classList.toggle('active', units === 'metric');
this.fahrenheitBtn.classList.toggle('active', units === 'imperial');
// Refresh data if we have a current city
if (this.currentCity) {
this.fetchWeatherData(this.currentCity);
}
}
showLoading() {
this.loading.style.display = 'block';
this.weatherDisplay.style.display = 'none';
this.welcomeMessage.style.display = 'none';
}
hideLoading() {
this.loading.style.display = 'none';
}
showError(message) {
this.errorMessage.style.display = 'flex';
this.errorMessage.querySelector('span').textContent = message;
// Auto-hide after 5 seconds
setTimeout(() => {
this.hideError();
}, 5000);
}
hideError() {
this.errorMessage.style.display = 'none';
}
saveLastCity(city) {
localStorage.setItem('lastCity', city);
}
loadLastCity() {
const lastCity = localStorage.getItem('lastCity');
if (lastCity) {
this.cityInput.value = lastCity;
this.fetchWeatherData(lastCity);
}
}
formatDate(timestamp, timezone) {
const date = new Date(timestamp * 1000);
return date.toLocaleString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
formatDateShort(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric'
});
}
}
// Initialize the app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
window.weatherApp = new WeatherApp();
});

Geolocation Module

File: assets/js/geolocation.js

/**
* Geolocation Module for Weather App
* Handles user location detection and reverse geocoding
*/
class GeolocationService {
constructor() {
this.options = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
};
}
/**
* Get current position
* @returns {Promise} Promise resolving to position object
*/
getCurrentPosition() {
return new Promise((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Geolocation is not supported by your browser'));
return;
}
navigator.geolocation.getCurrentPosition(
(position) => resolve(position),
(error) => this.handleError(error, reject),
this.options
);
});
}
/**
* Handle geolocation errors
*/
handleError(error, reject) {
let message = 'An unknown error occurred';
switch(error.code) {
case error.PERMISSION_DENIED:
message = 'Location access denied. Please enable location services.';
break;
case error.POSITION_UNAVAILABLE:
message = 'Location information is unavailable.';
break;
case error.TIMEOUT:
message = 'Location request timed out.';
break;
}
reject(new Error(message));
}
/**
* Get city name from coordinates using reverse geocoding
* @param {number} lat Latitude
* @param {number} lon Longitude
* @returns {Promise} Promise resolving to location name
*/
async getCityFromCoords(lat, lon) {
try {
// Using OpenWeatherMap reverse geocoding
const response = await fetch(
`https://api.openweathermap.org/geo/1.0/reverse?lat=${lat}&lon=${lon}&limit=1&appid=${this.getApiKey()}`
);
const data = await response.json();
if (data && data.length > 0) {
return {
city: data[0].name,
country: data[0].country,
lat: data[0].lat,
lon: data[0].lon
};
}
throw new Error('Location not found');
} catch (error) {
console.error('Reverse geocoding failed:', error);
throw error;
}
}
/**
* Get API key from meta tag or environment
* Note: In production, this should be handled by backend
*/
getApiKey() {
// This should be injected by backend
return 'YOUR_API_KEY';
}
/**
* Watch position changes
* @param {Function} callback Callback function for position updates
* @returns {number} Watch ID
*/
watchPosition(callback) {
return navigator.geolocation.watchPosition(
callback,
(error) => this.handleError(error, console.error),
this.options
);
}
/**
* Clear position watch
* @param {number} watchId Watch ID to clear
*/
clearWatch(watchId) {
navigator.geolocation.clearWatch(watchId);
}
/**
* Check if geolocation is available
* @returns {boolean} True if available
*/
isAvailable() {
return 'geolocation' in navigator;
}
/**
* Get approximate location from IP (fallback)
* @returns {Promise} Promise resolving to location data
*/
async getLocationFromIP() {
try {
const response = await fetch('https://ipapi.co/json/');
const data = await response.json();
return {
city: data.city,
country: data.country_code,
lat: data.latitude,
lon: data.longitude
};
} catch (error) {
console.error('IP geolocation failed:', error);
throw error;
}
}
}
// Export for use in main app
window.geolocationService = new GeolocationService();

Weather Utilities

File: assets/js/weather.js

/**
* Weather Utilities Module
* Helper functions for weather data processing
*/
class WeatherUtils {
/**
* Convert temperature between Celsius and Fahrenheit
*/
static convertTemperature(temp, fromUnit, toUnit) {
if (fromUnit === toUnit) return temp;
if (fromUnit === 'celsius' && toUnit === 'fahrenheit') {
return (temp * 9/5) + 32;
} else if (fromUnit === 'fahrenheit' && toUnit === 'celsius') {
return (temp - 32) * 5/9;
}
return temp;
}
/**
* Get weather description emoji
*/
static getWeatherEmoji(weatherCode) {
const emojiMap = {
'01d': 'β˜€οΈ', // clear sky day
'01n': 'πŸŒ™', // clear sky night
'02d': 'β›…', // few clouds day
'02n': '☁️', // few clouds night
'03d': '☁️', // scattered clouds
'03n': '☁️', // scattered clouds
'04d': '☁️', // broken clouds
'04n': '☁️', // broken clouds
'09d': '🌧️', // shower rain
'09n': '🌧️', // shower rain
'10d': '🌦️', // rain day
'10n': '🌧️', // rain night
'11d': 'β›ˆοΈ', // thunderstorm
'11n': 'β›ˆοΈ', // thunderstorm
'13d': '❄️', // snow
'13n': '❄️', // snow
'50d': '🌫️', // mist
'50n': '🌫️'  // mist
};
return emojiMap[weatherCode] || '🌑️';
}
/**
* Get weather advice based on conditions
*/
static getWeatherAdvice(weather, temp, windSpeed) {
let advice = [];
// Temperature based advice
if (temp > 30) {
advice.push("It's very hot! Stay hydrated and use sunscreen.");
} else if (temp < 0) {
advice.push("Freezing temperatures! Dress warmly and watch for ice.");
} else if (temp < 10) {
advice.push("It's chilly. Bring a jacket.");
}
// Weather condition based advice
const main = weather.toLowerCase();
if (main.includes('rain')) {
advice.push("Don't forget your umbrella!");
} else if (main.includes('snow')) {
advice.push("Snow expected. Drive carefully!");
} else if (main.includes('thunderstorm')) {
advice.push("Thunderstorms! Stay indoors if possible.");
} else if (main.includes('clear') || main.includes('sun')) {
advice.push("Great weather for outdoor activities!");
}
// Wind based advice
if (windSpeed > 10) {
advice.push("Windy conditions. Secure loose items.");
}
return advice.length > 0 ? advice : ["Weather looks normal. Enjoy your day!"];
}
/**
* Get background gradient based on weather and time
*/
static getBackgroundGradient(weather, isDay) {
const gradients = {
'Clear': isDay ? 
'linear-gradient(135deg, #f6d365 0%, #fda085 100%)' : 
'linear-gradient(135deg, #2c3e50 0%, #3498db 100%)',
'Clouds': 'linear-gradient(135deg, #bdc3c7 0%, #2c3e50 100%)',
'Rain': 'linear-gradient(135deg, #4b79a1 0%, #283e51 100%)',
'Snow': 'linear-gradient(135deg, #e6e9f0 0%, #eef2f3 100%)',
'Thunderstorm': 'linear-gradient(135deg, #373b44 0%, #4286f4 100%)',
'default': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
};
return gradients[weather] || gradients.default;
}
/**
* Get UV index description
*/
static getUVDescription(uvIndex) {
if (uvIndex < 3) return 'Low';
if (uvIndex < 6) return 'Moderate';
if (uvIndex < 8) return 'High';
if (uvIndex < 11) return 'Very High';
return 'Extreme';
}
/**
* Get wind direction from degrees
*/
static getWindDirection(degrees) {
const directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
const index = Math.round(degrees / 22.5) % 16;
return directions[index];
}
/**
* Get air quality description
*/
static getAirQualityDescription(aqi) {
const levels = {
1: 'Good',
2: 'Fair',
3: 'Moderate',
4: 'Poor',
5: 'Very Poor'
};
return levels[aqi] || 'Unknown';
}
/**
* Format sunrise/sunset time
*/
static formatSunTime(timestamp, timezone) {
return new Date(timestamp * 1000).toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'UTC' // Adjust based on location timezone
});
}
/**
* Check if it's daytime based on sunrise/sunset
*/
static isDaytime(current, sunrise, sunset) {
return current > sunrise && current < sunset;
}
/**
* Get pressure trend description
*/
static getPressureTrend(current, previous) {
const diff = current - previous;
if (Math.abs(diff) < 3) return 'Stable';
return diff > 0 ? 'Rising' : 'Falling';
}
}
// Export for use
window.WeatherUtils = WeatherUtils;

Composer Configuration

File: composer.json

{
"name": "weather-app/application",
"description": "Real-time Weather Application with OpenWeatherMap API",
"type": "project",
"require": {
"php": ">=7.4",
"ext-curl": "*",
"ext-json": "*",
"ext-mbstring": "*"
},
"require-dev": {
"phpunit/phpunit": "^9.0"
},
"autoload": {
"psr-4": {
"WeatherApp\\": "src/"
}
},
"scripts": {
"test": "phpunit tests"
}
}

README File

File: README.md

# Weather App with API Integration
A full-stack weather application that provides real-time weather information and 5-day forecasts for locations worldwide using the OpenWeatherMap API.
## Features
- 🌍 Search weather by city name
- πŸ“ Automatic location detection
- 🌑️ Current temperature, humidity, wind speed, and pressure
- πŸ“… 5-day weather forecast
- πŸ”„ Toggle between Celsius and Fahrenheit
- 🎨 Dynamic themes based on weather conditions
- πŸ“± Fully responsive design
- πŸ’Ύ Search history using localStorage
- ⚑ Real-time updates
- πŸ–ΌοΈ Weather condition icons
## Technologies Used
- **Frontend:** HTML5, CSS3, JavaScript (ES6+)
- **Backend:** PHP 8.0+
- **API:** OpenWeatherMap API
- **Additional:** cURL, Font Awesome
## Prerequisites
- PHP 7.4 or higher
- Web server (Apache/Nginx)
- OpenWeatherMap API key (free tier available)
## Installation
1. **Clone the repository**

bash
git clone https://github.com/yourusername/weather-app.git
cd weather-app

2. **Configure environment variables**

bash
cp .env.example .env

   Edit `.env` and add your OpenWeatherMap API key:

OPENWEATHER_API_KEY=your_api_key_here

3. **Set up permissions**

bash
chmod 755 cache/
chmod 755 logs/

4. **Start the server**

bash
php -S localhost:8000

5. **Access the application**
Open your browser and navigate to `http://localhost:8000`
## Project Structure

weather-app/
β”œβ”€β”€ api/
β”‚ └── weather.php # Backend API endpoint
β”œβ”€β”€ assets/
β”‚ β”œβ”€β”€ css/
β”‚ β”‚ β”œβ”€β”€ style.css # Main styles
β”‚ β”‚ └── responsive.css # Responsive design
β”‚ β”œβ”€β”€ js/
β”‚ β”‚ β”œβ”€β”€ main.js # Main application logic
β”‚ β”‚ β”œβ”€β”€ weather.js # Weather utilities
β”‚ β”‚ └── geolocation.js # Geolocation handling
β”‚ └── images/
β”œβ”€β”€ includes/
β”‚ β”œβ”€β”€ config.php # Configuration
β”‚ β”œβ”€β”€ functions.php # Helper functions
β”‚ └── WeatherService.php # Weather service class
β”œβ”€β”€ cache/ # API response cache
β”œβ”€β”€ logs/ # Error logs
β”œβ”€β”€ .env # Environment variables
β”œβ”€β”€ .gitignore # Git ignore file
β”œβ”€β”€ index.php # Main entry point
└── README.md # Documentation

## API Reference
This application uses the OpenWeatherMap API. The following endpoints are used:
- **Current Weather:** `https://api.openweathermap.org/data/2.5/weather`
- **5-Day Forecast:** `https://api.openweathermap.org/data/2.5/forecast`
### API Parameters
| Parameter | Description |
|-----------|-------------|
| `q` | City name (e.g., London, New York) |
| `lat` | Latitude for coordinate-based search |
| `lon` | Longitude for coordinate-based search |
| `units` | Units of measurement (metric/imperial) |
| `appid` | Your API key |
## Usage
### Searching for a City
1. Enter a city name in the search box
2. Press Enter or click the Search button
3. View current weather and 5-day forecast
### Using Current Location
1. Click the "Current Location" button
2. Allow location access when prompted
3. View weather for your current location
### Switching Temperature Units
- Click Β°C for Celsius
- Click Β°F for Fahrenheit
## Security Notes
- **API Key Security:** API keys are stored in `.env` file and never exposed to the client 
- **Input Validation:** All user inputs are sanitized before processing
- **Error Handling:** Comprehensive error handling with user-friendly messages 
## Performance Optimizations
- **Caching:** API responses are cached for 10 minutes to reduce requests 
- **Lazy Loading:** Weather data loads only when needed
- **Minified Assets:** CSS and JavaScript can be minified for production
## Error Handling
The application handles various error scenarios:
- Invalid city names
- Network connectivity issues
- API rate limiting
- Geolocation permission denied
- Server errors
## Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
- Opera (latest)
- Mobile browsers
## Contributing
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Acknowledgments
- [OpenWeatherMap](https://openweathermap.org/) for providing the weather API 
- [Font Awesome](https://fontawesome.com/) for icons
- All contributors and users of this application
## Troubleshooting
### Common Issues
1. **"City not found" error**
- Check spelling
- Try adding country code (e.g., "London,UK")
2. **Geolocation not working**
- Ensure location services are enabled
- Check browser permissions
3. **No data showing**
- Verify API key in `.env` file
- Check internet connection
- View browser console for errors
4. **Cache issues**
- Delete the `cache/` directory
- Clear browser cache
## Contact
Your Name - [@yourtwitter](https://twitter.com/yourtwitter) - [email protected]
Project Link: [https://github.com/yourusername/weather-app](https://github.com/yourusername/weather-app)

How to Use the Project (Step-by-Step Guide)

Prerequisites

  1. Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server
  2. PHP Version: 7.4 or higher
  3. OpenWeatherMap API Key: Free account at OpenWeatherMap

Step 1: Get API Key

  1. Go to OpenWeatherMap and sign up for a free account
  2. Navigate to your API keys section
  3. Copy your API key (free tier allows 60 calls/minute)

Step 2: Set Up Project

  1. Create project folder in your web server's root directory (e.g., htdocs/weather-app)
  2. Copy all files as per the file structure above
  3. Configure environment by creating .env file with your API key

Step 3: Test the Application

  1. Start your web server (Apache, Nginx, or PHP built-in server)
  2. Access the application at http://localhost/weather-app/
  3. Search for a city like "London" or "New York"
  4. Verify weather data appears correctly

Step 4: Deploy to Production

  1. Set up production server with PHP support
  2. Upload all files to your server
  3. Configure .env file with production API key
  4. Set proper permissions for cache and logs directories
  5. Test thoroughly before going live

Conclusion

The Weather App with API Integration demonstrates professional-grade API integration with proper security practices, error handling, and user experience considerations . This project serves as an excellent foundation for learning API integration, asynchronous programming, and building responsive web applications.

The application follows industry best practices including:

  • Separation of concerns between frontend and backend
  • Secure API key handling through environment variables
  • Comprehensive error handling for all scenarios
  • Performance optimization through caching
  • Responsive design for all devices
  • Clean, maintainable code structure

Whether you're a beginner learning API integration or an experienced developer looking for a reference implementation, this weather app provides a solid foundation that can be extended with additional features like weather maps, air quality data, or user accounts .

Leave a Reply

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


Macro Nepal Helper