users/ryan/modules/simple-bar/simple-bar-source/lib/components/data/weather.jsx

6ad98695409ab789b2b3b15c95de404daf46d1a7 · 6.5 KB · 214 lines raw

1 import * as Uebersicht from "uebersicht";
2 import * as DataWidget from "./data-widget.jsx";
3 import * as DataWidgetLoader from "./data-widget-loader.jsx";
4 import * as Icons from "../icons/icons.jsx";
5 import * as Utils from "../../utils";
6 import useWidgetRefresh from "../../hooks/use-widget-refresh";
7 import useServerSocket from "../../hooks/use-server-socket";
8 import { useSimpleBarContext } from "../simple-bar-context.jsx";
9
10 export { weatherStyles as styles } from "../../styles/components/data/weather";
11
12 const { React } = Uebersicht;
13
14 const DEFAULT_REFRESH_FREQUENCY = 1000 * 60 * 30; // Default refresh frequency set to 30 minutes
15
16 /**
17 * Weather widget component
18 */
19 export const Widget = React.memo(() => {
20 const { displayIndex, settings, pushMissive } = useSimpleBarContext();
21 const { widgets, weatherWidgetOptions } = settings;
22 const { weatherWidget } = widgets;
23 const {
24 refreshFrequency,
25 customLocation,
26 unit,
27 hideLocation,
28 hideGradient,
29 showOnDisplay,
30 showIcon,
31 } = weatherWidgetOptions;
32
33 const refresh = React.useMemo(
34 () =>
35 Utils.getRefreshFrequency(refreshFrequency, DEFAULT_REFRESH_FREQUENCY),
36 [refreshFrequency],
37 );
38
39 const visible =
40 Utils.isVisibleOnDisplay(displayIndex, showOnDisplay) && weatherWidget;
41
42 const [state, setState] = React.useState();
43 const [loading, setLoading] = React.useState(visible);
44 const location = React.useRef(
45 visible && customLocation.length ? customLocation : undefined,
46 );
47
48 /**
49 * Resets the widget state and loading status
50 */
51 const resetWidget = () => {
52 setState(undefined);
53 setLoading(false);
54 };
55
56 /**
57 * Fetches weather data from wttr.in
58 */
59 const getWeather = React.useCallback(async () => {
60 if (!visible) return;
61 if (!location.current) {
62 const position = await Promise.race([getPosition(), Utils.timeout(5000)]);
63 if (!position) await getWeather();
64 const { city, zip } = position?.address || {};
65 location.current = zip || city;
66 if (!location.current) return setLoading(false);
67 }
68 try {
69 const result = await fetch(
70 `https://wttr.in/${location.current}?format=j1`,
71 );
72 const data = await result.json();
73 setState({ location: location.current, data });
74 } catch {
75 // eslint-disable-next-line no-console
76 console.error("Error while fetching weather")
77 }
78 setLoading(false);
79 }, [visible, location]);
80
81 useServerSocket("weather", visible, getWeather, resetWidget, setLoading);
82 useWidgetRefresh(visible, getWeather, refresh);
83
84 if (loading) return <DataWidgetLoader.Widget className="weather" />;
85 if (!state || !state.data.current_condition) return null;
86
87 const {
88 temp_C: tempC,
89 temp_F: tempF,
90 weatherDesc,
91 } = state.data.current_condition[0];
92 const temperature = unit === "C" ? tempC : tempF;
93 const wttrUnitParam = unit === "C" ? "?m" : "?u";
94
95 const description = weatherDesc[0].value;
96
97 const { astronomy } = state.data.weather[0];
98 const sunriseData = astronomy[0].sunrise.replace(" AM", "").split(":");
99 const sunsetData = astronomy[0].sunset.replace(" PM", "").split(":");
100
101 const now = new Date();
102 const nowIntervalStart = new Date();
103 nowIntervalStart.setHours(nowIntervalStart.getHours() - 1);
104 const nowIntervalStop = new Date();
105 nowIntervalStop.setHours(nowIntervalStop.getHours() + 1);
106 const sunriseTime = new Date();
107 sunriseTime.setHours(
108 parseInt(sunriseData[0], 10),
109 parseInt(sunriseData[1], 10),
110 0,
111 0,
112 );
113 const sunsetTime = new Date();
114 sunsetTime.setHours(
115 parseInt(sunsetData[0], 10) + 12,
116 parseInt(sunsetData[1], 10),
117 0,
118 0,
119 );
120
121 const atNight = sunriseTime >= now || now >= sunsetTime;
122
123 const Icon = getIcon(description, atNight);
124 const label = getLabel(state.location, temperature, unit, hideLocation);
125
126 const sunRising =
127 sunriseTime >= nowIntervalStart && sunriseTime <= nowIntervalStop;
128 const sunSetting =
129 sunsetTime >= nowIntervalStart && sunsetTime <= nowIntervalStop;
130
131 /**
132 * Handles right-click event to refresh weather data
133 * @param {Event} e - The event object
134 */
135 const onRightClick = (e) => {
136 Utils.clickEffect(e);
137 setLoading(true);
138 getWeather();
139 Utils.notification("Refreshing forecast from wttr.in...", pushMissive);
140 };
141
142 const classes = Utils.classNames("weather", {
143 "weather--sunrise": sunRising,
144 "weather--sunset": sunSetting,
145 });
146
147 return (
148 <DataWidget.Widget
149 classes={classes}
150 Icon={showIcon ? Icon : null}
151 href={`https://wttr.in/${state.location}${wttrUnitParam}`}
152 onClick={(e) => openWeather(e, pushMissive)}
153 onRightClick={onRightClick}
154 disableSlider
155 >
156 {!hideGradient && <div className="weather__gradient" />}
157 {label}
158 </DataWidget.Widget>
159 );
160 });
161
162 Widget.displayName = "Weather";
163
164 /**
165 * Returns the appropriate weather icon based on the description and time of day
166 * @param {string} description - Weather description
167 * @param {boolean} atNight - Whether it is currently night time
168 * @returns {JSX.Element} - The weather icon component
169 */
170 function getIcon(description, atNight) {
171 if (description.includes("fog") || description.includes("mist")) {
172 return Icons.Fog;
173 }
174 if (description.includes("storm")) return Icons.Storm;
175 if (description.includes("snow")) return Icons.Snow;
176 if (description.includes("rain")) return Icons.Rain;
177 if (description.includes("cloud")) return Icons.Cloud;
178 if (atNight) return Icons.Moon;
179 return Icons.Sun;
180 }
181
182 /**
183 * Returns the label for the weather widget
184 * @param {string} location - The location name
185 * @param {string} temperature - The temperature value
186 * @param {string} unit - The temperature unit (C or F)
187 * @param {boolean} hideLocation - Whether to hide the location name
188 * @returns {string} - The label text
189 */
190 function getLabel(location, temperature, unit, hideLocation) {
191 if (!location) return "Fetching...";
192 if (hideLocation) return `${temperature}°${unit}`;
193 return `${location}, ${temperature}°${unit}`;
194 }
195
196 /**
197 * Opens the weather forecast in a new tab
198 * @param {Event} e - The event object
199 * @param {Function} pushMissive - Function to push notifications
200 */
201 function openWeather(e, pushMissive) {
202 Utils.clickEffect(e);
203 Utils.notification("Opening forecast from wttr.in...", pushMissive);
204 }
205
206 /**
207 * Gets the current geographical position of the user
208 * @returns {Promise<GeolocationPosition>} - The position object
209 */
210 async function getPosition() {
211 return new Promise((resolve) =>
212 navigator.geolocation.getCurrentPosition(resolve),
213 );
214 }