chore(release): automatic release v1.39.0

This commit is contained in:
homarr-releases[bot]
2025-09-26 19:13:45 +00:00
committed by GitHub
61 changed files with 1273 additions and 895 deletions

View File

@@ -33,6 +33,7 @@ body:
options:
# The below comment is used to insert a new version with on-release.yml
#NEXT_VERSION#
- 1.38.0
- 1.37.0
- 1.36.1
- 1.36.0

View File

@@ -57,10 +57,10 @@
"@mantine/modals": "^8.3.1",
"@mantine/tiptap": "^8.3.1",
"@million/lint": "1.0.14",
"@tabler/icons-react": "^3.34.1",
"@tanstack/react-query": "^5.87.4",
"@tanstack/react-query-devtools": "^5.87.4",
"@tanstack/react-query-next-experimental": "^5.87.4",
"@tabler/icons-react": "^3.35.0",
"@tanstack/react-query": "^5.89.0",
"@tanstack/react-query-devtools": "^5.89.0",
"@tanstack/react-query-next-experimental": "^5.89.0",
"@trpc/client": "^11.5.1",
"@trpc/next": "^11.5.1",
"@trpc/react-query": "^11.5.1",
@@ -83,24 +83,24 @@
"react-dom": "19.1.1",
"react-error-boundary": "^6.0.0",
"react-simple-code-editor": "^0.14.1",
"sass": "^1.92.1",
"sass": "^1.93.0",
"superjson": "2.2.2",
"swagger-ui-react": "^5.29.0",
"use-deep-compare-effect": "^1.8.1",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/chroma-js": "3.1.1",
"@types/node": "^22.18.3",
"@types/node": "^22.18.6",
"@types/prismjs": "^1.26.5",
"@types/react": "19.1.13",
"@types/react-dom": "19.1.9",
"@types/swagger-ui-react": "^5.18.0",
"concurrently": "^9.2.1",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"node-loader": "^2.1.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"

View File

@@ -47,10 +47,10 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node": "^22.18.3",
"@types/node": "^22.18.6",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.9",
"eslint": "^9.35.0",
"esbuild": "^0.25.10",
"eslint": "^9.36.0",
"prettier": "^3.6.2",
"tsx": "4.20.4",
"typescript": "^5.9.2"

View File

@@ -34,8 +34,8 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/ws": "^8.18.1",
"esbuild": "^0.25.9",
"eslint": "^9.35.0",
"esbuild": "^0.25.10",
"eslint": "^9.36.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}

View File

@@ -42,23 +42,23 @@
"@semantic-release/github": "^11.0.6",
"@semantic-release/npm": "^12.0.2",
"@semantic-release/release-notes-generator": "^14.1.0",
"@testcontainers/redis": "^11.5.1",
"@testcontainers/redis": "^11.6.0",
"@turbo/gen": "^2.5.6",
"@vitejs/plugin-react": "^5.0.2",
"@vitejs/plugin-react": "^5.0.3",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"conventional-changelog-conventionalcommits": "^9.1.0",
"cross-env": "^10.0.0",
"jsdom": "^27.0.0",
"prettier": "^3.6.2",
"semantic-release": "^24.2.8",
"testcontainers": "^11.5.1",
"semantic-release": "^24.2.9",
"testcontainers": "^11.6.0",
"turbo": "^2.5.6",
"typescript": "^5.9.2",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4"
},
"packageManager": "pnpm@10.16.1",
"packageManager": "pnpm@10.17.0",
"engines": {
"node": ">=22.19.0"
},
@@ -80,20 +80,20 @@
"axios@>=1.0.0 <1.8.2": ">=1.12.2",
"brace-expansion@>=2.0.0 <=2.0.1": ">=4.0.1",
"brace-expansion@>=1.0.0 <=1.1.11": ">=4.0.1",
"esbuild@<=0.24.2": ">=0.25.9",
"esbuild@<=0.24.2": ">=0.25.10",
"form-data@>=4.0.0 <4.0.4": ">=4.0.4",
"hono@<4.6.5": ">=4.9.7",
"hono@<4.6.5": ">=4.9.8",
"linkifyjs@<4.3.2": ">=4.3.2",
"nanoid@>=4.0.0 <5.0.9": ">=5.1.5",
"prismjs@<1.30.0": ">=1.30.0",
"proxmox-api>undici": "7.16.0",
"react-is": "^19.1.1",
"rollup@>=4.0.0 <4.22.4": ">=4.50.1",
"rollup@>=4.0.0 <4.22.4": ">=4.52.2",
"sha.js@<=2.4.11": ">=2.4.12",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.0",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.0",
"tar-fs@>=3.0.0 <3.0.9": ">=3.1.1",
"tar-fs@>=2.0.0 <2.1.3": ">=3.1.1",
"tmp@<=0.2.3": ">=0.2.5",
"vite@>=5.0.0 <=5.4.18": ">=7.1.5"
"vite@>=5.0.0 <=5.4.18": ">=7.1.7"
},
"patchedDependencies": {
"@types/node-unifi": "patches/@types__node-unifi.patch",

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -42,7 +42,7 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@kubernetes/client-node": "^1.3.0",
"@tanstack/react-query": "^5.87.4",
"@tanstack/react-query": "^5.89.0",
"@trpc/client": "^11.5.1",
"@trpc/react-query": "^11.5.1",
"@trpc/server": "^11.5.1",
@@ -53,13 +53,13 @@
"react-dom": "19.1.1",
"superjson": "2.2.2",
"trpc-to-openapi": "^3.0.1",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}

View File

@@ -1,6 +1,11 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod/v4";
import type { Modify } from "@homarr/common/types";
import type { Integration } from "@homarr/db/schema";
import type { IntegrationKindByCategory } from "@homarr/definitions";
import { getIntegrationKindsByCategory } from "@homarr/definitions";
import type { CalendarEvent } from "@homarr/integrations/types";
import { radarrReleaseTypes } from "@homarr/integrations/types";
import { calendarMonthRequestHandler } from "@homarr/request-handler/calendar";
@@ -19,14 +24,56 @@ export const calendarRouter = createTRPCRouter({
)
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
.query(async ({ ctx, input }) => {
const results = await Promise.all(
return await Promise.all(
ctx.integrations.map(async (integration) => {
const innerHandler = calendarMonthRequestHandler.handler(integration, input);
const { integrationIds: _integrationIds, ...handlerInput } = input;
const innerHandler = calendarMonthRequestHandler.handler(integration, handlerInput);
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data;
return {
events: data,
integration: {
id: integration.id,
name: integration.name,
kind: integration.kind,
},
};
}),
);
return results.flat();
}),
subscribeToEvents: publicProcedure
.input(
z.object({
year: z.number(),
month: z.number(),
releaseType: z.array(z.enum(radarrReleaseTypes)),
showUnmonitored: z.boolean(),
}),
)
.concat(createManyIntegrationMiddleware("query", ...getIntegrationKindsByCategory("calendar")))
.subscription(({ ctx, input }) => {
return observable<{
integration: Modify<Integration, { kind: IntegrationKindByCategory<"calendar"> }>;
events: CalendarEvent[];
}>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const { integrationIds: _integrationIds, ...handlerInput } = input;
const innerHandler = calendarMonthRequestHandler.handler(integrationWithSecrets, handlerInput);
const unsubscribe = innerHandler.subscribe((events) => {
emit.next({
integration,
events,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
});

View File

@@ -1,6 +1,8 @@
import { observable } from "@trpc/server/observable";
import { z } from "zod/v4";
import { fetchWithTimeout } from "@homarr/common";
import type { Weather } from "@homarr/request-handler/weather";
import { weatherRequestHandler } from "@homarr/request-handler/weather";
import { createTRPCRouter, publicProcedure } from "../../trpc";
@@ -9,45 +11,19 @@ const atLocationInput = z.object({
latitude: z.number(),
});
const atLocationOutput = z.object({
current_weather: z.object({
weathercode: z.number(),
temperature: z.number(),
windspeed: z.number(),
}),
daily: z.object({
time: z.array(z.string()),
weathercode: z.array(z.number()),
temperature_2m_max: z.array(z.number()),
temperature_2m_min: z.array(z.number()),
sunrise: z.array(z.string()),
sunset: z.array(z.string()),
wind_speed_10m_max: z.array(z.number()),
wind_gusts_10m_max: z.array(z.number()),
}),
});
export const weatherRouter = createTRPCRouter({
atLocation: publicProcedure.input(atLocationInput).query(async ({ input }) => {
const res = await fetchWithTimeout(
`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`,
);
const json: unknown = await res.json();
const weather = await atLocationOutput.parseAsync(json);
return {
current: weather.current_weather,
daily: weather.daily.time.map((value, index) => {
return {
time: value,
weatherCode: weather.daily.weathercode[index] ?? 404,
maxTemp: weather.daily.temperature_2m_max[index],
minTemp: weather.daily.temperature_2m_min[index],
sunrise: weather.daily.sunrise[index],
sunset: weather.daily.sunset[index],
maxWindSpeed: weather.daily.wind_speed_10m_max[index],
maxWindGusts: weather.daily.wind_gusts_10m_max[index],
};
}),
};
const handler = weatherRequestHandler.handler(input);
return await handler.getCachedOrUpdatedDataAsync({ forceUpdate: false }).then((result) => result.data);
}),
subscribeAtLocation: publicProcedure.input(atLocationInput).subscription(({ input }) => {
return observable<Weather>((emit) => {
const handler = weatherRequestHandler.handler(input);
const unsubscribe = handler.subscribe((data) => {
emit.next(data);
});
return unsubscribe;
});
}),
});

View File

@@ -39,7 +39,7 @@
"next-auth": "5.0.0-beta.29",
"react": "19.1.1",
"react-dom": "19.1.1",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -47,7 +47,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"@types/cookies": "0.9.1",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"prettier": "^3.6.2",
"typescript": "^5.9.2"
}

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -34,8 +34,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"esbuild": "^0.25.9",
"eslint": "^9.35.0",
"esbuild": "^0.25.10",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -31,20 +31,20 @@
"@homarr/log": "workspace:^0.1.0",
"@paralleldrive/cuid2": "^2.2.2",
"dayjs": "^1.11.18",
"dns-caching": "^0.2.5",
"dns-caching": "^0.2.7",
"next": "15.5.3",
"octokit": "^5.0.3",
"react": "19.1.1",
"react-dom": "19.1.1",
"undici": "7.16.0",
"zod": "^4.1.8",
"zod-validation-error": "^4.0.1"
"zod": "^4.1.11",
"zod-validation-error": "^4.0.2"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -11,13 +11,17 @@ const calculateTimeAgo = (timestamp: Date) => {
};
export const useTimeAgo = (timestamp: Date, updateFrequency = 1000) => {
const [timeAgo, setTimeAgo] = useState(calculateTimeAgo(timestamp));
const [timeAgo, setTimeAgo] = useState(() => calculateTimeAgo(timestamp));
useEffect(() => {
setTimeAgo(calculateTimeAgo(timestamp));
}, [timestamp]);
useEffect(() => {
const intervalId = setInterval(() => setTimeAgo(calculateTimeAgo(timestamp)), updateFrequency);
return () => clearInterval(intervalId); // clear interval on hook unmount
}, [timestamp]);
}, [timestamp, updateFrequency]);
return timeAgo;
};

View File

@@ -26,13 +26,13 @@
"dependencies": {
"@t3-oss/env-nextjs": "^0.13.8",
"ioredis": "5.7.0",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -29,13 +29,13 @@
"@homarr/core": "workspace:^0.1.0",
"@homarr/cron-jobs": "workspace:^0.1.0",
"@homarr/log": "workspace:^0.1.0",
"@tanstack/react-query": "^5.87.4",
"@tanstack/react-query": "^5.89.0",
"@trpc/client": "^11.5.1",
"@trpc/server": "^11.5.1",
"@trpc/tanstack-react-query": "^11.5.1",
"node-cron": "^4.2.1",
"react": "19.1.1",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -43,7 +43,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"@types/react": "19.1.13",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-cron": "^3.0.11",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -44,7 +44,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -22,6 +22,7 @@ import { minecraftServerStatusJob } from "./jobs/minecraft-server-status";
import { pingJob } from "./jobs/ping";
import { rssFeedsJob } from "./jobs/rss-feeds";
import { updateCheckerJob } from "./jobs/update-checker";
import { weatherJob } from "./jobs/weather";
import { createCronJobGroup } from "./lib";
export const jobGroup = createCronJobGroup({
@@ -48,6 +49,7 @@ export const jobGroup = createCronJobGroup({
firewallVersion: firewallVersionJob,
firewallInterfaces: firewallInterfacesJob,
refreshNotifications: refreshNotificationsJob,
weather: weatherJob,
});
export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];

View File

@@ -0,0 +1,33 @@
import SuperJSON from "superjson";
import { EVERY_10_MINUTES } from "@homarr/cron-jobs-core/expressions";
import { db, eq } from "@homarr/db";
import { items } from "@homarr/db/schema";
import { logger } from "@homarr/log";
import { weatherRequestHandler } from "@homarr/request-handler/weather";
import type { WidgetComponentProps } from "../../../widgets";
import { createCronJob } from "../lib";
export const weatherJob = createCronJob("weather", EVERY_10_MINUTES).withCallback(async () => {
const weatherItems = await db.query.items.findMany({
where: eq(items.kind, "weather"),
});
const parsedItems = weatherItems.map((item) => ({
id: item.id,
options: SuperJSON.parse<WidgetComponentProps<"weather">["options"]>(item.options),
}));
for (const item of parsedItems) {
try {
const innerHandler = weatherRequestHandler.handler({
longitude: item.options.location.longitude,
latitude: item.options.location.latitude,
});
await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: true });
} catch (error) {
logger.error("Failed to update weather", { id: item.id, error });
}
}
});

View File

@@ -51,14 +51,14 @@
"@homarr/server-settings": "workspace:^0.1.0",
"@mantine/core": "^8.3.1",
"@paralleldrive/cuid2": "^2.2.2",
"@testcontainers/mysql": "^11.5.1",
"@testcontainers/postgresql": "^11.5.1",
"@testcontainers/mysql": "^11.6.0",
"@testcontainers/postgresql": "^11.6.0",
"better-sqlite3": "^12.2.0",
"dotenv": "^17.2.2",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "^0.44.5",
"drizzle-zod": "^0.8.3",
"mysql2": "3.14.5",
"mysql2": "3.15.0",
"pg": "^8.16.3",
"superjson": "2.2.2"
},
@@ -69,8 +69,8 @@
"@types/better-sqlite3": "7.6.13",
"@types/pg": "^8.15.5",
"dotenv-cli": "^10.0.0",
"esbuild": "^0.25.9",
"eslint": "^9.35.0",
"esbuild": "^0.25.10",
"eslint": "^9.36.0",
"prettier": "^3.6.2",
"tsx": "4.20.4",
"typescript": "^5.9.2"

View File

@@ -25,13 +25,13 @@
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"fast-xml-parser": "^5.2.5",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"tsx": "4.20.4",
"typescript": "^5.9.2"
}

View File

@@ -87,6 +87,7 @@ export type HomarrDocumentationPath =
| "/docs/tags/programming"
| "/docs/tags/proxy"
| "/docs/tags/puid"
| "/docs/tags/redis"
| "/docs/tags/responsive"
| "/docs/tags/roles"
| "/docs/tags/search"
@@ -159,6 +160,7 @@ export type HomarrDocumentationPath =
| "/docs/integrations/github"
| "/docs/integrations/gitlab"
| "/docs/integrations/home-assistant"
| "/docs/integrations/ical"
| "/docs/integrations/jellyfin"
| "/docs/integrations/jellyseerr"
| "/docs/integrations/kubernetes"

View File

@@ -161,7 +161,7 @@ export const integrationDefs = {
name: "Home Assistant",
secretKinds: [["apiKey"]],
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
category: ["smartHomeServer"],
category: ["smartHomeServer", "calendar"],
documentationUrl: createDocumentationLink("/docs/integrations/home-assistant"),
},
openmediavault: {

View File

@@ -32,8 +32,8 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/dockerode": "^3.3.43",
"eslint": "^9.35.0",
"@types/dockerode": "^3.3.44",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -28,13 +28,13 @@
"@homarr/validation": "workspace:^0.1.0",
"@mantine/form": "^8.3.1",
"mantine-form-zod-resolver": "^1.3.0",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -31,13 +31,13 @@
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.1",
"react": "19.1.1",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -31,7 +31,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -33,7 +33,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/bcrypt": "6.0.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -25,9 +25,9 @@
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@ctrl/deluge": "^7.2.0",
"@ctrl/qbittorrent": "^9.7.0",
"@ctrl/transmission": "^7.3.0",
"@ctrl/deluge": "^7.3.0",
"@ctrl/qbittorrent": "^9.8.0",
"@ctrl/transmission": "^7.4.0",
"@gitbeaker/rest": "^43.5.0",
"@homarr/certificates": "workspace:^0.1.0",
"@homarr/common": "workspace:^0.1.0",
@@ -43,13 +43,13 @@
"@octokit/auth-app": "^8.1.0",
"ical.js": "^2.2.1",
"maria2": "^0.4.1",
"node-ical": "^0.20.1",
"node-ical": "^0.21.0",
"octokit": "^5.0.3",
"proxmox-api": "1.1.1",
"tsdav": "^2.1.5",
"undici": "7.16.0",
"xml2js": "^0.6.2",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
@@ -57,7 +57,7 @@
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/node-unifi": "^2.5.1",
"@types/xml2js": "^0.4.14",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -1,14 +1,19 @@
import z from "zod";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server";
import { logger } from "@homarr/log";
import type { IntegrationTestingInput } from "../base/integration";
import { Integration } from "../base/integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration";
import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration";
import { entityStateSchema } from "./homeassistant-types";
import type { CalendarEvent } from "../types";
import { calendarEventSchema, calendarsSchema, entityStateSchema } from "./homeassistant-types";
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration {
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration, ICalendarIntegration {
public async getEntityStateAsync(entityId: string) {
try {
const response = await this.getAsync(`/api/states/${entityId}`);
@@ -62,6 +67,35 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
}
}
public async getCalendarEventsAsync(start: Date, end: Date): Promise<CalendarEvent[]> {
const calendarsResponse = await this.getAsync("/api/calendars");
if (!calendarsResponse.ok) throw new ResponseError(calendarsResponse);
const calendars = await calendarsSchema.parseAsync(await calendarsResponse.json());
return await Promise.all(
calendars.map(async (calendar) => {
const response = await this.getAsync(`/api/calendars/${calendar.entity_id}`, { start, end });
if (!response.ok) throw new ResponseError(response);
return await z.array(calendarEventSchema).parseAsync(await response.json());
}),
).then((events) =>
events.flat().map(
(event): CalendarEvent => ({
title: event.summary,
subTitle: null,
description: event.description,
// If not reseting it to 0 o'clock it uses utc time and therefore shows as 2 o'clock
startDate: "date" in event.start ? new Date(`${event.start.date}T00:00:00`) : new Date(event.start.dateTime),
endDate: "date" in event.end ? new Date(`${event.end.date}T00:00:00`) : new Date(event.end.dateTime),
image: null,
indicatorColor: "#18bcf2",
links: [],
location: event.location,
}),
),
);
}
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
const response = await input.fetchAsync(this.url("/api/config"), {
headers: this.getAuthHeaders(),
@@ -82,8 +116,8 @@ export class HomeAssistantIntegration extends Integration implements ISmartHomeI
* @param path full path to the API endpoint
* @returns the response from the API
*/
private async getAsync(path: `/api/${string}`) {
return await fetchWithTrustedCertificatesAsync(this.url(path), {
private async getAsync(path: `/api/${string}`, queryParams?: Record<string, string | Date | number | boolean>) {
return await fetchWithTrustedCertificatesAsync(this.url(path, queryParams), {
headers: this.getAuthHeaders(),
});
}

View File

@@ -12,3 +12,27 @@ export const entityStateSchema = z.object({
});
export type EntityState = z.infer<typeof entityStateSchema>;
export const calendarsSchema = z.array(
z.object({
name: z.string(),
entity_id: z.string(),
}),
);
const calendarMomentSchema = z
.object({
date: z.string(),
})
.or(
z.object({
dateTime: z.string(),
}),
);
export const calendarEventSchema = z.object({
start: calendarMomentSchema,
end: calendarMomentSchema,
summary: z.string(),
description: z.string().nullable(),
location: z.string().nullable(),
});

View File

@@ -47,6 +47,10 @@ export class SonarrIntegration extends Integration implements ICalendarIntegrati
? {
src: imageSrc,
aspectRatio: { width: 7, height: 12 },
badge: {
color: "red",
content: `S${event.seasonNumber}/E${event.episodeNumber}`,
},
}
: null,
location: null,

View File

@@ -69,7 +69,7 @@ const seriesRelease = (start: Date, end: Date): CalendarEvent => ({
src: "https://image.tmdb.org/t/p/original/sWgBv7LV2PRoQgkxwlibdGXKz1S.jpg",
aspectRatio: { width: 7, height: 12 },
badge: {
content: "S1:E1",
content: "S1/E1",
color: "red",
},
},

View File

@@ -27,13 +27,13 @@
"@homarr/core": "workspace:^0.1.0",
"superjson": "2.2.2",
"winston": "3.17.0",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -34,18 +34,18 @@
"@homarr/ui": "workspace:^0.1.0",
"@homarr/validation": "workspace:^0.1.0",
"@mantine/core": "^8.3.1",
"@tabler/icons-react": "^3.34.1",
"@tabler/icons-react": "^3.35.0",
"dayjs": "^1.11.18",
"next": "15.5.3",
"react": "19.1.1",
"react-dom": "19.1.1",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -32,7 +32,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -25,13 +25,13 @@
"dependencies": {
"@homarr/ui": "workspace:^0.1.0",
"@mantine/notifications": "^8.3.1",
"@tabler/icons-react": "^3.34.1"
"@tabler/icons-react": "^3.35.0"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -44,7 +44,7 @@
"react": "19.1.1",
"react-dom": "19.1.1",
"superjson": "2.2.2",
"zod": "^4.1.8",
"zod": "^4.1.11",
"zod-form-data": "^3.0.1"
},
"devDependencies": {
@@ -52,7 +52,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/adm-zip": "0.5.7",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -23,13 +23,13 @@
"prettier": "@homarr/prettier-config",
"dependencies": {
"@homarr/common": "workspace:^0.1.0",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -30,7 +30,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -34,7 +34,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -32,13 +32,14 @@
"dayjs": "^1.11.18",
"octokit": "^5.0.3",
"superjson": "2.2.2",
"undici": "7.16.0"
"undici": "7.16.0",
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -0,0 +1,70 @@
import dayjs from "dayjs";
import { z } from "zod";
import { fetchWithTimeout } from "@homarr/common";
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
export const weatherRequestHandler = createCachedWidgetRequestHandler({
queryKey: "weatherAtLocation",
widgetKind: "weather",
async requestAsync(input: { latitude: number; longitude: number }) {
const res = await fetchWithTimeout(
`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`,
);
const json: unknown = await res.json();
const weather = await atLocationOutput.parseAsync(json);
return {
current: weather.current_weather,
daily: weather.daily.time.map((value, index) => {
return {
time: value,
weatherCode: weather.daily.weathercode[index] ?? 404,
maxTemp: weather.daily.temperature_2m_max[index],
minTemp: weather.daily.temperature_2m_min[index],
sunrise: weather.daily.sunrise[index],
sunset: weather.daily.sunset[index],
maxWindSpeed: weather.daily.wind_speed_10m_max[index],
maxWindGusts: weather.daily.wind_gusts_10m_max[index],
};
}),
} satisfies Weather;
},
cacheDuration: dayjs.duration(1, "minute"),
});
const atLocationOutput = z.object({
current_weather: z.object({
weathercode: z.number(),
temperature: z.number(),
windspeed: z.number(),
}),
daily: z.object({
time: z.array(z.string()),
weathercode: z.array(z.number()),
temperature_2m_max: z.array(z.number()),
temperature_2m_min: z.array(z.number()),
sunrise: z.array(z.string()),
sunset: z.array(z.string()),
wind_speed_10m_max: z.array(z.number()),
wind_gusts_10m_max: z.array(z.number()),
}),
});
export interface Weather {
current: {
weathercode: number;
temperature: number;
windspeed: number;
};
daily: {
time: string;
weatherCode: number;
maxTemp: number | undefined;
minTemp: number | undefined;
sunrise: string | undefined;
sunset: string | undefined;
maxWindSpeed: number | undefined;
maxWindGusts: number | undefined;
}[];
}

View File

@@ -29,7 +29,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -35,7 +35,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -36,7 +36,7 @@
"@mantine/core": "^8.3.1",
"@mantine/hooks": "^8.3.1",
"@mantine/spotlight": "^8.3.1",
"@tabler/icons-react": "^3.34.1",
"@tabler/icons-react": "^3.35.0",
"jotai": "^2.14.0",
"next": "15.5.3",
"react": "19.1.1",
@@ -47,7 +47,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -33,7 +33,7 @@
"deepmerge": "4.3.1",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.5.3",
"next-intl": "4.3.8",
"next-intl": "4.3.9",
"react": "19.1.1",
"react-dom": "19.1.1"
},
@@ -41,7 +41,7 @@
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -1707,6 +1707,9 @@
"calendar": {
"name": "Calendar",
"description": "Display events from your integrations in a calendar view within a certain relative time period",
"duration": {
"allDay": "All day"
},
"option": {
"releaseType": {
"label": "Radarr release type",
@@ -3315,6 +3318,9 @@
},
"firewallInterfaces": {
"label": "Firewall Interfaces"
},
"weather": {
"label": "Weather"
}
},
"interval": {

View File

@@ -33,7 +33,7 @@
"@mantine/core": "^8.3.1",
"@mantine/dates": "^8.3.1",
"@mantine/hooks": "^8.3.1",
"@tabler/icons-react": "^3.34.1",
"@tabler/icons-react": "^3.35.0",
"mantine-react-table": "2.0.0-beta.9",
"next": "15.5.3",
"react": "19.1.1",
@@ -45,7 +45,7 @@
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/css-modules": "^1.0.5",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -24,14 +24,14 @@
"dependencies": {
"@homarr/definitions": "workspace:^0.1.0",
"@homarr/translation": "workspace:^0.1.0",
"zod": "^4.1.8",
"zod": "^4.1.11",
"zod-form-data": "^3.0.1"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -51,7 +51,7 @@
"@mantine/charts": "^8.3.1",
"@mantine/core": "^8.3.1",
"@mantine/hooks": "^8.3.1",
"@tabler/icons-react": "^3.34.1",
"@tabler/icons-react": "^3.35.0",
"@tiptap/extension-color": "2.26.1",
"@tiptap/extension-highlight": "2.26.1",
"@tiptap/extension-image": "2.26.1",
@@ -78,14 +78,14 @@
"react-markdown": "^10.1.0",
"recharts": "^2.15.4",
"video.js": "^8.23.4",
"zod": "^4.1.8"
"zod": "^4.1.11"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "^7.3.58",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}

View File

@@ -84,16 +84,24 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
<Group gap={3} wrap="nowrap" align={"center"}>
<IconClock opacity={0.7} size={"1rem"} />
<Text c={"dimmed"} size={"sm"}>
{dayjs(event.startDate).format("HH:mm")}
</Text>
{event.endDate !== null && (
{isAllDay(event) ? (
<Text c={"dimmed"} size={"sm"}>
{t("widget.calendar.duration.allDay")}
</Text>
) : (
<>
-{" "}
<Text c={"dimmed"} size={"sm"}>
{dayjs(event.endDate).format("HH:mm")}
{dayjs(event.startDate).format("HH:mm")}
</Text>
{event.endDate !== null && (
<>
-{" "}
<Text c={"dimmed"} size={"sm"}>
{dayjs(event.endDate).format("HH:mm")}
</Text>
</>
)}
</>
)}
</Group>
@@ -152,3 +160,12 @@ export const CalendarEventList = ({ events }: CalendarEventListProps) => {
</ScrollArea>
);
};
const isAllDay = (event: Pick<CalendarEvent, "startDate" | "endDate">) => {
if (!event.endDate) return false;
const start = dayjs(event.startDate);
const end = dayjs(event.endDate);
return start.startOf("day").isSame(start) && end.endOf("day").isSame(end);
};

View File

@@ -0,0 +1,66 @@
import { describe, expect, test } from "vitest";
import type { CalendarEvent } from "@homarr/integrations/types";
import { splitEvents } from "./component";
describe("splitEvents should split multi-day events into multiple single-day events", () => {
test("2 day all-day event should be split up into two all-day events", () => {
const event = createEvent(new Date(2025, 0, 1), new Date(2025, 0, 3));
const result = splitEvents([event]);
expect(result).toHaveLength(2);
expect(result[0]?.startDate).toEqual(event.startDate);
expect(result[0]?.endDate).toEqual(new Date(new Date(2025, 0, 2).getTime() - 1));
expect(result[1]?.startDate).toEqual(new Date(2025, 0, 2));
// Because we want to end the event on the previous day, we have not the same endDate.
// Otherwise there would be three single-day events, with the last being from 0:00 - 0:00
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(result[1]?.endDate).toEqual(new Date(event.endDate!.getTime() - 1));
});
test("2 day partial event should be split up into two events", () => {
const event = createEvent(new Date(2025, 0, 1, 15), new Date(2025, 0, 2, 9));
const result = splitEvents([event]);
expect(result).toHaveLength(2);
expect(result[0]?.startDate).toEqual(event.startDate);
expect(result[0]?.endDate).toEqual(new Date(new Date(2025, 0, 2).getTime() - 1));
expect(result[1]?.startDate).toEqual(new Date(2025, 0, 2));
expect(result[1]?.endDate).toEqual(event.endDate);
});
test("one day partial event should only have one event after split", () => {
const event = createEvent(new Date(2025, 0, 1), new Date(2025, 0, 2));
const result = splitEvents([event]);
expect(result).toHaveLength(1);
});
test("without endDate should not be split", () => {
const event = createEvent(new Date(2025, 0, 1));
const result = splitEvents([event]);
expect(result).toHaveLength(1);
});
test("startDate after endDate should not cause infinite loop", () => {
const event = createEvent(new Date(2025, 0, 2), new Date(2025, 0, 1));
const result = splitEvents([event]);
expect(result).toHaveLength(0);
});
});
const createEvent = (startDate: Date, endDate: Date | null = null): CalendarEvent => ({
title: "Test",
subTitle: null,
description: null,
startDate,
endDate,
image: null,
indicatorColor: "red",
links: [],
location: null,
});

View File

@@ -1,15 +1,15 @@
"use client";
import { useState } from "react";
import { useMemo, useState } from "react";
import { useParams } from "next/navigation";
import { useMantineTheme } from "@mantine/core";
import { Calendar } from "@mantine/dates";
import { useElementSize } from "@mantine/hooks";
import dayjs from "dayjs";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { useRequiredBoard } from "@homarr/boards/context";
import type { CalendarEvent } from "@homarr/integrations/types";
import { useSettings } from "@homarr/settings";
import type { WidgetComponentProps } from "../definition";
@@ -32,28 +32,43 @@ interface FetchCalendarProps extends WidgetComponentProps<"calendar"> {
}
const FetchCalendar = ({ month, setMonth, isEditMode, integrationIds, options }: FetchCalendarProps) => {
const [events] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(
{
integrationIds,
month: month.getMonth(),
year: month.getFullYear(),
releaseType: options.releaseType,
showUnmonitored: options.showUnmonitored,
const input = {
integrationIds,
month: month.getMonth(),
year: month.getFullYear(),
releaseType: options.releaseType,
showUnmonitored: options.showUnmonitored,
};
const [data] = clientApi.widget.calendar.findAllEvents.useSuspenseQuery(input, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
});
const utils = clientApi.useUtils();
clientApi.widget.calendar.subscribeToEvents.useSubscription(input, {
onData(data) {
utils.widget.calendar.findAllEvents.setData(input, (old) => {
return old?.map((item) => {
if (item.integration.id !== data.integration.id) return item;
return {
...item,
events: data.events,
};
});
});
},
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
retry: false,
},
);
});
const events = useMemo(() => data.flatMap((item) => item.events), [data]);
return <CalendarBase isEditMode={isEditMode} events={events} month={month} setMonth={setMonth} options={options} />;
};
interface CalendarBaseProps {
isEditMode: boolean;
events: RouterOutputs["widget"]["calendar"]["findAllEvents"];
events: CalendarEvent[];
month: Date;
setMonth: (date: Date) => void;
options: WidgetComponentProps<"calendar">["options"];
@@ -69,6 +84,8 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
const { ref, width, height } = useElementSize();
const isSmall = width < 256;
const normalizedEvents = useMemo(() => splitEvents(events), [events]);
return (
<Calendar
defaultDate={new Date()}
@@ -122,7 +139,7 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
},
}}
renderDay={(tileDate) => {
const eventsForDate = events
const eventsForDate = normalizedEvents
.filter((event) => dayjs(event.startDate).isSame(tileDate, "day"))
.filter(
(event) => event.metadata?.type !== "radarr" || options.releaseType.includes(event.metadata.releaseType),
@@ -145,3 +162,42 @@ const CalendarBase = ({ isEditMode, events, month, setMonth, options }: Calendar
/>
);
};
/**
* Splits multi-day events into multiple single-day events.
* @param events The events to split.
* @returns The split events.
*/
export const splitEvents = (events: CalendarEvent[]): CalendarEvent[] => {
const splitEvents: CalendarEvent[] = [];
for (const event of events) {
if (!event.endDate) {
splitEvents.push(event);
continue;
}
if (dayjs(event.startDate).isSame(event.endDate, "day")) {
splitEvents.push(event);
continue;
}
if (dayjs(event.startDate).isAfter(event.endDate)) {
// Invalid event, skip it
continue;
}
// Event spans multiple days, split it
let currentStart = dayjs(event.startDate);
while (currentStart.isBefore(event.endDate)) {
splitEvents.push({
...event,
startDate: currentStart.toDate(),
endDate: currentStart.endOf("day").isAfter(event.endDate) ? event.endDate : currentStart.endOf("day").toDate(),
});
currentStart = currentStart.add(1, "day").startOf("day");
}
}
return splitEvents;
};

View File

@@ -13,17 +13,20 @@ import type { WidgetComponentProps } from "../definition";
import { WeatherDescription, WeatherIcon } from "./icon";
export default function WeatherWidget({ isEditMode, options }: WidgetComponentProps<"weather">) {
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(
{
latitude: options.location.latitude,
longitude: options.location.longitude,
},
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
},
);
const input = {
latitude: options.location.latitude,
longitude: options.location.longitude,
};
const [weather] = clientApi.widget.weather.atLocation.useSuspenseQuery(input, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
const utils = clientApi.useUtils();
clientApi.widget.weather.subscribeAtLocation.useSubscription(input, {
onData: (data) => utils.widget.weather.atLocation.setData(input, data),
});
return (
<Stack

1450
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -24,12 +24,12 @@
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"typescript-eslint": "^8.43.0"
"typescript-eslint": "^8.44.0"
},
"devDependencies": {
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"eslint": "^9.35.0",
"eslint": "^9.36.0",
"typescript": "^5.9.2"
}
}