Add virtual Birthdays calendar computed from contacts
The combined month view now always includes a read-only "Birthdays" calendar (ref "@birthdays"), generated on the fly from every contact's BDAY field across the user's address books — no calendar objects are stored for it. Each birthday appears as an all-day event titled "🎂 Name (Age)" (age omitted if BDAY has no year, e.g. "--MM-DD"), colored pink, and links to the contact's edit page instead of an event editor. Handles both "YYYY-MM-DD"/"YYYYMMDD" and year-less vCard BDAY formats, and skips Feb 29 birthdays in non-leap years. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ type CalendarSummary struct {
|
||||
Color string // hex color like "#3b82f6", "" if unset
|
||||
Shared bool // true if owned by someone other than the viewer
|
||||
Writable bool
|
||||
Virtual bool // true for computed calendars (e.g. birthdays) with no import/export
|
||||
}
|
||||
|
||||
// EventSummary is a single event shown inside a month-view day cell.
|
||||
@@ -21,6 +22,10 @@ type EventSummary struct {
|
||||
Summary string
|
||||
TimeText string // e.g. "14:00" or "" for all-day events
|
||||
AllDay bool
|
||||
// LinkURL overrides the default "/web/calendar/{CalRef}/{ID}/edit"
|
||||
// link target, used by virtual/read-only calendars (e.g. birthdays)
|
||||
// that don't have an editable event object of their own.
|
||||
LinkURL string
|
||||
}
|
||||
|
||||
// MonthDay is one day cell in the month grid.
|
||||
@@ -123,7 +128,9 @@ templ calendarLegendRow(c CalendarSummary) {
|
||||
<span class="text-xs text-gray-400">(read-only)</span>
|
||||
}
|
||||
<span class="flex-1"></span>
|
||||
<a href={ templ.URL("/web/calendar/" + c.Ref + "/export") } class="text-indigo-600 hover:underline text-xs">Export .ics</a>
|
||||
if !c.Virtual {
|
||||
<a href={ templ.URL("/web/calendar/" + c.Ref + "/export") } class="text-indigo-600 hover:underline text-xs">Export .ics</a>
|
||||
}
|
||||
if c.Writable {
|
||||
<form method="POST" action={ templ.URL("/web/calendar/" + c.Ref + "/import") } enctype="multipart/form-data" class="flex items-center gap-1">
|
||||
<input type="file" name="file" accept=".ics,text/calendar" required class="text-xs"/>
|
||||
@@ -146,7 +153,7 @@ templ monthDayCell(day MonthDay) {
|
||||
</div>
|
||||
for _, ev := range day.Events {
|
||||
<a
|
||||
href={ templ.URL("/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit") }
|
||||
href={ templ.URL(eventLinkURL(ev)) }
|
||||
class="block truncate rounded px-1.5 py-0.5 text-xs hover:opacity-80"
|
||||
style={ "background-color: " + colorOrDefault(ev.Color) + "22; color: " + colorOrDefault(ev.Color) }
|
||||
title={ ev.Summary }
|
||||
@@ -161,6 +168,15 @@ templ monthDayCell(day MonthDay) {
|
||||
</div>
|
||||
}
|
||||
|
||||
func eventLinkURL(ev EventSummary) string {
|
||||
if ev.LinkURL != "" {
|
||||
return ev.LinkURL
|
||||
}
|
||||
return "/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit"
|
||||
}
|
||||
|
||||
|
||||
|
||||
templ EventForm(username string, data EventFormData, errMsg string) {
|
||||
@Layout("Calendar", username) {
|
||||
<a href="/web/calendar" class="text-sm text-indigo-600 hover:underline">← Calendar</a>
|
||||
|
||||
Reference in New Issue
Block a user