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
- Web Server: XAMPP, WAMP, MAMP, or any PHP-enabled server
- PHP Version: 7.4 or higher
- OpenWeatherMap API Key: Free account at OpenWeatherMap
Step 1: Get API Key
- Go to OpenWeatherMap and sign up for a free account
- Navigate to your API keys section
- Copy your API key (free tier allows 60 calls/minute)
Step 2: Set Up Project
- Create project folder in your web server's root directory (e.g.,
htdocs/weather-app) - Copy all files as per the file structure above
- Configure environment by creating
.envfile with your API key
Step 3: Test the Application
- Start your web server (Apache, Nginx, or PHP built-in server)
- Access the application at
http://localhost/weather-app/ - Search for a city like "London" or "New York"
- Verify weather data appears correctly
Step 4: Deploy to Production
- Set up production server with PHP support
- Upload all files to your server
- Configure
.envfile with production API key - Set proper permissions for cache and logs directories
- 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 .