diff --git a/src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx b/src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx
new file mode 100644
index 000000000..830dc5901
--- /dev/null
+++ b/src/components/Dashboard/Tiles/Calendar/CalendarDay.tsx
@@ -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
{date.getDate()}
;
+ }
+
+ return (
+
+
+
+
+
+
+
+ {date.getDate()}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+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 (
+
+ {children}
+
+ );
+};
diff --git a/src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx b/src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx
new file mode 100644
index 000000000..77220068a
--- /dev/null
+++ b/src/components/Dashboard/Tiles/Calendar/CalendarTile.tsx
@@ -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 (
+
+ {}}
+ 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) => (
+
+ )}
+ >
+
+ );
+};
+
+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,
+ };
+};
diff --git a/src/components/Dashboard/Tiles/Calendar/MediaList.tsx b/src/components/Dashboard/Tiles/Calendar/MediaList.tsx
new file mode 100644
index 000000000..5a33ae71c
--- /dev/null
+++ b/src/components/Dashboard/Tiles/Calendar/MediaList.tsx
@@ -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 (
+
+ {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')}
+
+ );
+};
+
+const mapMedias = (
+ medias: any[],
+ MediaComponent: (props: { media: any }) => JSX.Element | null,
+ containsLastItem: boolean
+) => {
+ return medias.map((media, index) => (
+
+
+ {containsLastItem && index === medias.length - 1 ? null : }
+
+ ));
+};
+
+const MediaDivider = () => ;
+
+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,
+ },
+}));
diff --git a/src/components/Dashboard/Tiles/Calendar/type.ts b/src/components/Dashboard/Tiles/Calendar/type.ts
new file mode 100644
index 000000000..c9b3b6bb9
--- /dev/null
+++ b/src/components/Dashboard/Tiles/Calendar/type.ts
@@ -0,0 +1,7 @@
+export interface MediasType {
+ tvShows: any[]; // Sonarr
+ movies: any[]; // Radarr
+ musics: any[]; // Lidarr
+ books: any[]; // Readarr
+ totalCount: number;
+}
diff --git a/src/components/Dashboard/Tiles/tilesDefinitions.tsx b/src/components/Dashboard/Tiles/tilesDefinitions.tsx
index bf9fc4452..e16ad78ad 100644
--- a/src/components/Dashboard/Tiles/tilesDefinitions.tsx
+++ b/src/components/Dashboard/Tiles/tilesDefinitions.tsx
@@ -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,
diff --git a/src/modules/calendar/mediaExample.ts b/src/modules/calendar/mediaExample.ts
deleted file mode 100644
index acdbc0352..000000000
--- a/src/modules/calendar/mediaExample.ts
+++ /dev/null
@@ -1,408 +0,0 @@
-export const medias = [
- {
- title: 'Doctor Strange in the Multiverse of Madness',
- originalTitle: 'Doctor Strange in the Multiverse of Madness',
- originalLanguage: {
- id: 1,
- name: 'English',
- },
- alternateTitles: [
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange 2',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 1,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Доктор Стрэндж 2',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 11,
- name: 'Russian',
- },
- id: 2,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doutor Estranho 2',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 3,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange v multivesmíre šialenstva',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 4,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange 2: El multiverso de la locura',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 3,
- name: 'Spanish',
- },
- id: 5,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doktor Strange Deliliğin Çoklu Evreninde',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 17,
- name: 'Turkish',
- },
- id: 6,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'মহাবিশ্বের পাগলামিতে অদ্ভুত চিকিৎসক',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 7,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'จอมเวทย์มหากาฬ ในมัลติเวิร์สมหาภัย',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 28,
- name: 'Thai',
- },
- id: 8,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: "Marvel Studios' Doctor Strange in the Multiverse of Madness",
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 9,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange en el Multiverso de la Locura de Marvel Studios',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 3,
- name: 'Spanish',
- },
- id: 10,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doktors Streindžs neprāta multivisumā',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 11,
- },
- ],
- secondaryYearSourceId: 0,
- sortTitle: 'doctor strange in multiverse madness',
- sizeOnDisk: 0,
- status: 'announced',
- overview:
- 'Doctor Strange, with the help of mystical allies both old and new, traverses the mind-bending and dangerous alternate realities of the Multiverse to confront a mysterious new adversary.',
- inCinemas: '2022-05-04T00:00:00Z',
- images: [
- {
- coverType: 'poster',
- url: 'https://image.tmdb.org/t/p/original/wRnbWt44nKjsFPrqSmwYki5vZtF.jpg',
- },
- {
- coverType: 'fanart',
- url: 'https://image.tmdb.org/t/p/original/ndCSoasjIZAMMDIuMxuGnNWu4DU.jpg',
- },
- ],
- website: 'https://www.marvel.com/movies/doctor-strange-in-the-multiverse-of-madness',
- year: 2022,
- hasFile: false,
- youTubeTrailerId: 'aWzlQ2N6qqg',
- studio: 'Marvel Studios',
- path: '/config/Doctor Strange in the Multiverse of Madness (2022)',
- qualityProfileId: 1,
- monitored: true,
- minimumAvailability: 'announced',
- isAvailable: true,
- folderName: '/config/Doctor Strange in the Multiverse of Madness (2022)',
- runtime: 126,
- cleanTitle: 'doctorstrangeinmultiversemadness',
- imdbId: 'tt9419884',
- tmdbId: 453395,
- titleSlug: '453395',
- certification: 'PG-13',
- genres: ['Fantasy', 'Action', 'Adventure'],
- tags: [],
- added: '2022-04-29T20:52:33Z',
- ratings: {
- tmdb: {
- votes: 0,
- value: 0,
- type: 'user',
- },
- },
- collection: {
- name: 'Doctor Strange Collection',
- tmdbId: 618529,
- images: [],
- },
- id: 1,
- },
- {
- title: 'Doctor Strange in the Multiverse of Madness',
- originalTitle: 'Doctor Strange in the Multiverse of Madness',
- originalLanguage: {
- id: 1,
- name: 'English',
- },
- alternateTitles: [
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange 2',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 1,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Доктор Стрэндж 2',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 11,
- name: 'Russian',
- },
- id: 2,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doutor Estranho 2',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 3,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange v multivesmíre šialenstva',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 4,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange 2: El multiverso de la locura',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 3,
- name: 'Spanish',
- },
- id: 5,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doktor Strange Deliliğin Çoklu Evreninde',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 17,
- name: 'Turkish',
- },
- id: 6,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'মহাবিশ্বের পাগলামিতে অদ্ভুত চিকিৎসক',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 7,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'จอมเวทย์มหากาฬ ในมัลติเวิร์สมหาภัย',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 28,
- name: 'Thai',
- },
- id: 8,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: "Marvel Studios' Doctor Strange in the Multiverse of Madness",
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 9,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doctor Strange en el Multiverso de la Locura de Marvel Studios',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 3,
- name: 'Spanish',
- },
- id: 10,
- },
- {
- sourceType: 'tmdb',
- movieId: 1,
- title: 'Doktors Streindžs neprāta multivisumā',
- sourceId: 0,
- votes: 0,
- voteCount: 0,
- language: {
- id: 1,
- name: 'English',
- },
- id: 11,
- },
- ],
- secondaryYearSourceId: 0,
- sortTitle: 'doctor strange in multiverse madness',
- sizeOnDisk: 0,
- status: 'announced',
- overview:
- 'Doctor Strange, with the help of mystical allies both old and new, traverses the mind-bending and dangerous alternate realities of the Multiverse to confront a mysterious new adversary.',
- inCinemas: '2022-05-05T00:00:00Z',
- images: [
- {
- coverType: 'poster',
- url: 'https://image.tmdb.org/t/p/original/wRnbWt44nKjsFPrqSmwYki5vZtF.jpg',
- },
- {
- coverType: 'fanart',
- url: 'https://image.tmdb.org/t/p/original/ndCSoasjIZAMMDIuMxuGnNWu4DU.jpg',
- },
- ],
- website: 'https://www.marvel.com/movies/doctor-strange-in-the-multiverse-of-madness',
- year: 2022,
- hasFile: false,
- youTubeTrailerId: 'aWzlQ2N6qqg',
- studio: 'Marvel Studios',
- path: '/config/Doctor Strange in the Multiverse of Madness (2022)',
- qualityProfileId: 1,
- monitored: true,
- minimumAvailability: 'announced',
- isAvailable: true,
- folderName: '/config/Doctor Strange in the Multiverse of Madness (2022)',
- runtime: 126,
- cleanTitle: 'doctorstrangeinmultiversemadness',
- imdbId: 'tt9419884',
- tmdbId: 453395,
- titleSlug: '453395',
- certification: 'PG-13',
- genres: ['Fantasy', 'Action', 'Adventure'],
- tags: [],
- added: '2022-04-29T20:52:33Z',
- ratings: {
- tmdb: {
- votes: 0,
- value: 0,
- type: 'user',
- },
- },
- collection: {
- name: 'Doctor Strange Collection',
- tmdbId: 618529,
- images: [],
- },
- id: 2,
- },
-];
diff --git a/src/pages/api/modules/calendar.ts b/src/pages/api/modules/calendar.ts
index 0bb41e545..36beae2b9 100644
--- a/src/pages/api/modules/calendar.ts
+++ b/src/pages/api/modules/calendar.ts
@@ -1,10 +1,9 @@
import axios from 'axios';
-import { getCookie } from 'cookies-next';
import { NextApiRequest, NextApiResponse } from 'next';
-import { getConfig } from '../../../tools/getConfig';
-import { Config } from '../../../tools/types';
+import { getConfig } from '../../../tools/config/getConfig';
+import { ServiceIntegrationApiKeyType, ServiceType } from '../../../types/service';
-async function Post(req: NextApiRequest, res: NextApiResponse) {
+/*async function Post(req: NextApiRequest, res: NextApiResponse) {
// Parse req.body as a ServiceItem
const { id } = req.body;
const { type } = req.query;
@@ -69,15 +68,97 @@ async function Post(req: NextApiRequest, res: NextApiResponse) {
// // Make a request to the URL
// const response = await axios.get(url);
// // Return the response
-}
+}*/
export default async (req: NextApiRequest, res: NextApiResponse) => {
// Filter out if the reuqest is a POST or a GET
- if (req.method === 'POST') {
- return Post(req, res);
+ if (req.method === 'GET') {
+ return Get(req, res);
}
return res.status(405).json({
statusCode: 405,
message: 'Method not allowed',
});
};
+
+async function Get(req: NextApiRequest, res: NextApiResponse) {
+ // Parse req.body as a ServiceItem
+ const {
+ month: monthString,
+ year: yearString,
+ configName,
+ } = req.query as { month: string; year: string; configName: string };
+
+ const month = parseInt(monthString);
+ const year = parseInt(yearString);
+
+ if (isNaN(month) || isNaN(year) || !configName) {
+ return res.status(400).json({
+ statusCode: 400,
+ message: 'Missing required parameter in url: year, month or configName',
+ });
+ }
+
+ const config = getConfig(configName);
+
+ const mediaServiceIntegrationTypes: ServiceIntegrationType['type'][] = [
+ 'sonarr',
+ 'radarr',
+ 'readarr',
+ 'lidarr',
+ ];
+ const mediaServices = config.services.filter(
+ (service) =>
+ service.integration && mediaServiceIntegrationTypes.includes(service.integration.type)
+ );
+
+ const medias = await Promise.all(
+ await mediaServices.map(async (service) => {
+ const integration = service.integration as ServiceIntegrationApiKeyType;
+ const endpoint = IntegrationTypeEndpointMap.get(integration.type);
+ if (!endpoint)
+ return {
+ type: integration.type,
+ items: [],
+ };
+
+ // Get the origin URL
+ let { href: origin } = new URL(service.url);
+ if (origin.endsWith('/')) {
+ origin = origin.slice(0, -1);
+ }
+
+ const start = new Date(year, month - 1, 1); // First day of month
+ const end = new Date(year, month, 0); // Last day of month
+
+ console.log(
+ `${origin}${endpoint}?apiKey=${integration.properties.apiKey}&end=${end}&start=${start}`
+ );
+ return await axios
+ .get(
+ `${origin}${endpoint}?apiKey=${
+ integration.properties.apiKey
+ }&end=${end.toISOString()}&start=${start.toISOString()}`
+ )
+ .then((x) => ({ type: integration.type, items: x.data as any[] }));
+ })
+ );
+
+ // FIXME: I need an integration for each of them
+ return res.status(200).json({
+ tvShows: medias.filter((m) => m.type === 'sonarr').flatMap((m) => m.items),
+ movies: medias.filter((m) => m.type === 'radarr').flatMap((m) => m.items),
+ books: medias.filter((m) => m.type === 'readarr').flatMap((m) => m.items),
+ musics: medias.filter((m) => m.type === 'lidarr').flatMap((m) => m.items),
+ totalCount: medias.reduce((p, c) => p + c.items.length, 0),
+ });
+}
+
+const IntegrationTypeEndpointMap = new Map([
+ ['sonarr', '/api/calendar'],
+ ['radarr', '/api/v3/calendar'],
+ ['lidarr', '/api/v1/calendar'],
+ ['readarr', '/api/v1/calendar'],
+]);
+
+type ServiceIntegrationType = Exclude;
diff --git a/src/tools/config/configExists.ts b/src/tools/config/configExists.ts
new file mode 100644
index 000000000..f6c05b523
--- /dev/null
+++ b/src/tools/config/configExists.ts
@@ -0,0 +1,7 @@
+import fs from 'fs';
+import { generateConfigPath } from './generateConfigPath';
+
+export const configExists = (name: string) => {
+ const path = generateConfigPath(name);
+ return fs.existsSync(path);
+};
diff --git a/src/tools/config/generateConfigPath.ts b/src/tools/config/generateConfigPath.ts
new file mode 100644
index 000000000..c1f2339a7
--- /dev/null
+++ b/src/tools/config/generateConfigPath.ts
@@ -0,0 +1,4 @@
+import path from 'path';
+
+export const generateConfigPath = (configName: string) =>
+ path.join(process.cwd(), 'data/configs', `${configName}.json`);
diff --git a/src/tools/config/getConfig.ts b/src/tools/config/getConfig.ts
new file mode 100644
index 000000000..3e0424f05
--- /dev/null
+++ b/src/tools/config/getConfig.ts
@@ -0,0 +1,9 @@
+import { ConfigType } from '../../types/config';
+import { configExists } from './configExists';
+import { getFallbackConfig } from './getFallbackConfig';
+import { readConfig } from './readConfig';
+
+export const getConfig = (name: string): ConfigType => {
+ if (!configExists(name)) return getFallbackConfig();
+ return readConfig(name);
+};
diff --git a/src/tools/config/getFallbackConfig.ts b/src/tools/config/getFallbackConfig.ts
new file mode 100644
index 000000000..835866fa4
--- /dev/null
+++ b/src/tools/config/getFallbackConfig.ts
@@ -0,0 +1,29 @@
+import { ConfigType } from '../../types/config';
+
+export const getFallbackConfig = (name?: string): ConfigType => ({
+ schemaVersion: '1.0.0',
+ configProperties: {
+ name: name ?? 'default',
+ },
+ categories: [],
+ integrations: {},
+ services: [],
+ settings: {
+ common: {
+ searchEngine: {
+ type: 'google',
+ },
+ },
+ customization: {
+ colors: {},
+ layout: {
+ enabledDocker: true,
+ enabledLeftSidebar: true,
+ enabledPing: true,
+ enabledRightSidebar: true,
+ enabledSearchbar: true,
+ },
+ },
+ },
+ wrappers: [],
+});
diff --git a/src/tools/config/readConfig.ts b/src/tools/config/readConfig.ts
new file mode 100644
index 000000000..c038d88b0
--- /dev/null
+++ b/src/tools/config/readConfig.ts
@@ -0,0 +1,7 @@
+import fs from 'fs';
+import { generateConfigPath } from './generateConfigPath';
+
+export function readConfig(name: string) {
+ const path = generateConfigPath(name);
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
+}
diff --git a/src/tools/isToday.ts b/src/tools/isToday.ts
new file mode 100644
index 000000000..8ca8441f0
--- /dev/null
+++ b/src/tools/isToday.ts
@@ -0,0 +1,8 @@
+export const isToday = (date: Date) => {
+ const today = new Date();
+ return (
+ today.getDate() === date.getDate() &&
+ today.getMonth() === date.getMonth() &&
+ date.getFullYear() === date.getFullYear()
+ );
+};