From 5250d5ebbbceb0e6c8e3fb75bd8a8d30c476a02c Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Mon, 9 Sep 2024 20:31:38 +0100 Subject: [PATCH] Use timelines API to reduce requests from three to one (#28) * First steps * Only cache the API request * Disable other views * realtime -> current * Log * Now complete * Hourly and daily home * Update hourly and daily * Fix import * Make nullish * Clouds nullish --- README.md | 2 +- src/app/_components/forecast-daily.tsx | 6 +- src/app/_components/forecast-hourly.tsx | 22 +- src/app/_components/forecast-now.tsx | 14 +- src/app/daily/_components/daily-charts.tsx | 7 +- src/app/hourly/_components/hourly-charts.tsx | 7 +- src/components/location-form.tsx | 2 +- src/components/weather-icon.tsx | 4 +- src/lib/local-storage.ts | 6 +- src/lib/{schema.ts => schemas/location.ts} | 0 src/lib/schemas/tomorrow-io.ts | 171 ++++++++ src/lib/schemas/weather.ts | 318 ++++++++++++++ src/lib/serverActions/tomorrow-io.ts | 305 +++++++++---- src/lib/tomorrowio/weather-codes.ts | 2 +- src/lib/types/tomorrow-io.ts | 429 ------------------- src/lib/utils.ts | 20 + 16 files changed, 752 insertions(+), 563 deletions(-) rename src/lib/{schema.ts => schemas/location.ts} (100%) create mode 100644 src/lib/schemas/tomorrow-io.ts create mode 100644 src/lib/schemas/weather.ts delete mode 100644 src/lib/types/tomorrow-io.ts diff --git a/README.md b/README.md index b5bd7d2..ec71373 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Weather data is provided by the [Tomorrow.io](https://www.tomorrow.io/) API. The - Set your location using coordinates. You can use your browser's geolocation feature to get your current location. - Get the current weather conditions. -- Get a 6 day forecast either in hourly or daily intervals. +- Get a 5 day forecast in hourly and daily intervals. - Dark and light themes - uses your system's theme preference as the default theme. - Responsive design - Works well on both desktop and mobile. - Progressive Web App - You can install the web app as a standalone app on your device using the install prompt or the install button in your browser's address bar. diff --git a/src/app/_components/forecast-daily.tsx b/src/app/_components/forecast-daily.tsx index 7723a5f..f1ac215 100644 --- a/src/app/_components/forecast-daily.tsx +++ b/src/app/_components/forecast-daily.tsx @@ -2,13 +2,11 @@ import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; +import { WeatherForecastErrorResponse } from "~/lib/schemas/tomorrow-io"; +import { WeatherForecastDaily } from "~/lib/schemas/weather"; import { getLocationFromLocalStorage } from "~/lib/local-storage"; import { getWeatherForecastDaily } from "~/lib/serverActions/tomorrow-io"; import { weatherCode } from "~/lib/tomorrowio/weather-codes"; -import { - type WeatherForecastErrorResponse, - type WeatherForecastDaily, -} from "~/lib/types/tomorrow-io"; import { WeatherIcon } from "~/components/weather-icon"; export function ForecastDaily() { diff --git a/src/app/_components/forecast-hourly.tsx b/src/app/_components/forecast-hourly.tsx index a065229..91fdbc6 100644 --- a/src/app/_components/forecast-hourly.tsx +++ b/src/app/_components/forecast-hourly.tsx @@ -2,13 +2,11 @@ import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; +import { WeatherForecastErrorResponse } from "~/lib/schemas/tomorrow-io"; +import { WeatherForecastHourly } from "~/lib/schemas/weather"; import { getLocationFromLocalStorage } from "~/lib/local-storage"; import { getWeatherForecastHourly } from "~/lib/serverActions/tomorrow-io"; import { weatherCode } from "~/lib/tomorrowio/weather-codes"; -import { - type WeatherForecastErrorResponse, - type WeatherForecastHourly, -} from "~/lib/types/tomorrow-io"; import { WeatherIcon } from "~/components/weather-icon"; export function ForecastHourly() { @@ -80,12 +78,16 @@ export function ForecastHourly() { {weatherCode[item.weatherCode] || "Unknown"} -
- - {item.temperature.toFixed(1)} - - °C -
+ {item.temperature ? ( +
+ + {item.temperature.toFixed(1)} + + °C +
+ ) : ( + ? + )} ); })} diff --git a/src/app/_components/forecast-now.tsx b/src/app/_components/forecast-now.tsx index 2b559bc..dcfcf79 100644 --- a/src/app/_components/forecast-now.tsx +++ b/src/app/_components/forecast-now.tsx @@ -4,12 +4,10 @@ import { useQuery } from "@tanstack/react-query"; import dayjs, { Dayjs } from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; +import { WeatherForecastErrorResponse } from "~/lib/schemas/tomorrow-io"; +import { WeatherForecastNow } from "~/lib/schemas/weather"; import { getLocationFromLocalStorage } from "~/lib/local-storage"; import { getWeatherForecastNow } from "~/lib/serverActions/tomorrow-io"; -import { - type WeatherForecastErrorResponse, - type WeatherForecastNow, -} from "~/lib/types/tomorrow-io"; import { weatherCode } from "~/lib/tomorrowio/weather-codes"; import { WeatherIcon } from "~/components/weather-icon"; @@ -51,14 +49,14 @@ export function ForecastNow() { return (
{forecastNow.isLoading ? ( - Loading realtime forecast... + Loading current forecast... ) : forecastNow.isError ? ( - Error loading realtime forecast. + Error loading current forecast. ) : !forecastNow.data ? ( - No realtime forecast data. + No current forecast data. ) : "code" in forecastNow.data ? ( - An error occured when loading realtime forecast data + An error occured when loading current forecast data {String(forecastNow.data.code).startsWith("429") && ": Too many requests to the API. Please try again later."} diff --git a/src/app/daily/_components/daily-charts.tsx b/src/app/daily/_components/daily-charts.tsx index 6ede3b9..e8a9912 100644 --- a/src/app/daily/_components/daily-charts.tsx +++ b/src/app/daily/_components/daily-charts.tsx @@ -1,7 +1,6 @@ "use client"; import { useQuery } from "@tanstack/react-query"; import { - Area, CartesianGrid, ComposedChart, Line, @@ -10,6 +9,8 @@ import { YAxis, } from "recharts"; +import { WeatherForecastErrorResponse } from "~/lib/schemas/tomorrow-io"; +import { WeatherForecastDailyCharts } from "~/lib/schemas/weather"; import { ChartConfig, ChartContainer, @@ -20,10 +21,6 @@ import { } from "~/components/ui/chart"; import { getLocationFromLocalStorage } from "~/lib/local-storage"; import { getWeatherForecastDailyCharts } from "~/lib/serverActions/tomorrow-io"; -import { - type WeatherForecastErrorResponse, - type WeatherForecastDailyCharts, -} from "~/lib/types/tomorrow-io"; const temperaturesChartConfig = { temperatureMin: { diff --git a/src/app/hourly/_components/hourly-charts.tsx b/src/app/hourly/_components/hourly-charts.tsx index e236d18..a6cd3aa 100644 --- a/src/app/hourly/_components/hourly-charts.tsx +++ b/src/app/hourly/_components/hourly-charts.tsx @@ -1,8 +1,9 @@ "use client"; import { useQuery } from "@tanstack/react-query"; -import dayjs from "dayjs"; import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; +import { WeatherForecastErrorResponse } from "~/lib/schemas/tomorrow-io"; +import { WeatherForecastHourlyCharts } from "~/lib/schemas/weather"; import { ChartConfig, ChartContainer, @@ -13,10 +14,6 @@ import { } from "~/components/ui/chart"; import { getLocationFromLocalStorage } from "~/lib/local-storage"; import { getWeatherForecastHourlyCharts } from "~/lib/serverActions/tomorrow-io"; -import { - type WeatherForecastErrorResponse, - type WeatherForecastHourlyCharts, -} from "~/lib/types/tomorrow-io"; const temperaturesChartConfig = { temperature: { diff --git a/src/components/location-form.tsx b/src/components/location-form.tsx index 301880a..cc57750 100644 --- a/src/components/location-form.tsx +++ b/src/components/location-form.tsx @@ -15,7 +15,7 @@ import { FormMessage, } from "~/components/ui/form"; import { Input } from "~/components/ui/input"; -import { type Location, LocationSchema } from "~/lib/schema"; +import { type Location, LocationSchema } from "~/lib/schemas/location"; import { getLocationFromLocalStorage } from "~/lib/local-storage"; import { DialogClose, DialogFooter } from "~/components/ui/dialog"; diff --git a/src/components/weather-icon.tsx b/src/components/weather-icon.tsx index 1b80d49..e8b512c 100644 --- a/src/components/weather-icon.tsx +++ b/src/components/weather-icon.tsx @@ -22,7 +22,7 @@ export function WeatherIcon({ className, night, }: { - code: keyof typeof weatherCode; + code: keyof typeof weatherCode | number; className?: string; night?: boolean; }) { @@ -53,7 +53,7 @@ export function WeatherIcon({ 8000: CloudLightning, // "Thunderstorm" }; - const Icon = weatherIcon[code]; + const Icon = weatherIcon[code as number]; if (!Icon) return ; diff --git a/src/lib/local-storage.ts b/src/lib/local-storage.ts index d201bf3..252389a 100644 --- a/src/lib/local-storage.ts +++ b/src/lib/local-storage.ts @@ -1,9 +1,9 @@ "use client"; -import { type Location } from "~/lib/schema"; +import { type Location } from "~/lib/schemas/location"; -// +// // Get the user's location from local storage, or default to the center of the earth -// +// export function getLocationFromLocalStorage(): Location { const data = localStorage.getItem("location"); if (data) { diff --git a/src/lib/schema.ts b/src/lib/schemas/location.ts similarity index 100% rename from src/lib/schema.ts rename to src/lib/schemas/location.ts diff --git a/src/lib/schemas/tomorrow-io.ts b/src/lib/schemas/tomorrow-io.ts new file mode 100644 index 0000000..21cc122 --- /dev/null +++ b/src/lib/schemas/tomorrow-io.ts @@ -0,0 +1,171 @@ +import { z } from "zod"; + +// +// Error +// +export const WeatherForecastErrorResponseSchema = z.object({ + code: z.number(), + type: z.string(), + message: z.string(), +}); +export type WeatherForecastErrorResponse = z.infer< + typeof WeatherForecastErrorResponseSchema +>; + +// +// Timelines +// +export const ValuesSchema = z.object({ + // Now + cloudBase: z.number().nullish(), + cloudCeiling: z.number().nullish(), + cloudCover: z.number().nullish(), + dewPoint: z.number().nullish(), + freezingRainIntensity: z.number().nullish(), + humidity: z.number().nullish(), + precipitationIntensity: z.number().nullish(), + precipitationProbability: z.number().nullish(), + precipitationType: z.number().nullish(), + pressureSurfaceLevel: z.number().nullish(), + rainIntensity: z.number().nullish(), + sleetIntensity: z.number().nullish(), + snowIntensity: z.number().nullish(), + temperature: z.number().nullish(), + temperatureApparent: z.number().nullish(), + uvHealthConcern: z.number().nullish(), + uvIndex: z.number().nullish(), + visibility: z.number().nullish(), + weatherCode: z.number().nullish(), + windDirection: z.number().nullish(), + windGust: z.number().nullish(), + windSpeed: z.number().nullish(), + // Hourly + evapotranspiration: z.number().nullish(), + iceAccumulation: z.number().nullish(), + rainAccumulation: z.number().nullish(), + sleetAccumulation: z.number().nullish(), + snowAccumulation: z.number().nullish(), + // Daily + cloudBaseAvg: z.number().nullish(), + cloudBaseMax: z.number().nullish(), + cloudBaseMin: z.number().nullish(), + cloudCeilingAvg: z.number().nullish(), + cloudCeilingMax: z.number().nullish(), + cloudCeilingMin: z.number().nullish(), + cloudCoverAvg: z.number().nullish(), + cloudCoverMax: z.number().nullish(), + cloudCoverMin: z.number().nullish(), + dewPointAvg: z.number().nullish(), + dewPointMax: z.number().nullish(), + dewPointMin: z.number().nullish(), + evapotranspirationAvg: z.number().nullish(), + evapotranspirationMax: z.number().nullish(), + evapotranspirationMin: z.number().nullish(), + evapotranspirationSum: z.number().nullish(), + freezingRainIntensityAvg: z.number().nullish(), + freezingRainIntensityMax: z.number().nullish(), + freezingRainIntensityMin: z.number().nullish(), + humidityAvg: z.number().nullish(), + humidityMax: z.number().nullish(), + humidityMin: z.number().nullish(), + iceAccumulationAvg: z.number().nullish(), + iceAccumulationMax: z.number().nullish(), + iceAccumulationMin: z.number().nullish(), + iceAccumulationSum: z.number().nullish(), + moonriseTime: z.string().nullish(), + moonsetTime: z.string().nullish(), + precipitationProbabilityAvg: z.number().nullish(), + precipitationProbabilityMax: z.number().nullish(), + precipitationProbabilityMin: z.number().nullish(), + pressureSurfaceLevelAvg: z.number().nullish(), + pressureSurfaceLevelMax: z.number().nullish(), + pressureSurfaceLevelMin: z.number().nullish(), + rainAccumulationAvg: z.number().nullish(), + rainAccumulationMax: z.number().nullish(), + rainAccumulationMin: z.number().nullish(), + rainAccumulationSum: z.number().nullish(), + rainIntensityAvg: z.number().nullish(), + rainIntensityMax: z.number().nullish(), + rainIntensityMin: z.number().nullish(), + sleetAccumulationAvg: z.number().nullish(), + sleetAccumulationMax: z.number().nullish(), + sleetAccumulationMin: z.number().nullish(), + // sleetAccumulationSum: z.number().nullish(), + sleetIntensityAvg: z.number().nullish(), + sleetIntensityMax: z.number().nullish(), + sleetIntensityMin: z.number().nullish(), + snowAccumulationAvg: z.number().nullish(), + snowAccumulationMax: z.number().nullish(), + snowAccumulationMin: z.number().nullish(), + snowAccumulationSum: z.number().nullish(), + snowIntensityAvg: z.number().nullish(), + snowIntensityMax: z.number().nullish(), + snowIntensityMin: z.number().nullish(), + sunriseTime: z.string().nullish(), + sunsetTime: z.string().nullish(), + temperatureApparentAvg: z.number().nullish(), + temperatureApparentMax: z.number().nullish(), + temperatureApparentMin: z.number().nullish(), + temperatureAvg: z.number().nullish(), + temperatureMax: z.number().nullish(), + temperatureMin: z.number().nullish(), + uvHealthConcernAvg: z.number().nullish(), + uvHealthConcernMax: z.number().nullish(), + uvHealthConcernMin: z.number().nullish(), + uvIndexAvg: z.number().nullish(), + uvIndexMax: z.number().nullish(), + uvIndexMin: z.number().nullish(), + visibilityAvg: z.number().nullish(), + visibilityMax: z.number().nullish(), + visibilityMin: z.number().nullish(), + weatherCodeMax: z.number().nullish(), + weatherCodeMin: z.number().nullish(), + windDirectionAvg: z.number().nullish(), + windGustAvg: z.number().nullish(), + windGustMax: z.number().nullish(), + windGustMin: z.number().nullish(), + windSpeedAvg: z.number().nullish(), + windSpeedMax: z.number().nullish(), + windSpeedMin: z.number().nullish(), +}); +export type Values = z.infer; + +export const MetaSchema = z.object({ + from: z.string(), + to: z.string(), + timestep: z.string(), +}); +export type Meta = z.infer; + +export const IntervalSchema = z.object({ + startTime: z.coerce.string(), + values: ValuesSchema.nullish(), +}); +export type Interval = z.infer; + +export const WarningSchema = z.object({ + code: z.number(), + type: z.string(), + message: z.string(), + meta: MetaSchema, +}); +export type Warning = z.infer; + +export const TimelineElementSchema = z.object({ + timestep: z.enum(["current", "1h", "1d"]), + endTime: z.coerce.string(), + startTime: z.coerce.string(), + intervals: z.array(IntervalSchema), +}); +export type TimelineElement = z.infer; + +export const DataSchema = z.object({ + timelines: z.array(TimelineElementSchema), + warnings: z.array(WarningSchema), +}); +export type Data = z.infer; + +export const TimelinesSchema = z.object({ + data: DataSchema, +}); +export type Timelines = z.infer; diff --git a/src/lib/schemas/weather.ts b/src/lib/schemas/weather.ts new file mode 100644 index 0000000..c629762 --- /dev/null +++ b/src/lib/schemas/weather.ts @@ -0,0 +1,318 @@ +import { z } from "zod"; + +import { ValuesSchema } from "~/lib/schemas/tomorrow-io"; + +// +// Timelines +// +export const WeatherForecastTimelinesSchema = z.object({ + current: ValuesSchema.extend({ + time: z.string(), + }), + hourly: z.array( + ValuesSchema.extend({ + time: z.string(), + }).nullish(), + ), + daily: z.array( + ValuesSchema.extend({ + time: z.string(), + }).nullish(), + ), +}); +export type WeatherForecastTimelines = z.infer< + typeof WeatherForecastTimelinesSchema +>; + +// +// Now +// +export const WeatherForecastNowSchema = z.object({ + time: z.string(), + cloudBase: z.number().nullish(), + cloudCeiling: z.number().nullish(), + cloudCover: z.number().nullish(), + dewPoint: z.number(), + freezingRainIntensity: z.number(), + humidity: z.number(), + precipitationIntensity: z.number(), + precipitationProbability: z.number(), + precipitationType: z.number(), + pressureSurfaceLevel: z.number(), + rainIntensity: z.number(), + sleetIntensity: z.number(), + snowIntensity: z.number(), + temperature: z.number(), + temperatureApparent: z.number(), + uvHealthConcern: z.number(), + uvIndex: z.number(), + visibility: z.number(), + weatherCode: z.number(), + windDirection: z.number(), + windDirectionCardinal: z.string(), + windGust: z.number(), + windSpeed: z.number(), +}); +export type WeatherForecastNow = z.infer; + +// +// Hourly +// +export const WeatherForecastHourlySchema = z.array( + z.object({ + time: z.string(), + cloudBase: z.number().nullish(), + cloudCeiling: z.number().nullish(), + cloudCover: z.number().nullish(), + dewPoint: z.number().nullish(), + evapotranspiration: z.number().nullish(), + freezingRainIntensity: z.number().nullish(), + humidity: z.number().nullish(), + iceAccumulation: z.number().nullish(), + precipitationProbability: z.number().nullish(), + pressureSurfaceLevel: z.number().nullish(), + rainAccumulation: z.number().nullish(), + rainIntensity: z.number().nullish(), + sleetAccumulation: z.number().nullish(), + sleetIntensity: z.number().nullish(), + snowAccumulation: z.number().nullish(), + snowIntensity: z.number().nullish(), + temperature: z.number().nullish(), + temperatureApparent: z.number().nullish(), + visibility: z.number().nullish(), + weatherCode: z.number().default(0), + windDirection: z.number().nullish(), + windGust: z.number().nullish(), + windSpeed: z.number().nullish(), + }), +); +export type WeatherForecastHourly = z.infer; + +export const WeatherForecastHourlyTemperatureChartSchema = z.array( + z.object({ + time: z.string(), + temperature: z.number().nullish(), + temperatureApparent: z.number().nullish(), + }), +); +export type WeatherForecastHourlyTemperatureChart = z.infer< + typeof WeatherForecastHourlyTemperatureChartSchema +>; + +export const WeatherForecastHourlyHumidityChartSchema = z.array( + z.object({ + time: z.string().nullish(), + humidity: z.number().nullish(), + }), +); +export type WeatherForecastHourlyHumidityChart = z.infer< + typeof WeatherForecastHourlyHumidityChartSchema +>; + +export const WeatherForecastHourlyWindSpeedChartSchema = z.array( + z.object({ + time: z.string(), + windSpeed: z.number().nullish(), + }), +); +export type WeatherForecastHourlyWindSpeedChart = z.infer< + typeof WeatherForecastHourlyWindSpeedChartSchema +>; + +export const WeatherForecastHourlyAccumulationChartSchema = z.array( + z.object({ + time: z.string(), + rainAccumulation: z.number().nullish(), + sleetAccumulation: z.number().nullish(), + snowAccumulation: z.number().nullish(), + iceAccumulation: z.number().nullish(), + }), +); +export type WeatherForecastHourlyAccumulationChart = z.infer< + typeof WeatherForecastHourlyAccumulationChartSchema +>; + +export const WeatherForecastHourlyChartsSchema = z.object({ + temperatures: WeatherForecastHourlyTemperatureChartSchema, + humidities: WeatherForecastHourlyHumidityChartSchema, + windSpeeds: WeatherForecastHourlyWindSpeedChartSchema, + precipitations: WeatherForecastHourlyAccumulationChartSchema, +}); +export type WeatherForecastHourlyCharts = z.infer< + typeof WeatherForecastHourlyChartsSchema +>; + +// +// Daily +// +export const WeatherForecastDailySchema = z.array( + z.object({ + time: z.string(), + cloudBaseAvg: z.number(), + cloudBaseMax: z.number(), + cloudBaseMin: z.number(), + cloudCeilingAvg: z.number(), + cloudCeilingMax: z.number(), + cloudCeilingMin: z.number(), + cloudCoverAvg: z.number(), + cloudCoverMax: z.number(), + cloudCoverMin: z.number(), + dewPointAvg: z.number(), + dewPointMax: z.number(), + dewPointMin: z.number(), + evapotranspirationAvg: z.number(), + evapotranspirationMax: z.number(), + evapotranspirationMin: z.number(), + evapotranspirationSum: z.number(), + freezingRainIntensityAvg: z.number(), + freezingRainIntensityMax: z.number(), + freezingRainIntensityMin: z.number(), + humidityAvg: z.number(), + humidityMax: z.number(), + humidityMin: z.number(), + iceAccumulationAvg: z.number(), + iceAccumulationMax: z.number(), + iceAccumulationMin: z.number(), + iceAccumulationSum: z.number(), + moonriseTime: z.string().nullish(), + moonsetTime: z.string().nullish(), + precipitationProbabilityAvg: z.number(), + precipitationProbabilityMax: z.number(), + precipitationProbabilityMin: z.number(), + pressureSurfaceLevelAvg: z.number(), + pressureSurfaceLevelMax: z.number(), + pressureSurfaceLevelMin: z.number(), + rainAccumulationAvg: z.number(), + rainAccumulationMax: z.number(), + rainAccumulationMin: z.number(), + rainAccumulationSum: z.number(), + rainIntensityAvg: z.number(), + rainIntensityMax: z.number(), + rainIntensityMin: z.number(), + sleetAccumulationAvg: z.number(), + sleetAccumulationMax: z.number(), + sleetAccumulationMin: z.number(), + // sleetAccumulationSum: z.number().nullish(), + sleetIntensityAvg: z.number(), + sleetIntensityMax: z.number(), + sleetIntensityMin: z.number(), + snowAccumulationAvg: z.number(), + snowAccumulationMax: z.number(), + snowAccumulationMin: z.number(), + snowAccumulationSum: z.number(), + snowIntensityAvg: z.number(), + snowIntensityMax: z.number(), + snowIntensityMin: z.number(), + sunriseTime: z.string(), + sunsetTime: z.string(), + temperatureApparentAvg: z.number(), + temperatureApparentMax: z.number(), + temperatureApparentMin: z.number(), + temperatureAvg: z.number(), + temperatureMax: z.number(), + temperatureMin: z.number(), + uvHealthConcernAvg: z.number().nullish(), + uvHealthConcernMax: z.number().nullish(), + uvHealthConcernMin: z.number().nullish(), + uvIndexAvg: z.number().nullish(), + uvIndexMax: z.number().nullish(), + uvIndexMin: z.number().nullish(), + visibilityAvg: z.number(), + visibilityMax: z.number(), + visibilityMin: z.number(), + weatherCodeMax: z.number(), + weatherCodeMin: z.number(), + windDirectionAvg: z.number(), + windGustAvg: z.number(), + windGustMax: z.number(), + windGustMin: z.number(), + windSpeedAvg: z.number(), + windSpeedMax: z.number(), + windSpeedMin: z.number(), + }), +); +export type WeatherForecastDaily = z.infer; + +export const WeatherForecastDailyTemperatureChartSchema = z.array( + z.object({ + time: z.string(), + temperatureMin: z.number(), + temperatureMax: z.number(), + temperatureAvg: z.number(), + temperatureRange: z.tuple([z.number(), z.number()]), + }), +); +export type WeatherForecastDailyTemperatureChart = z.infer< + typeof WeatherForecastDailyTemperatureChartSchema +>; + +export const WeatherForecastDailyHumidityChartSchema = z.array( + z.object({ + time: z.string(), + humidityMin: z.number(), + humidityMax: z.number(), + humidityAvg: z.number(), + humidityRange: z.tuple([z.number(), z.number()]), + }), +); +export type WeatherForecastDailyHumidityChart = z.infer< + typeof WeatherForecastDailyHumidityChartSchema +>; + +export const WeatherForecastDailyWindSpeedChartSchema = z.array( + z.object({ + time: z.string(), + windSpeedMin: z.number(), + windSpeedMax: z.number(), + windSpeedAvg: z.number(), + windSpeedRange: z.tuple([z.number(), z.number()]), + }), +); +export type WeatherForecastDailyWindSpeedChart = z.infer< + typeof WeatherForecastDailyWindSpeedChartSchema +>; + +export const WeatherForecastDailyAccumulationChartSchema = z.array( + z.object({ + time: z.string(), + rainAccumulationMin: z.number(), + rainAccumulationMax: z.number(), + rainAccumulationAvg: z.number(), + rainAccumulationRange: z.tuple([z.number(), z.number()]), + rainAccumulationSum: z.number(), + sleetAccumulationMin: z.number(), + sleetAccumulationMax: z.number(), + sleetAccumulationAvg: z.number(), + sleetAccumulationRange: z.tuple([z.number(), z.number()]), + // sleetAccumulationSum: z.number().nullish(), + snowAccumulationMin: z.number(), + snowAccumulationMax: z.number(), + snowAccumulationAvg: z.number(), + snowAccumulationRange: z.tuple([z.number(), z.number()]), + snowAccumulationSum: z.number(), + iceAccumulationMin: z.number(), + iceAccumulationMax: z.number(), + iceAccumulationAvg: z.number(), + iceAccumulationRange: z.tuple([z.number(), z.number()]), + iceAccumulationSum: z.number(), + }), +); +export type WeatherForecastDailyAccumulationChart = z.infer< + typeof WeatherForecastDailyAccumulationChartSchema +>; + +export const WeatherForecastDailyChartsSchema = z.object({ + temperatures: WeatherForecastDailyTemperatureChartSchema, + humidities: WeatherForecastDailyHumidityChartSchema, + windSpeeds: WeatherForecastDailyWindSpeedChartSchema, + precipitations: WeatherForecastDailyAccumulationChartSchema, +}); +export type WeatherForecastDailyCharts = z.infer< + typeof WeatherForecastDailyChartsSchema +>; + +// +// Weather Code Map +// +export const WeatherCodeMapSchema = z.record(z.string()); +export type WeatherCodeMap = z.infer; diff --git a/src/lib/serverActions/tomorrow-io.ts b/src/lib/serverActions/tomorrow-io.ts index 0034392..df25c35 100644 --- a/src/lib/serverActions/tomorrow-io.ts +++ b/src/lib/serverActions/tomorrow-io.ts @@ -4,63 +4,188 @@ import { unstable_cache } from "next/cache"; import dayjs from "dayjs"; import { env } from "~/env"; -import { type Location } from "~/lib/schema"; +import { type Location } from "~/lib/schemas/location"; import { - WeatherForecastDailyCharts, - type WeatherForecastDaily, - type WeatherForecastDailyResponse, type WeatherForecastErrorResponse, - type WeatherForecastHourly, - type WeatherForecastHourlyCharts, - type WeatherForecastHourlyResponse, - type WeatherForecastNow, - type WeatherForecastNowResponse, -} from "~/lib/types/tomorrow-io"; -import { getWindDirectionCardinalFromDegrees } from "~/lib/utils"; - -// All requests to the Tomorrow.io API require an API key -const BASE_PARAMS = `apikey=${env.WEATHER_API_KEY}&units=metric`; - -// Setup the base request options for all requests to the Tomorrow.io API -const BASE_REQUEST_OPTIONS: RequestInit = { - method: "GET", - headers: { - Accept: "application/json", - }, -}; + type Timelines, + ValuesSchema, +} from "~/lib/schemas/tomorrow-io"; +import { + getWindDirectionCardinalFromDegrees, + getZodSchemaFieldsShallow, +} from "~/lib/utils"; +import { + WeatherForecastDaily, + WeatherForecastDailyCharts, + WeatherForecastDailySchema, + WeatherForecastHourly, + WeatherForecastHourlyCharts, + WeatherForecastHourlySchema, + WeatherForecastNow, + WeatherForecastNowSchema, + WeatherForecastTimelines, + WeatherForecastTimelinesSchema, +} from "~/lib/schemas/weather"; // -// Get the current weather forecast for a location +// Get the current weather data for a location // -export async function getWeatherForecastNow( +export async function getWeatherForecastTimelines( location: Location, -): Promise { - return unstable_cache( - async (): Promise => { - const url = `https://api.tomorrow.io/v4/weather/realtime?${BASE_PARAMS}&location=${location.latitude},${location.longitude}`; - console.log("Get forecast now for location:", location, url); - const response = await fetch(url, BASE_REQUEST_OPTIONS); + timezone: string = "auto", + units: string = "metric", +): Promise { + const cachedResponseData = await unstable_cache( + async (): Promise => { + const url = new URL("https://api.tomorrow.io/v4/timelines"); + url.searchParams.append("apikey", env.WEATHER_API_KEY); + url.searchParams.append( + "location", + `${location.latitude},${location.longitude}`, + ); + url.searchParams.append( + "fields", + Object.keys(getZodSchemaFieldsShallow(ValuesSchema)).join(","), + ); + url.searchParams.append("units", units); + url.searchParams.append("timesteps", ["current", "1h", "1d"].join(",")); + url.searchParams.append("startTime", "now"); + url.searchParams.append("endTime", "nowPlus5d"); + url.searchParams.append("timezone", timezone); + + console.log("Get weather timelines for location:", location, url); + const response = await fetch(url, { + method: "GET", + headers: { + Accept: "application/json", + "Accept-Encoding": "gzip", + }, + }); + const responseData = (await response.json()) as | WeatherForecastErrorResponse - | WeatherForecastNowResponse; - console.log("Response:", JSON.stringify(responseData)); - // If there is an error, return it so the client can handle it - if ("code" in responseData) return responseData; - - return { - time: responseData.data.time, - ...responseData.data.values, - windDirectionCardinal: getWindDirectionCardinalFromDegrees( - responseData.data.values.windDirection, - ), - }; + | Timelines; + console.log("New response:", JSON.stringify(responseData)); + + return responseData; }, [`${location.latitude},${location.longitude}`], { - tags: ["forecast", "now"], - revalidate: 1000 * 60 * 5, // 5 minutes + tags: ["timelines"], + revalidate: 1000 * 60 * 2, // 2 minutes }, )(); + + // If there is an error, return it so the client can handle it + if ("code" in cachedResponseData) return cachedResponseData; + + console.log( + "Got weather forecast timelines:", + JSON.stringify({ + data: { + timelines: cachedResponseData.data.timelines.map((timeline) => ({ + timestep: timeline.timestep, + startTime: timeline.startTime, + endTime: timeline.endTime, + intervals: timeline.intervals.length, + })), + warnings: cachedResponseData.data.warnings, + }, + }), + ); + + const currentData = cachedResponseData.data.timelines.find( + (timeline) => timeline.timestep === "current", + ); + if ( + !currentData || + !currentData.intervals || + !currentData.intervals[0]?.values + ) { + console.error("No current data in response:", currentData); + return { + code: 500, + message: "No current data in response", + type: "error", + }; + } + + const hourlyData = cachedResponseData.data.timelines.find( + (timeline) => timeline.timestep === "1h", + ); + if (!hourlyData || !hourlyData.intervals) { + console.error("No hourly data in response:", hourlyData); + return { + code: 500, + message: "No hourly data in response", + type: "error", + }; + } + + const dailyData = cachedResponseData.data.timelines.find( + (timeline) => timeline.timestep === "1d", + ); + if (!dailyData || !dailyData.intervals) { + console.error("No daily data in response:", dailyData); + return { + code: 500, + message: "No daily data in response", + type: "error", + }; + } + + return WeatherForecastTimelinesSchema.parse({ + current: { + time: currentData.intervals[0].startTime, + ...currentData.intervals[0].values, + }, + hourly: hourlyData.intervals.map((interval) => ({ + time: interval.startTime, + ...interval.values, + })), + daily: dailyData.intervals.map((interval) => ({ + time: interval.startTime, + ...interval.values, + })), + }); +} + +// +// Get the current weather forecast for a location +// +export async function getWeatherForecastNow( + location: Location, + timezone: string = "auto", + units: string = "metric", +): Promise { + const timelines = await getWeatherForecastTimelines( + location, + timezone, + units, + ); + // If there is an error, return it so the client can handle it + if ("code" in timelines) return timelines; + + console.log("Got weather forecast now:", JSON.stringify(timelines.current)); + + if (!timelines.current.windDirection) { + console.error( + "No wind direction in current weather data:", + timelines.current, + ); + return { + code: 500, + message: "No wind direction in current weather data", + type: "error", + }; + } + + return WeatherForecastNowSchema.parse({ + ...timelines.current, + windDirectionCardinal: getWindDirectionCardinalFromDegrees( + timelines.current.windDirection, + ), + }); } // @@ -68,30 +193,20 @@ export async function getWeatherForecastNow( // export async function getWeatherForecastHourly( location: Location, + timezone: string = "auto", + units: string = "metric", ): Promise { - return unstable_cache( - async (): Promise => { - const url = `https://api.tomorrow.io/v4/weather/forecast?${BASE_PARAMS}&location=${location.latitude},${location.longitude}×teps=1h`; - console.log("Get hourly forecast for location:", location, url); - const response = await fetch(url, BASE_REQUEST_OPTIONS); - const responseData = (await response.json()) as - | WeatherForecastErrorResponse - | WeatherForecastHourlyResponse; - console.log("Response:", JSON.stringify(responseData)); - // If there is an error, return it so the client can handle it - if ("code" in responseData) return responseData; - - return responseData.timelines.hourly.map((hourly) => ({ - time: hourly.time, - ...hourly.values, - })); - }, - [`${location.latitude},${location.longitude}`], - { - tags: ["forecast", "hourly"], - revalidate: 1000 * 60 * 20, // 20 minutes - }, - )(); + const timelines = await getWeatherForecastTimelines( + location, + timezone, + units, + ); + // If there is an error, return it so the client can handle it + if ("code" in timelines) return timelines; + + console.log("Got weather forecast hourly:", timelines.hourly.length); + + return WeatherForecastHourlySchema.parse(timelines.hourly); } // @@ -100,8 +215,14 @@ export async function getWeatherForecastHourly( // export async function getWeatherForecastHourlyCharts( location: Location, + timezone: string = "auto", + units: string = "metric", ): Promise { - const hourlyForecast = await getWeatherForecastHourly(location); + const hourlyForecast = await getWeatherForecastHourly( + location, + timezone, + units, + ); // If there is an error, return it so the client can handle it if ("code" in hourlyForecast) return hourlyForecast; @@ -117,7 +238,7 @@ export async function getWeatherForecastHourlyCharts( })), windSpeeds: hourlyForecast.map((hourly) => ({ time: dayjs(hourly.time).format("ddd HH:mm"), - windSpeed: hourly.windSpeed, + windSpeed: hourly.windSpeed, })), precipitations: hourlyForecast.map((hourly) => ({ time: dayjs(hourly.time).format("ddd HH:mm"), @@ -145,30 +266,20 @@ export async function getWeatherForecastHourlyCharts( // export async function getWeatherForecastDaily( location: Location, + timezone: string = "auto", + units: string = "metric", ): Promise { - return unstable_cache( - async (): Promise => { - const url = `https://api.tomorrow.io/v4/weather/forecast?${BASE_PARAMS}&location=${location.latitude},${location.longitude}×teps=1d`; - console.log("Get daily forecast for location:", location, url); - const response = await fetch(url, BASE_REQUEST_OPTIONS); - const responseData = (await response.json()) as - | WeatherForecastErrorResponse - | WeatherForecastDailyResponse; - console.log("Response:", JSON.stringify(responseData)); - // If there is an error, return it so the client can handle it - if ("code" in responseData) return responseData; - - return responseData.timelines.daily.map((daily) => ({ - time: daily.time, - ...daily.values, - })); - }, - [`${location.latitude},${location.longitude}`], - { - tags: ["forecast", "daily"], - revalidate: 1000 * 60 * 30, // 30 minutes - }, - )(); + const timelines = await getWeatherForecastTimelines( + location, + timezone, + units, + ); + // If there is an error, return it so the client can handle it + if ("code" in timelines) return timelines; + + console.log("Got weather forecast daily:", timelines.daily.length); + + return WeatherForecastDailySchema.parse(timelines.daily); } // @@ -177,8 +288,14 @@ export async function getWeatherForecastDaily( // export async function getWeatherForecastDailyCharts( location: Location, + timezone: string = "auto", + units: string = "metric", ): Promise { - const dailyForecast = await getWeatherForecastDaily(location); + const dailyForecast = await getWeatherForecastDaily( + location, + timezone, + units, + ); // If there is an error, return it so the client can handle it if ("code" in dailyForecast) return dailyForecast; @@ -221,7 +338,7 @@ export async function getWeatherForecastDailyCharts( daily.sleetAccumulationMin, daily.sleetAccumulationMax, ], - sleetAccumulationSum: daily.sleetAccumulationSum, + // sleetAccumulationSum: daily.sleetAccumulationSum, snowAccumulationMin: daily.snowAccumulationMin, snowAccumulationMax: daily.snowAccumulationMax, snowAccumulationAvg: daily.snowAccumulationAvg, diff --git a/src/lib/tomorrowio/weather-codes.ts b/src/lib/tomorrowio/weather-codes.ts index 948f5eb..e88578a 100644 --- a/src/lib/tomorrowio/weather-codes.ts +++ b/src/lib/tomorrowio/weather-codes.ts @@ -2,7 +2,7 @@ // Weather codes from Tomorrow.io to human readable strings // -import { WeatherCodeMap } from "~/lib/types/tomorrow-io"; +import { WeatherCodeMap } from "~/lib/schemas/weather"; export const weatherCode: WeatherCodeMap = { 0: "Unknown", diff --git a/src/lib/types/tomorrow-io.ts b/src/lib/types/tomorrow-io.ts deleted file mode 100644 index ba291c2..0000000 --- a/src/lib/types/tomorrow-io.ts +++ /dev/null @@ -1,429 +0,0 @@ -export type WeatherForecastErrorResponse = { - code: 429001 | number; - type: "Too Many Calls" | string; - message: string; -}; - -export type WeatherForecastNowResponse = { - data: { - time: Date; - values: { - cloudBase: number; - cloudCeiling: number; - cloudCover: number; - dewPoint: number; - freezingRainIntensity: number; - humidity: number; - precipitationProbability: number; - pressureSurfaceLevel: number; - rainIntensity: number; - sleetIntensity: number; - snowIntensity: number; - temperature: number; - temperatureApparent: number; - uvHealthConcern: number; - uvIndex: number; - visibility: number; - weatherCode: number; - windDirection: number; - windGust: number; - windSpeed: number; - }; - }; - location: { lat: number; lon: number }; -}; - -export type WeatherForecastNow = { - time: Date; - cloudBase: number; - cloudCeiling: number; - cloudCover: number; - dewPoint: number; - freezingRainIntensity: number; - humidity: number; - precipitationProbability: number; - pressureSurfaceLevel: number; - rainIntensity: number; - sleetIntensity: number; - snowIntensity: number; - temperature: number; - temperatureApparent: number; - uvHealthConcern: number; - uvIndex: number; - visibility: number; - weatherCode: number; - windDirection: number; - windDirectionCardinal: string; - windGust: number; - windSpeed: number; -}; - -export type WeatherForecastHourlyResponse = { - timelines: { - hourly: Array<{ - time: Date; - values: { - cloudBase: number; - cloudCeiling: number; - cloudCover: number; - dewPoint: number; - evapotranspiration: number; - freezingRainIntensity: number; - humidity: number; - iceAccumulation: number; - iceAccumulationLwe: number; - precipitationProbability: number; - pressureSurfaceLevel: number; - rainAccumulation: number; - rainAccumulationLwe: number; - rainIntensity: number; - sleetAccumulation: number; - sleetAccumulationLwe: number; - sleetIntensity: number; - snowAccumulation: number; - snowAccumulationLwe: number; - snowIntensity: number; - temperature: number; - temperatureApparent: number; - visibility: number; - weatherCode: number; - windDirection: number; - windGust: number; - windSpeed: number; - }; - }>; - }; - location: { - lat: number; - lon: number; - }; -}; - -export type WeatherForecastHourly = Array<{ - time: Date; - cloudBase: number; - cloudCeiling: number; - cloudCover: number; - dewPoint: number; - evapotranspiration: number; - freezingRainIntensity: number; - humidity: number; - iceAccumulation: number; - iceAccumulationLwe: number; - precipitationProbability: number; - pressureSurfaceLevel: number; - rainAccumulation: number; - rainAccumulationLwe: number; - rainIntensity: number; - sleetAccumulation: number; - sleetAccumulationLwe: number; - sleetIntensity: number; - snowAccumulation: number; - snowAccumulationLwe: number; - snowIntensity: number; - temperature: number; - temperatureApparent: number; - visibility: number; - weatherCode: number; - windDirection: number; - windGust: number; - windSpeed: number; -}>; - -export type WeatherForecastHourlyCharts = { - temperatures: WeatherForecastHourlyTemperatureChart; - humidities: WeatherForecastHourlyHumidityChart; - windSpeeds: WeatherForecastHourlyWindSpeedChart; - precipitations: WeatherForecastHourlyAccumulationChart; -}; - -export type WeatherForecastHourlyTemperatureChart = Array<{ - time: string; - temperature: number; - temperatureApparent: number; -}>; - -export type WeatherForecastHourlyHumidityChart = Array<{ - time: string; - humidity: number; -}>; - -export type WeatherForecastHourlyWindSpeedChart = Array<{ - time: string; - windSpeed: number; -}>; - -export type WeatherForecastHourlyAccumulationChart = Array<{ - time: string; - rainAccumulation: number; - sleetAccumulation: number; - snowAccumulation: number; - iceAccumulation: number; -}>; - -export type WeatherForecastDailyResponse = { - timelines: { - daily: Array<{ - time: Date; - values: { - cloudBaseAvg: number; - cloudBaseMax: number; - cloudBaseMin: number; - cloudCeilingAvg: number; - cloudCeilingMax: number; - cloudCeilingMin: number; - cloudCoverAvg: number; - cloudCoverMax: number; - cloudCoverMin: number; - dewPointAvg: number; - dewPointMax: number; - dewPointMin: number; - evapotranspirationAvg: number; - evapotranspirationMax: number; - evapotranspirationMin: number; - evapotranspirationSum: number; - freezingRainIntensityAvg: number; - freezingRainIntensityMax: number; - freezingRainIntensityMin: number; - humidityAvg: number; - humidityMax: number; - humidityMin: number; - iceAccumulationAvg: number; - iceAccumulationLweAvg: number; - iceAccumulationLweMax: number; - iceAccumulationLweMin: number; - iceAccumulationLweSum: number; - iceAccumulationMax: number; - iceAccumulationMin: number; - iceAccumulationSum: number; - moonriseTime: Date; - moonsetTime: Date; - precipitationProbabilityAvg: number; - precipitationProbabilityMax: number; - precipitationProbabilityMin: number; - pressureSurfaceLevelAvg: number; - pressureSurfaceLevelMax: number; - pressureSurfaceLevelMin: number; - rainAccumulationAvg: number; - rainAccumulationLweAvg: number; - rainAccumulationLweMax: number; - rainAccumulationLweMin: number; - rainAccumulationMax: number; - rainAccumulationMin: number; - rainAccumulationSum: number; - rainIntensityAvg: number; - rainIntensityMax: number; - rainIntensityMin: number; - sleetAccumulationAvg: number; - sleetAccumulationLweAvg: number; - sleetAccumulationLweMax: number; - sleetAccumulationLweMin: number; - sleetAccumulationLweSum: number; - sleetAccumulationMax: number; - sleetAccumulationMin: number; - sleetAccumulationSum: number; - sleetIntensityAvg: number; - sleetIntensityMax: number; - sleetIntensityMin: number; - snowAccumulationAvg: number; - snowAccumulationLweAvg: number; - snowAccumulationLweMax: number; - snowAccumulationLweMin: number; - snowAccumulationLweSum: number; - snowAccumulationMax: number; - snowAccumulationMin: number; - snowAccumulationSum: number; - snowIntensityAvg: number; - snowIntensityMax: number; - snowIntensityMin: number; - sunriseTime: Date; - sunsetTime: Date; - temperatureApparentAvg: number; - temperatureApparentMax: number; - temperatureApparentMin: number; - temperatureAvg: number; - temperatureMax: number; - temperatureMin: number; - uvHealthConcernAvg: number; - uvHealthConcernMax: number; - uvHealthConcernMin: number; - uvIndexAvg: number; - uvIndexMax: number; - uvIndexMin: number; - visibilityAvg: number; - visibilityMax: number; - visibilityMin: number; - weatherCodeMax: number; - weatherCodeMin: number; - windDirectionAvg: number; - windGustAvg: number; - windGustMax: number; - windGustMin: number; - windSpeedAvg: number; - windSpeedMax: number; - windSpeedMin: number; - }; - }>; - }; - location: { - lat: number; - lon: number; - }; -}; - -export type WeatherForecastDaily = Array<{ - time: Date; - cloudBaseAvg: number; - cloudBaseMax: number; - cloudBaseMin: number; - cloudCeilingAvg: number; - cloudCeilingMax: number; - cloudCeilingMin: number; - cloudCoverAvg: number; - cloudCoverMax: number; - cloudCoverMin: number; - dewPointAvg: number; - dewPointMax: number; - dewPointMin: number; - evapotranspirationAvg: number; - evapotranspirationMax: number; - evapotranspirationMin: number; - evapotranspirationSum: number; - freezingRainIntensityAvg: number; - freezingRainIntensityMax: number; - freezingRainIntensityMin: number; - humidityAvg: number; - humidityMax: number; - humidityMin: number; - iceAccumulationAvg: number; - iceAccumulationLweAvg: number; - iceAccumulationLweMax: number; - iceAccumulationLweMin: number; - iceAccumulationLweSum: number; - iceAccumulationMax: number; - iceAccumulationMin: number; - iceAccumulationSum: number; - moonriseTime: Date; - moonsetTime: Date; - precipitationProbabilityAvg: number; - precipitationProbabilityMax: number; - precipitationProbabilityMin: number; - pressureSurfaceLevelAvg: number; - pressureSurfaceLevelMax: number; - pressureSurfaceLevelMin: number; - rainAccumulationAvg: number; - rainAccumulationLweAvg: number; - rainAccumulationLweMax: number; - rainAccumulationLweMin: number; - rainAccumulationMax: number; - rainAccumulationMin: number; - rainAccumulationSum: number; - rainIntensityAvg: number; - rainIntensityMax: number; - rainIntensityMin: number; - sleetAccumulationAvg: number; - sleetAccumulationLweAvg: number; - sleetAccumulationLweMax: number; - sleetAccumulationLweMin: number; - sleetAccumulationLweSum: number; - sleetAccumulationMax: number; - sleetAccumulationMin: number; - sleetAccumulationSum: number; - sleetIntensityAvg: number; - sleetIntensityMax: number; - sleetIntensityMin: number; - snowAccumulationAvg: number; - snowAccumulationLweAvg: number; - snowAccumulationLweMax: number; - snowAccumulationLweMin: number; - snowAccumulationLweSum: number; - snowAccumulationMax: number; - snowAccumulationMin: number; - snowAccumulationSum: number; - snowIntensityAvg: number; - snowIntensityMax: number; - snowIntensityMin: number; - sunriseTime: Date; - sunsetTime: Date; - temperatureApparentAvg: number; - temperatureApparentMax: number; - temperatureApparentMin: number; - temperatureAvg: number; - temperatureMax: number; - temperatureMin: number; - uvHealthConcernAvg: number; - uvHealthConcernMax: number; - uvHealthConcernMin: number; - uvIndexAvg: number; - uvIndexMax: number; - uvIndexMin: number; - visibilityAvg: number; - visibilityMax: number; - visibilityMin: number; - weatherCodeMax: number; - weatherCodeMin: number; - windDirectionAvg: number; - windGustAvg: number; - windGustMax: number; - windGustMin: number; - windSpeedAvg: number; - windSpeedMax: number; - windSpeedMin: number; -}>; - -export type WeatherForecastDailyCharts = { - temperatures: WeatherForecastDailyTemperatureChart; - humidities: WeatherForecastDailyHumidityChart; - windSpeeds: WeatherForecastDailyWindSpeedChart; - precipitations: WeatherForecastDailyAccumulationChart; -}; - -export type WeatherForecastDailyTemperatureChart = Array<{ - time: string; - temperatureMin: number; - temperatureMax: number; - temperatureAvg: number; - temperatureRange: [number, number]; -}>; - -export type WeatherForecastDailyHumidityChart = Array<{ - time: string; - humidityMin: number; - humidityMax: number; - humidityAvg: number; - humidityRange: [number, number]; -}>; - -export type WeatherForecastDailyWindSpeedChart = Array<{ - time: string; - windSpeedMin: number; - windSpeedMax: number; - windSpeedAvg: number; - windSpeedRange: [number, number]; -}>; - -export type WeatherForecastDailyAccumulationChart = Array<{ - time: string; - rainAccumulationMin: number; - rainAccumulationMax: number; - rainAccumulationAvg: number; - rainAccumulationRange: [number, number]; - rainAccumulationSum: number; - sleetAccumulationMin: number; - sleetAccumulationMax: number; - sleetAccumulationAvg: number; - sleetAccumulationRange: [number, number]; - sleetAccumulationSum: number; - snowAccumulationMin: number; - snowAccumulationMax: number; - snowAccumulationAvg: number; - snowAccumulationRange: [number, number]; - snowAccumulationSum: number; - iceAccumulationMin: number; - iceAccumulationMax: number; - iceAccumulationAvg: number; - iceAccumulationRange: [number, number]; - iceAccumulationSum: number; -}>; - -export type WeatherCodeMap = Record; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 91a744f..a4d2930 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,6 @@ import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; +import { ZodSchema } from "zod"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -33,3 +34,22 @@ export function getWindDirectionCardinalFromDegrees(degrees: number): string { // cardinal direction (N -> NNW) from the above array return cardinals[Math.round((degrees % 360) / 22.5)] || "N"; } + +// +// Extracts the fields from a flat Zod schema +// +export function getZodSchemaFieldsShallow( + schema: ZodSchema, +): Record { + const fields: Record = {}; + const proxy = new Proxy(fields, { + get(_, key) { + if (key === "then" || typeof key !== "string") { + return; + } + fields[key] = true; + }, + }); + schema.safeParse(proxy); + return fields; +}