Skip to content

Commit

Permalink
Feature: Added agenda view for calendar, calendar improvements (#2216)
Browse files Browse the repository at this point in the history
* Feature: Added agenda view for calendar, calendar improvements

* Fix duplicate event keys

* Additional hover on title, not date

* Show date once in list

* Rename monthly view for consistency

* Remove unneeded key props

* CSS cleanup, dont slice title to arbitrary 42 chars which can break column layouts

* Simplify agenda components

* Fix show date once in list

---------

Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
  • Loading branch information
denispapec and shamoon authored Oct 20, 2023
1 parent 792f768 commit 6898faa
Show file tree
Hide file tree
Showing 11 changed files with 219 additions and 68 deletions.
18 changes: 18 additions & 0 deletions docs/widgets/services/calendar.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ title: Calendar
description: Calendar widget
---

## Monthly view

<img alt="calendar" src="https://user-images.githubusercontent.com/5442891/271131282-6767a3ea-573e-4005-aeb9-6e14ee01e845.png">

This widget shows monthly calendar, with optional integrations to show events from supported widgets.
Expand All @@ -11,6 +13,8 @@ This widget shows monthly calendar, with optional integrations to show events fr
widget:
type: calendar
firstDayInWeek: sunday # optional - defaults to monday
view: monthly # optional - possible values monthly, agenda
maxEvents: 10 # optional - defaults to 10
integrations: # optional
- type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr
service_group: Media # group name where widget exists
Expand All @@ -20,6 +24,20 @@ widget:
unmonitored: true # optional - defaults to false, used with *arr stack
```
## Agenda
This view shows only list of events from configured integrations
```yaml
widget:
type: calendar
view: agenda
maxEvents: 10 # optional - defaults to 10
integrations: # same as in Monthly view example
```
## Integrations
Currently integrated widgets are [sonarr](sonarr.md), [radarr](radarr.md), [lidarr](lidarr.md) and [readarr](readarr.md).
Supported colors can be found on [color palette](../../configs/settings.md#color-palette).
6 changes: 2 additions & 4 deletions src/pages/_app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ColorProvider } from "utils/contexts/color";
import { ThemeProvider } from "utils/contexts/theme";
import { SettingsProvider } from "utils/contexts/settings";
import { TabProvider } from "utils/contexts/tab";
import { EventProvider, ShowDateProvider } from "utils/contexts/calendar";
import { EventProvider } from "utils/contexts/calendar";

function MyApp({ Component, pageProps }) {
return (
Expand All @@ -33,9 +33,7 @@ function MyApp({ Component, pageProps }) {
<SettingsProvider>
<TabProvider>
<EventProvider>
<ShowDateProvider>
<Component {...pageProps} />
</ShowDateProvider>
<Component {...pageProps} />
</EventProvider>
</TabProvider>
</SettingsProvider>
Expand Down
4 changes: 4 additions & 0 deletions src/utils/config/service-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,8 @@ export function cleanServiceGroups(groups) {
refreshInterval,
integrations, // calendar widget
firstDayInWeek,
view,
maxEvents,
} = cleanedService.widget;

let fieldsList = fields;
Expand Down Expand Up @@ -450,6 +452,8 @@ export function cleanServiceGroups(groups) {
if (type === "calendar") {
if (integrations) cleanedService.widget.integrations = integrations;
if (firstDayInWeek) cleanedService.widget.firstDayInWeek = firstDayInWeek;
if (view) cleanedService.widget.view = view;
if (maxEvents) cleanedService.widget.maxEvents = maxEvents;
}
}

Expand Down
13 changes: 0 additions & 13 deletions src/utils/contexts/calendar.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createContext, useState, useMemo } from "react";

export const EventContext = createContext();
export const ShowDateContext = createContext();

export function EventProvider({ initialEvent, children }) {
const [events, setEvents] = useState({});
Expand All @@ -14,15 +13,3 @@ export function EventProvider({ initialEvent, children }) {

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>;
}
101 changes: 101 additions & 0 deletions src/widgets/calendar/agenda.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { useContext, useState } from "react";
import { DateTime } from "luxon";
import classNames from "classnames";
import { useTranslation } from "next-i18next";
import { IoMdCheckmarkCircleOutline } from "react-icons/io";

import { EventContext } from "../../utils/contexts/calendar";

export function Event({ event, colorVariants, showDate = false }) {
const [hover, setHover] = useState(false);
const { i18n } = useTranslation();

return (
<div
className="flex flex-row text-theme-700 dark:text-theme-200 items-center text-xs text-left h-5 rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
onMouseEnter={() => setHover(!hover)}
onMouseLeave={() => setHover(!hover)}
>
<span className="ml-2 w-10">
<span>
{showDate &&
event.date.setLocale(i18n.language).startOf("day").toLocaleString({ month: "short", day: "numeric" })}
</span>
</span>
<span className="ml-2 h-2 w-2">
<span className={classNames("block w-2 h-2 rounded", colorVariants[event.color] ?? "gray")} />
</span>
<div className="ml-2 h-5 text-left relative truncate" style={{ width: "70%" }}>
<div className="absolute mt-0.5 text-xs">{hover && event.additional ? event.additional : event.title}</div>
</div>
{event.isCompleted && (
<span className="text-xs mr-1 ml-auto z-10">
<IoMdCheckmarkCircleOutline />
</span>
)}
</div>
);
}

export default function Agenda({ service, colorVariants, showDate }) {
const { widget } = service;
const { events } = useContext(EventContext);
const { i18n } = useTranslation();

if (!showDate) {
return <div className=" text-center" />;
}

const eventsArray = Object.keys(events)
.filter(
(eventKey) => showDate.startOf("day").toUnixInteger() <= events[eventKey].date?.startOf("day").toUnixInteger(),
)
.map((eventKey) => events[eventKey])
.sort((a, b) => a.date - b.date)
.slice(0, widget?.maxEvents ?? 10);

if (!eventsArray.length) {
return (
<div className="text-center">
<div className="p-2 ">
<div
className={classNames("flex flex-col pt-1 pb-1", !eventsArray.length && !events.length && "animate-pulse")}
>
<Event
key="no-event"
event={{
title: `No events for today!`,
date: DateTime.now(),
color: "gray",
}}
colorVariants={colorVariants}
i18n={i18n}
/>
</div>
</div>
</div>
);
}

const days = Array.from(new Set(eventsArray.map((e) => e.date.startOf("day").ts)));
const eventsByDay = days.map((d) => eventsArray.filter((e) => e.date.startOf("day").ts === d));

return (
<div className="p-2">
<div className={classNames("flex flex-col pt-1 pb-1", !eventsArray.length && !events.length && "animate-pulse")}>
{eventsByDay.map((eventsDay, i) => (
<div key={days[i]}>
{eventsDay.map((event, j) => (
<Event
key={`event${event.title}-${event.date}`}
event={event}
colorVariants={colorVariants}
showDate={j === 0}
/>
))}
</div>
))}
</div>
</div>
);
}
75 changes: 65 additions & 10 deletions src/widgets/calendar/component.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,51 @@
import { useContext, useMemo } from "react";
import { useEffect, useMemo, useState } from "react";
import dynamic from "next/dynamic";
import { DateTime } from "luxon";
import { useTranslation } from "next-i18next";

import { ShowDateContext } from "../../utils/contexts/calendar";

import MonthlyView from "./monthly-view";
import Monthly from "./monthly";
import Agenda from "./agenda";

import Container from "components/services/widget/container";

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",
};

export default function Component({ service }) {
const { widget } = service;
const { showDate } = useContext(ShowDateContext);
const { i18n } = useTranslation();
const [showDate, setShowDate] = useState(null);
const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");

useEffect(() => {
if (!showDate) {
setShowDate(currentDate);
}
}, [showDate, currentDate]);

// params for API fetch
const params = useMemo(() => {
Expand All @@ -27,10 +63,12 @@ export default function Component({ service }) {
// Load active integrations
const integrations = useMemo(
() =>
widget.integrations?.map((integration) => ({
service: dynamic(() => import(`./integrations/${integration?.type}`)),
widget: integration,
})) ?? [],
widget.integrations
?.filter((integration) => integration?.type)
.map((integration) => ({
service: dynamic(() => import(`./integrations/${integration.type}`)),
widget: integration,
})) ?? [],
[widget.integrations],
);

Expand All @@ -52,7 +90,24 @@ export default function Component({ service }) {
);
})}
</div>
<MonthlyView service={service} className="flex" />
{(!widget?.view || widget?.view === "monthly") && (
<Monthly
service={service}
colorVariants={colorVariants}
showDate={showDate}
setShowDate={setShowDate}
className="flex"
/>
)}
{widget?.view === "agenda" && (
<Agenda
service={service}
colorVariants={colorVariants}
showDate={showDate}
setShowDate={setShowDate}
className="flex"
/>
)}
</div>
</Container>
);
Expand Down
2 changes: 2 additions & 0 deletions src/widgets/calendar/integrations/lidarr.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export default function Integration({ config, params }) {
title,
date: DateTime.fromISO(event.releaseDate),
color: config?.color ?? "green",
isCompleted: event.grabbed,
additional: "",
};
});

Expand Down
6 changes: 6 additions & 0 deletions src/widgets/calendar/integrations/radarr.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,22 @@ export default function Integration({ config, params }) {
title: cinemaTitle,
date: DateTime.fromISO(event.inCinemas),
color: config?.color ?? "amber",
isCompleted: event.isAvailable,
additional: "",
};
eventsToAdd[physicalTitle] = {
title: physicalTitle,
date: DateTime.fromISO(event.physicalRelease),
color: config?.color ?? "cyan",
isCompleted: event.isAvailable,
additional: "",
};
eventsToAdd[digitalTitle] = {
title: digitalTitle,
date: DateTime.fromISO(event.digitalRelease),
color: config?.color ?? "emerald",
isCompleted: event.isAvailable,
additional: "",
};
});

Expand Down
2 changes: 2 additions & 0 deletions src/widgets/calendar/integrations/readarr.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export default function Integration({ config, params }) {
title,
date: DateTime.fromISO(event.releaseDate),
color: config?.color ?? "rose",
isCompleted: event.grabbed,
additional: "",
};
});

Expand Down
4 changes: 3 additions & 1 deletion src/widgets/calendar/integrations/sonarr.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ export default function Integration({ config, params }) {
const title = `${event.series.title ?? event.title} - S${event.seasonNumber}E${event.episodeNumber}`;

eventsToAdd[title] = {
title,
title: `${event.series.title ?? event.title}`,
date: DateTime.fromISO(event.airDateUtc),
color: config?.color ?? "teal",
isCompleted: event.hasFile,
additional: `S${event.seasonNumber} E${event.episodeNumber}`,
};
});

Expand Down
Loading

0 comments on commit 6898faa

Please sign in to comment.