Compare commits

..
Author SHA1 Message Date
ajnart d04c8c437f style: auto-fix lint and format 2026-07-21 15:59:33 +00:00
ajnart 28fefcd23f fix(widgets): fix hover card disappearing and remove group wrapper
- Remove wrapper div that broke HoverCard hover tracking between
  target and dropdown (root cause of instant disappear)
- Remove useElementSize, always show notification dots
- Increase closeDelay to 400ms (per Mantine interactive-elements
  pattern) so users can move mouse into dropdown with link buttons
- Remove HoverCard.Group from component.tsx per feedback
2026-07-21 17:58:19 +02:00
ajnart c08054736e fix(widgets): fix missing event dots and instant open in calendar
- Move useElementSize ref to wrapper div so HoverCard.Target's ref
  merging doesn't break element measurement (dots were hidden because
  height returned 0, triggering isTooSmallForIndicators)
- Add pos='relative' to Container so NotificationIndicator anchors
  to the day cell
- Add openDelay/closeDelay directly to each HoverCard as fallback in
  case HoverCard.Group context doesn't propagate through Mantine
  Calendar's internal rendering
2026-07-21 17:49:44 +02:00
ajnart 2e20fd7da5 fix(widgets): remove redundant calendar day CSS module
The border highlight was redundant — HoverCard's arrow already indicates
which day is active. Removes the CSS file and className import entirely.
2026-07-21 16:22:42 +02:00
ajnart 1856d20a47 fix(widgets): wrap calendar days in HoverCard.Group for shared delays
Uses HoverCard.Group with openDelay={350} and closeDelay={200} so all
calendar day hover cards share synchronized delays. The closeDelay
gives users time to move from the day cell to the dropdown where
interactive link buttons live (per Mantine's interactive elements
pattern).
2026-07-21 16:16:07 +02:00
ajnart 6df2f92268 fix(widgets): use HoverCard with openDelay for calendar day events
Replaces Popover with Mantine HoverCard for built-in hover behavior:
- openDelay={350} prevents triggering when dragging across days
- closeOnClickOutside and closeOnEscape are true by default
- No manual mouse handlers or state needed
2026-07-21 16:14:34 +02:00
ajnart c30f525a83 fix(widgets): simplify calendar day popover to uncontrolled click-to-open
Replaces the hover+pin+timeout implementation with Mantine Popover's
built-in uncontrolled behavior (click to open, click outside/escape to
close). Removes all component state, mouse handlers, timeout management,
and the cross-popover close machinery from the parent.

- 56 lines removed, 13 added across 3 files
- Border highlight now via CSS aria-expanded selector
- Resolves the messy hover navigation issue (#2637)
2026-07-21 16:02:18 +02:00
26 changed files with 84 additions and 216 deletions

No files matched your search

@@ -1,6 +1,6 @@
---
title: 'Immich Album Carousel'
description: 'Shows a slideshow of pictures from an Immich album or your full library'
description: 'Shows a slideshow of your album pictures from Immich'
hide_title: true
---
@@ -37,3 +37,4 @@ import screen from './img/screen.png';
### Configuration
<WidgetConfig configuration={immichAlbumCarouselWidget.configuration} />
@@ -4,16 +4,15 @@ import { IconPhoto } from "@tabler/icons-react";
export const immichAlbumCarouselWidget: WidgetDefinition = {
icon: IconPhoto,
name: "Immich Album",
description: "Shows a slideshow of pictures from an Immich album or your full library",
description: "Shows a slideshow of your album pictures from Immich",
path: "../../widgets/immich-album-carousel",
configuration: {
items: [
{
name: "Album",
description:
"Select an Immich album or All photos. An empty selection also shows random photos from your full library.",
description: "Select the Immich album to show in the slideshow.",
values: { type: "string" },
defaultValue: "All photos",
defaultValue: "",
},
{
name: "Interval in seconds",
@@ -16,10 +16,7 @@ export default async function AppEditPage(props: AppEditPageProps) {
const params = await props.params;
const session = await auth();
if (
!session?.user.permissions.includes("board-modify-all") ||
!session?.user.permissions.includes("app-modify-all")
) {
if (!session?.user.permissions.includes("app-modify-all")) {
notFound();
}
const app = await api.app.byId({ id: params.id });
@@ -10,7 +10,7 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
export default async function AppNewPage() {
const session = await auth();
if (!session?.user.permissions.includes("board-modify-all") || !session?.user.permissions.includes("app-create")) {
if (!session?.user.permissions.includes("app-create")) {
notFound();
}
@@ -1,5 +1,5 @@
import { Fragment } from "react";
import { notFound, redirect } from "next/navigation";
import { redirect } from "next/navigation";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text } from "@mantine/core";
import { IconBox, IconPencil } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -33,9 +33,6 @@ export default async function AppsPage(props: AppsPageProps) {
if (!session) {
redirect("/auth/login");
}
if (!session.user.permissions.includes("board-modify-all")) {
notFound();
}
const searchParams = searchParamsSchema.parse(await props.searchParams);
@@ -61,7 +61,7 @@ export default async function ManageLayout({ children }: PropsWithChildren) {
icon: IconBox,
href: "/manage/apps",
label: t("items.apps"),
hidden: !session?.user.permissions.includes("board-modify-all"),
hidden: !session,
iconProps: {
strokeWidth: 2.5,
},
+1 -2
View File
@@ -18,8 +18,7 @@ import { AppAccessControl } from "./app/app-access-control";
const defaultIcon = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/homarr.svg";
export const appRouter = createTRPCRouter({
getPaginated: permissionRequiredProcedure
.requiresPermission("board-modify-all")
getPaginated: protectedProcedure
.input(paginatedSchema)
.output(z.object({ items: z.array(selectAppSchema), totalCount: z.number() }))
.meta({
+1 -1
View File
@@ -101,7 +101,7 @@ export const homeRouter = createTRPCRouter({
});
}
if (ctx.session?.user.permissions.includes("board-modify-all")) {
if (ctx.session?.user) {
statistics.push({
titleKey: "app",
subtitleKey: "resources",
-27
View File
@@ -76,33 +76,6 @@ describe("all should return all apps", () => {
});
});
describe("getPaginated should require board modification access", () => {
test("should throw FORBIDDEN without board-modify-all", async () => {
const caller = appRouter.createCaller({
db: createDb(),
deviceType: undefined,
session: createDefaultSession(),
});
await expect(caller.getPaginated({ page: 1, pageSize: 10 })).rejects.toThrow("Permission denied");
});
test("should return apps with board-modify-all", async () => {
const db = createDb();
const caller = appRouter.createCaller({
db,
deviceType: undefined,
session: createDefaultSession(["board-modify-all"]),
});
await db.insert(apps).values({ id: "1", name: "Homarr", iconUrl: "https://homarr.dev/icon.svg" });
await expect(caller.getPaginated({ page: 1, pageSize: 10 })).resolves.toMatchObject({
totalCount: 1,
items: [{ id: "1" }],
});
});
});
describe("byId should return an app by id", () => {
test("should return an app by id when canUserSeeAppAsync returns true", async () => {
// Arrange
+1 -1
View File
@@ -25,7 +25,7 @@ export const immichRouter = createTRPCRouter({
.concat(createImmichIntegrationMiddleware("query"))
.input(
z.object({
albumId: z.string().optional(),
albumId: z.string(),
}),
)
.query(async ({ ctx, input }) => {
@@ -1,14 +1,11 @@
import {
AssetMediaSize,
AssetTypeEnum,
AssetVisibility,
getAlbumInfo,
getAllAlbums,
getMyUser,
getServerStatistics,
init,
searchAssets,
searchRandom,
searchUsers,
} from "@immich/sdk";
import type { AssetResponseDto, MetadataSearchDto } from "@immich/sdk";
@@ -46,7 +43,6 @@ export interface ImmichAsset {
}
const logger = createLogger({ module: "immich-integration" });
const randomPhotoCount = 50;
export class ImmichIntegration extends Integration {
constructor(integration: IntegrationInput) {
@@ -67,36 +63,38 @@ export class ImmichIntegration extends Integration {
};
}
public async getAlbumAsync(albumId?: string): Promise<ImmichAlbum> {
public async getAlbumAsync(albumId: string): Promise<ImmichAlbum> {
this.initClient();
const requestOptions = this.getRequestOptions();
if (!albumId) {
const assets = await searchRandom(
{
randomSearchDto: {
size: randomPhotoCount,
type: AssetTypeEnum.Image,
visibility: AssetVisibility.Timeline,
},
},
requestOptions,
);
return {
id: "",
albumName: "",
assets: await this.createAssetsAsync(assets),
};
}
const [album, albumAssets] = await Promise.all([
getAlbumInfo({ id: albumId }, requestOptions),
this.searchAlbumAssetsAsync(albumId),
]);
const imageProxy = new ImageProxy();
return {
albumName: album.albumName,
assets: await this.createAssetsAsync(albumAssets),
assets: await Promise.all(
albumAssets.map(async (asset) => {
const publicLink = await imageProxy.createImageAsync(
// This is the URL generated by the SDK's viewAsset call. Preview images are
// browser-compatible while avoiding the large original asset download.
this.url(`/api/assets/${asset.id}/thumbnail`, { size: AssetMediaSize.Preview }).toString(),
{
"x-api-key": this.getSecretValue("apiKey"),
},
);
return {
id: asset.id,
type: asset.type,
thumbhash: asset.thumbhash,
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
originalPath: asset.originalPath,
updatedAt: asset.updatedAt,
publicLink,
};
}),
),
id: album.id,
};
}
@@ -138,32 +136,6 @@ export class ImmichIntegration extends Integration {
return assets;
}
private async createAssetsAsync(assets: AssetResponseDto[]): Promise<ImmichAsset[]> {
const imageProxy = new ImageProxy();
return await Promise.all(
assets.map(async (asset) => {
const publicLink = await imageProxy.createImageAsync(
// This is the URL generated by the SDK's viewAsset call. Preview images are
// browser-compatible while avoiding the large original asset download.
this.url(`/api/assets/${asset.id}/thumbnail`, { size: AssetMediaSize.Preview }).toString(),
{
"x-api-key": this.getSecretValue("apiKey"),
},
);
return {
id: asset.id,
type: asset.type,
thumbhash: asset.thumbhash,
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
originalPath: asset.originalPath,
updatedAt: asset.updatedAt,
publicLink,
};
}),
);
}
private getRequestOptions(fetchAsync = fetchWithTrustedCertificatesAsync) {
return {
// Undici and node types are not the same
+1 -1
View File
@@ -33,7 +33,7 @@ export const immichAlbumsRequestHandler = createIntegrationRequestHandler<
export const immichAlbumRequestHandler = createIntegrationRequestHandler<
ImmichAlbum,
IntegrationKindByCategory<"photoService">,
{ albumId?: string }
{ albumId: string }
>({
async requestAsync(integration, input) {
const integrationInstance = await createIntegrationAsync(integration);
@@ -16,6 +16,4 @@ export const mediaRequestStatsRequestHandler = createIntegrationRequestHandler<
users: await integrationInstance.getUsersAsync(),
};
},
cacheTtlMs: 60_000,
fallbackToStaleOnError: true,
});
+1 -4
View File
@@ -9,7 +9,7 @@ export const weatherRequestHandler = createWidgetRequestHandler({
async requestAsync(input: { latitude: number; longitude: number }) {
const res = await withTimeoutAsync(async (signal) => {
return await fetchWithTrustedCertificatesAsync(
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max,relative_humidity_2m_mean&current_weather=true&timezone=auto`,
`https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,sunrise,sunset,wind_speed_10m_max,wind_gusts_10m_max&current_weather=true&timezone=auto`,
{ signal },
);
});
@@ -27,7 +27,6 @@ export const weatherRequestHandler = createWidgetRequestHandler({
sunset: weather.daily.sunset[index],
maxWindSpeed: weather.daily.wind_speed_10m_max[index],
maxWindGusts: weather.daily.wind_gusts_10m_max[index],
humidity: weather.daily.relative_humidity_2m_mean[index],
};
}),
} satisfies Weather;
@@ -49,7 +48,6 @@ const atLocationOutput = z.object({
sunset: z.array(z.string()),
wind_speed_10m_max: z.array(z.number()),
wind_gusts_10m_max: z.array(z.number()),
relative_humidity_2m_mean: z.array(z.number()),
}),
});
@@ -68,6 +66,5 @@ export interface Weather {
sunset: string | undefined;
maxWindSpeed: number | undefined;
maxWindGusts: number | undefined;
humidity: number | undefined;
}[];
}
@@ -17,10 +17,6 @@ export const appIntegrationBoardMode = {
return groups;
}
return groups.concat(
session.user.permissions.includes("board-modify-all")
? [appsSearchGroup, integrationsSearchGroup]
: [integrationsSearchGroup],
);
return groups.concat([appsSearchGroup, integrationsSearchGroup]);
},
} satisfies SearchMode;
@@ -87,8 +87,7 @@ export const globalCommandGroup = createGroup<Command>({
icon: IconBox,
name: tOption("newApp.label"),
useInteraction: interaction.link(() => ({ href: "/manage/apps/new" })),
hidden:
!session?.user.permissions.includes("board-modify-all") || !session?.user.permissions.includes("app-create"),
hidden: !session?.user.permissions.includes("app-create"),
},
{
commandKey: "newIntegration",
@@ -76,7 +76,7 @@ export const pagesSearchGroup = createGroup<{
icon: IconBox,
path: "/manage/apps",
name: t("manageApp.label"),
hidden: !session?.user.permissions.includes("board-modify-all"),
hidden: !session,
},
{
icon: IconPlug,
+5 -6
View File
@@ -2257,8 +2257,7 @@
"sunrise": "Sunrise",
"sunset": "Sunset",
"maxWindSpeed": "Max wind speed: {maxWindSpeed} {unit}",
"maxWindGusts": "Max wind gusts: {maxWindGusts} {unit}",
"humidity": "Humidity: {humidity}%"
"maxWindGusts": "Max wind gusts: {maxWindGusts} {unit}"
},
"kind": {
"clear": "Clear",
@@ -3007,11 +3006,10 @@
},
"immich-albumCarousel": {
"name": "Immich Album",
"allPhotos": "All photos",
"option": {
"albumId": {
"label": "Album",
"description": "Select an Immich album or All photos. An empty selection also shows random photos from your full library."
"description": "Select the Immich album to show in the slideshow."
},
"rotationIntervalSeconds": {
"label": "Interval in seconds",
@@ -3026,8 +3024,9 @@
"description": "Show photos in a random order instead of always starting with the same album order."
}
},
"description": "Slideshow of pictures from an Immich album or your full library",
"noPhotos": "No photos found"
"description": "Slideshow of pictures in your Immich album",
"noAlbumSelected": "No album selected, edit widget and choose an album",
"noPhotos": "No photos in this album"
},
"downloads": {
"name": "Download Client",
+13 -56
View File
@@ -1,6 +1,4 @@
import { useCallback, useState } from "react";
import { Box, Container, Flex, Popover, Text, useMantineTheme } from "@mantine/core";
import { useElementSize } from "@mantine/hooks";
import { Box, Container, Flex, HoverCard, Text, useMantineTheme } from "@mantine/core";
import { useRequiredBoard } from "@homarr/boards/context";
import type { CalendarEvent } from "@homarr/integrations/types";
@@ -13,14 +11,9 @@ interface CalendarDayProps {
disabled: boolean;
rootWidth: number;
rootHeight: number;
onOpen: (close: () => void) => void;
}
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onOpen }: CalendarDayProps) => {
const [opened, setOpened] = useState(false);
const [pinned, setPinned] = useState(false);
const { primaryColor } = useMantineTheme();
const { ref, height } = useElementSize();
export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth }: CalendarDayProps) => {
const board = useRequiredBoard();
const mantineTheme = useMantineTheme();
const actualItemRadius = mantineTheme.radius[board.itemRadius];
@@ -29,39 +22,19 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onO
const shouldScaleDown = minAxisSize < 350;
const isSmall = rootHeight < 256;
const isTooSmallForIndicators = height < 30;
const handleMouseEnter = useCallback(() => {
if (disabled) return;
onOpen(() => {
setOpened(false);
setPinned(false);
});
setOpened(true);
}, [disabled, onOpen]);
const handleMouseLeave = useCallback(() => {
if (!pinned) setOpened(false);
}, [pinned]);
return (
<Popover
<HoverCard
position="bottom"
withArrow
withinPortal
radius="lg"
shadow="sm"
transitionProps={{
transition: "pop",
}}
onChange={(value) => {
setOpened(value);
if (!value) setPinned(false);
}}
opened={opened}
transitionProps={{ transition: "pop" }}
openDelay={350}
closeDelay={400}
disabled={disabled}
>
<Popover.Target>
<HoverCard.Target>
<Container
h="100%"
w="100%"
@@ -69,38 +42,23 @@ export const CalendarDay = ({ date, events, disabled, rootHeight, rootWidth, onO
pt={isSmall ? 0 : 10}
pb={isSmall ? 0 : 10}
m={0}
ref={ref}
bd={`2px solid ${opened && !disabled ? primaryColor : "transparent"}`}
pos="relative"
style={{
alignContent: "center",
borderRadius: actualItemRadius,
cursor: disabled ? "default" : "pointer",
}}
onClick={() => {
if (!disabled) setPinned((prev) => !prev);
}}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Text ta={"center"} size={shouldScaleDown ? "xs" : "md"} lh={1}>
{date.getDate()}
</Text>
{!isTooSmallForIndicators && <NotificationIndicator events={events} isSmall={isSmall} />}
<NotificationIndicator events={events} isSmall={isSmall} />
</Container>
</Popover.Target>
{/* Popover has some offset on the left side, padding is removed because of scrollarea paddings */}
<Popover.Dropdown
maw="calc(100vw - 24px)"
w={512}
pe={4}
pb={0}
style={{ overflow: "hidden" }}
onMouseEnter={() => setOpened(true)}
onMouseLeave={handleMouseLeave}
>
</HoverCard.Target>
<HoverCard.Dropdown maw="calc(100vw - 24px)" w={512} pe={4} pb={0} style={{ overflow: "hidden" }}>
<CalendarEventList events={events} />
</Popover.Dropdown>
</Popover>
</HoverCard.Dropdown>
</HoverCard>
);
};
@@ -111,7 +69,6 @@ interface NotificationIndicatorProps {
const NotificationIndicator = ({ events, isSmall }: NotificationIndicatorProps) => {
const notificationEvents = [...new Set(events.map((event) => event.indicatorColor))].filter(String);
/* position bottom is lower when small to not be on top of number*/
return (
<Flex
w="75%"
+1 -7
View File
@@ -1,6 +1,6 @@
"use client";
import { useCallback, useMemo, useRef, useState } from "react";
import { useMemo, useState } from "react";
import { useParams } from "next/navigation";
import { useMantineTheme } from "@mantine/core";
import { Calendar } from "@mantine/dates";
@@ -65,11 +65,6 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
const isSmall = width < 256;
const normalizedEvents = useMemo(() => splitEvents(events), [events]);
const activeCloseRef = useRef<(() => void) | null>(null);
const onDayOpen = useCallback((close: () => void) => {
activeCloseRef.current?.();
activeCloseRef.current = close;
}, []);
return (
<Calendar
@@ -146,7 +141,6 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
disabled={isEditMode || eventsForDate.length === 0}
rootWidth={width}
rootHeight={height}
onOpen={onDayOpen}
/>
);
}}
@@ -11,7 +11,6 @@ import { useI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../../common/empty-state";
import type { WidgetComponentProps } from "../../definition";
import classes from "./component.module.css";
import { ALL_PHOTOS_ALBUM_ID } from "./constants";
export default function ImmichAlbumCarouselWidget({
integrationIds,
@@ -22,9 +21,9 @@ export default function ImmichAlbumCarouselWidget({
const { data: album } = clientApi.widget.immich.getAlbum.useQuery(
{
integrationId: integrationIds[0] ?? "",
albumId: options.albumId && options.albumId !== ALL_PHOTOS_ALBUM_ID ? options.albumId : undefined,
albumId: options.albumId ?? "",
},
{ enabled: integrationIds.length > 0 },
{ enabled: Boolean(options.albumId) },
);
const photoAssets = useMemo(() => {
@@ -36,6 +35,10 @@ export default function ImmichAlbumCarouselWidget({
setCurrentPhotoIndex(0);
}, [photoAssets]);
if (!options.albumId) {
return <NoAlbumSelected />;
}
if (!album) return <WidgetEmptyState />;
if (album.assets.length === 0) {
@@ -114,6 +117,20 @@ function Carousel({ assets, currentIndex, setCurrentIndex, rotationInterval, sho
);
}
function NoAlbumSelected() {
const t = useI18n();
return (
<Center h="100%">
<Stack align="center" gap="xs">
<IconAlertCircle size={32} />
<Text size="sm" fw={500}>
{t("widget.immich-albumCarousel.noAlbumSelected")}
</Text>
</Stack>
</Center>
);
}
function NoPhotosInAlbum() {
const t = useI18n();
return (
@@ -1 +0,0 @@
export const ALL_PHOTOS_ALBUM_ID = "all";
@@ -2,11 +2,9 @@ import { IconPhoto } from "@tabler/icons-react";
import z from "zod";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
import { createWidgetDefinition } from "../../definition";
import { optionsBuilder } from "../../options";
import { ALL_PHOTOS_ALBUM_ID } from "./constants";
export const { definition, componentLoader } = createWidgetDefinition("immich-albumCarousel", {
icon: IconPhoto,
@@ -17,11 +15,9 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
createOptions() {
return optionsBuilder.from((factory) => ({
albumId: factory.integrationSelect({
defaultValue: ALL_PHOTOS_ALBUM_ID,
withDescription: true,
clearable: true,
useOptions: (integrationIds: string[]) => {
const t = useScopedI18n("widget.immich-albumCarousel");
const {
data = [],
isPending,
@@ -30,14 +26,7 @@ export const { definition, componentLoader } = createWidgetDefinition("immich-al
{ integrationId: integrationIds[0] ?? "" },
{ enabled: integrationIds.length > 0, staleTime: 15 * 60 * 1000 },
);
return {
data: [
{ value: ALL_PHOTOS_ALBUM_ID, label: t("allPhotos") },
...data.map((album) => ({ value: album.id, label: album.albumName })),
],
isPending,
isError,
};
return { data: data.map((album) => ({ value: album.id, label: album.albumName })), isPending, isError };
},
}),
rotationIntervalSeconds: factory.number({
@@ -66,12 +66,10 @@ export default function MediaServerWidget({ options, integrationIds }: WidgetCom
const currentlyPlaying = row.original.currentlyPlaying;
if (!currentlyPlaying) return null;
const playback = currentlyPlaying.playback;
const isPaused = playback?.state === "paused";
const isPaused = currentlyPlaying.playback.state === "paused";
const Icon = isPaused ? IconPlayerPause : mediaTypeIconMap[currentlyPlaying.type];
const positionMs = playback?.positionMs ?? null;
const durationMs = playback?.durationMs ?? null;
const { positionMs, durationMs } = currentlyPlaying.playback;
const progressPercent =
positionMs !== null && durationMs !== null && durationMs > 0
? Math.min(100, Math.round((positionMs / durationMs) * 100))
+1 -8
View File
@@ -1,7 +1,7 @@
"use client";
import { Box, Group, HoverCard, Stack, Text } from "@mantine/core";
import { IconArrowDownRight, IconArrowUpRight, IconDroplets, IconMapPin, IconWind } from "@tabler/icons-react";
import { IconArrowDownRight, IconArrowUpRight, IconMapPin, IconWind } from "@tabler/icons-react";
import combineClasses from "clsx";
import dayjs from "dayjs";
@@ -87,12 +87,6 @@ const DailyWeather = ({ options, weather }: WeatherProps) => {
</Text>
</Group>
)}
{weather.daily[0]?.humidity !== undefined && (
<Group className="weather-humidity-group" wrap="nowrap" gap="xs">
<IconDroplets size={16} />
<Text fz={16}>{t("dailyForecast.humidity", { humidity: weather.daily[0].humidity })}</Text>
</Group>
)}
<Group className="weather-max-min-temp-group" wrap="nowrap" gap="sm">
<Group gap="xs" wrap="nowrap">
<IconArrowUpRight size={16} />
@@ -210,7 +204,6 @@ function Forecast({ weather, options }: WeatherProps) {
sunset={dayjs(dayWeather.sunset).format("HH:mm")}
maxWindSpeed={dayWeather.maxWindSpeed}
maxWindGusts={dayWeather.maxWindGusts}
humidity={dayWeather.humidity}
/>
</HoverCard.Dropdown>
</HoverCard>
-6
View File
@@ -5,7 +5,6 @@ import {
IconCloudRain,
IconCloudSnow,
IconCloudStorm,
IconDroplets,
IconMoon,
IconQuestionMark,
IconSnowflake,
@@ -52,7 +51,6 @@ interface WeatherDescriptionProps {
sunset?: string;
maxWindSpeed?: number;
maxWindGusts?: number;
humidity?: number;
}
/**
@@ -80,7 +78,6 @@ export const WeatherDescription = ({
sunset,
maxWindSpeed,
maxWindGusts,
humidity,
}: WeatherDescriptionProps) => {
const t = useScopedI18n("widget.weather");
const tCommon = useScopedI18n("common");
@@ -100,9 +97,6 @@ export const WeatherDescription = ({
<List.Item icon={<IconTemperatureMinus size={15} />}>{`${tCommon("information.min")}: ${minTemp}`}</List.Item>
<List.Item icon={<IconSun size={15} />}>{`${t("dailyForecast.sunrise")}: ${sunrise}`}</List.Item>
<List.Item icon={<IconMoon size={15} />}>{`${t("dailyForecast.sunset")}: ${sunset}`}</List.Item>
{humidity !== undefined && (
<List.Item icon={<IconDroplets size={15} />}>{t("dailyForecast.humidity", { humidity })}</List.Item>
)}
{maxWindSpeed !== undefined && (
<List.Item icon={<IconWind size={15} />}>
{t("dailyForecast.maxWindSpeed", {