Add calendar tile

This commit is contained in:
Meierschlumpf
2022-12-11 14:11:25 +01:00
parent 65970d3a2f
commit c2571190f6
13 changed files with 391 additions and 416 deletions

View File

@@ -0,0 +1,64 @@
import { Box, Indicator, IndicatorProps, Popover } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import { MediaList } from './MediaList';
import { MediasType } from './type';
interface CalendarDayProps {
date: Date;
medias: MediasType;
}
export const CalendarDay = ({ date, medias }: CalendarDayProps) => {
const [opened, { close, open }] = useDisclosure(false);
if (medias.totalCount === 0) {
return <div>{date.getDate()}</div>;
}
return (
<Popover
position="bottom"
withArrow
withinPortal
radius="lg"
shadow="sm"
transition="pop"
onClose={close}
opened={opened}
>
<Popover.Target>
<Box onClick={open}>
<DayIndicator color={'red'} position="bottom-start" medias={medias.books}>
<DayIndicator color={'yellow'} position="top-start" medias={medias.movies}>
<DayIndicator color={'blue'} position="top-end" medias={medias.tvShows}>
<DayIndicator color={'green'} position="bottom-end" medias={medias.musics}>
<div>{date.getDate()}</div>
</DayIndicator>
</DayIndicator>
</DayIndicator>
</DayIndicator>
</Box>
</Popover.Target>
<Popover.Dropdown>
<MediaList medias={medias} />
</Popover.Dropdown>
</Popover>
);
};
interface DayIndicatorProps {
color: string;
medias: any[];
children: JSX.Element;
position: IndicatorProps['position'];
}
const DayIndicator = ({ color, medias, children, position }: DayIndicatorProps) => {
if (medias.length === 0) return children;
return (
<Indicator size={10} withBorder offset={5} color={color} position={position}>
{children}
</Indicator>
);
};

View File

@@ -0,0 +1,100 @@
import { Card, createStyles, MantineThemeColors, useMantineTheme } from '@mantine/core';
import { Calendar } from '@mantine/dates';
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { useConfigContext } from '../../../../config/provider';
import { useColorTheme } from '../../../../tools/color';
import { isToday } from '../../../../tools/isToday';
import { CalendarIntegrationType } from '../../../../types/integration';
import { BaseTileProps } from '../type';
import { CalendarDay } from './CalendarDay';
import { MediasType } from './type';
interface CalendarTileProps extends BaseTileProps {
module: CalendarIntegrationType | undefined;
}
export const CalendarTile = ({ className, module }: CalendarTileProps) => {
const { secondaryColor } = useColorTheme();
const { name: configName } = useConfigContext();
const { classes, cx } = useStyles(secondaryColor);
const { colorScheme, colors } = useMantineTheme();
const [month, setMonth] = useState(new Date());
const { data: medias } = useQuery({
queryKey: ['calendar/medias', { month: month.getMonth(), year: month.getFullYear() }],
queryFn: async () =>
(await (
await fetch(
`/api/modules/calendar?year=${month.getFullYear()}&month=${
month.getMonth() + 1
}&configName=${configName}`
)
).json()) as MediasType,
});
if (!module) return <></>;
return (
<Card className={className} withBorder p="xs">
<Calendar
month={month}
onMonthChange={setMonth}
size="xs"
fullWidth
onChange={() => {}}
firstDayOfWeek={module.properties?.isWeekStartingAtSunday ? 'sunday' : 'monday'}
dayStyle={(date) => ({
margin: 1,
backgroundColor: isToday(date)
? colorScheme === 'dark'
? colors.dark[5]
: colors.gray[0]
: undefined,
})}
styles={{
calendarHeader: {
marginRight: 40,
marginLeft: 40,
},
}}
allowLevelChange={false}
dayClassName={(_, modifiers) => cx({ [classes.weekend]: modifiers.weekend })}
renderDay={(date) => (
<CalendarDay date={date} medias={getReleasedMediasForDate(medias, date)} />
)}
></Calendar>
</Card>
);
};
const useStyles = createStyles((theme, secondaryColor: keyof MantineThemeColors) => ({
weekend: {
color: `${secondaryColor} !important`,
},
}));
const getReleasedMediasForDate = (medias: MediasType | undefined, date: Date): MediasType => {
const books =
medias?.books.filter((b) => new Date(b.releaseDate).toDateString() === date.toDateString()) ??
[];
const movies =
medias?.movies.filter((m) => new Date(m.inCinemas).toDateString() === date.toDateString()) ??
[];
const musics =
medias?.musics.filter((m) => new Date(m.releaseDate).toDateString() === date.toDateString()) ??
[];
const tvShows =
medias?.tvShows.filter(
(tv) => new Date(tv.airDateUtc).toDateString() === date.toDateString()
) ?? [];
const totalCount = medias ? books.length + movies.length + musics.length + tvShows.length : 0;
return {
books,
movies,
musics,
tvShows,
totalCount,
};
};

View File

@@ -0,0 +1,66 @@
import { createStyles, Divider, ScrollArea } from '@mantine/core';
import React from 'react';
import {
LidarrMediaDisplay,
RadarrMediaDisplay,
ReadarrMediaDisplay,
SonarrMediaDisplay,
} from '../../../../modules/common';
import { MediasType } from './type';
interface MediaListProps {
medias: MediasType;
}
export const MediaList = ({ medias }: MediaListProps) => {
const { classes } = useStyles();
const lastMediaType = getLastMediaType(medias);
return (
<ScrollArea
offsetScrollbars
scrollbarSize={5}
pt={5}
className={classes.scrollArea}
styles={{
viewport: {
maxHeight: 450,
minHeight: 210,
},
}}
>
{mapMedias(medias.tvShows, SonarrMediaDisplay, lastMediaType === 'tv-show')}
{mapMedias(medias.movies, RadarrMediaDisplay, lastMediaType === 'movie')}
{mapMedias(medias.musics, LidarrMediaDisplay, lastMediaType === 'music')}
{mapMedias(medias.books, ReadarrMediaDisplay, lastMediaType === 'book')}
</ScrollArea>
);
};
const mapMedias = (
medias: any[],
MediaComponent: (props: { media: any }) => JSX.Element | null,
containsLastItem: boolean
) => {
return medias.map((media, index) => (
<React.Fragment>
<MediaComponent media={media} />
{containsLastItem && index === medias.length - 1 ? null : <MediaDivider />}
</React.Fragment>
));
};
const MediaDivider = () => <Divider variant="dashed" size="sm" my="xl" />;
const getLastMediaType = (medias: MediasType) => {
if (medias.books.length >= 1) return 'book';
if (medias.musics.length >= 1) return 'music';
if (medias.movies.length >= 1) return 'movie';
return 'tv-show';
};
const useStyles = createStyles(() => ({
scrollArea: {
width: 400,
},
}));

View File

@@ -0,0 +1,7 @@
export interface MediasType {
tvShows: any[]; // Sonarr
movies: any[]; // Radarr
musics: any[]; // Lidarr
books: any[]; // Readarr
totalCount: number;
}

View File

@@ -1,4 +1,5 @@
import { IntegrationsType } from '../../../types/integration';
import { CalendarTile } from './Calendar/CalendarTile';
import { ClockTile } from './Clock/ClockTile';
import { EmptyTile } from './EmptyTile';
import { ServiceTile } from './Service/ServiceTile';
@@ -34,7 +35,7 @@ export const Tiles: TileDefinitionProps = {
maxHeight: 12,
},
calendar: {
component: EmptyTile, //CalendarTile,
component: CalendarTile,
minWidth: 4,
maxWidth: 12,
minHeight: 5,