QuentinCaron commited on
Commit
aadabc6
·
verified ·
1 Parent(s): 4f4eb52

can you create a weather widget that mimic the one from perplexity fully interactive

Browse files
Files changed (5) hide show
  1. README.md +8 -5
  2. components/weather-widget.js +364 -0
  3. index.html +55 -19
  4. script.js +191 -0
  5. style.css +90 -19
README.md CHANGED
@@ -1,10 +1,13 @@
1
  ---
2
- title: Weatherwise Pro
3
- emoji: 👁
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
  ---
2
+ title: WeatherWise Pro ☀️
3
+ colorFrom: purple
4
+ colorTo: blue
5
+ emoji: 🐳
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite-v3
10
  ---
11
 
12
+ # Welcome to your new DeepSite project!
13
+ This project was created with [DeepSite](https://huggingface.co/deepsite).
components/weather-widget.js ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class WeatherWidget extends HTMLElement {
2
+ constructor() {
3
+ super();
4
+ this.state = window.WeatherState;
5
+ this.attachShadow({ mode: 'open' });
6
+ }
7
+
8
+ connectedCallback() {
9
+ this.render();
10
+ this.state.addListener(() => this.render());
11
+ this.setupEventListeners();
12
+ }
13
+
14
+ setupEventListeners() {
15
+ this.shadowRoot.addEventListener('click', (e) => {
16
+ if (e.target.closest('[data-action="search"]')) {
17
+ this.handleSearch();
18
+ }
19
+ if (e.target.closest('[data-action="refresh"]')) {
20
+ window.loadCurrentLocationWeather();
21
+ }
22
+ if (e.target.closest('[data-action="location"]')) {
23
+ this.toggleLocationSearch();
24
+ }
25
+ });
26
+
27
+ this.shadowRoot.addEventListener('input', (e) => {
28
+ if (e.target.name === 'location-search') {
29
+ this.handleSearchInput(e.target.value);
30
+ }
31
+ });
32
+
33
+ this.shadowRoot.addEventListener('keypress', (e) => {
34
+ if (e.key === 'Enter' && e.target.name === 'location-search') {
35
+ this.handleSearch();
36
+ }
37
+ });
38
+ }
39
+
40
+ handleSearchInput(value) {
41
+ const searchBtn = this.shadowRoot.querySelector('[data-action="search"]');
42
+ searchBtn.disabled = !value.trim();
43
+ }
44
+
45
+ handleSearch() {
46
+ const input = this.shadowRoot.querySelector('[name="location-search"]');
47
+ if (input.value.trim()) {
48
+ window.searchLocation(input.value.trim());
49
+ input.value = '';
50
+ this.toggleLocationSearch();
51
+ }
52
+ }
53
+
54
+ toggleLocationSearch() {
55
+ const searchContainer = this.shadowRoot.querySelector('.location-search-container');
56
+ searchContainer.classList.toggle('hidden');
57
+
58
+ if (!searchContainer.classList.contains('hidden')) {
59
+ const input = this.shadowRoot.querySelector('[name="location-search"]');
60
+ input.focus();
61
+ }
62
+ }
63
+
64
+ render() {
65
+ this.shadowRoot.innerHTML = `
66
+ <style>
67
+ :host {
68
+ display: block;
69
+ font-family: 'Inter', sans-serif;
70
+ }
71
+
72
+ .weather-card {
73
+ backdrop-filter: blur(10px);
74
+ background: rgba(255, 255, 255, 0.85);
75
+ border: 1px solid rgba(255, 255, 255, 0.2);
76
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
77
+ }
78
+
79
+ .weather-gradient {
80
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
81
+ }
82
+
83
+ .animate-pulse-soft {
84
+ animation: pulse-soft 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
85
+ }
86
+
87
+ .weather-icon {
88
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
89
+ }
90
+
91
+ .loading-dots {
92
+ display: inline-block;
93
+ position: relative;
94
+ width: 80px;
95
+ height: 80px;
96
+ }
97
+
98
+ .loading-dots div {
99
+ position: absolute;
100
+ top: 33px;
101
+ width: 13px;
102
+ height: 13px;
103
+ border-radius: 50%;
104
+ background: #0ea5e9;
105
+ animation-timing-function: cubic-bezier(0, 1, 1, 0);
106
+ }
107
+
108
+ .loading-dots div:nth-child(1) {
109
+ left: 8px;
110
+ animation: loading-dots1 0.6s infinite;
111
+ }
112
+
113
+ .loading-dots div:nth-child(2) {
114
+ left: 8px;
115
+ animation: loading-dots2 0.6s infinite;
116
+ }
117
+
118
+ .loading-dots div:nth-child(3) {
119
+ left: 32px;
120
+ animation: loading-dots2 0.6s infinite;
121
+ }
122
+
123
+ .loading-dots div:nth-child(4) {
124
+ left: 56px;
125
+ animation: loading-dots3 0.6s infinite;
126
+ }
127
+
128
+ @keyframes loading-dots1 {
129
+ 0% { transform: scale(0); }
130
+ 100% { transform: scale(1); }
131
+ }
132
+
133
+ @keyframes loading-dots3 {
134
+ 0% { transform: scale(1); }
135
+ 100% { transform: scale(0); }
136
+ }
137
+
138
+ @keyframes loading-dots2 {
139
+ 0% { transform: translate(0, 0); }
140
+ 100% { transform: translate(24px, 0); }
141
+ }
142
+
143
+ .slide-up {
144
+ animation: slideUp 0.3s ease-out;
145
+ }
146
+
147
+ @keyframes slideUp {
148
+ from {
149
+ opacity: 0;
150
+ transform: translateY(20px);
151
+ }
152
+ to {
153
+ opacity: 1;
154
+ transform: translateY(0);
155
+ }
156
+ }
157
+
158
+ @keyframes pulse-soft {
159
+ 0%, 100% {
160
+ opacity: 1;
161
+ }
162
+ 50% {
163
+ opacity: 0.8;
164
+ }
165
+ }
166
+
167
+ .glass-effect {
168
+ background: rgba(255, 255, 255, 0.25);
169
+ backdrop-filter: blur(10px);
170
+ border: 1px solid rgba(255, 255, 255, 0.18);
171
+ }
172
+ </style>
173
+
174
+ <div class="min-h-screen flex items-center justify-center p-4">
175
+ <div class="weather-card rounded-3xl p-6 max-w-md w-full mx-auto slide-up">
176
+ ${this.renderHeader()}
177
+ ${this.renderContent()}
178
+ ${this.renderForecast()}
179
+ ${this.renderDetails()}
180
+ </div>
181
+ </div>
182
+ `;
183
+
184
+ // Re-initialize feather icons
185
+ if (window.feather) {
186
+ setTimeout(() => window.feather.replace(), 0);
187
+ }
188
+ }
189
+
190
+ renderHeader() {
191
+ return `
192
+ <div class="flex items-center justify-between mb-6">
193
+ <div class="flex items-center space-x-3">
194
+ <button class="p-2 rounded-full glass-effect hover:bg-white/30 transition-all duration-200"
195
+ data-action="refresh"
196
+ ${this.state.loading ? 'disabled' : ''}>
197
+ <i data-feather="refresh-cw" class="w-4 h-4 text-primary-600"></i>
198
+ </button>
199
+ <div>
200
+ <h1 class="text-2xl font-bold text-gray-900">Weather</h1>
201
+ <p class="text-sm text-gray-600">${this.state.currentLocation}</p>
202
+ </div>
203
+ </div>
204
+ <div class="flex items-center space-x-2">
205
+ <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">
206
+ <div class="flex items-center space-x-2">
207
+ <input type="text"
208
+ name="location-search"
209
+ placeholder="Search location..."
210
+ class="flex-1 px-4 py-2 rounded-full border border-gray-200 focus:outline-none focus:ring-2 focus:ring-primary-500"
211
+ autocomplete="off">
212
+ <button class="p-2 rounded-full bg-primary-500 text-white hover:bg-primary-600 transition-colors duration-200"
213
+ data-action="search"
214
+ disabled>
215
+ <i data-feather="search" class="w-4 h-4"></i>
216
+ </button>
217
+ </div>
218
+ </div>
219
+ <button class="p-2 rounded-full glass-effect hover:bg-white/30 transition-all duration-200"
220
+ data-action="location">
221
+ <i data-feather="map-pin" class="w-4 h-4 text-primary-600"></i>
222
+ </button>
223
+ </div>
224
+ </div>
225
+ `;
226
+ }
227
+
228
+ renderContent() {
229
+ if (this.state.loading) {
230
+ return this.renderLoading();
231
+ }
232
+
233
+ if (this.state.error) {
234
+ return this.renderError();
235
+ }
236
+
237
+ if (!this.state.weatherData) {
238
+ return this.renderEmpty();
239
+ }
240
+
241
+ const { current } = this.state.weatherData;
242
+ const temp = window.WeatherUtils.kelvinToCelsius(current.temp);
243
+ const feelsLike = window.WeatherUtils.kelvinToCelsius(current.feels_like);
244
+ const icon = window.WeatherUtils.getWeatherIcon(current.weather[0].icon);
245
+
246
+ return `
247
+ <div class="text-center mb-8 slide-up">
248
+ <div class="weather-gradient rounded-2xl p-8 mb-4">
249
+ <div class="flex items-center justify-center mb-4">
250
+ <i data-feather="${icon}" class="weather-icon w-16 h-16 text-white"></i>
251
+ </div>
252
+ <div class="text-white">
253
+ <div class="text-6xl font-bold mb-2">${temp}°</div>
254
+ <div class="text-lg opacity-90">${current.weather[0].description}</div>
255
+ <div class="text-sm opacity-80 mt-1">Feels like ${feelsLike}°</div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ `;
260
+ }
261
+
262
+ renderForecast() {
263
+ if (this.state.loading || this.state.error || !this.state.weatherData) {
264
+ return '';
265
+ }
266
+
267
+ const { daily } = this.state.weatherData;
268
+ const forecastDays = daily.slice(1, 6); // Next 5 days
269
+
270
+ return `
271
+ <div class="mb-6 slide-up">
272
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">5-Day Forecast</h3>
273
+ <div class="grid grid-cols-5 gap-2">
274
+ ${forecastDays.map(day => {
275
+ const date = window.WeatherUtils.formatDate(day.dt);
276
+ const temp = window.WeatherUtils.kelvinToCelsius(day.temp.day);
277
+ const icon = window.WeatherUtils.getWeatherIcon(day.weather[0].icon);
278
+ return `
279
+ <div class="text-center p-2 rounded-xl hover:bg-gray-50 transition-colors duration-200">
280
+ <div class="text-sm font-medium text-gray-600 mb-1">${date}</div>
281
+ <i data-feather="${icon}" class="weather-icon w-6 h-6 text-primary-500 mx-auto mb-1"></i>
282
+ <div class="text-sm font-semibold text-gray-900">${temp}°</div>
283
+ </div>
284
+ `;
285
+ }).join('')}
286
+ </div>
287
+ </div>
288
+ `;
289
+ }
290
+
291
+ renderDetails() {
292
+ if (this.state.loading || this.state.error || !this.state.weatherData) {
293
+ return '';
294
+ }
295
+
296
+ const { current } = this.state.weatherData;
297
+
298
+ return `
299
+ <div class="slide-up">
300
+ <h3 class="text-lg font-semibold text-gray-900 mb-4">Details</h3>
301
+ <div class="grid grid-cols-2 gap-4">
302
+ <div class="bg-gray-50 rounded-xl p-4">
303
+ <div class="flex items-center space-x-2 text-gray-600">
304
+ <i data-feather="wind" class="w-4 h-4"></i>
305
+ <span class="text-sm">Wind</span>
306
+ </div>
307
+ <div class="text-lg font-semibold text-gray-900">${current.wind_speed} m/s</div>
308
+ </div>
309
+ <div class="bg-gray-50 rounded-xl p-4">
310
+ <div class="flex items-center space-x-2 text-gray-600">
311
+ <i data-feather="droplet" class="w-4 h-4"></i>
312
+ <span class="text-sm">Humidity</span>
313
+ </div>
314
+ <div class="text-lg font-semibold text-gray-900">${current.humidity}%</div>
315
+ </div>
316
+ <div class="bg-gray-50 rounded-xl p-4">
317
+ <div class="flex items-center space-x-2 text-gray-600">
318
+ <i data-feather="thermometer" class="w-4 h-4"></i>
319
+ <span class="text-sm">Pressure</span>
320
+ </div>
321
+ <div class="text-lg font-semibold text-gray-900">${current.pressure} hPa</div>
322
+ </div>
323
+ </div>
324
+ `;
325
+ }
326
+
327
+ renderLoading() {
328
+ return `
329
+ <div class="text-center py-12 slide-up">
330
+ <div class="loading-dots inline-block">
331
+ <div></div>
332
+ <div></div>
333
+ <div></div>
334
+ <div></div>
335
+ </div>
336
+ <p class="text-gray-600 mt-4">Loading weather data...</p>
337
+ </div>
338
+ `;
339
+ }
340
+
341
+ renderError() {
342
+ return `
343
+ <div class="text-center py-12 slide-up">
344
+ <i data-feather="alert-triangle" class="w-12 h-12 text-yellow-500 mx-auto mb-4"></i>
345
+ <p class="text-gray-600">${this.state.error}</p>
346
+ <button class="mt-4 px-6 py-2 bg-primary-500 text-white rounded-full hover:bg-primary-600 transition-colors duration-200"
347
+ onclick="window.loadCurrentLocationWeather()">
348
+ Try Again
349
+ </button>
350
+ </div>
351
+ `;
352
+ }
353
+
354
+ renderEmpty() {
355
+ return `
356
+ <div class="text-center py-12 slide-up">
357
+ <i data-feather="cloud" class="w-12 h-12 text-gray-400 mx-auto mb-4"></i>
358
+ <p class="text-gray-600">No weather data available</p>
359
+ </div>
360
+ `;
361
+ }
362
+ }
363
+
364
+ customElements.define('weather-widget', WeatherWidget);
index.html CHANGED
@@ -1,19 +1,55 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>WeatherWise Pro ☀️</title>
7
+ <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
8
+ <link rel="stylesheet" href="style.css">
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
11
+ <script src="https://unpkg.com/feather-icons"></script>
12
+ <script>
13
+ tailwind.config = {
14
+ theme: {
15
+ extend: {
16
+ colors: {
17
+ primary: {
18
+ 50: '#f0f9ff',
19
+ 100: '#e0f2fe',
20
+ 200: '#bae6fd',
21
+ 300: '#7dd3fc',
22
+ 400: '#38bdf8',
23
+ 500: '#0ea5e9',
24
+ 600: '#0284c7',
25
+ 700: '#0369a1',
26
+ 800: '#075985',
27
+ 900: '#0c4a6e',
28
+ },
29
+ secondary: {
30
+ 50: '#fafafa',
31
+ 100: '#f4f4f5',
32
+ 200: '#e4e4e7',
33
+ 300: '#d4d4d8',
34
+ 400: '#a1a1aa',
35
+ 500: '#71717a',
36
+ 600: '#52525b',
37
+ 700: '#3f3f46',
38
+ 800: '#27272a',
39
+ 900: '#18181b',
40
+ }
41
+ }
42
+ }
43
+ }
44
+ }
45
+ </script>
46
+ </head>
47
+ <body class="bg-gradient-to-br from-primary-50 via-white to-secondary-50 min-h-screen">
48
+ <weather-widget></weather-widget>
49
+
50
+ <script src="components/weather-widget.js"></script>
51
+ <script src="script.js"></script>
52
+ <script>feather.replace();</script>
53
+ <script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
54
+ </body>
55
+ </html>
script.js ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Weather API configuration
2
+ const WEATHER_API_KEY = 'demo_key'; // Replace with actual API key
3
+ const WEATHER_API_URL = 'https://api.openweathermap.org/data/2.5';
4
+
5
+ // Mock weather data for demonstration
6
+ const mockWeatherData = {
7
+ current: {
8
+ temp: 22,
9
+ feels_like: 24,
10
+ humidity: 65,
11
+ pressure: 1013,
12
+ wind_speed: 3.5,
13
+ weather: [{ main: 'Clear', description: 'clear sky', icon: '01d' }]
14
+ },
15
+ daily: [
16
+ { dt: Date.now()/1000 + 86400, temp: { day: 23 }, weather: [{ main: 'Clouds', icon: '02d' }] },
17
+ { dt: Date.now()/1000 + 172800, temp: { day: 21 }, weather: [{ main: 'Rain', icon: '10d' }] },
18
+ { dt: Date.now()/1000 + 259200, temp: { day: 25 }, weather: [{ main: 'Clear', icon: '01d' }] },
19
+ { dt: Date.now()/1000 + 345600, temp: { day: 20 }, weather: [{ main: 'Clouds', icon: '03d' }] },
20
+ { dt: Date.now()/1000 + 432000, temp: { day: 19 }, weather: [{ main: 'Rain', icon: '09d' }] }
21
+ ]
22
+ };
23
+
24
+ // Utility functions
25
+ class WeatherUtils {
26
+ static kelvinToCelsius(kelvin) {
27
+ return Math.round(kelvin - 273.15);
28
+ }
29
+
30
+ static getWeatherIcon(iconCode) {
31
+ const iconMap = {
32
+ '01d': 'sun',
33
+ '01n': 'moon',
34
+ '02d': 'cloud',
35
+ '02n': 'cloud',
36
+ '03d': 'cloud',
37
+ '03n': 'cloud',
38
+ '04d': 'cloud',
39
+ '04n': 'cloud',
40
+ '09d': 'cloud-rain',
41
+ '09n': 'cloud-rain',
42
+ '10d': 'cloud-drizzle',
43
+ '10n': 'cloud-drizzle',
44
+ '11d': 'cloud-lightning',
45
+ '11n': 'cloud-lightning',
46
+ '13d': 'cloud-snow',
47
+ '13n': 'cloud-snow',
48
+ '50d': 'wind',
49
+ '50n': 'wind'
50
+ };
51
+ return iconMap[iconCode] || 'cloud';
52
+ }
53
+
54
+ static formatDate(timestamp) {
55
+ const date = new Date(timestamp * 1000);
56
+ return date.toLocaleDateString('en-US', { weekday: 'short' });
57
+ }
58
+
59
+ static getWindDirection(degrees) {
60
+ const directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW'];
61
+ return directions[Math.round(degrees / 45) % 8];
62
+ }
63
+ }
64
+
65
+ // Weather API service
66
+ class WeatherService {
67
+ static async getWeatherData(lat, lon) {
68
+ try {
69
+ // For demo purposes, return mock data
70
+ // In production, uncomment the API call below
71
+ /*
72
+ const response = await fetch(
73
+ `${WEATHER_API_URL}/onecall?lat=${lat}&lon=${lon}&exclude=minutely,hourly,alerts&appid=${WEATHER_API_KEY}`
74
+ );
75
+ if (!response.ok) throw new Error('Weather data fetch failed');
76
+ return await response.json();
77
+ */
78
+
79
+ // Simulate API delay
80
+ await new Promise(resolve => setTimeout(resolve, 1000));
81
+ return mockWeatherData;
82
+ } catch (error) {
83
+ console.error('Error fetching weather data:', error);
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ static async getLocationWeather(city) {
89
+ try {
90
+ // For demo, return mock data with city name
91
+ const data = { ...mockWeatherData, city: city || 'Current Location' };
92
+ await new Promise(resolve => setTimeout(resolve, 800));
93
+ return data;
94
+ } catch (error) {
95
+ console.error('Error fetching location weather:', error);
96
+ throw error;
97
+ }
98
+ }
99
+ }
100
+
101
+ // Global state management
102
+ class WeatherState {
103
+ constructor() {
104
+ this.currentLocation = 'Current Location';
105
+ this.weatherData = null;
106
+ this.loading = false;
107
+ this.error = null;
108
+ }
109
+
110
+ setLoading(loading) {
111
+ this.loading = loading;
112
+ this.notifyListeners();
113
+ }
114
+
115
+ setWeatherData(data, location) {
116
+ this.weatherData = data;
117
+ this.currentLocation = location;
118
+ this.error = null;
119
+ this.notifyListeners();
120
+ }
121
+
122
+ setError(error) {
123
+ this.error = error;
124
+ this.weatherData = null;
125
+ this.notifyListeners();
126
+ }
127
+
128
+ addListener(listener) {
129
+ this.listeners = this.listeners || [];
130
+ this.listeners.push(listener);
131
+ }
132
+
133
+ notifyListeners() {
134
+ if (this.listeners) {
135
+ this.listeners.forEach(listener => listener(this));
136
+ }
137
+ }
138
+ }
139
+
140
+ // Initialize global state
141
+ const weatherState = new WeatherState();
142
+
143
+ // Event handlers
144
+ document.addEventListener('DOMContentLoaded', function() {
145
+ // Initialize with current location weather
146
+ loadCurrentLocationWeather();
147
+ });
148
+
149
+ async function loadCurrentLocationWeather() {
150
+ weatherState.setLoading(true);
151
+ try {
152
+ if (navigator.geolocation) {
153
+ navigator.geolocation.getCurrentPosition(
154
+ async (position) => {
155
+ const { latitude, longitude } = position.coords;
156
+ const data = await WeatherService.getWeatherData(latitude, longitude);
157
+ weatherState.setWeatherData(data, 'Current Location');
158
+ },
159
+ async (error) => {
160
+ // Fallback to default location
161
+ const data = await WeatherService.getLocationWeather('New York');
162
+ weatherState.setWeatherData(data, 'New York');
163
+ }
164
+ );
165
+ } else {
166
+ // Fallback to default location
167
+ const data = await WeatherService.getLocationWeather('New York');
168
+ weatherState.setWeatherData(data, 'New York');
169
+ }
170
+ } catch (error) {
171
+ weatherState.setError('Failed to load weather data');
172
+ }
173
+ }
174
+
175
+ async function searchLocation(city) {
176
+ if (!city.trim()) return;
177
+
178
+ weatherState.setLoading(true);
179
+ try {
180
+ const data = await WeatherService.getLocationWeather(city);
181
+ weatherState.setWeatherData(data, city);
182
+ } catch (error) {
183
+ weatherState.setError('Location not found');
184
+ }
185
+ }
186
+
187
+ // Export for use in components
188
+ window.WeatherState = weatherState;
189
+ window.WeatherUtils = WeatherUtils;
190
+ window.searchLocation = searchLocation;
191
+ window.loadCurrentLocationWeather = loadCurrentLocationWeather;
style.css CHANGED
@@ -1,28 +1,99 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  }
5
 
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
  }
10
 
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
  }
17
 
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
  }
25
 
26
- .card p:last-child {
27
- margin-bottom: 0;
 
28
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
2
+
3
+ * {
4
+ font-family: 'Inter', sans-serif;
5
+ }
6
+
7
+ .weather-card {
8
+ backdrop-filter: blur(10px);
9
+ background: rgba(255, 255, 255, 0.85);
10
+ border: 1px solid rgba(255, 255, 255, 0.2);
11
+ }
12
+
13
+ .weather-gradient {
14
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
15
+ }
16
+
17
+ .animate-pulse-soft {
18
+ animation: pulse-soft 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
19
+ }
20
+
21
+ @keyframes pulse-soft {
22
+ 0%, 100% {
23
+ opacity: 1;
24
+ }
25
+ 50% {
26
+ opacity: 0.8;
27
+ }
28
+ }
29
+
30
+ .weather-icon {
31
+ filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));
32
+ }
33
+
34
+ .loading-dots {
35
+ display: inline-block;
36
+ position: relative;
37
+ width: 80px;
38
+ height: 80px;
39
+ }
40
+
41
+ .loading-dots div {
42
+ position: absolute;
43
+ top: 33px;
44
+ width: 13px;
45
+ height: 13px;
46
+ border-radius: 50%;
47
+ background: #0ea5e9;
48
+ animation-timing-function: cubic-bezier(0, 1, 1, 0);
49
+ }
50
+
51
+ .loading-dots div:nth-child(1) {
52
+ left: 8px;
53
+ animation: loading-dots1 0.6s infinite;
54
+ }
55
+
56
+ .loading-dots div:nth-child(2) {
57
+ left: 8px;
58
+ animation: loading-dots2 0.6s infinite;
59
+ }
60
+
61
+ .loading-dots div:nth-child(3) {
62
+ left: 32px;
63
+ animation: loading-dots2 0.6s infinite;
64
  }
65
 
66
+ .loading-dots div:nth-child(4) {
67
+ left: 56px;
68
+ animation: loading-dots3 0.6s infinite;
69
  }
70
 
71
+ @keyframes loading-dots1 {
72
+ 0% { transform: scale(0); }
73
+ 100% { transform: scale(1); }
 
 
74
  }
75
 
76
+ @keyframes loading-dots3 {
77
+ 0% { transform: scale(1); }
78
+ 100% { transform: scale(0); }
 
 
 
79
  }
80
 
81
+ @keyframes loading-dots2 {
82
+ 0% { transform: translate(0, 0); }
83
+ 100% { transform: translate(24px, 0); }
84
  }
85
+
86
+ .slide-up {
87
+ animation: slideUp 0.3s ease-out;
88
+ }
89
+
90
+ @keyframes slideUp {
91
+ from {
92
+ opacity: 0;
93
+ transform: translateY(20px);
94
+ }
95
+ to {
96
+ opacity: 1;
97
+ transform: translateY(0);
98
+ }
99
+ }