weatherwise-pro / components /weather-widget.js
QuentinCaron's picture
can you create a weather widget that mimic the one from perplexity fully interactive
aadabc6 verified
Raw
History Blame Contribute Delete
13.8 kB
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);