diff --git a/public/components/current-weather.js b/public/components/current-weather.js
new file mode 100644
index 0000000..5b81238
--- /dev/null
+++ b/public/components/current-weather.js
@@ -0,0 +1,1050 @@
+class CurrentWeather extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this._weatherData = null;
+ this._aqiData = null;
+ this._unit = 'C';
+ this._lang = 'en';
+ this._isFavorite = false;
+ this._sunTimer = null;
+ }
+
+ static get observedAttributes() {
+ return ['lang', 'unit', 'is-favorite'];
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue === newValue) return;
+ if (name === 'lang') {
+ this._lang = newValue;
+ this.updateTranslations();
+ } else if (name === 'unit') {
+ this._unit = newValue;
+ this.updateUnit();
+ } else if (name === 'is-favorite') {
+ this._isFavorite = this.hasAttribute('is-favorite');
+ this.updateFavoriteButton();
+ }
+ }
+
+ get weatherData() { return this._weatherData; }
+ set weatherData(val) {
+ this._weatherData = val;
+ this.render();
+ }
+
+ get aqiData() { return this._aqiData; }
+ set aqiData(val) {
+ this._aqiData = val;
+ this.renderAqi();
+ }
+
+ connectedCallback() {
+ this.render();
+ this._sunTimer = setInterval(() => this.renderSunPosition(), 60000);
+ }
+
+ disconnectedCallback() {
+ if (this._sunTimer) {
+ clearInterval(this._sunTimer);
+ }
+ }
+
+ toUnit(kelvin) {
+ const DEGREE = '\u00B0';
+ if (this._unit === 'C') return `${Math.round(kelvin - 273.15)}${DEGREE}C`;
+ if (this._unit === 'F') return `${Math.round((kelvin - 273.15) * 9 / 5 + 32)}${DEGREE}F`;
+ return `${Math.round(kelvin)}K`;
+ }
+
+ toUnitNum(kelvin) {
+ if (this._unit === 'C') return Math.round(kelvin - 273.15);
+ if (this._unit === 'F') return Math.round((kelvin - 273.15) * 9 / 5 + 32);
+ return Math.round(kelvin);
+ }
+
+ unitLabel() {
+ const DEGREE = '\u00B0';
+ if (this._unit === 'C') return `${DEGREE}C`;
+ if (this._unit === 'F') return `${DEGREE}F`;
+ return 'K';
+ }
+
+ getWindDirection(deg) {
+ if (deg === undefined || deg === null) return '';
+ const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
+ const index = Math.round(deg / 45) % 8;
+ const arrows = ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖'];
+ return `${directions[index]} ${arrows[index]}`;
+ }
+
+ getAqiCategory(aqi) {
+ const value = Number(aqi);
+ if (!Number.isFinite(value)) {
+ return { label: 'Unknown', level: 0, badgeClass: '' };
+ }
+ 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' };
+ }
+ }
+
+ 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.';
+ }
+ }
+
+ getShiftedDate(unixSeconds, timezoneOffsetSeconds) {
+ return new Date((unixSeconds + timezoneOffsetSeconds) * 1000);
+ }
+
+ formatDateAtOffset(unixSeconds, timezoneOffsetSeconds) {
+ const locale = this._lang === 'hi' ? 'hi-IN' : this._lang === 'es' ? 'es-ES' : this._lang === 'fr' ? 'fr-FR' : 'en-US';
+ return this.getShiftedDate(unixSeconds, timezoneOffsetSeconds).toLocaleDateString(locale, {
+ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC'
+ });
+ }
+
+ formatTimeAtOffset(unixSeconds, timezoneOffsetSeconds) {
+ const locale = this._lang === 'hi' ? 'hi-IN' : this._lang === 'es' ? 'es-ES' : this._lang === 'fr' ? 'fr-FR' : 'en-US';
+ return this.getShiftedDate(unixSeconds, timezoneOffsetSeconds).toLocaleTimeString(locale, {
+ hour: '2-digit', minute: '2-digit', timeZone: 'UTC'
+ });
+ }
+
+ getNextSunriseSeconds(todaySunrise, nowSeconds) {
+ const daySeconds = 24 * 60 * 60;
+ const daysAhead = Math.floor((nowSeconds - todaySunrise) / daySeconds) + 1;
+ return todaySunrise + daysAhead * daySeconds;
+ }
+
+ formatDuration(seconds) {
+ const totalMinutes = Math.max(0, Math.round(seconds / 60));
+ const hours = Math.floor(totalMinutes / 60);
+ const minutes = totalMinutes % 60;
+ const hStr = this._lang === 'hi' ? 'घं' : 'h';
+ const mStr = this._lang === 'hi' ? 'मि' : 'm';
+ if (hours === 0) return `${minutes}${mStr}`;
+ if (minutes === 0) return `${hours}${hStr}`;
+ return `${hours}${hStr} ${minutes}${mStr}`;
+ }
+
+ render() {
+ if (!this._weatherData) {
+ this.shadowRoot.innerHTML = '';
+ return;
+ }
+
+ const data = this._weatherData;
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+ const ICON_URL = 'https://openweathermap.org/img/wn';
+ const iconCode = data.weather[0].icon;
+
+ this.shadowRoot.innerHTML = `
+
+
+
+
+
+
+
${this.formatDateAtOffset(Math.floor(Date.now() / 1000), data.timezone)}
+
+
+
+
+
![${data.weather[0].description}](${ICON_URL}/${iconCode}@4x.png)
+
+
+ ${this.toUnitNum(data.main.temp)}
+ ${this.unitLabel()}
+
+
${data.weather[0].description}
+
${t['feelsLikeMain'] ? t['feelsLikeMain'].replace('--°', this.toUnit(data.main.feels_like)) : `Feels like ${this.toUnit(data.main.feels_like)}`}
+
+
+
+
+
+
+
+
+
+ ${t['feelsLike'] || 'Feels Like'}
+ ${this.toUnit(data.main.feels_like)}
+
+
+
+
+
+
+ ${t['humidity'] || 'Humidity'}
+ ${data.main.humidity}%
+
+
+
+
+
+
+
${t['windSpeed'] || 'Wind Speed'}
+
+ ${Math.round(data.wind.speed * 3.6)} km/h ${this.getWindDirection(data.wind.deg)}
+
+
+
+
+
+
+
+
+
+
+ ${t['pressure'] || 'Pressure'}
+ ${data.main.pressure} hPa
+
+
+
+
+
+
+ ${t['visibility'] || 'Visibility'}
+ ${(data.visibility / 1000).toFixed(1)} km
+
+
+
+
+
+
+ ${t['aqi'] || 'Air Quality (AQI)'}
+
+ --
+ --
+
+ Pollutants --
+ Recommendation --
+
+
+
+
+
+
+ ${t['sunrise'] || 'Sunrise'}
+ ${this.formatTimeAtOffset(data.sys.sunrise, data.timezone)}
+
+
+
+
+
+
+ ${t['sunset'] || 'Sunset'}
+ ${this.formatTimeAtOffset(data.sys.sunset, data.timezone)}
+
+
+
+
+
+
+
+
+ ${t['uvIndex'] || 'UV Index'}
+ --
+
+
+
+ `;
+
+ this.setupShareAndFavorite();
+ this.renderSunPosition();
+ this.renderAqi();
+ }
+
+ setupShareAndFavorite() {
+ const shareBtn = this.shadowRoot.getElementById('share-btn');
+ const shareMenu = this.shadowRoot.getElementById('share-menu');
+ const copyLinkBtn = this.shadowRoot.getElementById('copy-link-btn');
+ const copySummaryBtn = this.shadowRoot.getElementById('copy-summary-btn');
+ const favoriteBtn = this.shadowRoot.getElementById('favorite-btn');
+
+ if (shareBtn) {
+ shareBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const isHidden = shareMenu.classList.contains('hidden');
+ shareMenu.classList.toggle('hidden', !isHidden);
+ shareBtn.setAttribute('aria-expanded', String(isHidden));
+ });
+ }
+
+ if (copyLinkBtn) {
+ copyLinkBtn.addEventListener('click', async () => {
+ const link = this.getShareableLink();
+ const copied = await this.copyTextToClipboard(link);
+ this.showShareStatus(copied ? 'Link copied!' : 'Failed to copy.');
+ if (copied) setTimeout(() => this.toggleShareMenu(false), 900);
+ });
+ }
+
+ if (copySummaryBtn) {
+ copySummaryBtn.addEventListener('click', async () => {
+ const summary = this.getShareSummary();
+ const copied = await this.copyTextToClipboard(summary);
+ this.showShareStatus(copied ? 'Summary copied!' : 'Failed to copy.');
+ if (copied) setTimeout(() => this.toggleShareMenu(false), 900);
+ });
+ }
+
+ if (favoriteBtn) {
+ favoriteBtn.addEventListener('click', () => {
+ this.dispatchEvent(new CustomEvent('toggle-favorite', {
+ detail: {
+ query: `${this._weatherData.name},${this._weatherData.sys.country}`,
+ label: `${this._weatherData.name}, ${this._weatherData.sys.country}`
+ },
+ bubbles: true,
+ composed: true
+ }));
+ });
+ }
+
+ document.addEventListener('click', (e) => {
+ const path = e.composedPath();
+ if (!path.includes(shareBtn) && !path.includes(shareMenu)) {
+ this.toggleShareMenu(false);
+ }
+ });
+ }
+
+ toggleShareMenu(show) {
+ const shareMenu = this.shadowRoot.getElementById('share-menu');
+ const shareBtn = this.shadowRoot.getElementById('share-btn');
+ if (shareMenu && shareBtn) {
+ shareMenu.classList.toggle('hidden', !show);
+ shareBtn.setAttribute('aria-expanded', String(show));
+ if (!show) this.showShareStatus('');
+ }
+ }
+
+ showShareStatus(msg) {
+ const status = this.shadowRoot.getElementById('share-status-text');
+ if (status) status.textContent = msg;
+ }
+
+ async copyTextToClipboard(text) {
+ if (!text) return false;
+ if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {}
+ }
+ const textarea = document.createElement('textarea');
+ textarea.value = text;
+ textarea.setAttribute('readonly', '');
+ textarea.style.position = 'absolute';
+ textarea.style.left = '-9999px';
+ this.shadowRoot.appendChild(textarea);
+ textarea.select();
+ let success = false;
+ try {
+ success = document.execCommand('copy');
+ } catch {
+ success = false;
+ }
+ this.shadowRoot.removeChild(textarea);
+ return success;
+ }
+
+ getShareableLink() {
+ const city = `${this._weatherData.name},${this._weatherData.sys.country}`;
+ const url = new URL(window.location.href);
+ url.searchParams.set('city', city);
+ url.searchParams.set('units', this._unit);
+ return url.toString();
+ }
+
+ getShareSummary() {
+ const description = this._weatherData.weather?.[0]?.description || '';
+ const formattedDescription = description
+ .split(' ')
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
+ const temperature = `${this.toUnitNum(this._weatherData.main.temp)}${this.unitLabel()}`;
+ const humidityValue = this._weatherData.main.humidity !== undefined ? `${this._weatherData.main.humidity}%` : '--';
+ const cityLabel = `${this._weatherData.name}, ${this._weatherData.sys.country}`;
+ return `${cityLabel}: ${temperature}, Humidity ${humidityValue}, ${formattedDescription}`;
+ }
+
+ renderSunPosition() {
+ if (!this._weatherData) return;
+
+ const sunMarker = this.shadowRoot.getElementById('sun-marker');
+ const sunPhase = this.shadowRoot.getElementById('sun-phase');
+ const sunProgress = this.shadowRoot.getElementById('sun-progress');
+ const solarNoon = this.shadowRoot.getElementById('solar-noon');
+ if (!sunMarker || !sunPhase || !sunProgress || !solarNoon) return;
+
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ const nowSeconds = Math.floor(Date.now() / 1000);
+ const timezone = this._weatherData.timezone;
+ const sunriseTime = this._weatherData.sys.sunrise;
+ const sunsetTime = this._weatherData.sys.sunset;
+ const daylight = Math.max(sunsetTime - sunriseTime, 1);
+ const midpoint = sunriseTime + Math.floor(daylight / 2);
+ let progress = 0;
+ let phaseText = '';
+ let progressText = '';
+
+ if (nowSeconds <= sunriseTime) {
+ phaseText = this._lang === 'hi' ? 'सूर्योदय से पहले' : 'Before sunrise';
+ progressText = this._lang === 'hi' ? `सूर्योदय में ${this.formatDuration(sunriseTime - nowSeconds)}` : `${this.formatDuration(sunriseTime - nowSeconds)} until sunrise`;
+ progress = 0;
+ } else if (nowSeconds >= sunsetTime) {
+ phaseText = this._lang === 'hi' ? 'सूर्यास्त के बाद' : 'After sunset';
+ progressText = this._lang === 'hi' ? `सूर्योदय में ${this.formatDuration(this.getNextSunriseSeconds(sunriseTime, nowSeconds) - nowSeconds)}` : `${this.formatDuration(this.getNextSunriseSeconds(sunriseTime, nowSeconds) - nowSeconds)} until sunrise`;
+ progress = 100;
+ } else {
+ progress = ((nowSeconds - sunriseTime) / daylight) * 100;
+ phaseText = t['daylight'] || 'Daylight';
+ progressText = this._lang === 'hi' ? `दिन का ${Math.round(progress)}% हिस्सा पूरा हुआ` : `${Math.round(progress)}% of daylight completed`;
+ }
+
+ sunMarker.style.left = `${Math.min(Math.max(progress, 0), 100)}%`;
+ sunPhase.textContent = phaseText;
+ sunProgress.textContent = progressText;
+
+ const midpointLabel = this._lang === 'hi' ? 'सौर मध्यबिंदु' : this._lang === 'es' ? 'Mediodía solar' : this._lang === 'fr' ? 'Midi solaire' : 'Solar midpoint';
+ solarNoon.textContent = `${midpointLabel} ${this.formatTimeAtOffset(midpoint, timezone)}`;
+ }
+
+ renderAqi() {
+ const aqiCard = this.shadowRoot.getElementById('aqi-card');
+ const aqiValueEl = this.shadowRoot.getElementById('aqi-value');
+ const aqiBadgeEl = this.shadowRoot.getElementById('aqi-badge');
+ const aqiPollutantsEl = this.shadowRoot.getElementById('aqi-pollutants');
+ const aqiRecommendationEl = this.shadowRoot.getElementById('aqi-recommendation');
+
+ if (!aqiCard) return;
+
+ if (!this._aqiData || !Array.isArray(this._aqiData.list) || this._aqiData.list.length === 0) {
+ aqiCard.classList.add('hidden');
+ return;
+ }
+
+ aqiCard.classList.remove('hidden');
+
+ const entry = this._aqiData.list[0];
+ const aqi = entry?.main?.aqi;
+ const pollutants = entry?.components || {};
+
+ const { label, badgeClass } = this.getAqiCategory(aqi);
+
+ if (aqiValueEl) aqiValueEl.textContent = Number.isFinite(Number(aqi)) ? String(aqi) : '--';
+
+ if (aqiBadgeEl) {
+ aqiBadgeEl.textContent = label;
+ aqiBadgeEl.className = 'aqi-badge'; // Reset
+ if (badgeClass) aqiBadgeEl.classList.add(badgeClass);
+ }
+
+ if (aqiPollutantsEl) {
+ const parts = [];
+ const add = (key, name) => {
+ const v = pollutants[key];
+ if (v !== undefined && v !== null) parts.push(`${name}: ${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: ${this.getAqiHealthRecommendation(label)}`;
+ }
+ }
+
+ updateFavoriteButton() {
+ const btn = this.shadowRoot.getElementById('favorite-btn');
+ if (btn) {
+ btn.setAttribute('aria-pressed', String(this._isFavorite));
+ const icon = btn.querySelector('.favorite-icon');
+ if (icon) icon.textContent = this._isFavorite ? '★' : '☆';
+ btn.title = this._isFavorite ? 'Remove favorite' : 'Favorite';
+ }
+ }
+
+ updateUnit() {
+ // Redraw temperatures
+ if (!this._weatherData) return;
+ this.render();
+ }
+
+ updateTranslations() {
+ if (!this._weatherData) return;
+ this.render();
+ }
+}
+
+customElements.define('current-weather', CurrentWeather);
diff --git a/public/components/forecast-list.js b/public/components/forecast-list.js
new file mode 100644
index 0000000..cc32bff
--- /dev/null
+++ b/public/components/forecast-list.js
@@ -0,0 +1,986 @@
+class ForecastList extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this._forecastData = null;
+ this._trendData = [];
+ this._unit = 'C';
+ this._lang = 'en';
+ this._selectedHourlyMetric = 'humidity'; // humidity or precip
+ this._selectedTrendMetric = 'avg'; // avg, high, low
+ this._tempChart = null;
+ this._humidityPrecipChart = null;
+ }
+
+ static get observedAttributes() {
+ return ['lang', 'unit'];
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue === newValue) return;
+ if (name === 'lang') {
+ this._lang = newValue;
+ this.updateTranslationsAndRedraw();
+ } else if (name === 'unit') {
+ this._unit = newValue;
+ this.updateUnitAndRedraw();
+ }
+ }
+
+ get forecastData() { return this._forecastData; }
+ set forecastData(val) {
+ this._forecastData = val;
+ if (val) {
+ this._trendData = this.buildDailyTrendData(val.list, val.city?.timezone || 0);
+ this.render();
+ }
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ disconnectedCallback() {
+ this.destroyCharts();
+ }
+
+ destroyCharts() {
+ if (this._tempChart) {
+ this._tempChart.destroy();
+ this._tempChart = null;
+ }
+ if (this._humidityPrecipChart) {
+ this._humidityPrecipChart.destroy();
+ this._humidityPrecipChart = null;
+ }
+ }
+
+ toUnit(kelvin) {
+ const DEGREE = '\u00B0';
+ if (this._unit === 'C') return `${Math.round(kelvin - 273.15)}${DEGREE}C`;
+ if (this._unit === 'F') return `${Math.round((kelvin - 273.15) * 9 / 5 + 32)}${DEGREE}F`;
+ return `${Math.round(kelvin)}K`;
+ }
+
+ toUnitNum(kelvin) {
+ if (this._unit === 'C') return Math.round(kelvin - 273.15);
+ if (this._unit === 'F') return Math.round((kelvin - 273.15) * 9 / 5 + 32);
+ return Math.round(kelvin);
+ }
+
+ unitLabel() {
+ const DEGREE = '\u00B0';
+ if (this._unit === 'C') return `${DEGREE}C`;
+ if (this._unit === 'F') return `${DEGREE}F`;
+ return 'K';
+ }
+
+ getShiftedDate(unixSeconds, timezoneOffsetSeconds) {
+ return new Date((unixSeconds + timezoneOffsetSeconds) * 1000);
+ }
+
+ buildHourlyPoints(forecastList, timezoneOffsetSeconds, hoursAhead) {
+ if (!Array.isArray(forecastList) || forecastList.length === 0) return [];
+ const nowSeconds = Math.floor(Date.now() / 1000);
+ const endSeconds = nowSeconds + hoursAhead * 60 * 60;
+ const locale = this._lang === 'hi' ? 'hi-IN' : this._lang === 'es' ? 'es-ES' : this._lang === 'fr' ? 'fr-FR' : 'en-US';
+
+ const points = forecastList
+ .slice()
+ .sort((a, b) => a.dt - b.dt)
+ .filter((item) => item?.dt >= nowSeconds - 3600 && item?.dt <= endSeconds);
+
+ return points.slice(0, 10).map((item) => {
+ const localDate = this.getShiftedDate(item.dt, timezoneOffsetSeconds);
+ return {
+ raw: item,
+ dt: item.dt,
+ timeLabel: localDate.toLocaleTimeString(locale, { hour: 'numeric' }),
+ humidity: item.main?.humidity ?? null,
+ precipProb: item.pop ?? null,
+ precipAmount: item.rain?.['3h'] ?? null
+ };
+ });
+ }
+
+ buildDailyTrendData(forecastList, timezoneOffsetSeconds) {
+ const groupedDays = new Map();
+ const locale = this._lang === 'hi' ? 'hi-IN' : this._lang === 'es' ? 'es-ES' : this._lang === 'fr' ? 'fr-FR' : 'en-US';
+
+ forecastList.forEach((item) => {
+ const localDate = this.getShiftedDate(item.dt, timezoneOffsetSeconds);
+ const dateKey = localDate.toISOString().slice(0, 10);
+
+ if (!groupedDays.has(dateKey)) {
+ groupedDays.set(dateKey, {
+ dateKey,
+ dayLabel: localDate.toLocaleDateString(locale, { weekday: 'short', timeZone: 'UTC' }),
+ dateLabel: localDate.toLocaleDateString(locale, { month: 'short', day: 'numeric', timeZone: 'UTC' }),
+ temperatures: []
+ });
+ }
+ groupedDays.get(dateKey).temperatures.push(item.main.temp);
+ });
+
+ return Array.from(groupedDays.values()).slice(0, 5).map((day) => {
+ const high = Math.max(...day.temperatures);
+ const low = Math.min(...day.temperatures);
+ const avg = day.temperatures.reduce((total, temp) => total + temp, 0) / day.temperatures.length;
+ return { ...day, high, low, avg };
+ });
+ }
+
+ render() {
+ if (!this._forecastData) {
+ this.shadowRoot.innerHTML = '';
+ return;
+ }
+
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ this.shadowRoot.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
${t['forecast24h'] || '24-Hour Forecast'}
+
${t['forecastDesc'] || 'Upcoming weather trend will appear here.'}
+
+
+
+
+
+
+
+
+ `;
+
+ this.setupToggles();
+ this.renderHourlyForecastCards();
+ this.renderCharts();
+ this.renderWeatherTrends();
+ }
+
+ setupToggles() {
+ const hourlyToggles = this.shadowRoot.querySelectorAll('.hourly-toggle');
+ hourlyToggles.forEach(btn => {
+ btn.addEventListener('click', () => {
+ this._selectedHourlyMetric = btn.dataset.hourlyMetric;
+ hourlyToggles.forEach(b => b.classList.toggle('active', b === btn));
+ this.renderHumidityPrecipChart();
+ });
+ });
+
+ const trendToggles = this.shadowRoot.querySelectorAll('[data-trend-metric]');
+ trendToggles.forEach(btn => {
+ btn.addEventListener('click', () => {
+ this._selectedTrendMetric = btn.dataset.trendMetric;
+ trendToggles.forEach(b => b.classList.toggle('active', b === btn));
+ this.renderTrendChart();
+ });
+ });
+ }
+
+ renderHourlyForecastCards() {
+ const container = this.shadowRoot.getElementById('forecast-container');
+ if (!container) return;
+
+ const timezoneOffsetSeconds = this._forecastData.city?.timezone || 0;
+ const locale = this._lang === 'hi' ? 'hi-IN' : this._lang === 'es' ? 'es-ES' : this._lang === 'fr' ? 'fr-FR' : 'en-US';
+ const ICON_URL = 'https://openweathermap.org/img/wn';
+
+ const hoursAhead = 24;
+ const nowSeconds = Math.floor(Date.now() / 1000);
+ const endSeconds = nowSeconds + hoursAhead * 60 * 60;
+ const points = this._forecastData.list
+ .slice()
+ .sort((a, b) => a.dt - b.dt)
+ .filter((item) => item?.dt >= nowSeconds - 3600 && item?.dt <= endSeconds)
+ .slice(0, 10);
+
+ container.innerHTML = points.map(item => {
+ const localDate = this.getShiftedDate(item.dt, timezoneOffsetSeconds);
+ const timeLabel = localDate.toLocaleTimeString(locale, { hour: 'numeric', hour12: true });
+ const iconCode = item.weather[0].icon;
+ const temp = this.toUnitNum(item.main.temp);
+ const desc = item.weather[0].description;
+
+ return `
+
+
${timeLabel}
+
+

+
+
${temp}${this.unitLabel()}
+
${desc}
+
+ `;
+ }).join('');
+ }
+
+ renderCharts() {
+ this.destroyCharts();
+ this.renderTemperatureChart();
+ this.renderHumidityPrecipChart();
+ }
+
+ renderTemperatureChart() {
+ const canvas = this.shadowRoot.getElementById('temperature-chart');
+ if (!canvas) return;
+
+ const timezoneOffsetSeconds = this._forecastData.city?.timezone || 0;
+ const hoursAhead = 24;
+ const nowSeconds = Math.floor(Date.now() / 1000);
+ const endSeconds = nowSeconds + hoursAhead * 60 * 60;
+ const hourlyData = this._forecastData.list
+ .slice()
+ .sort((a, b) => a.dt - b.dt)
+ .filter((item) => item?.dt >= nowSeconds - 3600 && item?.dt <= endSeconds)
+ .slice(0, 10);
+
+ if (!hourlyData.length) return;
+
+ const locale = this._lang === 'hi' ? 'hi-IN' : this._lang === 'es' ? 'es-ES' : this._lang === 'fr' ? 'fr-FR' : 'en-US';
+ const labels = hourlyData.map(item => {
+ const date = new Date(item.dt * 1000);
+ return date.toLocaleTimeString(locale, { hour: 'numeric', hour12: true });
+ });
+
+ const temperatures = hourlyData.map(item => this.toUnitNum(item.main.temp));
+
+ const minTemp = Math.min(...temperatures);
+ const maxTemp = Math.max(...temperatures);
+ const graphRange = this.shadowRoot.getElementById('graph-range');
+ if (graphRange) graphRange.textContent = `${minTemp}${this.unitLabel()} - ${maxTemp}${this.unitLabel()}`;
+
+ // Set summary
+ const forecastSummary = this.shadowRoot.getElementById('forecast-summary');
+ if (forecastSummary) {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+ const trend = temperatures[temperatures.length - 1] > temperatures[0]
+ ? (this._lang === 'hi' ? 'तापमान में बढ़ोतरी होगी' : 'Temperatures are expected to rise')
+ : (this._lang === 'hi' ? 'तापमान में गिरावट होगी' : 'Temperatures are expected to fall');
+ forecastSummary.textContent = `${trend} (${minTemp}${this.unitLabel()} - ${maxTemp}${this.unitLabel()}).`;
+ }
+
+ const data = {
+ labels: labels,
+ datasets: [{
+ label: `Temperature (${this.unitLabel()})`,
+ data: temperatures,
+ borderColor: 'rgba(75, 192, 192, 1)',
+ backgroundColor: 'rgba(75, 192, 192, 0.2)',
+ fill: true,
+ tension: 0.4
+ }]
+ };
+
+ const config = {
+ type: 'line',
+ data: data,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: { legend: { display: false } },
+ scales: {
+ y: {
+ beginAtZero: false,
+ ticks: { callback: (value) => value + this.unitLabel() }
+ }
+ }
+ }
+ };
+
+ if (window.Chart) {
+ this._tempChart = new window.Chart(canvas, config);
+ }
+ }
+
+ renderHumidityPrecipChart() {
+ const canvas = this.shadowRoot.getElementById('humidity-precip-chart');
+ const rangeEl = this.shadowRoot.getElementById('humidity-precip-range');
+ if (!canvas || !rangeEl) return;
+
+ const timezoneOffsetSeconds = this._forecastData.city?.timezone || 0;
+ const hourlyPoints = this.buildHourlyPoints(this._forecastData.list, timezoneOffsetSeconds, 24);
+
+ if (hourlyPoints.length === 0) {
+ rangeEl.textContent = '--';
+ return;
+ }
+
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+ const metric = this._selectedHourlyMetric;
+ const labels = hourlyPoints.map(p => p.timeLabel);
+ const values = hourlyPoints.map(p => {
+ if (metric === 'humidity') return p.humidity || 0;
+ return (p.precipProb || 0) * 100;
+ });
+
+ const data = {
+ labels: labels,
+ datasets: [{
+ label: metric === 'humidity' ? `${t['humidity'] || 'Humidity'} (%)` : `${t['precip'] || 'Precipitation'} (%)`,
+ data: values,
+ borderColor: metric === 'humidity' ? 'rgba(54, 162, 235, 1)' : 'rgba(255, 99, 132, 1)',
+ backgroundColor: metric === 'humidity' ? 'rgba(54, 162, 235, 0.2)' : 'rgba(255, 99, 132, 0.2)',
+ fill: true,
+ tension: 0.4
+ }]
+ };
+
+ const config = {
+ type: 'line',
+ data: data,
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: { legend: { display: false } },
+ scales: {
+ y: {
+ beginAtZero: true,
+ max: 100,
+ ticks: { callback: (value) => value + '%' }
+ }
+ }
+ }
+ };
+
+ if (this._humidityPrecipChart) {
+ this._humidityPrecipChart.destroy();
+ }
+
+ if (window.Chart) {
+ this._humidityPrecipChart = new window.Chart(canvas, config);
+ }
+
+ const minV = Math.min(...values);
+ const maxV = Math.max(...values);
+ rangeEl.textContent = `${minV}% - ${maxV}%`;
+ }
+
+ renderWeatherTrends() {
+ const trendStatsEl = this.shadowRoot.getElementById('trend-stats');
+ const trendsSummaryEl = this.shadowRoot.getElementById('trends-summary');
+ if (!trendStatsEl || !this._trendData.length) return;
+
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ // Render stats cards
+ trendStatsEl.innerHTML = this._trendData.map(day => `
+
+
+ ${day.dayLabel}
+ ${day.dateLabel}
+
+
+ ${this.toUnitNum(day.high)}${this.unitLabel()} ${t['high'] ? t['high'].toLowerCase() : 'high'}
+ ${this.toUnitNum(day.low)}${this.unitLabel()} ${t['low'] ? t['low'].toLowerCase() : 'low'}
+ ${this.toUnitNum(day.avg)}${this.unitLabel()} ${t['avg'] ? t['avg'].toLowerCase() : 'avg'}
+
+
+ `).join('');
+
+ // Update summary text
+ const avgs = this._trendData.map(day => this.toUnitNum(day.avg));
+ const overallAvg = Math.round(avgs.reduce((sum, v) => sum + v, 0) / avgs.length);
+ if (trendsSummaryEl) {
+ trendsSummaryEl.textContent = this._lang === 'hi'
+ ? `अगले 5 दिनों का औसत तापमान लगभग ${overallAvg}${this.unitLabel()} रहेगा।`
+ : `Average temperature over the next 5 days will be around ${overallAvg}${this.unitLabel()}.`;
+ }
+
+ this.renderTrendChart();
+ }
+
+ renderTrendChart() {
+ const trendChart = this.shadowRoot.getElementById('trend-chart');
+ const labelEl = this.shadowRoot.getElementById('trend-chart-label');
+ const rangeEl = this.shadowRoot.getElementById('trend-chart-range');
+ if (!trendChart || !this._trendData.length) return;
+
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+ const metricLabels = {
+ high: t['high'] || 'Daily High Temperature',
+ low: t['low'] || 'Daily Low Temperature',
+ avg: t['avgTemp'] || 'Daily Average Temperature'
+ };
+ const metricColors = {
+ high: '#f97316',
+ low: '#0ea5e9',
+ avg: '#667eea'
+ };
+ const metric = this._selectedTrendMetric;
+ const width = 760;
+ const height = 280;
+ 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 = this._trendData.map((day) => this.toUnitNum(day[metric]));
+ const lowValues = this._trendData.map((day) => this.toUnitNum(day.low));
+ const highValues = this._trendData.map((day) => this.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);
+ const barWidth = Math.min(58, innerWidth / this._trendData.length * 0.45);
+
+ const getY = (value) => padding.top + ((maxValue - value) / range) * innerHeight;
+ const points = this._trendData.map((day, index) => {
+ const x = padding.left + (index * innerWidth) / Math.max(this._trendData.length - 1, 1);
+ const val = this.toUnitNum(day[metric]);
+ return { ...day, x, y: getY(val), value: val };
+ });
+
+ const baselineY = height - padding.bottom;
+ const bars = points.map((point) => {
+ const barHeight = Math.max(baselineY - point.y, 3);
+ return `
+
+
+
+
+
+
+ `;
+ }).join('');
+ const linePoints = points.map((point) => `${point.x},${point.y}`).join(' ');
+ const gridLines = [0, 0.5, 1].map((step) => {
+ const y = padding.top + innerHeight * step;
+ const value = Math.round(maxValue - range * step);
+ return `
+
+ ${value}${this.unitLabel()}
+ `;
+ }).join('');
+ const labels = points.map((point) => `
+
+
+ ${Math.round(point.value)}${this.unitLabel()}
+ ${point.dayLabel}
+
+ `).join('');
+
+ trendChart.style.setProperty('--trend-color', metricColors[metric]);
+ trendChart.innerHTML = `
+ ${gridLines}
+ ${bars}
+
+ ${labels}
+ `;
+
+ if (labelEl) labelEl.textContent = metricLabels[metric];
+ if (rangeEl) rangeEl.textContent = `${Math.round(Math.min(...values))}${this.unitLabel()} - ${Math.round(Math.max(...values))}${this.unitLabel()}`;
+ }
+
+ updateUnitAndRedraw() {
+ if (!this._forecastData) return;
+ this._trendData = this.buildDailyTrendData(this._forecastData.list, this._forecastData.city?.timezone || 0);
+ this.render();
+ }
+
+ updateTranslationsAndRedraw() {
+ if (!this._forecastData) return;
+ this._trendData = this.buildDailyTrendData(this._forecastData.list, this._forecastData.city?.timezone || 0);
+ this.render();
+ }
+}
+
+customElements.define('forecast-list', ForecastList);
diff --git a/public/components/weather-header.js b/public/components/weather-header.js
new file mode 100644
index 0000000..3ab1b60
--- /dev/null
+++ b/public/components/weather-header.js
@@ -0,0 +1,305 @@
+class WeatherHeader extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this._lang = 'en';
+ this._theme = 'light';
+ this._unit = 'C';
+ }
+
+ static get observedAttributes() {
+ return ['lang', 'theme', 'unit'];
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue === newValue) return;
+ if (name === 'lang') {
+ this._lang = newValue;
+ this.updateTranslations();
+ } else if (name === 'theme') {
+ this._theme = newValue;
+ this.updateTheme();
+ } else if (name === 'unit') {
+ this._unit = newValue;
+ this.updateUnit();
+ }
+ }
+
+ connectedCallback() {
+ this.render();
+ this.setupEventListeners();
+ }
+
+ render() {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ this.shadowRoot.innerHTML = `
+
+
+ ${t['appTitle'] || 'Weatherify ⛅'}
+
+
+
+
+
+
+
+
+
+
+
+ `;
+
+ this.shadowRoot.getElementById('language-toggle').value = this._lang;
+ this.updateThemeEffects();
+ }
+
+ setupEventListeners() {
+ const langToggle = this.shadowRoot.getElementById('language-toggle');
+ langToggle.addEventListener('change', (e) => {
+ const lang = e.target.value;
+ this.dispatchEvent(new CustomEvent('lang-change', {
+ detail: { lang },
+ bubbles: true,
+ composed: true
+ }));
+ });
+
+ const themeToggle = this.shadowRoot.getElementById('theme-toggle');
+ themeToggle.addEventListener('click', () => {
+ const newTheme = this._theme === 'dark' ? 'light' : 'dark';
+ this.dispatchEvent(new CustomEvent('theme-change', {
+ detail: { theme: newTheme },
+ bubbles: true,
+ composed: true
+ }));
+ });
+
+ const unitButtons = this.shadowRoot.querySelectorAll('.unit-btn');
+ unitButtons.forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ const unit = btn.dataset.unit;
+ this.dispatchEvent(new CustomEvent('unit-change', {
+ detail: { unit },
+ bubbles: true,
+ composed: true
+ }));
+ });
+ });
+ }
+
+ updateTranslations() {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+ const titleEl = this.shadowRoot.getElementById('app-title');
+ if (titleEl) titleEl.textContent = t['appTitle'] || 'Weatherify ⛅';
+
+ const langToggle = this.shadowRoot.getElementById('language-toggle');
+ if (langToggle) langToggle.value = this._lang;
+
+ const labelEl = this.shadowRoot.querySelector('.toggle-label');
+ if (labelEl) {
+ labelEl.textContent = this._theme === 'dark' ? (t['themeLight'] || 'Light') : (t['themeDark'] || 'Dark');
+ }
+ }
+
+ updateTheme() {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+ const labelEl = this.shadowRoot.querySelector('.toggle-label');
+ if (labelEl) {
+ labelEl.textContent = this._theme === 'dark' ? (t['themeLight'] || 'Light') : (t['themeDark'] || 'Dark');
+ }
+ this.updateThemeEffects();
+ }
+
+ updateThemeEffects() {
+ const themeToggle = this.shadowRoot.getElementById('theme-toggle');
+ if (themeToggle) {
+ const isDark = this._theme === 'dark';
+ themeToggle.setAttribute('aria-pressed', String(isDark));
+ themeToggle.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to dark theme');
+ themeToggle.title = isDark ? 'Switch to light theme' : 'Switch to dark theme';
+ const icon = themeToggle.querySelector('.toggle-icon');
+ if (icon) icon.textContent = isDark ? '☀️' : '🌙';
+ }
+ }
+
+ updateUnit() {
+ const buttons = this.shadowRoot.querySelectorAll('.unit-btn');
+ buttons.forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.unit === this._unit);
+ });
+ }
+}
+
+customElements.define('weather-header', WeatherHeader);
diff --git a/public/components/weather-map.js b/public/components/weather-map.js
new file mode 100644
index 0000000..1bcc199
--- /dev/null
+++ b/public/components/weather-map.js
@@ -0,0 +1,285 @@
+class WeatherMap extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this._lang = 'en';
+ this._lat = null;
+ this._lon = null;
+ this._cityName = '';
+ this._mapInstance = null;
+ this._mapMarker = null;
+ this._activeWeatherLayer = null;
+ }
+
+ static get observedAttributes() {
+ return ['lang'];
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue === newValue) return;
+ if (name === 'lang') {
+ this._lang = newValue;
+ this.updateTranslations();
+ }
+ }
+
+ setMapCoords(lat, lon, cityName) {
+ this._lat = lat;
+ this._lon = lon;
+ this._cityName = cityName;
+ this.initOrUpdateMap();
+ }
+
+ connectedCallback() {
+ this.render();
+ }
+
+ render() {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ this.shadowRoot.innerHTML = `
+
+
+
+
+ `;
+
+ this.setupLayerControls();
+ this.initOrUpdateMap();
+ }
+
+ setupLayerControls() {
+ const controls = this.shadowRoot.querySelectorAll('.map-layer-controls .trend-toggle');
+ controls.forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ controls.forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+
+ const layerType = btn.dataset.layer;
+ this.updateWeatherLayer(layerType);
+ });
+ });
+ }
+
+ initOrUpdateMap() {
+ const mapElement = this.shadowRoot.getElementById('weather-map');
+ if (!mapElement || this._lat === null || this._lon === null) return;
+
+ const L = window.L;
+ if (!L) return;
+
+ if (!this._mapInstance) {
+ this._mapInstance = L.map(mapElement).setView([this._lat, this._lon], 10);
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '© OpenStreetMap contributors'
+ }).addTo(this._mapInstance);
+ this._mapMarker = L.marker([this._lat, this._lon]).addTo(this._mapInstance);
+ } else {
+ this._mapInstance.setView([this._lat, this._lon], 10);
+ this._mapMarker.setLatLng([this._lat, this._lon]);
+ }
+ this._mapMarker.bindPopup(`${this._cityName}`).openPopup();
+ }
+
+ updateWeatherLayer(layerType) {
+ const L = window.L;
+ if (!L || !this._mapInstance) return;
+
+ if (this._activeWeatherLayer) {
+ this._mapInstance.removeLayer(this._activeWeatherLayer);
+ this._activeWeatherLayer = null;
+ }
+
+ let layerUrl = '';
+ if (layerType === 'precipitation') {
+ layerUrl = 'https://tile.openweathermap.org/map/precipitation_new/{z}/{x}/{y}.png?appid=MOCK_KEY';
+ } else if (layerType === 'clouds') {
+ layerUrl = 'https://tile.openweathermap.org/map/clouds_new/{z}/{x}/{y}.png?appid=MOCK_KEY';
+ }
+
+ if (layerUrl && layerType !== 'base') {
+ // Keep the code disabled as in original codebase
+ // this._activeWeatherLayer = L.tileLayer(layerUrl, { opacity: 0.6 }).addTo(this._mapInstance);
+ }
+ }
+
+ updateTranslations() {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ const titleEl = this.shadowRoot.getElementById('weather-map-title');
+ if (titleEl) titleEl.textContent = t['interactiveMap'] || 'Interactive Map';
+
+ const summaryEl = this.shadowRoot.getElementById('map-summary');
+ if (summaryEl) summaryEl.textContent = t['mapDesc'] || 'Live spatial weather patterns.';
+
+ const controls = this.shadowRoot.querySelectorAll('.map-layer-controls .trend-toggle');
+ controls.forEach(btn => {
+ const layer = btn.dataset.layer;
+ if (layer === 'base') btn.textContent = t['mapBase'] || 'Base';
+ else if (layer === 'precipitation') btn.textContent = t['mapRain'] || 'Rain';
+ else if (layer === 'clouds') btn.textContent = t['mapClouds'] || 'Clouds';
+ });
+ }
+}
+
+customElements.define('weather-map', WeatherMap);
diff --git a/public/components/weather-search.js b/public/components/weather-search.js
new file mode 100644
index 0000000..0023835
--- /dev/null
+++ b/public/components/weather-search.js
@@ -0,0 +1,839 @@
+class WeatherSearch extends HTMLElement {
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this._lang = 'en';
+ this._loading = false;
+ this._error = '';
+ this._welcome = true;
+ this._favorites = [];
+ this._recent = [];
+ this._debounceTimer = null;
+ }
+
+ static get observedAttributes() {
+ return ['lang', 'loading', 'error', 'welcome'];
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (oldValue === newValue) return;
+ if (name === 'lang') {
+ this._lang = newValue;
+ this.updateTranslations();
+ } else if (name === 'loading') {
+ this._loading = this.hasAttribute('loading');
+ this.updateLoadingState();
+ } else if (name === 'error') {
+ this._error = newValue || '';
+ this.updateErrorState();
+ } else if (name === 'welcome') {
+ this._welcome = this.hasAttribute('welcome');
+ this.updateWelcomeState();
+ }
+ }
+
+ get favorites() { return this._favorites; }
+ set favorites(val) {
+ this._favorites = val || [];
+ this.renderHistory();
+ }
+
+ get recent() { return this._recent; }
+ set recent(val) {
+ this._recent = val || [];
+ this.renderHistory();
+ }
+
+ connectedCallback() {
+ this.render();
+ this.setupEventListeners();
+ }
+
+ render() {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+ this.shadowRoot.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
${this._error || (t['cityNotFound'] || 'City not found. Please check spelling and try again.')}
+
+
+
+
+
+
+
+
${t['welcomeTitle'] || 'Welcome to Weatherify ⛅'}
+
${t['welcomeDesc'] || 'Search for a city to view its weather forecast'}
+
+
+
+
+
+ `;
+
+ this.renderHistory();
+ this.updateLoadingState();
+ this.updateErrorState();
+ this.updateWelcomeState();
+ }
+
+ setupEventListeners() {
+ const cityInput = this.shadowRoot.getElementById('city-input');
+ const clearBtn = this.shadowRoot.getElementById('clear-btn');
+ const searchBtn = this.shadowRoot.getElementById('search-btn');
+ const locationBtn = this.shadowRoot.getElementById('location-btn');
+
+ cityInput.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ this.hideSuggestions();
+ this.handleSearchSubmit();
+ }
+ });
+
+ cityInput.addEventListener('focus', () => {
+ if (!cityInput.value.trim()) {
+ this.openHistoryDropdown();
+ }
+ });
+
+ cityInput.addEventListener('input', (e) => {
+ this.setAttribute('error', ''); // Clear error state on input
+ const query = e.target.value.trim();
+ searchBtn.disabled = query.length === 0;
+ clearBtn.classList.toggle('hidden', cityInput.value.length === 0);
+
+ clearTimeout(this._debounceTimer);
+
+ if (query.length < 2) {
+ this.hideSuggestions();
+ if (!query) {
+ this.openHistoryDropdown();
+ } else {
+ this.closeHistoryDropdown();
+ }
+ return;
+ }
+
+ this.closeHistoryDropdown();
+ this._debounceTimer = setTimeout(() => this.fetchCitySuggestions(query), 300);
+ });
+
+ clearBtn.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ cityInput.value = '';
+ cityInput.dispatchEvent(new Event('input', { bubbles: true }));
+ cityInput.focus();
+ });
+
+ searchBtn.addEventListener('click', () => this.handleSearchSubmit());
+
+ if (locationBtn) {
+ locationBtn.addEventListener('click', () => {
+ this.hideSuggestions();
+ this.dispatchEvent(new CustomEvent('location-request', {
+ bubbles: true,
+ composed: true
+ }));
+ });
+ }
+
+ // Handle clicking outside suggestions/history
+ document.addEventListener('click', (e) => {
+ const path = e.composedPath();
+ if (!path.includes(this)) {
+ this.hideSuggestions();
+ this.closeHistoryDropdown();
+ }
+ });
+ }
+
+ handleSearchSubmit() {
+ const cityInput = this.shadowRoot.getElementById('city-input');
+ const city = cityInput.value.trim();
+ if (city) {
+ this.dispatchEvent(new CustomEvent('city-select', {
+ detail: { city },
+ bubbles: true,
+ composed: true
+ }));
+ }
+ }
+
+ async fetchCitySuggestions(query) {
+ try {
+ const url = `/api/geo?q=${encodeURIComponent(query)}&limit=5&lang=${this._lang}`;
+ const response = await fetch(url);
+ if (!response.ok) return;
+
+ const cities = await response.json();
+ this.displaySuggestions(cities);
+ } catch (error) {
+ console.error('Error fetching suggestions:', error);
+ }
+ }
+
+ displaySuggestions(cities) {
+ const container = this.shadowRoot.getElementById('suggestions-dropdown');
+ if (!container) return;
+
+ if (cities.length === 0) {
+ this.hideSuggestions();
+ return;
+ }
+
+ container.innerHTML = '';
+
+ cities.forEach((city) => {
+ const suggestion = document.createElement('div');
+ suggestion.className = 'suggestion-item';
+
+ const localName = city.local_names && city.local_names[this._lang] ? city.local_names[this._lang] : city.name;
+ const text = `${localName}, ${city.state ? `${city.state}, ` : ''}${city.country}`;
+ suggestion.textContent = text;
+
+ suggestion.addEventListener('click', () => {
+ const cityInput = this.shadowRoot.getElementById('city-input');
+ const clearBtn = this.shadowRoot.getElementById('clear-btn');
+
+ cityInput.value = localName;
+ clearBtn.classList.remove('hidden');
+ this.hideSuggestions();
+
+ this.dispatchEvent(new CustomEvent('city-select', {
+ detail: { city: localName },
+ bubbles: true,
+ composed: true
+ }));
+ });
+
+ container.appendChild(suggestion);
+ });
+
+ container.classList.remove('hidden');
+ }
+
+ hideSuggestions() {
+ const container = this.shadowRoot.getElementById('suggestions-dropdown');
+ if (container) container.classList.add('hidden');
+ }
+
+ openHistoryDropdown() {
+ const dropdown = this.shadowRoot.getElementById('history-dropdown');
+ if (dropdown && (this._favorites.length || this._recent.length)) {
+ dropdown.classList.remove('hidden');
+ }
+ }
+
+ closeHistoryDropdown() {
+ const dropdown = this.shadowRoot.getElementById('history-dropdown');
+ if (dropdown) dropdown.classList.add('hidden');
+ }
+
+ renderHistory() {
+ const dropdown = this.shadowRoot.getElementById('history-dropdown');
+ if (!dropdown) return;
+
+ const inner = dropdown.querySelector('.history-dropdown-inner');
+ if (!inner) return;
+
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ const favoriteMarkup = this._favorites.map((item) => `
+
+ `).join('');
+
+ const recentMarkup = this._recent.map((item) => `
+
+ `).join('');
+
+ const hasHistory = this._favorites.length > 0 || this._recent.length > 0;
+ const noRecentText = t['recent'] ? `No ${t['recent'].toLowerCase()} searches yet.` : 'No recent searches yet.';
+
+ const emptyState = !hasHistory ? `
+
+ ` : '';
+
+ inner.innerHTML = `
+
+
${t['favorites'] || 'Favorites'}
+
${favoriteMarkup}
+
+
+
${t['recent'] || 'Recent'}
+
${recentMarkup}
+
+ ${emptyState}
+
+
+
+ `;
+
+ // Add event listeners inside history list
+ inner.querySelectorAll('.history-button').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const query = btn.dataset.query;
+ this.closeHistoryDropdown();
+
+ // Also set the input value
+ const cityInput = this.shadowRoot.getElementById('city-input');
+ const clearBtn = this.shadowRoot.getElementById('clear-btn');
+ const searchBtn = this.shadowRoot.getElementById('search-btn');
+ if (cityInput) {
+ cityInput.value = btn.querySelector('span').textContent;
+ if (clearBtn) clearBtn.classList.remove('hidden');
+ if (searchBtn) searchBtn.disabled = false;
+ }
+
+ this.dispatchEvent(new CustomEvent('city-select', {
+ detail: { city: query },
+ bubbles: true,
+ composed: true
+ }));
+ });
+ });
+
+ const clearBtn = inner.querySelector('.clear-history-btn');
+ if (clearBtn) {
+ clearBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ this.dispatchEvent(new CustomEvent('clear-history', {
+ bubbles: true,
+ composed: true
+ }));
+ });
+ }
+ }
+
+ updateLoadingState() {
+ const loadingEl = this.shadowRoot.getElementById('loading');
+ if (loadingEl) {
+ loadingEl.classList.toggle('hidden', !this._loading);
+ }
+ }
+
+ updateErrorState() {
+ const errorEl = this.shadowRoot.getElementById('error-message');
+ if (errorEl) {
+ const p = errorEl.querySelector('p');
+ if (this._error) {
+ p.textContent = this._error;
+ errorEl.classList.remove('hidden');
+ } else {
+ errorEl.classList.add('hidden');
+ }
+ }
+ }
+
+ updateWelcomeState() {
+ const welcomeEl = this.shadowRoot.getElementById('no-data-message');
+ if (welcomeEl) {
+ welcomeEl.classList.toggle('hidden', !this._welcome);
+ }
+ }
+
+ updateTranslations() {
+ const t = window.translations?.[this._lang] || window.translations?.['en'] || {};
+
+ // placeholder
+ const cityInput = this.shadowRoot.getElementById('city-input');
+ if (cityInput) cityInput.placeholder = t['searchPlaceholder'] || 'Enter city name...';
+
+ // location label
+ const locationLabel = this.shadowRoot.querySelector('.location-label');
+ if (locationLabel) locationLabel.textContent = t['useMyLocation'] || 'Use My Location';
+
+ // welcome message
+ const welcomeEl = this.shadowRoot.getElementById('no-data-message');
+ if (welcomeEl) {
+ const h2 = welcomeEl.querySelector('h2');
+ const p = welcomeEl.querySelector('p');
+ h2.textContent = t['welcomeTitle'] || 'Welcome to Weatherify ⛅';
+ p.textContent = t['welcomeDesc'] || 'Search for a city to view its weather forecast';
+ }
+
+ // history headers
+ this.renderHistory();
+ }
+
+ // Public method to set input value
+ setInputValue(val) {
+ const cityInput = this.shadowRoot.getElementById('city-input');
+ const clearBtn = this.shadowRoot.getElementById('clear-btn');
+ const searchBtn = this.shadowRoot.getElementById('search-btn');
+ if (cityInput) {
+ cityInput.value = val;
+ if (clearBtn) clearBtn.classList.toggle('hidden', !val);
+ if (searchBtn) searchBtn.disabled = !val;
+ }
+ }
+}
+
+customElements.define('weather-search', WeatherSearch);
diff --git a/public/index.html b/public/index.html
index 5e74865..84fd3f2 100644
--- a/public/index.html
+++ b/public/index.html
@@ -35,356 +35,31 @@
+
+
+
+
+
+
+
-
- Weatherify ⛅
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
City not found. Please check spelling and try again.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Welcome to Weatherify ⛅
-
Search for a city to view its weather forecast
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
-
-
-
-
- --
- °C
-
-
Weather Description
-
Feels like --°C
-
-
-
-
-
-
-
-
- Sunrise in --
- Solar midpoint --:--
-
-
-
-
-
-
-
-
-
-
- Feels Like
- --°C
-
-
-
-
-
-
-
-
-
Wind Speed
-
-- km/h
-
Wind Speed
-
-- km/h
-
-
-
-
-
-
-
-
-
- Pressure
- -- hPa
-
-
-
-
-
-
- Visibility
- -- km
-
-
-
-
-
-
- Air Quality (AQI)
-
- --
- --
-
- Pollutants --
- Recommendation --
-
-
-
-
-
-
- Sunrise
- --:--
-
-
-
-
-
-
-
-
-
-
- UV Index
- --
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
24-Hour Forecast
-
Upcoming weather trend will appear here.
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
diff --git a/public/script.js b/public/script.js
index d39ed4e..8c939ec 100644
--- a/public/script.js
+++ b/public/script.js
@@ -1,203 +1,44 @@
// Weather and geocoding go through same-origin `/api/*` proxy (see server.js). No API key in the browser.
const API_BASE = '/api';
-const ICON_URL = 'https://openweathermap.org/img/wn';
-const DEGREE = '\u00B0';
const DEFAULT_CITY = 'New Delhi';
let currentUnit = 'C';
let rawData = { current: null, forecast: null, airQuality: null };
-
-// ============================================================
-// 🌍 MULTI-LANGUAGE SUPPORT (i18n) INTEGRATION
-// ============================================================
let currentLang = localStorage.getItem('weatherify-lang') || 'en';
+let currentTheme = localStorage.getItem('weatherify-theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
-function applyTranslations(lang) {
- if (!window.translations || !window.translations[lang]) return;
- const t = window.translations[lang];
-
- document.querySelectorAll('[data-i18n]').forEach(el => {
- const key = el.getAttribute('data-i18n');
- if (t[key]) el.textContent = t[key];
- });
-
- document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
- const key = el.getAttribute('data-i18n-placeholder');
- if (t[key]) el.setAttribute('placeholder', t[key]);
- });
-
- const isDark = document.body.classList.contains('dark-mode');
- const toggleLabel = document.querySelector('.toggle-label');
- if (toggleLabel) toggleLabel.textContent = isDark ? t['themeLight'] : t['themeDark'];
-
- if (rawData.current) {
- const feelsLikeMain = document.getElementById('feels-like-main');
- if (feelsLikeMain) feelsLikeMain.textContent = `${t['feelsLike']} ${toUnit(rawData.current.main.feels_like)}`;
- }
-}
-
-const languageToggle = document.getElementById('language-toggle');
-if (languageToggle) {
- languageToggle.value = currentLang;
- languageToggle.addEventListener('change', (e) => {
- currentLang = e.target.value;
- localStorage.setItem('weatherify-lang', currentLang);
- applyTranslations(currentLang);
- if (currentCityQuery) fetchWeatherData(currentCityQuery);
- else fetchWeatherData(DEFAULT_CITY);
- });
-}
-document.addEventListener('DOMContentLoaded', () => applyTranslations(currentLang));
-
-
-// ============================================================
-// 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);
+let currentCityQuery = '';
+let currentCityLabel = '';
+let recentSearches = [];
+let favoriteCities = [];
- if (aqiValueEl) aqiValueEl.textContent = Number.isFinite(Number(aqi)) ? String(aqi) : '--';
+const STORAGE_RECENT = 'weatherify-recent-cities';
+const STORAGE_FAVORITES = 'weatherify-favorite-cities';
+const MAX_RECENT_SEARCHES = 8;
- if (aqiBadgeEl) {
- aqiBadgeEl.textContent = label;
- aqiBadgeEl.classList.remove('aqi-1', 'aqi-2', 'aqi-3', 'aqi-4', 'aqi-5');
- if (badgeClass) aqiBadgeEl.classList.add(badgeClass);
- }
+// DOM Components references
+let appHeader, appSearch, mainWeather, dailyForecast, weatherMapComponent, weatherContainer, spinnerLoading;
- if (aqiPollutantsEl) {
- 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)}`;
- }
+function getShiftedDate(unixSeconds, timezoneOffsetSeconds) {
+ return new Date((unixSeconds + timezoneOffsetSeconds) * 1000);
}
-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();
+// i18n
+function applyTheme(theme) {
+ currentTheme = theme;
+ const isDark = theme === 'dark';
+ document.documentElement.classList.toggle('dark-mode', isDark);
+ document.body.classList.toggle('dark-mode', isDark);
+ localStorage.setItem('weatherify-theme', theme);
+ if (appHeader) appHeader.setAttribute('theme', theme);
}
function initUnitDisplay() {
- const unitElement = document.querySelector('.unit');
- if (unitElement) {
- unitElement.textContent = unitLabel();
- }
-}
-
-if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initUnitDisplay);
-} else {
- initUnitDisplay();
+ if (appHeader) appHeader.setAttribute('unit', currentUnit);
}
function toUnit(kelvin) {
+ const DEGREE = '\u00B0';
if (currentUnit === 'C') return `${Math.round(kelvin - 273.15)}${DEGREE}C`;
if (currentUnit === 'F') return `${Math.round((kelvin - 273.15) * 9 / 5 + 32)}${DEGREE}F`;
return `${Math.round(kelvin)}K`;
@@ -210,94 +51,12 @@ function toUnitNum(kelvin) {
}
function unitLabel() {
+ const DEGREE = '\u00B0';
if (currentUnit === 'C') return `${DEGREE}C`;
if (currentUnit === 'F') return `${DEGREE}F`;
return 'K';
}
-async function parseJsonSafe(response) {
- try {
- return await response.json();
- } catch {
- return null;
- }
-}
-
-// ============================================================
-// DOM Elements
-// ============================================================
-const cityInput = document.getElementById('city-input');
-const clearBtn = document.getElementById('clear-btn');
-const searchBtn = document.getElementById('search-btn');
-const locationBtn = document.getElementById('location-btn');
-const weatherContainer = document.getElementById('weather-container');
-const spinnerLoading = document.getElementById("spinner-loading");
-const errorMessage = document.getElementById('error-message');
-
-// Create suggestions dropdown
-const suggestionsContainer = document.createElement('div');
-suggestionsContainer.className = 'suggestions-container hidden';
-cityInput.parentNode.insertBefore(suggestionsContainer, cityInput.nextSibling);
-
-// Weather data elements
-const cityName = document.getElementById('city-name');
-const dateElement = document.getElementById('date');
-const tempElement = document.getElementById('temp');
-const weatherDesc = document.getElementById('weather-desc');
-const weatherIcon = document.getElementById('weather-icon');
-const feelsLike = document.getElementById('feels-like');
-const humidity = document.getElementById('humidity');
-const windSpeed = document.getElementById('wind-speed');
-const windCompassIcon = document.getElementById('wind-compass-icon');
-const pressure = document.getElementById('pressure');
-const visibility = document.getElementById('visibility');
-const sunrise = document.getElementById('sunrise');
-const sunset = document.getElementById('sunset');
-const sunPhase = document.getElementById('sun-phase');
-const sunMarker = document.getElementById('sun-marker');
-const sunProgress = document.getElementById('sun-progress');
-const solarNoon = document.getElementById('solar-noon');
-const forecastContainer = document.getElementById('forecast-container');
-const temperatureChartCanvas = document.getElementById('temperature-chart');
-const forecastSummary = document.getElementById('forecast-summary');
-const graphRange = document.getElementById('graph-range');
-
-const humidityPrecipChartCanvas = document.getElementById('humidity-precip-chart');
-const humidityPrecipRange = document.getElementById('humidity-precip-range');
-const hourlyMetricControls = document.querySelectorAll('.hourly-toggle');
-let selectedHourlyMetric = 'humidity';
-
-const trendsSummary = document.getElementById('trends-summary');
-const trendChart = document.getElementById('trend-chart');
-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 shareBtn = document.getElementById('share-btn');
-const shareMenu = document.getElementById('share-menu');
-const copyLinkBtn = document.getElementById('copy-link-btn');
-const copySummaryBtn = document.getElementById('copy-summary-btn');
-const shareStatusText = document.getElementById('share-status-text');
-
-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();
}
@@ -316,76 +75,24 @@ 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 t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : window.translations['en'];
- const noRecentText = t && t['recent'] ? `No ${t['recent'].toLowerCase()} searches yet.` : 'No recent searches yet.';
-
- const emptyState = !hasHistory ? `
-
- ` : '';
-
- const inner = historyDropdown.querySelector('.history-dropdown-inner');
- if (inner) {
- inner.innerHTML = `
-
-
${t ? t['favorites'] : 'Favorites'}
-
${favoriteMarkup}
-
-
-
${t ? t['recent'] : 'Recent'}
-
${recentMarkup}
-
- ${emptyState}
-
-
-
- `;
- }
-
- const newFavoriteList = document.getElementById('favorite-list');
- const newRecentList = document.getElementById('recent-list');
- const newClearHistoryBtn = document.getElementById('clear-history-btn');
+function initHistory() {
+ recentSearches = loadHistoryArray(STORAGE_RECENT);
+ favoriteCities = loadHistoryArray(STORAGE_FAVORITES);
- if (newFavoriteList) {
- newFavoriteList.addEventListener('click', handleHistoryClick);
- }
- if (newRecentList) {
- newRecentList.addEventListener('click', handleHistoryClick);
- }
- if (newClearHistoryBtn) {
- newClearHistoryBtn.addEventListener('click', (event) => {
- event.stopPropagation();
- clearRecentHistory();
- });
+ if (appSearch) {
+ appSearch.favorites = favoriteCities;
+ appSearch.recent = recentSearches;
}
}
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';
+ if (!mainWeather) return;
+ const isFav = favoriteCities.some((item) => normalizeCityKey(item.query) === normalizeCityKey(currentCityQuery));
+ if (isFav) {
+ mainWeather.setAttribute('is-favorite', '');
+ } else {
+ mainWeather.removeAttribute('is-favorite');
+ }
}
function createCityEntry(data) {
@@ -406,100 +113,30 @@ function addRecentSearch(entry) {
}
saveHistoryArray(STORAGE_RECENT, recentSearches);
- renderHistory();
+ if (appSearch) appSearch.recent = recentSearches;
}
function toggleFavoriteCity(entry) {
if (!entry || !entry.query) return;
const normalized = normalizeCityKey(entry.query);
- const existingIndex = favoriteCities.findIndex((item) => normalizeCityKey(item.query) === normalized);
+ const existsIndex = favoriteCities.findIndex((item) => normalizeCityKey(item.query) === normalized);
- if (existingIndex >= 0) {
- favoriteCities.splice(existingIndex, 1);
+ if (existsIndex > -1) {
+ favoriteCities.splice(existsIndex, 1);
} else {
- if(favoriteCities.length >= 5) {
- alert("You can only save up to 5 favorite cities.");
- return;
- }
- favoriteCities.unshift(entry);
+ favoriteCities.push(entry);
}
saveHistoryArray(STORAGE_FAVORITES, favoriteCities);
+ if (appSearch) appSearch.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;
- 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 });
- });
- }
+ if (appSearch) appSearch.recent = recentSearches;
}
function setCurrentCity(data) {
@@ -508,6 +145,9 @@ function setCurrentCity(data) {
updateFavoriteButton();
updateUrlParams(currentCityQuery, currentUnit);
addRecentSearch({ query: currentCityQuery, label: currentCityLabel });
+
+ // update search input value
+ if (appSearch) appSearch.setInputValue(currentCityLabel);
}
function updateUrlParams(city, units, replaceState = true) {
@@ -527,1102 +167,369 @@ function updateUrlParams(city, units, replaceState = true) {
window.history.replaceState({}, '', url);
}
} catch {
- // ignore invalid URL updates in older browsers
+ // ignore
}
}
function setCurrentUnit(unit) {
if (!['C', 'F', 'K'].includes(unit)) return;
currentUnit = unit;
- document.querySelectorAll('.unit-btn').forEach((btn) => {
- btn.classList.toggle('active', btn.dataset.unit === unit);
- });
- initUnitDisplay();
+ if (appHeader) appHeader.setAttribute('unit', unit);
updateUrlParams(currentCityQuery || undefined, currentUnit);
if (rawData.current) {
- updateUI(rawData.current);
- updateForecastUI(rawData.forecast);
- }
-}
-
-function toggleShareMenu(forceState) {
- if (!shareMenu || !shareBtn) return;
- const isOpen = shareMenu.classList.contains('hidden');
- const show = typeof forceState === 'boolean' ? forceState : isOpen;
- shareMenu.classList.toggle('hidden', !show);
- shareBtn.setAttribute('aria-expanded', String(show));
- if (!show) {
- showShareStatus('');
- }
-}
-
-function showShareStatus(message) {
- if (!shareStatusText) return;
- shareStatusText.textContent = message;
-}
-
-async function copyTextToClipboard(text) {
- if (!text) return false;
- if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
- try {
- await navigator.clipboard.writeText(text);
- return true;
- } catch {}
- }
-
- const textarea = document.createElement('textarea');
- textarea.value = text;
- textarea.setAttribute('readonly', '');
- textarea.style.position = 'absolute';
- textarea.style.left = '-9999px';
- document.body.appendChild(textarea);
- textarea.select();
-
- let success = false;
- try {
- success = document.execCommand('copy');
- } catch {
- success = false;
+ if (mainWeather) {
+ mainWeather.setAttribute('unit', unit);
+ mainWeather.setWeatherData(rawData.current, rawData.airQuality, currentUnit, currentLang, isFavorite(currentCityQuery));
+ }
+ if (dailyForecast) {
+ dailyForecast.setAttribute('unit', unit);
+ dailyForecast.forecastData = rawData.forecast;
+ }
}
- document.body.removeChild(textarea);
- return success;
}
-function getShareableLink() {
- const city = currentCityQuery || cityInput.value.trim();
- if (!city) return window.location.href;
- try {
- const url = new URL(window.location.href);
- url.searchParams.set('city', city);
- url.searchParams.set('units', currentUnit);
- return url.toString();
- } catch {
- return `${window.location.pathname}?city=${encodeURIComponent(city)}&units=${encodeURIComponent(currentUnit)}`;
- }
+function isFavorite(cityQuery) {
+ return favoriteCities.some((item) => normalizeCityKey(item.query) === normalizeCityKey(cityQuery));
}
-function getShareSummary() {
- if (!rawData.current) return '';
- const description = rawData.current.weather?.[0]?.description || '';
- const formattedDescription = description
- .split(' ')
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ');
- const temperature = `${toUnitNum(rawData.current.main.temp)}${unitLabel()}`;
- const humidityValue = rawData.current.main.humidity !== undefined ? `${rawData.current.main.humidity}%` : '--';
- const cityLabel = currentCityLabel || `${rawData.current.name}, ${rawData.current.sys.country}`;
- return `${cityLabel}: ${temperature}, Humidity ${humidityValue}, ${formattedDescription}`;
-}
+// Dynamic backgrounds based on weather type
+function updateDynamicBackground(data) {
+ const body = document.body;
+ const weatherType = (data.weather?.[0]?.main || '').toLowerCase();
+ const isNight = data.weather?.[0]?.icon?.includes('n');
+ const themeClasses = [
+ 'theme-clear-day', 'theme-clear-night', 'theme-clouds', 'theme-rain',
+ 'theme-drizzle', 'theme-thunderstorm', 'theme-snow', 'theme-mist',
+ 'theme-fog', 'theme-haze'
+ ];
-async function handleCopyLink() {
- const link = getShareableLink();
- const copied = await copyTextToClipboard(link);
- showShareStatus(copied ? 'Link copied to clipboard!' : 'Unable to copy link.');
- if (copied) setTimeout(() => toggleShareMenu(false), 900);
-}
+ body.classList.remove(...themeClasses);
-async function handleCopySummary() {
- const summary = getShareSummary();
- if (!summary) {
- showShareStatus('Weather unavailable to share.');
- return;
- }
- const copied = await copyTextToClipboard(summary);
- showShareStatus(copied ? 'Summary copied to clipboard!' : 'Unable to copy summary.');
- if (copied) setTimeout(() => toggleShareMenu(false), 900);
+ if (weatherType === 'clear') body.classList.add(isNight ? 'theme-clear-night' : 'theme-clear-day');
+ else if (weatherType === 'clouds') body.classList.add('theme-clouds');
+ else if (weatherType === 'rain') body.classList.add('theme-rain');
+ else if (weatherType === 'drizzle') body.classList.add('theme-drizzle');
+ else if (weatherType === 'thunderstorm') body.classList.add('theme-thunderstorm');
+ else if (weatherType === 'snow') body.classList.add('theme-snow');
+ else if (['mist', 'fog', 'haze', 'smoke'].includes(weatherType)) body.classList.add('theme-mist');
+ else body.classList.add(isNight ? 'theme-clear-night' : 'theme-clear-day');
}
-// Hourly metric toggle
-hourlyMetricControls.forEach((btn) => {
- btn.addEventListener('click', () => {
- selectedHourlyMetric = btn.dataset.hourlyMetric;
- hourlyMetricControls.forEach((b) => b.classList.toggle('active', b === btn));
- if (rawData.forecast) {
- const hoursAhead = 24;
- const hourlyPoints = buildHourlyPoints(rawData.forecast.list, rawData.forecast.city?.timezone || 0, hoursAhead);
- renderHumidityPrecipChart(hourlyPoints);
- }
- });
-});
-
-// Unit toggle
-document.querySelectorAll('.unit-btn').forEach((btn) => {
- btn.addEventListener('click', () => {
- setCurrentUnit(btn.dataset.unit);
- });
-});
-
-// Event Listeners
-searchBtn.addEventListener('click', handleSearch);
-cityInput.addEventListener('keypress', (e) => {
- if (e.key === 'Enter') {
- hideSuggestions();
- handleSearch();
+function setWeatherBackground(condition) {
+ const body = document.body;
+ switch (condition.toLowerCase()) {
+ case "clear":
+ body.style.background = "linear-gradient(to right, #f7b267, #f77f00)";
+ break;
+ case "clouds":
+ body.style.background = "linear-gradient(to right, #bdc3c7, #2c3e50)";
+ break;
+ case "rain":
+ body.style.background = "linear-gradient(to right, #3a7bd5, #3a6073)";
+ break;
+ case "drizzle":
+ body.style.background = "linear-gradient(to right, #89f7fe, #66a6ff)";
+ break;
+ case "snow":
+ body.style.background = "linear-gradient(to right, #e6dada, #274046)";
+ break;
+ case "thunderstorm":
+ body.style.background = "linear-gradient(to right, #141e30, #243b55)";
+ break;
+ default:
+ body.style.background = "linear-gradient(to right, #89f7fe, #66a6ff)";
}
-});
-
-if (locationBtn) {
- locationBtn.addEventListener('click', () => {
- hideSuggestions();
- requestWeatherFromMyLocation();
- });
}
-if (shareBtn) {
- shareBtn.addEventListener('click', (event) => {
- event.stopPropagation();
- toggleShareMenu();
- });
-}
-
-if (copyLinkBtn) {
- copyLinkBtn.addEventListener('click', handleCopyLink);
+function showLoading() {
+ if (appSearch) appSearch.setAttribute('loading', '');
+ if (spinnerLoading) spinnerLoading.classList.remove('hidden');
}
-if (copySummaryBtn) {
- copySummaryBtn.addEventListener('click', handleCopySummary);
+function hideLoading() {
+ if (appSearch) appSearch.removeAttribute('loading');
+ if (spinnerLoading) spinnerLoading.classList.add('hidden');
}
-// Autocomplete functionality
-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);
-
- if (query.length < 2) {
- hideSuggestions();
- return;
- }
-
- debounceTimer = setTimeout(() => fetchCitySuggestions(query), 300);
-});
-
-clearBtn.addEventListener('click', (e) => {
- e.preventDefault();
- e.stopPropagation();
- cityInput.value = '';
- cityInput.dispatchEvent(new Event('input', { bubbles: true }));
- cityInput.focus();
-});
-
-// Hide suggestions, history, and share menu when clicking outside search controls
-document.addEventListener('click', (e) => {
- if (!e.target.closest('.search-box')) hideSuggestions();
- if (!e.target.closest('.search-container')) closeHistoryDropdown();
- if (!e.target.closest('#share-menu') && !e.target.closest('#share-btn')) toggleShareMenu(false);
-});
-
-if (trendControls) {
- trendControls.addEventListener('click', (event) => {
- const button = event.target.closest('[data-trend-metric]');
- if (!button) return;
-
- selectedTrendMetric = button.dataset.trendMetric;
- updateTrendToggleState();
- renderTrendChart(dailyTrendData);
- });
+function showWeather() {
+ if (weatherContainer) weatherContainer.classList.remove('hidden');
+ if (appSearch) appSearch.removeAttribute('welcome');
}
-async function fetchCitySuggestions(query) {
- try {
- const url = `${API_BASE}/geo?q=${encodeURIComponent(query)}&limit=5&lang=${currentLang}`;
- const response = await fetch(url);
-
- if (!response.ok) return;
-
- const cities = await response.json();
- displaySuggestions(cities);
- } catch (error) {
- console.error('Error fetching suggestions:', error);
- }
+function hideWeather() {
+ if (weatherContainer) weatherContainer.classList.add('hidden');
+ if (appSearch) appSearch.setAttribute('welcome', '');
}
-function displaySuggestions(cities) {
- if (cities.length === 0) {
- hideSuggestions();
- return;
+function showError(message) {
+ if (appSearch) {
+ appSearch.setAttribute('error', message || 'City not found. Please check spelling and try again.');
}
-
- suggestionsContainer.innerHTML = '';
-
- cities.forEach((city) => {
- const suggestion = document.createElement('div');
- suggestion.className = 'suggestion-item';
-
- const localName = city.local_names && city.local_names[currentLang] ? city.local_names[currentLang] : city.name;
- suggestion.textContent = `${localName}, ${city.state ? `${city.state}, ` : ''}${city.country}`;
-
- suggestion.addEventListener('click', () => {
- cityInput.value = localName;
- clearBtn.classList.remove('hidden');
- hideSuggestions();
- fetchWeatherData(localName);
- });
-
- suggestionsContainer.appendChild(suggestion);
- });
-
- suggestionsContainer.classList.remove('hidden');
-}
-
-function hideSuggestions() {
- suggestionsContainer.classList.add('hidden');
}
-// Initialize with default city or shared link
-window.addEventListener('DOMContentLoaded', () => {
- if ('serviceWorker' in navigator) {
- navigator.serviceWorker.register('/sw.js').catch((err) => {
- console.error('Service Worker registration failed:', err);
- });
- }
- initHistory();
-
- const params = new URLSearchParams(window.location.search);
- const sharedUnits = params.get('units')?.toUpperCase();
- const sharedCity = params.get('city');
-
- if (sharedUnits) {
- setCurrentUnit(sharedUnits);
+function hideError() {
+ if (appSearch) {
+ appSearch.removeAttribute('error');
}
-
- if (sharedCity) {
- fetchWeatherData(sharedCity);
- } else {
- fetchWeatherData(DEFAULT_CITY);
- }
-});
-
-function handleSearch() {
- const city = cityInput.value.trim();
- if (city) {
- fetchWeatherData(city);
- }
-}
-
-function getLocationErrorMessage(error) {
- if (!error) return 'Unable to get your location.';
- if (error.code === 1) return 'Location permission denied. Please enable location access in your browser settings.';
- if (error.code === 2) return 'Your location is unavailable right now.';
- if (error.code === 3) return 'Timed out while trying to get your location. Please try again.';
- return 'Unable to get your location.';
}
-async function requestWeatherFromMyLocation() {
- if (locationBtn) locationBtn.disabled = true;
-
- if (!navigator.geolocation) {
- hideLoading();
- showError('Geolocation is not supported by your browser.');
- if (locationBtn) locationBtn.disabled = false;
- return;
+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}`);
}
-
- showLoading();
- hideError();
-
- navigator.geolocation.getCurrentPosition(
- (position) => {
- const { latitude, longitude } = position.coords;
- fetchWeatherByCoords(latitude, longitude);
- },
- (error) => {
- hideLoading();
- showError(getLocationErrorMessage(error));
-
- if (!navigator.onLine) {
- const cachedData = localStorage.getItem('weatherify-last-data');
- if (cachedData) {
- rawData = JSON.parse(cachedData);
- updateUI(rawData.current);
- updateForecastUI(rawData.forecast);
- showWeather();
- showError('You are offline. Showing last known weather data.');
- }
- }
-
- if (locationBtn) locationBtn.disabled = false;
- },
- { enableHighAccuracy: true, timeout: 10000, maximumAge: 30000 }
- );
-}
-
-function detectUserLocation() {
- requestWeatherFromMyLocation();
+ return response.json();
}
async function fetchWeatherByCoords(lat, lon) {
showLoading();
hideError();
+ hideWeather();
try {
- const [currentResponse, forecastResponse] = await Promise.all([
- fetch(`${API_BASE}/weather?lat=${lat}&lon=${lon}&units=standard&lang=${currentLang}`),
- fetch(`${API_BASE}/forecast?lat=${lat}&lon=${lon}&units=standard&lang=${currentLang}`)
+ const currentUrl = `${API_BASE}/weather?lat=${lat}&lon=${lon}&lang=${currentLang}`;
+ const forecastUrl = `${API_BASE}/forecast?lat=${lat}&lon=${lon}&lang=${currentLang}`;
+
+ const [currentRes, forecastRes] = await Promise.all([
+ fetch(currentUrl),
+ fetch(forecastUrl)
]);
- if (!currentResponse.ok || !forecastResponse.ok) {
- throw new Error('Unable to fetch location weather');
+ if (!currentRes.ok || !forecastRes.ok) {
+ throw new Error('Coordinate geocoding weather lookup failed');
}
- const currentData = await currentResponse.json();
- const forecastData = await forecastResponse.json();
+ const currentData = await currentRes.json();
+ const forecastData = await forecastRes.json();
- if (!currentData || !currentData.name || !currentData.sys || !forecastData || !forecastData.list) {
- throw new Error('Unable to fetch location weather');
+ let aqiData = null;
+ try {
+ aqiData = await fetchAirQualityByCoords(lat, lon);
+ } catch (e) {
+ console.warn('AQI fetch failed:', e);
}
- 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 (error) {
- rawData.airQuality = null;
- if (aqiCard) aqiCard.classList.add('hidden');
- }
- }
+ rawData = { current: currentData, forecast: forecastData, airQuality: aqiData };
- localStorage.setItem('weatherify-last-data', JSON.stringify(rawData));
- updateUI(currentData);
- updateForecastUI(forecastData);
- showWeather();
setCurrentCity(currentData);
+ updateDynamicBackground(currentData);
+ setWeatherBackground(currentData.weather[0].main);
- } catch (error) {
- if (!navigator.onLine) {
- const cachedData = localStorage.getItem('weatherify-last-data');
- if (cachedData) {
- rawData = JSON.parse(cachedData);
- updateUI(rawData.current);
- updateForecastUI(rawData.forecast);
- showWeather();
- showError('You are offline. Showing last known weather data.');
- hideLoading();
- return;
- }
+ if (mainWeather) {
+ mainWeather.setAttribute('unit', currentUnit);
+ mainWeather.setWeatherData(currentData, aqiData, currentUnit, currentLang, isFavorite(currentCityQuery));
}
- fetchWeatherData(DEFAULT_CITY);
+
+ if (dailyForecast) {
+ dailyForecast.setAttribute('unit', currentUnit);
+ dailyForecast.forecastData = forecastData;
+ }
+
+ if (weatherMapComponent) {
+ weatherMapComponent.setMapCoords(lat, lon, currentData.name);
+ }
+
+ showWeather();
+ } catch (err) {
+ console.error(err);
+ showError(currentLang === 'hi' ? 'मौसम का डेटा प्राप्त करने में विफल।' : 'Failed to retrieve weather data.');
} finally {
hideLoading();
- if (locationBtn) locationBtn.disabled = false;
}
}
async function fetchWeatherData(city) {
- if (aqiCard) aqiCard.classList.add('hidden');
+ if (!city) return;
+
showLoading();
hideError();
+ hideWeather();
try {
- const [currentResponse, forecastResponse] = await Promise.all([
- fetch(`${API_BASE}/weather?q=${encodeURIComponent(city)}&units=standard&lang=${currentLang}`),
- fetch(`${API_BASE}/forecast?q=${encodeURIComponent(city)}&units=standard&lang=${currentLang}`)
+ const currentUrl = `${API_BASE}/weather?q=${encodeURIComponent(city)}&lang=${currentLang}`;
+ const forecastUrl = `${API_BASE}/forecast?q=${encodeURIComponent(city)}&lang=${currentLang}`;
+
+ const [currentRes, forecastRes] = await Promise.all([
+ fetch(currentUrl),
+ fetch(forecastUrl)
]);
- if (!currentResponse.ok) {
- const errorData = await parseJsonSafe(currentResponse);
+ if (!currentRes.ok) {
+ const errorData = await currentRes.json().catch(() => null);
throw new Error(errorData?.message || 'City not found');
}
- if (!forecastResponse.ok) {
- const errorData = await parseJsonSafe(forecastResponse);
- throw new Error(errorData?.message || 'Forecast unavailable');
+ if (!forecastRes.ok) {
+ throw new Error('Forecast fetch failed');
}
- const currentData = await currentResponse.json();
- const forecastData = await forecastResponse.json();
+ const currentData = await currentRes.json();
+ const forecastData = await forecastRes.json();
- if (!currentData || !currentData.name || !currentData.sys || !forecastData || !forecastData.list) {
- throw new Error('City not found');
+ let aqiData = null;
+ try {
+ aqiData = await fetchAirQualityByCoords(currentData.coord.lat, currentData.coord.lon);
+ } catch (e) {
+ console.warn('AQI fetch failed:', e);
}
- 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');
- }
- }
+ rawData = { current: currentData, forecast: forecastData, airQuality: aqiData };
- 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) {
- const cachedData = localStorage.getItem('weatherify-last-data');
- if (cachedData) {
- rawData = JSON.parse(cachedData);
- updateUI(rawData.current);
- updateForecastUI(rawData.forecast);
- showWeather();
- showError('You are offline. Showing last known weather data.');
- hideLoading();
- return;
- }
+ updateDynamicBackground(currentData);
+ setWeatherBackground(currentData.weather[0].main);
+
+ if (mainWeather) {
+ mainWeather.setAttribute('unit', currentUnit);
+ mainWeather.setWeatherData(currentData, aqiData, currentUnit, currentLang, isFavorite(currentCityQuery));
+ }
+
+ if (dailyForecast) {
+ dailyForecast.setAttribute('unit', currentUnit);
+ dailyForecast.forecastData = forecastData;
}
- showError('City not found. Please check spelling and try again.');
+
+ if (weatherMapComponent) {
+ weatherMapComponent.setMapCoords(currentData.coord.lat, currentData.coord.lon, currentData.name);
+ }
+
+ showWeather();
+ } catch (err) {
+ console.error(err);
+ showError(currentLang === 'hi' ? 'शहर नहीं मिला। कृपया पुनः प्रयास करें।' : 'City not found. Please try again.');
} finally {
hideLoading();
}
}
-function setWeatherBackground(condition) {
- const body = document.body;
- switch (condition.toLowerCase()) {
- case "clear":
- body.style.background = "linear-gradient(to right, #ff7e5f, #feb47b)";
- break;
- case "clouds":
- body.style.background = "linear-gradient(to right, #bdc3c7, #2c3e50)";
- break;
- case "rain":
- body.style.background = "linear-gradient(to right, #4b79a1, #283e51)";
- break;
- case "snow":
- body.style.background = "linear-gradient(to right, #e6dada, #274046)";
- break;
- case "thunderstorm":
- body.style.background = "linear-gradient(to right, #141e30, #243b55)";
- break;
+function getLocationErrorMessage(error) {
+ switch (error.code) {
+ case error.PERMISSION_DENIED:
+ return currentLang === 'hi' ? 'उपयोगकर्ता ने स्थान का अनुरोध अस्वीकार कर दिया।' : 'User denied the request for Geolocation.';
+ case error.POSITION_UNAVAILABLE:
+ return currentLang === 'hi' ? 'स्थान की जानकारी अनुपलब्ध है।' : 'Location information is unavailable.';
+ case error.TIMEOUT:
+ return currentLang === 'hi' ? 'स्थान प्राप्त करने का समय समाप्त हो गया।' : 'The request to get user location timed out.';
default:
- body.style.background = "linear-gradient(to right, #89f7fe, #66a6ff)";
+ return currentLang === 'hi' ? 'एक अज्ञात त्रुटि हुई।' : 'An unknown error occurred.';
}
}
-function updateUI(data) {
- cityName.textContent = `${data.name}, ${data.sys.country}`;
- const locale = currentLang === 'hi' ? 'hi-IN' : currentLang === 'es' ? 'es-ES' : currentLang === 'fr' ? 'fr-FR' : 'en-US';
- const d = getShiftedDate(Math.floor(Date.now() / 1000), data.timezone);
- dateElement.textContent = d.toLocaleDateString(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' });
-
- tempElement.textContent = toUnitNum(data.main.temp);
- weatherDesc.textContent = data.weather[0].description;
-
- const iconCode = data.weather[0].icon;
- weatherIcon.innerHTML = `
`;
-
- feelsLike.textContent = toUnit(data.main.feels_like);
- const feelsLikeMain = document.getElementById('feels-like-main');
- if (feelsLikeMain) {
- const t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : window.translations['en'];
- feelsLikeMain.textContent = `${t['feelsLike']} ${toUnit(data.main.feels_like)}`;
- }
-
- humidity.textContent = `${data.main.humidity}%`;
- const windDir = getWindDirection(data.wind.deg);
- windSpeed.textContent = `${Math.round(data.wind.speed * 3.6)} km/h ${windDir}`;
- // Dynamic wind direction compass needle rotation logic
-if (windCompassIcon && data && data.wind && data.wind.deg !== undefined) {
- windCompassIcon.style.transform = `rotate(${data.wind.deg}deg)`;
-}
- pressure.textContent = `${data.main.pressure} hPa`;
- visibility.textContent = `${(data.visibility / 1000).toFixed(1)} km`;
-
- sunrise.textContent = formatTimeAtOffset(data.sys.sunrise, data.timezone);
- sunset.textContent = formatTimeAtOffset(data.sys.sunset, data.timezone);
-
- updateSunPosition(data);
- updateDynamicBackground(data);
- setWeatherBackground(data.weather[0].main);
-
- // Call interactive map
- initOrUpdateMap(data.coord.lat, data.coord.lon, data.name);
-}
-
-function updateForecastUI(forecastData) {
- if (!forecastContainer) {
+async function requestWeatherFromMyLocation() {
+ if (!navigator.geolocation) {
+ showError(currentLang === 'hi' ? 'आपके ब्राउज़र द्वारा जियोलोकेशन समर्थित नहीं है।' : 'Geolocation is not supported by your browser.');
return;
}
- const hourlyData = forecastData.list.slice(0, 8);
- dailyTrendData = buildDailyTrendData(forecastData.list, forecastData.city?.timezone || 0);
-
- forecastContainer.innerHTML = '';
- const locale = currentLang === 'hi' ? 'hi-IN' : currentLang === 'es' ? 'es-ES' : currentLang === 'fr' ? 'fr-FR' : 'en-US';
-
- hourlyData.forEach((hour) => {
- const date = new Date(hour.dt * 1000);
- const timeString = date.toLocaleTimeString(locale, { hour: 'numeric', hour12: true });
- const iconCode = hour.weather[0].icon;
- const description = hour.weather[0].description;
-
- const card = document.createElement('div');
- card.className = 'forecast-card';
- card.innerHTML = `
- ${timeString}
-
-

-
- ${toUnit(hour.main.temp)}
- ${description}
- `;
-
- forecastContainer.appendChild(card);
- });
-
- updateForecastSummary(hourlyData);
- renderTemperatureChart(hourlyData);
-
- const hoursAhead = 24;
- const hourlyPoints = buildHourlyPoints(forecastData.list, forecastData.city?.timezone || 0, hoursAhead);
- renderHumidityPrecipChart(hourlyPoints);
-
- updateWeatherTrends(dailyTrendData);
+ navigator.geolocation.getCurrentPosition(
+ async (position) => {
+ const { latitude, longitude } = position.coords;
+ await fetchWeatherByCoords(latitude, longitude);
+ },
+ (error) => {
+ console.warn('[Geolocation] Error:', error);
+ showError(getLocationErrorMessage(error));
+ },
+ { enableHighAccuracy: true, timeout: 8000, maximumAge: 0 }
+ );
}
-function updateForecastSummary(chartData) {
- if (!forecastSummary || !graphRange) return;
-
- if (!chartData.length) {
- forecastSummary.textContent = 'Forecast trend is unavailable right now.';
- graphRange.textContent = '--';
- return;
- }
-
- const temperatures = chartData.map((item) => item.main.temp);
- const firstTemp = temperatures[0];
- const lastTemp = temperatures[temperatures.length - 1];
- const minTemp = Math.min(...temperatures);
- const maxTemp = Math.max(...temperatures);
- const delta = lastTemp - firstTemp;
-
- let trend = 'Temperatures are expected to stay fairly steady';
- if(currentLang === 'hi') trend = delta > 1.5 ? 'तापमान बढ़ने की उम्मीद है' : delta < -1.5 ? 'तापमान कम होने की उम्मीद है' : 'तापमान स्थिर रहने की उम्मीद है';
- else if(currentLang === 'es') trend = delta > 1.5 ? 'Se espera que las temperaturas suban' : delta < -1.5 ? 'Se espera que las temperaturas bajen' : 'Temperaturas estables';
- else if(currentLang === 'fr') trend = delta > 1.5 ? 'Les températures devraient augmenter' : delta < -1.5 ? 'Les températures devraient baisser' : 'Températures stables';
- else trend = delta > 1.5 ? 'Temperatures are expected to rise' : delta < -1.5 ? 'Temperatures are expected to cool down' : 'Temperatures are expected to stay fairly steady';
-
- forecastSummary.textContent = `${trend} (${toUnit(minTemp)} - ${toUnit(maxTemp)}).`;
- graphRange.textContent = `${toUnit(minTemp)} - ${toUnit(maxTemp)}`;
-}
+// Wire components together using CustomEvents
+function setupComponentListeners() {
+ // 1. Language change
+ appHeader.addEventListener('lang-change', (e) => {
+ currentLang = e.detail.lang;
+ localStorage.setItem('weatherify-lang', currentLang);
+
+ // Propagate to other components
+ [appHeader, appSearch, mainWeather, dailyForecast, weatherMapComponent].forEach(el => {
+ if (el) el.setAttribute('lang', currentLang);
+ });
-function buildDailyTrendData(forecastList, timezoneOffsetSeconds) {
- const groupedDays = new Map();
- const locale = currentLang === 'hi' ? 'hi-IN' : currentLang === 'es' ? 'es-ES' : currentLang === 'fr' ? 'fr-FR' : 'en-US';
-
- forecastList.forEach((item) => {
- const localDate = getShiftedDate(item.dt, timezoneOffsetSeconds);
- const dateKey = localDate.toISOString().slice(0, 10);
-
- if (!groupedDays.has(dateKey)) {
- groupedDays.set(dateKey, {
- dateKey,
- dayLabel: localDate.toLocaleDateString(locale, { weekday: 'short', timeZone: 'UTC' }),
- dateLabel: localDate.toLocaleDateString(locale, { month: 'short', day: 'numeric', timeZone: 'UTC' }),
- temperatures: []
- });
+ if (currentCityQuery) {
+ fetchWeatherData(currentCityQuery);
+ } else {
+ fetchWeatherData(DEFAULT_CITY);
}
-
- groupedDays.get(dateKey).temperatures.push(item.main.temp);
});
- return Array.from(groupedDays.values()).slice(0, 5).map((day) => {
- const high = Math.max(...day.temperatures);
- const low = Math.min(...day.temperatures);
- const avg = day.temperatures.reduce((total, temp) => total + temp, 0) / day.temperatures.length;
-
- return { ...day, high, low, avg };
+ // 2. Theme change
+ appHeader.addEventListener('theme-change', (e) => {
+ applyTheme(e.detail.theme);
});
-}
-function updateWeatherTrends(trendData) {
- if (!trendsSummary || !trendStats || !trendChart) return;
-
- if (!trendData.length) {
- trendsSummary.textContent = 'Daily trend data is unavailable right now.';
- trendStats.innerHTML = '';
- trendChart.innerHTML = '';
- trendChartRange.textContent = '--';
- return;
- }
-
- const firstAverage = trendData[0].avg;
- const lastAverage = trendData[trendData.length - 1].avg;
- const averageDelta = toUnitNum(lastAverage) - toUnitNum(firstAverage);
-
- let trendText = 'Weather remaining stable over the next few days.';
- if(currentLang==='hi') trendText = averageDelta > 1.5 ? 'आने वाले दिनों में तापमान बढ़ेगा।' : averageDelta < -1.5 ? 'आने वाले दिनों में ठंडक की उम्मीद है।' : 'मौसम स्थिर रहेगा।';
- else if(currentLang==='es') trendText = averageDelta > 1.5 ? 'Temperaturas subiendo en los próximos días.' : averageDelta < -1.5 ? 'Tendencia al enfriamiento.' : 'Clima estable.';
- else if(currentLang==='fr') trendText = averageDelta > 1.5 ? 'Températures en hausse dans les jours à venir.' : averageDelta < -1.5 ? 'Tendance au refroidissement.' : 'Météo stable.';
- else trendText = averageDelta > 1.5 ? 'Temperatures rising over the next few days.' : averageDelta < -1.5 ? 'Cooling trend expected over the next few days.' : 'Weather remaining stable over the next few days.';
-
- trendsSummary.textContent = trendText;
- renderTrendStats(trendData);
- renderTrendChart(trendData);
-}
-
-function renderTrendStats(trendData) {
- if (!trendStats) return;
- const t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : window.translations['en'];
-
- trendStats.innerHTML = trendData.map((day) => `
-
-
- ${day.dayLabel}
- ${day.dateLabel}
-
-
- ${toUnitNum(day.high)}${unitLabel()} ${t['high'] ? t['high'].toLowerCase() : 'high'}
- ${toUnitNum(day.low)}${unitLabel()} ${t['low'] ? t['low'].toLowerCase() : 'low'}
- ${toUnitNum(day.avg)}${unitLabel()} ${t['avg'] ? t['avg'].toLowerCase() : 'avg'}
-
-
- `).join('');
-}
-
-function updateTrendToggleState() {
- if (!trendControls) return;
-
- trendControls.querySelectorAll('[data-trend-metric]').forEach((button) => {
- button.classList.toggle('active', button.dataset.trendMetric === selectedTrendMetric);
+ // 3. Unit change
+ appHeader.addEventListener('unit-change', (e) => {
+ setCurrentUnit(e.detail.unit);
});
-}
-
-function renderTrendChart(trendData) {
- if (!trendChart) return;
-
- if (!trendData.length) {
- trendChart.innerHTML = '';
- return;
- }
-
- const t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : window.translations['en'];
- const metricLabels = {
- high: t['high'] || 'Daily High Temperature',
- low: t['low'] || 'Daily Low Temperature',
- avg: t['avgTemp'] || 'Daily Average Temperature'
- };
- const metricColors = {
- high: '#f97316',
- low: '#0ea5e9',
- avg: '#667eea'
- };
- const metric = selectedTrendMetric in metricLabels ? selectedTrendMetric : 'avg';
- const width = 760;
- const height = 280;
- 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) => 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);
- const barWidth = Math.min(58, innerWidth / trendData.length * 0.45);
-
- 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 val = toUnitNum(day[metric]);
- return { ...day, x, y: getY(val), value: val };
- });
-
- const baselineY = height - padding.bottom;
- const bars = points.map((point) => {
- const barHeight = Math.max(baselineY - point.y, 3);
- return `
-
-
-
-
-
-
- `;
- }).join('');
- const linePoints = points.map((point) => `${point.x},${point.y}`).join(' ');
- const gridLines = [0, 0.5, 1].map((step) => {
- const y = padding.top + innerHeight * step;
- const value = Math.round(maxValue - range * step);
- return `
-
- ${value}${unitLabel()}
- `;
- }).join('');
- const labels = points.map((point) => `
-
-
- ${Math.round(point.value)}${unitLabel()}
- ${point.dayLabel}
-
- `).join('');
-
- trendChart.style.setProperty('--trend-color', metricColors[metric]);
- trendChart.innerHTML = `
- ${gridLines}
- ${bars}
-
- ${labels}
- `;
-
- if (trendChartLabel) trendChartLabel.textContent = metricLabels[metric];
- if (trendChartRange) trendChartRange.textContent = `${Math.round(Math.min(...values))}${unitLabel()} - ${Math.round(Math.max(...values))}${unitLabel()}`;
-}
-
-function updateSunPosition(data) {
- if (!sunMarker || !sunPhase || !sunProgress || !solarNoon) return;
-
- sunTimeline = {
- timezone: data.timezone,
- sunrise: data.sys.sunrise,
- sunset: data.sys.sunset
- };
-
- renderSunPosition();
-}
-
-function updateDynamicBackground(data) {
- const body = document.body;
- const weatherType = (data.weather?.[0]?.main || '').toLowerCase();
- const isNight = data.weather?.[0]?.icon?.includes('n');
- const themeClasses = [
- 'theme-clear-day', 'theme-clear-night', 'theme-clouds', 'theme-rain',
- 'theme-drizzle', 'theme-thunderstorm', 'theme-snow', 'theme-mist',
- 'theme-fog', 'theme-haze'
- ];
- body.classList.remove(...themeClasses);
-
- if (weatherType === 'clear') body.classList.add(isNight ? 'theme-clear-night' : 'theme-clear-day');
- else if (weatherType === 'clouds') body.classList.add('theme-clouds');
- else if (weatherType === 'rain') body.classList.add('theme-rain');
- else if (weatherType === 'drizzle') body.classList.add('theme-drizzle');
- else if (weatherType === 'thunderstorm') body.classList.add('theme-thunderstorm');
- else if (weatherType === 'snow') body.classList.add('theme-snow');
- else if (['mist', 'fog', 'haze', 'smoke'].includes(weatherType)) body.classList.add('theme-mist');
- else body.classList.add(isNight ? 'theme-clear-night' : 'theme-clear-day');
-}
-
-function renderSunPosition() {
- if (!sunTimeline || !sunMarker || !sunPhase || !sunProgress || !solarNoon) return;
- const t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : window.translations['en'];
-
- const nowSeconds = Math.floor(Date.now() / 1000);
- const { timezone, sunrise, sunset } = sunTimeline;
- const daylight = Math.max(sunset - sunrise, 1);
- const midpoint = sunrise + Math.floor(daylight / 2);
- let progress = 0;
- let phaseText = '';
- let progressText = '';
-
- if (nowSeconds <= sunrise) {
- phaseText = currentLang==='hi'?'सूर्योदय से पहले':'Before sunrise';
- progressText = currentLang==='hi'?`सूर्योदय में ${formatDuration(sunrise - nowSeconds)}`:`${formatDuration(sunrise - nowSeconds)} until sunrise`;
- progress = 0;
- } else if (nowSeconds >= sunset) {
- phaseText = currentLang==='hi'?'सूर्यास्त के बाद':'After sunset';
- progressText = currentLang==='hi'?`सूर्योदय में ${formatDuration(getNextSunriseSeconds(sunrise, nowSeconds) - nowSeconds)}`:`${formatDuration(getNextSunriseSeconds(sunrise, nowSeconds) - nowSeconds)} until sunrise`;
- progress = 100;
- } else {
- progress = ((nowSeconds - sunrise) / daylight) * 100;
- phaseText = t['daylight'] || 'Daylight';
- progressText = currentLang==='hi'?`दिन का ${Math.round(progress)}% हिस्सा पूरा हुआ`:`${Math.round(progress)}% of daylight completed`;
- }
-
- sunMarker.style.left = `${Math.min(Math.max(progress, 0), 100)}%`;
- sunPhase.textContent = phaseText;
- sunProgress.textContent = progressText;
-
- const midpointLabel = currentLang === 'hi' ? 'सौर मध्यबिंदु' : currentLang === 'es' ? 'Mediodía solar' : currentLang === 'fr' ? 'Midi solaire' : 'Solar midpoint';
- solarNoon.textContent = `${midpointLabel} ${formatTimeAtOffset(midpoint, timezone)}`;
-}
-
-function getShiftedDate(unixSeconds, timezoneOffsetSeconds) {
- return new Date((unixSeconds + timezoneOffsetSeconds) * 1000);
-}
-
-function formatDateAtOffset(unixSeconds, timezoneOffsetSeconds) {
- const locale = currentLang === 'hi' ? 'hi-IN' : currentLang === 'es' ? 'es-ES' : currentLang === 'fr' ? 'fr-FR' : 'en-US';
- return getShiftedDate(unixSeconds, timezoneOffsetSeconds).toLocaleDateString(locale, {
- weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC'
+ // 4. City selection from search suggestion or history
+ appSearch.addEventListener('city-select', (e) => {
+ fetchWeatherData(e.detail.city);
});
-}
-function formatTimeAtOffset(unixSeconds, timezoneOffsetSeconds) {
- const locale = currentLang === 'hi' ? 'hi-IN' : currentLang === 'es' ? 'es-ES' : currentLang === 'fr' ? 'fr-FR' : 'en-US';
- return getShiftedDate(unixSeconds, timezoneOffsetSeconds).toLocaleTimeString(locale, {
- hour: '2-digit', minute: '2-digit', timeZone: 'UTC'
+ // 5. Use My Location button clicked
+ appSearch.addEventListener('location-request', () => {
+ requestWeatherFromMyLocation();
});
-}
-function getNextSunriseSeconds(todaySunrise, nowSeconds) {
- const daySeconds = 24 * 60 * 60;
- const daysAhead = Math.floor((nowSeconds - todaySunrise) / daySeconds) + 1;
- return todaySunrise + daysAhead * daySeconds;
-}
-
-function formatDuration(seconds) {
- const totalMinutes = Math.max(0, Math.round(seconds / 60));
- const hours = Math.floor(totalMinutes / 60);
- const minutes = totalMinutes % 60;
- const hStr = currentLang==='hi'?'घं':'h';
- const mStr = currentLang==='hi'?'मि':'m';
-
- if (hours === 0) return `${minutes}${mStr}`;
- if (minutes === 0) return `${hours}${hStr}`;
- return `${hours}${hStr} ${minutes}${mStr}`;
-}
-
-function renderTemperatureChart(hourlyData) {
- if (!temperatureChartCanvas) return;
-
- if (!hourlyData.length) {
- if (window.temperatureChart) window.temperatureChart.destroy();
- return;
- }
-
- const locale = currentLang === 'hi' ? 'hi-IN' : currentLang === 'es' ? 'es-ES' : currentLang === 'fr' ? 'fr-FR' : 'en-US';
- const labels = hourlyData.map(item => {
- const date = new Date(item.dt * 1000);
- return date.toLocaleTimeString(locale, { hour: 'numeric', hour12: true });
+ // 6. Clear history clicked
+ appSearch.addEventListener('clear-history', () => {
+ clearRecentHistory();
});
- const temperatures = hourlyData.map(item => toUnitNum(item.main.temp));
-
- const data = {
- labels: labels,
- datasets: [{
- label: `Temperature (${unitLabel()})`,
- data: temperatures,
- borderColor: 'rgba(75, 192, 192, 1)',
- backgroundColor: 'rgba(75, 192, 192, 0.2)',
- fill: true,
- tension: 0.4
- }]
- };
-
- const config = {
- type: 'line',
- data: data,
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- scales: {
- y: {
- beginAtZero: false,
- ticks: { callback: function(value) { return value + unitLabel(); } }
- }
- }
- }
- };
-
- if (window.temperatureChart) window.temperatureChart.destroy();
- window.temperatureChart = new Chart(temperatureChartCanvas, config);
-}
-
-function buildHourlyPoints(forecastList, timezoneOffsetSeconds, hoursAhead) {
- if (!Array.isArray(forecastList) || forecastList.length === 0) return [];
-
- const nowSeconds = Math.floor(Date.now() / 1000);
- const endSeconds = nowSeconds + hoursAhead * 60 * 60;
- const locale = currentLang === 'hi' ? 'hi-IN' : currentLang === 'es' ? 'es-ES' : currentLang === 'fr' ? 'fr-FR' : 'en-US';
-
- const points = forecastList
- .slice()
- .sort((a, b) => a.dt - b.dt)
- .filter((item) => item?.dt >= nowSeconds - 3600 && item?.dt <= endSeconds);
-
- return points.slice(0, 10).map((item) => {
- const localDate = getShiftedDate(item.dt, timezoneOffsetSeconds);
- return {
- raw: item,
- dt: item.dt,
- timeLabel: localDate.toLocaleTimeString(locale, { hour: 'numeric' }),
- humidity: item.main?.humidity ?? null,
- precipProb: item.pop ?? null,
- precipAmount: item.rain?.['3h'] ?? null
- };
+ // 7. Favorite toggle inside weather detail card
+ mainWeather.addEventListener('toggle-favorite', (e) => {
+ toggleFavoriteCity({ query: e.detail.query, label: e.detail.label });
});
}
-function renderHumidityPrecipChart(hourlyPoints) {
- if (!humidityPrecipChartCanvas || !humidityPrecipRange) return;
-
- if (!hourlyPoints || hourlyPoints.length === 0) {
- if (window.humidityPrecipChart) window.humidityPrecipChart.destroy();
- humidityPrecipRange.textContent = '--';
- return;
+// Initial startup
+window.addEventListener('DOMContentLoaded', () => {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.register('/sw.js').catch((err) => {
+ console.error('Service Worker registration failed:', err);
+ });
}
- const t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : window.translations['en'];
- const metric = selectedHourlyMetric;
- const labels = hourlyPoints.map(p => p.timeLabel);
- const values = hourlyPoints.map(p => {
- if (metric === 'humidity') return p.humidity || 0;
- return (p.precipProb || 0) * 100;
- });
-
- const data = {
- labels: labels,
- datasets: [{
- label: metric === 'humidity' ? `${t ? t['humidity'] : 'Humidity'} (%)` : `${t ? t['precip'] : 'Precipitation'} (%)`,
- data: values,
- borderColor: metric === 'humidity' ? 'rgba(54, 162, 235, 1)' : 'rgba(255, 99, 132, 1)',
- backgroundColor: metric === 'humidity' ? 'rgba(54, 162, 235, 0.2)' : 'rgba(255, 99, 132, 0.2)',
- fill: true,
- tension: 0.4
- }]
- };
-
- const config = {
- type: 'line',
- data: data,
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: { legend: { display: false } },
- scales: {
- y: {
- beginAtZero: true,
- max: 100,
- ticks: { callback: function(value) { return value + '%'; } }
- }
- }
- }
- };
-
- if (window.humidityPrecipChart) window.humidityPrecipChart.destroy();
- window.humidityPrecipChart = new Chart(humidityPrecipChartCanvas, config);
-
- const minV = Math.min(...values);
- const maxV = Math.max(...values);
- humidityPrecipRange.textContent = `${minV}% - ${maxV}%`;
-}
-
-function getWindDirection(deg) {
- if (deg === undefined || deg === null) return '';
- const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
- const index = Math.round(deg / 45) % 8;
- const arrows = ['↑', '↗', '→', '↘', '↓', '↙', '←', '↖'];
- return `${directions[index]} ${arrows[index]}`;
-}
-
-function showLoading() { spinnerLoading.classList.remove("hidden"); }
-function hideLoading() { spinnerLoading.classList.add("hidden"); }
-function showWeather() { weatherContainer.classList.remove('hidden'); }
-function hideWeather() { weatherContainer.classList.add('hidden'); }
-function showError(message) {
- if (message) errorMessage.querySelector('p').textContent = message;
- errorMessage.classList.remove('hidden');
-}
-function hideError() { errorMessage.classList.add('hidden'); }
-
-setInterval(renderSunPosition, 60000);
-
-// ============================================================
-// ✅ Dark Mode Toggle — Issue #14
-// ============================================================
-(function initDarkMode() {
- const STORAGE_KEY = 'weatherify-theme';
- const DARK_CLASS = 'dark-mode';
+ // Resolve component elements
+ appHeader = document.getElementById('app-header');
+ appSearch = document.getElementById('app-search');
+ mainWeather = document.getElementById('main-weather');
+ dailyForecast = document.getElementById('daily-forecast');
+ weatherMapComponent = document.getElementById('weather-map-component');
+ weatherContainer = document.getElementById('weather-container');
+ spinnerLoading = document.getElementById('spinner-loading');
- const toggleBtn = document.getElementById('theme-toggle');
- const icon = toggleBtn ? toggleBtn.querySelector('.toggle-icon') : null;
- const label = toggleBtn ? toggleBtn.querySelector('.toggle-label') : null;
+ // Initialize themes, history, units
+ applyTheme(currentTheme);
+ initHistory();
+ initUnitDisplay();
- function applyTheme(isDark) {
- document.documentElement.classList.toggle(DARK_CLASS, isDark);
- if (document.body) document.body.classList.toggle(DARK_CLASS, isDark);
- if (icon) icon.textContent = isDark ? '☀️' : '🌙';
-
- const t = window.translations && window.translations[currentLang] ? window.translations[currentLang] : window.translations['en'];
- if (label) label.textContent = isDark ? (t ? t['themeLight'] : 'Light') : (t ? t['themeDark'] : 'Dark');
-
- if (toggleBtn) {
- toggleBtn.setAttribute('aria-pressed', String(isDark));
- toggleBtn.setAttribute('aria-label', isDark ? 'Switch to light theme' : 'Switch to dark theme');
- toggleBtn.title = isDark ? 'Switch to light theme' : 'Switch to dark theme';
- }
- }
+ // Propagate initial states to components
+ [appHeader, appSearch, mainWeather, dailyForecast, weatherMapComponent].forEach(el => {
+ if (el) el.setAttribute('lang', currentLang);
+ });
+ if (appHeader) appHeader.setAttribute('theme', currentTheme);
+ if (appHeader) appHeader.setAttribute('unit', currentUnit);
+ if (mainWeather) mainWeather.setAttribute('unit', currentUnit);
+ if (dailyForecast) dailyForecast.setAttribute('unit', currentUnit);
- function getInitialPreference() {
- const saved = localStorage.getItem(STORAGE_KEY);
- if (saved !== null) return saved === 'dark';
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
- }
+ // Setup component observers and listeners
+ setupComponentListeners();
- applyTheme(getInitialPreference());
+ // Load initial weather
+ const params = new URLSearchParams(window.location.search);
+ const sharedUnits = params.get('units')?.toUpperCase();
+ const sharedCity = params.get('city');
- if (toggleBtn) {
- toggleBtn.addEventListener('click', () => {
- const isDark = !document.body.classList.contains(DARK_CLASS);
- applyTheme(isDark);
- localStorage.setItem(STORAGE_KEY, isDark ? 'dark' : 'light');
- });
+ if (sharedUnits) {
+ setCurrentUnit(sharedUnits);
}
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
- if (localStorage.getItem(STORAGE_KEY) === null) {
- applyTheme(e.matches);
- }
- });
-})();
-
-// ============================================================
-// 🗺️ INTERACTIVE WEATHER MAP INTEGRATION (Leaflet)
-// ============================================================
-let mapInstance = null;
-let mapMarker = null;
-let activeWeatherLayer = null;
-
-function initOrUpdateMap(lat, lon, cityName) {
- const mapElement = document.getElementById('weather-map');
- if (!mapElement) return;
-
- if (!mapInstance) {
- mapInstance = L.map('weather-map').setView([lat, lon], 10);
- L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
- attribution: '© OpenStreetMap contributors'
- }).addTo(mapInstance);
- mapMarker = L.marker([lat, lon]).addTo(mapInstance);
- setupMapLayerControls();
+ if (sharedCity) {
+ fetchWeatherData(sharedCity);
} else {
- mapInstance.setView([lat, lon], 10);
- mapMarker.setLatLng([lat, lon]);
+ fetchWeatherData(DEFAULT_CITY);
}
- mapMarker.bindPopup(`${cityName}`).openPopup();
-}
-
-function setupMapLayerControls() {
- const mapControls = document.querySelectorAll('.map-layer-controls .trend-toggle');
- if (!mapControls.length) return;
-
- mapControls.forEach(btn => {
- btn.addEventListener('click', (e) => {
- mapControls.forEach(b => b.classList.remove('active'));
- e.target.classList.add('active');
-
- const layerType = e.target.dataset.layer;
-
- if (activeWeatherLayer) {
- mapInstance.removeLayer(activeWeatherLayer);
- activeWeatherLayer = null;
- }
-
- let layerUrl = '';
- if (layerType === 'precipitation') layerUrl = 'https://tile.openweathermap.org/map/precipitation_new/{z}/{x}/{y}.png?appid=MOCK_KEY';
- else if (layerType === 'clouds') layerUrl = 'https://tile.openweathermap.org/map/clouds_new/{z}/{x}/{y}.png?appid=MOCK_KEY';
-
- if (layerUrl && layerType !== 'base') {
- /* activeWeatherLayer = L.tileLayer(layerUrl, { opacity: 0.6 }).addTo(mapInstance); */
- }
- });
- });
-}
+});
diff --git a/public/style.css b/public/style.css
index 4cb20eb..c8afb1f 100644
--- a/public/style.css
+++ b/public/style.css
@@ -1,5 +1,9 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
+:root {
+ --card-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
+}
+
* {
box-sizing: border-box;
margin: 0;
@@ -67,6 +71,10 @@ body.theme-thunderstorm { --bg-start: #1f2745; --bg-mid: #40527d; --bg-end: #7f8
body.theme-snow { --bg-start: #bbd3e8; --bg-mid: #eef7ff; --bg-end: #d6e5f2; }
body.theme-mist, body.theme-fog, body.theme-haze { --bg-start: #9aa7b5; --bg-mid: #dde5eb; --bg-end: #c8d0d8; }
+body.dark-mode {
+ color: #e2e8f0;
+}
+
.container {
width: 100%;
max-width: 90rem;
@@ -92,262 +100,15 @@ body.theme-mist, body.theme-fog, body.theme-haze { --bg-start: #9aa7b5; --bg-mid
isolation: isolate;
}
-.weather-app, .container, #weather-container {
- overflow-x: hidden;
-}
-
-/* ===== HEADER ===== */
-header {
- text-align: left;
- margin-bottom: 28px;
- animation: fadeIn 0.8s ease-out;
- display: grid;
- grid-template-columns: minmax(0, 1fr) auto auto;
- grid-template-areas:
- "title theme units"
- "search search search";
- column-gap: 14px;
- row-gap: 18px;
- align-items: center;
-}
-
-header h1 { grid-area: title; }
-.search-container { grid-area: search; display: flex; flex-direction: column; width: 100%; position: relative; }
-.theme-toggle { grid-area: theme; }
-.unit-toggle { grid-area: units; }
-
-h1 {
- font-size: 36px;
- font-weight: 700;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- -webkit-background-clip: text;
- -webkit-text-fill-color: transparent;
- background-clip: text;
- margin-bottom: 0;
- letter-spacing: -0.5px;
- line-height: 1.1;
-}
-
-.search-box {
- display: flex;
- gap: 12px;
- width: 100%;
- position: relative;
- flex-shrink: 0;
-}
-
-.search-input-wrap {
- flex: 1;
- position: relative;
- min-width: 0;
-}
-
-#city-input {
- width: 100%;
- box-sizing: border-box;
- padding: 14px 40px 14px 20px;
- border: 2px solid #e2e8f0;
- border-radius: 14px;
- font-size: 16px;
- font-family: inherit;
- outline: none;
- transition: all 0.3s ease;
- background: rgba(255, 255, 255, 0.8);
-}
-
-#city-input:focus {
- border-color: #667eea;
- box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.15);
- background: white;
- transform: translateY(-2px);
-}
-
-#city-input::placeholder { color: #cbd5e0; }
-
-.clear-btn {
- position: absolute;
- top: 50%;
- right: 10px;
- transform: translateY(-50%);
- width: 32px;
- height: 32px;
- padding: 0;
- border: none;
- border-radius: 10px;
- background: transparent;
- color: #64748b;
- font-size: 22px;
- line-height: 1;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: color 0.2s ease, background 0.2s ease;
-}
-
-.clear-btn:hover { color: #475569; background: rgba(100, 116, 139, 0.12); }
-.clear-btn:focus-visible { outline: 2px solid #667eea; outline-offset: 2px; }
-
-#search-btn {
- width: 3.5rem;
- min-width: 3.5rem;
- height: 3.5rem;
- flex-shrink: 0;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- border: none;
- border-radius: 0.875rem;
- cursor: pointer;
- color: white;
- transition: transform 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease;
- display: flex;
- align-items: center;
- justify-content: center;
- font-weight: 600;
- box-shadow: 0 0.25rem 0.9375rem rgba(102, 126, 234, 0.3);
-}
-
-#search-btn:hover { transform: translateY(-3px); box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5); }
-#search-btn:active { transform: translateY(-1px); }
-
-.suggestions-container {
- position: absolute;
- top: calc(100% + 12px);
- left: 0;
- right: 0;
- background: white;
- border-radius: 14px;
- box-shadow: 0 15px 50px rgba(0, 0, 0, 0.2);
- max-height: 300px;
- overflow-y: auto;
- z-index: 1000;
- animation: slideDown 0.3s ease-out;
- 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;
-}
-
-.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;
+body.dark-mode .weather-app {
+ background: rgba(15, 23, 42, 0.92);
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.06);
}
-.favorite-btn:hover { transform: scale(1.1); }
-
-.suggestion-item {
- padding: 14px 20px;
- cursor: pointer;
- border-bottom: 1px solid #f0f0f0;
- transition: all 0.2s ease;
- font-size: 15px;
- color: #2d3748;
+.weather-app, .container, #weather-container {
+ overflow-x: hidden;
}
-.suggestion-item:last-child { border-bottom: none; }
-.suggestion-item:hover { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding-left: 24px; }
-.suggestion-item:first-child { border-radius: 14px 14px 0 0; }
-.suggestion-item:last-child { border-radius: 0 0 14px 14px; }
-
-/* ===== MAIN WEATHER SECTION ===== */
#weather-container {
display: flex;
flex-direction: column;
@@ -355,436 +116,10 @@ h1 {
animation: fadeIn 0.8s ease-out 0.2s both;
}
-/* ===== WEATHER MAIN: two columns — temp left, sun right ===== */
-.weather-main {
- display: grid;
- grid-template-columns: 1fr 1fr;
- grid-template-rows: auto 1fr;
- gap: 1.25rem;
- width: 100%;
- min-width: 0;
- align-items: stretch;
-}
-
-/* City/date info spans full width on top */
-.location-info {
- grid-column: 1 / -1;
- grid-row: 1;
- animation: slideInLeft 0.6s ease-out;
- width: 100%;
- text-align: left;
-}
-
-/* Temp card in left column */
-.temperature-section {
- grid-column: 1;
- grid-row: 2;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 28px 26px;
- background: linear-gradient(145deg, #f0f4ff 0%, #e8eeff 40%, #f5f0ff 100%);
- border-radius: 24px;
- animation: slideInLeft 0.6s ease-out 0.2s both;
- border: 1px solid rgba(102, 126, 234, 0.15);
- position: relative;
- transition: background 0.4s ease, border-color 0.4s ease;
- min-height: 220px;
-}
-
-/* Sun card in right column, same height as temp */
-.sun-position-card {
- grid-column: 2;
- grid-row: 2;
- background: linear-gradient(135deg, #fff7dd 0%, #fff2c5 100%);
- border: 2px solid rgba(217, 119, 6, 0.2);
- border-radius: 20px;
- padding: 20px 24px;
- transition: all 0.3s ease;
- box-shadow: 0 10px 30px rgba(217, 119, 6, 0.1);
- animation: slideInLeft 0.6s ease-out 0.1s both;
- display: flex;
- flex-direction: column;
- justify-content: center;
-}
-
-.sun-position-card:hover {
- transform: translateY(-3px);
- box-shadow: 0 15px 40px rgba(217, 119, 6, 0.15);
-}
-
-#city-name {
- font-size: 26px;
- font-weight: 700;
- color: #2d3748;
- margin-bottom: 3px;
- letter-spacing: -0.5px;
- transition: color 0.4s ease;
-}
-
-#date {
- font-size: 13px;
- color: #718096;
- font-weight: 500;
- transition: color 0.4s ease;
-}
-
-.weather-icon {
- width: 100px;
- height: 100px;
- animation: bounce 2s ease-in-out infinite;
-}
-
-.weather-icon img {
- width: 100%;
- height: 100%;
- object-fit: contain;
- filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.1));
-}
-
-.temperature {
- display: flex;
- align-items: flex-start;
- gap: 5px;
-}
-
-#temp {
- font-size: 64px;
- font-weight: 300;
- line-height: 1;
- color: #2d3748;
- letter-spacing: -2px;
- transition: color 0.4s ease;
-}
-
-.unit {
- font-size: 24px;
- font-weight: 400;
- color: #718096;
- margin-top: 8px;
- transition: color 0.4s ease;
-}
-
-#weather-desc {
- font-size: 16px;
- color: #718096;
- text-transform: capitalize;
- font-weight: 500;
- transition: color 0.4s ease;
-}
-
-/* ===== SUN POSITION INTERNALS ===== */
-.sun-position-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-bottom: 12px;
-}
-
-.sun-position-header h3 {
- font-size: 14px;
- font-weight: 700;
- color: #7c4a03;
- text-transform: uppercase;
- letter-spacing: 1px;
-}
-
-#sun-phase {
- font-size: 12px;
- font-weight: 700;
- letter-spacing: 0.1em;
- text-transform: uppercase;
- color: #b45309;
- background: rgba(180, 83, 9, 0.1);
- padding: 4px 10px;
- border-radius: 6px;
-}
-
-.sun-track {
- position: relative;
- height: 50px;
- margin-bottom: 15px;
-}
-
-.sun-track-line {
- position: absolute;
- left: 0;
- right: 0;
- top: 50%;
- transform: translateY(-50%);
- height: 6px;
- border-radius: 999px;
- background: linear-gradient(90deg, #f59e0b 0%, #facc15 50%, #f97316 100%);
- opacity: 0.4;
-}
-
-.sun-track-marker {
- position: absolute;
- left: 0;
- top: 50%;
- transform: translate(-50%, -50%);
- width: 40px;
- height: 40px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: radial-gradient(circle at 30% 30%, #fff9c4 0%, #fbbf24 55%, #f59e0b 100%);
- box-shadow: 0 8px 20px rgba(245, 158, 11, 0.4), inset -2px -2px 5px rgba(0, 0, 0, 0.1);
- animation: float 3s ease-in-out infinite;
-}
-
-.sun-icon { font-size: 20px; line-height: 1; }
-
-.sun-position-meta {
- display: flex;
- justify-content: space-between;
- gap: 15px;
- font-size: 12px;
- color: #8a5a13;
- font-weight: 500;
-}
-
-/* ===== WEATHER DETAILS GRID ===== */
-.weather-details {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
- gap: 1rem;
- animation: slideInLeft 0.6s ease-out 0.3s both;
+#weather-container.hidden {
+ display: none !important;
}
-.detail-card {
- background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
- border-radius: 16px;
- padding: 18px 16px;
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 10px;
- transition: all 0.3s ease;
- border: 1px solid rgba(102, 126, 234, 0.12);
- cursor: pointer;
- position: relative;
- overflow: hidden;
-}
-
-.detail-card::before {
- content: '';
- position: absolute;
- inset: 0;
- background: linear-gradient(135deg, transparent 0%, rgba(102, 126, 234, 0.1) 100%);
- opacity: 0;
- transition: opacity 0.3s ease;
-}
-
-.detail-card:hover { transform: translateY(-5px); box-shadow: 0 12px 35px rgba(0, 0, 0, 0.1); border-color: #667eea; }
-.detail-card:hover::before { opacity: 1; }
-
-.detail-icon { color: #667eea; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; }
-.detail-info { display: flex; flex-direction: column; align-items: center; gap: 4px; 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 {
- font-size: 11px;
- line-height: 1.5;
- color: #475569;
- margin-top: 6px;
- text-align: center;
- max-width: 160px;
- overflow: hidden;
- display: -webkit-box;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
- line-clamp: 2;
-}
-
-.detail-label { font-size: 11px; color: #718096; text-transform: uppercase; letter-spacing: 0.8px; font-weight: 600; transition: color 0.4s ease; }
-.detail-value { font-size: 16px; font-weight: 700; color: #2d3748; transition: color 0.4s ease; }
-
-/* ===== FORECAST SECTION ===== */
-.forecast-section {
- animation: slideInRight 0.6s ease-out 0.2s both;
- display: flex;
- flex-direction: column;
- gap: 18px;
-}
-
-.hourly-toggle-row { display: flex; gap: 8px; margin: 0 0 12px 0; flex-wrap: wrap; }
-
-.hourly-toggle {
- border: none;
- border-radius: 12px;
- padding: 9px 14px;
- background: #edf2f7;
- color: #718096;
- cursor: pointer;
- font: inherit;
- font-size: 13px;
- font-weight: 700;
- transition: all 0.2s ease;
-}
-
-.hourly-toggle:hover { color: #4c51bf; background: rgba(255, 255, 255, 0.72); }
-.hourly-toggle.active { color: white; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 6px 18px rgba(102, 126, 234, 0.25); }
-
-.forecast-title { font-size: 24px; font-weight: 700; color: #2d3748; letter-spacing: -0.5px; transition: color 0.4s ease; }
-.forecast-subtitle { font-size: 13px; line-height: 1.6; color: #718096; transition: color 0.4s ease; }
-
-.forecast-graph-card {
- background: linear-gradient(180deg, #eef4ff 0%, #f8fbff 100%);
- border: 2px solid rgba(102, 126, 234, 0.15);
- border-radius: 20px;
- padding: 22px;
- transition: all 0.3s ease;
-}
-
-.forecast-graph-card:hover { border-color: #667eea; box-shadow: 0 15px 40px rgba(102, 126, 234, 0.1); }
-
-.graph-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 12px;
- margin-bottom: 15px;
- font-size: 12px;
- font-weight: 700;
- text-transform: uppercase;
- letter-spacing: 0.1em;
- color: #667eea;
-}
-
-.graph-wrapper { width: 100%; height: 240px; }
-#temperature-chart { width: 100%; height: 100%; }
-
-.graph-grid-line { stroke: rgba(102, 126, 234, 0.15); stroke-width: 1; stroke-dasharray: 5 5; }
-.graph-area { fill: rgba(102, 126, 234, 0.12); animation: fillArea 0.8s ease-out forwards; }
-.graph-line { fill: none; stroke: #667eea; stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; animation: drawLine 1s ease-out forwards; stroke-dasharray: 1000; stroke-dashoffset: 1000; }
-.graph-point { fill: #ffffff; stroke: #667eea; stroke-width: 3; animation: popIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; opacity: 0; }
-.graph-point-label { fill: #2d3748; font-size: 12px; font-weight: 700; }
-.graph-axis-label { fill: #718096; font-size: 11px; font-weight: 600; }
-
-.forecast-container {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
- gap: 14px;
-}
-
-.forecast-card {
- background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
- border-radius: 16px;
- padding: 18px 14px;
- text-align: center;
- transition: all 0.3s ease;
- border: 1px solid rgba(102, 126, 234, 0.12);
- cursor: pointer;
- position: relative;
- overflow: hidden;
-}
-
-.forecast-card::before {
- content: '';
- position: absolute;
- inset: 0;
- background: linear-gradient(135deg, transparent 0%, rgba(102, 126, 234, 0.1) 100%);
- opacity: 0;
- transition: opacity 0.3s ease;
-}
-
-.forecast-card:hover { transform: translateY(-8px); box-shadow: 0 12px 35px rgba(0, 0, 0, 0.1); border-color: #667eea; }
-.forecast-card:hover::before { opacity: 1; }
-.forecast-day { font-size: 13px; font-weight: 700; color: #667eea; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px; }
-.forecast-icon { width: 45px; height: 45px; margin: 0 auto 10px; transition: transform 0.3s ease; }
-.forecast-card:hover .forecast-icon { transform: scale(1.1) rotate(5deg); }
-.forecast-icon img { width: 100%; height: 100%; object-fit: contain; }
-.forecast-temp { font-size: 16px; font-weight: 700; color: #2d3748; margin-bottom: 4px; transition: color 0.4s ease; }
-.forecast-desc { font-size: 12px; color: #718096; text-transform: capitalize; transition: color 0.4s ease; }
-
-/* ===== WEATHER TRENDS ===== */
-.weather-trends-section { display: flex; flex-direction: column; gap: 20px; animation: slideUp 0.6s ease-out 0.25s both; }
-.trends-header { display: flex; justify-content: space-between; gap: 20px; align-items: center; }
-
-.trend-controls {
- display: inline-flex;
- gap: 6px;
- padding: 5px;
- background: #edf2f7;
- border: 1px solid rgba(226, 232, 240, 0.9);
- border-radius: 14px;
- flex-shrink: 0;
-}
-
-.trend-toggle { border: none; border-radius: 10px; padding: 9px 14px; background: transparent; color: #718096; cursor: pointer; font: inherit; font-size: 13px; font-weight: 700; transition: all 0.2s ease; }
-.trend-toggle:hover { color: #4c51bf; background: rgba(255, 255, 255, 0.72); }
-.trend-toggle.active { color: white; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 6px 18px rgba(102, 126, 234, 0.25); }
-
-.trends-layout { display: grid; grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.8fr); gap: 18px; align-items: stretch; }
-
-.trend-chart-card {
- background: linear-gradient(180deg, #f8fbff 0%, #eef4ff 100%);
- border: 2px solid rgba(102, 126, 234, 0.15);
- border-radius: 20px;
- padding: 22px;
- transition: all 0.3s ease;
-}
-
-.trend-chart-card:hover { border-color: #667eea; box-shadow: 0 15px 40px rgba(102, 126, 234, 0.1); }
-.trend-chart-wrapper { height: 280px; width: 100%; }
-#trend-chart { --trend-color: #667eea; width: 100%; height: 100%; overflow: visible; }
-
-.trend-bar { fill: rgba(102, 126, 234, 0.18); stroke: rgba(102, 126, 234, 0.45); stroke-width: 1; transition: all 0.25s ease; }
-.trend-bar-group:hover .trend-bar { fill: rgba(102, 126, 234, 0.28); }
-.trend-range-line { stroke: rgba(45, 55, 72, 0.22); stroke-width: 3; stroke-linecap: round; }
-.trend-high-dot { fill: #f97316; stroke: white; stroke-width: 2; opacity: 0.9; }
-.trend-low-dot { fill: #0ea5e9; stroke: white; stroke-width: 2; opacity: 0.9; }
-.trend-line { fill: none; stroke: var(--trend-color); stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; animation: drawLine 1s ease-out forwards; stroke-dasharray: 1000; stroke-dashoffset: 1000; }
-.trend-line-point { fill: white; stroke: var(--trend-color); stroke-width: 3; animation: popIn 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
-.trend-value-label { paint-order: stroke; stroke: #f8fbff; stroke-width: 5px; stroke-linejoin: round; }
-
-.trend-stats { display: grid; grid-template-columns: 1fr; gap: 12px; }
-
-.trend-stat-card {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
- padding: 16px;
- border-radius: 16px;
- background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
- border: 1px solid rgba(102, 126, 234, 0.12);
- transition: all 0.25s ease;
-}
-
-.trend-stat-card:hover { transform: translateY(-3px); border-color: #667eea; box-shadow: 0 10px 28px rgba(0, 0, 0, 0.08); }
-.trend-stat-date { display: flex; flex-direction: column; gap: 3px; min-width: 58px; }
-.trend-stat-date span { color: #667eea; font-size: 13px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase; }
-.trend-stat-date small { color: #718096; font-size: 12px; font-weight: 600; }
-.trend-stat-values { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px 12px; font-size: 12px; color: #718096; }
-.trend-stat-values strong { color: #2d3748; }
-
/* ===== ANIMATIONS ===== */
@keyframes slideUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@@ -799,415 +134,63 @@ h1 {
@keyframes skyShift { 0% { background-position: 0% 35%; } 100% { background-position: 100% 65%; } }
@keyframes floatGlow { 0% { transform: translate3d(0, 0, 0) scale(1); } 50% { transform: translate3d(18px, -14px, 0) scale(1.06); } 100% { transform: translate3d(-10px, 12px, 0) scale(0.96); } }
-/* Loading State */
-.loading { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.4); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 9999; }
-.spinner { width: 50px; height: 50px; border: 5px solid rgba(255, 255, 255, 0.2); border-top: 5px solid white; border-radius: 50%; animation: spin 1s linear infinite; }
-@keyframes spin { to { transform: rotate(360deg); } }
-.loading p { color: #718096; font-size: 16px; font-weight: 500; margin-top: 16px; }
-
-.error { text-align: center; padding: 40px; background: linear-gradient(135deg, #fed7d7 0%, #fecaca 100%); border: 2px solid #fc8181; border-radius: 14px; margin-top: 20px; animation: slideUp 0.4s ease-out; }
-.error p { color: #c53030; font-size: 15px; font-weight: 500; }
-.hidden { display: none !important; }
-
-/* ===== RESPONSIVE ===== */
-@media screen and (max-width: 1024px) {
- .weather-app { padding: 1.5rem; }
- .weather-details { grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); }
- .trends-layout { grid-template-columns: 1fr; }
- .trend-stats { grid-template-columns: repeat(2, 1fr); }
-}
-
-@media screen and (max-width: 768px) {
- .weather-app { padding: 22px; }
- h1 { font-size: 28px; }
- header { grid-template-columns: 1fr; grid-template-areas: "title" "theme" "units" "search"; align-items: stretch; }
- .theme-toggle, .unit-toggle { width: fit-content; }
- #city-name { font-size: 26px; }
- #temp { font-size: 52px; }
- /* Stack temp and sun vertically on tablet */
- .weather-main { grid-template-columns: 1fr; }
- .temperature-section { grid-column: 1; grid-row: 2; }
- .sun-position-card { grid-column: 2; grid-row: 2; }
- .weather-details { grid-template-columns: repeat(2, 1fr); gap: 12px; }
- .detail-card { padding: 15px; }
- .forecast-container { grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 12px; }
- .forecast-card { padding: 14px 12px; }
- .graph-wrapper { height: 180px; }
- .trends-header { flex-direction: column; align-items: stretch; }
- .trend-controls { align-self: flex-start; }
- .trend-chart-wrapper { height: 220px; }
-}
-
-@media screen and (max-width: 600px) {
- .weather-app { padding: 1.125rem; border-radius: 1.25rem; }
- header { margin-bottom: 18px; }
- h1 { font-size: 24px; line-height: 1.1; }
- .search-box { flex-direction: column; gap: 10px; }
- .search-input-wrap { width: 100%; }
- #city-input, #search-btn { width: 100%; padding: 12px; font-size: 15px; }
- #location-btn.location-btn { width: 100%; justify-content: center; padding: 12px; flex-shrink: 0; }
- #city-input { padding-right: 40px; }
- #city-name { font-size: 22px; }
- #temp { font-size: 48px; }
- .weather-icon { width: 80px; height: 80px; }
- .temperature-section { padding: 22px 18px; }
- .weather-details { grid-template-columns: 1fr; gap: 10px; }
- .detail-card { padding: 12px; }
- .detail-label { font-size: 10px; }
- .detail-value { font-size: 14px; }
- .forecast-title { font-size: 20px; }
- .forecast-container { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
- .forecast-card { padding: 12px; }
- .graph-wrapper { height: 150px; }
- .trend-stats { grid-template-columns: 1fr; }
- .trend-stat-card { align-items: flex-start; flex-direction: column; }
- .trend-stat-values { justify-content: flex-start; }
- .trend-chart-wrapper { height: 190px; }
- .sun-position-card { padding: 16px; }
- .sun-track { height: 40px; }
- .sun-position-meta { flex-direction: column; gap: 8px; }
- .toggle-label { display: none; }
- .theme-toggle { padding: 8px 10px; }
- .unit-btn { padding: 8px 10px; font-size: 12px; }
- .share-menu { right: 0; left: 0; }
-}
-
-@media screen and (max-width: 450px) {
- .container { max-width: 100%; }
- .weather-app { padding: 16px; border-radius: 18px; }
- h1 { font-size: 22px; }
- #city-name { font-size: 20px; }
- #temp { font-size: 44px; }
- .weather-details { grid-template-columns: 1fr; }
- .forecast-container { grid-template-columns: repeat(2, 1fr); }
- .weather-main { gap: 15px; }
- .forecast-title { font-size: 18px; }
- .trend-controls { display: grid; grid-template-columns: repeat(3, 1fr); width: 100%; }
- .trend-toggle { padding: 9px 8px; }
- .detail-card { flex-direction: row; align-items: center; }
- .detail-info { align-items: flex-start; text-align: left; }
-}
-
-/* ===== CSS VARIABLES ===== */
-:root {
- --page-bg: #f1f5f9;
- --app-bg: #ffffff;
- --panel-bg: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
- --text: #0f172a;
- --muted: #64748b;
- --accent-start: #667eea;
- --accent-end: #764ba2;
- --border: rgba(226, 232, 240, 0.5);
- --card-shadow: 0 20px 60px rgba(0, 0, 0, 0.04);
-}
-
-body, .weather-app, .temperature-section, .detail-card, .forecast-graph-card {
- transition: background-color 240ms ease, color 240ms ease, border-color 240ms ease;
-}
-
-body { background-color: var(--page-bg); color: var(--text); }
-
-body.dark-mode {
- --page-bg: #0b1220;
- --app-bg: rgba(15, 23, 42, 0.92);
- --panel-bg: linear-gradient(135deg, rgba(10, 14, 25, 0.6) 0%, rgba(20, 26, 40, 0.6) 100%);
- --text: #e2e8f0;
- --muted: #94a3b8;
- --accent-start: #f59e0b;
- --accent-end: #f97316;
- --border: rgba(148, 163, 184, 0.08);
- --card-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
-}
-
-/* ===== UNIT TOGGLE ===== */
-.unit-toggle { display: flex; border: 1.5px solid rgba(102, 126, 234, 0.3); border-radius: 50px; overflow: hidden; flex-shrink: 0; }
-.unit-btn { padding: 8px 14px; border: none; background: transparent; color: #718096; font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.2s ease, color 0.2s ease; }
-.unit-btn:not(:last-child) { border-right: 1.5px solid rgba(102, 126, 234, 0.2); }
-.unit-btn.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
-.unit-btn:not(.active):hover { background: rgba(102, 126, 234, 0.1); color: #667eea; }
-
-body.dark-mode .unit-toggle { border-color: rgba(100, 116, 139, 0.4); }
-body.dark-mode .unit-btn { color: #94a3b8; }
-body.dark-mode .unit-btn:not(:last-child) { border-right-color: rgba(100, 116, 139, 0.3); }
-body.dark-mode .unit-btn:not(.active):hover { background: rgba(102, 126, 234, 0.15); color: #a5b4fc; }
-
-/* ===== CITY HEADER ROW ===== */
-.city-header-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; position: relative; }
-.city-header-actions { display: flex; align-items: center; gap: 8px; }
-
-/* ===== SHARE BUTTON & MENU ===== */
-.share-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 8px 14px;
- border: 1.5px solid rgba(102, 126, 234, 0.3);
- border-radius: 50px;
- background: rgba(102, 126, 234, 0.08);
- color: #2d3748;
- font-family: inherit;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- transition: background 0.3s ease, border-color 0.3s ease, transform 0.15s ease;
- white-space: nowrap;
-}
-
-.share-btn:hover { background: rgba(102, 126, 234, 0.15); border-color: #667eea; transform: translateY(-1px); }
-body.dark-mode .share-btn { color: #f8fafc; border-color: rgba(148, 163, 184, 0.4); background: rgba(102, 126, 234, 0.22); }
-
-.share-menu {
- position: absolute;
- top: calc(100% + 8px);
- left: 0;
- width: min(220px, 100%);
- padding: 12px;
- border-radius: 18px;
- background: white;
- border: 1px solid rgba(148, 163, 184, 0.3);
- box-shadow: 0 16px 40px rgba(15, 23, 42, 0.12);
- z-index: 10;
+/* Global Loading State */
+.loading {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.4);
display: flex;
flex-direction: column;
- gap: 8px;
-}
-
-.share-menu.hidden { display: none; }
-
-.share-menu-item {
- border: none;
- background: transparent;
- padding: 10px 12px;
- border-radius: 10px;
- font-family: inherit;
- font-size: 14px;
- font-weight: 600;
- text-align: left;
- cursor: pointer;
- transition: background 0.2s ease;
- color: #2d3748;
-}
-
-.share-menu-item:hover { background: rgba(102, 126, 234, 0.14); }
-.share-status-text { font-size: 12px; color: #475569; min-height: 18px; padding: 0 4px; }
-
-body.dark-mode .share-menu { background: rgba(15, 23, 42, 0.96); border-color: rgba(148, 163, 184, 0.24); box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45); }
-body.dark-mode .share-menu-item { background: rgba(255, 255, 255, 0.05); color: #e2e8f0; }
-body.dark-mode .share-menu-item:hover { background: rgba(255, 255, 255, 0.12); }
-body.dark-mode .share-status-text { color: #cbd5e1; }
-
-/* ===== UV INDEX ===== */
-#uv-index {
- display: inline-flex;
align-items: center;
justify-content: center;
- min-width: 32px;
- height: 32px;
- border-radius: 50%;
- font-weight: 700;
- color: white;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ z-index: 9999;
}
-#uv-index:empty, #uv-index[data-empty="true"] {
- background: transparent;
- color: #94a3b8;
- font-size: 13px;
- font-weight: 600;
- min-width: auto;
- height: auto;
- border-radius: 0;
-}
-
-/* ===== DARK MODE TOGGLE ===== */
-.theme-toggle {
- display: flex;
- align-items: center;
- gap: 6px;
- padding: 8px 16px;
- border: 1.5px solid rgba(102, 126, 234, 0.3);
- border-radius: 50px;
- background: rgba(102, 126, 234, 0.08);
- color: #2d3748;
- font-family: inherit;
- font-size: 14px;
- font-weight: 600;
- cursor: pointer;
- white-space: nowrap;
- flex-shrink: 0;
- transition: background 0.3s ease, border-color 0.3s ease, color 0.3s ease, transform 0.15s ease, box-shadow 0.3s ease;
- user-select: none;
-}
-
-.theme-toggle:hover { background: rgba(102, 126, 234, 0.15); border-color: #667eea; transform: translateY(-1px); }
-.theme-toggle:active { transform: translateY(0); }
-.toggle-icon { font-style: normal; font-size: 16px; transition: transform 0.4s ease; line-height: 1; }
-body.dark-mode .toggle-icon { transform: rotate(20deg); }
-body.dark-mode .theme-toggle { background: rgba(251, 191, 36, 0.08); border-color: rgba(251, 191, 36, 0.3); color: #fbbf24; }
-body.dark-mode .theme-toggle:hover { background: rgba(251, 191, 36, 0.15); border-color: #fbbf24; }
-
-/* ===== DARK MODE OVERRIDES ===== */
-body.dark-mode { color: #e2e8f0; }
-body.dark-mode .weather-app { background: rgba(15, 23, 42, 0.92); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.06); }
-body.dark-mode #city-input { background: rgba(30, 41, 59, 0.8); border-color: rgba(100, 116, 139, 0.4); color: #e2e8f0; }
-body.dark-mode #city-input::placeholder { color: #64748b; }
-body.dark-mode #city-input:focus { background: rgba(30, 41, 59, 1); border-color: #667eea; }
-body.dark-mode .clear-btn { color: #94a3b8; }
-body.dark-mode .clear-btn:hover { color: #e2e8f0; background: rgba(148, 163, 184, 0.15); }
-body.dark-mode .suggestions-container { background: #1e293b; box-shadow: 0 15px 50px rgba(0, 0, 0, 0.5); }
-body.dark-mode .suggestion-item { color: #cbd5e0; border-bottom-color: rgba(100, 116, 139, 0.2); }
-body.dark-mode .suggestion-item:hover { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; }
-body.dark-mode .temperature-section { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-color: rgba(100, 116, 139, 0.2); }
-body.dark-mode #city-name, body.dark-mode #temp, body.dark-mode .forecast-title { color: #f1f5f9; }
-body.dark-mode #date, body.dark-mode #weather-desc, body.dark-mode .unit, body.dark-mode .forecast-subtitle, body.dark-mode .detail-label { color: #94a3b8; }
-body.dark-mode .detail-card { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-color: rgba(100, 116, 139, 0.2); }
-body.dark-mode .detail-card:hover { border-color: #667eea; box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4); }
-body.dark-mode .detail-value { color: #f1f5f9; }
-body.dark-mode .aqi-value { color: #f1f5f9; }
-body.dark-mode .aqi-pollutants, body.dark-mode .aqi-recommendation { color: #94a3b8; }
-body.dark-mode .forecast-graph-card { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: rgba(102, 126, 234, 0.2); }
-body.dark-mode .forecast-card { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-color: rgba(100, 116, 139, 0.2); }
-body.dark-mode .forecast-card:hover { border-color: #667eea; box-shadow: 0 12px 35px rgba(0, 0, 0, 0.4); }
-body.dark-mode .forecast-temp { color: #f1f5f9; }
-body.dark-mode .forecast-desc { color: #94a3b8; }
-body.dark-mode .graph-point-label { fill: #f1f5f9; }
-body.dark-mode .graph-axis-label { fill: #94a3b8; }
-body.dark-mode .graph-point { fill: #1e293b; }
-body.dark-mode .sun-position-card { background: linear-gradient(135deg, #2a1f0a 0%, #1f1506 100%); border-color: rgba(217, 119, 6, 0.3); }
-body.dark-mode .sun-position-header h3 { color: #fbbf24; }
-body.dark-mode #sun-phase { color: #fbbf24; background: rgba(251, 191, 36, 0.1); }
-body.dark-mode .sun-position-meta { color: #d97706; }
-body.dark-mode .location-btn { color: #e2e8f0; border-color: rgba(102, 126, 234, 0.65); background: linear-gradient(135deg, rgba(102, 126, 234, 0.22) 0%, rgba(118, 75, 162, 0.22) 100%); box-shadow: 0 4px 20px rgba(102, 126, 234, 0.22); }
-body.dark-mode .location-btn:hover { border-color: rgba(102, 126, 234, 0.95); background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 100%); }
-body.dark-mode .location-label { color: #e2e8f0; }
-body.dark-mode .location-icon { color: #a5b4fc; }
-body.dark-mode .spinner { border-color: rgba(100, 116, 139, 0.3); border-top-color: white; }
-body.dark-mode .loading p { color: #94a3b8; }
-body.dark-mode .trend-chart-card { background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%); border-color: rgba(102, 126, 234, 0.2); }
-body.dark-mode .trend-stat-card { background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border-color: rgba(100, 116, 139, 0.2); }
-body.dark-mode .trend-stat-card:hover { border-color: #667eea; }
-body.dark-mode .trend-stat-values strong { color: #f1f5f9; }
-body.dark-mode .trend-stat-date span { color: #a5b4fc; }
-body.dark-mode .trend-stat-date small { color: #64748b; }
-body.dark-mode .trend-value-label { stroke: #0f172a; }
-body.dark-mode .history-dropdown { background: rgba(15, 23, 42, 0.95); border-color: rgba(99, 102, 241, 0.3); }
-body.dark-mode .history-panel-header h2 { color: #f1f5f9; }
-body.dark-mode .history-panel-header p { color: #94a3b8; }
-body.dark-mode .history-button { background: rgba(30, 41, 59, 0.8); border-color: rgba(100, 116, 139, 0.3); color: #e2e8f0; }
-body.dark-mode .history-button:hover { background: rgba(51, 65, 85, 0.9); }
-body.dark-mode .history-section-title { color: #94a3b8; }
-body.dark-mode .clear-history-btn { background: rgba(30, 41, 59, 0.8); border-color: rgba(100, 116, 139, 0.3); color: #94a3b8; }
-body.dark-mode .hourly-toggle { background: rgba(30, 41, 59, 0.9); color: #94a3b8; }
-body.dark-mode .hourly-toggle.active { color: white; }
-body.dark-mode .trend-controls { background: rgba(30, 41, 59, 0.9); border-color: rgba(100, 116, 139, 0.2); }
-body.dark-mode .trend-toggle { color: #94a3b8; }
-body.dark-mode .trend-toggle:hover { color: #a5b4fc; background: rgba(102, 126, 234, 0.15); }
-
-/* Hide welcome message when weather data is visible */
-#weather-container:not(.hidden) ~ #no-data-message,
-#weather-container:not(.hidden) + #no-data-message { display: none !important; }
-
-/* ===== UTILITY ===== */
-.text-red-500 { color: #ef4444 !important; }
-.text-sm { font-size: 0.875rem !important; }
-.mt-2 { margin-top: 0.5rem !important; margin-left: 0.5rem !important; }
-
-#search-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; box-shadow: none; background: #cbd5e0; }
-
-/* ===== LOCATION BUTTON ===== */
-.location-btn {
- background: linear-gradient(135deg, rgba(102, 126, 234, 0.12) 0%, rgba(118, 75, 162, 0.12) 100%);
- border: 1.5px solid rgba(102, 126, 234, 0.35);
- border-radius: 14px;
- padding: 14px 16px;
- cursor: pointer;
- color: #2d3748;
- transition: all 0.3s ease;
- display: flex;
- align-items: center;
- gap: 10px;
- font-weight: 700;
- white-space: nowrap;
- flex-shrink: 0;
- box-shadow: 0 4px 15px rgba(102, 126, 234, 0.10);
+.spinner {
+ width: 50px;
+ height: 50px;
+ border: 5px solid rgba(255, 255, 255, 0.2);
+ border-top: 5px solid white;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
}
-.location-btn:hover { transform: translateY(-3px); box-shadow: 0 10px 25px rgba(102, 126, 234, 0.25); border-color: rgba(102, 126, 234, 0.65); }
-.location-btn:active { transform: translateY(-1px); }
-.location-btn:focus-visible { outline: 2px solid #667eea; outline-offset: 2px; }
-.location-btn:disabled { opacity: 0.6; cursor: not-allowed; transform: none; box-shadow: none; }
-.location-icon { font-size: 18px; line-height: 1; }
-.location-label { font-size: 14px; }
-
-.feels-like-main { font-size: 14px; color: #94a3b8; font-weight: 500; transition: color 0.4s ease; }
-body.dark-mode .feels-like-main { color: #94a3b8; }
-
-.no-data-message { text-align: center; padding: 40px 20px; color: #718096; }
-.no-data-message h2 { font-size: 22px; font-weight: 700; color: #2d3748; margin-bottom: 8px; transition: color 0.4s ease; }
-body.dark-mode .no-data-message h2 { color: #f1f5f9; }
-body.dark-mode .no-data-message { color: #94a3b8; }
-
-/* ===== SKELETON LOADER ===== */
-.skeleton-loader { padding: 20px; }
-.skeleton-text, .skeleton-card { background: linear-gradient(90deg, #e2e8f0 25%, #f1f5f9 50%, #e2e8f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 8px; }
-@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
-body.dark-mode .skeleton-text, body.dark-mode .skeleton-card { background: linear-gradient(90deg, #1e293b 25%, #2d3748 50%, #1e293b 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; }
-
+@keyframes spin { to { transform: rotate(360deg); } }
-/* ===== WEATHER MAP SECTION ===== */
-.weather-map-section {
- display: flex;
- flex-direction: column;
- gap: 20px;
- animation: slideUp 0.6s ease-out 0.3s both;
- margin-top: 10px;
+.loading p {
+ color: #cbd5e1;
+ font-size: 16px;
+ font-weight: 500;
+ margin-top: 16px;
}
-.map-card {
- background: linear-gradient(180deg, #f8fbff 0%, #eef4ff 100%);
- border: 2px solid rgba(102, 126, 234, 0.15);
- border-radius: 20px;
- padding: 12px;
- transition: all 0.3s ease;
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
- position: relative;
- z-index: 1; /* Keep map controls below header dropdowns */
+body.dark-mode .spinner {
+ border-color: rgba(100, 116, 139, 0.3);
+ border-top-color: white;
}
-.map-card:hover {
- border-color: #667eea;
- box-shadow: 0 15px 40px rgba(102, 126, 234, 0.15);
+body.dark-mode .loading p {
+ color: #94a3b8;
}
-/* Dark Mode Overrides for Map */
-body.dark-mode .map-card {
- background: linear-gradient(180deg, #1e293b 0%, #0f172a 100%);
- border-color: rgba(102, 126, 234, 0.2);
+.hidden {
+ display: none !important;
}
-body.dark-mode .map-card:hover {
- border-color: #667eea;
+/* ===== RESPONSIVE ===== */
+@media screen and (max-width: 1024px) {
+ .weather-app { padding: 1.5rem; }
}
-/* Leaflet Dark Mode UI Adjustments */
-body.dark-mode .leaflet-control-zoom a {
- background-color: #1e293b;
- color: #e2e8f0;
- border-color: #334155;
+@media screen and (max-width: 768px) {
+ .weather-app { padding: 22px; }
}
-body.dark-mode .leaflet-control-zoom a:hover {
- background-color: #334155;
+@media screen and (max-width: 600px) {
+ .weather-app { padding: 1.125rem; border-radius: 1.25rem; }
}
-body.dark-mode .leaflet-popup-content-wrapper,
-body.dark-mode .leaflet-popup-tip {
- background-color: #1e293b;
- color: #f1f5f9;
- box-shadow: 0 3px 14px rgba(0,0,0,0.4);
-/* Wind Compass Styling */
-#wind-compass-icon {
- transform-origin: center center;
- transition: transform 0.8s cubic-bezier(0.25, 0.46, 0.45, 0.94);
- display: inline-block;
+@media screen and (max-width: 450px) {
+ .container { max-width: 100%; }
+ .weather-app { padding: 16px; border-radius: 18px; }
}
\ No newline at end of file