mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-10 07:25:48 +01:00
✨ Improve location selection for weather
This commit is contained in:
34
public/locales/en/widgets/location.json
Normal file
34
public/locales/en/widgets/location.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"form": {
|
||||||
|
"field": {
|
||||||
|
"query": "City / postal code",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
},
|
||||||
|
"button": {
|
||||||
|
"search": {
|
||||||
|
"label": "Search",
|
||||||
|
"disabledTooltip": "Please choose a city / postal code first"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"empty": "Unknown location"
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"title": "Choose a location",
|
||||||
|
"table": {
|
||||||
|
"header": {
|
||||||
|
"city": "City",
|
||||||
|
"country": "Country",
|
||||||
|
"coordinates": "Coordinates",
|
||||||
|
"population": "Population"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"select": "Select {{city}}, {{countryCode}}"
|
||||||
|
},
|
||||||
|
"population": {
|
||||||
|
"fallback": "Unknown",
|
||||||
|
"count": "{{count}} people"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Group,
|
||||||
|
TextInput,
|
||||||
|
Button,
|
||||||
|
NumberInput,
|
||||||
|
Modal,
|
||||||
|
Table,
|
||||||
|
Tooltip,
|
||||||
|
ActionIcon,
|
||||||
|
Loader,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useDisclosure } from '@mantine/hooks';
|
||||||
|
import { IconListSearch, IconClick } from '@tabler/icons-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { IntegrationOptionsValueType } from '../WidgetsEditModal';
|
||||||
|
import { City } from '~/server/api/routers/weather';
|
||||||
|
import { api } from '~/utils/api';
|
||||||
|
|
||||||
|
type LocationSelectionProps = {
|
||||||
|
widgetId: string;
|
||||||
|
propName: string;
|
||||||
|
value: any;
|
||||||
|
handleChange: (key: string, value: IntegrationOptionsValueType) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LocationSelection = ({
|
||||||
|
widgetId,
|
||||||
|
propName: key,
|
||||||
|
value,
|
||||||
|
handleChange,
|
||||||
|
}: LocationSelectionProps) => {
|
||||||
|
const { t } = useTranslation('widgets/location');
|
||||||
|
const [query, setQuery] = useState(value.name ?? '');
|
||||||
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
|
const selectionEnabled = query.length > 1;
|
||||||
|
const EMPTY_LOCATION = t('form.empty');
|
||||||
|
|
||||||
|
const onCitySelected = (city: City) => {
|
||||||
|
close();
|
||||||
|
handleChange(key, {
|
||||||
|
name: city.name,
|
||||||
|
latitude: city.latitude,
|
||||||
|
longitude: city.longitude,
|
||||||
|
});
|
||||||
|
setQuery(city.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card>
|
||||||
|
<Stack spacing="xs">
|
||||||
|
<Title order={5}>{t(`modules/${widgetId}:descriptor.settings.${key}.label`)}</Title>
|
||||||
|
|
||||||
|
<Group noWrap align="end">
|
||||||
|
<TextInput
|
||||||
|
w="100%"
|
||||||
|
label={t('form.field.query')}
|
||||||
|
value={query}
|
||||||
|
onChange={(ev) => {
|
||||||
|
setQuery(ev.currentTarget.value);
|
||||||
|
handleChange(key, {
|
||||||
|
name: ev.currentTarget.value,
|
||||||
|
longitude: '',
|
||||||
|
latitude: '',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Tooltip hidden={selectionEnabled} label={t('form.button.search.disabledTooltip')}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
disabled={!selectionEnabled}
|
||||||
|
onClick={() => {
|
||||||
|
if (selectionEnabled) open();
|
||||||
|
}}
|
||||||
|
variant="light"
|
||||||
|
leftIcon={<IconListSearch size={16} />}
|
||||||
|
>
|
||||||
|
{t('form.button.search.label')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group grow>
|
||||||
|
<NumberInput
|
||||||
|
value={value.latitude}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (typeof v !== 'number') return;
|
||||||
|
handleChange(key, {
|
||||||
|
...value,
|
||||||
|
name: EMPTY_LOCATION,
|
||||||
|
latitude: v,
|
||||||
|
});
|
||||||
|
setQuery(EMPTY_LOCATION);
|
||||||
|
}}
|
||||||
|
precision={5}
|
||||||
|
label={t('form.field.latitude')}
|
||||||
|
hideControls
|
||||||
|
/>
|
||||||
|
<NumberInput
|
||||||
|
value={value.longitude}
|
||||||
|
onChange={(v) => {
|
||||||
|
if (typeof v !== 'number') return;
|
||||||
|
handleChange(key, {
|
||||||
|
...value,
|
||||||
|
name: EMPTY_LOCATION,
|
||||||
|
longitude: v,
|
||||||
|
});
|
||||||
|
setQuery(EMPTY_LOCATION);
|
||||||
|
}}
|
||||||
|
precision={5}
|
||||||
|
label={t('form.field.longitude')}
|
||||||
|
hideControls
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
<CitySelectModal
|
||||||
|
opened={opened}
|
||||||
|
closeModal={close}
|
||||||
|
query={query}
|
||||||
|
onCitySelected={onCitySelected}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CitySelectModalProps = {
|
||||||
|
opened: boolean;
|
||||||
|
closeModal: () => void;
|
||||||
|
query: string;
|
||||||
|
onCitySelected: (location: City) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CitySelectModal = ({ opened, closeModal, query, onCitySelected }: CitySelectModalProps) => {
|
||||||
|
const { t } = useTranslation('widgets/location');
|
||||||
|
const { isLoading, data } = api.weather.findCity.useQuery(
|
||||||
|
{ query },
|
||||||
|
{
|
||||||
|
enabled: opened,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
refetchOnMount: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Title order={4}>
|
||||||
|
{t('modal.title')} - {query}
|
||||||
|
</Title>
|
||||||
|
}
|
||||||
|
size="xl"
|
||||||
|
opened={opened}
|
||||||
|
onClose={closeModal}
|
||||||
|
zIndex={250}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<Table striped>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style={{ width: '70%' }}>{t('modal.table.header.city')}</th>
|
||||||
|
<th style={{ width: '50%' }}>{t('modal.table.header.country')}</th>
|
||||||
|
<th>{t('modal.table.header.coordinates')}</th>
|
||||||
|
<th>{t('modal.table.header.population')}</th>
|
||||||
|
<th style={{ width: 40 }} />
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{isLoading && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={5}>
|
||||||
|
<Group position="center">
|
||||||
|
<Loader />
|
||||||
|
</Group>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{data?.results.map((city) => (
|
||||||
|
<tr key={city.id}>
|
||||||
|
<td>
|
||||||
|
<Text style={{ whiteSpace: 'nowrap' }}>{city.name}</Text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Text style={{ whiteSpace: 'nowrap' }}>{city.country}</Text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Text style={{ whiteSpace: 'nowrap' }}>
|
||||||
|
{city.latitude}, {city.longitude}
|
||||||
|
</Text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{city.population ? (
|
||||||
|
<Text style={{ whiteSpace: 'nowrap' }}>
|
||||||
|
{t('modal.population.count', { count: city.population })}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text color="dimmed"> {t('modal.population.fallback')}</Text>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Tooltip
|
||||||
|
label={t('modal.table.action.select', {
|
||||||
|
city: city.name,
|
||||||
|
countryCode: city.country_code,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
color="red"
|
||||||
|
variant="subtle"
|
||||||
|
onClick={() => {
|
||||||
|
onCitySelected(city);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconClick size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
<Group position="right">
|
||||||
|
<Button variant="light" onClick={() => closeModal()}>
|
||||||
|
{t('common:cancel')}
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -26,6 +26,7 @@ import Widgets from '../../../../widgets';
|
|||||||
import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets';
|
import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets';
|
||||||
import { IWidget } from '../../../../widgets/widgets';
|
import { IWidget } from '../../../../widgets/widgets';
|
||||||
import { DraggableList } from './Inputs/DraggableList';
|
import { DraggableList } from './Inputs/DraggableList';
|
||||||
|
import { LocationSelection } from './Inputs/LocationSelection';
|
||||||
import { StaticDraggableList } from './Inputs/StaticDraggableList';
|
import { StaticDraggableList } from './Inputs/StaticDraggableList';
|
||||||
|
|
||||||
export type WidgetEditModalInnerProps = {
|
export type WidgetEditModalInnerProps = {
|
||||||
@@ -35,7 +36,7 @@ export type WidgetEditModalInnerProps = {
|
|||||||
widgetOptions: IWidget<string, any>['properties'];
|
widgetOptions: IWidget<string, any>['properties'];
|
||||||
};
|
};
|
||||||
|
|
||||||
type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
|
export type IntegrationOptionsValueType = IWidget<string, any>['properties'][string];
|
||||||
|
|
||||||
export const WidgetsEditModal = ({
|
export const WidgetsEditModal = ({
|
||||||
context,
|
context,
|
||||||
@@ -200,6 +201,16 @@ const WidgetOptionTypeSwitch: FC<{
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
case 'location':
|
||||||
|
return (
|
||||||
|
<LocationSelection
|
||||||
|
propName={key}
|
||||||
|
value={value}
|
||||||
|
handleChange={handleChange}
|
||||||
|
widgetId={widgetId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
case 'draggable-list':
|
case 'draggable-list':
|
||||||
/* eslint-disable no-case-declarations */
|
/* eslint-disable no-case-declarations */
|
||||||
const typedVal = value as IDraggableListInputValue['defaultValue'];
|
const typedVal = value as IDraggableListInputValue['defaultValue'];
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { mediaServerRouter } from './routers/media-server';
|
|||||||
import { overseerrRouter } from './routers/overseerr';
|
import { overseerrRouter } from './routers/overseerr';
|
||||||
import { usenetRouter } from './routers/usenet/router';
|
import { usenetRouter } from './routers/usenet/router';
|
||||||
import { calendarRouter } from './routers/calendar';
|
import { calendarRouter } from './routers/calendar';
|
||||||
|
import { weatherRouter } from './routers/weather';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -32,6 +33,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
overseerr: overseerrRouter,
|
overseerr: overseerrRouter,
|
||||||
usenet: usenetRouter,
|
usenet: usenetRouter,
|
||||||
calendar: calendarRouter,
|
calendar: calendarRouter,
|
||||||
|
weather: weatherRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
58
src/server/api/routers/weather.ts
Normal file
58
src/server/api/routers/weather.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
|
const citySchema = z.object({
|
||||||
|
id: z.number(),
|
||||||
|
name: z.string(),
|
||||||
|
country: z.string(),
|
||||||
|
country_code: z.string(),
|
||||||
|
latitude: z.number(),
|
||||||
|
longitude: z.number(),
|
||||||
|
population: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const weatherSchema = z.object({
|
||||||
|
current_weather: z.object({
|
||||||
|
weathercode: z.number(),
|
||||||
|
temperature: z.number(),
|
||||||
|
}),
|
||||||
|
daily: z.object({
|
||||||
|
temperature_2m_max: z.array(z.number()),
|
||||||
|
temperature_2m_min: z.array(z.number()),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const weatherRouter = createTRPCRouter({
|
||||||
|
findCity: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
query: z.string().min(2),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(
|
||||||
|
z.object({
|
||||||
|
results: z.array(citySchema),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`);
|
||||||
|
return res.json();
|
||||||
|
}),
|
||||||
|
at: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
longitude: z.number(),
|
||||||
|
latitude: z.number(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.output(weatherSchema)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=Europe%2FLondon`
|
||||||
|
);
|
||||||
|
return res.json();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type City = z.infer<typeof citySchema>;
|
||||||
|
export type Weather = z.infer<typeof weatherSchema>;
|
||||||
@@ -44,6 +44,7 @@ export const dashboardNamespaces = [
|
|||||||
'modules/bookmark',
|
'modules/bookmark',
|
||||||
'widgets/error-boundary',
|
'widgets/error-boundary',
|
||||||
'widgets/draggable-list',
|
'widgets/draggable-list',
|
||||||
|
'widgets/location',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const loginNamespaces = ['authentication/login'];
|
export const loginNamespaces = ['authentication/login'];
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Center, Group, Skeleton, Stack, Text, Title } from '@mantine/core';
|
import { Center, Group, Skeleton, Stack, Text, Title } from '@mantine/core';
|
||||||
import { useElementSize } from '@mantine/hooks';
|
import { useElementSize } from '@mantine/hooks';
|
||||||
import { IconArrowDownRight, IconArrowUpRight, IconCloudRain } from '@tabler/icons-react';
|
import { IconArrowDownRight, IconArrowUpRight, IconCloudRain } from '@tabler/icons-react';
|
||||||
|
import { api } from '~/utils/api';
|
||||||
import { defineWidget } from '../helper';
|
import { defineWidget } from '../helper';
|
||||||
import { IWidget } from '../widgets';
|
import { IWidget } from '../widgets';
|
||||||
import { useWeatherForCity } from './useWeatherForCity';
|
|
||||||
import { WeatherIcon } from './WeatherIcon';
|
import { WeatherIcon } from './WeatherIcon';
|
||||||
|
|
||||||
const definition = defineWidget({
|
const definition = defineWidget({
|
||||||
@@ -15,8 +15,12 @@ const definition = defineWidget({
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
},
|
},
|
||||||
location: {
|
location: {
|
||||||
type: 'text',
|
type: 'location',
|
||||||
defaultValue: 'Paris',
|
defaultValue: {
|
||||||
|
name: 'Paris',
|
||||||
|
latitude: 48.85341,
|
||||||
|
longitude: 2.3488,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
gridstack: {
|
gridstack: {
|
||||||
@@ -35,8 +39,8 @@ interface WeatherTileProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function WeatherTile({ widget }: WeatherTileProps) {
|
function WeatherTile({ widget }: WeatherTileProps) {
|
||||||
const { data: weather, isLoading, isError } = useWeatherForCity(widget.properties.location);
|
const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location);
|
||||||
const { width, height, ref } = useElementSize();
|
const { width, ref } = useElementSize();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -77,10 +81,10 @@ function WeatherTile({ widget }: WeatherTileProps) {
|
|||||||
style={{ height: '100%', width: '100%' }}
|
style={{ height: '100%', width: '100%' }}
|
||||||
>
|
>
|
||||||
<Group align="center" position="center" spacing="xs">
|
<Group align="center" position="center" spacing="xs">
|
||||||
<WeatherIcon code={weather!.current_weather.weathercode} />
|
<WeatherIcon code={weather.current_weather.weathercode} />
|
||||||
<Title>
|
<Title>
|
||||||
{getPerferedUnit(
|
{getPerferedUnit(
|
||||||
weather!.current_weather.temperature,
|
weather.current_weather.temperature,
|
||||||
widget.properties.displayInFahrenheit
|
widget.properties.displayInFahrenheit
|
||||||
)}
|
)}
|
||||||
</Title>
|
</Title>
|
||||||
@@ -89,12 +93,12 @@ function WeatherTile({ widget }: WeatherTileProps) {
|
|||||||
<Group noWrap spacing="xs">
|
<Group noWrap spacing="xs">
|
||||||
<IconArrowUpRight />
|
<IconArrowUpRight />
|
||||||
{getPerferedUnit(
|
{getPerferedUnit(
|
||||||
weather!.daily.temperature_2m_max[0],
|
weather.daily.temperature_2m_max[0],
|
||||||
widget.properties.displayInFahrenheit
|
widget.properties.displayInFahrenheit
|
||||||
)}
|
)}
|
||||||
<IconArrowDownRight />
|
<IconArrowDownRight />
|
||||||
{getPerferedUnit(
|
{getPerferedUnit(
|
||||||
weather!.daily.temperature_2m_min[0],
|
weather.daily.temperature_2m_min[0],
|
||||||
widget.properties.displayInFahrenheit
|
widget.properties.displayInFahrenheit
|
||||||
)}
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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¤t_weather=true&timezone=Europe%2FLondon`
|
|
||||||
);
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
return (await res.json()) as WeatherResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Coordinates = { latitude: number; longitude: number };
|
|
||||||
@@ -40,7 +40,8 @@ export type IWidgetOptionValue =
|
|||||||
| INumberInputOptionValue
|
| INumberInputOptionValue
|
||||||
| IDraggableListInputValue
|
| IDraggableListInputValue
|
||||||
| IDraggableEditableListInputValue<any>
|
| IDraggableEditableListInputValue<any>
|
||||||
| IMultipleTextInputOptionValue;
|
| IMultipleTextInputOptionValue
|
||||||
|
| ILocationOptionValue;
|
||||||
|
|
||||||
// Interface for data type
|
// Interface for data type
|
||||||
interface DataType {
|
interface DataType {
|
||||||
@@ -95,6 +96,11 @@ export type ISliderInputOptionValue = {
|
|||||||
inputProps?: Partial<SliderProps>;
|
inputProps?: Partial<SliderProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ILocationOptionValue = {
|
||||||
|
type: 'location';
|
||||||
|
defaultValue: { latitude: number; longitude: number };
|
||||||
|
};
|
||||||
|
|
||||||
// will show a sortable list that can have sub settings
|
// will show a sortable list that can have sub settings
|
||||||
export type IDraggableListInputValue = {
|
export type IDraggableListInputValue = {
|
||||||
type: 'draggable-list';
|
type: 'draggable-list';
|
||||||
|
|||||||
Reference in New Issue
Block a user