Skip to content

Commit ac473d1

Browse files
weather ui and features
1 parent 4868e31 commit ac473d1

3 files changed

Lines changed: 324 additions & 16 deletions

File tree

projects/weather/index.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@
1111
<body>
1212
<main>
1313
<h1>Weather</h1>
14-
<form id="form"><input id="city" placeholder="Enter city" required> <button>Get</button></form>
14+
<form id="form">
15+
<div class="controls">
16+
<input id="city" placeholder="Enter city" required>
17+
<button type="submit">Get</button>
18+
<button type="button" class="btn-secondary btn-icon" id="geoBtn" title="Use my location">📍</button>
19+
<button type="button" class="btn-secondary btn-icon" id="unitBtn" title="Toggle units">°C</button>
20+
</div>
21+
</form>
1522
<div id="out"></div>
16-
<p class="notes">Use a public API; add icons, units, and caching.</p>
23+
<p class="notes">Weather data cached for 30 minutes. Click 📍 for your location.</p>
1724
</main>
1825
<script type="module" src="./main.js"></script>
1926
</body>

projects/weather/main.js

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,185 @@
1-
const form = document.getElementById('form'); const city = document.getElementById('city'); const out = document.getElementById('out');
1+
const form = document.getElementById('form');
2+
const city = document.getElementById('city');
3+
const out = document.getElementById('out');
4+
const geoBtn = document.getElementById('geoBtn');
5+
const unitBtn = document.getElementById('unitBtn');
6+
7+
// State
8+
let unit = 'C'; // C or F
9+
const cache = {};
10+
const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes
11+
12+
// Weather icons mapping
13+
const weatherIcons = {
14+
'Sunny': '☀️',
15+
'Clear': '🌙',
16+
'Partly cloudy': '⛅',
17+
'Cloudy': '☁️',
18+
'Overcast': '☁️',
19+
'Mist': '🌫️',
20+
'Fog': '🌫️',
21+
'Light rain': '🌦️',
22+
'Moderate rain': '🌧️',
23+
'Heavy rain': '🌧️',
24+
'Light snow': '🌨️',
25+
'Moderate snow': '❄️',
26+
'Heavy snow': '❄️',
27+
'Thunderstorm': '⛈️',
28+
'default': '🌤️'
29+
};
30+
31+
function getWeatherIcon(desc) {
32+
for (const [key, icon] of Object.entries(weatherIcons)) {
33+
if (desc.includes(key)) return icon;
34+
}
35+
return weatherIcons.default;
36+
}
37+
38+
function getCacheKey(cityName) {
39+
return `weather_${cityName.toLowerCase()}`;
40+
}
41+
42+
function isCacheValid(timestamp) {
43+
return Date.now() - timestamp < CACHE_DURATION;
44+
}
45+
46+
function convertTemp(tempC) {
47+
if (unit === 'F') {
48+
return Math.round(tempC * 9 / 5 + 32);
49+
}
50+
return tempC;
51+
}
52+
53+
function displayWeather(data, cityName) {
54+
const cur = data.current_condition?.[0];
55+
if (!cur) {
56+
out.innerHTML = '<div class="error">No weather data available</div>';
57+
return;
58+
}
59+
60+
const desc = cur.weatherDesc?.[0]?.value || 'Unknown';
61+
const icon = getWeatherIcon(desc);
62+
const temp = convertTemp(parseInt(cur.temp_C));
63+
const feelsLike = convertTemp(parseInt(cur.FeelsLikeC));
64+
const unitSymbol = unit === 'C' ? '°C' : '°F';
65+
66+
const cacheKey = getCacheKey(cityName);
67+
const cacheTime = cache[cacheKey]?.timestamp
68+
? new Date(cache[cacheKey].timestamp).toLocaleTimeString()
69+
: 'just now';
70+
71+
out.innerHTML = `
72+
<div class="weather-card">
73+
<div class="weather-header">
74+
<div>
75+
<h2 class="weather-city">${cityName}</h2>
76+
<p class="weather-desc">${desc}</p>
77+
</div>
78+
<div class="weather-icon">${icon}</div>
79+
</div>
80+
<div class="weather-temp">${temp}${unitSymbol}</div>
81+
<div class="weather-details">
82+
<div class="detail-item">
83+
<span class="detail-label">Feels like</span>
84+
<span class="detail-value">${feelsLike}${unitSymbol}</span>
85+
</div>
86+
<div class="detail-item">
87+
<span class="detail-label">Humidity</span>
88+
<span class="detail-value">${cur.humidity}%</span>
89+
</div>
90+
<div class="detail-item">
91+
<span class="detail-label">Wind</span>
92+
<span class="detail-value">${cur.windspeedKmph} km/h</span>
93+
</div>
94+
<div class="detail-item">
95+
<span class="detail-label">Pressure</span>
96+
<span class="detail-value">${cur.pressure} mb</span>
97+
</div>
98+
</div>
99+
<div class="cache-info">Last updated: ${cacheTime}</div>
100+
</div>
101+
`;
102+
}
103+
104+
async function fetchWeather(cityName) {
105+
const cacheKey = getCacheKey(cityName);
106+
107+
// Check cache
108+
if (cache[cacheKey] && isCacheValid(cache[cacheKey].timestamp)) {
109+
displayWeather(cache[cacheKey].data, cityName);
110+
return;
111+
}
112+
113+
out.innerHTML = '<div class="loading">Loading weather data...</div>';
114+
115+
try {
116+
const q = encodeURIComponent(cityName);
117+
const res = await fetch(`https://wttr.in/${q}?format=j1`);
118+
119+
if (!res.ok) {
120+
throw new Error(`Failed to fetch: ${res.status}`);
121+
}
122+
123+
const data = await res.json();
124+
125+
// Cache the result
126+
cache[cacheKey] = {
127+
data: data,
128+
timestamp: Date.now()
129+
};
130+
131+
displayWeather(data, cityName);
132+
} catch (err) {
133+
out.innerHTML = `<div class="error">Failed to fetch weather data. Please check the city name and try again.</div>`;
134+
console.error(err);
135+
}
136+
}
137+
138+
// Form submit handler
2139
form.addEventListener('submit', async e => {
3-
e.preventDefault(); out.textContent = 'Loading...'; try { const q = encodeURIComponent(city.value); const res = await fetch(`https://wttr.in/${q}?format=j1`); const data = await res.json(); const cur = data.current_condition?.[0]; out.innerHTML = cur ? `<strong>${city.value}</strong>: ${cur.temp_C}°C, ${cur.weatherDesc?.[0]?.value}` : 'No data'; } catch (err) { out.textContent = 'Failed to fetch weather'; }
140+
e.preventDefault();
141+
const cityName = city.value.trim();
142+
if (cityName) {
143+
await fetchWeather(cityName);
144+
}
4145
});
5-
// TODOs: use icons; toggle units; cache; geolocation; error states
146+
147+
// Geolocation handler
148+
geoBtn.addEventListener('click', async () => {
149+
if (!navigator.geolocation) {
150+
out.innerHTML = '<div class="error">Geolocation is not supported by your browser</div>';
151+
return;
152+
}
153+
154+
geoBtn.disabled = true;
155+
out.innerHTML = '<div class="loading">Getting your location...</div>';
156+
157+
navigator.geolocation.getCurrentPosition(
158+
async (position) => {
159+
const { latitude, longitude } = position.coords;
160+
const cityName = `${latitude},${longitude}`;
161+
city.value = 'My Location';
162+
await fetchWeather(cityName);
163+
geoBtn.disabled = false;
164+
},
165+
(error) => {
166+
out.innerHTML = `<div class="error">Unable to get location: ${error.message}</div>`;
167+
geoBtn.disabled = false;
168+
}
169+
);
170+
});
171+
172+
// Unit toggle handler
173+
unitBtn.addEventListener('click', () => {
174+
unit = unit === 'C' ? 'F' : 'C';
175+
unitBtn.textContent = ${unit}`;
176+
177+
// Re-render current weather if exists
178+
const currentCity = city.value.trim();
179+
if (currentCity) {
180+
const cacheKey = getCacheKey(currentCity);
181+
if (cache[cacheKey]) {
182+
displayWeather(cache[cacheKey].data, currentCity);
183+
}
184+
}
185+
});

projects/weather/styles.css

Lines changed: 132 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,163 @@ body {
55
margin: 0;
66
padding: 2rem;
77
display: grid;
8-
place-items: center
8+
place-items: center;
9+
min-height: 100vh;
910
}
1011

1112
main {
1213
max-width: 560px;
13-
width: 100%
14+
width: 100%;
15+
}
16+
17+
h1 {
18+
margin: 0 0 1.5rem;
19+
}
20+
21+
.controls {
22+
display: flex;
23+
gap: 0.5rem;
24+
margin-bottom: 1rem;
1425
}
1526

1627
input,
1728
button {
18-
font: inherit
29+
font: inherit;
1930
}
2031

2132
input {
22-
padding: .5rem .75rem;
23-
border-radius: .5rem;
33+
padding: 0.5rem 0.75rem;
34+
border-radius: 0.5rem;
2435
border: 1px solid #262631;
2536
background: #17171c;
26-
color: #eef1f8
37+
color: #eef1f8;
38+
flex: 1;
39+
}
40+
41+
input::placeholder {
42+
color: #6b7280;
2743
}
2844

2945
button {
3046
background: #6ee7b7;
3147
color: #0b1020;
3248
border: none;
33-
padding: .5rem .75rem;
34-
border-radius: .5rem;
49+
padding: 0.5rem 0.75rem;
50+
border-radius: 0.5rem;
3551
font-weight: 600;
36-
cursor: pointer
52+
cursor: pointer;
53+
transition: background 0.2s;
54+
}
55+
56+
button:hover {
57+
background: #5dd6a5;
58+
}
59+
60+
button:disabled {
61+
opacity: 0.5;
62+
cursor: not-allowed;
63+
}
64+
65+
.btn-secondary {
66+
background: #374151;
67+
color: #eef1f8;
68+
}
69+
70+
.btn-secondary:hover {
71+
background: #4b5563;
72+
}
73+
74+
.btn-icon {
75+
padding: 0.5rem;
76+
min-width: 38px;
3777
}
3878

3979
#out {
40-
margin-top: 1rem
80+
margin-top: 1.5rem;
81+
min-height: 100px;
82+
}
83+
84+
.weather-card {
85+
background: #17171c;
86+
border: 1px solid #262631;
87+
border-radius: 1rem;
88+
padding: 1.5rem;
89+
}
90+
91+
.weather-header {
92+
display: flex;
93+
justify-content: space-between;
94+
align-items: flex-start;
95+
margin-bottom: 1rem;
96+
}
97+
98+
.weather-city {
99+
font-size: 1.5rem;
100+
font-weight: 600;
101+
margin: 0;
102+
}
103+
104+
.weather-icon {
105+
font-size: 3rem;
106+
}
107+
108+
.weather-temp {
109+
font-size: 3rem;
110+
font-weight: 700;
111+
margin: 0.5rem 0;
112+
}
113+
114+
.weather-desc {
115+
font-size: 1.125rem;
116+
color: #a6adbb;
117+
margin: 0 0 1rem;
118+
}
119+
120+
.weather-details {
121+
display: grid;
122+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
123+
gap: 1rem;
124+
padding-top: 1rem;
125+
border-top: 1px solid #262631;
126+
}
127+
128+
.detail-item {
129+
display: flex;
130+
flex-direction: column;
131+
gap: 0.25rem;
132+
}
133+
134+
.detail-label {
135+
font-size: 0.875rem;
136+
color: #6b7280;
137+
}
138+
139+
.detail-value {
140+
font-size: 1.125rem;
141+
font-weight: 600;
142+
}
143+
144+
.error {
145+
background: #991b1b;
146+
color: #fecaca;
147+
padding: 1rem;
148+
border-radius: 0.5rem;
149+
}
150+
151+
.loading {
152+
text-align: center;
153+
color: #6b7280;
154+
padding: 2rem;
41155
}
42156

43157
.notes {
44158
color: #a6adbb;
45-
font-size: .9rem
159+
font-size: 0.9rem;
160+
margin-top: 1rem;
161+
}
162+
163+
.cache-info {
164+
font-size: 0.75rem;
165+
color: #6b7280;
166+
margin-top: 0.5rem;
46167
}

0 commit comments

Comments
 (0)