Spaces:
Running
Running
can you create a weather widget that mimic the one from perplexity fully interactive
aadabc6 verified | class WeatherWidget extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.state = window.WeatherState; | |
| this.attachShadow({ mode: 'open' }); | |
| } | |
| connectedCallback() { | |
| this.render(); | |
| this.state.addListener(() => this.render()); | |
| this.setupEventListeners(); | |
| } | |
| setupEventListeners() { | |
| this.shadowRoot.addEventListener('click', (e) => { | |
| if (e.target.closest('[data-action="search"]')) { | |
| this.handleSearch(); | |
| } | |
| if (e.target.closest('[data-action="refresh"]')) { | |
| window.loadCurrentLocationWeather(); | |
| } | |
| if (e.target.closest('[data-action="location"]')) { | |
| this.toggleLocationSearch(); | |
| } | |
| }); | |
| this.shadowRoot.addEventListener('input', (e) => { | |
| if (e.target.name === 'location-search') { | |
| this.handleSearchInput(e.target.value); | |
| } | |
| }); | |
| this.shadowRoot.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && e.target.name === 'location-search') { | |
| this.handleSearch(); | |
| } | |
| }); | |
| } | |
| handleSearchInput(value) { | |
| const searchBtn = this.shadowRoot.querySelector('[data-action="search"]'); | |
| searchBtn.disabled = !value.trim(); | |
| } | |
| handleSearch() { | |
| const input = this.shadowRoot.querySelector('[name="location-search"]'); | |
| if (input.value.trim()) { | |
| window.searchLocation(input.value.trim()); | |
| input.value = ''; | |
| this.toggleLocationSearch(); | |
| } | |
| } | |
| toggleLocationSearch() { | |
| const searchContainer = this.shadowRoot.querySelector('.location-search-container'); | |
| searchContainer.classList.toggle('hidden'); | |
| if (!searchContainer.classList.contains('hidden')) { | |
| const input = this.shadowRoot.querySelector('[name="location-search"]'); | |
| input.focus(); | |
| } | |
| } | |
| render() { | |
| this.shadowRoot.innerHTML = ` | |
| <style> | |
| :host { | |
| display: block; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .weather-card { | |
| backdrop-filter: blur(10px); | |
| background: rgba(255, 255, 255, 0.85); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
| } | |
| .weather-gradient { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| } | |
| .animate-pulse-soft { | |
| animation: pulse-soft 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| .weather-icon { | |
| filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1)); | |
| } | |
| .loading-dots { | |
| display: inline-block; | |
| position: relative; | |
| width: 80px; | |
| height: 80px; | |
| } | |
| .loading-dots div { | |
| position: absolute; | |
| top: 33px; | |
| width: 13px; | |
| height: 13px; | |
| border-radius: 50%; | |
| background: #0ea5e9; | |
| animation-timing-function: cubic-bezier(0, 1, 1, 0); | |
| } | |
| .loading-dots div:nth-child(1) { | |
| left: 8px; | |
| animation: loading-dots1 0.6s infinite; | |
| } | |
| .loading-dots div:nth-child(2) { | |
| left: 8px; | |
| animation: loading-dots2 0.6s infinite; | |
| } | |
| .loading-dots div:nth-child(3) { | |
| left: 32px; | |
| animation: loading-dots2 0.6s infinite; | |
| } | |
| .loading-dots div:nth-child(4) { | |
| left: 56px; | |
| animation: loading-dots3 0.6s infinite; | |
| } | |
| @keyframes loading-dots1 { | |
| 0% { transform: scale(0); } | |
| 100% { transform: scale(1); } | |
| } | |
| @keyframes loading-dots3 { | |
| 0% { transform: scale(1); } | |
| 100% { transform: scale(0); } | |
| } | |
| @keyframes loading-dots2 { | |
| 0% { transform: translate(0, 0); } | |
| 100% { transform: translate(24px, 0); } | |
| } | |
| .slide-up { | |
| animation: slideUp 0.3s ease-out; | |
| } | |
| @keyframes slideUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes pulse-soft { | |
| 0%, 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.8; | |
| } | |
| } | |
| .glass-effect { | |
| background: rgba(255, 255, 255, 0.25); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.18); | |
| } | |
| </style> | |
| <div class="min-h-screen flex items-center justify-center p-4"> | |
| <div class="weather-card rounded-3xl p-6 max-w-md w-full mx-auto slide-up"> | |
| ${this.renderHeader()} | |
| ${this.renderContent()} | |
| ${this.renderForecast()} | |
| ${this.renderDetails()} | |
| </div> | |
| </div> | |
| `; | |
| // Re-initialize feather icons | |
| if (window.feather) { | |
| setTimeout(() => window.feather.replace(), 0); | |
| } | |
| } | |
| renderHeader() { | |
| return ` | |
| <div class="flex items-center justify-between mb-6"> | |
| <div class="flex items-center space-x-3"> | |
| <button class="p-2 rounded-full glass-effect hover:bg-white/30 transition-all duration-200" | |
| data-action="refresh" | |
| ${this.state.loading ? 'disabled' : ''}> | |
| <i data-feather="refresh-cw" class="w-4 h-4 text-primary-600"></i> | |
| </button> | |
| <div> | |
| <h1 class="text-2xl font-bold text-gray-900">Weather</h1> | |
| <p class="text-sm text-gray-600">${this.state.currentLocation}</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <div class="location-search-container hidden absolute top-20 left-1/2 transform -translate-x-1/2 z-10 bg-white rounded-2xl shadow-xl p-4 w-80"> | |
| <div class="flex items-center space-x-2"> | |
| <input type="text" | |
| name="location-search" | |
| placeholder="Search location..." | |
| class="flex-1 px-4 py-2 rounded-full border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary-500" | |
| autocomplete="off"> | |
| <button class="p-2 rounded-full bg-primary-500 text-white hover:bg-primary-600 transition-colors duration-200" | |
| data-action="search" | |
| disabled> | |
| <i data-feather="search" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <button class="p-2 rounded-full glass-effect hover:bg-white/30 transition-all duration-200" | |
| data-action="location"> | |
| <i data-feather="map-pin" class="w-4 h-4 text-primary-600"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| renderContent() { | |
| if (this.state.loading) { | |
| return this.renderLoading(); | |
| } | |
| if (this.state.error) { | |
| return this.renderError(); | |
| } | |
| if (!this.state.weatherData) { | |
| return this.renderEmpty(); | |
| } | |
| const { current } = this.state.weatherData; | |
| const temp = window.WeatherUtils.kelvinToCelsius(current.temp); | |
| const feelsLike = window.WeatherUtils.kelvinToCelsius(current.feels_like); | |
| const icon = window.WeatherUtils.getWeatherIcon(current.weather[0].icon); | |
| return ` | |
| <div class="text-center mb-8 slide-up"> | |
| <div class="weather-gradient rounded-2xl p-8 mb-4"> | |
| <div class="flex items-center justify-center mb-4"> | |
| <i data-feather="${icon}" class="weather-icon w-16 h-16 text-white"></i> | |
| </div> | |
| <div class="text-white"> | |
| <div class="text-6xl font-bold mb-2">${temp}°</div> | |
| <div class="text-lg opacity-90">${current.weather[0].description}</div> | |
| <div class="text-sm opacity-80 mt-1">Feels like ${feelsLike}°</div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| renderForecast() { | |
| if (this.state.loading || this.state.error || !this.state.weatherData) { | |
| return ''; | |
| } | |
| const { daily } = this.state.weatherData; | |
| const forecastDays = daily.slice(1, 6); // Next 5 days | |
| return ` | |
| <div class="mb-6 slide-up"> | |
| <h3 class="text-lg font-semibold text-gray-900 mb-4">5-Day Forecast</h3> | |
| <div class="grid grid-cols-5 gap-2"> | |
| ${forecastDays.map(day => { | |
| const date = window.WeatherUtils.formatDate(day.dt); | |
| const temp = window.WeatherUtils.kelvinToCelsius(day.temp.day); | |
| const icon = window.WeatherUtils.getWeatherIcon(day.weather[0].icon); | |
| return ` | |
| <div class="text-center p-2 rounded-xl hover:bg-gray-50 transition-colors duration-200"> | |
| <div class="text-sm font-medium text-gray-600 mb-1">${date}</div> | |
| <i data-feather="${icon}" class="weather-icon w-6 h-6 text-primary-500 mx-auto mb-1"></i> | |
| <div class="text-sm font-semibold text-gray-900">${temp}°</div> | |
| </div> | |
| `; | |
| }).join('')} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| renderDetails() { | |
| if (this.state.loading || this.state.error || !this.state.weatherData) { | |
| return ''; | |
| } | |
| const { current } = this.state.weatherData; | |
| return ` | |
| <div class="slide-up"> | |
| <h3 class="text-lg font-semibold text-gray-900 mb-4">Details</h3> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div class="bg-gray-50 rounded-xl p-4"> | |
| <div class="flex items-center space-x-2 text-gray-600"> | |
| <i data-feather="wind" class="w-4 h-4"></i> | |
| <span class="text-sm">Wind</span> | |
| </div> | |
| <div class="text-lg font-semibold text-gray-900">${current.wind_speed} m/s</div> | |
| </div> | |
| <div class="bg-gray-50 rounded-xl p-4"> | |
| <div class="flex items-center space-x-2 text-gray-600"> | |
| <i data-feather="droplet" class="w-4 h-4"></i> | |
| <span class="text-sm">Humidity</span> | |
| </div> | |
| <div class="text-lg font-semibold text-gray-900">${current.humidity}%</div> | |
| </div> | |
| <div class="bg-gray-50 rounded-xl p-4"> | |
| <div class="flex items-center space-x-2 text-gray-600"> | |
| <i data-feather="thermometer" class="w-4 h-4"></i> | |
| <span class="text-sm">Pressure</span> | |
| </div> | |
| <div class="text-lg font-semibold text-gray-900">${current.pressure} hPa</div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| renderLoading() { | |
| return ` | |
| <div class="text-center py-12 slide-up"> | |
| <div class="loading-dots inline-block"> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| <div></div> | |
| </div> | |
| <p class="text-gray-600 mt-4">Loading weather data...</p> | |
| </div> | |
| `; | |
| } | |
| renderError() { | |
| return ` | |
| <div class="text-center py-12 slide-up"> | |
| <i data-feather="alert-triangle" class="w-12 h-12 text-yellow-500 mx-auto mb-4"></i> | |
| <p class="text-gray-600">${this.state.error}</p> | |
| <button class="mt-4 px-6 py-2 bg-primary-500 text-white rounded-full hover:bg-primary-600 transition-colors duration-200" | |
| onclick="window.loadCurrentLocationWeather()"> | |
| Try Again | |
| </button> | |
| </div> | |
| `; | |
| } | |
| renderEmpty() { | |
| return ` | |
| <div class="text-center py-12 slide-up"> | |
| <i data-feather="cloud" class="w-12 h-12 text-gray-400 mx-auto mb-4"></i> | |
| <p class="text-gray-600">No weather data available</p> | |
| </div> | |
| `; | |
| } | |
| } | |
| customElements.define('weather-widget', WeatherWidget); |