mirror of
				https://github.com/karl0ss/homepage.git
				synced 2025-11-04 08:20:58 +00:00 
			
		
		
		
	Feature: Implement iCal integration for calendar, improve styling (#2376)
* Feature: Implement iCal integration, improve calendar/agenda styling * Delete calendar.jsx * Calendar proxy handler * code style * Add some basic error handling --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									518ed7fc4e
								
							
						
					
					
						commit
						95d66707f5
					
				@ -15,13 +15,20 @@ widget:
 | 
				
			|||||||
  firstDayInWeek: sunday # optional - defaults to monday
 | 
					  firstDayInWeek: sunday # optional - defaults to monday
 | 
				
			||||||
  view: monthly # optional - possible values monthly, agenda
 | 
					  view: monthly # optional - possible values monthly, agenda
 | 
				
			||||||
  maxEvents: 10 # optional - defaults to 10
 | 
					  maxEvents: 10 # optional - defaults to 10
 | 
				
			||||||
 | 
					  showTime: true # optional - show time for event happening today - defaults to false
 | 
				
			||||||
  integrations: # optional
 | 
					  integrations: # optional
 | 
				
			||||||
    - type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr
 | 
					    - type: sonarr # active widget type that is currently enabled on homepage - possible values: radarr, sonarr, lidarr, readarr, ical
 | 
				
			||||||
      service_group: Media # group name where widget exists
 | 
					      service_group: Media # group name where widget exists
 | 
				
			||||||
      service_name: Sonarr # service name for that widget
 | 
					      service_name: Sonarr # service name for that widget
 | 
				
			||||||
      color: teal # optional - defaults to pre-defined color for the service (teal for sonarr)
 | 
					      color: teal # optional - defaults to pre-defined color for the service (teal for sonarr)
 | 
				
			||||||
      params: # optional - additional params for the service
 | 
					      params: # optional - additional params for the service
 | 
				
			||||||
        unmonitored: true # optional - defaults to false, used with *arr stack
 | 
					        unmonitored: true # optional - defaults to false, used with *arr stack
 | 
				
			||||||
 | 
					    - type: ical # Show calendar events from another service
 | 
				
			||||||
 | 
					      url: https://domain.url/with/link/to.ics # URL with calendar events
 | 
				
			||||||
 | 
					      name: My Events # required - name for these calendar events
 | 
				
			||||||
 | 
					      color: zinc # optional - defaults to pre-defined color for the service (zinc for ical)
 | 
				
			||||||
 | 
					      params: # optional - additional params for the service
 | 
				
			||||||
 | 
					        showName: true # optional - show name before event title in event line - defaults to false
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Agenda
 | 
					## Agenda
 | 
				
			||||||
@ -33,6 +40,7 @@ widget:
 | 
				
			|||||||
  type: calendar
 | 
					  type: calendar
 | 
				
			||||||
  view: agenda
 | 
					  view: agenda
 | 
				
			||||||
  maxEvents: 10 # optional - defaults to 10
 | 
					  maxEvents: 10 # optional - defaults to 10
 | 
				
			||||||
 | 
					  showTime: true # optional - show time for event happening today - defaults to false
 | 
				
			||||||
  previousDays: 3 # optional - shows events since three days ago - defaults to 0
 | 
					  previousDays: 3 # optional - shows events since three days ago - defaults to 0
 | 
				
			||||||
  integrations: # same as in Monthly view example
 | 
					  integrations: # same as in Monthly view example
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
@ -42,3 +50,7 @@ widget:
 | 
				
			|||||||
Currently integrated widgets are [sonarr](sonarr.md), [radarr](radarr.md), [lidarr](lidarr.md) and [readarr](readarr.md).
 | 
					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).
 | 
					Supported colors can be found on [color palette](../../configs/settings.md#color-palette).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### iCal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This custom integration allows you to show events from any calendar that supports iCal format, for example, Google Calendar (go to `Settings`, select specific calendar, go to `Integrate calendar`, copy URL from `Public Address in iCal format`).
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										28
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										28
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -10,6 +10,7 @@
 | 
				
			|||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "@headlessui/react": "^1.7.2",
 | 
					        "@headlessui/react": "^1.7.2",
 | 
				
			||||||
        "@kubernetes/client-node": "^0.17.1",
 | 
					        "@kubernetes/client-node": "^0.17.1",
 | 
				
			||||||
 | 
					        "cal-parser": "^1.0.2",
 | 
				
			||||||
        "classnames": "^2.3.2",
 | 
					        "classnames": "^2.3.2",
 | 
				
			||||||
        "compare-versions": "^5.0.1",
 | 
					        "compare-versions": "^5.0.1",
 | 
				
			||||||
        "dockerode": "^3.3.4",
 | 
					        "dockerode": "^3.3.4",
 | 
				
			||||||
@ -1250,6 +1251,15 @@
 | 
				
			|||||||
        "node": ">=14.16"
 | 
					        "node": ">=14.16"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/cal-parser": {
 | 
				
			||||||
 | 
					      "version": "1.0.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/cal-parser/-/cal-parser-1.0.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-wlQwcF0fl4eLclyGdncF9rcNNq0ipRYZGagG6h3LVgRXvCWE1fdMUaCLXwfC9YWoz9jKKbjQAq7TpO2Y3yrvmA==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "ical-date-parser": "^4.0.0",
 | 
				
			||||||
 | 
					        "rrule": "^2.6.8"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/call-bind": {
 | 
					    "node_modules/call-bind": {
 | 
				
			||||||
      "version": "1.0.2",
 | 
					      "version": "1.0.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
 | 
				
			||||||
@ -3382,6 +3392,11 @@
 | 
				
			|||||||
      "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-1.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-1.2.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-pUx3AcgXCbur0jpFA7U67Z2RJflAcIi698Y8VL+phdOqUchahxriV3Cs+M6UkPNQSS/zPEzWLfdJ8EgjB7HVxg=="
 | 
					      "integrity": "sha512-pUx3AcgXCbur0jpFA7U67Z2RJflAcIi698Y8VL+phdOqUchahxriV3Cs+M6UkPNQSS/zPEzWLfdJ8EgjB7HVxg=="
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/ical-date-parser": {
 | 
				
			||||||
 | 
					      "version": "4.0.0",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/ical-date-parser/-/ical-date-parser-4.0.0.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-XRCK/FU1akC2ZaJOdKIeZI6BLLgzWUuE0pegSrrkEva89GOan5mNkLVqCU4EMhCJ9nkG5TLWdMXrVX1fNAkFzw=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/iconv-lite": {
 | 
					    "node_modules/iconv-lite": {
 | 
				
			||||||
      "version": "0.4.24",
 | 
					      "version": "0.4.24",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
 | 
				
			||||||
@ -5485,6 +5500,19 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/isaacs"
 | 
					        "url": "https://github.com/sponsors/isaacs"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/rrule": {
 | 
				
			||||||
 | 
					      "version": "2.7.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/rrule/-/rrule-2.7.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==",
 | 
				
			||||||
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "tslib": "^2.4.0"
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "node_modules/rrule/node_modules/tslib": {
 | 
				
			||||||
 | 
					      "version": "2.6.2",
 | 
				
			||||||
 | 
					      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
 | 
				
			||||||
 | 
					      "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "node_modules/run-parallel": {
 | 
					    "node_modules/run-parallel": {
 | 
				
			||||||
      "version": "1.2.0",
 | 
					      "version": "1.2.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,7 @@
 | 
				
			|||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@headlessui/react": "^1.7.2",
 | 
					    "@headlessui/react": "^1.7.2",
 | 
				
			||||||
    "@kubernetes/client-node": "^0.17.1",
 | 
					    "@kubernetes/client-node": "^0.17.1",
 | 
				
			||||||
 | 
					    "cal-parser": "^1.0.2",
 | 
				
			||||||
    "classnames": "^2.3.2",
 | 
					    "classnames": "^2.3.2",
 | 
				
			||||||
    "compare-versions": "^5.0.1",
 | 
					    "compare-versions": "^5.0.1",
 | 
				
			||||||
    "dockerode": "^3.3.4",
 | 
					    "dockerode": "^3.3.4",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										20
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@ -11,6 +11,9 @@ dependencies:
 | 
				
			|||||||
  '@kubernetes/client-node':
 | 
					  '@kubernetes/client-node':
 | 
				
			||||||
    specifier: ^0.17.1
 | 
					    specifier: ^0.17.1
 | 
				
			||||||
    version: 0.17.1
 | 
					    version: 0.17.1
 | 
				
			||||||
 | 
					  cal-parser:
 | 
				
			||||||
 | 
					    specifier: ^1.0.2
 | 
				
			||||||
 | 
					    version: 1.0.2
 | 
				
			||||||
  classnames:
 | 
					  classnames:
 | 
				
			||||||
    specifier: ^2.3.2
 | 
					    specifier: ^2.3.2
 | 
				
			||||||
    version: 2.3.2
 | 
					    version: 2.3.2
 | 
				
			||||||
@ -864,6 +867,13 @@ packages:
 | 
				
			|||||||
      responselike: 3.0.0
 | 
					      responselike: 3.0.0
 | 
				
			||||||
    dev: false
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /cal-parser@1.0.2:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-wlQwcF0fl4eLclyGdncF9rcNNq0ipRYZGagG6h3LVgRXvCWE1fdMUaCLXwfC9YWoz9jKKbjQAq7TpO2Y3yrvmA==}
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      ical-date-parser: 4.0.0
 | 
				
			||||||
 | 
					      rrule: 2.7.2
 | 
				
			||||||
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /call-bind@1.0.2:
 | 
					  /call-bind@1.0.2:
 | 
				
			||||||
    resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
 | 
					    resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
@ -2232,6 +2242,10 @@ packages:
 | 
				
			|||||||
      '@babel/runtime': 7.21.0
 | 
					      '@babel/runtime': 7.21.0
 | 
				
			||||||
    dev: false
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /ical-date-parser@4.0.0:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-XRCK/FU1akC2ZaJOdKIeZI6BLLgzWUuE0pegSrrkEva89GOan5mNkLVqCU4EMhCJ9nkG5TLWdMXrVX1fNAkFzw==}
 | 
				
			||||||
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /iconv-lite@0.4.24:
 | 
					  /iconv-lite@0.4.24:
 | 
				
			||||||
    resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
 | 
					    resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
 | 
				
			||||||
    engines: {node: '>=0.10.0'}
 | 
					    engines: {node: '>=0.10.0'}
 | 
				
			||||||
@ -3538,6 +3552,12 @@ packages:
 | 
				
			|||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
      glob: 7.2.3
 | 
					      glob: 7.2.3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /rrule@2.7.2:
 | 
				
			||||||
 | 
					    resolution: {integrity: sha512-NkBsEEB6FIZOZ3T8frvEBOB243dm46SPufpDckY/Ap/YH24V1zLeMmDY8OA10lk452NdrF621+ynDThE7FQU2A==}
 | 
				
			||||||
 | 
					    dependencies:
 | 
				
			||||||
 | 
					      tslib: 2.5.0
 | 
				
			||||||
 | 
					    dev: false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /run-parallel@1.2.0:
 | 
					  /run-parallel@1.2.0:
 | 
				
			||||||
    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
 | 
					    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
 | 
				
			||||||
    dependencies:
 | 
					    dependencies:
 | 
				
			||||||
 | 
				
			|||||||
@ -765,6 +765,7 @@
 | 
				
			|||||||
        "inCinemas": "In cinemas",
 | 
					        "inCinemas": "In cinemas",
 | 
				
			||||||
        "physicalRelease": "Physical release",
 | 
					        "physicalRelease": "Physical release",
 | 
				
			||||||
        "digitalRelease": "Digital release",
 | 
					        "digitalRelease": "Digital release",
 | 
				
			||||||
        "noEventsToday": "No events for today!"
 | 
					        "noEventsToday": "No events for today!",
 | 
				
			||||||
 | 
					        "noEventsFound": "No events found"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,6 @@ 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 } from "utils/contexts/calendar";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function MyApp({ Component, pageProps }) {
 | 
					function MyApp({ Component, pageProps }) {
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@ -32,9 +31,7 @@ function MyApp({ Component, pageProps }) {
 | 
				
			|||||||
        <ThemeProvider>
 | 
					        <ThemeProvider>
 | 
				
			||||||
          <SettingsProvider>
 | 
					          <SettingsProvider>
 | 
				
			||||||
            <TabProvider>
 | 
					            <TabProvider>
 | 
				
			||||||
              <EventProvider>
 | 
					              <Component {...pageProps} />
 | 
				
			||||||
                <Component {...pageProps} />
 | 
					 | 
				
			||||||
              </EventProvider>
 | 
					 | 
				
			||||||
            </TabProvider>
 | 
					            </TabProvider>
 | 
				
			||||||
          </SettingsProvider>
 | 
					          </SettingsProvider>
 | 
				
			||||||
        </ThemeProvider>
 | 
					        </ThemeProvider>
 | 
				
			||||||
 | 
				
			|||||||
@ -351,6 +351,7 @@ export function cleanServiceGroups(groups) {
 | 
				
			|||||||
          firstDayInWeek,
 | 
					          firstDayInWeek,
 | 
				
			||||||
          integrations,
 | 
					          integrations,
 | 
				
			||||||
          maxEvents,
 | 
					          maxEvents,
 | 
				
			||||||
 | 
					          showTime,
 | 
				
			||||||
          previousDays,
 | 
					          previousDays,
 | 
				
			||||||
          view,
 | 
					          view,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -519,6 +520,7 @@ export function cleanServiceGroups(groups) {
 | 
				
			|||||||
          if (view) cleanedService.widget.view = view;
 | 
					          if (view) cleanedService.widget.view = view;
 | 
				
			||||||
          if (maxEvents) cleanedService.widget.maxEvents = maxEvents;
 | 
					          if (maxEvents) cleanedService.widget.maxEvents = maxEvents;
 | 
				
			||||||
          if (previousDays) cleanedService.widget.previousDays = previousDays;
 | 
					          if (previousDays) cleanedService.widget.previousDays = previousDays;
 | 
				
			||||||
 | 
					          if (showTime) cleanedService.widget.showTime = showTime;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +0,0 @@
 | 
				
			|||||||
import { createContext, useState, useMemo } from "react";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const EventContext = 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>;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,45 +1,11 @@
 | 
				
			|||||||
import { useContext, useState } from "react";
 | 
					 | 
				
			||||||
import { DateTime } from "luxon";
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
import classNames from "classnames";
 | 
					import classNames from "classnames";
 | 
				
			||||||
import { useTranslation } from "next-i18next";
 | 
					import { useTranslation } from "next-i18next";
 | 
				
			||||||
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { EventContext } from "../../utils/contexts/calendar";
 | 
					import Event from "./event";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Event({ event, colorVariants, showDate = false }) {
 | 
					export default function Agenda({ service, colorVariants, events, showDate }) {
 | 
				
			||||||
  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 { widget } = service;
 | 
				
			||||||
  const { events } = useContext(EventContext);
 | 
					 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!showDate) {
 | 
					  if (!showDate) {
 | 
				
			||||||
@ -59,10 +25,8 @@ export default function Agenda({ service, colorVariants, showDate }) {
 | 
				
			|||||||
  if (!eventsArray.length) {
 | 
					  if (!eventsArray.length) {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <div className="text-center">
 | 
					      <div className="text-center">
 | 
				
			||||||
        <div className="p-2 ">
 | 
					        <div className="pl-2 pr-2">
 | 
				
			||||||
          <div
 | 
					          <div className={classNames("flex flex-col", !eventsArray.length && !events.length && "animate-pulse")}>
 | 
				
			||||||
            className={classNames("flex flex-col pt-1 pb-1", !eventsArray.length && !events.length && "animate-pulse")}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <Event
 | 
					            <Event
 | 
				
			||||||
              key="no-event"
 | 
					              key="no-event"
 | 
				
			||||||
              event={{
 | 
					              event={{
 | 
				
			||||||
@ -82,16 +46,17 @@ export default function Agenda({ service, colorVariants, showDate }) {
 | 
				
			|||||||
  const eventsByDay = days.map((d) => eventsArray.filter((e) => e.date.startOf("day").ts === d));
 | 
					  const eventsByDay = days.map((d) => eventsArray.filter((e) => e.date.startOf("day").ts === d));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    <div className="p-2">
 | 
					    <div className="pl-1 pr-1 pb-1">
 | 
				
			||||||
      <div className={classNames("flex flex-col pt-1 pb-1", !eventsArray.length && !events.length && "animate-pulse")}>
 | 
					      <div className={classNames("flex flex-col", !eventsArray.length && !events.length && "animate-pulse")}>
 | 
				
			||||||
        {eventsByDay.map((eventsDay, i) => (
 | 
					        {eventsByDay.map((eventsDay, i) => (
 | 
				
			||||||
          <div key={days[i]}>
 | 
					          <div key={days[i]}>
 | 
				
			||||||
            {eventsDay.map((event, j) => (
 | 
					            {eventsDay.map((event, j) => (
 | 
				
			||||||
              <Event
 | 
					              <Event
 | 
				
			||||||
                key={`event${event.title}-${event.date}`}
 | 
					                key={`event-agenda-${event.title}-${event.date}-${event.additional}`}
 | 
				
			||||||
                event={event}
 | 
					                event={event}
 | 
				
			||||||
                colorVariants={colorVariants}
 | 
					                colorVariants={colorVariants}
 | 
				
			||||||
                showDate={j === 0}
 | 
					                showDate={j === 0}
 | 
				
			||||||
 | 
					                showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -40,6 +40,7 @@ export default function Component({ service }) {
 | 
				
			|||||||
  const { widget } = service;
 | 
					  const { widget } = service;
 | 
				
			||||||
  const { i18n } = useTranslation();
 | 
					  const { i18n } = useTranslation();
 | 
				
			||||||
  const [showDate, setShowDate] = useState(null);
 | 
					  const [showDate, setShowDate] = useState(null);
 | 
				
			||||||
 | 
					  const [events, setEvents] = useState({});
 | 
				
			||||||
  const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
 | 
					  const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
 | 
				
			||||||
  const { settings } = useContext(SettingsContext);
 | 
					  const { settings } = useContext(SettingsContext);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -69,9 +70,9 @@ export default function Component({ service }) {
 | 
				
			|||||||
        ?.filter((integration) => integration?.type)
 | 
					        ?.filter((integration) => integration?.type)
 | 
				
			||||||
        .map((integration) => ({
 | 
					        .map((integration) => ({
 | 
				
			||||||
          service: dynamic(() => import(`./integrations/${integration.type}`)),
 | 
					          service: dynamic(() => import(`./integrations/${integration.type}`)),
 | 
				
			||||||
          widget: integration,
 | 
					          widget: { ...widget, ...integration },
 | 
				
			||||||
        })) ?? [],
 | 
					        })) ?? [],
 | 
				
			||||||
    [widget.integrations],
 | 
					    [widget],
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
@ -80,13 +81,14 @@ export default function Component({ service }) {
 | 
				
			|||||||
        <div className="sticky top-0">
 | 
					        <div className="sticky top-0">
 | 
				
			||||||
          {integrations.map((integration) => {
 | 
					          {integrations.map((integration) => {
 | 
				
			||||||
            const Integration = integration.service;
 | 
					            const Integration = integration.service;
 | 
				
			||||||
            const key = integration.widget.type + integration.widget.service_name + integration.widget.service_group;
 | 
					            const key = `integration-${integration.widget.type}-${integration.widget.service_name}-${integration.widget.service_group}-${integration.widget.name}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return (
 | 
					            return (
 | 
				
			||||||
              <Integration
 | 
					              <Integration
 | 
				
			||||||
                key={key}
 | 
					                key={key}
 | 
				
			||||||
                config={integration.widget}
 | 
					                config={integration.widget}
 | 
				
			||||||
                params={params}
 | 
					                params={params}
 | 
				
			||||||
 | 
					                setEvents={setEvents}
 | 
				
			||||||
                hideErrors={settings.hideErrors}
 | 
					                hideErrors={settings.hideErrors}
 | 
				
			||||||
                className="fixed bottom-0 left-0 bg-red-500 w-screen h-12"
 | 
					                className="fixed bottom-0 left-0 bg-red-500 w-screen h-12"
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
@ -95,8 +97,10 @@ export default function Component({ service }) {
 | 
				
			|||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        {(!widget?.view || widget?.view === "monthly") && (
 | 
					        {(!widget?.view || widget?.view === "monthly") && (
 | 
				
			||||||
          <Monthly
 | 
					          <Monthly
 | 
				
			||||||
 | 
					            key={`monthly-${showDate?.toFormat("yyyy-MM-dd")}`}
 | 
				
			||||||
            service={service}
 | 
					            service={service}
 | 
				
			||||||
            colorVariants={colorVariants}
 | 
					            colorVariants={colorVariants}
 | 
				
			||||||
 | 
					            events={events}
 | 
				
			||||||
            showDate={showDate}
 | 
					            showDate={showDate}
 | 
				
			||||||
            setShowDate={setShowDate}
 | 
					            setShowDate={setShowDate}
 | 
				
			||||||
            className="flex"
 | 
					            className="flex"
 | 
				
			||||||
@ -104,8 +108,10 @@ export default function Component({ service }) {
 | 
				
			|||||||
        )}
 | 
					        )}
 | 
				
			||||||
        {widget?.view === "agenda" && (
 | 
					        {widget?.view === "agenda" && (
 | 
				
			||||||
          <Agenda
 | 
					          <Agenda
 | 
				
			||||||
 | 
					            key={`agenda-${showDate?.toFormat("yyyy-MM-dd")}`}
 | 
				
			||||||
            service={service}
 | 
					            service={service}
 | 
				
			||||||
            colorVariants={colorVariants}
 | 
					            colorVariants={colorVariants}
 | 
				
			||||||
 | 
					            events={events}
 | 
				
			||||||
            showDate={showDate}
 | 
					            showDate={showDate}
 | 
				
			||||||
            setShowDate={setShowDate}
 | 
					            setShowDate={setShowDate}
 | 
				
			||||||
            className="flex"
 | 
					            className="flex"
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										41
									
								
								src/widgets/calendar/event.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/widgets/calendar/event.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					import { useState } from "react";
 | 
				
			||||||
 | 
					import { useTranslation } from "next-i18next";
 | 
				
			||||||
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
 | 
					import classNames from "classnames";
 | 
				
			||||||
 | 
					import { IoMdCheckmarkCircleOutline } from "react-icons/io";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Event({ event, colorVariants, showDate = false, showTime = false, showDateColumn = true }) {
 | 
				
			||||||
 | 
					  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 relative h-5 w-full rounded-md bg-theme-200/50 dark:bg-theme-900/20 mt-1"
 | 
				
			||||||
 | 
					      onMouseEnter={() => setHover(!hover)}
 | 
				
			||||||
 | 
					      onMouseLeave={() => setHover(!hover)}
 | 
				
			||||||
 | 
					      key={`event-${event.title}-${event.date}-${event.additional}`}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      {showDateColumn && (
 | 
				
			||||||
 | 
					        <span className="ml-2 w-10">
 | 
				
			||||||
 | 
					          <span>
 | 
				
			||||||
 | 
					            {(showDate || showTime) &&
 | 
				
			||||||
 | 
					              event.date
 | 
				
			||||||
 | 
					                .setLocale(i18n.language)
 | 
				
			||||||
 | 
					                .toLocaleString(showTime ? DateTime.TIME_24_SIMPLE : { 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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										58
									
								
								src/widgets/calendar/integrations/ical.jsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/widgets/calendar/integrations/ical.jsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,58 @@
 | 
				
			|||||||
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
 | 
					import { parseString } from "cal-parser";
 | 
				
			||||||
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					import { useTranslation } from "next-i18next";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
				
			||||||
 | 
					import Error from "../../../components/services/widget/error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function Integration({ config, params, setEvents, hideErrors }) {
 | 
				
			||||||
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
 | 
					  const { data: icalData, error: icalError } = useWidgetAPI(config, config.name, {
 | 
				
			||||||
 | 
					    refreshInterval: 300000, // 5 minutes
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    let parsedIcal;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!icalError && icalData && !icalData.error) {
 | 
				
			||||||
 | 
					      parsedIcal = parseString(icalData.data);
 | 
				
			||||||
 | 
					      if (parsedIcal.events.length === 0) {
 | 
				
			||||||
 | 
					        icalData.error = { message: `'${config.name}': ${t("calendar.noEventsFound")}` };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (icalError || !parsedIcal) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const eventsToAdd = {};
 | 
				
			||||||
 | 
					    const events = parsedIcal?.getEventsBetweenDates(
 | 
				
			||||||
 | 
					      DateTime.fromISO(params.start).toJSDate(),
 | 
				
			||||||
 | 
					      DateTime.fromISO(params.end).toJSDate(),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    events?.forEach((event) => {
 | 
				
			||||||
 | 
					      let title = `${event?.summary?.value}`;
 | 
				
			||||||
 | 
					      if (config?.params?.showName) {
 | 
				
			||||||
 | 
					        title = `${config.name}: ${title}`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      event.matchingDates.forEach((date) => {
 | 
				
			||||||
 | 
					        eventsToAdd[event?.uid?.value] = {
 | 
				
			||||||
 | 
					          title,
 | 
				
			||||||
 | 
					          date: DateTime.fromJSDate(date),
 | 
				
			||||||
 | 
					          color: config?.color ?? "zinc",
 | 
				
			||||||
 | 
					          isCompleted: DateTime.fromJSDate(date) < DateTime.now(),
 | 
				
			||||||
 | 
					          additional: event.location?.value,
 | 
				
			||||||
 | 
					          type: "ical",
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    setEvents((prevEvents) => ({ ...prevEvents, ...eventsToAdd }));
 | 
				
			||||||
 | 
					  }, [icalData, icalError, config, params, setEvents, t]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const error = icalError ?? icalData?.error;
 | 
				
			||||||
 | 
					  return error && !hideErrors && <Error error={{ message: `${config.type}: ${error.message ?? error}` }} />;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,12 +1,10 @@
 | 
				
			|||||||
import { DateTime } from "luxon";
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
import { useContext, useEffect } from "react";
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
					import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
				
			||||||
import { EventContext } from "../../../utils/contexts/calendar";
 | 
					 | 
				
			||||||
import Error from "../../../components/services/widget/error";
 | 
					import Error from "../../../components/services/widget/error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Integration({ config, params, hideErrors = false }) {
 | 
					export default function Integration({ config, params, setEvents, hideErrors = false }) {
 | 
				
			||||||
  const { setEvents } = useContext(EventContext);
 | 
					 | 
				
			||||||
  const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar", {
 | 
					  const { data: lidarrData, error: lidarrError } = useWidgetAPI(config, "calendar", {
 | 
				
			||||||
    ...params,
 | 
					    ...params,
 | 
				
			||||||
    includeArtist: "false",
 | 
					    includeArtist: "false",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,12 @@
 | 
				
			|||||||
import { DateTime } from "luxon";
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
import { useEffect, useContext } from "react";
 | 
					import { useEffect } from "react";
 | 
				
			||||||
import { useTranslation } from "next-i18next";
 | 
					import { useTranslation } from "next-i18next";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
					import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
				
			||||||
import { EventContext } from "../../../utils/contexts/calendar";
 | 
					 | 
				
			||||||
import Error from "../../../components/services/widget/error";
 | 
					import Error from "../../../components/services/widget/error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Integration({ config, params, hideErrors = false }) {
 | 
					export default function Integration({ config, params, setEvents, hideErrors = false }) {
 | 
				
			||||||
  const { t } = useTranslation();
 | 
					  const { t } = useTranslation();
 | 
				
			||||||
  const { setEvents } = useContext(EventContext);
 | 
					 | 
				
			||||||
  const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar", {
 | 
					  const { data: radarrData, error: radarrError } = useWidgetAPI(config, "calendar", {
 | 
				
			||||||
    ...params,
 | 
					    ...params,
 | 
				
			||||||
    ...(config?.params ?? {}),
 | 
					    ...(config?.params ?? {}),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,10 @@
 | 
				
			|||||||
import { DateTime } from "luxon";
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
import { useEffect, useContext } from "react";
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
					import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
				
			||||||
import { EventContext } from "../../../utils/contexts/calendar";
 | 
					 | 
				
			||||||
import Error from "../../../components/services/widget/error";
 | 
					import Error from "../../../components/services/widget/error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Integration({ config, params, hideErrors = false }) {
 | 
					export default function Integration({ config, params, setEvents, hideErrors = false }) {
 | 
				
			||||||
  const { setEvents } = useContext(EventContext);
 | 
					 | 
				
			||||||
  const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar", {
 | 
					  const { data: readarrData, error: readarrError } = useWidgetAPI(config, "calendar", {
 | 
				
			||||||
    ...params,
 | 
					    ...params,
 | 
				
			||||||
    includeAuthor: "true",
 | 
					    includeAuthor: "true",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,10 @@
 | 
				
			|||||||
import { DateTime } from "luxon";
 | 
					import { DateTime } from "luxon";
 | 
				
			||||||
import { useEffect, useContext } from "react";
 | 
					import { useEffect } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
					import useWidgetAPI from "../../../utils/proxy/use-widget-api";
 | 
				
			||||||
import { EventContext } from "../../../utils/contexts/calendar";
 | 
					 | 
				
			||||||
import Error from "../../../components/services/widget/error";
 | 
					import Error from "../../../components/services/widget/error";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Integration({ config, params, hideErrors = false }) {
 | 
					export default function Integration({ config, params, setEvents, hideErrors = false }) {
 | 
				
			||||||
  const { setEvents } = useContext(EventContext);
 | 
					 | 
				
			||||||
  const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar", {
 | 
					  const { data: sonarrData, error: sonarrError } = useWidgetAPI(config, "calendar", {
 | 
				
			||||||
    ...params,
 | 
					    ...params,
 | 
				
			||||||
    includeSeries: "true",
 | 
					    includeSeries: "true",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,9 @@
 | 
				
			|||||||
import { useContext, useMemo } from "react";
 | 
					import { useMemo } from "react";
 | 
				
			||||||
import { DateTime, Info } from "luxon";
 | 
					import { DateTime, Info } from "luxon";
 | 
				
			||||||
import classNames from "classnames";
 | 
					import classNames from "classnames";
 | 
				
			||||||
import { useTranslation } from "next-i18next";
 | 
					import { useTranslation } from "next-i18next";
 | 
				
			||||||
import { IoMdCheckmarkCircleOutline } from "react-icons/io";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { EventContext } from "../../utils/contexts/calendar";
 | 
					import Event from "./event";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const cellStyle = "relative w-10 flex items-center justify-center flex-col";
 | 
					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";
 | 
					const monthButton = "pl-6 pr-6 ml-2 mr-2 hover:bg-theme-100/20 dark:hover:bg-white/5 rounded-md cursor-pointer";
 | 
				
			||||||
@ -32,11 +31,11 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // selected same day style
 | 
					    // selected same day style
 | 
				
			||||||
    style +=
 | 
					    style +=
 | 
				
			||||||
      displayDate.toFormat("MM-dd-yyyy") === showDate.toFormat("MM-dd-yyyy")
 | 
					      displayDate.startOf("day").ts === showDate.startOf("day").ts
 | 
				
			||||||
        ? "text-black-500 bg-theme-100/20 dark:bg-white/10 rounded-md "
 | 
					        ? "text-black-500 bg-theme-100/20 dark:bg-white/10 rounded-md "
 | 
				
			||||||
        : "";
 | 
					        : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (displayDate.toFormat("MM-dd-yyyy") === currentDate.toFormat("MM-dd-yyyy")) {
 | 
					    if (displayDate.startOf("day").ts === currentDate.startOf("day").ts) {
 | 
				
			||||||
      // today style
 | 
					      // today style
 | 
				
			||||||
      style += "text-black-500 bg-theme-100/20 dark:bg-black/20 rounded-md ";
 | 
					      style += "text-black-500 bg-theme-100/20 dark:bg-black/20 rounded-md ";
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
@ -61,7 +60,7 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
 | 
				
			|||||||
            .slice(0, 4)
 | 
					            .slice(0, 4)
 | 
				
			||||||
            .map((event) => (
 | 
					            .map((event) => (
 | 
				
			||||||
              <span
 | 
					              <span
 | 
				
			||||||
                key={event.date.toLocaleString() + event.color + event.title}
 | 
					                key={`${event.date.ts}+${event.color}-${event.title}-${event.additional}`}
 | 
				
			||||||
                className={classNames("inline-flex h-1 w-1 m-0.5 rounded", colorVariants[event.color] ?? "gray")}
 | 
					                className={classNames("inline-flex h-1 w-1 m-0.5 rounded", colorVariants[event.color] ?? "gray")}
 | 
				
			||||||
              />
 | 
					              />
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
@ -70,25 +69,6 @@ export function Day({ weekNumber, weekday, events, colorVariants, showDate, setS
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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}
 | 
					 | 
				
			||||||
        {event.additional ? ` - ${event.additional}` : ""}
 | 
					 | 
				
			||||||
      </span>
 | 
					 | 
				
			||||||
      {event.isCompleted && (
 | 
					 | 
				
			||||||
        <span className="text-right text-xs flex justify-end mr-1 mt-1 z-10 ">
 | 
					 | 
				
			||||||
          <IoMdCheckmarkCircleOutline />
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const dayInWeekId = {
 | 
					const dayInWeekId = {
 | 
				
			||||||
  monday: 1,
 | 
					  monday: 1,
 | 
				
			||||||
  tuesday: 2,
 | 
					  tuesday: 2,
 | 
				
			||||||
@ -99,10 +79,9 @@ const dayInWeekId = {
 | 
				
			|||||||
  sunday: 7,
 | 
					  sunday: 7,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function Monthly({ service, colorVariants, showDate, setShowDate }) {
 | 
					export default function Monthly({ service, colorVariants, events, showDate, setShowDate }) {
 | 
				
			||||||
  const { widget } = service;
 | 
					  const { widget } = service;
 | 
				
			||||||
  const { i18n } = useTranslation();
 | 
					  const { i18n } = useTranslation();
 | 
				
			||||||
  const { events } = useContext(EventContext);
 | 
					 | 
				
			||||||
  const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
 | 
					  const currentDate = DateTime.now().setLocale(i18n.language).startOf("day");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const dayNames = Info.weekdays("short", { locale: i18n.language });
 | 
					  const dayNames = Info.weekdays("short", { locale: i18n.language });
 | 
				
			||||||
@ -161,7 +140,7 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
 | 
				
			|||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div className="p-2 w-full">
 | 
					      <div className="pl-1 pr-1 pb-1 w-full">
 | 
				
			||||||
        <div className="flex justify-between flex-wrap">
 | 
					        <div className="flex justify-between flex-wrap">
 | 
				
			||||||
          {dayNames.map((name) => (
 | 
					          {dayNames.map((name) => (
 | 
				
			||||||
            <span key={name} className={classNames(cellStyle)} style={{ width: "14%" }}>
 | 
					            <span key={name} className={classNames(cellStyle)} style={{ width: "14%" }}>
 | 
				
			||||||
@ -172,7 +151,7 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          className={classNames(
 | 
					          className={classNames(
 | 
				
			||||||
            "flex justify-between flex-wrap",
 | 
					            "flex justify-between flex-wrap pb-1",
 | 
				
			||||||
            !eventsArray.length && widget?.integrations?.length && "animate-pulse",
 | 
					            !eventsArray.length && widget?.integrations?.length && "animate-pulse",
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
@ -191,12 +170,18 @@ export default function Monthly({ service, colorVariants, showDate, setShowDate
 | 
				
			|||||||
          )}
 | 
					          )}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div className="flex flex-col pt-1 pb-1">
 | 
					        <div className="flex flex-col">
 | 
				
			||||||
          {eventsArray
 | 
					          {eventsArray
 | 
				
			||||||
            ?.filter((event) => showDate.startOf("day").toUnixInteger() === event.date?.startOf("day").toUnixInteger())
 | 
					            ?.filter((event) => showDate.startOf("day").ts === event.date?.startOf("day").ts)
 | 
				
			||||||
            .slice(0, widget?.maxEvents ?? 10)
 | 
					            .slice(0, widget?.maxEvents ?? 10)
 | 
				
			||||||
            .map((event) => (
 | 
					            .map((event) => (
 | 
				
			||||||
              <Event key={`event${event.title}-${event.additional}`} event={event} />
 | 
					              <Event
 | 
				
			||||||
 | 
					                key={`event-monthly-${event.title}-${event.date}-${event.additional}`}
 | 
				
			||||||
 | 
					                event={event}
 | 
				
			||||||
 | 
					                colorVariants={colorVariants}
 | 
				
			||||||
 | 
					                showDateColumn={widget?.showTime ?? false}
 | 
				
			||||||
 | 
					                showTime={widget?.showTime && event.date.startOf("day").ts === showDate.startOf("day").ts}
 | 
				
			||||||
 | 
					              />
 | 
				
			||||||
            ))}
 | 
					            ))}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										33
									
								
								src/widgets/calendar/proxy.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/widgets/calendar/proxy.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					import getServiceWidget from "utils/config/service-helpers";
 | 
				
			||||||
 | 
					import { httpProxy } from "utils/proxy/http";
 | 
				
			||||||
 | 
					import createLogger from "utils/logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const logger = createLogger("calendarProxyHandler");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default async function calendarProxyHandler(req, res) {
 | 
				
			||||||
 | 
					  const { group, service, endpoint } = req.query;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (group && service) {
 | 
				
			||||||
 | 
					    const widget = await getServiceWidget(group, service);
 | 
				
			||||||
 | 
					    const integration = widget.integrations?.find((i) => i.name === endpoint);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (integration) {
 | 
				
			||||||
 | 
					      if (!integration.url) {
 | 
				
			||||||
 | 
					        return res.status(403).json({ error: "No integration URL specified" });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const [status, contentType, data] = await httpProxy(integration.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (contentType) res.setHeader("Content-Type", contentType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (status !== 200) {
 | 
				
			||||||
 | 
					        logger.debug(`HTTTP ${status} retrieving data from integration URL ${integration.url} : ${data}`);
 | 
				
			||||||
 | 
					        return res.status(status).send(data);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return res.status(status).json({ data: data.toString() });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return res.status(400).json({ error: "Invalid integration" });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/widgets/calendar/widget.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/widgets/calendar/widget.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					import calendarProxyHandler from "./proxy";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const widget = {
 | 
				
			||||||
 | 
					  api: "{url}",
 | 
				
			||||||
 | 
					  proxyHandler: calendarProxyHandler,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default widget;
 | 
				
			||||||
@ -6,6 +6,7 @@ import autobrr from "./autobrr/widget";
 | 
				
			|||||||
import azuredevops from "./azuredevops/widget";
 | 
					import azuredevops from "./azuredevops/widget";
 | 
				
			||||||
import bazarr from "./bazarr/widget";
 | 
					import bazarr from "./bazarr/widget";
 | 
				
			||||||
import caddy from "./caddy/widget";
 | 
					import caddy from "./caddy/widget";
 | 
				
			||||||
 | 
					import calendar from "./calendar/widget";
 | 
				
			||||||
import calibreweb from "./calibreweb/widget";
 | 
					import calibreweb from "./calibreweb/widget";
 | 
				
			||||||
import changedetectionio from "./changedetectionio/widget";
 | 
					import changedetectionio from "./changedetectionio/widget";
 | 
				
			||||||
import channelsdvrserver from "./channelsdvrserver/widget";
 | 
					import channelsdvrserver from "./channelsdvrserver/widget";
 | 
				
			||||||
@ -131,6 +132,7 @@ const widgets = {
 | 
				
			|||||||
  homeassistant,
 | 
					  homeassistant,
 | 
				
			||||||
  homebridge,
 | 
					  homebridge,
 | 
				
			||||||
  healthchecks,
 | 
					  healthchecks,
 | 
				
			||||||
 | 
					  ical: calendar,
 | 
				
			||||||
  immich,
 | 
					  immich,
 | 
				
			||||||
  jackett,
 | 
					  jackett,
 | 
				
			||||||
  jdownloader,
 | 
					  jdownloader,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user