package templates import "fmt" import "strconv" import "strings" // CalendarSummary is one calendar (own or shared) shown in the combined // month view's legend. type CalendarSummary struct { Ref string // path-safe reference: "name" or "owner~name" Name string Owner string // owner's username 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. type EventSummary struct { ID string CalRef string Color string 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. type MonthDay struct { Date string // "YYYY-MM-DD", used for links and the "new event" date Day int // day-of-month number shown in the cell InMonth bool // false for the leading/trailing days of neighboring months IsToday bool Events []EventSummary } // MonthViewData is everything the combined month grid template needs to // render one month across every calendar (own + shared) visible to the // viewer. type MonthViewData struct { Calendars []CalendarSummary HasWritable bool // true if the viewer can create events in at least one calendar MonthLabel string // e.g. "August 2026" Weeks [][]MonthDay PrevMonthURL string NextMonthURL string TodayURL string } // CalendarOption is one entry in the "new event" calendar } } templ monthDayCell(day MonthDay) {
{ fmt.Sprint(day.Day) }
for _, ev := range day.Events { if !ev.AllDay && ev.TimeText != "" { { ev.TimeText } } { " " + ev.Summary } }
} func eventLinkURL(ev EventSummary) string { if ev.LinkURL != "" { return ev.LinkURL } return "/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit" } // eventTextColor returns a color derived from hex, darkened if needed so // it stays legible as text on monthDayCell's very light (~13% opacity) // tinted event background. Without this, a light calendar color like // white or pale yellow renders its own event text as invisible or // near-invisible, since the same color is used for both the tint and the // text. Colors that are already dark enough are returned unchanged. func eventTextColor(hex string) string { c := colorOrDefault(hex) r, g, b, ok := parseHexColor(c) if !ok { return c } // Perceived luminance (ITU-R BT.601). luminance := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b) const maxReadableLuminance = 170 if luminance <= maxReadableLuminance { return c } // Scale each channel down so the color darkens towards black while // keeping its hue, ending up comfortably below the threshold. scale := maxReadableLuminance / luminance * 0.75 return fmt.Sprintf("#%02x%02x%02x", clampByte(float64(r)*scale), clampByte(float64(g)*scale), clampByte(float64(b)*scale)) } // parseHexColor parses a "#rrggbb" string into its red/green/blue // components. ok is false for anything else (including 3-digit shorthand // hex, which colorOrDefault/hexColorRe never produce). func parseHexColor(s string) (r, g, b int, ok bool) { s = strings.TrimPrefix(s, "#") if len(s) != 6 { return 0, 0, 0, false } v, err := strconv.ParseInt(s, 16, 32) if err != nil { return 0, 0, 0, false } return int(v >> 16 & 0xff), int(v >> 8 & 0xff), int(v & 0xff), true } func clampByte(v float64) int { if v < 0 { return 0 } if v > 255 { return 255 } return int(v) } templ EventForm(username string, data EventFormData, errMsg string) { @Layout("Calendar", username) { ← Calendar

if data.ID == "" { New event } else { Edit event }

if errMsg != "" {

{ errMsg }

} if data.ID != "" && !data.Writable {

This calendar was shared with you as read-only — you can view this event but not change it.

}
if data.ID == "" { } else {

{ data.CalendarLabel }

}
if data.ID == "" || data.Writable { } if data.ID != "" && data.Writable { } if data.ID != "" { Export .ics }
} } func eventFormAction(data EventFormData) templ.SafeURL { if data.ID == "" { return templ.URL("/web/calendar/new") } return templ.URL("/web/calendar/" + data.CalRef + "/" + data.ID + "/edit") } func weekdayLabels() []string { return []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} }