Weather.tsx 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import { createSignal, onMount, onCleanup, Show } from "solid-js";
  2. import type { WidgetSettings, WidgetSchema } from "../types/widget";
  3. import { getRefreshInterval } from "../utils/timeUtils";
  4. interface WeatherProps {
  5. settings: WidgetSettings;
  6. }
  7. interface WeatherData {
  8. temp: number;
  9. description: string;
  10. humidity: number;
  11. windSpeed: number;
  12. }
  13. export function Weather(props: WeatherProps) {
  14. const [weather, setWeather] = createSignal<WeatherData | null>(null);
  15. const [error, setError] = createSignal<string>("");
  16. const fetchWeather = async () => {
  17. try {
  18. const city = (props.settings.city as string) || "London";
  19. const lat = (props.settings.lat as string) || "";
  20. const lon = (props.settings.lon as string) || "";
  21. const apiKey = (props.settings.apiKey as string) || "";
  22. const units = (props.settings.units as string) || "metric";
  23. if (!apiKey) {
  24. setError("API key required");
  25. return;
  26. }
  27. // Build URL based on whether lat/lon or city is provided
  28. let url = `/api/weather?apiKey=${encodeURIComponent(apiKey)}&units=${units}`;
  29. if (lat && lon) {
  30. url += `&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}`;
  31. } else {
  32. url += `&city=${encodeURIComponent(city)}`;
  33. }
  34. // Use our server endpoint instead of calling OpenWeatherMap directly
  35. const response = await fetch(url);
  36. if (!response.ok) {
  37. throw new Error("Failed to fetch weather");
  38. }
  39. const data = await response.json();
  40. setWeather({
  41. temp: Math.round(data.main.temp),
  42. description: data.weather[0].description,
  43. humidity: data.main.humidity,
  44. windSpeed: data.wind.speed,
  45. });
  46. setError("");
  47. } catch (err) {
  48. setError("Error fetching weather");
  49. console.error(err);
  50. }
  51. };
  52. onMount(() => {
  53. fetchWeather();
  54. const refreshInterval = getRefreshInterval(props.settings, 10, "minutes");
  55. const interval = setInterval(fetchWeather, refreshInterval);
  56. onCleanup(() => clearInterval(interval));
  57. });
  58. return (
  59. <div
  60. style={{
  61. border: "1px solid var(--border)",
  62. padding: "1rem",
  63. width: "100%",
  64. height: "100%",
  65. "box-sizing": "border-box",
  66. display: "flex",
  67. "flex-direction": "column",
  68. "justify-content": "center",
  69. "align-items": "center",
  70. background: "var(--bg, #fff)",
  71. }}
  72. >
  73. <Show when={error()}>
  74. <div
  75. style={{
  76. color: "#ff4444",
  77. "font-size": "clamp(0.7rem, 2vw + 0.5vh, 1rem)",
  78. "text-align": "center",
  79. }}
  80. >
  81. {error()}
  82. </div>
  83. </Show>
  84. <Show when={!error() && weather()}>
  85. <div
  86. style={{
  87. "font-size": "clamp(0.7rem, 2vw + 0.5vh, 1.2rem)",
  88. "font-weight": "bold",
  89. "margin-bottom": "0.5rem",
  90. "text-align": "center",
  91. }}
  92. >
  93. {props.settings.lat && props.settings.lon
  94. ? `${props.settings.lat}, ${props.settings.lon}`
  95. : props.settings.city || "London"}
  96. </div>
  97. <div
  98. style={{
  99. "font-size": "clamp(1.5rem, 5vw + 1vh, 3.5rem)",
  100. "font-weight": "bold",
  101. "text-align": "center",
  102. "line-height": "1.1",
  103. }}
  104. >
  105. {weather()?.temp}°
  106. {(props.settings.units as string) === "metric" ? "C" : "F"}
  107. </div>
  108. <div
  109. style={{
  110. "font-size": "clamp(0.7rem, 1.8vw + 0.5vh, 1.1rem)",
  111. "text-align": "center",
  112. "margin-top": "0.3rem",
  113. "text-transform": "capitalize",
  114. color: "var(--gray)",
  115. }}
  116. >
  117. {weather()?.description}
  118. </div>
  119. <Show when={props.settings.showDetails}>
  120. <div
  121. style={{
  122. display: "flex",
  123. gap: "1rem",
  124. "margin-top": "0.8rem",
  125. "font-size": "clamp(0.6rem, 1.5vw + 0.5vh, 0.9rem)",
  126. color: "var(--gray)",
  127. }}
  128. >
  129. <div>💧 {weather()?.humidity}%</div>
  130. <div>💨 {weather()?.windSpeed} m/s</div>
  131. </div>
  132. </Show>
  133. </Show>
  134. </div>
  135. );
  136. }
  137. export const weatherSchema: WidgetSchema = {
  138. name: "Weather",
  139. description: "Display current weather from OpenWeatherMap API",
  140. settingsSchema: {
  141. city: {
  142. type: "string",
  143. label: "City Name",
  144. default: "London",
  145. required: false,
  146. },
  147. lat: {
  148. type: "string",
  149. label: "Latitude (optional, overrides city)",
  150. default: "",
  151. required: false,
  152. },
  153. lon: {
  154. type: "string",
  155. label: "Longitude (optional, overrides city)",
  156. default: "",
  157. required: false,
  158. },
  159. apiKey: {
  160. type: "string",
  161. label: "OpenWeatherMap API Key",
  162. default: "",
  163. required: true,
  164. },
  165. units: {
  166. type: "select",
  167. label: "Temperature Units",
  168. default: "metric",
  169. options: ["metric", "imperial"],
  170. },
  171. showDetails: {
  172. type: "boolean",
  173. label: "Show Humidity & Wind",
  174. default: true,
  175. },
  176. refreshIntervalValue: {
  177. type: "number",
  178. label: "Refresh Interval",
  179. default: 10,
  180. required: true,
  181. },
  182. refreshIntervalUnit: {
  183. type: "select",
  184. label: "Unit",
  185. options: ["seconds", "minutes", "hours", "days"],
  186. default: "minutes",
  187. required: true,
  188. },
  189. },
  190. };