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:
2026-08-20 19:27:55 +02:00
co-authored by Copilot
parent adabfd22ca
commit cd96b365d0
3 changed files with 315 additions and 111 deletions
+175 -5
View File
@@ -16,6 +16,7 @@ import (
"time"
ical "github.com/emersion/go-ical"
vcard "github.com/emersion/go-vcard"
"github.com/yourusername/caldav-server/internal/db"
"github.com/yourusername/caldav-server/internal/web/templates"
)
@@ -82,6 +83,11 @@ func (s *Server) ownsCalendar(username, cal string) (bool, error) {
// store.Store/db.DB. If requireWrite is true, a read-only share is
// rejected with errCalendarReadOnly.
func (s *Server) resolveCalRef(username, ref string, requireWrite bool) (owner, name string, err error) {
if ref == birthdaysCalRef {
// The birthdays calendar is virtual/computed, not backed by any
// stored calendar object — there's nothing to resolve to.
return "", "", errCalendarNotFound
}
if owner, name, ok := strings.Cut(ref, calRefSep); ok {
if !resourceNameRe.MatchString(owner) || !resourceNameRe.MatchString(name) {
return "", "", errCalendarNotFound
@@ -117,13 +123,28 @@ type calendarEntry struct {
Name string // calendar's own name (unqualified)
Color string
Writable bool
Virtual bool // true for computed calendars (e.g. birthdays) with no backing store objects
}
// listCalendarEntries returns every calendar visible to username: their
// own calendars (always writable) plus any calendars shared with them
// (writable only if the share grants write permission).
// birthdaysCalRef is the fixed reference for the synthetic "Birthdays"
// calendar. It deliberately can't collide with a real calendar's ref: "@"
// is not in resourceNameRe's character class (so it can't be a bare own
// calendar name) and it contains no calRefSep ("~") (so it can't be
// mistaken for an "owner~name" shared-calendar ref either).
const birthdaysCalRef = "@birthdays"
// birthdayColor is the fixed display color for the virtual birthdays
// calendar (a pink, distinct from typical user-picked calendar colors).
const birthdayColor = "#ec4899"
// listCalendarEntries returns every calendar visible to username: the
// synthetic birthdays calendar, their own calendars (always writable),
// and any calendars shared with them (writable only if the share grants
// write permission).
func (s *Server) listCalendarEntries(username string) ([]calendarEntry, error) {
var entries []calendarEntry
entries := []calendarEntry{
{Ref: birthdaysCalRef, Owner: username, Name: "Birthdays", Color: birthdayColor, Writable: false, Virtual: true},
}
own, err := s.dbase.ListCalendars(username)
if err != nil {
@@ -241,9 +262,16 @@ func (s *Server) buildMonthView(username string, year int, month time.Month) (te
}
calSummaries = append(calSummaries, templates.CalendarSummary{
Ref: entry.Ref, Name: entry.Name, Owner: entry.Owner, Color: entry.Color,
Shared: entry.Owner != username, Writable: entry.Writable,
Shared: entry.Owner != username, Writable: entry.Writable, Virtual: entry.Virtual,
})
if entry.Ref == birthdaysCalRef {
if err := s.addBirthdayEvents(username, entry, gridStart, gridEnd, dayIndex, days); err != nil {
s.logger.Warn("computing birthday events", "error", err)
}
continue
}
ids, err := s.store.ListObjects(entry.Owner, "cal-"+entry.Name)
if err != nil {
continue
@@ -944,3 +972,145 @@ func (s *Server) handleCalendarImport(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/web/calendar?imported="+strconv.Itoa(imported), http.StatusSeeOther)
}
// birthdayContact holds one contact's parsed birthday, ready to be placed
// on the month grid.
type birthdayContact struct {
Name string
Book string
ID string
Month time.Month
Day int
Year int // 0 if the vCard's BDAY has no year (e.g. "--08-20")
}
// birthdayFullRe matches a BDAY value that includes a year, in either
// "YYYY-MM-DD" or the older vCard 3.0 "YYYYMMDD" form.
var birthdayFullRe = regexp.MustCompile(`^(\d{4})-?(\d{2})-?(\d{2})`)
// birthdayNoYearRe matches a year-less BDAY value per RFC 6350 §4.3.1,
// "--MM-DD" or "--MMDD".
var birthdayNoYearRe = regexp.MustCompile(`^--(\d{2})-?(\d{2})`)
// parseBirthday extracts month/day (and year, if present) from a vCard
// BDAY field value. Returns ok=false if v isn't a recognized date format
// or names an impossible month/day.
func parseBirthday(v string) (month time.Month, day int, year int, ok bool) {
v = strings.TrimSpace(v)
if m := birthdayNoYearRe.FindStringSubmatch(v); m != nil {
mo, _ := strconv.Atoi(m[1])
d, _ := strconv.Atoi(m[2])
if mo < 1 || mo > 12 || d < 1 || d > 31 {
return 0, 0, 0, false
}
return time.Month(mo), d, 0, true
}
if m := birthdayFullRe.FindStringSubmatch(v); m != nil {
y, _ := strconv.Atoi(m[1])
mo, _ := strconv.Atoi(m[2])
d, _ := strconv.Atoi(m[3])
if mo < 1 || mo > 12 || d < 1 || d > 31 {
return 0, 0, 0, false
}
return time.Month(mo), d, y, true
}
return 0, 0, 0, false
}
// collectBirthdays scans every one of username's own address books for
// contacts with a parseable BDAY field.
func (s *Server) collectBirthdays(username string) ([]birthdayContact, error) {
books, err := s.dbase.ListAddressBooks(username)
if err != nil {
return nil, err
}
var contacts []birthdayContact
for _, book := range books {
ids, err := s.store.ListObjects(username, "card-"+book)
if err != nil {
continue
}
for _, id := range ids {
data, err := s.store.GetObject(username, "card-"+book, id)
if err != nil {
continue
}
card, err := vcard.NewDecoder(bytes.NewReader(data)).Decode()
if err != nil {
continue
}
bday := card.PreferredValue(vcard.FieldBirthday)
if bday == "" {
continue
}
month, day, year, ok := parseBirthday(bday)
if !ok {
continue
}
name := card.PreferredValue(vcard.FieldFormattedName)
if name == "" {
name = id
}
contacts = append(contacts, birthdayContact{
Name: name, Book: book, ID: id, Month: month, Day: day, Year: year,
})
}
}
return contacts, nil
}
// addBirthdayEvents places one virtual all-day event per contact
// birthday falling within [gridStart, gridEnd] into days, titled
// "🎂 Name" (or "🎂 Name (Age)" when the birth year is known). Each event
// links to the contact's edit page instead of an event edit page, since
// there is no underlying calendar object to edit.
func (s *Server) addBirthdayEvents(username string, entry calendarEntry, gridStart, gridEnd time.Time, dayIndex map[string]int, days []templates.MonthDay) error {
contacts, err := s.collectBirthdays(username)
if err != nil {
return err
}
if len(contacts) == 0 {
return nil
}
// The 42-day grid can span at most two calendar years (e.g. a
// December/January boundary), so only those years' occurrences need
// checking.
years := []int{gridStart.Year()}
if gridEnd.Year() != gridStart.Year() {
years = append(years, gridEnd.Year())
}
for _, c := range contacts {
for _, year := range years {
occurrence := time.Date(year, c.Month, c.Day, 0, 0, 0, 0, gridStart.Location())
// Guard against date normalization (e.g. Feb 29 in a
// non-leap year rolling over into March) placing the event
// on the wrong day.
if occurrence.Month() != c.Month || occurrence.Day() != c.Day {
continue
}
if occurrence.Before(gridStart) || occurrence.After(gridEnd) {
continue
}
idx, ok := dayIndex[occurrence.Format(dateLayout)]
if !ok {
continue
}
summary := "🎂 " + c.Name
if c.Year > 0 {
summary += fmt.Sprintf(" (%d)", year-c.Year)
}
days[idx].Events = append(days[idx].Events, templates.EventSummary{
ID: c.Book + "/" + c.ID,
CalRef: entry.Ref,
Color: entry.Color,
Summary: summary,
AllDay: true,
LinkURL: "/web/contacts/" + c.Book + "/" + c.ID + "/edit",
})
}
}
return nil
}
+18 -2
View File
@@ -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">&larr; Calendar</a>
+122 -104
View File
@@ -19,6 +19,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.
@@ -29,6 +30,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.
@@ -116,7 +121,7 @@ func MonthView(username string, data MonthViewData) templ.Component {
var templ_7745c5c3_Var3 templ.SafeURL
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.PrevMonthURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 76, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 81, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -129,7 +134,7 @@ func MonthView(username string, data MonthViewData) templ.Component {
var templ_7745c5c3_Var4 templ.SafeURL
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.TodayURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 77, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 82, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -142,7 +147,7 @@ func MonthView(username string, data MonthViewData) templ.Component {
var templ_7745c5c3_Var5 templ.SafeURL
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(data.NextMonthURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 78, Col: 42}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 83, Col: 42}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -155,7 +160,7 @@ func MonthView(username string, data MonthViewData) templ.Component {
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(data.MonthLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 79, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 84, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -193,7 +198,7 @@ func MonthView(username string, data MonthViewData) templ.Component {
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(wd)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 97, Col: 91}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 102, Col: 91}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -265,7 +270,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + colorOrDefault(c.Color))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 117, Col: 100}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 122, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
@@ -278,7 +283,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(c.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 118, Col: 36}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 123, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
@@ -296,7 +301,7 @@ func calendarLegendRow(c CalendarSummary) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(c.Owner)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 120, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 125, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
@@ -313,43 +318,49 @@ func calendarLegendRow(c CalendarSummary) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"flex-1\"></span> <a href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span class=\"flex-1\"></span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + c.Ref + "/export"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 126, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"text-indigo-600 hover:underline text-xs\">Export .ics</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
if !c.Virtual {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 templ.SafeURL
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + c.Ref + "/export"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 132, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" class=\"text-indigo-600 hover:underline text-xs\">Export .ics</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if c.Writable {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<form method=\"POST\" action=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<form method=\"POST\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var13 templ.SafeURL
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + c.Ref + "/import"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 128, Col: 79}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 135, Col: 79}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" enctype=\"multipart/form-data\" class=\"flex items-center gap-1\"><input type=\"file\" name=\"file\" accept=\".ics,text/calendar\" required class=\"text-xs\"> <button type=\"submit\" class=\"text-indigo-600 hover:underline text-xs\">Import</button></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" enctype=\"multipart/form-data\" class=\"flex items-center gap-1\"><input type=\"file\" name=\"file\" accept=\".ics,text/calendar\" required class=\"text-xs\"> <button type=\"submit\" class=\"text-indigo-600 hover:underline text-xs\">Import</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -383,7 +394,7 @@ func monthDayCell(day MonthDay) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<div class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -396,7 +407,7 @@ func monthDayCell(day MonthDay) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\"><div class=\"flex items-center justify-between\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"><div class=\"flex items-center justify-between\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -405,20 +416,20 @@ func monthDayCell(day MonthDay) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<a href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 templ.SafeURL
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/new?date=" + day.Date))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 140, Col: 58}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 147, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -431,95 +442,95 @@ func monthDayCell(day MonthDay) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" title=\"New event\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" title=\"New event\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprint(day.Day))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 144, Col: 25}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 151, Col: 25}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, ev := range day.Events {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<a href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 templ.SafeURL
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit"))
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(eventLinkURL(ev)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 149, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 156, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\" class=\"block truncate rounded px-1.5 py-0.5 text-xs hover:opacity-80\" style=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" class=\"block truncate rounded px-1.5 py-0.5 text-xs hover:opacity-80\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + colorOrDefault(ev.Color) + "22; color: " + colorOrDefault(ev.Color))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 151, Col: 102}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 158, Col: 102}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" title=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(ev.Summary)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 152, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 159, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\"><span class=\"inline-block w-1.5 h-1.5 rounded-full mr-1\" style=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"><span class=\"inline-block w-1.5 h-1.5 rounded-full mr-1\" style=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + colorOrDefault(ev.Color))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 154, Col: 116}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 161, Col: 116}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\"></span> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\"></span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !ev.AllDay && ev.TimeText != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<span class=\"font-medium\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<span class=\"font-medium\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(ev.TimeText)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 156, Col: 44}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 163, Col: 44}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -527,18 +538,18 @@ func monthDayCell(day MonthDay) templ.Component {
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(" " + ev.Summary)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 158, Col: 22}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 165, Col: 22}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -546,6 +557,13 @@ func monthDayCell(day MonthDay) templ.Component {
})
}
func eventLinkURL(ev EventSummary) string {
if ev.LinkURL != "" {
return ev.LinkURL
}
return "/web/calendar/" + ev.CalRef + "/" + ev.ID + "/edit"
}
func EventForm(username string, data EventFormData, errMsg string) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
@@ -579,188 +597,188 @@ func EventForm(username string, data EventFormData, errMsg string) templ.Compone
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a href=\"/web/calendar\" class=\"text-sm text-indigo-600 hover:underline\">&larr; Calendar</a><h1 class=\"text-2xl font-semibold mt-2 mb-6\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<a href=\"/web/calendar\" class=\"text-sm text-indigo-600 hover:underline\">&larr; Calendar</a><h1 class=\"text-2xl font-semibold mt-2 mb-6\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ID == "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "New event")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "New event")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "Edit event")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "Edit event")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</h1>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</h1>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if errMsg != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<p class=\"mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<p class=\"mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(errMsg)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 175, Col: 98}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 191, Col: 98}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, " ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ID != "" && !data.Writable {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<p class=\"mb-4 text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded px-3 py-2\">This calendar was shared with you as read-only — you can view this event but not change it.</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<p class=\"mb-4 text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded px-3 py-2\">This calendar was shared with you as read-only — you can view this event but not change it.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " <form method=\"POST\" action=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, " <form method=\"POST\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var30 templ.SafeURL
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinURLErrs(eventFormAction(data))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 184, Col: 33}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 200, Col: 33}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\" class=\"bg-white rounded-lg border border-gray-200 p-6 space-y-6 max-w-xl\"><fieldset")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\" class=\"bg-white rounded-lg border border-gray-200 p-6 space-y-6 max-w-xl\"><fieldset")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ID != "" && !data.Writable {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " disabled")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " disabled")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, " class=\"space-y-6\"><div><label class=\"block text-sm font-medium text-gray-700\">Calendar</label> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " class=\"space-y-6\"><div><label class=\"block text-sm font-medium text-gray-700\">Calendar</label> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ID == "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<select name=\"calendar\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<select name=\"calendar\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, c := range data.Calendars {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<option value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<option value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(c.Ref)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 193, Col: 28}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 209, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if c.Ref == data.CalRef {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, " selected")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " selected")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, ">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(c.Label)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 193, Col: 75}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 209, Col: 75}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</option>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</option>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</select>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</select>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<p class=\"mt-1 text-sm text-gray-600\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<p class=\"mt-1 text-sm text-gray-600\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(data.CalendarLabel)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 197, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 213, Col: 63}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "</p>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</div><div><label class=\"block text-sm font-medium text-gray-700\">Title</label> <input name=\"summary\" type=\"text\" required value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</div><div><label class=\"block text-sm font-medium text-gray-700\">Title</label> <input name=\"summary\" type=\"text\" required value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Summary)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 203, Col: 67}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 219, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div><label class=\"flex items-center gap-2 text-sm text-gray-700\"><input id=\"all-day\" name=\"all_day\" type=\"checkbox\" value=\"1\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div><label class=\"flex items-center gap-2 text-sm text-gray-700\"><input id=\"all-day\" name=\"all_day\" type=\"checkbox\" value=\"1\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.AllDay {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " checked")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " checked")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "> All-day event</label><div class=\"grid grid-cols-2 gap-4\"><div><label class=\"block text-sm font-medium text-gray-700\">Start date</label> <input name=\"start_date\" type=\"date\" required value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "> All-day event</label><div class=\"grid grid-cols-2 gap-4\"><div><label class=\"block text-sm font-medium text-gray-700\">Start date</label> <input name=\"start_date\" type=\"date\" required value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var35 string
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.StartDate)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 215, Col: 73}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 231, Col: 73}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -769,7 +787,7 @@ func EventForm(username string, data EventFormData, errMsg string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<div id=\"start-time-field\" class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "<div id=\"start-time-field\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -782,33 +800,33 @@ func EventForm(username string, data EventFormData, errMsg string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"><label class=\"block text-sm font-medium text-gray-700\">Start time</label> <input name=\"start_time\" type=\"time\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\"><label class=\"block text-sm font-medium text-gray-700\">Start time</label> <input name=\"start_time\" type=\"time\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.StartTime)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 220, Col: 64}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 236, Col: 64}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div><div><label class=\"block text-sm font-medium text-gray-700\">End date</label> <input name=\"end_date\" type=\"date\" required value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div><div><label class=\"block text-sm font-medium text-gray-700\">End date</label> <input name=\"end_date\" type=\"date\" required value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var39 string
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.EndDate)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 225, Col: 69}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 241, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -817,7 +835,7 @@ func EventForm(username string, data EventFormData, errMsg string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "<div id=\"end-time-field\" class=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<div id=\"end-time-field\" class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -830,94 +848,94 @@ func EventForm(username string, data EventFormData, errMsg string) templ.Compone
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\"><label class=\"block text-sm font-medium text-gray-700\">End time</label> <input name=\"end_time\" type=\"time\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\"><label class=\"block text-sm font-medium text-gray-700\">End time</label> <input name=\"end_time\" type=\"time\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var42 string
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.EndTime)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 230, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 246, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div></div><div><label class=\"block text-sm font-medium text-gray-700\">Location</label> <input name=\"location\" type=\"text\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div></div><div><label class=\"block text-sm font-medium text-gray-700\">Location</label> <input name=\"location\" type=\"text\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var43 string
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(data.Location)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 237, Col: 60}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 253, Col: 60}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div><div><label class=\"block text-sm font-medium text-gray-700\">Description</label> <textarea name=\"description\" rows=\"4\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\"></div><div><label class=\"block text-sm font-medium text-gray-700\">Description</label> <textarea name=\"description\" rows=\"4\" class=\"mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var44 string
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(data.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 243, Col: 103}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 259, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</textarea></div></fieldset><div class=\"flex items-center justify-between pt-2\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "</textarea></div></fieldset><div class=\"flex items-center justify-between pt-2\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ID == "" || data.Writable {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-indigo-700\">Save</button> ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<button type=\"submit\" class=\"bg-indigo-600 text-white rounded-md px-4 py-2 text-sm font-medium hover:bg-indigo-700\">Save</button> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ID != "" && data.Writable {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<form method=\"POST\" action=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<form method=\"POST\" action=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var45 templ.SafeURL
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + data.CalRef + "/" + data.ID + "/delete"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 254, Col: 103}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 270, Col: 103}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "\" onsubmit=\"return confirm('Delete this event?')\"><button type=\"submit\" class=\"text-red-600 hover:underline text-sm\">Delete event</button></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "\" onsubmit=\"return confirm('Delete this event?')\"><button type=\"submit\" class=\"text-red-600 hover:underline text-sm\">Delete event</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if data.ID != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<a href=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var46 templ.SafeURL
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + data.CalRef + "/" + data.ID + "/export"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 259, Col: 84}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 275, Col: 84}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "\" class=\"text-indigo-600 hover:underline text-sm\">Export .ics</a>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "\" class=\"text-indigo-600 hover:underline text-sm\">Export .ics</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</div></form><script type=\"module\" src=\"/web/static/calendar.js\"></script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "</div></form><script type=\"module\" src=\"/web/static/calendar.js\"></script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}