From ec1cca2e027b543ab662591e3050754aa1c753b6 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Wed, 13 May 2026 23:10:37 +0530 Subject: [PATCH 01/13] Add Air Quality Index (AQI) support with health indicators --- public/index.html | 50 +++++- public/script.js | 403 +++++++++++++++++++++++++++++++++++++++++++++- public/style.css | 213 ++++++++++++++++++++++++ server.js | 2 + 4 files changed, 664 insertions(+), 4 deletions(-) diff --git a/public/index.html b/public/index.html index c246fb1..b89dd83 100644 --- a/public/index.html +++ b/public/index.html @@ -64,6 +64,28 @@

Weatherify ⛅

+ +
+
+

Saved Cities

+

Quickly reopen favorite cities or recent searches.

+
+
+
+
Favorites
+
+
+ +
+
Recent
+
+
+ +
+ +
+
+
@@ -72,7 +94,12 @@

Weatherify ⛅

-

City Name

+
+

City Name

+ +

Date

@@ -149,6 +176,26 @@

Sun Position

-- km
+ +
+ +
+ Air Quality (AQI) + + -- + -- + + Pollutants -- + Recommendation -- +
+
+
@@ -179,6 +226,7 @@

Sun Position

+
diff --git a/public/script.js b/public/script.js index f0cc7d7..eaee046 100644 --- a/public/script.js +++ b/public/script.js @@ -5,9 +5,141 @@ const DEGREE = '\u00B0'; const DEFAULT_CITY = 'New Delhi'; let currentUnit = 'C'; -let rawData = { current: null, forecast: null }; +let rawData = { current: null, forecast: null, airQuality: null }; + +// AQI UI elements +const aqiCard = document.getElementById('aqi-card'); +const aqiValueEl = document.getElementById('aqi-value'); +const aqiBadgeEl = document.getElementById('aqi-badge'); +const aqiPollutantsEl = document.getElementById('aqi-pollutants'); +const aqiRecommendationEl = document.getElementById('aqi-recommendation'); + +function getAqiCategory(aqi) { + const value = Number(aqi); + if (!Number.isFinite(value)) { + return { label: 'Unknown', level: 0, badgeClass: '' }; + } + + // OpenWeatherMap air pollution uses a 1-5 AQI index. + switch (value) { + case 1: + return { label: 'Good', level: 1, badgeClass: 'aqi-1' }; + case 2: + return { label: 'Fair', level: 2, badgeClass: 'aqi-2' }; + case 3: + return { label: 'Moderate', level: 3, badgeClass: 'aqi-3' }; + case 4: + return { label: 'Poor', level: 4, badgeClass: 'aqi-4' }; + case 5: + return { label: 'Very Poor', level: 5, badgeClass: 'aqi-5' }; + default: + if (value <= 50) return { label: 'Good', level: 1, badgeClass: 'aqi-1' }; + if (value <= 100) return { label: 'Fair', level: 2, badgeClass: 'aqi-2' }; + if (value <= 150) return { label: 'Moderate', level: 3, badgeClass: 'aqi-3' }; + if (value <= 200) return { label: 'Poor', level: 4, badgeClass: 'aqi-4' }; + return { label: 'Very Poor', level: 5, badgeClass: 'aqi-5' }; + } +} + +function getAqiHealthRecommendation(categoryLabel) { + switch (categoryLabel) { + case 'Good': + return 'Enjoy outdoor activities. Sensitive groups may still consider monitoring.'; + case 'Fair': + return 'Unusually sensitive individuals should reduce prolonged outdoor exertion.'; + case 'Moderate': + return 'Consider reducing prolonged outdoor activities if you experience symptoms.'; + case 'Poor': + return 'Limit outdoor activity; keep windows closed and consider an air purifier.'; + case 'Very Poor': + return 'Avoid outdoor activity. Stay indoors and follow local health guidance.'; + default: + return 'Air quality information is unavailable right now.'; + } +} + +function renderAqiUI(airPollution) { + console.log('[AQI] renderAqiUI called with:', airPollution); + + if (!airPollution) { + console.warn('[AQI] No airPollution data provided'); + if (aqiCard) aqiCard.classList.add('hidden'); + return; + } + + if (!Array.isArray(airPollution.list)) { + console.warn('[AQI] airPollution.list is not an array:', typeof airPollution.list, airPollution.list); + if (aqiCard) aqiCard.classList.add('hidden'); + return; + } + + if (airPollution.list.length === 0) { + console.warn('[AQI] airPollution.list is empty'); + if (aqiCard) aqiCard.classList.add('hidden'); + return; + } + + console.log('[AQI] Rendering AQI data'); + if (aqiCard) aqiCard.classList.remove('hidden'); + + const entry = airPollution.list[0]; + console.log('[AQI] Entry:', entry); + const aqi = entry?.main?.aqi; + const pollutants = entry?.components || {}; + console.log('[AQI] AQI value:', aqi, 'Pollutants:', pollutants); + + const { label, badgeClass } = getAqiCategory(aqi); + + if (aqiValueEl) aqiValueEl.textContent = Number.isFinite(Number(aqi)) ? String(aqi) : '--'; + + if (aqiBadgeEl) { + aqiBadgeEl.textContent = label; + aqiBadgeEl.classList.remove('aqi-1', 'aqi-2', 'aqi-3', 'aqi-4', 'aqi-5'); + if (badgeClass) aqiBadgeEl.classList.add(badgeClass); + } + + if (aqiPollutantsEl) { + // Show the most common pollutant components when available + const parts = []; + const pm25 = pollutants.pm2_5; + const pm10 = pollutants.pm10; + const o3 = pollutants.o3; + const no2 = pollutants.no2; + const so2 = pollutants.so2; + const co = pollutants.co; + + const add = (key, label) => { + const v = pollutants[key]; + if (v === undefined || v === null) return; + parts.push(`${label}: ${v}`); + }; + + add('pm2_5', 'PM2.5'); + add('pm10', 'PM10'); + add('o3', 'O₃'); + add('no2', 'NO₂'); + add('so2', 'SO₂'); + add('co', 'CO'); + + aqiPollutantsEl.textContent = parts.length ? `Pollutants ${parts.join(' • ')}` : 'Pollutants --'; + } + + if (aqiRecommendationEl) { + aqiRecommendationEl.textContent = `Recommendation: ${getAqiHealthRecommendation(label)}`; + } +} + +async function fetchAirQualityByCoords(lat, lon) { + const url = `${API_BASE}/air-quality?lat=${lat}&lon=${lon}`; + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Air quality fetch failed: ${response.status}`); + } + + return response.json(); +} -// Initialize unit display after DOM is ready function initUnitDisplay() { const unitElement = document.querySelector('.unit'); if (unitElement) { @@ -88,9 +220,234 @@ const trendStats = document.getElementById('trend-stats'); const trendChartLabel = document.getElementById('trend-chart-label'); const trendChartRange = document.getElementById('trend-chart-range'); const trendControls = document.querySelector('.trend-controls'); +const historyDropdown = document.getElementById('history-dropdown'); +const favoriteList = document.getElementById('favorite-list'); +const recentList = document.getElementById('recent-list'); +const clearHistoryBtn = document.getElementById('clear-history-btn'); +const favoriteToggleBtn = document.getElementById('favorite-btn'); + +const STORAGE_RECENT = 'weatherify-recent-cities'; +const STORAGE_FAVORITES = 'weatherify-favorite-cities'; +const MAX_RECENT_SEARCHES = 8; + let sunTimeline = null; let dailyTrendData = []; let selectedTrendMetric = 'avg'; +let currentCityQuery = ''; +let currentCityLabel = ''; +let recentSearches = []; +let favoriteCities = []; + +function normalizeCityKey(city) { + return city.trim().toLowerCase(); +} + +function loadHistoryArray(key) { + try { + const stored = localStorage.getItem(key); + const parsed = stored ? JSON.parse(stored) : []; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function saveHistoryArray(key, entries) { + localStorage.setItem(key, JSON.stringify(entries)); +} + +function renderHistory() { + if (!historyDropdown) return; + + const favoriteMarkup = favoriteCities.map((item) => ` + + `).join(''); + + const recentMarkup = recentSearches.map((item) => ` + + `).join(''); + + const hasHistory = favoriteCities.length > 0 || recentSearches.length > 0; + const emptyState = !hasHistory ? ` +
+

No recent searches yet.

+ Search a city and it will appear here for quick access. +
+ ` : ''; + + const inner = historyDropdown.querySelector('.history-dropdown-inner'); + if (inner) { + inner.innerHTML = ` +
+
Favorites
+
${favoriteMarkup}
+
+
+
Recent
+
${recentMarkup}
+
+ ${emptyState} +
+ +
+ `; + } + + const newFavoriteList = document.getElementById('favorite-list'); + const newRecentList = document.getElementById('recent-list'); + const newClearHistoryBtn = document.getElementById('clear-history-btn'); + + if (newFavoriteList) { + newFavoriteList.addEventListener('click', handleHistoryClick); + } + if (newRecentList) { + newRecentList.addEventListener('click', handleHistoryClick); + } + if (newClearHistoryBtn) { + newClearHistoryBtn.addEventListener('click', (event) => { + event.stopPropagation(); + clearRecentHistory(); + }); + } +} + +function updateFavoriteButton() { + if (!favoriteToggleBtn) return; + + const active = favoriteCities.some((item) => normalizeCityKey(item.query) === normalizeCityKey(currentCityQuery)); + const icon = favoriteToggleBtn.querySelector('.favorite-icon'); + favoriteToggleBtn.setAttribute('aria-pressed', String(active)); + if (icon) icon.textContent = active ? '★' : '☆'; + favoriteToggleBtn.title = active ? 'Remove favorite' : 'Favorite'; +} + +function createCityEntry(data) { + const label = `${data.name}, ${data.sys.country}`; + const query = `${data.name},${data.sys.country}`; + return { query, label }; +} + +function addRecentSearch(entry) { + if (!entry || !entry.query) return; + + const normalized = normalizeCityKey(entry.query); + recentSearches = recentSearches.filter((item) => normalizeCityKey(item.query) !== normalized); + recentSearches.unshift(entry); + + if (recentSearches.length > MAX_RECENT_SEARCHES) { + recentSearches = recentSearches.slice(0, MAX_RECENT_SEARCHES); + } + + saveHistoryArray(STORAGE_RECENT, recentSearches); + renderHistory(); +} + +function toggleFavoriteCity(entry) { + if (!entry || !entry.query) return; + + const normalized = normalizeCityKey(entry.query); + const existingIndex = favoriteCities.findIndex((item) => normalizeCityKey(item.query) === normalized); + + if (existingIndex >= 0) { + favoriteCities.splice(existingIndex, 1); + } else { + favoriteCities.unshift(entry); + } + + saveHistoryArray(STORAGE_FAVORITES, favoriteCities); + updateFavoriteButton(); + renderHistory(); +} + +function clearRecentHistory() { + recentSearches = []; + saveHistoryArray(STORAGE_RECENT, recentSearches); + renderHistory(); +} + +function handleHistoryClick(event) { + const button = event.target.closest('button.history-button'); + if (!button) return; + + const query = button.dataset.query; + if (!query) return; + + cityInput.value = query.replace(/,([A-Z]{2})$/, ', $1'); + clearBtn.classList.remove('hidden'); + cityInput.dispatchEvent(new Event('input', { bubbles: true })); + hideSuggestions(); + closeHistoryDropdown(); + fetchWeatherData(query); +} + +function openHistoryDropdown() { + if (!historyDropdown) return; + // Only show if user isn't typing and there is something to show + const hasAny = (favoriteCities && favoriteCities.length) || (recentSearches && recentSearches.length); + if (!hasAny) return; + historyDropdown.classList.remove('hidden'); +} + +function closeHistoryDropdown() { + if (!historyDropdown) return; + historyDropdown.classList.add('hidden'); +} + +function initHistory() { + recentSearches = loadHistoryArray(STORAGE_RECENT); + favoriteCities = loadHistoryArray(STORAGE_FAVORITES); + renderHistory(); + + if (cityInput) { + cityInput.addEventListener('focus', () => { + if (!cityInput.value.trim()) { + openHistoryDropdown(); + } + }); + + cityInput.addEventListener('input', () => { + if (!cityInput.value.trim()) { + if (favoriteCities.length || recentSearches.length) { + openHistoryDropdown(); + } + } else { + closeHistoryDropdown(); + } + }); + } + + + if (historyDropdown) { + historyDropdown.addEventListener('click', handleHistoryClick); + } + + if (clearHistoryBtn) { + clearHistoryBtn.classList.toggle('hidden', recentSearches.length === 0); + } + + if (favoriteToggleBtn) { + favoriteToggleBtn.addEventListener('click', () => { + if (!currentCityQuery || !currentCityLabel) return; + toggleFavoriteCity({ query: currentCityQuery, label: currentCityLabel }); + }); + } +} + + +function setCurrentCity(data) { + currentCityLabel = `${data.name}, ${data.sys.country}`; + currentCityQuery = `${data.name},${data.sys.country}`; + updateFavoriteButton(); + + // Record as recent search (prevent duplicates via addRecentSearch) + addRecentSearch({ query: currentCityQuery, label: currentCityLabel }); +} + // Unit toggle document.querySelectorAll('.unit-btn').forEach(btn => { @@ -147,11 +504,14 @@ clearBtn.addEventListener('click', (e) => { cityInput.focus(); }); -// Hide suggestions when clicking outside +// Hide suggestions and history when clicking outside search controls document.addEventListener('click', (e) => { if (!e.target.closest('.search-box')) { hideSuggestions(); } + if (!e.target.closest('.search-container')) { + closeHistoryDropdown(); + } }); if (trendControls) { @@ -216,6 +576,7 @@ window.addEventListener('DOMContentLoaded', () => { console.error('Service Worker registration failed:', err); }); } + initHistory(); fetchWeatherData(DEFAULT_CITY); }); @@ -318,10 +679,30 @@ async function fetchWeatherByCoords(lat, lon) { rawData.current = currentData; rawData.forecast = forecastData; + + // Fetch AQI using resolved coordinates when available + if (currentData?.coord?.lat !== undefined && currentData?.coord?.lon !== undefined) { + try { + console.log('[AQI] Fetching AQI for coordinates:', currentData.coord.lat, currentData.coord.lon); + const airQuality = await fetchAirQualityByCoords(currentData.coord.lat, currentData.coord.lon); + rawData.airQuality = airQuality; + console.log('[AQI] Rendering AQI UI after fetch'); + renderAqiUI(airQuality); + } catch (error) { + console.error('[AQI] Error fetching/rendering AQI:', error); + rawData.airQuality = null; + if (aqiCard) aqiCard.classList.add('hidden'); + } + } else { + console.warn('[AQI] No coordinates available for AQI fetch'); + } + localStorage.setItem('weatherify-last-data', JSON.stringify(rawData)); updateUI(currentData); updateForecastUI(forecastData); showWeather(); + setCurrentCity(currentData); + } catch (error) { if (!navigator.onLine) { const cachedData = localStorage.getItem('weatherify-last-data'); @@ -342,6 +723,9 @@ async function fetchWeatherByCoords(lat, lon) { } } async function fetchWeatherData(city) { + // reset AQI section while fetching new data + if (aqiCard) aqiCard.classList.add('hidden'); + showLoading(); hideError(); // hideWeather(); @@ -370,10 +754,23 @@ async function fetchWeatherData(city) { rawData.current = currentData; rawData.forecast = forecastData; + + if (currentData?.coord?.lat !== undefined && currentData?.coord?.lon !== undefined) { + try { + const airQuality = await fetchAirQualityByCoords(currentData.coord.lat, currentData.coord.lon); + rawData.airQuality = airQuality; + renderAqiUI(airQuality); + } catch { + rawData.airQuality = null; + if (aqiCard) aqiCard.classList.add('hidden'); + } + } + localStorage.setItem('weatherify-last-data', JSON.stringify(rawData)); updateUI(currentData); updateForecastUI(forecastData); showWeather(); + setCurrentCity(currentData); } catch (error) { console.error('Fetch error:', error); if (!navigator.onLine) { diff --git a/public/style.css b/public/style.css index a869734..385c8bc 100644 --- a/public/style.css +++ b/public/style.css @@ -158,6 +158,7 @@ h1 { width: 100%; max-width: none; margin-left: 0; + position: relative; } .theme-toggle { @@ -278,6 +279,167 @@ h1 { transition: background 0.4s ease; } +.history-dropdown { + position: static; + margin-top: 18px; + background: #eef2ff; + border: 1px solid #c7d2fe; + border-radius: 22px; + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.12); + padding: 20px; + width: 100%; + overflow: hidden; + animation: none; +} + +.history-dropdown.hidden { + display: none; +} + +.history-panel-header { + margin-bottom: 16px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(99, 102, 241, 0.15); +} + +.history-panel-header h2 { + margin: 0; + font-size: 1.2rem; + font-weight: 700; + color: #1e293b; +} + +.history-panel-header p { + margin: 8px 0 0; + color: #475569; + font-size: 0.95rem; + line-height: 1.45; +} + +.history-empty { + padding: 18px; + border-radius: 16px; + background: #f8fafc; + color: #475569; + border: 1px dashed #cbd5e0; + text-align: center; +} + +.history-empty p { + font-weight: 600; + margin-bottom: 6px; +} + +.history-empty small { + color: #64748b; +} + +.history-actions { + display: flex; + justify-content: flex-end; + padding-top: 8px; +} + +.history-toggle-btn { + border: none; + background: #eef2ff; + color: #4f46e5; + border-radius: 14px; + padding: 0 14px; + min-width: 48px; + height: 48px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 0.2s ease, transform 0.2s ease; +} + +.history-toggle-btn:hover { + background: #e0e7ff; + transform: translateY(-1px); +} + +.history-dropdown-inner { + display: grid; + gap: 14px; +} + +.history-section { + display: grid; + gap: 10px; +} + +.history-section-title { + font-size: 0.9rem; + font-weight: 700; + color: #334155; +} + +.history-list { + display: grid; + gap: 8px; + max-height: 160px; + overflow-y: auto; + padding-right: 4px; +} + +.history-button { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 12px 14px; + border-radius: 14px; + border: 1px solid #e2e8f0; + background: #f8fafc; + color: #0f172a; + font-size: 0.95rem; + text-align: left; + cursor: pointer; + transition: background 0.2s ease, transform 0.2s ease; +} + +.history-button:hover { + transform: translateX(2px); + background: #e2e8f0; +} + +.history-badge { + font-size: 0.9rem; + color: #ca8a04; +} + +.clear-history-btn { + width: fit-content; + border: 1px solid #cbd5e0; + background: #ffffff; + color: #475569; + border-radius: 12px; + padding: 10px 16px; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease; +} + +.clear-history-btn:hover { + background: #f8fafc; +} + +.favorite-btn { + border: none; + background: transparent; + color: #f59e0b; + font-size: 1.6rem; + line-height: 1; + cursor: pointer; + transition: transform 0.2s ease; +} + +.favorite-btn:hover { + transform: scale(1.1); +} + .suggestion-item { padding: 14px 20px; cursor: pointer; @@ -564,6 +726,57 @@ h1 { text-align: center; } +.aqi-value { + font-size: 1.2rem; + font-weight: 800; + color: #1f2937; +} + +.aqi-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 5px 12px; + border-radius: 999px; + color: #ffffff; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; + white-space: nowrap; +} + +.aqi-1 { + background: #16a34a; +} + +.aqi-2 { + background: #fbbf24; + color: #1f2937; +} + +.aqi-3 { + background: #f97316; +} + +.aqi-4 { + background: #dc2626; +} + +.aqi-5 { + background: #9333ea; +} + +.aqi-pollutants, +.aqi-recommendation { + display: block; + font-size: 12px; + line-height: 1.5; + color: #475569; + margin-top: 8px; + text-align: center; +} + .detail-label { font-size: 11px; color: #718096; diff --git a/server.js b/server.js index 3a623f3..e82476c 100644 --- a/server.js +++ b/server.js @@ -68,6 +68,8 @@ async function proxyOpenWeather(basePath, req, res) { app.get('/api/weather', (req, res) => proxyOpenWeather('/data/2.5/weather', req, res)); app.get('/api/forecast', (req, res) => proxyOpenWeather('/data/2.5/forecast', req, res)); app.get('/api/geo', (req, res) => proxyOpenWeather('/geo/1.0/direct', req, res)); +app.get('/api/air-quality', (req, res) => proxyOpenWeather('/data/2.5/air_pollution', req, res)); + app.use(express.static(path.join(__dirname, 'public'))); From 1af94ac19b0039b3d134103ef7bac5ae0c5be11e Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Wed, 13 May 2026 23:21:51 +0530 Subject: [PATCH 02/13] Add favorite cities and recent searches support --- public/index.html | 1 + public/manifest.json | 1 + public/script.js | 1 + public/style.css | 1 + public/sw.js | 1 + server.js | 1 + 6 files changed, 6 insertions(+) diff --git a/public/index.html b/public/index.html index b89dd83..17e2174 100644 --- a/public/index.html +++ b/public/index.html @@ -64,6 +64,7 @@

Weatherify ⛅

+
diff --git a/public/manifest.json b/public/manifest.json index 407ceb6..aa09cb0 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -12,6 +12,7 @@ "sizes": "192x192 512x512", "type": "image/svg+xml", "purpose": "any maskable" + } ] } diff --git a/public/script.js b/public/script.js index eaee046..9d783bb 100644 --- a/public/script.js +++ b/public/script.js @@ -100,6 +100,7 @@ function renderAqiUI(airPollution) { if (aqiPollutantsEl) { // Show the most common pollutant components when available + // const parts = []; const pm25 = pollutants.pm2_5; const pm10 = pollutants.pm10; diff --git a/public/style.css b/public/style.css index 385c8bc..77f274e 100644 --- a/public/style.css +++ b/public/style.css @@ -1211,6 +1211,7 @@ h1 { @keyframes popIn { 0% { opacity: 0; transform: scale(0); } 100% { opacity: 1; transform: scale(1); } + } @keyframes drawLine { diff --git a/public/sw.js b/public/sw.js index d8630b7..6fcf8ac 100644 --- a/public/sw.js +++ b/public/sw.js @@ -15,6 +15,7 @@ self.addEventListener('install', (event) => { caches.open(CACHE_NAME) .then((cache) => cache.addAll(ASSETS_TO_CACHE)) .then(() => self.skipWaiting()) + ); }); diff --git a/server.js b/server.js index e82476c..754e4da 100644 --- a/server.js +++ b/server.js @@ -13,6 +13,7 @@ const app = express(); * Rate limiter — 100 requests per 15 minutes per IP. * Applied to all /api/ routes to protect the OpenWeatherMap API key * from exhaustion by malicious actors or rogue scripts. + * ***** */ const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes From 309e7f907fb38e091fb1b64abe51ebca0daaf88c Mon Sep 17 00:00:00 2001 From: Muskankr Date: Wed, 13 May 2026 23:23:45 +0530 Subject: [PATCH 03/13] B fix: improve weather dashboard validation and graph readability --- public/script.js | 41 +++++++++++++++++++++++++++++++++++++---- public/style.css | 35 ++++++++++++++++++++++++++--------- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/public/script.js b/public/script.js index 5bc19a6..d267972 100644 --- a/public/script.js +++ b/public/script.js @@ -79,6 +79,11 @@ let sunTimeline = null; let dailyTrendData = []; let selectedTrendMetric = 'avg'; +function isValidCity(city) { + const cityRegex = /^[A-Za-z\s\-'.]{2,}$/; + return cityRegex.test(city.trim()); +} + // Unit toggle document.querySelectorAll('.unit-btn').forEach(btn => { btn.addEventListener('click', () => { @@ -95,7 +100,7 @@ document.querySelectorAll('.unit-btn').forEach(btn => { // Event Listeners searchBtn.addEventListener('click', handleSearch); cityInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { + if (e.key === 'Enter' && !searchBtn.disabled) { hideSuggestions(); handleSearch(); } @@ -105,11 +110,32 @@ cityInput.addEventListener('keypress', (e) => { let debounceTimer; cityInput.addEventListener('input', (e) => { hideError(); + const query = e.target.value.trim(); - searchBtn.disabled = query.length === 0; clearTimeout(debounceTimer); + // Empty input + if (query.length === 0) { + searchBtn.disabled = true; + cityInput.classList.remove('input-error'); + hideSuggestions(); + return; + } + + // Invalid city input + if (!isValidCity(query)) { + searchBtn.disabled = true; + cityInput.classList.add('input-error'); + showError('Please enter a valid city name.'); + hideSuggestions(); + return; + } + + // Valid input + cityInput.classList.remove('input-error'); + searchBtn.disabled = false; + if (query.length < 2) { hideSuggestions(); return; @@ -191,10 +217,17 @@ window.addEventListener('DOMContentLoaded', () => { function handleSearch() { const city = cityInput.value.trim(); - if (city) { - fetchWeatherData(city); + + if (!isValidCity(city)) { + cityInput.classList.add('input-error'); + showError('Please enter a valid city name.'); + return; } + + cityInput.classList.remove('input-error'); + fetchWeatherData(city); } + function detectUserLocation() { if (!navigator.geolocation) { fetchWeatherData('London'); diff --git a/public/style.css b/public/style.css index 2afc69c..19c7c46 100644 --- a/public/style.css +++ b/public/style.css @@ -581,7 +581,7 @@ h1 { } .graph-grid-line { - stroke: rgba(102, 126, 234, 0.15); + stroke: rgba(255, 255, 255, 0.12); stroke-width: 1; stroke-dasharray: 5 5; } @@ -610,15 +610,20 @@ h1 { opacity: 0; } -.graph-point-label { - fill: #2d3748; - font-size: 12px; +.graph-point-label, +.trend-value-label { + fill: #ffffff; + font-size: 13px; font-weight: 700; + text-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); + paint-order: stroke; + stroke: rgba(0, 0, 0, 0.35); + stroke-width: 1px; } .graph-axis-label { - fill: #718096; - font-size: 11px; + fill: rgba(255, 255, 255, 0.88); + font-size: 12px; font-weight: 600; } @@ -834,12 +839,12 @@ h1 { animation: popIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } -.trend-value-label { +/* .trend-value-label { paint-order: stroke; stroke: #f8fbff; stroke-width: 5px; stroke-linejoin: round; -} +} */ .trend-stats { display: grid; @@ -1608,5 +1613,17 @@ body.dark-mode .loading p { } @keyframes spin { - 100% { transform: rotate(360deg); } + 100% { + transform: rotate(360deg); + } +} + +.input-error { + border: 1px solid #ff4d4f !important; + box-shadow: 0 0 10px rgba(255, 77, 79, 0.35); + transition: all 0.2s ease; +} + +#error-message { + transition: all 0.2s ease; } \ No newline at end of file From d72515a53cf44e404b18abc2bb7ead339c53d919 Mon Sep 17 00:00:00 2001 From: Aishwarya Date: Thu, 14 May 2026 00:29:55 +0530 Subject: [PATCH 04/13] Add hourly weather forecast with charts --- public/index.html | 30 ++++-- public/script.js | 252 +++++++++++++++++++++++++++++++++------------- public/style.css | 44 +++++++- 3 files changed, 245 insertions(+), 81 deletions(-) diff --git a/public/index.html b/public/index.html index 17e2174..1069282 100644 --- a/public/index.html +++ b/public/index.html @@ -25,11 +25,11 @@ - + @@ -235,21 +235,37 @@

Sun Position

--
- - - +
+ +
+
+ Humidity & Precip (Next 24 Hours) + -- +
+ +
+ + +
+ +
+ +
+
+
-

5-Day Forecast

+

24-Hour Forecast

Upcoming weather trend will appear here.

- +
+
- ${Math.round(day.high)}${DEGREE}C high - ${Math.round(day.low)}${DEGREE}C low - ${Math.round(day.avg)}${DEGREE}C avg + ${toUnit(day.high)} high + ${toUnit(day.low)} low + ${toUnit(day.avg)} avg
`).join(''); @@ -613,9 +613,9 @@ function renderTrendChart(trendData) { const padding = { top: 46, right: 42, bottom: 48, left: 54 }; const innerWidth = width - padding.left - padding.right; const innerHeight = height - padding.top - padding.bottom; - const values = trendData.map((day) => day[metric]); - const lowValues = trendData.map((day) => day.low); - const highValues = trendData.map((day) => day.high); + const values = trendData.map((day) => toUnitNum(day[metric])); + const lowValues = trendData.map((day) => toUnitNum(day.low)); + const highValues = trendData.map((day) => toUnitNum(day.high)); const minValue = Math.floor(Math.min(...lowValues) - 1); const maxValue = Math.ceil(Math.max(...highValues) + 1); const range = Math.max(maxValue - minValue, 1); @@ -624,11 +624,14 @@ function renderTrendChart(trendData) { const getY = (value) => padding.top + ((maxValue - value) / range) * innerHeight; const points = trendData.map((day, index) => { const x = padding.left + (index * innerWidth) / Math.max(trendData.length - 1, 1); + const convertedMetric = toUnitNum(day[metric]); return { ...day, x, - y: getY(day[metric]), - value: day[metric] + y: getY(convertedMetric), + value: convertedMetric, + highConverted: toUnitNum(day.high), + lowConverted: toUnitNum(day.low) }; }); @@ -638,9 +641,9 @@ function renderTrendChart(trendData) { return ` - - - + + + `; }).join(''); @@ -650,13 +653,13 @@ function renderTrendChart(trendData) { const value = Math.round(maxValue - range * step); return ` - ${value}${DEGREE} + ${value}${currentUnit === 'K' ? 'K' : DEGREE} `; }).join(''); const labels = points.map((point) => ` - ${Math.round(point.value)}${DEGREE} + ${Math.round(point.value)}${currentUnit === 'K' ? 'K' : DEGREE} ${point.dayLabel} `).join(''); @@ -674,7 +677,7 @@ function renderTrendChart(trendData) { } if (trendChartRange) { - trendChartRange.textContent = `${Math.round(Math.min(...values))}${DEGREE}C - ${Math.round(Math.max(...values))}${DEGREE}C`; + trendChartRange.textContent = `${Math.round(Math.min(...values))}${unitLabel()} - ${Math.round(Math.max(...values))}${unitLabel()}`; } } From b18bf3e4b290e643951778864486cc109652a9e2 Mon Sep 17 00:00:00 2001 From: Ashutosh Singh <121851703+ashujsrfox@users.noreply.github.com> Date: Thu, 14 May 2026 23:37:58 +0530 Subject: [PATCH 06/13] Revert "fix: improve weather dashboard validation and graph readability" --- public/script.js | 41 +++-------------------------------------- public/style.css | 35 ++++++++--------------------------- 2 files changed, 11 insertions(+), 65 deletions(-) diff --git a/public/script.js b/public/script.js index 6c8ce58..7470084 100644 --- a/public/script.js +++ b/public/script.js @@ -472,11 +472,6 @@ hourlyMetricControls.forEach((btn) => { }); -function isValidCity(city) { - const cityRegex = /^[A-Za-z\s\-'.]{2,}$/; - return cityRegex.test(city.trim()); -} - // Unit toggle document.querySelectorAll('.unit-btn').forEach(btn => { @@ -494,7 +489,7 @@ document.querySelectorAll('.unit-btn').forEach(btn => { // Event Listeners searchBtn.addEventListener('click', handleSearch); cityInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter' && !searchBtn.disabled) { + if (e.key === 'Enter') { hideSuggestions(); handleSearch(); } @@ -511,34 +506,12 @@ if (locationBtn) { let debounceTimer; cityInput.addEventListener('input', (e) => { hideError(); - const query = e.target.value.trim(); searchBtn.disabled = query.length === 0; clearBtn.classList.toggle('hidden', cityInput.value.length === 0); clearTimeout(debounceTimer); - // Empty input - if (query.length === 0) { - searchBtn.disabled = true; - cityInput.classList.remove('input-error'); - hideSuggestions(); - return; - } - - // Invalid city input - if (!isValidCity(query)) { - searchBtn.disabled = true; - cityInput.classList.add('input-error'); - showError('Please enter a valid city name.'); - hideSuggestions(); - return; - } - - // Valid input - cityInput.classList.remove('input-error'); - searchBtn.disabled = false; - if (query.length < 2) { hideSuggestions(); return; @@ -633,18 +606,10 @@ window.addEventListener('DOMContentLoaded', () => { function handleSearch() { const city = cityInput.value.trim(); - - if (!isValidCity(city)) { - cityInput.classList.add('input-error'); - showError('Please enter a valid city name.'); - return; + if (city) { + fetchWeatherData(city); } - - cityInput.classList.remove('input-error'); - fetchWeatherData(city); } - -function detectUserLocation() { function getLocationErrorMessage(error) { if (!error) return 'Unable to get your location.'; diff --git a/public/style.css b/public/style.css index d18d374..f0c35a7 100644 --- a/public/style.css +++ b/public/style.css @@ -894,7 +894,7 @@ body.dark-mode .hourly-toggle.active { } .graph-grid-line { - stroke: rgba(255, 255, 255, 0.12); + stroke: rgba(102, 126, 234, 0.15); stroke-width: 1; stroke-dasharray: 5 5; } @@ -923,20 +923,15 @@ body.dark-mode .hourly-toggle.active { opacity: 0; } -.graph-point-label, -.trend-value-label { - fill: #ffffff; - font-size: 13px; +.graph-point-label { + fill: #2d3748; + font-size: 12px; font-weight: 700; - text-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); - paint-order: stroke; - stroke: rgba(0, 0, 0, 0.35); - stroke-width: 1px; } .graph-axis-label { - fill: rgba(255, 255, 255, 0.88); - font-size: 12px; + fill: #718096; + font-size: 11px; font-weight: 600; } @@ -1152,12 +1147,12 @@ body.dark-mode .hourly-toggle.active { animation: popIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } -/* .trend-value-label { +.trend-value-label { paint-order: stroke; stroke: #f8fbff; stroke-width: 5px; stroke-linejoin: round; -} */ +} .trend-stats { display: grid; @@ -2064,20 +2059,6 @@ body.dark-mode .loading p { } @keyframes spin { - 100% { - transform: rotate(360deg); - } -} - -.input-error { - border: 1px solid #ff4d4f !important; - box-shadow: 0 0 10px rgba(255, 77, 79, 0.35); - transition: all 0.2s ease; -} - -#error-message { - transition: all 0.2s ease; -} 100% { transform: rotate(360deg); } } From afad04f1ec01087e351bfa71ee42973161886e7e Mon Sep 17 00:00:00 2001 From: Ashutosh Singh <121851703+ashujsrfox@users.noreply.github.com> Date: Thu, 14 May 2026 23:39:47 +0530 Subject: [PATCH 07/13] Revert "Feature/hourly weather charts" --- public/index.html | 81 +----- public/manifest.json | 1 - public/script.js | 658 +++++-------------------------------------- public/style.css | 258 +---------------- public/sw.js | 1 - server.js | 3 - 6 files changed, 86 insertions(+), 916 deletions(-) diff --git a/public/index.html b/public/index.html index 26266ef..96ebaf5 100644 --- a/public/index.html +++ b/public/index.html @@ -25,11 +25,11 @@ + - @@ -65,29 +65,6 @@

Weatherify ⛅

- - -
-
-

Saved Cities

-

Quickly reopen favorite cities or recent searches.

-
-
-
-
Favorites
-
-
- -
-
Recent
-
-
- -
- -
-
-
@@ -114,12 +91,7 @@

Welcome to Weatherify ⛅

-
-

City Name

- -
+

City Name

Date

@@ -196,26 +168,6 @@

Sun Position

-- km
- -
- -
- Air Quality (AQI) - - -- - -- - - Pollutants -- - Recommendation -- -
-
-
@@ -246,7 +198,6 @@

Sun Position

-
@@ -254,37 +205,21 @@

Sun Position

--
- -
-
- -
-
- Humidity & Precip (Next 24 Hours) - -- -
- -
- - -
- -
- + + +
-
-

24-Hour Forecast

+

5-Day Forecast

Upcoming weather trend will appear here.

- +
-
@@ -91,7 +114,12 @@

Welcome to Weatherify ⛅

-

City Name

+
+

City Name

+ +

Date

@@ -168,6 +196,26 @@

Sun Position

-- km
+ +
+ +
+ Air Quality (AQI) + + -- + -- + + Pollutants -- + Recommendation -- +
+
+
@@ -198,6 +246,7 @@

Sun Position

+
@@ -205,21 +254,37 @@

Sun Position

--
- - - +
+ +
+
+ Humidity & Precip (Next 24 Hours) + -- +
+ +
+ + +
+ +
+ +
+
+
-

5-Day Forecast

+

24-Hour Forecast

Upcoming weather trend will appear here.

- +
+