From d00a317202c6e713ed8c7cf7d2cdd240c9af04c8 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 10 Jun 2023 23:53:04 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20Improve=20location=20selection?= =?UTF-8?q?=20for=20weather?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/en/widgets/location.json | 34 +++ .../Widgets/Inputs/LocationSelection.tsx | 236 ++++++++++++++++++ .../Tiles/Widgets/WidgetsEditModal.tsx | 13 +- src/server/api/root.ts | 2 + src/server/api/routers/weather.ts | 58 +++++ src/tools/server/translation-namespaces.ts | 1 + src/widgets/weather/WeatherTile.tsx | 22 +- src/widgets/weather/types.ts | 41 --- src/widgets/weather/useWeatherForCity.ts | 60 ----- src/widgets/widgets.ts | 8 +- 10 files changed, 363 insertions(+), 112 deletions(-) create mode 100644 public/locales/en/widgets/location.json create mode 100644 src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx create mode 100644 src/server/api/routers/weather.ts delete mode 100644 src/widgets/weather/types.ts delete mode 100644 src/widgets/weather/useWeatherForCity.ts diff --git a/public/locales/en/widgets/location.json b/public/locales/en/widgets/location.json new file mode 100644 index 000000000..b83836e20 --- /dev/null +++ b/public/locales/en/widgets/location.json @@ -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" + } + } + } +} diff --git a/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx new file mode 100644 index 000000000..d9ca54216 --- /dev/null +++ b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx @@ -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 ( + <> + + + {t(`modules/${widgetId}:descriptor.settings.${key}.label`)} + + + { + setQuery(ev.currentTarget.value); + handleChange(key, { + name: ev.currentTarget.value, + longitude: '', + latitude: '', + }); + }} + /> + + + + + { + if (typeof v !== 'number') return; + handleChange(key, { + ...value, + name: EMPTY_LOCATION, + latitude: v, + }); + setQuery(EMPTY_LOCATION); + }} + precision={5} + label={t('form.field.latitude')} + hideControls + /> + { + if (typeof v !== 'number') return; + handleChange(key, { + ...value, + name: EMPTY_LOCATION, + longitude: v, + }); + setQuery(EMPTY_LOCATION); + }} + precision={5} + label={t('form.field.longitude')} + hideControls + /> + + + + + + ); +}; + +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 ( + + {t('modal.title')} - {query} + + } + size="xl" + opened={opened} + onClose={closeModal} + zIndex={250} + > + + + + + + + + + + + + {isLoading && ( + + + + )} + {data?.results.map((city) => ( + + + + + + + + ))} + +
{t('modal.table.header.city')}{t('modal.table.header.country')}{t('modal.table.header.coordinates')}{t('modal.table.header.population')} +
+ + + +
+ {city.name} + + {city.country} + + + {city.latitude}, {city.longitude} + + + {city.population ? ( + + {t('modal.population.count', { count: city.population })} + + ) : ( + {t('modal.population.fallback')} + )} + + + { + onCitySelected(city); + }} + > + + + +
+ + + +
+
+ ); +}; diff --git a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx index 2d8177f89..ecc817d13 100644 --- a/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx +++ b/src/components/Dashboard/Tiles/Widgets/WidgetsEditModal.tsx @@ -26,6 +26,7 @@ import Widgets from '../../../../widgets'; import type { IDraggableListInputValue, IWidgetOptionValue } from '../../../../widgets/widgets'; import { IWidget } from '../../../../widgets/widgets'; import { DraggableList } from './Inputs/DraggableList'; +import { LocationSelection } from './Inputs/LocationSelection'; import { StaticDraggableList } from './Inputs/StaticDraggableList'; export type WidgetEditModalInnerProps = { @@ -35,7 +36,7 @@ export type WidgetEditModalInnerProps = { widgetOptions: IWidget['properties']; }; -type IntegrationOptionsValueType = IWidget['properties'][string]; +export type IntegrationOptionsValueType = IWidget['properties'][string]; export const WidgetsEditModal = ({ context, @@ -200,6 +201,16 @@ const WidgetOptionTypeSwitch: FC<{ /> ); + case 'location': + return ( + + ); + case 'draggable-list': /* eslint-disable no-case-declarations */ const typedVal = value as IDraggableListInputValue['defaultValue']; diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 5395df87d..4b02b83b4 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -12,6 +12,7 @@ import { mediaServerRouter } from './routers/media-server'; import { overseerrRouter } from './routers/overseerr'; import { usenetRouter } from './routers/usenet/router'; import { calendarRouter } from './routers/calendar'; +import { weatherRouter } from './routers/weather'; /** * This is the primary router for your server. @@ -32,6 +33,7 @@ export const rootRouter = createTRPCRouter({ overseerr: overseerrRouter, usenet: usenetRouter, calendar: calendarRouter, + weather: weatherRouter, }); // export type definition of API diff --git a/src/server/api/routers/weather.ts b/src/server/api/routers/weather.ts new file mode 100644 index 000000000..acb85c9ea --- /dev/null +++ b/src/server/api/routers/weather.ts @@ -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; +export type Weather = z.infer; diff --git a/src/tools/server/translation-namespaces.ts b/src/tools/server/translation-namespaces.ts index 2031682f7..b7f50623f 100644 --- a/src/tools/server/translation-namespaces.ts +++ b/src/tools/server/translation-namespaces.ts @@ -44,6 +44,7 @@ export const dashboardNamespaces = [ 'modules/bookmark', 'widgets/error-boundary', 'widgets/draggable-list', + 'widgets/location', ]; export const loginNamespaces = ['authentication/login']; diff --git a/src/widgets/weather/WeatherTile.tsx b/src/widgets/weather/WeatherTile.tsx index 0f42d9424..5f0440284 100644 --- a/src/widgets/weather/WeatherTile.tsx +++ b/src/widgets/weather/WeatherTile.tsx @@ -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%' }} > - + {getPerferedUnit( - weather!.current_weather.temperature, + weather.current_weather.temperature, widget.properties.displayInFahrenheit )} @@ -89,12 +93,12 @@ function WeatherTile({ widget }: WeatherTileProps) { {getPerferedUnit( - weather!.daily.temperature_2m_max[0], + weather.daily.temperature_2m_max[0], widget.properties.displayInFahrenheit )} {getPerferedUnit( - weather!.daily.temperature_2m_min[0], + weather.daily.temperature_2m_min[0], widget.properties.displayInFahrenheit )} diff --git a/src/widgets/weather/types.ts b/src/widgets/weather/types.ts deleted file mode 100644 index 39fd42135..000000000 --- a/src/widgets/weather/types.ts +++ /dev/null @@ -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; -} diff --git a/src/widgets/weather/useWeatherForCity.ts b/src/widgets/weather/useWeatherForCity.ts deleted file mode 100644 index be7a28ac0..000000000 --- a/src/widgets/weather/useWeatherForCity.ts +++ /dev/null @@ -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 }; diff --git a/src/widgets/widgets.ts b/src/widgets/widgets.ts index f40ca4a4a..be5322b60 100644 --- a/src/widgets/widgets.ts +++ b/src/widgets/widgets.ts @@ -40,7 +40,8 @@ export type IWidgetOptionValue = | INumberInputOptionValue | IDraggableListInputValue | IDraggableEditableListInputValue - | IMultipleTextInputOptionValue; + | IMultipleTextInputOptionValue + | ILocationOptionValue; // Interface for data type interface DataType { @@ -95,6 +96,11 @@ export type ISliderInputOptionValue = { inputProps?: Partial; }; +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'; From f033697579f8888f83d84034ec09059be096d039 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 10 Jun 2023 23:59:32 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9B=20Fix=20translation=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx index d9ca54216..e79db0d60 100644 --- a/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx +++ b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx @@ -197,10 +197,10 @@ const CitySelectModal = ({ opened, closeModal, query, onCitySelected }: CitySele {city.population ? ( - {t('modal.population.count', { count: city.population })} + {t('modal.table.population.count', { count: city.population })} ) : ( - {t('modal.population.fallback')} + {t('modal.table.population.fallback')} )} From c7e066392fce58d9ac09cada0a100ef72bb86688 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 11 Jun 2023 00:33:28 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=90=9B=20Fix=20issue=20with=20old=20m?= =?UTF-8?q?igration=20and=20add=20migration=20for=20location?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/configs/default.json | 776 +++++++++++++------------- src/pages/[slug].tsx | 2 +- src/pages/index.tsx | 2 +- src/server/api/routers/weather.ts | 14 +- src/tools/config/getFrontendConfig.ts | 53 +- src/tools/config/migrateConfig.ts | 10 +- 6 files changed, 458 insertions(+), 399 deletions(-) diff --git a/data/configs/default.json b/data/configs/default.json index 11635a8b2..d690977bb 100644 --- a/data/configs/default.json +++ b/data/configs/default.json @@ -1,389 +1,393 @@ { - "schemaVersion": 1, - "configProperties": { - "name": "default" - }, - "categories": [ - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f", - "position": 1, - "name": "Welcome to Homarr 🎉", - "type": "category" - } - ], - "wrappers": [ - { - "id": "default", - "position": 0 - }, - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326", - "position": 1 - } - ], - "apps": [ - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337", - "name": "Discord", - "url": "https://discord.com/invite/aCsmEV5RgA", - "behaviour": { - "onClickUrl": "https://discord.com/invite/aCsmEV5RgA", - "isOpeningNewTab": true, - "externalUrl": "https://discord.com/invite/aCsmEV5RgA" - }, - "network": { - "enabledStatusChecker": false, - "statusCodes": [ - "200" - ] - }, - "appearance": { - "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "md": { - "location": { - "x": 3, - "y": 1 - }, - "size": { - "width": 3, - "height": 1 - } - }, - "sm": { - "location": { - "x": 2, - "y": 1 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "lg": { - "location": { - "x": 2, - "y": 1 - }, - "size": { - "width": 1, - "height": 1 - } - } - } - }, - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990", - "name": "Donate", - "url": "https://ko-fi.com/ajnart", - "behaviour": { - "onClickUrl": "https://ko-fi.com/ajnart", - "externalUrl": "https://ko-fi.com/ajnart", - "isOpeningNewTab": true - }, - "network": { - "enabledStatusChecker": false, - "statusCodes": [ - "200" - ] - }, - "appearance": { - "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "md": { - "location": { - "x": 2, - "y": 1 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "sm": { - "location": { - "x": 2, - "y": 2 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "lg": { - "location": { - "x": 3, - "y": 1 - }, - "size": { - "width": 1, - "height": 1 - } - } - } - }, - { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330", - "name": "Contribute", - "url": "https://github.com/ajnart/homarr", - "behaviour": { - "onClickUrl": "https://github.com/ajnart/homarr", - "externalUrl": "https://github.com/ajnart/homarr", - "isOpeningNewTab": true - }, - "network": { - "enabledStatusChecker": false, - "statusCodes": [] - }, - "appearance": { - "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "md": { - "location": { - "x": 2, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "sm": { - "location": { - "x": 0, - "y": 2 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "lg": { - "location": { - "x": 4, - "y": 0 - }, - "size": { - "width": 2, - "height": 2 - } - } - } - }, - { - "id": "5df743d9-5cb1-457c-85d2-64ff86855652", - "name": "Documentation", - "url": "https://homarr.dev", - "behaviour": { - "onClickUrl": "https://homarr.dev", - "externalUrl": "https://homarr.dev", - "isOpeningNewTab": true - }, - "network": { - "enabledStatusChecker": false, - "statusCodes": [ - "200" - ] - }, - "appearance": { - "iconUrl": "/imgs/logo/logo.png" - }, - "integration": { - "type": null, - "properties": [] - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "md": { - "location": { - "x": 0, - "y": 1 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "sm": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 1, - "height": 1 - } - }, - "lg": { - "location": { - "x": 0, - "y": 1 - }, - "size": { - "width": 2, - "height": 1 - } - } - } - } - ], - "widgets": [ - { - "id": "971aa859-8570-49a1-8d34-dd5c7b3638d1", - "type": "date", - "properties": { - "display24HourFormat": true - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "sm": { - "location": { - "x": 0, - "y": 1 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "md": { - "location": { - "x": 4, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "lg": { - "location": { - "x": 2, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - } - } - }, - { - "id": "e3004052-6b83-480e-b458-56e8ccdca5f0", - "type": "weather", - "properties": { - "displayInFahrenheit": false, - "location": "Paris" - }, - "area": { - "type": "category", - "properties": { - "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" - } - }, - "shape": { - "md": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "sm": { - "location": { - "x": 1, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - }, - "lg": { - "location": { - "x": 0, - "y": 0 - }, - "size": { - "width": 2, - "height": 1 - } - } - } - } - ], - "settings": { - "common": { - "searchEngine": { - "type": "google", - "properties": {} - } - }, - "customization": { - "layout": { - "enabledLeftSidebar": false, - "enabledRightSidebar": false, - "enabledDocker": false, - "enabledPing": false, - "enabledSearchbar": true - }, - "pageTitle": "Homarr v0.12 ⭐️", - "logoImageUrl": "/imgs/logo/logo.png", - "faviconUrl": "/imgs/favicon/favicon-squared.png", - "backgroundImageUrl": "", - "customCss": "", - "colors": { - "primary": "red", - "secondary": "yellow", - "shade": 7 - }, - "appOpacity": 100 - } + "schemaVersion": 1, + "configProperties": { + "name": "default" + }, + "categories": [ + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f", + "position": 1, + "name": "Welcome to Homarr 🎉", + "type": "category" } + ], + "wrappers": [ + { + "id": "default", + "position": 0 + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a326", + "position": 1 + } + ], + "apps": [ + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a337", + "name": "Discord", + "url": "https://discord.com/invite/aCsmEV5RgA", + "behaviour": { + "onClickUrl": "https://discord.com/invite/aCsmEV5RgA", + "isOpeningNewTab": true, + "externalUrl": "https://discord.com/invite/aCsmEV5RgA" + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [ + "200" + ] + }, + "appearance": { + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/discord.png" + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 3, + "y": 1 + }, + "size": { + "width": 3, + "height": 1 + } + }, + "sm": { + "location": { + "x": 2, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 2, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + } + } + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a990", + "name": "Donate", + "url": "https://ko-fi.com/ajnart", + "behaviour": { + "onClickUrl": "https://ko-fi.com/ajnart", + "externalUrl": "https://ko-fi.com/ajnart", + "isOpeningNewTab": true + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [ + "200" + ] + }, + "appearance": { + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/ko-fi.png" + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 2, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "sm": { + "location": { + "x": 2, + "y": 2 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 3, + "y": 1 + }, + "size": { + "width": 1, + "height": 1 + } + } + } + }, + { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a330", + "name": "Contribute", + "url": "https://github.com/ajnart/homarr", + "behaviour": { + "onClickUrl": "https://github.com/ajnart/homarr", + "externalUrl": "https://github.com/ajnart/homarr", + "isOpeningNewTab": true + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [] + }, + "appearance": { + "iconUrl": "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/github.png" + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 2 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 4, + "y": 0 + }, + "size": { + "width": 2, + "height": 2 + } + } + } + }, + { + "id": "5df743d9-5cb1-457c-85d2-64ff86855652", + "name": "Documentation", + "url": "https://homarr.dev", + "behaviour": { + "onClickUrl": "https://homarr.dev", + "externalUrl": "https://homarr.dev", + "isOpeningNewTab": true + }, + "network": { + "enabledStatusChecker": false, + "statusCodes": [ + "200" + ] + }, + "appearance": { + "iconUrl": "/imgs/logo/logo.png" + }, + "integration": { + "type": null, + "properties": [] + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "sm": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 1, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + } + ], + "widgets": [ + { + "id": "971aa859-8570-49a1-8d34-dd5c7b3638d1", + "type": "date", + "properties": { + "display24HourFormat": true + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "sm": { + "location": { + "x": 0, + "y": 1 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "md": { + "location": { + "x": 4, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 2, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + }, + { + "id": "e3004052-6b83-480e-b458-56e8ccdca5f0", + "type": "weather", + "properties": { + "displayInFahrenheit": false, + "location": { + "name": "Paris", + "latitude": 48.85341, + "longitude": 2.3488 + } + }, + "area": { + "type": "category", + "properties": { + "id": "47af36c0-47c1-4e5b-bfc7-ad645ee6a33f" + } + }, + "shape": { + "md": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "sm": { + "location": { + "x": 1, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + }, + "lg": { + "location": { + "x": 0, + "y": 0 + }, + "size": { + "width": 2, + "height": 1 + } + } + } + } + ], + "settings": { + "common": { + "searchEngine": { + "type": "google", + "properties": {} + } + }, + "customization": { + "layout": { + "enabledLeftSidebar": false, + "enabledRightSidebar": false, + "enabledDocker": false, + "enabledPing": false, + "enabledSearchbar": true + }, + "pageTitle": "Homarr v0.12 ⭐️", + "logoImageUrl": "/imgs/logo/logo.png", + "faviconUrl": "/imgs/favicon/favicon-squared.png", + "backgroundImageUrl": "", + "customCss": "", + "colors": { + "primary": "red", + "secondary": "yellow", + "shade": 7 + }, + "appOpacity": 100 + } + } } diff --git a/src/pages/[slug].tsx b/src/pages/[slug].tsx index 9ce096887..52d2aaade 100644 --- a/src/pages/[slug].tsx +++ b/src/pages/[slug].tsx @@ -38,7 +38,7 @@ export async function getServerSideProps({ }; } - const config = getFrontendConfig(configName as string); + const config = await getFrontendConfig(configName as string); setCookie('config-name', configName, { req, res, diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 7ff04c4cb..d268021ae 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -47,7 +47,7 @@ export async function getServerSideProps({ } const translations = await getServerSideTranslations(dashboardNamespaces, locale, req, res); - const config = getFrontendConfig(configName as string); + const config = await getFrontendConfig(configName as string); return { props: { diff --git a/src/server/api/routers/weather.ts b/src/server/api/routers/weather.ts index acb85c9ea..b2d8c3ace 100644 --- a/src/server/api/routers/weather.ts +++ b/src/server/api/routers/weather.ts @@ -34,10 +34,7 @@ export const weatherRouter = createTRPCRouter({ 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(); - }), + .query(async ({ input }) => fetchCity(input.query)), at: publicProcedure .input( z.object({ @@ -56,3 +53,12 @@ export const weatherRouter = createTRPCRouter({ export type City = z.infer; export type Weather = z.infer; + +const outputSchema = z.object({ + results: z.array(citySchema), +}); + +export const fetchCity = async (query: string) => { + const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${query}`); + return outputSchema.parse(await res.json()); +}; diff --git a/src/tools/config/getFrontendConfig.ts b/src/tools/config/getFrontendConfig.ts index 376522d4c..0a4003697 100644 --- a/src/tools/config/getFrontendConfig.ts +++ b/src/tools/config/getFrontendConfig.ts @@ -1,10 +1,19 @@ import Consola from 'consola'; - -import { ConfigType } from '../../types/config'; +import fs from 'fs'; +import { BackendConfigType, ConfigType } from '../../types/config'; import { getConfig } from './getConfig'; +import { fetchCity } from '~/server/api/routers/weather'; -export const getFrontendConfig = (name: string): ConfigType => { - const config = getConfig(name); +export const getFrontendConfig = async (name: string): Promise => { + let config = getConfig(name); + + const anyWeatherWidgetWithStringLocation = config.widgets.some( + (widget) => widget.type === 'weather' && typeof widget.properties.location === 'string' + ); + + if (anyWeatherWidgetWithStringLocation) { + config = await migrateLocation(config); + } Consola.info(`Requested frontend content of configuration '${name}'`); // If not, return the config @@ -41,3 +50,39 @@ export const getFrontendConfig = (name: string): ConfigType => { })), }; }; + +const migrateLocation = async (config: BackendConfigType) => { + Consola.log('Migrating config file to new location schema...', config.configProperties.name); + + const configName = config.configProperties.name; + const migratedConfig = { + ...config, + widgets: await Promise.all( + config.widgets.map(async (widget) => + widget.type !== 'weather' || typeof widget.properties.location !== 'string' + ? widget + : { + ...widget, + properties: { + ...widget.properties, + location: await fetchCity(widget.properties.location) + .then(({ results }) => ({ + name: results[0].name, + latitude: results[0].latitude, + longitude: results[0].longitude, + })) + .catch(() => ({ + name: '', + latitude: 0, + longitude: 0, + })), + }, + } + ) + ), + }; + + fs.writeFileSync(`./data/configs/${configName}.json`, JSON.stringify(migratedConfig, null, 2)); + + return migratedConfig; +}; diff --git a/src/tools/config/migrateConfig.ts b/src/tools/config/migrateConfig.ts index defb78a26..b85e83e38 100644 --- a/src/tools/config/migrateConfig.ts +++ b/src/tools/config/migrateConfig.ts @@ -2,13 +2,11 @@ import Consola from 'consola'; import { v4 as uuidv4 } from 'uuid'; -import { Config, serviceItem } from '../types'; import { ConfigAppIntegrationType, ConfigAppType, IntegrationType } from '../../types/app'; import { AreaType } from '../../types/area'; import { CategoryType } from '../../types/category'; import { BackendConfigType } from '../../types/config'; import { SearchEngineCommonSettingsType } from '../../types/settings'; -import { IWidget } from '../../widgets/widgets'; import { ICalendarWidget } from '../../widgets/calendar/CalendarTile'; import { IDashDotTile } from '../../widgets/dashDot/DashDotTile'; import { IDateWidget } from '../../widgets/date/DateTile'; @@ -16,6 +14,8 @@ import { ITorrentNetworkTraffic } from '../../widgets/download-speed/TorrentNetw import { ITorrent } from '../../widgets/torrent/TorrentTile'; import { IUsenetWidget } from '../../widgets/useNet/UseNetTile'; import { IWeatherWidget } from '../../widgets/weather/WeatherTile'; +import { IWidget } from '../../widgets/widgets'; +import { Config, serviceItem } from '../types'; export function migrateConfig(config: Config): BackendConfigType { const newConfig: BackendConfigType = { @@ -208,7 +208,11 @@ const migrateModules = (config: Config): IWidget[] => { type: 'weather', properties: { displayInFahrenheit: oldModule.options?.freedomunit?.value ?? false, - location: oldModule.options?.location?.value ?? 'Paris', + location: { + name: oldModule.options?.location?.value ?? '', + latitude: 0, + longitude: 0, + }, }, area: { type: 'wrapper', From a8b44941a067467ecaf2efff43585a5df79e18c3 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 11 Jun 2023 11:59:46 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9B=20Fix=20pull=20request=20issue?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Widgets/Inputs/LocationSelection.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx index e79db0d60..008d07c76 100644 --- a/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx +++ b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx @@ -38,7 +38,7 @@ export const LocationSelection = ({ const [query, setQuery] = useState(value.name ?? ''); const [opened, { open, close }] = useDisclosure(false); const selectionEnabled = query.length > 1; - const EMPTY_LOCATION = t('form.empty'); + const emptyLocation = t('form.empty'); const onCitySelected = (city: City) => { close(); @@ -89,14 +89,14 @@ export const LocationSelection = ({ { - if (typeof v !== 'number') return; + onChange={(inputValue) => { + if (typeof inputValue !== 'number') return; handleChange(key, { ...value, - name: EMPTY_LOCATION, - latitude: v, + name: emptyLocation, + latitude: inputValue, }); - setQuery(EMPTY_LOCATION); + setQuery(emptyLocation); }} precision={5} label={t('form.field.latitude')} @@ -104,14 +104,14 @@ export const LocationSelection = ({ /> { - if (typeof v !== 'number') return; + onChange={(inputValue) => { + if (typeof inputValue !== 'number') return; handleChange(key, { ...value, - name: EMPTY_LOCATION, - longitude: v, + name: emptyLocation, + longitude: inputValue, }); - setQuery(EMPTY_LOCATION); + setQuery(emptyLocation); }} precision={5} label={t('form.field.longitude')} From e29ba6455e5cfc3ae095693e09258b6387c8ee36 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 11 Jun 2023 13:54:15 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=90=9B=20Fix=20pull=20request=20issue?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/locales/en/widgets/location.json | 5 ++--- .../Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/public/locales/en/widgets/location.json b/public/locales/en/widgets/location.json index b83836e20..d2afe05cd 100644 --- a/public/locales/en/widgets/location.json +++ b/public/locales/en/widgets/location.json @@ -26,9 +26,8 @@ "select": "Select {{city}}, {{countryCode}}" }, "population": { - "fallback": "Unknown", - "count": "{{count}} people" + "fallback": "Unknown" } } } -} +} \ No newline at end of file diff --git a/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx index 008d07c76..90ff64b02 100644 --- a/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx +++ b/src/components/Dashboard/Tiles/Widgets/Inputs/LocationSelection.tsx @@ -196,9 +196,7 @@ const CitySelectModal = ({ opened, closeModal, query, onCitySelected }: CitySele {city.population ? ( - - {t('modal.table.population.count', { count: city.population })} - + {city.population} ) : ( {t('modal.table.population.fallback')} )}