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 .