Improve location selection for weather

This commit is contained in:
Meier Lukas
2023-06-10 23:53:04 +02:00
parent a5f3d48a71
commit d00a317202
10 changed files with 363 additions and 112 deletions

View File

@@ -1,9 +1,9 @@
import { Center, Group, Skeleton, Stack, Text, Title } from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconArrowDownRight, IconArrowUpRight, IconCloudRain } from '@tabler/icons-react';
import { api } from '~/utils/api';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { useWeatherForCity } from './useWeatherForCity';
import { WeatherIcon } from './WeatherIcon';
const definition = defineWidget({
@@ -15,8 +15,12 @@ const definition = defineWidget({
defaultValue: false,
},
location: {
type: 'text',
defaultValue: 'Paris',
type: 'location',
defaultValue: {
name: 'Paris',
latitude: 48.85341,
longitude: 2.3488,
},
},
},
gridstack: {
@@ -35,8 +39,8 @@ interface WeatherTileProps {
}
function WeatherTile({ widget }: WeatherTileProps) {
const { data: weather, isLoading, isError } = useWeatherForCity(widget.properties.location);
const { width, height, ref } = useElementSize();
const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location);
const { width, ref } = useElementSize();
if (isLoading) {
return (
@@ -77,10 +81,10 @@ function WeatherTile({ widget }: WeatherTileProps) {
style={{ height: '100%', width: '100%' }}
>
<Group align="center" position="center" spacing="xs">
<WeatherIcon code={weather!.current_weather.weathercode} />
<WeatherIcon code={weather.current_weather.weathercode} />
<Title>
{getPerferedUnit(
weather!.current_weather.temperature,
weather.current_weather.temperature,
widget.properties.displayInFahrenheit
)}
</Title>
@@ -89,12 +93,12 @@ function WeatherTile({ widget }: WeatherTileProps) {
<Group noWrap spacing="xs">
<IconArrowUpRight />
{getPerferedUnit(
weather!.daily.temperature_2m_max[0],
weather.daily.temperature_2m_max[0],
widget.properties.displayInFahrenheit
)}
<IconArrowDownRight />
{getPerferedUnit(
weather!.daily.temperature_2m_min[0],
weather.daily.temperature_2m_min[0],
widget.properties.displayInFahrenheit
)}
</Group>

View File

@@ -1,41 +0,0 @@
// To parse this data:
//
// import { Convert, WeatherResponse } from "./file";
//
// const weatherResponse = Convert.toWeatherResponse(json);
//
// These functions will throw an error if the JSON doesn't
// match the expected interface, even if the JSON is valid.
export interface WeatherResponse {
current_weather: CurrentWeather;
utc_offset_seconds: number;
latitude: number;
elevation: number;
longitude: number;
generationtime_ms: number;
daily_units: DailyUnits;
daily: Daily;
}
export interface CurrentWeather {
winddirection: number;
windspeed: number;
time: string;
weathercode: number;
temperature: number;
}
export interface Daily {
temperature_2m_max: number[];
time: Date[];
temperature_2m_min: number[];
weathercode: number[];
}
export interface DailyUnits {
temperature_2m_max: string;
temperature_2m_min: string;
time: string;
weathercode: string;
}

View File

@@ -1,60 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { WeatherResponse } from './types';
/**
* Requests the weather of the specified city
* @param cityName name of the city where the weather should be requested
* @returns weather of specified city
*/
export const useWeatherForCity = (cityName: string) => {
const {
data: city,
isLoading,
isError,
} = useQuery({
queryKey: ['weatherCity', { cityName }],
queryFn: () => fetchCity(cityName),
cacheTime: 1000 * 60 * 60 * 24, // the city is cached for 24 hours
staleTime: Infinity, // the city is never considered stale
});
const weatherQuery = useQuery({
queryKey: ['weather', { cityName }],
queryFn: () => fetchWeather(city?.results[0]),
enabled: Boolean(city),
cacheTime: 1000 * 60 * 60 * 6, // the weather is cached for 6 hours
staleTime: 1000 * 60 * 5, // the weather is considered stale after 5 minutes
});
return {
...weatherQuery,
isLoading: weatherQuery.isLoading || isLoading,
isError: weatherQuery.isError || isError,
};
};
/**
* Requests the coordinates of a city
* @param cityName name of city
* @returns list with all coordinates for citites with specified name
*/
const fetchCity = async (cityName: string) => {
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${cityName}`);
return (await res.json()) as { results: Coordinates[] };
};
/**
* Requests the weather of specific coordinates
* @param coordinates of the location the weather should be fetched
* @returns weather of specified coordinates
*/
async function fetchWeather(coordinates?: Coordinates) {
if (!coordinates) return null;
const { longitude, latitude } = coordinates;
const res = await fetch(
`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min&current_weather=true&timezone=Europe%2FLondon`
);
// eslint-disable-next-line consistent-return
return (await res.json()) as WeatherResponse;
}
type Coordinates = { latitude: number; longitude: number };

View File

@@ -40,7 +40,8 @@ export type IWidgetOptionValue =
| INumberInputOptionValue
| IDraggableListInputValue
| IDraggableEditableListInputValue<any>
| IMultipleTextInputOptionValue;
| IMultipleTextInputOptionValue
| ILocationOptionValue;
// Interface for data type
interface DataType {
@@ -95,6 +96,11 @@ export type ISliderInputOptionValue = {
inputProps?: Partial<SliderProps>;
};
type ILocationOptionValue = {
type: 'location';
defaultValue: { latitude: number; longitude: number };
};
// will show a sortable list that can have sub settings
export type IDraggableListInputValue = {
type: 'draggable-list';