diff --git a/docs/glance.yml b/docs/glance.yml index b5c68c4c..eda1db91 100644 --- a/docs/glance.yml +++ b/docs/glance.yml @@ -7,6 +7,9 @@ pages: widgets: - type: calendar first-day-of-week: monday + ics: + - https://www.voetbalkrant.com/soccer/calendar/team/team_43_nl.ics + - https://www.voetbalkrant.com/soccer/calendar/team/team_1_nl.ics - type: rss limit: 10 diff --git a/go.mod b/go.mod index d59c9e6c..825829f5 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( require ( github.com/PuerkitoBio/goquery v1.10.3 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect + github.com/arran4/golang-ical v0.3.2 github.com/ebitengine/purego v0.8.4 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/json-iterator/go v1.1.12 // indirect diff --git a/go.sum b/go.sum index 56ae190d..5f2d5abe 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiU github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/arran4/golang-ical v0.3.2 h1:MGNjcXJFSuCXmYX/RpZhR2HDCYoFuK8vTPFLEdFC3JY= +github.com/arran4/golang-ical v0.3.2/go.mod h1:xblDGxxIUMWwFZk9dlECUlc1iXNV65LJZOTHLVwu8bo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/glance/static/css/widget-calendar.css b/internal/glance/static/css/widget-calendar.css index f9b0d9d1..1ee829d0 100644 --- a/internal/glance/static/css/widget-calendar.css +++ b/internal/glance/static/css/widget-calendar.css @@ -33,6 +33,10 @@ color: var(--color-text-highlight); } +.calendar-event-date { + color: var(--color-primary) +} + .calendar-spillover-date { color: var(--color-text-subdue); } @@ -69,3 +73,15 @@ height: 2rem; margin-left: 0.7rem; } + +.calendar-event-tooltip { + position: absolute; + background: rgba(0,0,0,0.8); + color: white; + padding: 5px 10px; + border-radius: 4px; + pointer-events: none; + display: none; + z-index: 9999; + font-size: 12px; +} \ No newline at end of file diff --git a/internal/glance/static/js/calendar.js b/internal/glance/static/js/calendar.js index fbf2e98f..b3d6119f 100644 --- a/internal/glance/static/js/calendar.js +++ b/internal/glance/static/js/calendar.js @@ -31,12 +31,13 @@ const undoEntrance = slideFade({ direction: "left", distance: "100%", duration: export default function(element) { element.swapWith(Calendar( - Number(element.dataset.firstDayOfWeek ?? 1) + Number(element.dataset.firstDayOfWeek ?? 1), + element.dataset.events )); } // TODO: when viewing the previous/next month, display the current date if it's within the spill-over days -function Calendar(firstDay) { +function Calendar(firstDay, event) { let header, dates; let advanceTimeTicker; let now = new Date(); @@ -63,7 +64,7 @@ function Calendar(firstDay) { const calendar = elem().classes("calendar").append( header = Header(nextClicked, prevClicked, undoClicked), - dates = Dates(firstDay) + dates = Dates(firstDay, event) ); update(now); @@ -127,7 +128,7 @@ function Header(nextClicked, prevClicked, undoClicked) { }); } -function Dates(firstDay) { +function Dates(firstDay, events) { let dates, lastRenderedDate; const updateFullMonth = function(now, newDate) { @@ -152,12 +153,39 @@ function Dates(firstDay) { ) } - for (let i = 1; i <= currentMonthDays; i++, index++) { - children[index] + const tooltip = document.createElement("div"); + tooltip.className = "calendar-event-tooltip"; // style this in CSS + document.body.appendChild(tooltip); + for (let i = 2; i <= currentMonthDays; i++, index++) { + const thisDate = new Date(newDate.getFullYear(), newDate.getMonth(), i); + var child = children[index]; + child .classesIf(isCurrentMonth && i === currentDate, "calendar-current-date") .text(i); + if(events && events !== "null") { + const hasEvent = checkIfDateHasEvent(newDate, i, events); + if(hasEvent) { + child.classes("calendar-event-date") + child.addEventListener("mouseenter", (e) => { + tooltip.innerHTML = getEventsForDate(thisDate, events).join("
") + tooltip.style.display = "block"; + tooltip.style.left = e.pageX + 10 + "px"; + tooltip.style.top = e.pageY + 10 + "px"; + }); + + child.addEventListener("mousemove", (e) => { + tooltip.style.left = e.pageX + 10 + "px"; + tooltip.style.top = e.pageY + 10 + "px"; + }); + + child.addEventListener("mouseleave", () => { + tooltip.style.display = "none"; + }); + } + } } + for (let i = 0; i < nextMonthSpilloverDays; i++, index++) { children[index].classes("calendar-spillover-date").text(i + 1); } @@ -210,3 +238,36 @@ function msTillNextDay(now) { now.getHours() * 3_600_000 ); } + +function checkIfDateHasEvent(activeMonth, date, events) { + const eventsObject = JSON.parse(events) + + return eventsObject.some(event => { + const eventDate = new Date(event.Date) + return eventDate.getDate() === date && activeMonth.getMonth() === eventDate.getMonth() + }) +} + +function getEventsForDate(date, events) { + const eventsObject = JSON.parse(events) + //const target = formatDateLocal(date) + + return eventsObject + .filter(ev => { + //const evDate = formatDateLocal(new Date(ev.Date)) + const evDate = new Date(ev.Date) + return isSameDay(date, evDate) + }) + .map(ev => { + const evDate = new Date(ev.Date) + const hours = String(evDate.getHours()).padStart(2, '0'); + const minutes = String(evDate.getMinutes()).padStart(2, '0'); + return `${hours}:${minutes} - ${ev.Name}`; + }); +} + +function isSameDay(dateOnly, timeDate) { + return dateOnly.getFullYear() === timeDate.getFullYear() && + dateOnly.getMonth() === timeDate.getMonth() && + dateOnly.getDate() === timeDate.getDate(); +} diff --git a/internal/glance/templates/calendar.html b/internal/glance/templates/calendar.html index b3c4a694..3710b5fe 100644 --- a/internal/glance/templates/calendar.html +++ b/internal/glance/templates/calendar.html @@ -2,6 +2,6 @@ {{ define "widget-content" }}
-
+
{{ end }} diff --git a/internal/glance/widget-calendar.go b/internal/glance/widget-calendar.go index 9537e537..b383fde9 100644 --- a/internal/glance/widget-calendar.go +++ b/internal/glance/widget-calendar.go @@ -1,9 +1,14 @@ package glance import ( + "encoding/json" "errors" + "fmt" "html/template" + "net/http" "time" + + ics "github.com/arran4/golang-ical" ) var calendarWidgetTemplate = mustParseTemplate("calendar.html", "widget-base.html") @@ -21,8 +26,15 @@ var calendarWeekdaysToInt = map[string]time.Weekday{ type calendarWidget struct { widgetBase `yaml:",inline"` FirstDayOfWeek string `yaml:"first-day-of-week"` + Ics []string `yaml:"ics"` FirstDay int `yaml:"-"` cachedHTML template.HTML `yaml:"-"` + Events string `yaml:"events"` +} + +type calendarEvent struct { + Date time.Time + Name string } func (widget *calendarWidget) initialize() error { @@ -34,6 +46,30 @@ func (widget *calendarWidget) initialize() error { return errors.New("invalid first day of week") } + var events []*ics.VEvent + var widgetEvents []calendarEvent + for _, url := range widget.Ics { + newEvents, err := ReadPublicIcs(url) + if err != nil { + fmt.Println(err) + } + events = append(events, newEvents...) + } + + for _, event := range events { + startDate, _ := event.GetStartAt() + e := calendarEvent{ + Date: startDate, + Name: event.GetProperty("SUMMARY").Value, + } + widgetEvents = append(widgetEvents, e) + + } + jsonBytes, err := json.Marshal(widgetEvents) + if err != nil { + panic(err) + } + widget.Events = string(jsonBytes) widget.FirstDay = int(calendarWeekdaysToInt[widget.FirstDayOfWeek]) widget.cachedHTML = widget.renderTemplate(widget, calendarWidgetTemplate) @@ -43,3 +79,17 @@ func (widget *calendarWidget) initialize() error { func (widget *calendarWidget) Render() template.HTML { return widget.cachedHTML } + +func ReadPublicIcs(url string) ([]*ics.VEvent, error) { + response, err := http.Get(url) + if err != nil { + return nil, err + } + defer response.Body.Close() + cal, err := ics.ParseCalendar(response.Body) + if err != nil { + return nil, err + } + events := cal.Events() + return events, nil +}