feat: add local ad-free weather page

This commit is contained in:
2026-05-27 13:33:44 +00:00
parent fd519a64b0
commit 1b06a29e53
5 changed files with 598 additions and 262 deletions
+348
View File
@@ -0,0 +1,348 @@
<!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://ipapi.co/json/");
const data = await resp.json();
let locName = "Your Area";
if (data.city && data.region) {
locName = `${data.city}, ${data.region}`;
} else if (data.city) {
locName = data.city;
}
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}&current=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>