Skip to content

Commit

Permalink
Use timelines API to reduce requests from three to one (#28)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
timmo001 authored Sep 9, 2024
1 parent 36100da commit 5250d5e
Show file tree
Hide file tree
Showing 16 changed files with 752 additions and 563 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 2 additions & 4 deletions src/app/_components/forecast-daily.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
22 changes: 12 additions & 10 deletions src/app/_components/forecast-hourly.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -80,12 +78,16 @@ export function ForecastHourly() {
{weatherCode[item.weatherCode] || "Unknown"}
</span>
</div>
<div className="flex flex-row items-center gap-1">
<span className="text-xl font-bold">
{item.temperature.toFixed(1)}
</span>
<span className="text-sm font-semibold">°C</span>
</div>
{item.temperature ? (
<div className="flex flex-row items-center gap-1">
<span className="text-xl font-bold">
{item.temperature.toFixed(1)}
</span>
<span className="text-sm font-semibold">°C</span>
</div>
) : (
<span className="text-xl font-bold">?</span>
)}
</div>
);
})}
Expand Down
14 changes: 6 additions & 8 deletions src/app/_components/forecast-now.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -51,14 +49,14 @@ export function ForecastNow() {
return (
<div className="flex flex-col items-center gap-1 text-center">
{forecastNow.isLoading ? (
<span>Loading realtime forecast...</span>
<span>Loading current forecast...</span>
) : forecastNow.isError ? (
<span>Error loading realtime forecast.</span>
<span>Error loading current forecast.</span>
) : !forecastNow.data ? (
<span>No realtime forecast data.</span>
<span>No current forecast data.</span>
) : "code" in forecastNow.data ? (
<span>
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."}
</span>
Expand Down
7 changes: 2 additions & 5 deletions src/app/daily/_components/daily-charts.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";
import { useQuery } from "@tanstack/react-query";
import {
Area,
CartesianGrid,
ComposedChart,
Line,
Expand All @@ -10,6 +9,8 @@ import {
YAxis,
} from "recharts";

import { WeatherForecastErrorResponse } from "~/lib/schemas/tomorrow-io";
import { WeatherForecastDailyCharts } from "~/lib/schemas/weather";
import {
ChartConfig,
ChartContainer,
Expand All @@ -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: {
Expand Down
7 changes: 2 additions & 5 deletions src/app/hourly/_components/hourly-charts.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/components/location-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
4 changes: 2 additions & 2 deletions src/components/weather-icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function WeatherIcon({
className,
night,
}: {
code: keyof typeof weatherCode;
code: keyof typeof weatherCode | number;
className?: string;
night?: boolean;
}) {
Expand Down Expand Up @@ -53,7 +53,7 @@ export function WeatherIcon({
8000: CloudLightning, // "Thunderstorm"
};

const Icon = weatherIcon[code];
const Icon = weatherIcon[code as number];

if (!Icon) return <Cloud className={className} />;

Expand Down
6 changes: 3 additions & 3 deletions src/lib/local-storage.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down
File renamed without changes.
171 changes: 171 additions & 0 deletions src/lib/schemas/tomorrow-io.ts
Original file line number Diff line number Diff line change
@@ -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<typeof ValuesSchema>;

export const MetaSchema = z.object({
from: z.string(),
to: z.string(),
timestep: z.string(),
});
export type Meta = z.infer<typeof MetaSchema>;

export const IntervalSchema = z.object({
startTime: z.coerce.string(),
values: ValuesSchema.nullish(),
});
export type Interval = z.infer<typeof IntervalSchema>;

export const WarningSchema = z.object({
code: z.number(),
type: z.string(),
message: z.string(),
meta: MetaSchema,
});
export type Warning = z.infer<typeof WarningSchema>;

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<typeof TimelineElementSchema>;

export const DataSchema = z.object({
timelines: z.array(TimelineElementSchema),
warnings: z.array(WarningSchema),
});
export type Data = z.infer<typeof DataSchema>;

export const TimelinesSchema = z.object({
data: DataSchema,
});
export type Timelines = z.infer<typeof TimelinesSchema>;
Loading

0 comments on commit 5250d5e

Please sign in to comment.