349 lines
11 KiB
HTML
349 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Weather</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
|
color: white;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Frosted glass container card */
|
|
.container {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 32px;
|
|
padding: 40px 30px;
|
|
width: 100%;
|
|
max-width: 900px;
|
|
text-align: center;
|
|
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
|
|
}
|
|
|
|
#location-name {
|
|
font-size: 38px;
|
|
font-weight: bold;
|
|
margin-bottom: 8px;
|
|
color: #e0f2fe;
|
|
}
|
|
|
|
#loading-text {
|
|
font-size: 26px;
|
|
margin: 40px 0;
|
|
color: #aac8e8;
|
|
}
|
|
|
|
.weather-main {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 30px;
|
|
margin: 30px 0;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.main-temp {
|
|
font-size: 88px;
|
|
font-weight: bold;
|
|
line-height: 1;
|
|
color: #fff;
|
|
}
|
|
|
|
.weather-icon {
|
|
font-size: 96px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.condition-desc {
|
|
font-size: 32px;
|
|
font-weight: bold;
|
|
color: #38bdf8;
|
|
margin-bottom: 10px;
|
|
text-transform: capitalize;
|
|
}
|
|
|
|
.feels-like {
|
|
font-size: 20px;
|
|
color: #cbd5e1;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
/* Info pills */
|
|
.details-row {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 20px;
|
|
margin-bottom: 40px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.pill {
|
|
background: rgba(255, 255, 255, 0.08);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
padding: 14px 24px;
|
|
border-radius: 16px;
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Forecast layout */
|
|
.forecast-title {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin-bottom: 20px;
|
|
text-align: left;
|
|
border-bottom: 2px solid rgba(255, 255, 255, 0.1);
|
|
padding-bottom: 8px;
|
|
color: #93c5fd;
|
|
}
|
|
|
|
.forecast-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 18px;
|
|
margin-bottom: 40px;
|
|
}
|
|
|
|
.forecast-card {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
border-radius: 20px;
|
|
padding: 20px 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.fc-day {
|
|
font-size: 22px;
|
|
font-weight: bold;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
.fc-icon {
|
|
font-size: 48px;
|
|
}
|
|
|
|
.fc-temp {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
}
|
|
|
|
/* Go Home Navigation button */
|
|
.home-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: linear-gradient(135deg, #10b981, #059669);
|
|
color: white;
|
|
text-decoration: none;
|
|
padding: 20px 50px;
|
|
font-size: 26px;
|
|
font-weight: bold;
|
|
border-radius: 20px;
|
|
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.3);
|
|
cursor: pointer;
|
|
transition: transform 0.1s, box-shadow 0.1s;
|
|
border: none;
|
|
}
|
|
|
|
.home-btn:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.45);
|
|
}
|
|
|
|
.home-btn:active {
|
|
transform: translateY(0px);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="container" id="weather-container">
|
|
<div id="loading-box">
|
|
<div id="loading-text">🔍 Determining your location...</div>
|
|
</div>
|
|
|
|
<div id="weather-content" style="display: none;">
|
|
<div id="location-name">Detecting Location...</div>
|
|
|
|
<div class="weather-main">
|
|
<div class="weather-icon" id="main-icon">🌤️</div>
|
|
<div class="main-temp" id="temp-val">--°F</div>
|
|
</div>
|
|
|
|
<div class="condition-desc" id="condition-txt">Partly Cloudy</div>
|
|
<div class="feels-like" id="feels-like-txt">Feels like --°F</div>
|
|
|
|
<div class="details-row">
|
|
<div class="pill">💧 Humidity: <span id="humidity-val">--%</span></div>
|
|
<div class="pill">💨 Wind: <span id="wind-val">-- mph</span></div>
|
|
</div>
|
|
|
|
<div class="forecast-title">3-Day Forecast</div>
|
|
<div class="forecast-grid" id="forecast-box"></div>
|
|
</div>
|
|
|
|
<a href="index.html" class="home-btn">⌂ Go Home</a>
|
|
</div>
|
|
|
|
<script>
|
|
// WMO Weather interpretation codes mapping
|
|
const WEATHER_CODES = {
|
|
0: { txt: "Sunny", emoji: "☀️" },
|
|
1: { txt: "Mainly Clear", emoji: "🌤️" },
|
|
2: { txt: "Partly Cloudy", emoji: "⛅" },
|
|
3: { txt: "Overcast", emoji: "☁️" },
|
|
45: { txt: "Foggy", emoji: "🌫️" },
|
|
48: { txt: "Depositing Rime Fog", emoji: "🌫️" },
|
|
51: { txt: "Light Drizzle", emoji: "🌦️" },
|
|
53: { txt: "Moderate Drizzle", emoji: "🌦️" },
|
|
55: { txt: "Dense Drizzle", emoji: "🌦️" },
|
|
61: { txt: "Slight Rain", emoji: "🌧️" },
|
|
63: { txt: "Moderate Rain", emoji: "🌧️" },
|
|
65: { txt: "Heavy Rain", emoji: "🌧️" },
|
|
71: { txt: "Light Snowfall", emoji: "🌨️" },
|
|
73: { txt: "Moderate Snowfall", emoji: "🌨️" },
|
|
75: { txt: "Heavy Snowfall", emoji: "🌨️" },
|
|
77: { txt: "Snow Grains", emoji: "🌨️" },
|
|
80: { txt: "Slight Showers", emoji: "🌧️" },
|
|
81: { txt: "Moderate Showers", emoji: "🌧️" },
|
|
82: { txt: "Violent Showers", emoji: "⛈️" },
|
|
85: { txt: "Slight Snow Showers", emoji: "🌨️" },
|
|
86: { txt: "Heavy Snow Showers", emoji: "🌨️" },
|
|
95: { txt: "Thunderstorm", emoji: "⛈️" },
|
|
96: { txt: "Thunderstorm with Hail", emoji: "⛈️" },
|
|
99: { txt: "Thunderstorm with Heavy Hail", emoji: "⛈️" }
|
|
};
|
|
|
|
function getWeatherInfo(code) {
|
|
return WEATHER_CODES[code] || { txt: "Weather Details", emoji: "🌤️" };
|
|
}
|
|
|
|
async function initWeather() {
|
|
// 1. Get coordinates using IP API fallback or direct Geolocation
|
|
try {
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(
|
|
async (pos) => {
|
|
// Successfully got GPS coordinates
|
|
await fetchForecast(pos.coords.latitude, pos.coords.longitude, "Local Location");
|
|
},
|
|
async () => {
|
|
// Denied or failed - fall back to IP lookup
|
|
await fetchViaIP();
|
|
},
|
|
{ timeout: 5000 }
|
|
);
|
|
} else {
|
|
await fetchViaIP();
|
|
}
|
|
} catch (e) {
|
|
document.getElementById('loading-text').textContent = "⚠️ Unable to load weather. Check internet connection.";
|
|
}
|
|
}
|
|
|
|
async function fetchViaIP() {
|
|
try {
|
|
document.getElementById('loading-text').textContent = "🌐 Looking up location via IP...";
|
|
const resp = await fetch("https://freeipapi.com/api/json");
|
|
const data = await resp.json();
|
|
|
|
let locName = "Your Area";
|
|
if (data.cityName && data.regionName) {
|
|
locName = `${data.cityName}, ${data.regionName}`;
|
|
} else if (data.cityName) {
|
|
locName = data.cityName;
|
|
}
|
|
|
|
if (data.latitude && data.longitude) {
|
|
await fetchForecast(data.latitude, data.longitude, locName);
|
|
} else {
|
|
throw new Error("Invalid IP response coordinates");
|
|
}
|
|
} catch (e) {
|
|
// Ultimate hard-coded default (e.g. general center of US/state if offline)
|
|
document.getElementById('loading-text').textContent = "⚠️ Location lookup failed. Please verify internet.";
|
|
}
|
|
}
|
|
|
|
async function fetchForecast(lat, lon, locationLabel) {
|
|
try {
|
|
document.getElementById('loading-text').textContent = "📊 Loading weather forecasts...";
|
|
|
|
// Query the completely open, keyless Open-Meteo API
|
|
const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}¤t=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m&daily=weather_code,temperature_2m_max,temperature_2m_min&temperature_unit=fahrenheit&wind_speed_unit=mph&precipitation_unit=inch&timezone=auto`;
|
|
|
|
const resp = await fetch(url);
|
|
const data = await resp.json();
|
|
|
|
// Render current stats
|
|
const current = data.current;
|
|
const wInfo = getWeatherInfo(current.weather_code);
|
|
|
|
document.getElementById('location-name').textContent = locationLabel;
|
|
document.getElementById('temp-val').textContent = `${Math.round(current.temperature_2m)}°F`;
|
|
document.getElementById('main-icon').textContent = wInfo.emoji;
|
|
document.getElementById('condition-txt').textContent = wInfo.txt;
|
|
document.getElementById('feels-like-txt').textContent = `Feels like ${Math.round(current.apparent_temperature)}°F`;
|
|
document.getElementById('humidity-val').textContent = `${current.relative_humidity_2m}%`;
|
|
document.getElementById('wind-val').textContent = `${Math.round(current.wind_speed_10m)} mph`;
|
|
|
|
// Render 3-Day Forecast
|
|
const daily = data.daily;
|
|
const forecastBox = document.getElementById('forecast-box');
|
|
forecastBox.innerHTML = ""; // Clear loader
|
|
|
|
// We render days 1, 2, and 3 (index 1 to 3, as index 0 is today)
|
|
for (let i = 1; i <= 3; i++) {
|
|
const dateStr = daily.time[i];
|
|
const dateObj = new Date(dateStr + "T00:00:00");
|
|
const dayName = dateObj.toLocaleDateString('en-US', { weekday: 'long' });
|
|
const code = daily.weather_code[i];
|
|
const fcInfo = getWeatherInfo(code);
|
|
const maxT = Math.round(daily.temperature_2m_max[i]);
|
|
const minT = Math.round(daily.temperature_2m_min[i]);
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'forecast-card';
|
|
card.innerHTML = `
|
|
<div class="fc-day">${dayName}</div>
|
|
<div class="fc-icon">${fcInfo.emoji}</div>
|
|
<div class="fc-temp">🔆 ${maxT}° / 🌙 ${minT}°F</div>
|
|
`;
|
|
forecastBox.appendChild(card);
|
|
}
|
|
|
|
// Hide loader and show content
|
|
document.getElementById('loading-box').style.display = 'none';
|
|
document.getElementById('weather-content').style.display = 'block';
|
|
|
|
} catch (e) {
|
|
document.getElementById('loading-text').textContent = "⚠️ Error pulling forecast data.";
|
|
}
|
|
}
|
|
|
|
window.onload = initWeather;
|
|
</script>
|
|
</body>
|
|
</html>
|