mirror of
https://github.com/karl0ss/homepage.git
synced 2025-04-29 12:03:41 +01:00
Feature: calendar widget (#2077)
* Implemented calendar Signed-off-by: Denis Papec <denis.papec@gmail.com> * Added lidarr events to calendar Signed-off-by: Denis Papec <denis.papec@gmail.com> * Added radarr events to calendar Signed-off-by: Denis Papec <denis.papec@gmail.com> * Added readarr events to calendar Signed-off-by: Denis Papec <denis.papec@gmail.com> * Added sonarr events to calendar Signed-off-by: Denis Papec <denis.papec@gmail.com> * fix sonarr series title * integrations * fix bad setstate call * handle user sets includeSeries: false for sonarr * Translate radarr release strings * Support all widths * readarr get author * Finished first day in week config Signed-off-by: Denis Papec <denis.papec@gmail.com> --------- Signed-off-by: Denis Papec <denis.papec@gmail.com> Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
parent
855f12e4c1
commit
4cd4103edf
9
package-lock.json
generated
9
package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"i18next": "^21.9.2",
|
"i18next": "^21.9.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-rpc-2.0": "^1.4.1",
|
"json-rpc-2.0": "^1.4.1",
|
||||||
|
"luxon": "^3.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"minecraft-ping-js": "^1.0.2",
|
"minecraft-ping-js": "^1.0.2",
|
||||||
"next": "^12.3.1",
|
"next": "^12.3.1",
|
||||||
@ -4098,6 +4099,14 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/luxon": {
|
||||||
|
"version": "3.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz",
|
||||||
|
"integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/memory-cache": {
|
"node_modules/memory-cache": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
"i18next": "^21.9.2",
|
"i18next": "^21.9.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json-rpc-2.0": "^1.4.1",
|
"json-rpc-2.0": "^1.4.1",
|
||||||
|
"luxon": "^3.4.3",
|
||||||
"memory-cache": "^0.2.0",
|
"memory-cache": "^0.2.0",
|
||||||
"minecraft-ping-js": "^1.0.2",
|
"minecraft-ping-js": "^1.0.2",
|
||||||
"next": "^12.3.1",
|
"next": "^12.3.1",
|
||||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -35,6 +35,9 @@ dependencies:
|
|||||||
json-rpc-2.0:
|
json-rpc-2.0:
|
||||||
specifier: ^1.4.1
|
specifier: ^1.4.1
|
||||||
version: 1.5.1
|
version: 1.5.1
|
||||||
|
luxon:
|
||||||
|
specifier: ^3.4.3
|
||||||
|
version: 3.4.3
|
||||||
memory-cache:
|
memory-cache:
|
||||||
specifier: ^0.2.0
|
specifier: ^0.2.0
|
||||||
version: 0.2.0
|
version: 0.2.0
|
||||||
@ -2646,6 +2649,11 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
||||||
|
/luxon@3.4.3:
|
||||||
|
resolution: {integrity: sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/memory-cache@0.2.0:
|
/memory-cache@0.2.0:
|
||||||
resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==}
|
resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -748,5 +748,10 @@
|
|||||||
"seemsdown": "Seems Down",
|
"seemsdown": "Seems Down",
|
||||||
"down": "Down",
|
"down": "Down",
|
||||||
"unknown": "Unknown"
|
"unknown": "Unknown"
|
||||||
|
},
|
||||||
|
"calendar": {
|
||||||
|
"inCinemas": "In cinemas",
|
||||||
|
"physicalRelease": "Physical release",
|
||||||
|
"digitalRelease": "Digital release"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import { ColorProvider } from "utils/contexts/color";
|
|||||||
import { ThemeProvider } from "utils/contexts/theme";
|
import { ThemeProvider } from "utils/contexts/theme";
|
||||||
import { SettingsProvider } from "utils/contexts/settings";
|
import { SettingsProvider } from "utils/contexts/settings";
|
||||||
import { TabProvider } from "utils/contexts/tab";
|
import { TabProvider } from "utils/contexts/tab";
|
||||||
|
import { EventProvider, ShowDateProvider } from "utils/contexts/calendar";
|
||||||
|
|
||||||
function MyApp({ Component, pageProps }) {
|
function MyApp({ Component, pageProps }) {
|
||||||
return (
|
return (
|
||||||
@ -28,7 +29,11 @@ function MyApp({ Component, pageProps }) {
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<TabProvider>
|
<TabProvider>
|
||||||
|
<EventProvider>
|
||||||
|
<ShowDateProvider>
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
|
</ShowDateProvider>
|
||||||
|
</EventProvider>
|
||||||
</TabProvider>
|
</TabProvider>
|
||||||
</SettingsProvider>
|
</SettingsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
@ -357,6 +357,7 @@ export function cleanServiceGroups(groups) {
|
|||||||
method, // openmediavault widget
|
method, // openmediavault widget
|
||||||
mappings, // customapi widget
|
mappings, // customapi widget
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
|
integrations, // calendar widget
|
||||||
} = cleanedService.widget;
|
} = cleanedService.widget;
|
||||||
|
|
||||||
let fieldsList = fields;
|
let fieldsList = fields;
|
||||||
@ -440,6 +441,9 @@ export function cleanServiceGroups(groups) {
|
|||||||
if (mappings) cleanedService.widget.mappings = mappings;
|
if (mappings) cleanedService.widget.mappings = mappings;
|
||||||
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
|
if (refreshInterval) cleanedService.widget.refreshInterval = refreshInterval;
|
||||||
}
|
}
|
||||||
|
if (type === "calendar") {
|
||||||
|
if (integrations) cleanedService.widget.integrations = integrations;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleanedService;
|
return cleanedService;
|
||||||
|
28
src/utils/contexts/calendar.jsx
Normal file
28
src/utils/contexts/calendar.jsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { createContext, useState, useMemo } from "react";
|
||||||
|
|
||||||
|
export const EventContext = createContext();
|
||||||
|
export const ShowDateContext = createContext();
|
||||||
|
|
||||||
|
export function EventProvider({ initialEvent, children }) {
|
||||||
|
const [events, setEvents] = useState({});
|
||||||
|
|
||||||
|
if (initialEvent) {
|
||||||
|
setEvents(initialEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ events, setEvents }), [events]);
|
||||||
|
|
||||||
|
return <EventContext.Provider value={value}>{children}</EventContext.Provider>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShowDateProvider({ initialDate, children }) {
|
||||||
|
const [showDate, setShowDate] = useState(null);
|
||||||
|
|
||||||
|
if (initialDate) {
|
||||||
|
setShowDate(initialDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = useMemo(() => ({ showDate, setShowDate }), [showDate]);
|
||||||
|
|
||||||
|
return <ShowDateContext.Provider value={value}>{children}</ShowDateContext.Provider>;
|
||||||
|
}
|
@ -18,7 +18,8 @@ export default async function genericProxyHandler(req, res, map) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (widget) {
|
if (widget) {
|
||||||
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }));
|
// if there are more than one question marks, replace others to &
|
||||||
|
const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget }).replace(/(?<=\?.*)\?/g, '&'));
|
||||||
|
|
||||||
const headers = req.extraHeaders ?? widget.headers ?? {};
|
const headers = req.extraHeaders ?? widget.headers ?? {};
|
||||||
|
|
||||||
|
47
src/widgets/calendar/component.jsx
Normal file
47
src/widgets/calendar/component.jsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
import { ShowDateContext } from "../../utils/contexts/calendar";
|
||||||
|
|
||||||
|
import MonthlyView from "./monthly-view";
|
||||||
|
|
||||||
|
import Container from "components/services/widget/container";
|
||||||
|
|
||||||
|
export default function Component({ service }) {
|
||||||
|
const { widget } = service;
|
||||||
|
const { showDate } = useContext(ShowDateContext);
|
||||||
|
|
||||||
|
// params for API fetch
|
||||||
|
const params = useMemo(() => {
|
||||||
|
if (!showDate) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
start: showDate.minus({months: 3}).toFormat("yyyy-MM-dd"),
|
||||||
|
end: showDate.plus({months: 3}).toFormat("yyyy-MM-dd"),
|
||||||
|
unmonitored: 'false',
|
||||||
|
};
|
||||||
|
}, [showDate]);
|
||||||
|
|
||||||
|
// Load active integrations
|
||||||
|
const integrations = useMemo(() => widget.integrations?.map(integration => ({
|
||||||
|
service: dynamic(() => import(`./integrations/${integration?.type}`)),
|
||||||
|
widget: integration,
|
||||||
|
})) ?? [], [widget.integrations]);
|
||||||
|
|
||||||
|
return <Container service={service}>
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="sticky top-0">
|
||||||
|
{integrations.map(integration => {
|
||||||
|
const Integration = integration.service;
|
||||||
|
const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group;
|
||||||
|
|
||||||
|
return <Integration key={key} config={integration.widget} params={params}
|
||||||
|
className="fixed bottom-0 left-0 bg-red-500 w-screen h-12" />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<MonthlyView service={service} className="flex"/>
|
||||||
|
</div>
|
||||||
|
</Container>;
|
||||||
|
}
|
36
src/widgets/calendar/integrations/lidarr.jsx
Normal file
36
src/widgets/calendar/integrations/lidarr.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { useContext, useEffect } from "react";
|
||||||
|
|
||||||
|
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||||
|
import { EventContext } from "../../../utils/contexts/calendar";
|
||||||
|
import Error from "../../../components/services/widget/error";
|
||||||
|
|
||||||
|
export default function Integration({ config, params }) {
|
||||||
|
const { setEvents } = useContext(EventContext);
|
||||||
|
const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar",
|
||||||
|
{ ...params, includeArtist: 'false', ...config?.params ?? {} }
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lidarrData || lidarrError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsToAdd = {};
|
||||||
|
|
||||||
|
lidarrData?.forEach(event => {
|
||||||
|
const title = `${event.artist.artistName} - ${event.title}`;
|
||||||
|
|
||||||
|
eventsToAdd[title] = {
|
||||||
|
title,
|
||||||
|
date: DateTime.fromISO(event.releaseDate),
|
||||||
|
color: config?.color ?? 'green'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||||
|
}, [lidarrData, lidarrError, config, setEvents]);
|
||||||
|
|
||||||
|
const error = lidarrError ?? lidarrData?.error;
|
||||||
|
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||||
|
}
|
49
src/widgets/calendar/integrations/radarr.jsx
Normal file
49
src/widgets/calendar/integrations/radarr.jsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { useEffect, useContext } from "react";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||||
|
import { EventContext } from "../../../utils/contexts/calendar";
|
||||||
|
import Error from "../../../components/services/widget/error";
|
||||||
|
|
||||||
|
export default function Integration({ config, params }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setEvents } = useContext(EventContext);
|
||||||
|
const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar",
|
||||||
|
{ ...params, ...config?.params ?? {} }
|
||||||
|
);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!radarrData || radarrError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsToAdd = {};
|
||||||
|
|
||||||
|
radarrData?.forEach(event => {
|
||||||
|
const cinemaTitle = `${event.title} - ${t("calendar.inCinemas")}`;
|
||||||
|
const physicalTitle = `${event.title} - ${t("calendar.physicalRelease")}`;
|
||||||
|
const digitalTitle = `${event.title} - ${t("calendar.digitalRelease")}`;
|
||||||
|
|
||||||
|
eventsToAdd[cinemaTitle] = {
|
||||||
|
title: cinemaTitle,
|
||||||
|
date: DateTime.fromISO(event.inCinemas),
|
||||||
|
color: config?.color ?? 'amber'
|
||||||
|
};
|
||||||
|
eventsToAdd[physicalTitle] = {
|
||||||
|
title: physicalTitle,
|
||||||
|
date: DateTime.fromISO(event.physicalRelease),
|
||||||
|
color: config?.color ?? 'cyan'
|
||||||
|
};
|
||||||
|
eventsToAdd[digitalTitle] = {
|
||||||
|
title: digitalTitle,
|
||||||
|
date: DateTime.fromISO(event.digitalRelease),
|
||||||
|
color: config?.color ?? 'emerald'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||||
|
}, [radarrData, radarrError, config, setEvents, t]);
|
||||||
|
|
||||||
|
const error = radarrError ?? radarrData?.error;
|
||||||
|
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||||
|
}
|
37
src/widgets/calendar/integrations/readarr.jsx
Normal file
37
src/widgets/calendar/integrations/readarr.jsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { useEffect, useContext } from "react";
|
||||||
|
|
||||||
|
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||||
|
import { EventContext } from "../../../utils/contexts/calendar";
|
||||||
|
import Error from "../../../components/services/widget/error";
|
||||||
|
|
||||||
|
export default function Integration({ config, params }) {
|
||||||
|
const { setEvents } = useContext(EventContext);
|
||||||
|
const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar",
|
||||||
|
{ ...params, includeAuthor: 'true', ...config?.params ?? {} },
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!readarrData || readarrError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsToAdd = {};
|
||||||
|
|
||||||
|
readarrData?.forEach(event => {
|
||||||
|
const authorName = event.author?.authorName ?? event.authorTitle.replace(event.title, '');
|
||||||
|
const title = `${authorName} - ${event.title} ${event?.seriesTitle ? `(${event.seriesTitle})` : ''} `;
|
||||||
|
|
||||||
|
eventsToAdd[title] = {
|
||||||
|
title,
|
||||||
|
date: DateTime.fromISO(event.releaseDate),
|
||||||
|
color: config?.color ?? 'rose'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||||
|
}, [readarrData, readarrError, config, setEvents]);
|
||||||
|
|
||||||
|
const error = readarrError ?? readarrData?.error;
|
||||||
|
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||||
|
}
|
36
src/widgets/calendar/integrations/sonarr.jsx
Normal file
36
src/widgets/calendar/integrations/sonarr.jsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { DateTime } from "luxon";
|
||||||
|
import { useEffect, useContext } from "react";
|
||||||
|
|
||||||
|
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
|
||||||
|
import { EventContext } from "../../../utils/contexts/calendar";
|
||||||
|
import Error from "../../../components/services/widget/error";
|
||||||
|
|
||||||
|
export default function Integration({ config, params }) {
|
||||||
|
const { setEvents } = useContext(EventContext);
|
||||||
|
const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar",
|
||||||
|
{ ...params, includeSeries: 'true', includeEpisodeFile: 'false', includeEpisodeImages: 'false', ...config?.params ?? {} }
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sonarrData || sonarrError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsToAdd = {};
|
||||||
|
|
||||||
|
sonarrData?.forEach(event => {
|
||||||
|
const title = `${event.series.title ?? event.title} - S${event.seasonNumber}E${event.episodeNumber}`;
|
||||||
|
|
||||||
|
eventsToAdd[title] = {
|
||||||
|
title,
|
||||||
|
date: DateTime.fromISO(event.airDateUtc),
|
||||||
|
color: config?.color ?? 'teal'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
|
||||||
|
setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
|
||||||
|
}, [sonarrData, sonarrError, config, setEvents]);
|
||||||
|
|
||||||
|
const error = sonarrError ?? sonarrData?.error;
|
||||||
|
return error && <Error error={{ message: `${config.type}: ${error.message ?? error}`}} />
|
||||||
|
}
|
153
src/widgets/calendar/monthly-view.jsx
Normal file
153
src/widgets/calendar/monthly-view.jsx
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { useContext, useEffect, useMemo } from "react";
|
||||||
|
import { DateTime, Info } from "luxon";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
|
import { EventContext, ShowDateContext } from "../../utils/contexts/calendar";
|
||||||
|
|
||||||
|
const colorVariants = {
|
||||||
|
// https://tailwindcss.com/docs/content-configuration#dynamic-class-names
|
||||||
|
amber: "bg-amber-500", blue: "bg-blue-500", cyan: "bg-cyan-500",
|
||||||
|
emerald: "bg-emerald-500", fuchsia: "bg-fuchsia-500", gray: "bg-gray-500",
|
||||||
|
green: "bg-green-500", indigo: "bg-indigo-500", lime: "bg-lime-500",
|
||||||
|
neutral: "bg-neutral-500", orange: "bg-orange-500", pink: "bg-pink-500",
|
||||||
|
purple: "bg-purple-500", red: "bg-red-500", rose: "bg-rose-500",
|
||||||
|
sky: "bg-sky-500", slate: "bg-slate-500", stone: "bg-stone-500",
|
||||||
|
teal: "bg-teal-500", violet: "bg-violet-500", white: "bg-white-500",
|
||||||
|
yellow: "bg-yellow-500", zinc: "bg-zinc-500",
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellStyle = "relative w-10 flex items-center justify-center flex-col";
|
||||||
|
const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer"
|
||||||
|
|
||||||
|
export function Day({ weekNumber, weekday, events }) {
|
||||||
|
const currentDate = DateTime.now();
|
||||||
|
const { showDate, setShowDate } = useContext(ShowDateContext);
|
||||||
|
|
||||||
|
const cellDate = showDate.set({ weekday, weekNumber }).startOf("day");
|
||||||
|
const filteredEvents = events?.filter(event => event.date?.startOf("day").toUnixInteger() === cellDate.toUnixInteger());
|
||||||
|
|
||||||
|
const dayStyles = (displayDate) => {
|
||||||
|
let style = "h-9 ";
|
||||||
|
|
||||||
|
if ([6,7].includes(displayDate.weekday)) {
|
||||||
|
// weekend style
|
||||||
|
style += "text-red-500 ";
|
||||||
|
// different month style
|
||||||
|
style += displayDate.month !== showDate.month ? "text-red-500/40 " : "";
|
||||||
|
} else if (displayDate.month !== showDate.month) {
|
||||||
|
// different month style
|
||||||
|
style += "text-gray-500 ";
|
||||||
|
}
|
||||||
|
|
||||||
|
// selected same day style
|
||||||
|
style += displayDate.toFormat("MM-dd-yyyy") === showDate.toFormat("MM-dd-yyyy") ? "text-black-500 bg-theme-100/20 dark:bg-white/10 rounded-md " : "";
|
||||||
|
|
||||||
|
if (displayDate.toFormat("MM-dd-yyyy") === currentDate.toFormat("MM-dd-yyyy")) {
|
||||||
|
// today style
|
||||||
|
style += "text-black-500 bg-theme-100/20 dark:bg-black/20 rounded-md ";
|
||||||
|
} else {
|
||||||
|
style += "hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer ";
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <button
|
||||||
|
key={`day${weekday}${weekNumber}}`} type="button" className={classNames(dayStyles(cellDate), cellStyle)}
|
||||||
|
style={{ width: "14%" }} onClick={() => setShowDate(cellDate)}
|
||||||
|
>
|
||||||
|
{cellDate.day}
|
||||||
|
<span className="flex justify-center items-center absolute w-full -mb-6">
|
||||||
|
{filteredEvents && filteredEvents.slice(0, 4).map(event => <span
|
||||||
|
key={event.date.toLocaleString() + event.color + event.title}
|
||||||
|
className={classNames(
|
||||||
|
"inline-flex h-1 w-1 m-0.5 rounded",
|
||||||
|
colorVariants[event.color] ?? "gray"
|
||||||
|
)}
|
||||||
|
/>)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Event({ event }) {
|
||||||
|
return <div
|
||||||
|
key={event.title}
|
||||||
|
className="text-theme-700 dark:text-theme-200 text-xs relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
|
||||||
|
><span className="absolute left-2 text-left text-xs mt-[2px] truncate text-ellipsis" style={{width: '96%'}}>{event.title}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayInWeekId = {
|
||||||
|
monday: 1, tuesday: 2, wednesday: 3, thursday: 4, friday: 5, saturday: 6, sunday: 7
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default function MonthlyView({ service }) {
|
||||||
|
const { widget } = service;
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
const { showDate, setShowDate } = useContext(ShowDateContext);
|
||||||
|
const { events } = useContext(EventContext);
|
||||||
|
const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showDate) {
|
||||||
|
setShowDate(currentDate);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const dayNames = Info.weekdays("short", { locale: i18n.language });
|
||||||
|
|
||||||
|
const firstDayInCalendar = widget?.firstDayInCalendar ? widget?.firstDayInCalendar?.toLowerCase() : "monday";
|
||||||
|
for (let i = 1; i < dayInWeekId[firstDayInCalendar]; i+=1) {
|
||||||
|
dayNames.push(dayNames.shift());
|
||||||
|
}
|
||||||
|
|
||||||
|
const daysInWeek = useMemo(() => [ ...Array(7).keys() ].map( i => i + dayInWeekId[firstDayInCalendar]
|
||||||
|
), [(firstDayInCalendar)]);
|
||||||
|
|
||||||
|
if (!showDate) {
|
||||||
|
return <div className="w-full text-center" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstWeek = DateTime.local(showDate.year, showDate.month, 1).setLocale(i18n.language);
|
||||||
|
|
||||||
|
let weekNumbers = [ ...Array(Math.ceil(5) + 1).keys() ]
|
||||||
|
.map(i => firstWeek.weekNumber+i);
|
||||||
|
|
||||||
|
if (weekNumbers.includes(55)) {
|
||||||
|
// if we went too far with the weeks, it's the beginning of the year
|
||||||
|
weekNumbers = weekNumbers.map(weekNum => weekNum-52 );
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsArray = Object.keys(events).map(eventKey => events[eventKey]);
|
||||||
|
|
||||||
|
return <div className="w-full text-center">
|
||||||
|
<div className="flex-col">
|
||||||
|
<span><button type="button" onClick={ () => setShowDate(showDate.minus({ months: 1 }).startOf("day")) } className={classNames(monthButton)}><</button></span>
|
||||||
|
<span>{ showDate.setLocale(i18n.language).toFormat("MMMM y") }</span>
|
||||||
|
<span><button type="button" onClick={ () => setShowDate(showDate.plus({ months: 1 }).startOf("day")) } className={classNames(monthButton)}>></button></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-2 w-full">
|
||||||
|
<div className="flex justify-between flex-wrap">
|
||||||
|
{ dayNames.map(name => <span key={name} className={classNames(cellStyle)} style={{ width: "14%" }}>{name}</span>) }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={classNames(
|
||||||
|
"flex justify-between flex-wrap",
|
||||||
|
!eventsArray.length && widget?.integrations?.length && "animate-pulse"
|
||||||
|
)}>{weekNumbers.map(weekNumber =>
|
||||||
|
daysInWeek.map(dayInWeek =>
|
||||||
|
<Day key={`week${weekNumber}day${dayInWeek}}`} weekNumber={weekNumber} weekday={dayInWeek} events={eventsArray} />
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col pt-1 pb-1">
|
||||||
|
{eventsArray?.filter(event => showDate.startOf("day").toUnixInteger() === event.date?.startOf("day").toUnixInteger())
|
||||||
|
.map(event => <Event key={`event${event.title}`} event={event} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
@ -9,6 +9,7 @@ const components = {
|
|||||||
azuredevops: dynamic(() => import("./azuredevops/component")),
|
azuredevops: dynamic(() => import("./azuredevops/component")),
|
||||||
bazarr: dynamic(() => import("./bazarr/component")),
|
bazarr: dynamic(() => import("./bazarr/component")),
|
||||||
caddy: dynamic(() => import("./caddy/component")),
|
caddy: dynamic(() => import("./caddy/component")),
|
||||||
|
calendar: dynamic(() => import("./calendar/component")),
|
||||||
calibreweb: dynamic(() => import("./calibreweb/component")),
|
calibreweb: dynamic(() => import("./calibreweb/component")),
|
||||||
changedetectionio: dynamic(() => import("./changedetectionio/component")),
|
changedetectionio: dynamic(() => import("./changedetectionio/component")),
|
||||||
channelsdvrserver: dynamic(() => import("./channelsdvrserver/component")),
|
channelsdvrserver: dynamic(() => import("./channelsdvrserver/component")),
|
||||||
|
@ -14,6 +14,10 @@ const widget = {
|
|||||||
"queue/status": {
|
"queue/status": {
|
||||||
endpoint: "queue/status",
|
endpoint: "queue/status",
|
||||||
},
|
},
|
||||||
|
calendar: {
|
||||||
|
endpoint: "calendar",
|
||||||
|
params: ["start", "end", "unmonitored", "includeArtist"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -52,6 +52,10 @@ const widget = {
|
|||||||
return 0;
|
return 0;
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
calendar: {
|
||||||
|
endpoint: "calendar",
|
||||||
|
params: ["start", "end", "unmonitored"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +18,10 @@ const widget = {
|
|||||||
"wanted/missing": {
|
"wanted/missing": {
|
||||||
endpoint: "wanted/missing",
|
endpoint: "wanted/missing",
|
||||||
},
|
},
|
||||||
|
calendar: {
|
||||||
|
endpoint: "calendar",
|
||||||
|
params: ["start", "end", "unmonitored", "includeAuthor"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,7 +57,11 @@ const widget = {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
calendar: {
|
||||||
|
endpoint: "calendar",
|
||||||
|
params: ["start", "end", "unmonitored", "includeSeries", "includeEpisodeFile", "includeEpisodeImages"],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user