chore: update prettier configuration for print width (#519)
* feat: update prettier configuration for print width * chore: apply code formatting to entire repository * fix: remove build files * fix: format issue --------- Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -9,9 +9,7 @@ export interface CommonWidgetInputProps<TKey extends WidgetOptionType> {
|
||||
options: Omit<WidgetOptionOfType<TKey>, "defaultValue" | "type">;
|
||||
}
|
||||
|
||||
type UseWidgetInputTranslationReturnType = (
|
||||
key: "label" | "description",
|
||||
) => string;
|
||||
type UseWidgetInputTranslationReturnType = (key: "label" | "description") => string;
|
||||
|
||||
/**
|
||||
* Short description why as and unknown convertions are used below:
|
||||
@@ -23,10 +21,7 @@ type UseWidgetInputTranslationReturnType = (
|
||||
* - The label translation can be used for every input, especially considering that all options should have defined a label for themself. The description translation should only be used when withDescription
|
||||
* is defined for the option. The method does sadly not reconize issues with those definitions. So it does not yell at you when you somewhere show the label without having it defined in the translations.
|
||||
*/
|
||||
export const useWidgetInputTranslation = (
|
||||
kind: WidgetKind,
|
||||
property: string,
|
||||
): UseWidgetInputTranslationReturnType => {
|
||||
export const useWidgetInputTranslation = (kind: WidgetKind, property: string): UseWidgetInputTranslationReturnType => {
|
||||
return useScopedI18n(
|
||||
`widget.${kind}.option.${property}` as never, // Because the type is complex and not recognized by typescript, we need to cast it to never to make it work.
|
||||
) as unknown as UseWidgetInputTranslationReturnType;
|
||||
|
||||
@@ -4,5 +4,4 @@ import { createFormContext } from "@homarr/form";
|
||||
|
||||
import type { WidgetEditModalState } from "../modals/widget-edit-modal";
|
||||
|
||||
export const [FormProvider, useFormContext, useForm] =
|
||||
createFormContext<WidgetEditModalState>();
|
||||
export const [FormProvider, useFormContext, useForm] = createFormContext<WidgetEditModalState>();
|
||||
|
||||
@@ -20,8 +20,6 @@ const mapping = {
|
||||
app: WidgetAppInput,
|
||||
} satisfies Record<WidgetOptionType, unknown>;
|
||||
|
||||
export const getInputForType = <TType extends WidgetOptionType>(
|
||||
type: TType,
|
||||
) => {
|
||||
export const getInputForType = <TType extends WidgetOptionType>(type: TType) => {
|
||||
return mapping[type];
|
||||
};
|
||||
|
||||
@@ -12,11 +12,7 @@ import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetAppInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"app">) => {
|
||||
export const WidgetAppInput = ({ property, kind, options }: CommonWidgetInputProps<"app">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
const { data: apps, isPending } = clientApi.app.selectable.useQuery();
|
||||
@@ -31,9 +27,7 @@ export const WidgetAppInput = ({
|
||||
label={t("label")}
|
||||
searchable
|
||||
limit={10}
|
||||
leftSection={
|
||||
<MemoizedLeftSection isPending={isPending} currentApp={currentApp} />
|
||||
}
|
||||
leftSection={<MemoizedLeftSection isPending={isPending} currentApp={currentApp} />}
|
||||
renderOption={renderSelectOption}
|
||||
data={
|
||||
apps?.map((app) => ({
|
||||
@@ -55,18 +49,13 @@ const iconProps = {
|
||||
size: 18,
|
||||
};
|
||||
|
||||
const renderSelectOption: SelectProps["renderOption"] = ({
|
||||
option,
|
||||
checked,
|
||||
}) => (
|
||||
const renderSelectOption: SelectProps["renderOption"] = ({ option, checked }) => (
|
||||
<Group flex="1" gap="xs">
|
||||
{"iconUrl" in option && typeof option.iconUrl === "string" ? (
|
||||
<img width={20} height={20} src={option.iconUrl} alt={option.label} />
|
||||
) : null}
|
||||
{option.label}
|
||||
{checked && (
|
||||
<IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />
|
||||
)}
|
||||
{checked && <IconCheck style={{ marginInlineStart: "auto" }} {...iconProps} />}
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -82,14 +71,7 @@ const LeftSection = ({ isPending, currentApp }: LeftSectionProps) => {
|
||||
}
|
||||
|
||||
if (currentApp) {
|
||||
return (
|
||||
<img
|
||||
width={size}
|
||||
height={size}
|
||||
src={currentApp.iconUrl}
|
||||
alt={currentApp.name}
|
||||
/>
|
||||
);
|
||||
return <img width={size} height={size} src={currentApp.iconUrl} alt={currentApp.name} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -28,10 +28,7 @@ import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetLocationInput = ({
|
||||
property,
|
||||
kind,
|
||||
}: CommonWidgetInputProps<"location">) => {
|
||||
export const WidgetLocationInput = ({ property, kind }: CommonWidgetInputProps<"location">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const tLocation = useScopedI18n("widget.common.location");
|
||||
const form = useFormContext();
|
||||
@@ -39,8 +36,7 @@ export const WidgetLocationInput = ({
|
||||
const value = form.values.options[property] as OptionLocation;
|
||||
const selectionEnabled = value.name.length > 1;
|
||||
|
||||
const handleChange = form.getInputProps(`options.${property}`)
|
||||
.onChange as LocationOnChange;
|
||||
const handleChange = form.getInputProps(`options.${property}`).onChange as LocationOnChange;
|
||||
const unknownLocation = tLocation("unknownLocation");
|
||||
|
||||
const onQueryChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -95,16 +91,8 @@ export const WidgetLocationInput = ({
|
||||
<Fieldset legend={t("label")}>
|
||||
<Stack gap="xs">
|
||||
<Group wrap="nowrap" align="end">
|
||||
<TextInput
|
||||
w="100%"
|
||||
label={tLocation("query")}
|
||||
value={value.name}
|
||||
onChange={onQueryChange}
|
||||
/>
|
||||
<Tooltip
|
||||
hidden={selectionEnabled}
|
||||
label={tLocation("disabledTooltip")}
|
||||
>
|
||||
<TextInput w="100%" label={tLocation("query")} value={value.name} onChange={onQueryChange} />
|
||||
<Tooltip hidden={selectionEnabled} label={tLocation("disabledTooltip")}>
|
||||
<div>
|
||||
<Button
|
||||
disabled={!selectionEnabled}
|
||||
@@ -151,61 +139,57 @@ interface LocationSearchInnerProps {
|
||||
onLocationSelect: (location: OptionLocation) => void;
|
||||
}
|
||||
|
||||
const LocationSearchModal = createModal<LocationSearchInnerProps>(
|
||||
({ actions, innerProps }) => {
|
||||
const t = useScopedI18n("widget.common.location.table");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const { data, isPending, error } = clientApi.location.searchCity.useQuery({
|
||||
query: innerProps.query,
|
||||
});
|
||||
const LocationSearchModal = createModal<LocationSearchInnerProps>(({ actions, innerProps }) => {
|
||||
const t = useScopedI18n("widget.common.location.table");
|
||||
const tCommon = useScopedI18n("common");
|
||||
const { data, isPending, error } = clientApi.location.searchCity.useQuery({
|
||||
query: innerProps.query,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Table striped>
|
||||
<Table.Thead>
|
||||
return (
|
||||
<Stack>
|
||||
<Table striped>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th style={{ width: "70%" }}>{t("header.city")}</Table.Th>
|
||||
<Table.Th style={{ width: "50%" }}>{t("header.country")}</Table.Th>
|
||||
<Table.Th>{t("header.coordinates")}</Table.Th>
|
||||
<Table.Th>{t("header.population")}</Table.Th>
|
||||
<Table.Th style={{ width: 40 }} />
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{isPending && (
|
||||
<Table.Tr>
|
||||
<Table.Th style={{ width: "70%" }}>{t("header.city")}</Table.Th>
|
||||
<Table.Th style={{ width: "50%" }}>
|
||||
{t("header.country")}
|
||||
</Table.Th>
|
||||
<Table.Th>{t("header.coordinates")}</Table.Th>
|
||||
<Table.Th>{t("header.population")}</Table.Th>
|
||||
<Table.Th style={{ width: 40 }} />
|
||||
<Table.Td colSpan={5}>
|
||||
<Group justify="center">
|
||||
<Loader />
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{isPending && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={5}>
|
||||
<Group justify="center">
|
||||
<Loader />
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
{data?.results.map((city) => (
|
||||
<LocationSelectTableRow
|
||||
key={city.id}
|
||||
city={city}
|
||||
onLocationSelect={innerProps.onLocationSelect}
|
||||
closeModal={actions.closeModal}
|
||||
/>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<Group justify="right">
|
||||
<Button variant="light" onClick={actions.closeModal}>
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
).withOptions({
|
||||
)}
|
||||
{data?.results.map((city) => (
|
||||
<LocationSelectTableRow
|
||||
key={city.id}
|
||||
city={city}
|
||||
onLocationSelect={innerProps.onLocationSelect}
|
||||
closeModal={actions.closeModal}
|
||||
/>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
<Group justify="right">
|
||||
<Button variant="light" onClick={actions.closeModal}>
|
||||
{tCommon("action.cancel")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
}).withOptions({
|
||||
defaultTitle(t) {
|
||||
return t("widget.common.location.search");
|
||||
},
|
||||
@@ -218,11 +202,7 @@ interface LocationSearchTableRowProps {
|
||||
closeModal: () => void;
|
||||
}
|
||||
|
||||
const LocationSelectTableRow = ({
|
||||
city,
|
||||
onLocationSelect,
|
||||
closeModal,
|
||||
}: LocationSearchTableRowProps) => {
|
||||
const LocationSelectTableRow = ({ city, onLocationSelect, closeModal }: LocationSearchTableRowProps) => {
|
||||
const t = useScopedI18n("widget.common.location.table");
|
||||
const onSelect = useCallback(() => {
|
||||
onLocationSelect({
|
||||
@@ -244,10 +224,7 @@ const LocationSelectTableRow = ({
|
||||
<Text style={{ whiteSpace: "nowrap" }}>{city.country}</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Anchor
|
||||
target="_blank"
|
||||
href={`https://www.google.com/maps/place/${city.latitude},${city.longitude}`}
|
||||
>
|
||||
<Anchor target="_blank" href={`https://www.google.com/maps/place/${city.latitude},${city.longitude}`}>
|
||||
<Text style={{ whiteSpace: "nowrap" }}>
|
||||
{city.latitude}, {city.longitude}
|
||||
</Text>
|
||||
@@ -255,9 +232,7 @@ const LocationSelectTableRow = ({
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{city.population ? (
|
||||
<Text style={{ whiteSpace: "nowrap" }}>
|
||||
{formatter.format(city.population)}
|
||||
</Text>
|
||||
<Text style={{ whiteSpace: "nowrap" }}>{formatter.format(city.population)}</Text>
|
||||
) : (
|
||||
<Text c="gray"> {t("population.fallback")}</Text>
|
||||
)}
|
||||
|
||||
@@ -7,11 +7,7 @@ import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
import type { SelectOption } from "./widget-select-input";
|
||||
|
||||
export const WidgetMultiSelectInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"multiSelect">) => {
|
||||
export const WidgetMultiSelectInput = ({ property, kind, options }: CommonWidgetInputProps<"multiSelect">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetNumberInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"number">) => {
|
||||
export const WidgetNumberInput = ({ property, kind, options }: CommonWidgetInputProps<"number">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
|
||||
@@ -13,18 +13,13 @@ export type SelectOption =
|
||||
}
|
||||
| string;
|
||||
|
||||
export type inferSelectOptionValue<TOption extends SelectOption> =
|
||||
TOption extends {
|
||||
value: infer TValue;
|
||||
}
|
||||
? TValue
|
||||
: TOption;
|
||||
export type inferSelectOptionValue<TOption extends SelectOption> = TOption extends {
|
||||
value: infer TValue;
|
||||
}
|
||||
? TValue
|
||||
: TOption;
|
||||
|
||||
export const WidgetSelectInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"select">) => {
|
||||
export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInputProps<"select">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetSliderInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"slider">) => {
|
||||
export const WidgetSliderInput = ({ property, kind, options }: CommonWidgetInputProps<"slider">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetSwitchInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"switch">) => {
|
||||
export const WidgetSwitchInput = ({ property, kind, options }: CommonWidgetInputProps<"switch">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
|
||||
@@ -6,11 +6,7 @@ import type { CommonWidgetInputProps } from "./common";
|
||||
import { useWidgetInputTranslation } from "./common";
|
||||
import { useFormContext } from "./form";
|
||||
|
||||
export const WidgetTextInput = ({
|
||||
property,
|
||||
kind,
|
||||
options,
|
||||
}: CommonWidgetInputProps<"text">) => {
|
||||
export const WidgetTextInput = ({ property, kind, options }: CommonWidgetInputProps<"text">) => {
|
||||
const t = useWidgetInputTranslation(kind, property);
|
||||
const form = useFormContext();
|
||||
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import {
|
||||
Center,
|
||||
Flex,
|
||||
Loader,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { Center, Flex, Loader, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
|
||||
import { IconDeviceDesktopX } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -19,13 +11,7 @@ import { useScopedI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./app.module.css";
|
||||
|
||||
export default function AppWidget({
|
||||
options,
|
||||
serverData,
|
||||
isEditMode,
|
||||
width,
|
||||
height,
|
||||
}: WidgetComponentProps<"app">) {
|
||||
export default function AppWidget({ options, serverData, isEditMode, width, height }: WidgetComponentProps<"app">) {
|
||||
const t = useScopedI18n("widget.app");
|
||||
const isQueryEnabled = Boolean(options.appId);
|
||||
const {
|
||||
@@ -90,11 +76,7 @@ export default function AppWidget({
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLink
|
||||
href={app?.href ?? ""}
|
||||
openInNewTab={options.openInNewTab}
|
||||
enabled={Boolean(app?.href) && !isEditMode}
|
||||
>
|
||||
<AppLink href={app?.href ?? ""} openInNewTab={options.openInNewTab} enabled={Boolean(app?.href) && !isEditMode}>
|
||||
<Flex align="center" justify="center" h="100%">
|
||||
<Tooltip.Floating
|
||||
label={app?.description}
|
||||
@@ -118,11 +100,7 @@ export default function AppWidget({
|
||||
{app?.name}
|
||||
</Text>
|
||||
)}
|
||||
<img
|
||||
src={app?.iconUrl}
|
||||
alt={app?.name}
|
||||
className={classes.appIcon}
|
||||
/>
|
||||
<img src={app?.iconUrl} alt={app?.name} className={classes.appIcon} />
|
||||
</Flex>
|
||||
</Tooltip.Floating>
|
||||
</Flex>
|
||||
@@ -136,20 +114,9 @@ interface AppLinkProps {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const AppLink = ({
|
||||
href,
|
||||
openInNewTab,
|
||||
enabled,
|
||||
children,
|
||||
}: PropsWithChildren<AppLinkProps>) =>
|
||||
const AppLink = ({ href, openInNewTab, enabled, children }: PropsWithChildren<AppLinkProps>) =>
|
||||
enabled ? (
|
||||
<UnstyledButton
|
||||
component="a"
|
||||
href={href}
|
||||
target={openInNewTab ? "_blank" : undefined}
|
||||
h="100%"
|
||||
w="100%"
|
||||
>
|
||||
<UnstyledButton component="a" href={href} target={openInNewTab ? "_blank" : undefined} h="100%" w="100%">
|
||||
{children}
|
||||
</UnstyledButton>
|
||||
) : (
|
||||
|
||||
@@ -3,14 +3,13 @@ import { IconApps } from "@tabler/icons-react";
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader, serverDataLoader } =
|
||||
createWidgetDefinition("app", {
|
||||
icon: IconApps,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
appId: factory.app(),
|
||||
openInNewTab: factory.switch({ defaultValue: true }),
|
||||
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
||||
})),
|
||||
})
|
||||
.withServerData(() => import("./serverData"))
|
||||
.withDynamicImport(() => import("./component"));
|
||||
export const { definition, componentLoader, serverDataLoader } = createWidgetDefinition("app", {
|
||||
icon: IconApps,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
appId: factory.app(),
|
||||
openInNewTab: factory.switch({ defaultValue: true }),
|
||||
showDescriptionTooltip: factory.switch({ defaultValue: false }),
|
||||
})),
|
||||
})
|
||||
.withServerData(() => import("./serverData"))
|
||||
.withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -4,9 +4,7 @@ import { api } from "@homarr/api/server";
|
||||
|
||||
import type { WidgetProps } from "../definition";
|
||||
|
||||
export default async function getServerDataAsync({
|
||||
options,
|
||||
}: WidgetProps<"app">) {
|
||||
export default async function getServerDataAsync({ options }: WidgetProps<"app">) {
|
||||
try {
|
||||
const app = await api.app.byId({ id: options.appId });
|
||||
return { app };
|
||||
|
||||
@@ -13,37 +13,19 @@ dayjs.extend(advancedFormat);
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezones);
|
||||
|
||||
export default function ClockWidget({
|
||||
options,
|
||||
}: WidgetComponentProps<"clock">) {
|
||||
export default function ClockWidget({ options }: WidgetComponentProps<"clock">) {
|
||||
const secondsFormat = options.showSeconds ? ":ss" : "";
|
||||
const timeFormat = options.is24HourFormat
|
||||
? `HH:mm${secondsFormat}`
|
||||
: `h:mm${secondsFormat} A`;
|
||||
const timeFormat = options.is24HourFormat ? `HH:mm${secondsFormat}` : `h:mm${secondsFormat} A`;
|
||||
const dateFormat = options.dateFormat;
|
||||
const timezone = options.useCustomTimezone
|
||||
? options.timezone
|
||||
: Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const timezone = options.useCustomTimezone ? options.timezone : Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
const time = useCurrentTime(options);
|
||||
return (
|
||||
<Flex
|
||||
classNames={{ root: "clock-wrapper" }}
|
||||
align="center"
|
||||
justify="center"
|
||||
h="100%"
|
||||
>
|
||||
<Flex classNames={{ root: "clock-wrapper" }} align="center" justify="center" h="100%">
|
||||
<Stack classNames={{ root: "clock-text-stack" }} align="center" gap="xs">
|
||||
{options.customTitleToggle && (
|
||||
<Text classNames={{ root: "clock-customTitle-text" }}>
|
||||
{options.customTitle}
|
||||
</Text>
|
||||
<Text classNames={{ root: "clock-customTitle-text" }}>{options.customTitle}</Text>
|
||||
)}
|
||||
<Text
|
||||
classNames={{ root: "clock-time-text" }}
|
||||
fw={700}
|
||||
size="2.125rem"
|
||||
lh="1"
|
||||
>
|
||||
<Text classNames={{ root: "clock-time-text" }} fw={700} size="2.125rem" lh="1">
|
||||
{dayjs(time).tz(timezone).format(timeFormat)}
|
||||
</Text>
|
||||
{options.showDate && (
|
||||
@@ -64,10 +46,7 @@ const useCurrentTime = ({ showSeconds }: UseCurrentTimeProps) => {
|
||||
const [time, setTime] = useState(new Date());
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
const intervalRef = useRef<NodeJS.Timeout>();
|
||||
const intervalMultiplier = useMemo(
|
||||
() => (showSeconds ? 1 : 60),
|
||||
[showSeconds],
|
||||
);
|
||||
const intervalMultiplier = useMemo(() => (showSeconds ? 1 : 60), [showSeconds]);
|
||||
|
||||
useEffect(() => {
|
||||
setTime(new Date());
|
||||
@@ -79,8 +58,7 @@ const useCurrentTime = ({ showSeconds }: UseCurrentTimeProps) => {
|
||||
setTime(new Date());
|
||||
}, intervalMultiplier * 1000);
|
||||
},
|
||||
intervalMultiplier * 1000 -
|
||||
(1000 * (showSeconds ? 0 : dayjs().second()) + dayjs().millisecond()),
|
||||
intervalMultiplier * 1000 - (1000 * (showSeconds ? 0 : dayjs().second()) + dayjs().millisecond()),
|
||||
);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -4,10 +4,7 @@ import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import type { WidgetImports } from ".";
|
||||
import type {
|
||||
inferOptionsFromDefinition,
|
||||
WidgetOptionsRecord,
|
||||
} from "./options";
|
||||
import type { inferOptionsFromDefinition, WidgetOptionsRecord } from "./options";
|
||||
import type { IntegrationSelectOption } from "./widget-integration-select";
|
||||
|
||||
type ServerDataLoader<TKind extends WidgetKind> = () => Promise<{
|
||||
@@ -29,9 +26,7 @@ const createWithDynamicImport =
|
||||
WidgetComponentProps<TKind> &
|
||||
(TServerDataLoader extends ServerDataLoader<TKind>
|
||||
? {
|
||||
serverData: Awaited<
|
||||
ReturnType<Awaited<ReturnType<TServerDataLoader>>["default"]>
|
||||
>;
|
||||
serverData: Awaited<ReturnType<Awaited<ReturnType<TServerDataLoader>>["default"]>>;
|
||||
}
|
||||
: never)
|
||||
>,
|
||||
@@ -46,30 +41,18 @@ const createWithDynamicImport =
|
||||
});
|
||||
|
||||
const createWithServerData =
|
||||
<TKind extends WidgetKind, TDefinition extends WidgetDefinition>(
|
||||
kind: TKind,
|
||||
definition: TDefinition,
|
||||
) =>
|
||||
<TServerDataLoader extends ServerDataLoader<TKind>>(
|
||||
serverDataLoader: TServerDataLoader,
|
||||
) => ({
|
||||
<TKind extends WidgetKind, TDefinition extends WidgetDefinition>(kind: TKind, definition: TDefinition) =>
|
||||
<TServerDataLoader extends ServerDataLoader<TKind>>(serverDataLoader: TServerDataLoader) => ({
|
||||
definition: {
|
||||
...definition,
|
||||
kind,
|
||||
},
|
||||
kind,
|
||||
serverDataLoader,
|
||||
withDynamicImport: createWithDynamicImport(
|
||||
kind,
|
||||
definition,
|
||||
serverDataLoader,
|
||||
),
|
||||
withDynamicImport: createWithDynamicImport(kind, definition, serverDataLoader),
|
||||
});
|
||||
|
||||
export const createWidgetDefinition = <
|
||||
TKind extends WidgetKind,
|
||||
TDefinition extends WidgetDefinition,
|
||||
>(
|
||||
export const createWidgetDefinition = <TKind extends WidgetKind, TDefinition extends WidgetDefinition>(
|
||||
kind: TKind,
|
||||
definition: TDefinition,
|
||||
) => ({
|
||||
@@ -85,41 +68,32 @@ export interface WidgetDefinition {
|
||||
|
||||
export interface WidgetProps<TKind extends WidgetKind> {
|
||||
options: inferOptionsFromDefinition<WidgetOptionsRecordOf<TKind>>;
|
||||
integrations: inferIntegrationsFromDefinition<
|
||||
WidgetImports[TKind]["definition"]
|
||||
>;
|
||||
integrations: inferIntegrationsFromDefinition<WidgetImports[TKind]["definition"]>;
|
||||
}
|
||||
|
||||
type inferServerDataForKind<TKind extends WidgetKind> =
|
||||
WidgetImports[TKind] extends { serverDataLoader: ServerDataLoader<TKind> }
|
||||
? Awaited<
|
||||
ReturnType<
|
||||
Awaited<
|
||||
ReturnType<WidgetImports[TKind]["serverDataLoader"]>
|
||||
>["default"]
|
||||
>
|
||||
>
|
||||
: undefined;
|
||||
type inferServerDataForKind<TKind extends WidgetKind> = WidgetImports[TKind] extends {
|
||||
serverDataLoader: ServerDataLoader<TKind>;
|
||||
}
|
||||
? Awaited<ReturnType<Awaited<ReturnType<WidgetImports[TKind]["serverDataLoader"]>>["default"]>>
|
||||
: undefined;
|
||||
|
||||
export type WidgetComponentProps<TKind extends WidgetKind> =
|
||||
WidgetProps<TKind> & {
|
||||
serverData?: inferServerDataForKind<TKind>;
|
||||
} & {
|
||||
itemId: string | undefined; // undefined when in preview mode
|
||||
boardId: string | undefined; // undefined when in preview mode
|
||||
isEditMode: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
export type WidgetComponentProps<TKind extends WidgetKind> = WidgetProps<TKind> & {
|
||||
serverData?: inferServerDataForKind<TKind>;
|
||||
} & {
|
||||
itemId: string | undefined; // undefined when in preview mode
|
||||
boardId: string | undefined; // undefined when in preview mode
|
||||
isEditMode: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type inferIntegrationsFromDefinition<TDefinition extends WidgetDefinition> =
|
||||
TDefinition extends {
|
||||
supportedIntegrations: infer TSupportedIntegrations;
|
||||
} // check if definition has supportedIntegrations
|
||||
? TSupportedIntegrations extends IntegrationKind[] // check if supportedIntegrations is an array of IntegrationKind
|
||||
? IntegrationSelectOptionFor<TSupportedIntegrations[number]>[] // if so, return an array of IntegrationSelectOptionFor
|
||||
: IntegrationSelectOption[] // otherwise, return an array of IntegrationSelectOption without specifying the kind
|
||||
: IntegrationSelectOption[];
|
||||
type inferIntegrationsFromDefinition<TDefinition extends WidgetDefinition> = TDefinition extends {
|
||||
supportedIntegrations: infer TSupportedIntegrations;
|
||||
} // check if definition has supportedIntegrations
|
||||
? TSupportedIntegrations extends IntegrationKind[] // check if supportedIntegrations is an array of IntegrationKind
|
||||
? IntegrationSelectOptionFor<TSupportedIntegrations[number]>[] // if so, return an array of IntegrationSelectOptionFor
|
||||
: IntegrationSelectOption[] // otherwise, return an array of IntegrationSelectOption without specifying the kind
|
||||
: IntegrationSelectOption[];
|
||||
|
||||
interface IntegrationSelectOptionFor<TIntegration extends IntegrationKind> {
|
||||
id: string;
|
||||
@@ -128,5 +102,4 @@ interface IntegrationSelectOptionFor<TIntegration extends IntegrationKind> {
|
||||
kind: TIntegration[number];
|
||||
}
|
||||
|
||||
export type WidgetOptionsRecordOf<TKind extends WidgetKind> =
|
||||
WidgetImports[TKind]["definition"]["options"];
|
||||
export type WidgetOptionsRecordOf<TKind extends WidgetKind> = WidgetImports[TKind]["definition"]["options"];
|
||||
|
||||
@@ -7,9 +7,7 @@ import { useI18n } from "@homarr/translation/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function IFrameWidget({
|
||||
options,
|
||||
}: WidgetComponentProps<"iframe">) {
|
||||
export default function IFrameWidget({ options }: WidgetComponentProps<"iframe">) {
|
||||
const t = useI18n();
|
||||
const { embedUrl, ...permissions } = options;
|
||||
const allowedPermissions = getAllowedPermissions(permissions);
|
||||
@@ -18,12 +16,7 @@ export default function IFrameWidget({
|
||||
|
||||
return (
|
||||
<Box h="100%" w="100%">
|
||||
<iframe
|
||||
className={classes.iframe}
|
||||
src={embedUrl}
|
||||
title="widget iframe"
|
||||
allow={allowedPermissions.join(" ")}
|
||||
>
|
||||
<iframe className={classes.iframe} src={embedUrl} title="widget iframe" allow={allowedPermissions.join(" ")}>
|
||||
<Text>{t("widget.iframe.error.noBrowerSupport")}</Text>
|
||||
</iframe>
|
||||
</Box>
|
||||
@@ -41,9 +34,7 @@ const NoUrl = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const getAllowedPermissions = (
|
||||
permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">,
|
||||
) => {
|
||||
const getAllowedPermissions = (permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">) => {
|
||||
return objectEntries(permissions)
|
||||
.filter(([_key, value]) => value)
|
||||
.map(([key]) => permissionMapping[key]);
|
||||
@@ -58,7 +49,4 @@ const permissionMapping = {
|
||||
allowPayment: "payment",
|
||||
allowScrolling: "scrolling",
|
||||
allowTransparency: "transparency",
|
||||
} satisfies Record<
|
||||
keyof Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">,
|
||||
string
|
||||
>;
|
||||
} satisfies Record<keyof Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">, string>;
|
||||
|
||||
@@ -3,22 +3,19 @@ import { IconBrowser } from "@tabler/icons-react";
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition(
|
||||
"iframe",
|
||||
{
|
||||
icon: IconBrowser,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
embedUrl: factory.text(),
|
||||
allowFullScreen: factory.switch(),
|
||||
allowScrolling: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
allowTransparency: factory.switch(),
|
||||
allowPayment: factory.switch(),
|
||||
allowAutoPlay: factory.switch(),
|
||||
allowMicrophone: factory.switch(),
|
||||
allowCamera: factory.switch(),
|
||||
allowGeolocation: factory.switch(),
|
||||
})),
|
||||
},
|
||||
).withDynamicImport(() => import("./component"));
|
||||
export const { definition, componentLoader } = createWidgetDefinition("iframe", {
|
||||
icon: IconBrowser,
|
||||
options: optionsBuilder.from((factory) => ({
|
||||
embedUrl: factory.text(),
|
||||
allowFullScreen: factory.switch(),
|
||||
allowScrolling: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
allowTransparency: factory.switch(),
|
||||
allowPayment: factory.switch(),
|
||||
allowAutoPlay: factory.switch(),
|
||||
allowMicrophone: factory.switch(),
|
||||
allowCamera: factory.switch(),
|
||||
allowGeolocation: factory.switch(),
|
||||
})),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -32,10 +32,7 @@ export const widgetImports = {
|
||||
export type WidgetImports = typeof widgetImports;
|
||||
export type WidgetImportKey = keyof WidgetImports;
|
||||
|
||||
const loadedComponents = new Map<
|
||||
WidgetKind,
|
||||
ComponentType<WidgetComponentProps<WidgetKind>>
|
||||
>();
|
||||
const loadedComponents = new Map<WidgetKind, ComponentType<WidgetComponentProps<WidgetKind>>>();
|
||||
|
||||
export const loadWidgetDynamic = <TKind extends WidgetKind>(kind: TKind) => {
|
||||
const existingComponent = loadedComponents.get(kind);
|
||||
|
||||
@@ -27,69 +27,51 @@ interface ModalProps<TSort extends WidgetKind> {
|
||||
integrationSupport: boolean;
|
||||
}
|
||||
|
||||
export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(
|
||||
({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useForm({
|
||||
initialValues: innerProps.value,
|
||||
});
|
||||
export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, innerProps }) => {
|
||||
const t = useI18n();
|
||||
const form = useForm({
|
||||
initialValues: innerProps.value,
|
||||
});
|
||||
|
||||
const { definition } = widgetImports[innerProps.kind];
|
||||
const { definition } = widgetImports[innerProps.kind];
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
innerProps.onSuccessfulEdit(values);
|
||||
actions.closeModal();
|
||||
})}
|
||||
>
|
||||
<FormProvider form={form}>
|
||||
<Stack>
|
||||
{innerProps.integrationSupport && (
|
||||
<WidgetIntegrationSelect
|
||||
label={t("item.edit.field.integrations.label")}
|
||||
data={innerProps.integrationData}
|
||||
{...form.getInputProps("integrations")}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(definition.options).map(
|
||||
([key, value]: [string, OptionsBuilderResult[string]]) => {
|
||||
const Input = getInputForType(value.type);
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
innerProps.onSuccessfulEdit(values);
|
||||
actions.closeModal();
|
||||
})}
|
||||
>
|
||||
<FormProvider form={form}>
|
||||
<Stack>
|
||||
{innerProps.integrationSupport && (
|
||||
<WidgetIntegrationSelect
|
||||
label={t("item.edit.field.integrations.label")}
|
||||
data={innerProps.integrationData}
|
||||
{...form.getInputProps("integrations")}
|
||||
/>
|
||||
)}
|
||||
{Object.entries(definition.options).map(([key, value]: [string, OptionsBuilderResult[string]]) => {
|
||||
const Input = getInputForType(value.type);
|
||||
|
||||
if (
|
||||
!Input ||
|
||||
value.shouldHide?.(form.values.options as never)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
if (!Input || value.shouldHide?.(form.values.options as never)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
key={key}
|
||||
kind={innerProps.kind}
|
||||
property={key}
|
||||
options={value as never}
|
||||
/>
|
||||
);
|
||||
},
|
||||
)}
|
||||
<Group justify="right">
|
||||
<Button
|
||||
onClick={actions.closeModal}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
>
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
).withOptions({
|
||||
return <Input key={key} kind={innerProps.kind} property={key} options={value as never} />;
|
||||
})}
|
||||
<Group justify="right">
|
||||
<Button onClick={actions.closeModal} variant="subtle" color="gray">
|
||||
{t("common.action.cancel")}
|
||||
</Button>
|
||||
<Button type="submit" color="teal">
|
||||
{t("common.action.saveChanges")}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</FormProvider>
|
||||
</form>
|
||||
);
|
||||
}).withOptions({
|
||||
keepMounted: true,
|
||||
});
|
||||
|
||||
@@ -4,15 +4,10 @@ import "@mantine/tiptap/styles.css";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
|
||||
const Notebook = dynamic(
|
||||
() => import("./notebook").then((module) => module.Notebook),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
);
|
||||
const Notebook = dynamic(() => import("./notebook").then((module) => module.Notebook), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
export default function NotebookWidget(
|
||||
props: WidgetComponentProps<"notebook">,
|
||||
) {
|
||||
export default function NotebookWidget(props: WidgetComponentProps<"notebook">) {
|
||||
return <Notebook {...props} />;
|
||||
}
|
||||
|
||||
@@ -4,27 +4,24 @@ import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
import { defaultContent } from "./default-content";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition(
|
||||
"notebook",
|
||||
{
|
||||
icon: IconNotes,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
showToolbar: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
allowReadOnlyCheck: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
content: factory.text({
|
||||
defaultValue: defaultContent,
|
||||
}),
|
||||
export const { definition, componentLoader } = createWidgetDefinition("notebook", {
|
||||
icon: IconNotes,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
showToolbar: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
{
|
||||
content: {
|
||||
shouldHide: () => true, // Hide the content option as it can be modified in the editor
|
||||
},
|
||||
allowReadOnlyCheck: factory.switch({
|
||||
defaultValue: true,
|
||||
}),
|
||||
content: factory.text({
|
||||
defaultValue: defaultContent,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
content: {
|
||||
shouldHide: () => true, // Hide the content option as it can be modified in the editor
|
||||
},
|
||||
),
|
||||
},
|
||||
).withDynamicImport(() => import("./component"));
|
||||
},
|
||||
),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -16,11 +16,7 @@ import {
|
||||
useMantineTheme,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import {
|
||||
Link,
|
||||
RichTextEditor,
|
||||
useRichTextEditorContext,
|
||||
} from "@mantine/tiptap";
|
||||
import { Link, RichTextEditor, useRichTextEditorContext } from "@mantine/tiptap";
|
||||
import {
|
||||
IconCheck,
|
||||
IconCircleOff,
|
||||
@@ -78,12 +74,7 @@ const controlIconProps = {
|
||||
stroke: 1.5,
|
||||
};
|
||||
|
||||
export function Notebook({
|
||||
options,
|
||||
isEditMode,
|
||||
boardId,
|
||||
itemId,
|
||||
}: WidgetComponentProps<"notebook">) {
|
||||
export function Notebook({ options, isEditMode, boardId, itemId }: WidgetComponentProps<"notebook">) {
|
||||
const [content, setContent] = useState(options.content);
|
||||
const [toSaveContent, setToSaveContent] = useState(content);
|
||||
|
||||
@@ -137,12 +128,9 @@ export function Notebook({
|
||||
backgroundColor: {
|
||||
default: undefined,
|
||||
renderHTML: (attributes) => ({
|
||||
style: attributes.backgroundColor
|
||||
? `background-color: ${attributes.backgroundColor}`
|
||||
: undefined,
|
||||
style: attributes.backgroundColor ? `background-color: ${attributes.backgroundColor}` : undefined,
|
||||
}),
|
||||
parseHTML: (element) =>
|
||||
element.style.backgroundColor || undefined,
|
||||
parseHTML: (element) => element.style.backgroundColor || undefined,
|
||||
},
|
||||
};
|
||||
},
|
||||
@@ -178,9 +166,7 @@ export function Notebook({
|
||||
[toSaveContent],
|
||||
);
|
||||
|
||||
const handleOnReadOnlyCheck = (
|
||||
event: CustomEventInit<{ node: Node; checked: boolean }>,
|
||||
) => {
|
||||
const handleOnReadOnlyCheck = (event: CustomEventInit<{ node: Node; checked: boolean }>) => {
|
||||
if (!options.allowReadOnlyCheck) return;
|
||||
if (!editor) return;
|
||||
|
||||
@@ -251,8 +237,7 @@ export function Notebook({
|
||||
"& .ProseMirror": {
|
||||
padding: "0 !important",
|
||||
},
|
||||
backgroundColor:
|
||||
colorScheme === "dark" ? theme.colors.dark[6] : "white",
|
||||
backgroundColor: colorScheme === "dark" ? theme.colors.dark[6] : "white",
|
||||
border: "none",
|
||||
borderRadius: "0.5rem",
|
||||
display: "flex",
|
||||
@@ -270,8 +255,7 @@ export function Notebook({
|
||||
>
|
||||
<RichTextEditor.Toolbar
|
||||
style={{
|
||||
display:
|
||||
isEditing && options.showToolbar === true ? "flex" : "none",
|
||||
display: isEditing && options.showToolbar === true ? "flex" : "none",
|
||||
}}
|
||||
>
|
||||
<RichTextEditor.ControlsGroup>
|
||||
@@ -319,9 +303,7 @@ export function Notebook({
|
||||
<RichTextEditor.BulletList title={tControls("bulletList")} />
|
||||
<RichTextEditor.OrderedList title={tControls("orderedList")} />
|
||||
<TaskListToggle />
|
||||
{(editor?.isActive("taskList") ||
|
||||
editor?.isActive("bulletList") ||
|
||||
editor?.isActive("orderedList")) && (
|
||||
{(editor?.isActive("taskList") || editor?.isActive("bulletList") || editor?.isActive("orderedList")) && (
|
||||
<>
|
||||
<ListIndentIncrease />
|
||||
<ListIndentDecrease />
|
||||
@@ -368,9 +350,7 @@ export function Notebook({
|
||||
{enabled && (
|
||||
<>
|
||||
<ActionIcon
|
||||
title={
|
||||
isEditing ? t("common.action.save") : t("common.action.edit")
|
||||
}
|
||||
title={isEditing ? t("common.action.save") : t("common.action.edit")}
|
||||
style={{
|
||||
zIndex: 1,
|
||||
}}
|
||||
@@ -383,11 +363,7 @@ export function Notebook({
|
||||
radius={"md"}
|
||||
onClick={handleEditToggle}
|
||||
>
|
||||
{isEditing ? (
|
||||
<IconDeviceFloppy {...iconProps} />
|
||||
) : (
|
||||
<IconEdit {...iconProps} />
|
||||
)}
|
||||
{isEditing ? <IconDeviceFloppy {...iconProps} /> : <IconEdit {...iconProps} />}
|
||||
</ActionIcon>
|
||||
{isEditing && (
|
||||
<ActionIcon
|
||||
@@ -482,9 +458,7 @@ function ColorCellControl() {
|
||||
const { editor } = useRichTextEditorContext();
|
||||
|
||||
const getCurrent = useCallback(() => {
|
||||
return editor?.getAttributes("tableCell").backgroundColor as
|
||||
| string
|
||||
| undefined;
|
||||
return editor?.getAttributes("tableCell").backgroundColor as string | undefined;
|
||||
}, [editor]);
|
||||
|
||||
const update = useCallback(
|
||||
@@ -513,13 +487,7 @@ interface ColorControlProps {
|
||||
ariaLabel: string;
|
||||
}
|
||||
|
||||
const ColorControl = ({
|
||||
defaultColor,
|
||||
getCurrent,
|
||||
update,
|
||||
icon: Icon,
|
||||
ariaLabel,
|
||||
}: ColorControlProps) => {
|
||||
const ColorControl = ({ defaultColor, getCurrent, update, icon: Icon, ariaLabel }: ColorControlProps) => {
|
||||
const { editor } = useRichTextEditorContext();
|
||||
const [color, setColor] = useState(defaultColor);
|
||||
const { colors, white } = useMantineTheme();
|
||||
@@ -591,33 +559,15 @@ const ColorControl = ({
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Stack gap={8}>
|
||||
<ColorPicker
|
||||
value={color}
|
||||
onChange={setColor}
|
||||
format="hexa"
|
||||
swatches={palette}
|
||||
swatchesPerRow={6}
|
||||
/>
|
||||
<ColorPicker value={color} onChange={setColor} format="hexa" swatches={palette} swatchesPerRow={6} />
|
||||
<Group justify="right" gap={8}>
|
||||
<ActionIcon
|
||||
title={t("common.action.cancel")}
|
||||
variant="default"
|
||||
onClick={close}
|
||||
>
|
||||
<ActionIcon title={t("common.action.cancel")} variant="default" onClick={close}>
|
||||
<IconX stroke={1.5} size="1rem" />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
title={t("common.action.apply")}
|
||||
variant="default"
|
||||
onClick={handleApplyColor}
|
||||
>
|
||||
<ActionIcon title={t("common.action.apply")} variant="default" onClick={handleApplyColor}>
|
||||
<IconCheck stroke={1.5} size="1rem" />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
title={t("widget.notebook.popover.clearColor")}
|
||||
variant="default"
|
||||
onClick={handleClearColor}
|
||||
>
|
||||
<ActionIcon title={t("widget.notebook.popover.clearColor")} variant="default" onClick={handleClearColor}>
|
||||
<IconCircleOff stroke={1.5} size="1rem" />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
@@ -676,11 +626,7 @@ function EmbedImage() {
|
||||
trapFocus
|
||||
>
|
||||
<Popover.Target>
|
||||
<RichTextEditor.Control
|
||||
onClick={toggle}
|
||||
title={tControls("image")}
|
||||
active={editor?.isActive("image")}
|
||||
>
|
||||
<RichTextEditor.Control onClick={toggle} title={tControls("image")} active={editor?.isActive("image")}>
|
||||
<IconPhoto stroke={1.5} size="1rem" />
|
||||
</RichTextEditor.Control>
|
||||
</Popover.Target>
|
||||
@@ -777,11 +723,7 @@ const handleAddColumnBefore = (editor: Editor) => {
|
||||
};
|
||||
|
||||
const TableAddColumnBefore = () => (
|
||||
<TableControl
|
||||
title="addColumnLeft"
|
||||
onClick={handleAddColumnBefore}
|
||||
icon={IconColumnInsertLeft}
|
||||
/>
|
||||
<TableControl title="addColumnLeft" onClick={handleAddColumnBefore} icon={IconColumnInsertLeft} />
|
||||
);
|
||||
|
||||
const handleAddColumnAfter = (editor: Editor) => {
|
||||
@@ -789,11 +731,7 @@ const handleAddColumnAfter = (editor: Editor) => {
|
||||
};
|
||||
|
||||
const TableAddColumnAfter = () => (
|
||||
<TableControl
|
||||
title="addColumnRight"
|
||||
onClick={handleAddColumnAfter}
|
||||
icon={IconColumnInsertRight}
|
||||
/>
|
||||
<TableControl title="addColumnRight" onClick={handleAddColumnAfter} icon={IconColumnInsertRight} />
|
||||
);
|
||||
|
||||
const handleRemoveColumn = (editor: Editor) => {
|
||||
@@ -801,54 +739,31 @@ const handleRemoveColumn = (editor: Editor) => {
|
||||
};
|
||||
|
||||
const TableRemoveColumn = () => (
|
||||
<TableControl
|
||||
title="deleteColumn"
|
||||
onClick={handleRemoveColumn}
|
||||
icon={IconColumnRemove}
|
||||
/>
|
||||
<TableControl title="deleteColumn" onClick={handleRemoveColumn} icon={IconColumnRemove} />
|
||||
);
|
||||
|
||||
const handleAddRowBefore = (editor: Editor) => {
|
||||
editor.commands.addRowBefore();
|
||||
};
|
||||
|
||||
const TableAddRowBefore = () => (
|
||||
<TableControl
|
||||
title="addRowTop"
|
||||
onClick={handleAddRowBefore}
|
||||
icon={IconRowInsertTop}
|
||||
/>
|
||||
);
|
||||
const TableAddRowBefore = () => <TableControl title="addRowTop" onClick={handleAddRowBefore} icon={IconRowInsertTop} />;
|
||||
|
||||
const handleAddRowAfter = (editor: Editor) => {
|
||||
editor.commands.addRowAfter();
|
||||
};
|
||||
|
||||
const TableAddRowAfter = () => (
|
||||
<TableControl
|
||||
title="addRowBelow"
|
||||
onClick={handleAddRowAfter}
|
||||
icon={IconRowInsertBottom}
|
||||
/>
|
||||
<TableControl title="addRowBelow" onClick={handleAddRowAfter} icon={IconRowInsertBottom} />
|
||||
);
|
||||
|
||||
const handleRemoveRow = (editor: Editor) => {
|
||||
editor.commands.deleteRow();
|
||||
};
|
||||
|
||||
const TableRemoveRow = () => (
|
||||
<TableControl
|
||||
title="deleteRow"
|
||||
onClick={handleRemoveRow}
|
||||
icon={IconRowRemove}
|
||||
/>
|
||||
);
|
||||
const TableRemoveRow = () => <TableControl title="deleteRow" onClick={handleRemoveRow} icon={IconRowRemove} />;
|
||||
|
||||
interface TableControlProps {
|
||||
title: Exclude<
|
||||
keyof TranslationObject["widget"]["notebook"]["controls"],
|
||||
"align" | "heading"
|
||||
>;
|
||||
title: Exclude<keyof TranslationObject["widget"]["notebook"]["controls"], "align" | "heading">;
|
||||
onClick: (editor: Editor) => void;
|
||||
icon: TablerIcon;
|
||||
}
|
||||
@@ -862,10 +777,7 @@ const TableControl = ({ title, onClick, icon: Icon }: TableControlProps) => {
|
||||
}, [editor, onClick]);
|
||||
|
||||
return (
|
||||
<RichTextEditor.Control
|
||||
title={tControls(title)}
|
||||
onClick={handleControlClick}
|
||||
>
|
||||
<RichTextEditor.Control title={tControls(title)} onClick={handleControlClick}>
|
||||
<Icon {...controlIconProps} />
|
||||
</RichTextEditor.Control>
|
||||
);
|
||||
@@ -958,26 +870,14 @@ function TableToggle() {
|
||||
active={isActive}
|
||||
onClick={handleControlClick}
|
||||
>
|
||||
{isActive ? (
|
||||
<IconTableOff stroke={1.5} size="1rem" />
|
||||
) : (
|
||||
<IconTablePlus stroke={1.5} size="1rem" />
|
||||
)}
|
||||
{isActive ? <IconTableOff stroke={1.5} size="1rem" /> : <IconTablePlus stroke={1.5} size="1rem" />}
|
||||
</RichTextEditor.Control>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
<Stack gap={5}>
|
||||
<NumberInput
|
||||
label={t("widget.notebook.popover.columns")}
|
||||
min={1}
|
||||
{...form.getInputProps("cols")}
|
||||
/>
|
||||
<NumberInput
|
||||
label={t("widget.notebook.popover.rows")}
|
||||
min={1}
|
||||
{...form.getInputProps("rows")}
|
||||
/>
|
||||
<NumberInput label={t("widget.notebook.popover.columns")} min={1} {...form.getInputProps("cols")} />
|
||||
<NumberInput label={t("widget.notebook.popover.rows")} min={1} {...form.getInputProps("rows")} />
|
||||
<Button type="submit" variant="default" mt={10} mb={5}>
|
||||
{t("common.action.insert")}
|
||||
</Button>
|
||||
|
||||
@@ -3,10 +3,7 @@ import type { WidgetKind } from "@homarr/definitions";
|
||||
import type { z } from "@homarr/validation";
|
||||
|
||||
import { widgetImports } from ".";
|
||||
import type {
|
||||
inferSelectOptionValue,
|
||||
SelectOption,
|
||||
} from "./_inputs/widget-select-input";
|
||||
import type { inferSelectOptionValue, SelectOption } from "./_inputs/widget-select-input";
|
||||
|
||||
interface CommonInput<TType> {
|
||||
defaultValue?: TType;
|
||||
@@ -57,21 +54,16 @@ const optionsFactory = {
|
||||
withDescription: input?.withDescription ?? false,
|
||||
validate: input?.validate,
|
||||
}),
|
||||
multiSelect: <const TOptions extends SelectOption[]>(
|
||||
input: MultiSelectInput<TOptions>,
|
||||
) => ({
|
||||
multiSelect: <const TOptions extends SelectOption[]>(input: MultiSelectInput<TOptions>) => ({
|
||||
type: "multiSelect" as const,
|
||||
defaultValue: input.defaultValue ?? [],
|
||||
options: input.options,
|
||||
searchable: input.searchable ?? false,
|
||||
withDescription: input.withDescription ?? false,
|
||||
}),
|
||||
select: <const TOptions extends SelectOption[]>(
|
||||
input: SelectInput<TOptions>,
|
||||
) => ({
|
||||
select: <const TOptions extends SelectOption[]>(input: SelectInput<TOptions>) => ({
|
||||
type: "select" as const,
|
||||
defaultValue: (input.defaultValue ??
|
||||
input.options[0]) as inferSelectOptionValue<TOptions[number]>,
|
||||
defaultValue: (input.defaultValue ?? input.options[0]) as inferSelectOptionValue<TOptions[number]>,
|
||||
options: input.options,
|
||||
searchable: input.searchable ?? false,
|
||||
withDescription: input.withDescription ?? false,
|
||||
@@ -112,18 +104,12 @@ const optionsFactory = {
|
||||
};
|
||||
|
||||
type WidgetOptionFactory = typeof optionsFactory;
|
||||
export type WidgetOptionDefinition = ReturnType<
|
||||
WidgetOptionFactory[keyof WidgetOptionFactory]
|
||||
>;
|
||||
export type WidgetOptionDefinition = ReturnType<WidgetOptionFactory[keyof WidgetOptionFactory]>;
|
||||
export type WidgetOptionsRecord = Record<string, WidgetOptionDefinition>;
|
||||
export type WidgetOptionType = WidgetOptionDefinition["type"];
|
||||
export type WidgetOptionOfType<TType extends WidgetOptionType> = Extract<
|
||||
WidgetOptionDefinition,
|
||||
{ type: TType }
|
||||
>;
|
||||
export type WidgetOptionOfType<TType extends WidgetOptionType> = Extract<WidgetOptionDefinition, { type: TType }>;
|
||||
|
||||
type inferOptionFromDefinition<TDefinition extends WidgetOptionDefinition> =
|
||||
TDefinition["defaultValue"];
|
||||
type inferOptionFromDefinition<TDefinition extends WidgetOptionDefinition> = TDefinition["defaultValue"];
|
||||
export type inferOptionsFromDefinition<TOptions extends WidgetOptionsRecord> = {
|
||||
[key in keyof TOptions]: inferOptionFromDefinition<TOptions[key]>;
|
||||
};
|
||||
@@ -162,10 +148,7 @@ export const optionsBuilder = {
|
||||
from: createOptions,
|
||||
};
|
||||
|
||||
export const reduceWidgetOptionsWithDefaultValues = (
|
||||
kind: WidgetKind,
|
||||
currentValue: Record<string, unknown> = {},
|
||||
) => {
|
||||
export const reduceWidgetOptionsWithDefaultValues = (kind: WidgetKind, currentValue: Record<string, unknown> = {}) => {
|
||||
const definition = widgetImports[kind].definition;
|
||||
const options = definition.options as Record<string, WidgetOptionDefinition>;
|
||||
return objectEntries(options).reduce(
|
||||
|
||||
@@ -12,31 +12,21 @@ type Data = Record<
|
||||
>;
|
||||
|
||||
interface GlobalItemServerDataContext {
|
||||
setItemServerData: (
|
||||
id: string,
|
||||
data: Record<string, unknown> | undefined,
|
||||
) => void;
|
||||
setItemServerData: (id: string, data: Record<string, unknown> | undefined) => void;
|
||||
data: Data;
|
||||
initalItemIds: string[];
|
||||
}
|
||||
|
||||
const GlobalItemServerDataContext =
|
||||
createContext<GlobalItemServerDataContext | null>(null);
|
||||
const GlobalItemServerDataContext = createContext<GlobalItemServerDataContext | null>(null);
|
||||
|
||||
interface Props {
|
||||
initalItemIds: string[];
|
||||
}
|
||||
|
||||
export const GlobalItemServerDataProvider = ({
|
||||
children,
|
||||
initalItemIds,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
export const GlobalItemServerDataProvider = ({ children, initalItemIds }: PropsWithChildren<Props>) => {
|
||||
const [data, setData] = useState<Data>({});
|
||||
|
||||
const setItemServerData = (
|
||||
id: string,
|
||||
itemData: Record<string, unknown> | undefined,
|
||||
) => {
|
||||
const setItemServerData = (id: string, itemData: Record<string, unknown> | undefined) => {
|
||||
setData((prev) => ({
|
||||
...prev,
|
||||
[id]: {
|
||||
@@ -47,9 +37,7 @@ export const GlobalItemServerDataProvider = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<GlobalItemServerDataContext.Provider
|
||||
value={{ setItemServerData, data, initalItemIds }}
|
||||
>
|
||||
<GlobalItemServerDataContext.Provider value={{ setItemServerData, data, initalItemIds }}>
|
||||
{children}
|
||||
</GlobalItemServerDataContext.Provider>
|
||||
);
|
||||
@@ -73,10 +61,7 @@ export const useServerDataFor = (id: string) => {
|
||||
return context.data[id];
|
||||
};
|
||||
|
||||
export const useServerDataInitializer = (
|
||||
id: string,
|
||||
serverData: Record<string, unknown> | undefined,
|
||||
) => {
|
||||
export const useServerDataInitializer = (id: string, serverData: Record<string, unknown> | undefined) => {
|
||||
const context = useContext(GlobalItemServerDataContext);
|
||||
|
||||
if (!context) {
|
||||
|
||||
@@ -14,11 +14,7 @@ type Props = PropsWithChildren<{
|
||||
board: Board;
|
||||
}>;
|
||||
|
||||
export const GlobalItemServerDataRunner = ({
|
||||
board,
|
||||
shouldRun,
|
||||
children,
|
||||
}: Props) => {
|
||||
export const GlobalItemServerDataRunner = ({ board, shouldRun, children }: Props) => {
|
||||
if (!shouldRun) return children;
|
||||
|
||||
const allItems = board.sections.flatMap((section) => section.items);
|
||||
@@ -45,10 +41,7 @@ const ItemDataLoader = async ({ item }: ItemDataLoaderProps) => {
|
||||
return <ClientServerDataInitalizer id={item.id} serverData={undefined} />;
|
||||
}
|
||||
const loader = await widgetImport.serverDataLoader();
|
||||
const optionsWithDefault = reduceWidgetOptionsWithDefaultValues(
|
||||
item.kind,
|
||||
item.options,
|
||||
);
|
||||
const optionsWithDefault = reduceWidgetOptionsWithDefaultValues(item.kind, item.options);
|
||||
const data = await loader.default({
|
||||
...item,
|
||||
options: optionsWithDefault as never,
|
||||
|
||||
@@ -13,9 +13,7 @@ import classes from "./component.module.css";
|
||||
|
||||
import "video.js/dist/video-js.css";
|
||||
|
||||
export default function VideoWidget({
|
||||
options,
|
||||
}: WidgetComponentProps<"video">) {
|
||||
export default function VideoWidget({ options }: WidgetComponentProps<"video">) {
|
||||
if (options.feedUrl.trim() === "") {
|
||||
return <NoUrl />;
|
||||
}
|
||||
@@ -48,9 +46,7 @@ const ForYoutubeUseIframe = () => {
|
||||
<Stack align="center" gap="xs">
|
||||
<IconBrandYoutube />
|
||||
<Title order={4}>{t("widget.video.error.forYoutubeUseIframe")}</Title>
|
||||
<Anchor href="https://homarr.dev/docs/widgets/iframe/">
|
||||
{t("common.action.checkoutDocs")}
|
||||
</Anchor>
|
||||
<Anchor href="https://homarr.dev/docs/widgets/iframe/">{t("common.action.checkoutDocs")}</Anchor>
|
||||
</Stack>
|
||||
</Center>
|
||||
);
|
||||
@@ -80,10 +76,7 @@ const Feed = ({ options }: Pick<WidgetComponentProps<"video">, "options">) => {
|
||||
|
||||
return (
|
||||
<Group justify="center" w="100%" h="100%" pos="relative">
|
||||
<video
|
||||
className={combineClasses("video-js", classes.video)}
|
||||
ref={videoRef}
|
||||
>
|
||||
<video className={combineClasses("video-js", classes.video)} ref={videoRef}>
|
||||
<source src={options.feedUrl} />
|
||||
</video>
|
||||
</Group>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Card, Flex, Group, Stack, Text, Title } from "@mantine/core";
|
||||
import {
|
||||
IconArrowDownRight,
|
||||
IconArrowUpRight,
|
||||
IconMapPin,
|
||||
} from "@tabler/icons-react";
|
||||
import { IconArrowDownRight, IconArrowUpRight, IconMapPin } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -11,10 +7,7 @@ import { clientApi } from "@homarr/api/client";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { WeatherIcon } from "./icon";
|
||||
|
||||
export default function WeatherWidget({
|
||||
options,
|
||||
width,
|
||||
}: WidgetComponentProps<"weather">) {
|
||||
export default function WeatherWidget({ options, width }: WidgetComponentProps<"weather">) {
|
||||
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(
|
||||
{
|
||||
latitude: options.location.latitude,
|
||||
@@ -29,34 +22,18 @@ export default function WeatherWidget({
|
||||
|
||||
return (
|
||||
<Stack w="100%" h="100%" justify="space-around" gap={0} align="center">
|
||||
<WeeklyForecast
|
||||
weather={weather}
|
||||
width={width}
|
||||
options={options}
|
||||
shouldHide={!options.hasForecast}
|
||||
/>
|
||||
<DailyWeather
|
||||
weather={weather}
|
||||
width={width}
|
||||
options={options}
|
||||
shouldHide={options.hasForecast}
|
||||
/>
|
||||
<WeeklyForecast weather={weather} width={width} options={options} shouldHide={!options.hasForecast} />
|
||||
<DailyWeather weather={weather} width={width} options={options} shouldHide={options.hasForecast} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface DailyWeatherProps
|
||||
extends Pick<WidgetComponentProps<"weather">, "width" | "options"> {
|
||||
interface DailyWeatherProps extends Pick<WidgetComponentProps<"weather">, "width" | "options"> {
|
||||
shouldHide: boolean;
|
||||
weather: RouterOutputs["widget"]["weather"]["atLocation"];
|
||||
}
|
||||
|
||||
const DailyWeather = ({
|
||||
shouldHide,
|
||||
width,
|
||||
options,
|
||||
weather,
|
||||
}: DailyWeatherProps) => {
|
||||
const DailyWeather = ({ shouldHide, width, options, weather }: DailyWeatherProps) => {
|
||||
if (shouldHide) {
|
||||
return null;
|
||||
}
|
||||
@@ -69,30 +46,16 @@ const DailyWeather = ({
|
||||
justify={"center"}
|
||||
direction={width < 200 ? "column" : "row"}
|
||||
>
|
||||
<WeatherIcon
|
||||
size={width < 300 ? 30 : 50}
|
||||
code={weather.current_weather.weathercode}
|
||||
/>
|
||||
<Title order={2}>
|
||||
{getPreferredUnit(
|
||||
weather.current_weather.temperature,
|
||||
options.isFormatFahrenheit,
|
||||
)}
|
||||
</Title>
|
||||
<WeatherIcon size={width < 300 ? 30 : 50} code={weather.current_weather.weathercode} />
|
||||
<Title order={2}>{getPreferredUnit(weather.current_weather.temperature, options.isFormatFahrenheit)}</Title>
|
||||
</Flex>
|
||||
|
||||
{width > 200 && (
|
||||
<Group wrap="nowrap" gap="xs">
|
||||
<IconArrowUpRight />
|
||||
{getPreferredUnit(
|
||||
weather.daily.temperature_2m_max[0]!,
|
||||
options.isFormatFahrenheit,
|
||||
)}
|
||||
{getPreferredUnit(weather.daily.temperature_2m_max[0]!, options.isFormatFahrenheit)}
|
||||
<IconArrowDownRight />
|
||||
{getPreferredUnit(
|
||||
weather.daily.temperature_2m_min[0]!,
|
||||
options.isFormatFahrenheit,
|
||||
)}
|
||||
{getPreferredUnit(weather.daily.temperature_2m_min[0]!, options.isFormatFahrenheit)}
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -106,30 +69,19 @@ const DailyWeather = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface WeeklyForecastProps
|
||||
extends Pick<WidgetComponentProps<"weather">, "width" | "options"> {
|
||||
interface WeeklyForecastProps extends Pick<WidgetComponentProps<"weather">, "width" | "options"> {
|
||||
shouldHide: boolean;
|
||||
weather: RouterOutputs["widget"]["weather"]["atLocation"];
|
||||
}
|
||||
|
||||
const WeeklyForecast = ({
|
||||
shouldHide,
|
||||
width,
|
||||
options,
|
||||
weather,
|
||||
}: WeeklyForecastProps) => {
|
||||
const WeeklyForecast = ({ shouldHide, width, options, weather }: WeeklyForecastProps) => {
|
||||
if (shouldHide) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex
|
||||
align="center"
|
||||
gap={width < 120 ? "0.25rem" : "xs"}
|
||||
justify="center"
|
||||
direction="row"
|
||||
>
|
||||
<Flex align="center" gap={width < 120 ? "0.25rem" : "xs"} justify="center" direction="row">
|
||||
{options.showCity && (
|
||||
<Group wrap="nowrap" gap="xs" align="center">
|
||||
<IconMapPin color="blue" size={30} />
|
||||
@@ -138,18 +90,9 @@ const WeeklyForecast = ({
|
||||
</Text>
|
||||
</Group>
|
||||
)}
|
||||
<WeatherIcon
|
||||
size={width < 300 ? 30 : 50}
|
||||
code={weather.current_weather.weathercode}
|
||||
/>
|
||||
<Title
|
||||
order={2}
|
||||
c={weather.current_weather.temperature > 20 ? "red" : "blue"}
|
||||
>
|
||||
{getPreferredUnit(
|
||||
weather.current_weather.temperature,
|
||||
options.isFormatFahrenheit,
|
||||
)}
|
||||
<WeatherIcon size={width < 300 ? 30 : 50} code={weather.current_weather.weathercode} />
|
||||
<Title order={2} c={weather.current_weather.temperature > 20 ? "red" : "blue"}>
|
||||
{getPreferredUnit(weather.current_weather.temperature, options.isFormatFahrenheit)}
|
||||
</Title>
|
||||
</Flex>
|
||||
<Forecast weather={weather} options={options} width={width} />
|
||||
@@ -157,8 +100,7 @@ const WeeklyForecast = ({
|
||||
);
|
||||
};
|
||||
|
||||
interface ForecastProps
|
||||
extends Pick<WidgetComponentProps<"weather">, "options" | "width"> {
|
||||
interface ForecastProps extends Pick<WidgetComponentProps<"weather">, "options" | "width"> {
|
||||
weather: RouterOutputs["widget"]["weather"]["atLocation"];
|
||||
}
|
||||
|
||||
@@ -166,31 +108,19 @@ function Forecast({ weather, options, width }: ForecastProps) {
|
||||
return (
|
||||
<Flex align="center" direction="row" justify="space-between" w="100%">
|
||||
{weather.daily.time
|
||||
.slice(
|
||||
0,
|
||||
Math.min(options.forecastDayCount, width / (width < 300 ? 64 : 92)),
|
||||
)
|
||||
.slice(0, Math.min(options.forecastDayCount, width / (width < 300 ? 64 : 92)))
|
||||
.map((time, index) => (
|
||||
<Card key={time}>
|
||||
<Flex direction="column" align="center">
|
||||
<Text fw={700} lh="1.25rem">
|
||||
{new Date(time).getDate().toString().padStart(2, "0")}
|
||||
</Text>
|
||||
<WeatherIcon
|
||||
size={width < 300 ? 20 : 50}
|
||||
code={weather.daily.weathercode[index]!}
|
||||
/>
|
||||
<WeatherIcon size={width < 300 ? 20 : 50} code={weather.daily.weathercode[index]!} />
|
||||
<Text fz={width < 300 ? "xs" : "sm"} lh="1rem">
|
||||
{getPreferredUnit(
|
||||
weather.daily.temperature_2m_max[index]!,
|
||||
options.isFormatFahrenheit,
|
||||
)}
|
||||
{getPreferredUnit(weather.daily.temperature_2m_max[index]!, options.isFormatFahrenheit)}
|
||||
</Text>
|
||||
<Text fz={width < 300 ? "xs" : "sm"} lh="1rem" c="grey">
|
||||
{getPreferredUnit(
|
||||
weather.daily.temperature_2m_min[index]!,
|
||||
options.isFormatFahrenheit,
|
||||
)}
|
||||
{getPreferredUnit(weather.daily.temperature_2m_min[index]!, options.isFormatFahrenheit)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Card>
|
||||
@@ -200,6 +130,4 @@ function Forecast({ weather, options, width }: ForecastProps) {
|
||||
}
|
||||
|
||||
const getPreferredUnit = (value: number, isFahrenheit = false): string =>
|
||||
isFahrenheit
|
||||
? `${(value * (9 / 5) + 32).toFixed(1)}°F`
|
||||
: `${value.toFixed(1)}°C`;
|
||||
isFahrenheit ? `${(value * (9 / 5) + 32).toFixed(1)}°F` : `${value.toFixed(1)}°C`;
|
||||
|
||||
@@ -28,8 +28,7 @@ export const WeatherIcon = ({ code, size = 50 }: WeatherIconProps) => {
|
||||
const t = useScopedI18n("widget.weather");
|
||||
|
||||
const { icon: Icon, name } =
|
||||
weatherDefinitions.find((definition) => definition.codes.includes(code)) ??
|
||||
unknownWeather;
|
||||
weatherDefinitions.find((definition) => definition.codes.includes(code)) ?? unknownWeather;
|
||||
|
||||
return (
|
||||
<Tooltip withinPortal withArrow label={t(`kind.${name}`)}>
|
||||
|
||||
@@ -5,36 +5,33 @@ import { z } from "@homarr/validation";
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition(
|
||||
"weather",
|
||||
{
|
||||
icon: IconCloud,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
isFormatFahrenheit: factory.switch(),
|
||||
location: factory.location({
|
||||
defaultValue: {
|
||||
name: "Paris",
|
||||
latitude: 48.85341,
|
||||
longitude: 2.3488,
|
||||
},
|
||||
}),
|
||||
showCity: factory.switch(),
|
||||
hasForecast: factory.switch(),
|
||||
forecastDayCount: factory.slider({
|
||||
defaultValue: 5,
|
||||
validate: z.number().min(1).max(7),
|
||||
step: 1,
|
||||
withDescription: true,
|
||||
}),
|
||||
export const { definition, componentLoader } = createWidgetDefinition("weather", {
|
||||
icon: IconCloud,
|
||||
options: optionsBuilder.from(
|
||||
(factory) => ({
|
||||
isFormatFahrenheit: factory.switch(),
|
||||
location: factory.location({
|
||||
defaultValue: {
|
||||
name: "Paris",
|
||||
latitude: 48.85341,
|
||||
longitude: 2.3488,
|
||||
},
|
||||
}),
|
||||
{
|
||||
forecastDayCount: {
|
||||
shouldHide({ hasForecast }) {
|
||||
return !hasForecast;
|
||||
},
|
||||
showCity: factory.switch(),
|
||||
hasForecast: factory.switch(),
|
||||
forecastDayCount: factory.slider({
|
||||
defaultValue: 5,
|
||||
validate: z.number().min(1).max(7),
|
||||
step: 1,
|
||||
withDescription: true,
|
||||
}),
|
||||
}),
|
||||
{
|
||||
forecastDayCount: {
|
||||
shouldHide({ hasForecast }) {
|
||||
return !hasForecast;
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
).withDynamicImport(() => import("./component"));
|
||||
},
|
||||
),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
.pill {
|
||||
cursor: default;
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-white),
|
||||
var(--mantine-color-dark-7)
|
||||
);
|
||||
border: rem(1px) solid
|
||||
light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-7));
|
||||
background-color: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
|
||||
border: rem(1px) solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-7));
|
||||
padding-left: var(--mantine-spacing-xs);
|
||||
border-radius: var(--mantine-radius-xl);
|
||||
}
|
||||
|
||||
@@ -65,11 +65,7 @@ export const WidgetIntegrationSelect = ({
|
||||
|
||||
const options = data.map((item) => {
|
||||
return (
|
||||
<Combobox.Option
|
||||
value={item.id}
|
||||
key={item.id}
|
||||
active={multiSelectValues.includes(item.id)}
|
||||
>
|
||||
<Combobox.Option value={item.id} key={item.id} active={multiSelectValues.includes(item.id)}>
|
||||
<Group gap="sm" align="center">
|
||||
{multiSelectValues.includes(item.id) ? <CheckIcon size={12} /> : null}
|
||||
<Group gap={7} align="center">
|
||||
@@ -87,25 +83,11 @@ export const WidgetIntegrationSelect = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
store={combobox}
|
||||
onOptionSubmit={handleValueSelect}
|
||||
withinPortal={false}
|
||||
>
|
||||
<Combobox store={combobox} onOptionSubmit={handleValueSelect} withinPortal={false}>
|
||||
<Combobox.DropdownTarget>
|
||||
<PillsInput
|
||||
pointer
|
||||
onClick={() => combobox.toggleDropdown()}
|
||||
{...props}
|
||||
>
|
||||
<PillsInput pointer onClick={() => combobox.toggleDropdown()} {...props}>
|
||||
<Pill.Group>
|
||||
{values.length > 0 ? (
|
||||
values
|
||||
) : (
|
||||
<Input.Placeholder>
|
||||
{t("common.multiSelect.placeholder")}
|
||||
</Input.Placeholder>
|
||||
)}
|
||||
{values.length > 0 ? values : <Input.Placeholder>{t("common.multiSelect.placeholder")}</Input.Placeholder>}
|
||||
|
||||
<Combobox.EventsTarget>
|
||||
<PillsInput.Field
|
||||
@@ -115,9 +97,7 @@ export const WidgetIntegrationSelect = ({
|
||||
if (event.key !== "Backspace") return;
|
||||
|
||||
event.preventDefault();
|
||||
handleValueRemove(
|
||||
multiSelectValues[multiSelectValues.length - 1]!,
|
||||
);
|
||||
handleValueRemove(multiSelectValues[multiSelectValues.length - 1]!);
|
||||
}}
|
||||
/>
|
||||
</Combobox.EventsTarget>
|
||||
@@ -150,13 +130,6 @@ const IntegrationPill = ({ option, onRemove }: IntegrationPillProps) => (
|
||||
<Text span size="xs" lh={1} fw={500}>
|
||||
{option.name}
|
||||
</Text>
|
||||
<CloseButton
|
||||
onMouseDown={onRemove}
|
||||
variant="transparent"
|
||||
color="gray"
|
||||
size={22}
|
||||
iconSize={14}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<CloseButton onMouseDown={onRemove} variant="transparent" color="gray" size={22} iconSize={14} tabIndex={-1} />
|
||||
</Group>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user