weather icons
This commit is contained in:
@@ -35,7 +35,13 @@ app.use(helmet({
|
|||||||
scriptSrc: ["'self'", "'unsafe-inline'", "https://maps.googleapis.com"],
|
scriptSrc: ["'self'", "'unsafe-inline'", "https://maps.googleapis.com"],
|
||||||
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
|
||||||
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
fontSrc: ["'self'", "https://fonts.gstatic.com"],
|
||||||
imgSrc: ["'self'", "data:", "https://maps.googleapis.com", "https://maps.gstatic.com"],
|
imgSrc: [
|
||||||
|
"'self'",
|
||||||
|
"data:",
|
||||||
|
"https://maps.googleapis.com",
|
||||||
|
"https://maps.gstatic.com",
|
||||||
|
"https://openweathermap.org"
|
||||||
|
],
|
||||||
connectSrc: ["'self'", "https://api.openweathermap.org"]
|
connectSrc: ["'self'", "https://api.openweathermap.org"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ router.get('/:propertyId/forecast', async (req, res, next) => {
|
|||||||
windSpeed: [],
|
windSpeed: [],
|
||||||
precipitation: 0,
|
precipitation: 0,
|
||||||
conditions: [],
|
conditions: [],
|
||||||
|
icons: [],
|
||||||
timestamps: []
|
timestamps: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -229,6 +230,7 @@ router.get('/:propertyId/forecast', async (req, res, next) => {
|
|||||||
dailyForecast[date].windSpeed.push(item.wind?.speed || 0);
|
dailyForecast[date].windSpeed.push(item.wind?.speed || 0);
|
||||||
dailyForecast[date].precipitation += item.rain?.['3h'] || 0;
|
dailyForecast[date].precipitation += item.rain?.['3h'] || 0;
|
||||||
dailyForecast[date].conditions.push(item.weather[0].description);
|
dailyForecast[date].conditions.push(item.weather[0].description);
|
||||||
|
dailyForecast[date].icons.push(item.weather[0].icon);
|
||||||
dailyForecast[date].timestamps.push(new Date(item.dt * 1000));
|
dailyForecast[date].timestamps.push(new Date(item.dt * 1000));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -246,6 +248,7 @@ router.get('/:propertyId/forecast', async (req, res, next) => {
|
|||||||
maxWindSpeed: Math.round(Math.max(...windSpeeds)),
|
maxWindSpeed: Math.round(Math.max(...windSpeeds)),
|
||||||
totalPrecipitation: Math.round(day.precipitation * 100) / 100,
|
totalPrecipitation: Math.round(day.precipitation * 100) / 100,
|
||||||
conditions: day.conditions[0], // Use first condition of the day
|
conditions: day.conditions[0], // Use first condition of the day
|
||||||
|
icon: day.icons[0],
|
||||||
timestamps: day.timestamps
|
timestamps: day.timestamps
|
||||||
};
|
};
|
||||||
}).slice(0, 5); // Limit to 5 days
|
}).slice(0, 5); // Limit to 5 days
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { propertiesAPI, weatherAPI } from '../../services/api';
|
|||||||
|
|
||||||
const Weather = () => {
|
const Weather = () => {
|
||||||
const [properties, setProperties] = useState([]);
|
const [properties, setProperties] = useState([]);
|
||||||
const [weatherMap, setWeatherMap] = useState({}); // propertyId -> weather
|
const [weatherMap, setWeatherMap] = useState({}); // propertyId -> { weather, forecast }
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
@@ -20,14 +20,26 @@ const Weather = () => {
|
|||||||
.filter(p => p.latitude && p.longitude)
|
.filter(p => p.latitude && p.longitude)
|
||||||
.map(p => p.id);
|
.map(p => p.id);
|
||||||
|
|
||||||
const results = await Promise.allSettled(ids.map(id => weatherAPI.getCurrent(id)));
|
// Current weather
|
||||||
|
const currentResults = await Promise.allSettled(ids.map(id => weatherAPI.getCurrent(id)));
|
||||||
const map = {};
|
const map = {};
|
||||||
results.forEach((r, idx) => {
|
currentResults.forEach((r, idx) => {
|
||||||
const id = ids[idx];
|
const id = ids[idx];
|
||||||
if (r.status === 'fulfilled') {
|
if (r.status === 'fulfilled') {
|
||||||
map[id] = r.value.data.data.weather;
|
map[id] = { weather: r.value.data.data.weather };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 5-day forecast
|
||||||
|
const forecastResults = await Promise.allSettled(ids.map(id => weatherAPI.getForecast(id)));
|
||||||
|
forecastResults.forEach((r, idx) => {
|
||||||
|
const id = ids[idx];
|
||||||
|
if (r.status === 'fulfilled') {
|
||||||
|
map[id] = map[id] || {};
|
||||||
|
map[id].forecast = r.value.data.data.forecast;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setWeatherMap(map);
|
setWeatherMap(map);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -58,20 +70,35 @@ const Weather = () => {
|
|||||||
</div>
|
</div>
|
||||||
{!p.latitude || !p.longitude ? (
|
{!p.latitude || !p.longitude ? (
|
||||||
<div className="text-sm text-gray-600">No coordinates set</div>
|
<div className="text-sm text-gray-600">No coordinates set</div>
|
||||||
) : weatherMap[p.id] ? (
|
) : weatherMap[p.id]?.weather ? (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<img alt="icon" src={`https://openweathermap.org/img/wn/${weatherMap[p.id].current.icon}@2x.png`} />
|
<img alt="" width="50" height="50" src={`https://openweathermap.org/img/wn/${weatherMap[p.id].weather.current.icon}@2x.png`} />
|
||||||
<div className="ml-3">
|
<div className="ml-3">
|
||||||
<div className="text-xl font-bold">{weatherMap[p.id].current.temperature}°F</div>
|
<div className="text-xl font-bold">{weatherMap[p.id].weather.current.temperature}°F</div>
|
||||||
<div className="text-sm text-gray-700">{weatherMap[p.id].current.conditions}</div>
|
<div className="text-sm text-gray-700">{weatherMap[p.id].weather.current.conditions}</div>
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
Humidity {weatherMap[p.id].current.humidity}% • Wind {Math.round(weatherMap[p.id].current.windSpeed)} mph
|
Humidity {weatherMap[p.id].weather.current.humidity}% • Wind {Math.round(weatherMap[p.id].weather.current.windSpeed)} mph
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-gray-600">Loading weather…</div>
|
<div className="text-sm text-gray-600">Loading weather…</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Forecast */}
|
||||||
|
{weatherMap[p.id]?.forecast?.daily && (
|
||||||
|
<div className="mt-3 grid grid-cols-5 gap-2 text-center">
|
||||||
|
{weatherMap[p.id].forecast.daily.map((d, i) => (
|
||||||
|
<div key={i} className="text-xs text-gray-700">
|
||||||
|
<div className="font-medium">
|
||||||
|
{new Date(d.date).toLocaleDateString(undefined, { weekday: 'short' })}
|
||||||
|
</div>
|
||||||
|
<img alt="" className="mx-auto" width="40" height="40" src={`https://openweathermap.org/img/wn/${d.icon}.png`} />
|
||||||
|
<div>{d.temperatureHigh}° / {d.temperatureLow}°</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user