add weather module

This commit is contained in:
2025-08-29 13:15:38 +02:00
parent b27be08b87
commit 69f46059e0
12 changed files with 527 additions and 9 deletions

34
src/api/weather.ts Normal file
View File

@@ -0,0 +1,34 @@
import { fetchWeatherApi } from "openmeteo";
const params = {
latitude: 49.0094,
longitude: 8.4044,
daily: ["temperature_2m_max", "temperature_2m_min"],
hourly: [
"temperature_2m",
"precipitation",
"rain",
"precipitation_probability",
],
current: [
"temperature_2m",
"precipitation",
"rain",
"showers",
"snowfall",
"relative_humidity_2m",
"apparent_temperature",
"weather_code",
"cloud_cover",
"is_day",
],
timezone: "Europe/Berlin",
timeformat: "unixtime",
};
const url = "https://api.open-meteo.com/v1/forecast";
function fetchWeatherData() {
return fetchWeatherApi(url, params);
}
export default fetchWeatherData;

View File

@@ -1,5 +1,11 @@
import style from "./style.module.css";
export default function Card({ active, children }) {
export default function Card({
active,
children,
}: {
active?: boolean;
children: React.ReactNode;
}) {
return <div className={style.card}>{children}</div>;
}

View File

@@ -3,7 +3,7 @@ import style from "./style.module.css";
export default function CardHeader({ icon, content, active = false }) {
let containerClass = style.container;
if (active) {
containerClass += " " + style.active;
containerClass += ` ${style.active}`;
}
return (
<div className={containerClass}>

View File

@@ -7,7 +7,7 @@ import Flatastic from "@/components/Flatastic/Flatastic";
import Footer from "@/components/Footer/Footer";
import Terminal from "@/components/Terminal/Terminal";
import Timetable from "@/components/Timetable/Timetable";
import Weather from "../Weather/Weather";
import style from "./style.module.css";
export default function Dashboard() {
@@ -43,14 +43,18 @@ export default function Dashboard() {
<CardHeader icon="🚊" content="Timetable" />
<Timetable />
</Card>
<div className={style.small}>
<div className={style.clockAndWeather}>
<div className={style.small}>
<Card>
<CardHeader icon="🕐" content="Clock" />
<Datetime />
</Card>
</div>
<Card>
<CardHeader icon="🕐" content="Clock" />
<Datetime />
<CardHeader icon="🌤️" content="Weather" />
<Weather />
</Card>
</div>
<Card>
<CardHeader icon="🔔" content="Terminal" active={true} />
<Terminal />
@@ -61,7 +65,6 @@ export default function Dashboard() {
<Flatastic />
</Card>
</div>
<div className={style.footer}>
<Footer />
</div>

View File

@@ -20,6 +20,11 @@
background-color: #2a3f55;
}
.clockAndWeather {
display: flex;
align-items: center;
}
.cardWrapper {
margin: 30px;
height: 100%;

View File

@@ -9,10 +9,12 @@ export default function Flatastic() {
const fetchFlatasticData = useFlatasticStore((state) => state.fetch);
const flatasticData = useFlatasticStore((state) => state.flatasticData);
const chores = (flatasticData?.chores as FlatasticChore[]) || [];
chores.sort(
(a, b) =>
a.timeLeftNext - b.timeLeftNext && b.rotationTime - a.rotationTime,
);
const users = flatasticData?.users;
const idToNameMap: Record<number, string> = {};
users.forEach((user: FlatasticUser) => {

View File

@@ -0,0 +1,41 @@
import { useEffect } from "react";
import { useWeatherStore } from "@/store/weather";
import styles from "./style.module.css";
export default function Weather() {
const weatherData = useWeatherStore((state) => state.weatherData);
const fetchWeatherData = useWeatherStore((state) => state.fetchWeatherData);
useEffect(() => {
fetchWeatherData();
const interval = setInterval(() => {
fetchWeatherData();
}, 10 * 60000);
return () => clearInterval(interval);
}, [fetchWeatherData]);
if (!weatherData.current) {
return <div>Loading...</div>;
}
return (
<div className={styles.weatherContainer}>
<div className={styles.currentTemperature}>
<img
src={weatherData.current.icon}
alt={weatherData.current.weather_description}
/>
<span>{weatherData.current.temperature_2m.toFixed(1)}°C</span>
</div>
<div className={styles.dailyTemperatures}>
<span className={styles.minTemperature}>
{weatherData.daily.temperature_2m_min[0].toFixed(1)}°C
</span>
<span className={styles.maxTemperature}>
{weatherData.daily.temperature_2m_max[0].toFixed(1)}°C
</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,30 @@
.weatherContainer {
display: flex;
flex-direction: row;
width: 100%;
padding: 3px;
padding-right: 15px;
}
.currentTemperature {
display: flex;
align-items: center;
font-size: 2rem;
font-weight: bold;
padding-right: 5px;
}
.dailyTemperatures {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.min-temperature {
color: blue;
}
.max-temperature {
color: red;
}

389
src/store/weather.ts Normal file
View File

@@ -0,0 +1,389 @@
import { create } from "zustand";
import { devtools } from "zustand/middleware";
import fetchWeatherData from "@/api/weather";
const iconNumberToPng = {
"0": {
day: {
description: "Sunny",
image: "http://openweathermap.org/img/wn/01d@2x.png",
},
night: {
description: "Clear",
image: "http://openweathermap.org/img/wn/01n@2x.png",
},
},
"1": {
day: {
description: "Mainly Sunny",
image: "http://openweathermap.org/img/wn/01d@2x.png",
},
night: {
description: "Mainly Clear",
image: "http://openweathermap.org/img/wn/01n@2x.png",
},
},
"2": {
day: {
description: "Partly Cloudy",
image: "http://openweathermap.org/img/wn/02d@2x.png",
},
night: {
description: "Partly Cloudy",
image: "http://openweathermap.org/img/wn/02n@2x.png",
},
},
"3": {
day: {
description: "Cloudy",
image: "http://openweathermap.org/img/wn/03d@2x.png",
},
night: {
description: "Cloudy",
image: "http://openweathermap.org/img/wn/03n@2x.png",
},
},
"45": {
day: {
description: "Foggy",
image: "http://openweathermap.org/img/wn/50d@2x.png",
},
night: {
description: "Foggy",
image: "http://openweathermap.org/img/wn/50n@2x.png",
},
},
"48": {
day: {
description: "Rime Fog",
image: "http://openweathermap.org/img/wn/50d@2x.png",
},
night: {
description: "Rime Fog",
image: "http://openweathermap.org/img/wn/50n@2x.png",
},
},
"51": {
day: {
description: "Light Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Light Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"53": {
day: {
description: "Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"55": {
day: {
description: "Heavy Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Heavy Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"56": {
day: {
description: "Light Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Light Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"57": {
day: {
description: "Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Freezing Drizzle",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"61": {
day: {
description: "Light Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png",
},
night: {
description: "Light Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png",
},
},
"63": {
day: {
description: "Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png",
},
night: {
description: "Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png",
},
},
"65": {
day: {
description: "Heavy Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png",
},
night: {
description: "Heavy Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png",
},
},
"66": {
day: {
description: "Light Freezing Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png",
},
night: {
description: "Light Freezing Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png",
},
},
"67": {
day: {
description: "Freezing Rain",
image: "http://openweathermap.org/img/wn/10d@2x.png",
},
night: {
description: "Freezing Rain",
image: "http://openweathermap.org/img/wn/10n@2x.png",
},
},
"71": {
day: {
description: "Light Snow",
image: "http://openweathermap.org/img/wn/13d@2x.png",
},
night: {
description: "Light Snow",
image: "http://openweathermap.org/img/wn/13n@2x.png",
},
},
"73": {
day: {
description: "Snow",
image: "http://openweathermap.org/img/wn/13d@2x.png",
},
night: {
description: "Snow",
image: "http://openweathermap.org/img/wn/13n@2x.png",
},
},
"75": {
day: {
description: "Heavy Snow",
image: "http://openweathermap.org/img/wn/13d@2x.png",
},
night: {
description: "Heavy Snow",
image: "http://openweathermap.org/img/wn/13n@2x.png",
},
},
"77": {
day: {
description: "Snow Grains",
image: "http://openweathermap.org/img/wn/13d@2x.png",
},
night: {
description: "Snow Grains",
image: "http://openweathermap.org/img/wn/13n@2x.png",
},
},
"80": {
day: {
description: "Light Showers",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Light Showers",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"81": {
day: {
description: "Showers",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Showers",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"82": {
day: {
description: "Heavy Showers",
image: "http://openweathermap.org/img/wn/09d@2x.png",
},
night: {
description: "Heavy Showers",
image: "http://openweathermap.org/img/wn/09n@2x.png",
},
},
"85": {
day: {
description: "Light Snow Showers",
image: "http://openweathermap.org/img/wn/13d@2x.png",
},
night: {
description: "Light Snow Showers",
image: "http://openweathermap.org/img/wn/13n@2x.png",
},
},
"86": {
day: {
description: "Snow Showers",
image: "http://openweathermap.org/img/wn/13d@2x.png",
},
night: {
description: "Snow Showers",
image: "http://openweathermap.org/img/wn/13n@2x.png",
},
},
"95": {
day: {
description: "Thunderstorm",
image: "http://openweathermap.org/img/wn/11d@2x.png",
},
night: {
description: "Thunderstorm",
image: "http://openweathermap.org/img/wn/11n@2x.png",
},
},
"96": {
day: {
description: "Light Thunderstorms With Hail",
image: "http://openweathermap.org/img/wn/11d@2x.png",
},
night: {
description: "Light Thunderstorms With Hail",
image: "http://openweathermap.org/img/wn/11n@2x.png",
},
},
"99": {
day: {
description: "Thunderstorm With Hail",
image: "http://openweathermap.org/img/wn/11d@2x.png",
},
night: {
description: "Thunderstorm With Hail",
image: "http://openweathermap.org/img/wn/11n@2x.png",
},
},
};
const useWeatherStore = create(
devtools(
(set) => ({
weatherData: {},
fetchWeatherData: async () => {
const data = await fetchWeatherData();
// Process first location. Add a for-loop for multiple locations or weather models
const response = data[0];
if (response === null) {
console.error("Failed to fetch weather data");
return;
}
// Attributes for timezone and location
const utcOffsetSeconds = response.utcOffsetSeconds();
const current = response.current();
const hourly = response.hourly();
const daily = response.daily();
if (!current || !hourly || !daily) {
console.error("Failed to fetch weather data");
return;
}
// Note: The order of weather variables in the URL query and the indices below need to match!
const weatherData = {
current: {
time: current.time(),
temperature_2m: current.variables(0)?.value(),
precipitation: current.variables(1)?.value(),
rain: current.variables(2)?.value(),
showers: current.variables(3)?.value(),
snowfall: current.variables(4)?.value(),
relative_humidity_2m: current.variables(5)?.value(),
apparent_temperature: current.variables(6)?.value(),
weather_code: current.variables(7)?.value(),
cloud_cover: current.variables(8)?.value(),
is_day: current.variables(9)?.value(),
},
hourly: {
time: [
...Array(
(Number(hourly.timeEnd()) -
Number(hourly.time())) /
hourly.interval(),
),
].map(
(_, i) =>
new Date(
(Number(hourly.time()) +
i * hourly.interval() +
utcOffsetSeconds) *
1000,
),
),
temperature_2m: hourly.variables(0)?.valuesArray(),
precipitation: hourly.variables(1)?.valuesArray(),
rain: hourly.variables(2)?.valuesArray(),
precipitation_probability: hourly
.variables(3)
?.valuesArray(),
},
daily: {
time: [
...Array(
(Number(daily.timeEnd()) -
Number(daily.time())) /
daily.interval(),
),
].map(
(_, i) =>
new Date(
(Number(daily.time()) +
i * daily.interval() +
utcOffsetSeconds) *
1000,
),
),
temperature_2m_max: daily.variables(0)?.valuesArray(),
temperature_2m_min: daily.variables(1)?.valuesArray(),
},
};
const isDay = weatherData.current.is_day === 1;
const weatherCode = weatherData.current.weather_code;
const url =
iconNumberToPng[weatherCode][isDay ? "day" : "night"].image;
weatherData.current = { ...weatherData.current, icon: url };
set({ weatherData });
},
}),
{
name: "weather-store",
},
),
);
export { useWeatherStore };