Option to show time for a city (#1236)

This commit is contained in:
Tagaishi
2023-08-09 20:33:17 +02:00
committed by GitHub
parent 6460e433a5
commit ffa850b081
10 changed files with 444 additions and 71 deletions

View File

@@ -67,9 +67,12 @@
"dockerode": "^3.3.2", "dockerode": "^3.3.2",
"fily-publish-gridstack": "^0.0.13", "fily-publish-gridstack": "^0.0.13",
"framer-motion": "^10.0.0", "framer-motion": "^10.0.0",
"geo-tz": "^7.0.7",
"html-entities": "^2.3.3", "html-entities": "^2.3.3",
"i18next": "^22.5.1", "i18next": "^22.5.1",
"js-file-download": "^0.4.12", "js-file-download": "^0.4.12",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"next": "13.4.10", "next": "13.4.10",
"next-i18next": "^13.0.0", "next-i18next": "^13.0.0",
"nzbget-api": "^0.0.3", "nzbget-api": "^0.0.3",

View File

@@ -6,6 +6,27 @@
"title": "Settings for Date and Time widget", "title": "Settings for Date and Time widget",
"display24HourFormat": { "display24HourFormat": {
"label": "Display full time (24-hour)" "label": "Display full time (24-hour)"
},
"dateFormat": {
"label": "Date formatting",
"data": {
"hide": "Hide Date"
}
},
"enableTimezone": {
"label": "Display a custom Timezone"
},
"timezoneLocation": {
"label": "Timezone Location"
},
"titleState": {
"label": "City title",
"info": "In case you activate the Timezone option, the name of the city and the timezone code can be shown.<br/>You can also show the city alone or even show none.",
"data": {
"both": "City and Timezone",
"city": "City only",
"none": "None"
}
} }
} }
} }

View File

@@ -19,13 +19,13 @@ import { IconAlertTriangle, IconPlaylistX, IconPlus } from '@tabler/icons-react'
import { Trans, useTranslation } from 'next-i18next'; import { Trans, useTranslation } from 'next-i18next';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { InfoCard } from '../../../InfoCard/InfoCard';
import { useConfigContext } from '../../../../config/provider'; import { useConfigContext } from '../../../../config/provider';
import { useConfigStore } from '../../../../config/store'; import { useConfigStore } from '../../../../config/store';
import { mapObject } from '../../../../tools/client/objects'; import { mapObject } from '../../../../tools/client/objects';
import Widgets from '../../../../widgets'; 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 { InfoCard } from '../../../InfoCard/InfoCard';
import { DraggableList } from './Inputs/DraggableList'; import { DraggableList } from './Inputs/DraggableList';
import { LocationSelection } from './Inputs/LocationSelection'; import { LocationSelection } from './Inputs/LocationSelection';
import { StaticDraggableList } from './Inputs/StaticDraggableList'; import { StaticDraggableList } from './Inputs/StaticDraggableList';
@@ -148,15 +148,17 @@ const WidgetOptionTypeSwitch: FC<{
onChange={(ev) => handleChange(key, ev.currentTarget.checked)} onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
{...option.inputProps} {...option.inputProps}
/> />
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
); );
case 'text': case 'text':
return ( return (
<Stack spacing={0}> <Stack spacing={0}>
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text> <Text size="0.875rem" weight="500">
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<TextInput <TextInput
value={value as string} value={value as string}
@@ -169,8 +171,10 @@ const WidgetOptionTypeSwitch: FC<{
return ( return (
<Stack spacing={0}> <Stack spacing={0}>
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text> <Text size="0.875rem" weight="500">
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<MultiSelect <MultiSelect
data={option.data} data={option.data}
@@ -183,15 +187,26 @@ const WidgetOptionTypeSwitch: FC<{
</Stack> </Stack>
); );
case 'select': case 'select':
const items = typeof option.data === 'function' ? option.data() : option.data;
const data = items.map((dataType) => {
return !dataType.label
? {
value: dataType.value,
label: t(`descriptor.settings.${key}.data.${dataType.value}`),
}
: dataType;
});
return ( return (
<Stack spacing={0}> <Stack spacing={0}>
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text> <Text size="0.875rem" weight="500">
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<Select <Select
defaultValue={option.defaultValue} defaultValue={option.defaultValue}
data={option.data} data={data}
value={value as string} value={value as string}
onChange={(v) => handleChange(key, v ?? option.defaultValue)} onChange={(v) => handleChange(key, v ?? option.defaultValue)}
withinPortal withinPortal
@@ -203,8 +218,10 @@ const WidgetOptionTypeSwitch: FC<{
return ( return (
<Stack spacing={0}> <Stack spacing={0}>
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text> <Text size="0.875rem" weight="500">
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<NumberInput <NumberInput
value={value as number} value={value as number}
@@ -217,8 +234,10 @@ const WidgetOptionTypeSwitch: FC<{
return ( return (
<Stack spacing={0}> <Stack spacing={0}>
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text> <Text size="0.875rem" weight="500">
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<Slider <Slider
label={value} label={value}
@@ -270,7 +289,7 @@ const WidgetOptionTypeSwitch: FC<{
<Stack spacing="xs"> <Stack spacing="xs">
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text>{t(`descriptor.settings.${key}.label`)}</Text> <Text>{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<StaticDraggableList <StaticDraggableList
value={typedVal} value={typedVal}
@@ -298,8 +317,10 @@ const WidgetOptionTypeSwitch: FC<{
return ( return (
<Stack spacing={0}> <Stack spacing={0}>
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text size="0.875rem" weight="500">{t(`descriptor.settings.${key}.label`)}</Text> <Text size="0.875rem" weight="500">
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {t(`descriptor.settings.${key}.label`)}
</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<MultiSelect <MultiSelect
data={value.map((name: any) => ({ value: name, label: name }))} data={value.map((name: any) => ({ value: name, label: name }))}
@@ -324,7 +345,7 @@ const WidgetOptionTypeSwitch: FC<{
<Stack spacing="xs"> <Stack spacing="xs">
<Group align="center" spacing="sm"> <Group align="center" spacing="sm">
<Text>{t(`descriptor.settings.${key}.label`)}</Text> <Text>{t(`descriptor.settings.${key}.label`)}</Text>
{info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link}/>} {info && <InfoCard message={t(`descriptor.settings.${key}.info`)} link={link} />}
</Group> </Group>
<DraggableList <DraggableList
items={Array.from(value).map((v: any) => ({ items={Array.from(value).map((v: any) => ({

View File

@@ -8,12 +8,15 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import Consola from 'consola'; import Consola from 'consola';
import { getCookie } from 'cookies-next'; import { getCookie } from 'cookies-next';
import moment from 'moment-timezone';
import { GetServerSidePropsContext } from 'next'; import { GetServerSidePropsContext } from 'next';
import { appWithTranslation } from 'next-i18next'; import { appWithTranslation } from 'next-i18next';
import { AppProps } from 'next/app'; import { AppProps } from 'next/app';
import Head from 'next/head'; import Head from 'next/head';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import 'video.js/dist/video-js.css'; import 'video.js/dist/video-js.css';
import { getLanguageByCode } from '~/tools/language';
import { ConfigType } from '~/types/config';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
import nextI18nextConfig from '../../next-i18next.config'; import nextI18nextConfig from '../../next-i18next.config';
@@ -35,7 +38,6 @@ import {
getServiceSidePackageAttributes, getServiceSidePackageAttributes,
} from '../tools/server/getPackageVersion'; } from '../tools/server/getPackageVersion';
import { theme } from '../tools/server/theme/theme'; import { theme } from '../tools/server/theme/theme';
import { ConfigType } from '~/types/config';
function App( function App(
this: any, this: any,
@@ -46,13 +48,24 @@ function App(
defaultColorScheme: ColorScheme; defaultColorScheme: ColorScheme;
config?: ConfigType; config?: ConfigType;
configName?: string; configName?: string;
locale: string;
}> }>
) { ) {
const { Component, pageProps } = props; const { Component, pageProps } = props;
// TODO: make mapping from our locales to moment locales
const language = getLanguageByCode(pageProps.locale);
require('moment/locale/' + language.momentLocale);
moment.locale(language.momentLocale);
const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>(props.pageProps.config?.settings.customization.colors.primary || 'red'); const [primaryColor, setPrimaryColor] = useState<MantineTheme['primaryColor']>(
const [secondaryColor, setSecondaryColor] = useState<MantineTheme['primaryColor']>(props.pageProps.config?.settings.customization.colors.secondary || 'orange'); props.pageProps.config?.settings.customization.colors.primary || 'red'
const [primaryShade, setPrimaryShade] = useState<MantineTheme['primaryShade']>(props.pageProps.config?.settings.customization.colors.shade || 6); );
const [secondaryColor, setSecondaryColor] = useState<MantineTheme['primaryColor']>(
props.pageProps.config?.settings.customization.colors.secondary || 'orange'
);
const [primaryShade, setPrimaryShade] = useState<MantineTheme['primaryShade']>(
props.pageProps.config?.settings.customization.colors.shade || 6
);
const colorTheme = { const colorTheme = {
primaryColor, primaryColor,
secondaryColor, secondaryColor,
@@ -172,6 +185,7 @@ App.getInitialProps = ({ ctx }: { ctx: GetServerSidePropsContext }) => {
packageAttributes: getServiceSidePackageAttributes(), packageAttributes: getServiceSidePackageAttributes(),
editModeEnabled: !disableEditMode, editModeEnabled: !disableEditMode,
defaultColorScheme: colorScheme, defaultColorScheme: colorScheme,
locale: ctx.locale ?? 'en',
}, },
}; };
}; };

View File

@@ -12,6 +12,7 @@ import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server'; import { mediaServerRouter } from './routers/media-server';
import { overseerrRouter } from './routers/overseerr'; import { overseerrRouter } from './routers/overseerr';
import { rssRouter } from './routers/rss'; import { rssRouter } from './routers/rss';
import { timezoneRouter } from './routers/timezone';
import { usenetRouter } from './routers/usenet/router'; import { usenetRouter } from './routers/usenet/router';
import { weatherRouter } from './routers/weather'; import { weatherRouter } from './routers/weather';
@@ -22,18 +23,19 @@ import { weatherRouter } from './routers/weather';
*/ */
export const rootRouter = createTRPCRouter({ export const rootRouter = createTRPCRouter({
app: appRouter, app: appRouter,
rss: rssRouter, calendar: calendarRouter,
config: configRouter, config: configRouter,
docker: dockerRouter,
icon: iconRouter,
dashDot: dashDotRouter, dashDot: dashDotRouter,
dnsHole: dnsHoleRouter, dnsHole: dnsHoleRouter,
docker: dockerRouter,
download: downloadRouter, download: downloadRouter,
icon: iconRouter,
mediaRequest: mediaRequestsRouter, mediaRequest: mediaRequestsRouter,
mediaServer: mediaServerRouter, mediaServer: mediaServerRouter,
overseerr: overseerrRouter, overseerr: overseerrRouter,
rss: rssRouter,
timezone: timezoneRouter,
usenet: usenetRouter, usenet: usenetRouter,
calendar: calendarRouter,
weather: weatherRouter, weather: weatherRouter,
}); });

View File

@@ -0,0 +1,17 @@
import { z } from 'zod';
import { find } from 'geo-tz'
import { createTRPCRouter, publicProcedure } from '../trpc';
export const timezoneRouter = createTRPCRouter({
at: publicProcedure
.input(
z.object({
longitude: z.number(),
latitude: z.number(),
})
)
.query(async ({ input }) => {
return find(input.latitude,input.longitude)[0];
}),
})

View File

@@ -1,16 +1,10 @@
export class Language { export type Language = {
shortName: string; shortName: string;
originalName: string; originalName: string;
translatedName: string; translatedName: string;
emoji: string; emoji: string;
momentLocale: string;
constructor(shortName: string, originalName: string, translatedName: string, emoji: string) { };
this.shortName = shortName;
this.originalName = originalName;
this.translatedName = translatedName;
this.emoji = emoji;
}
}
export const languages: Language[] = [ export const languages: Language[] = [
{ {
@@ -18,12 +12,14 @@ export const languages: Language[] = [
originalName: 'Deutsch', originalName: 'Deutsch',
translatedName: 'German', translatedName: 'German',
emoji: '🇩🇪', emoji: '🇩🇪',
momentLocale: 'de',
}, },
{ {
shortName: 'en', shortName: 'en',
originalName: 'English', originalName: 'English',
translatedName: 'English', translatedName: 'English',
emoji: '🇬🇧', emoji: '🇬🇧',
momentLocale: 'en-gb',
}, },
// Danish // Danish
{ {
@@ -31,6 +27,7 @@ export const languages: Language[] = [
originalName: 'Dansk', originalName: 'Dansk',
translatedName: 'Danish', translatedName: 'Danish',
emoji: '🇩🇰', emoji: '🇩🇰',
momentLocale: 'da',
}, },
// Hebrew // Hebrew
{ {
@@ -38,42 +35,49 @@ export const languages: Language[] = [
originalName: 'עברית', originalName: 'עברית',
translatedName: 'Hebrew', translatedName: 'Hebrew',
emoji: '🇮🇱', emoji: '🇮🇱',
momentLocale: 'he',
}, },
{ {
shortName: 'es', shortName: 'es',
originalName: 'Español', originalName: 'Español',
translatedName: 'Spanish', translatedName: 'Spanish',
emoji: '🇪🇸', emoji: '🇪🇸',
momentLocale: 'es',
}, },
{ {
shortName: 'fr', shortName: 'fr',
originalName: 'Français', originalName: 'Français',
translatedName: 'French', translatedName: 'French',
emoji: '🇫🇷', emoji: '🇫🇷',
momentLocale: 'fr',
}, },
{ {
shortName: 'it', shortName: 'it',
originalName: 'Italiano', originalName: 'Italiano',
translatedName: 'Italian', translatedName: 'Italian',
emoji: '🇮🇹', emoji: '🇮🇹',
momentLocale: 'it',
}, },
{ {
shortName: 'ja', shortName: 'ja',
originalName: '日本語', originalName: '日本語',
translatedName: 'Japanese', translatedName: 'Japanese',
emoji: '🇯🇵', emoji: '🇯🇵',
momentLocale: 'ja',
}, },
{ {
shortName: 'ko', shortName: 'ko',
originalName: '한국어', originalName: '한국어',
translatedName: 'Korean', translatedName: 'Korean',
emoji: '🇰🇷', emoji: '🇰🇷',
momentLocale: 'ko',
}, },
{ {
shortName: 'lol', shortName: 'lol',
originalName: 'LOLCAT', originalName: 'LOLCAT',
translatedName: 'LOLCAT', translatedName: 'LOLCAT',
emoji: '🐱', emoji: '🐱',
momentLocale: 'en-gb',
}, },
// Norwegian // Norwegian
{ {
@@ -81,6 +85,7 @@ export const languages: Language[] = [
originalName: 'Norsk', originalName: 'Norsk',
translatedName: 'Norwegian', translatedName: 'Norwegian',
emoji: '🇳🇴', emoji: '🇳🇴',
momentLocale: 'nb',
}, },
// Slovak // Slovak
{ {
@@ -88,36 +93,42 @@ export const languages: Language[] = [
originalName: 'Slovenčina', originalName: 'Slovenčina',
translatedName: 'Slovak', translatedName: 'Slovak',
emoji: '🇸🇰', emoji: '🇸🇰',
momentLocale: 'sk',
}, },
{ {
shortName: 'nl', shortName: 'nl',
originalName: 'Nederlands', originalName: 'Nederlands',
translatedName: 'Dutch', translatedName: 'Dutch',
emoji: '🇳🇱', emoji: '🇳🇱',
momentLocale: 'nl',
}, },
{ {
shortName: 'pl', shortName: 'pl',
originalName: 'Polski', originalName: 'Polski',
translatedName: 'Polish', translatedName: 'Polish',
emoji: '🇵🇱', emoji: '🇵🇱',
momentLocale: 'pl',
}, },
{ {
shortName: 'pt', shortName: 'pt',
originalName: 'Português', originalName: 'Português',
translatedName: 'Portuguese', translatedName: 'Portuguese',
emoji: '🇵🇹', emoji: '🇵🇹',
momentLocale: 'pt',
}, },
{ {
shortName: 'ru', shortName: 'ru',
originalName: 'Русский', originalName: 'Русский',
translatedName: 'Russian', translatedName: 'Russian',
emoji: '🇷🇺', emoji: '🇷🇺',
momentLocale: 'ru',
}, },
{ {
shortName: 'sl', shortName: 'sl',
originalName: 'Slovenščina', originalName: 'Slovenščina',
translatedName: 'Slovenian', translatedName: 'Slovenian',
emoji: '🇸🇮', emoji: '🇸🇮',
momentLocale: 'sl',
}, },
{ {
@@ -125,12 +136,14 @@ export const languages: Language[] = [
originalName: 'Svenska', originalName: 'Svenska',
translatedName: 'Swedish', translatedName: 'Swedish',
emoji: '🇸🇪', emoji: '🇸🇪',
momentLocale: 'sv',
}, },
{ {
shortName: 'uk', shortName: 'uk',
originalName: 'Українська', originalName: 'Українська',
translatedName: 'Ukrainian', translatedName: 'Ukrainian',
emoji: '🇺🇦', emoji: '🇺🇦',
momentLocale: 'uk',
}, },
// Vietnamese // Vietnamese
{ {
@@ -138,37 +151,42 @@ export const languages: Language[] = [
originalName: 'Tiếng Việt', originalName: 'Tiếng Việt',
translatedName: 'Vietnamese', translatedName: 'Vietnamese',
emoji: '🇻🇳', emoji: '🇻🇳',
momentLocale: 'vi',
}, },
{ {
shortName: 'zh', shortName: 'zh',
originalName: '中文', originalName: '中文',
translatedName: 'Chinese', translatedName: 'Chinese',
emoji: '🇨🇳', emoji: '🇨🇳',
momentLocale: 'zh-cn',
}, },
{ {
shortName: 'el', shortName: 'el',
originalName: 'Ελληνικά', originalName: 'Ελληνικά',
translatedName: 'Greek', translatedName: 'Greek',
emoji: '🇬🇷', emoji: '🇬🇷',
momentLocale: 'el',
}, },
{ {
shortName: 'tr', shortName: 'tr',
originalName: 'Türkçe', originalName: 'Türkçe',
translatedName: 'Turkish', translatedName: 'Turkish',
emoji: '🇹🇷', emoji: '🇹🇷',
momentLocale: 'tr',
}, },
{ {
shortName: 'lv', shortName: 'lv',
originalName: 'Latvian', originalName: 'Latvian',
translatedName: 'Latvian', translatedName: 'Latvian',
emoji: '🇱🇻', emoji: '🇱🇻',
momentLocale: 'lv',
}, },
// Croatian
{ {
shortName: 'hr', shortName: 'hr',
originalName: 'Hrvatski', originalName: 'Hrvatski',
translatedName: 'Croatian', translatedName: 'Croatian',
emoji: '🇭🇷', emoji: '🇭🇷',
momentLocale: 'hr',
}, },
]; ];

View File

@@ -1,8 +1,11 @@
import { Stack, Text, Title } from '@mantine/core'; import { Stack, Text, createStyles } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useElementSize } from '@mantine/hooks';
import { IconClock } from '@tabler/icons-react'; import { IconClock } from '@tabler/icons-react';
import dayjs from 'dayjs'; import moment from 'moment-timezone';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { getLanguageByCode } from '~/tools/language';
import { api } from '~/utils/api';
import { useSetSafeInterval } from '../../hooks/useSetSafeInterval'; import { useSetSafeInterval } from '../../hooks/useSetSafeInterval';
import { defineWidget } from '../helper'; import { defineWidget } from '../helper';
@@ -16,6 +19,39 @@ const definition = defineWidget({
type: 'switch', type: 'switch',
defaultValue: false, defaultValue: false,
}, },
dateFormat: {
type: 'select',
defaultValue: 'dddd, MMMM D',
data: () => [
{ value: 'hide' },
{ value: 'dddd, MMMM D', label: moment().format('dddd, MMMM D') },
{ value: 'dddd, D MMMM', label: moment().format('dddd, D MMMM') },
{ value: 'MMM D', label: moment().format('MMM D') },
{ value: 'D MMM', label: moment().format('D MMM') },
{ value: 'DD/MM/YYYY', label: moment().format('DD/MM/YYYY') },
{ value: 'MM/DD/YYYY', label: moment().format('MM/DD/YYYY') },
{ value: 'DD/MM', label: moment().format('DD/MM') },
{ value: 'MM/DD', label: moment().format('MM/DD') },
],
},
enableTimezone: {
type: 'switch',
defaultValue: false,
},
timezoneLocation: {
type: 'location',
defaultValue: {
name: 'Paris',
latitude: 48.85341,
longitude: 2.3488,
},
},
titleState: {
type: 'select',
defaultValue: 'both',
data: [{ value: 'both' }, { value: 'city' }, { value: 'none' }],
info: true,
},
}, },
gridstack: { gridstack: {
minWidth: 1, minWidth: 1,
@@ -33,52 +69,102 @@ interface DateTileProps {
} }
function DateTile({ widget }: DateTileProps) { function DateTile({ widget }: DateTileProps) {
const date = useDateState(); const date = useDateState(
widget.properties.enableTimezone ? widget.properties.timezoneLocation : undefined
);
const formatString = widget.properties.display24HourFormat ? 'HH:mm' : 'h:mm A'; const formatString = widget.properties.display24HourFormat ? 'HH:mm' : 'h:mm A';
const { width, ref } = useElementSize(); const { ref, width } = useElementSize();
const { cx, classes } = useStyles();
return ( return (
<Stack ref={ref} spacing="xs" justify="space-around" align="center" style={{ height: '100%' }}> <Stack ref={ref} className={cx(classes.wrapper, 'dashboard-tile-clock-wrapper')}>
<Title>{dayjs(date).format(formatString)}</Title> {widget.properties.enableTimezone && widget.properties.titleState !== 'none' && (
{width > 200 && <Text size="lg">{dayjs(date).format('dddd, MMMM D')}</Text>} <Text
size={width < 150 ? 'sm' : 'lg'}
className={cx(classes.extras, 'dashboard-tile-clock-city')}
>
{widget.properties.timezoneLocation.name}
{widget.properties.titleState === 'both' && moment(date).format(' (z)')}
</Text>
)}
<Text className={cx(classes.clock, 'dashboard-tile-clock-hour')}>
{moment(date).format(formatString)}
</Text>
{!widget.properties.dateFormat.includes('hide') && (
<Text
size={width < 150 ? 'sm' : 'lg'}
pt="0.2rem"
className={cx(classes.extras, 'dashboard-tile-clock-date')}
>
{moment(date).format(widget.properties.dateFormat)}
</Text>
)}
</Stack> </Stack>
); );
} }
const useStyles = createStyles(()=>({
wrapper:{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-evenly',
alignItems: 'center',
height: '100%',
gap: 0,
},
clock:{
lineHeight: '1',
whiteSpace: 'nowrap',
fontWeight: 700,
fontSize: '2.125rem',
},
extras:{
lineHeight: '1',
whiteSpace: 'nowrap',
}
}))
/** /**
* State which updates when the minute is changing * State which updates when the minute is changing
* @returns current date updated every new minute * @returns current date updated every new minute
*/ */
const useDateState = () => { const useDateState = (location?: { latitude: number; longitude: number }) => {
const [date, setDate] = useState(new Date()); //Gets a timezone from user input location. If location is undefined, then it means it's a local timezone so keep undefined
const { data: timezone } = api.timezone.at.useQuery(location!, {
enabled: location !== undefined,
});
const { locale } = useRouter();
const [date, setDate] = useState(getNewDate(timezone));
const setSafeInterval = useSetSafeInterval(); const setSafeInterval = useSetSafeInterval();
const timeoutRef = useRef<NodeJS.Timeout>(); // reference for initial timeout until first minute change const timeoutRef = useRef<NodeJS.Timeout>(); // reference for initial timeout until first minute change
useEffect(() => { useEffect(() => {
timeoutRef.current = setTimeout(() => { const language = getLanguageByCode(locale ?? 'en');
setDate(new Date()); moment.locale(language.momentLocale);
// Starts intervall which update the date every minute setDate(getNewDate(timezone));
setSafeInterval(() => { timeoutRef.current = setTimeout(
setDate(new Date()); () => {
}, 1000 * 60); setDate(getNewDate(timezone));
}, getMsUntilNextMinute()); // Starts interval which update the date every minute
setSafeInterval(() => {
setDate(getNewDate(timezone));
}, 1000 * 60);
//1 minute - current seconds and milliseconds count
},
1000 * 60 - (1000 * moment().seconds() + moment().milliseconds())
);
return () => timeoutRef.current && clearTimeout(timeoutRef.current); return () => timeoutRef.current && clearTimeout(timeoutRef.current);
}, []); }, [timezone, locale]);
return date; return date;
}; };
// calculates the amount of milliseconds until next minute starts. //Returns a local date if no inputs or returns date from input zone
const getMsUntilNextMinute = () => { const getNewDate = (timezone?: string) => {
const now = new Date(); if (timezone) {
const nextMinute = new Date( return moment().tz(timezone);
now.getFullYear(), }
now.getMonth(), return moment();
now.getDate(),
now.getHours(),
now.getMinutes() + 1
);
return nextMinute.getTime() - now.getTime();
}; };
export default definition; export default definition;

View File

@@ -42,18 +42,19 @@ export type IWidgetOptionValue = (
| IDraggableEditableListInputValue<any> | IDraggableEditableListInputValue<any>
| IMultipleTextInputOptionValue | IMultipleTextInputOptionValue
| ILocationOptionValue | ILocationOptionValue
) & ICommonWidgetOptions; ) &
ICommonWidgetOptions;
// Interface for data type // Interface for data type
interface DataType { interface DataType {
label: string; label?: string;
value: string; value: string;
} }
interface ICommonWidgetOptions { interface ICommonWidgetOptions {
info?: boolean; info?: boolean;
infoLink?: string; infoLink?: string;
}; }
// will show a multi-select with specified data // will show a multi-select with specified data
export type IMultiSelectOptionValue = { export type IMultiSelectOptionValue = {
@@ -67,7 +68,7 @@ export type IMultiSelectOptionValue = {
export type ISelectOptionValue = { export type ISelectOptionValue = {
type: 'select'; type: 'select';
defaultValue: string; defaultValue: string;
data: DataType[]; data: DataType[] | (() => DataType[]);
inputProps?: Partial<SelectProps>; inputProps?: Partial<SelectProps>;
}; };

196
yarn.lock
View File

@@ -2311,6 +2311,32 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@turf/boolean-point-in-polygon@npm:^6.5.0":
version: 6.5.0
resolution: "@turf/boolean-point-in-polygon@npm:6.5.0"
dependencies:
"@turf/helpers": ^6.5.0
"@turf/invariant": ^6.5.0
checksum: 624e54d9956b8f9d955285065f44c80ae66cd4a87e5d893f85871009b62ad9721cca520245a56f8c4401467767c153dda58f67fec0968e3971c7bb3a39617105
languageName: node
linkType: hard
"@turf/helpers@npm:^6.5.0":
version: 6.5.0
resolution: "@turf/helpers@npm:6.5.0"
checksum: d57f746351357838c654e0a9b98be3285a14b447504fd6d59753d90c6d437410bb24805d61c65b612827f07f6c2ade823bb7e56e41a1a946217abccfbd64c117
languageName: node
linkType: hard
"@turf/invariant@npm:^6.5.0":
version: 6.5.0
resolution: "@turf/invariant@npm:6.5.0"
dependencies:
"@turf/helpers": ^6.5.0
checksum: f45109ee41429d4aab49db9cfcc68f832cadf18b16c1b2c7031f0a6e52545bc4d64d0efd0a980f4d05f22532ed89d6e915aeaab9db44865898d4d030221d968e
languageName: node
linkType: hard
"@types/aria-query@npm:^5.0.1": "@types/aria-query@npm:^5.0.1":
version: 5.0.1 version: 5.0.1
resolution: "@types/aria-query@npm:5.0.1" resolution: "@types/aria-query@npm:5.0.1"
@@ -3332,6 +3358,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"array-source@npm:0.0":
version: 0.0.4
resolution: "array-source@npm:0.0.4"
checksum: 35478c6a02f0262c86bbe7761a1b7a6385d76df3aa0623cb90d596a49c4f1bff1a9609a1e31ac7140c921c8546b287eb6129e54d0f1d79a0a29d84e18a31d626
languageName: node
linkType: hard
"array-union@npm:^2.1.0": "array-union@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "array-union@npm:2.1.0" resolution: "array-union@npm:2.1.0"
@@ -3557,6 +3590,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"buffer-from@npm:^1.0.0":
version: 1.1.2
resolution: "buffer-from@npm:1.1.2"
checksum: 0448524a562b37d4d7ed9efd91685a5b77a50672c556ea254ac9a6d30e3403a517d8981f10e565db24e8339413b43c97ca2951f10e399c6125a0d8911f5679bb
languageName: node
linkType: hard
"buffer-from@npm:~0.1.1": "buffer-from@npm:~0.1.1":
version: 0.1.2 version: 0.1.2
resolution: "buffer-from@npm:0.1.2" resolution: "buffer-from@npm:0.1.2"
@@ -3913,7 +3953,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"commander@npm:^2.20.3": "commander@npm:2, commander@npm:^2.20.3":
version: 2.20.3 version: 2.20.3
resolution: "commander@npm:2.20.3" resolution: "commander@npm:2.20.3"
checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e checksum: ab8c07884e42c3a8dbc5dd9592c606176c7eb5c1ca5ff274bcf907039b2c41de3626f684ea75ccf4d361ba004bbaff1f577d5384c155f3871e456bdf27becf9e
@@ -3941,6 +3981,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"concat-stream@npm:^2.0.0":
version: 2.0.0
resolution: "concat-stream@npm:2.0.0"
dependencies:
buffer-from: ^1.0.0
inherits: ^2.0.3
readable-stream: ^3.0.2
typedarray: ^0.0.6
checksum: d7f75d48f0ecd356c1545d87e22f57b488172811b1181d96021c7c4b14ab8855f5313280263dca44bb06e5222f274d047da3e290a38841ef87b59719bde967c7
languageName: node
linkType: hard
"consola@npm:^3.0.0": "consola@npm:^3.0.0":
version: 3.2.3 version: 3.2.3
resolution: "consola@npm:3.2.3" resolution: "consola@npm:3.2.3"
@@ -5306,6 +5358,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"file-source@npm:0.6":
version: 0.6.1
resolution: "file-source@npm:0.6.1"
dependencies:
stream-source: 0.3
checksum: db27232df214b27ddd7026bbd202caf0bde31123811ebac0a9772d6a41735efdc3ee8050661f56d02d43811bde79938adb5ed8cbfe5f813c7cd09b86d19b6c84
languageName: node
linkType: hard
"fill-range@npm:^7.0.1": "fill-range@npm:^7.0.1":
version: 7.0.1 version: 7.0.1
resolution: "fill-range@npm:7.0.1" resolution: "fill-range@npm:7.0.1"
@@ -5532,6 +5593,33 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"geo-tz@npm:^7.0.7":
version: 7.0.7
resolution: "geo-tz@npm:7.0.7"
dependencies:
"@turf/boolean-point-in-polygon": ^6.5.0
"@turf/helpers": ^6.5.0
geobuf: ^3.0.2
pbf: ^3.2.1
checksum: 3edd225887cedcab4b372bc3ae4b35f27f8b325b797a91d8d7385cb73d566edcfd479e99599d8d08ec850e5cdaab30ad2fa6e5c0d0b4044c6bce3a06a8a1c87f
languageName: node
linkType: hard
"geobuf@npm:^3.0.2":
version: 3.0.2
resolution: "geobuf@npm:3.0.2"
dependencies:
concat-stream: ^2.0.0
pbf: ^3.2.1
shapefile: ~0.6.6
bin:
geobuf2json: bin/geobuf2json
json2geobuf: bin/json2geobuf
shp2geobuf: bin/shp2geobuf
checksum: 956f8dd2a86c593ce7124595512c45bcfc6570af8b9232645ed3b1feaa8db25c052d633d1eb836496a272dd8615436e3ff0ccb9dc866b4e75682ab15e9a2e3ed
languageName: node
linkType: hard
"get-caller-file@npm:^2.0.5": "get-caller-file@npm:^2.0.5":
version: 2.0.5 version: 2.0.5
resolution: "get-caller-file@npm:2.0.5" resolution: "get-caller-file@npm:2.0.5"
@@ -5972,10 +6060,13 @@ __metadata:
eslint-plugin-vitest: ^0.2.0 eslint-plugin-vitest: ^0.2.0
fily-publish-gridstack: ^0.0.13 fily-publish-gridstack: ^0.0.13
framer-motion: ^10.0.0 framer-motion: ^10.0.0
geo-tz: ^7.0.7
happy-dom: ^10.0.0 happy-dom: ^10.0.0
html-entities: ^2.3.3 html-entities: ^2.3.3
i18next: ^22.5.1 i18next: ^22.5.1
js-file-download: ^0.4.12 js-file-download: ^0.4.12
moment: ^2.29.4
moment-timezone: ^0.5.43
next: 13.4.10 next: 13.4.10
next-i18next: ^13.0.0 next-i18next: ^13.0.0
node-mocks-http: ^1.12.2 node-mocks-http: ^1.12.2
@@ -6174,7 +6265,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ieee754@npm:^1.1.13": "ieee754@npm:^1.1.12, ieee754@npm:^1.1.13":
version: 1.2.1 version: 1.2.1
resolution: "ieee754@npm:1.2.1" resolution: "ieee754@npm:1.2.1"
checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e checksum: 5144c0c9815e54ada181d80a0b810221a253562422e7c6c3a60b1901154184f49326ec239d618c416c1c5945a2e197107aee8d986a3dd836b53dffefd99b5e7e
@@ -7368,6 +7459,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"moment-timezone@npm:^0.5.43":
version: 0.5.43
resolution: "moment-timezone@npm:0.5.43"
dependencies:
moment: ^2.29.4
checksum: 8075c897ed8a044f992ef26fe8cdbcad80caf974251db424cae157473cca03be2830de8c74d99341b76edae59f148c9d9d19c1c1d9363259085688ec1cf508d0
languageName: node
linkType: hard
"moment@npm:^2.29.4":
version: 2.29.4
resolution: "moment@npm:2.29.4"
checksum: 0ec3f9c2bcba38dc2451b1daed5daded747f17610b92427bebe1d08d48d8b7bdd8d9197500b072d14e326dd0ccf3e326b9e3d07c5895d3d49e39b6803b76e80e
languageName: node
linkType: hard
"mpd-parser@npm:^1.0.1, mpd-parser@npm:^1.1.1": "mpd-parser@npm:^1.0.1, mpd-parser@npm:^1.1.1":
version: 1.1.1 version: 1.1.1
resolution: "mpd-parser@npm:1.1.1" resolution: "mpd-parser@npm:1.1.1"
@@ -7980,6 +8087,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"path-source@npm:0.1":
version: 0.1.3
resolution: "path-source@npm:0.1.3"
dependencies:
array-source: 0.0
file-source: 0.6
checksum: 131eb109e146cb68e04228fc47665735d091e6cb852f13b7f90b5aaa185abbefecb6030a6213a834d59fe1c5426bf2aa9b63f138d42798d8a4024d028c879184
languageName: node
linkType: hard
"path-type@npm:^4.0.0": "path-type@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "path-type@npm:4.0.0" resolution: "path-type@npm:4.0.0"
@@ -8001,6 +8118,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pbf@npm:^3.2.1":
version: 3.2.1
resolution: "pbf@npm:3.2.1"
dependencies:
ieee754: ^1.1.12
resolve-protobuf-schema: ^2.1.0
bin:
pbf: bin/pbf
checksum: 8033f5e21fffdc485e85d50bbff07ab3313c6841c3630a4ba9bc9e82d2e9005ab92000a1a90cb911223caa44316293e367bab607dd8110d5c771e6c8aaad63e1
languageName: node
linkType: hard
"picocolors@npm:^1.0.0": "picocolors@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "picocolors@npm:1.0.0" resolution: "picocolors@npm:1.0.0"
@@ -8340,6 +8469,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"protocol-buffers-schema@npm:^3.3.1":
version: 3.6.0
resolution: "protocol-buffers-schema@npm:3.6.0"
checksum: 8713b5770f6745ddbcdf3bbd03ee020624d506233bb567927a6615a6f69a5bd620a5f49597f34f4115792b853a4c9cb9e2d5d6b930a1c04bf198023e45c1c349
languageName: node
linkType: hard
"proxy-from-env@npm:^1.1.0": "proxy-from-env@npm:^1.1.0":
version: 1.1.0 version: 1.1.0
resolution: "proxy-from-env@npm:1.1.0" resolution: "proxy-from-env@npm:1.1.0"
@@ -8614,7 +8750,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0": "readable-stream@npm:^3.0.2, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.0":
version: 3.6.2 version: 3.6.2
resolution: "readable-stream@npm:3.6.2" resolution: "readable-stream@npm:3.6.2"
dependencies: dependencies:
@@ -8716,6 +8852,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"resolve-protobuf-schema@npm:^2.1.0":
version: 2.1.0
resolution: "resolve-protobuf-schema@npm:2.1.0"
dependencies:
protocol-buffers-schema: ^3.3.1
checksum: 88fffab2a3757888884a36f9aa4e24be5186b01820a8c26297dc1ce406b9daf776594926bdf524c2c8e8e5b0aba8ac48362b6584cdecc9a7083215ebca01c599
languageName: node
linkType: hard
"resolve@npm:^1.19.0, resolve@npm:^1.22.1": "resolve@npm:^1.19.0, resolve@npm:^1.22.1":
version: 1.22.3 version: 1.22.3
resolution: "resolve@npm:1.22.3" resolution: "resolve@npm:1.22.3"
@@ -8976,6 +9121,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"shapefile@npm:~0.6.6":
version: 0.6.6
resolution: "shapefile@npm:0.6.6"
dependencies:
array-source: 0.0
commander: 2
path-source: 0.1
slice-source: 0.4
stream-source: 0.3
text-encoding: ^0.6.4
bin:
dbf2json: bin/dbf2json
shp2json: bin/shp2json
checksum: 97ccf5412e8baad7a22e71700473f4b5aeab2d200093081dd8d77e180cc58e885127ce4f3b85897cd7fd49206084bace9b05c8591605f9912ba2cab0e93b427f
languageName: node
linkType: hard
"shebang-command@npm:^2.0.0": "shebang-command@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "shebang-command@npm:2.0.0" resolution: "shebang-command@npm:2.0.0"
@@ -9053,6 +9215,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"slice-source@npm:0.4":
version: 0.4.1
resolution: "slice-source@npm:0.4.1"
checksum: ce360e1e488e86bb5c5202cf44627a5bd3dbe4203bc2d4c98e558eb8a49b97999cf9d31a916559ffc5c8b896116d4204e73a80c698e450375682aa0a5df8f805
languageName: node
linkType: hard
"smart-buffer@npm:^4.2.0": "smart-buffer@npm:^4.2.0":
version: 4.2.0 version: 4.2.0
resolution: "smart-buffer@npm:4.2.0" resolution: "smart-buffer@npm:4.2.0"
@@ -9160,6 +9329,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"stream-source@npm:0.3":
version: 0.3.5
resolution: "stream-source@npm:0.3.5"
checksum: 65a0e0a38014dcfa6e219e92d83bde5fe716cf16df339e9d7e951525c0d129771416de4d77c3389c38cdd20a43d9575f90cc91041d6fa0c2f053b989780e3591
languageName: node
linkType: hard
"streamsearch@npm:^1.1.0": "streamsearch@npm:^1.1.0":
version: 1.1.0 version: 1.1.0
resolution: "streamsearch@npm:1.1.0" resolution: "streamsearch@npm:1.1.0"
@@ -9456,6 +9632,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"text-encoding@npm:^0.6.4":
version: 0.6.4
resolution: "text-encoding@npm:0.6.4"
checksum: 12616521dc62442657be3130a705506ebc6f081a68f70b335b514de3b57619754a7acb4a95a19243acfd84e226768e80be5380b43f4890e91f74c950a4e01d8d
languageName: node
linkType: hard
"text-table@npm:^0.2.0": "text-table@npm:^0.2.0":
version: 0.2.0 version: 0.2.0
resolution: "text-table@npm:0.2.0" resolution: "text-table@npm:0.2.0"
@@ -9800,6 +9983,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"typedarray@npm:^0.0.6":
version: 0.0.6
resolution: "typedarray@npm:0.0.6"
checksum: 33b39f3d0e8463985eeaeeacc3cb2e28bc3dfaf2a5ed219628c0b629d5d7b810b0eb2165f9f607c34871d5daa92ba1dc69f49051cf7d578b4cbd26c340b9d1b1
languageName: node
linkType: hard
"typescript@npm:^5.1.0": "typescript@npm:^5.1.0":
version: 5.1.6 version: 5.1.6
resolution: "typescript@npm:5.1.6" resolution: "typescript@npm:5.1.6"