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:
Thomas Camlong
2024-05-19 22:38:39 +02:00
committed by GitHub
parent 919161798e
commit f1b1ec59ec
234 changed files with 2444 additions and 5375 deletions

View File

@@ -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;

View File

@@ -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>();

View File

@@ -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];
};

View File

@@ -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;

View File

@@ -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>
)}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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>
) : (

View File

@@ -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"));

View File

@@ -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 };

View File

@@ -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 () => {

View File

@@ -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"];

View File

@@ -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>;

View File

@@ -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"));

View File

@@ -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);

View File

@@ -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,
});

View File

@@ -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} />;
}

View File

@@ -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"));

View File

@@ -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>

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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>

View File

@@ -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`;

View File

@@ -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}`)}>

View File

@@ -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"));

View File

@@ -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);
}

View File

@@ -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>
);