.ics" back into its parts.
+var birthdayObjIDRe = regexp.MustCompile(`^([^~]+)~([^~]+)~(\d{4})\.ics$`)
+
+// birthdayObjID builds the stable object ID for contact c's birthday
+// occurrence in year, encoding enough information to regenerate that
+// exact VEVENT on demand in GetCalendarObject.
+func birthdayObjID(c birthdays.Contact, year int) string {
+ base := strings.TrimSuffix(c.ID, path.Ext(c.ID))
+ return fmt.Sprintf("%s~%s~%d.ics", c.Book, base, year)
+}
+
+// birthdaysCalendarMeta returns the caldav.Calendar metadata for
+// username's synthetic Birthdays calendar.
+func (b *Backend) birthdaysCalendarMeta(username string) caldav.Calendar {
+ return caldav.Calendar{
+ Path: calHomePath() + birthdaysCalendarName + "/",
+ Name: "Birthdays",
+ Description: "Birthdays computed from your contacts",
+ SupportedComponentSet: []string{"VEVENT"},
+ MaxResourceSize: 64 * 1024,
+ }
+}
+
+// listBirthdayCalendarObjects generates one VEVENT per (contact, year)
+// pair within the fixed rolling window for every contact of username's
+// that has a parseable birthday.
+func (b *Backend) listBirthdayCalendarObjects(username string) ([]caldav.CalendarObject, error) {
+ contacts, err := birthdays.Collect(b.store, b.dbase, username)
+ if err != nil {
+ return nil, fmt.Errorf("collecting birthdays: %w", err)
+ }
+
+ now := time.Now()
+ var objs []caldav.CalendarObject
+ for _, c := range contacts {
+ for year := now.Year() - birthdayWindowPast; year <= now.Year()+birthdayWindowFuture; year++ {
+ obj, ok := b.encodeBirthdayObject(c, year)
+ if !ok {
+ continue
+ }
+ objs = append(objs, *obj)
+ }
+ }
+ return objs, nil
+}
+
+// birthdayCalendarObject regenerates a single VEVENT for the (contact,
+// year) pair encoded in objID.
+func (b *Backend) birthdayCalendarObject(username, objPath, objID string) (*caldav.CalendarObject, error) {
+ m := birthdayObjIDRe.FindStringSubmatch(objID)
+ if m == nil {
+ return nil, webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("unrecognized birthday object id"))
+ }
+ book, idBase, year := m[1], m[2], m[3]
+ yearNum, err := strconv.Atoi(year)
+ if err != nil {
+ return nil, webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("invalid year"))
+ }
+
+ contacts, err := birthdays.Collect(b.store, b.dbase, username)
+ if err != nil {
+ return nil, fmt.Errorf("collecting birthdays: %w", err)
+ }
+ for _, c := range contacts {
+ if c.Book != book {
+ continue
+ }
+ if strings.TrimSuffix(c.ID, path.Ext(c.ID)) != idBase {
+ continue
+ }
+ obj, ok := b.encodeBirthdayObject(c, yearNum)
+ if !ok {
+ break
+ }
+ obj.Path = objPath
+ return obj, nil
+ }
+ return nil, webdav.NewHTTPError(http.StatusNotFound, fmt.Errorf("birthday event not found"))
+}
+
+// encodeBirthdayObject builds the all-day VEVENT for contact c's birthday
+// occurrence in year, returning ok=false if that (month, day) combination
+// doesn't exist in year (e.g. Feb 29 in a non-leap year).
+func (b *Backend) encodeBirthdayObject(c birthdays.Contact, year int) (*caldav.CalendarObject, bool) {
+ start, ok := birthdays.OccurrenceDate(c, year, time.UTC)
+ if !ok {
+ return nil, false
+ }
+ end := start.AddDate(0, 0, 1)
+
+ event := ical.NewEvent()
+ uid := birthdayObjID(c, year)
+ event.Props.SetText(ical.PropUID, uid)
+ event.Props.SetText(ical.PropSummary, birthdays.Summary(c, year))
+ event.Props.SetDate(ical.PropDateTimeStart, start)
+ event.Props.SetDate(ical.PropDateTimeEnd, end)
+ event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
+ event.Props.SetText(ical.PropTransparency, "TRANSPARENT")
+
+ cal := ical.NewCalendar()
+ cal.Props.SetText(ical.PropVersion, "2.0")
+ cal.Props.SetText(ical.PropProductID, "-//nidus//birthdays//EN")
+ cal.Children = append(cal.Children, event.Component)
+
+ var buf strings.Builder
+ if err := ical.NewEncoder(&buf).Encode(cal); err != nil {
+ return nil, false
+ }
+
+ objID := birthdayObjID(c, year)
+ return &caldav.CalendarObject{
+ Path: calObjectPath(birthdaysCalendarName, objID),
+ ModTime: time.Now(),
+ ContentLength: int64(buf.Len()),
+ ETag: fmt.Sprintf(`"bday-%s-%d"`, strings.TrimSuffix(c.ID, path.Ext(c.ID)), year),
+ Data: cal,
+ }, true
+}
diff --git a/internal/db/db.go b/internal/db/db.go
index 0eb1607..cf05862 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -96,6 +96,15 @@ CREATE TABLE IF NOT EXISTS addressbook_shares (
UNIQUE (owner, addressbook_name, shared_with)
);
+-- Per-user display color for the synthetic "Birthdays" calendar (computed
+-- from contacts' BDAY fields, not a real user-created calendar โ see
+-- internal/web/calendar.go and internal/caldav/backend.go). One row per
+-- user; absent means "use the built-in default color".
+CREATE TABLE IF NOT EXISTS birthday_calendars (
+ owner TEXT PRIMARY KEY REFERENCES users (username) ON DELETE CASCADE,
+ color TEXT NOT NULL DEFAULT ''
+);
+
-- Web UI login sessions. Sessions are opaque random tokens stored server
-- side (not JWTs) so they can be revoked instantly by deleting the row.
CREATE TABLE IF NOT EXISTS web_sessions (
diff --git a/internal/db/users.go b/internal/db/users.go
index b2d5de3..9e4537f 100644
--- a/internal/db/users.go
+++ b/internal/db/users.go
@@ -24,6 +24,19 @@ var ErrResourceExists = errors.New("resource already exists")
// that doesn't exist.
var ErrResourceNotFound = errors.New("resource not found")
+// ErrReservedName is returned when trying to create a calendar whose name
+// is reserved for a computed/virtual calendar (see reservedCalendarNames).
+var ErrReservedName = errors.New("calendar name is reserved")
+
+// reservedCalendarNames are calendar names that can't be used for a real,
+// user-created calendar because they're reserved for a synthetic,
+// computed calendar shown alongside real ones (e.g. "birthdays", see
+// internal/web/calendar.go and internal/caldav/backend.go). Matched
+// case-insensitively.
+var reservedCalendarNames = map[string]bool{
+ "birthdays": true,
+}
+
// User is an account stored in the database.
type User struct {
Username string
@@ -175,8 +188,11 @@ func (d *DB) CreateCalendar(owner, name string) error {
// CreateCalendarWithColor registers a new calendar owned by owner with the
// given color (an empty string means no color is set, in which case the
// client picks its own default). Returns ErrResourceExists if it already
-// exists.
+// exists, or ErrReservedName if name is reserved for a computed calendar.
func (d *DB) CreateCalendarWithColor(owner, name, color string) error {
+ if reservedCalendarNames[strings.ToLower(name)] {
+ return ErrReservedName
+ }
_, err := d.conn.Exec(`INSERT INTO calendars (owner, name, color) VALUES (?, ?, ?)`, owner, name, color)
if err != nil {
if isUniqueConstraintErr(err) {
@@ -254,6 +270,34 @@ func (d *DB) ListCalendars(owner string) ([]Calendar, error) {
return cals, rows.Err()
}
+// GetBirthdayCalendarColor returns owner's display color for the
+// synthetic "Birthdays" calendar, or "" if they haven't set one (callers
+// should fall back to a built-in default in that case).
+func (d *DB) GetBirthdayCalendarColor(owner string) (string, error) {
+ var color string
+ err := d.conn.QueryRow(`SELECT color FROM birthday_calendars WHERE owner = ?`, owner).Scan(&color)
+ if errors.Is(err, sql.ErrNoRows) {
+ return "", nil
+ }
+ if err != nil {
+ return "", fmt.Errorf("getting birthday calendar color: %w", err)
+ }
+ return color, nil
+}
+
+// SetBirthdayCalendarColor sets (or updates) owner's display color for
+// the synthetic "Birthdays" calendar.
+func (d *DB) SetBirthdayCalendarColor(owner, color string) error {
+ _, err := d.conn.Exec(`
+ INSERT INTO birthday_calendars (owner, color) VALUES (?, ?)
+ ON CONFLICT (owner) DO UPDATE SET color = excluded.color`,
+ owner, color)
+ if err != nil {
+ return fmt.Errorf("setting birthday calendar color: %w", err)
+ }
+ return nil
+}
+
// -------- address books --------
// CreateAddressBook registers a new address book owned by owner. Returns
diff --git a/internal/web/calendar.go b/internal/web/calendar.go
index 615e63d..bfadf15 100644
--- a/internal/web/calendar.go
+++ b/internal/web/calendar.go
@@ -16,7 +16,7 @@ import (
"time"
ical "github.com/emersion/go-ical"
- vcard "github.com/emersion/go-vcard"
+ "github.com/yourusername/caldav-server/internal/birthdays"
"github.com/yourusername/caldav-server/internal/db"
"github.com/yourusername/caldav-server/internal/web/templates"
)
@@ -133,9 +133,20 @@ type calendarEntry struct {
// 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"
+// defaultBirthdayColor is the display color for the virtual birthdays
+// calendar used until the user picks their own from the dashboard (a
+// pink, distinct from typical user-picked calendar colors).
+const defaultBirthdayColor = "#ec4899"
+
+// birthdayCalendarColor returns username's chosen color for the virtual
+// birthdays calendar, falling back to defaultBirthdayColor if unset.
+func (s *Server) birthdayCalendarColor(username string) string {
+ color, err := s.dbase.GetBirthdayCalendarColor(username)
+ if err != nil || color == "" {
+ return defaultBirthdayColor
+ }
+ return color
+}
// listCalendarEntries returns every calendar visible to username: the
// synthetic birthdays calendar, their own calendars (always writable),
@@ -143,7 +154,7 @@ const birthdayColor = "#ec4899"
// write permission).
func (s *Server) listCalendarEntries(username string) ([]calendarEntry, error) {
entries := []calendarEntry{
- {Ref: birthdaysCalRef, Owner: username, Name: "Birthdays", Color: birthdayColor, Writable: false, Virtual: true},
+ {Ref: birthdaysCalRef, Owner: username, Name: "Birthdays", Color: s.birthdayCalendarColor(username), Writable: false, Virtual: true},
}
own, err := s.dbase.ListCalendars(username)
@@ -973,100 +984,13 @@ 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)
+ contacts, err := birthdays.Collect(s.store, s.dbase, username)
if err != nil {
return err
}
@@ -1084,11 +1008,8 @@ func (s *Server) addBirthdayEvents(username string, entry calendarEntry, gridSta
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 {
+ occurrence, ok := birthdays.OccurrenceDate(c, year, gridStart.Location())
+ if !ok {
continue
}
if occurrence.Before(gridStart) || occurrence.After(gridEnd) {
@@ -1098,15 +1019,11 @@ func (s *Server) addBirthdayEvents(username string, entry calendarEntry, gridSta
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,
+ Summary: birthdays.Summary(c, year),
AllDay: true,
LinkURL: "/web/contacts/" + c.Book + "/" + c.ID + "/edit",
})
diff --git a/internal/web/dashboard.go b/internal/web/dashboard.go
index 980fd13..a00bb22 100644
--- a/internal/web/dashboard.go
+++ b/internal/web/dashboard.go
@@ -49,7 +49,9 @@ func (s *Server) handleDashboard(w http.ResponseWriter, r *http.Request) {
// #resources list after a create/delete (since the set of cards changes,
// unlike a share update which only changes one card's contents).
func (s *Server) resourceCards(username string) ([]templates.ResourceCard, error) {
- var resources []templates.ResourceCard
+ resources := []templates.ResourceCard{
+ {Kind: "calendar", Name: "Birthdays", Color: s.birthdayCalendarColor(username), Virtual: true},
+ }
calNames, err := s.dbase.ListCalendars(username)
if err != nil {
diff --git a/internal/web/resources.go b/internal/web/resources.go
index f35f829..8fb5155 100644
--- a/internal/web/resources.go
+++ b/internal/web/resources.go
@@ -69,6 +69,37 @@ func (s *Server) handleCalendarColor(w http.ResponseWriter, r *http.Request) {
_ = templates.ResourceCardView(card).Render(r.Context(), w)
}
+// handleBirthdayColor updates the current user's display color for the
+// synthetic "Birthdays" calendar, mounted at /resources/birthdays/color.
+// It re-renders just that card (not the whole list), since the set of
+// cards doesn't change.
+func (s *Server) handleBirthdayColor(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ w.Header().Set("Allow", "POST")
+ http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+ return
+ }
+ username := userFromContext(r.Context())
+ if err := r.ParseForm(); err != nil {
+ http.Error(w, "invalid form", http.StatusBadRequest)
+ return
+ }
+ color := strings.TrimSpace(r.PostForm.Get("color"))
+ if color != "" && !hexColorRe.MatchString(color) {
+ http.Error(w, "color must be a hex value like #3b82f6", http.StatusBadRequest)
+ return
+ }
+ if err := s.dbase.SetBirthdayCalendarColor(username, color); err != nil {
+ s.logger.Error("setting birthday calendar color", "error", err)
+ http.Error(w, "internal error", http.StatusInternalServerError)
+ return
+ }
+
+ card := templates.ResourceCard{Kind: "calendar", Name: "Birthdays", Color: s.birthdayCalendarColor(username), Virtual: true}
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ _ = templates.ResourceCardView(card).Render(r.Context(), w)
+}
+
func (s *Server) handleResource(w http.ResponseWriter, r *http.Request, kind string) {
username := userFromContext(r.Context())
@@ -109,6 +140,10 @@ func (s *Server) handleResource(w http.ResponseWriter, r *http.Request, kind str
http.Error(w, "already exists", http.StatusConflict)
return
}
+ if err == db.ErrReservedName {
+ http.Error(w, "this name is reserved for a computed calendar", http.StatusConflict)
+ return
+ }
s.logger.Error("creating resource", "kind", kind, "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
diff --git a/internal/web/server.go b/internal/web/server.go
index de52186..9eaf37e 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -44,6 +44,7 @@ func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
mux.HandleFunc("/shares/addressbook", s.requireLogin(s.handleAddressBookShare))
mux.HandleFunc("/resources/calendar", s.requireLogin(s.handleCalendarResource))
mux.HandleFunc("/resources/calendar/color", s.requireLogin(s.handleCalendarColor))
+ mux.HandleFunc("/resources/birthdays/color", s.requireLogin(s.handleBirthdayColor))
mux.HandleFunc("/resources/addressbook", s.requireLogin(s.handleAddressBookResource))
mux.HandleFunc("/files/{path...}", s.requireLogin(s.handleFiles))
mux.HandleFunc("/contacts", s.requireLogin(s.handleContactsHome))
diff --git a/internal/web/templates/dashboard.templ b/internal/web/templates/dashboard.templ
index 85e9cbe..bf383a7 100644
--- a/internal/web/templates/dashboard.templ
+++ b/internal/web/templates/dashboard.templ
@@ -11,10 +11,11 @@ type ShareRow struct {
// ResourceCard describes one of the user's own calendars/address books
// plus who it's currently shared with.
type ResourceCard struct {
- Kind string // "calendar" or "addressbook"
- Name string
- Color string // hex color like "#3b82f6"; only used for calendars
- Shares []ShareRow
+ Kind string // "calendar" or "addressbook"
+ Name string
+ Color string // hex color like "#3b82f6"; only used for calendars
+ Shares []ShareRow
+ Virtual bool // true for computed calendars (e.g. birthdays): no delete/sharing, just a color picker
}
// SharedWithMeItem describes a resource another user has shared with the
@@ -115,7 +116,7 @@ templ ResourceCardView(r ResourceCard) {
if r.Kind == "calendar" {
+
+ }
}
+func colorEndpoint(r ResourceCard) string {
+ if r.Virtual {
+ return "/web/resources/birthdays/color"
+ }
+ return "/web/resources/calendar/color"
+}
+
func shareEndpoint(kind string) string {
if kind == "calendar" {
return "/web/shares/calendar"
diff --git a/internal/web/templates/dashboard_templ.go b/internal/web/templates/dashboard_templ.go
index 5a12a0f..537e9d0 100644
--- a/internal/web/templates/dashboard_templ.go
+++ b/internal/web/templates/dashboard_templ.go
@@ -19,10 +19,11 @@ type ShareRow struct {
// ResourceCard describes one of the user's own calendars/address books
// plus who it's currently shared with.
type ResourceCard struct {
- Kind string // "calendar" or "addressbook"
- Name string
- Color string // hex color like "#3b82f6"; only used for calendars
- Shares []ShareRow
+ Kind string // "calendar" or "addressbook"
+ Name string
+ Color string // hex color like "#3b82f6"; only used for calendars
+ Shares []ShareRow
+ Virtual bool // true for computed calendars (e.g. birthdays): no delete/sharing, just a color picker
}
// SharedWithMeItem describes a resource another user has shared with the
@@ -92,7 +93,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Owner)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 85, Col: 45}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 86, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -105,7 +106,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 85, Col: 68}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 86, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -118,7 +119,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Kind)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 86, Col: 47}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 87, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -131,7 +132,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Permission)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 88, Col: 83}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 89, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -233,7 +234,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue("resource-" + r.Kind + "-" + r.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 113, Col: 46}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 114, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -244,245 +245,288 @@ func ResourceCardView(r ResourceCard) templ.Component {
return templ_7745c5c3_Err
}
if r.Kind == "calendar" {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- var templ_7745c5c3_Var13 string
- templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 133, Col: 12}
- }
- _, 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, 19, " ")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
var templ_7745c5c3_Var14 string
- templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Kind)
+ templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(r.Name)
if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 134, Col: 77}
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 134, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- for _, sh := range r.Shares {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "- ")
+ if !r.Virtual {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">Delete")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
- if len(r.Shares) == 0 {
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "- Not shared with anyone yet.
")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
}
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\" hx-target=\"")
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- var templ_7745c5c3_Var25 string
- templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue("#resource-" + r.Kind + "-" + r.Name)
- if templ_7745c5c3_Err != nil {
- return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 175, Col: 51}
- }
- _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
- if templ_7745c5c3_Err != nil {
- return templ_7745c5c3_Err
- }
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" hx-swap=\"outerHTML\">
Share")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -490,6 +534,13 @@ func ResourceCardView(r ResourceCard) templ.Component {
})
}
+func colorEndpoint(r ResourceCard) string {
+ if r.Virtual {
+ return "/web/resources/birthdays/color"
+ }
+ return "/web/resources/calendar/color"
+}
+
func shareEndpoint(kind string) string {
if kind == "calendar" {
return "/web/shares/calendar"