Add ICS/webcal HTTP subscription calendars

Users can now subscribe to a remote ICS/webcal feed from the dashboard
(name + color), the same way they set up a real calendar or the virtual
Birthdays calendar. Subscriptions are read-only, per-user, and share the
"/cal/home/<name>/" namespace with real calendars and "birthdays" (name
collisions are rejected in both directions).

- internal/db: new ics_subscriptions table + CRUD (internal/db/ics.go);
  CreateCalendarWithColor checks for a colliding subscription name.
- internal/icssub: shared HTTP-fetch + TTL cache (15 min) for remote ICS
  calendars, with webcal:// -> https:// rewriting and stale-on-error
  fallback, used by both the web UI and the CalDAV backend.
- internal/web: dashboard "Subscribe to an ICS/webcal calendar" form,
  color picker, delete button (internal/web/ics.go,
  templates/dashboard.templ); month view renders subscription events in
  their chosen color, read-only (internal/web/calendar.go).
- internal/caldav: subscriptions are exposed as read-only calendars
  (internal/caldav/ics.go) - listed in PROPFIND, events served via
  GET/REPORT, PUT/DELETE on individual events rejected with 403, but
  DELETE on the calendar itself unsubscribes; calendar-color is injected
  the same way as for real calendars and Birthdays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-08-20 22:10:49 +02:00
co-authored by Copilot
parent 2cb6e98db6
commit 1707ab4060
12 changed files with 818 additions and 104 deletions
+101 -7
View File
@@ -83,9 +83,10 @@ 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.
if ref == birthdaysCalRef || strings.HasPrefix(ref, icsRefPrefix) {
// Both the birthdays calendar and ICS subscriptions are
// 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 {
@@ -123,7 +124,8 @@ 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
Virtual bool // true for computed calendars (e.g. birthdays) with no backing store objects
ICSURL string // set only for ICS-subscription entries (Ref has icsRefPrefix); the remote URL to fetch events from
}
// birthdaysCalRef is the fixed reference for the synthetic "Birthdays"
@@ -133,6 +135,19 @@ type calendarEntry struct {
// mistaken for an "owner~name" shared-calendar ref either).
const birthdaysCalRef = "@birthdays"
// icsRefPrefix marks a calendarEntry's Ref as referring to one of
// username's own ICS/webcal subscriptions (see internal/db/ics.go). Like
// "@" for the birthdays calendar, "!" isn't in resourceNameRe's character
// class and can't appear in a calRefSep-joined shared-calendar ref either,
// so "!<name>" can't collide with any other kind of ref.
const icsRefPrefix = "!"
// icsCalRef builds the reference string for one of username's own ICS
// subscriptions named name.
func icsCalRef(name string) string {
return icsRefPrefix + name
}
// 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).
@@ -150,8 +165,8 @@ func (s *Server) birthdayCalendarColor(username string) string {
// 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).
// any calendars shared with them (writable only if the share grants write
// permission), and their own read-only ICS/webcal subscriptions.
func (s *Server) listCalendarEntries(username string) ([]calendarEntry, error) {
entries := []calendarEntry{
{Ref: birthdaysCalRef, Owner: username, Name: "Birthdays", Color: s.birthdayCalendarColor(username), Writable: false, Virtual: true},
@@ -185,6 +200,17 @@ func (s *Server) listCalendarEntries(username string) ([]calendarEntry, error) {
})
}
subs, err := s.dbase.ListICSSubscriptions(username)
if err != nil {
return nil, err
}
for _, sub := range subs {
entries = append(entries, calendarEntry{
Ref: icsCalRef(sub.Name), Owner: username, Name: sub.Name, Color: sub.Color,
Writable: false, Virtual: true, ICSURL: sub.URL,
})
}
sort.Slice(entries, func(i, j int) bool {
if entries[i].Owner != entries[j].Owner {
return entries[i].Owner < entries[j].Owner
@@ -283,6 +309,13 @@ func (s *Server) buildMonthView(username string, year int, month time.Month) (te
continue
}
if strings.HasPrefix(entry.Ref, icsRefPrefix) {
if err := s.addICSEvents(entry, gridStart, gridEnd, loc, dayIndex, days); err != nil {
s.logger.Warn("fetching ics subscription events", "calendar", entry.Name, "error", err)
}
continue
}
ids, err := s.store.ListObjects(entry.Owner, "cal-"+entry.Name)
if err != nil {
continue
@@ -766,8 +799,14 @@ func eventFormFromICS(id string, data []byte) (templates.EventFormData, error) {
if len(events) == 0 {
return templates.EventFormData{}, fmt.Errorf("no VEVENT in %s", id)
}
ev := events[0]
return eventFormFromComponent(id, events[0])
}
// eventFormFromComponent extracts an EventFormData from a single decoded
// VEVENT, shared by eventFormFromICS (one event per stored .ics object)
// and the ICS-subscription rendering path (many events per fetched
// calendar, see addICSEvents).
func eventFormFromComponent(id string, ev ical.Event) (templates.EventFormData, error) {
form := templates.EventFormData{ID: id}
if p := ev.Props.Get(ical.PropSummary); p != nil {
form.Summary = p.Value
@@ -1031,3 +1070,58 @@ func (s *Server) addBirthdayEvents(username string, entry calendarEntry, gridSta
}
return nil
}
// addICSEvents fetches entry's remote ICS/webcal calendar (via s.icsCache,
// which caches it for a while so every month-view render doesn't re-fetch
// from origin) and places each VEVENT's occurrence onto the month grid,
// the same way a stored calendar object would be. There's no per-event
// edit page for these (the source is external and read-only), so each
// event's LinkURL is left pointing nowhere useful ("#").
func (s *Server) addICSEvents(entry calendarEntry, gridStart, gridEnd time.Time, loc *time.Location, dayIndex map[string]int, days []templates.MonthDay) error {
cal, err := s.icsCache.Get(entry.ICSURL)
if err != nil {
return err
}
const totalDays = 42
for i, ev := range cal.Events() {
id := fmt.Sprintf("ics-%d", i)
form, err := eventFormFromComponent(id, ev)
if err != nil {
continue
}
startDay, endDay, err := eventDayRange(form, loc)
if err != nil {
continue
}
if endDay.Before(gridStart) || startDay.After(gridEnd) {
continue
}
if startDay.Before(gridStart) {
startDay = gridStart
}
if endDay.After(gridEnd) {
endDay = gridEnd
}
for d, n := startDay, 0; !d.After(endDay) && n < totalDays; d, n = d.AddDate(0, 0, 1), n+1 {
idx, ok := dayIndex[d.Format(dateLayout)]
if !ok {
continue
}
timeText := ""
if !form.AllDay {
timeText = form.StartTime
}
days[idx].Events = append(days[idx].Events, templates.EventSummary{
ID: id,
CalRef: entry.Ref,
Color: entry.Color,
Summary: form.Summary,
TimeText: timeText,
AllDay: form.AllDay,
LinkURL: "#",
})
}
}
return nil
}
+8
View File
@@ -93,6 +93,14 @@ func (s *Server) resourceCards(username string) ([]templates.ResourceCard, error
resources = append(resources, card)
}
subs, err := s.dbase.ListICSSubscriptions(username)
if err != nil {
return nil, fmt.Errorf("listing ics subscriptions: %w", err)
}
for _, sub := range subs {
resources = append(resources, templates.ResourceCard{Kind: "ics", Name: sub.Name, Color: sub.Color, URL: sub.URL})
}
return resources, nil
}
+130
View File
@@ -0,0 +1,130 @@
package web
import (
"context"
"net/http"
"strings"
"github.com/yourusername/caldav-server/internal/db"
"github.com/yourusername/caldav-server/internal/web/templates"
)
// icsURLValid does a light sanity check on a subscription URL: it must be
// http(s):// (fetched directly) or webcal:// (rewritten to https:// by
// internal/icssub before fetching), and within a reasonable length.
func icsURLValid(u string) bool {
if len(u) == 0 || len(u) > 2048 {
return false
}
return strings.HasPrefix(u, "http://") || strings.HasPrefix(u, "https://") || strings.HasPrefix(u, "webcal://")
}
// handleICSResource handles POST (create) and DELETE (remove) for the
// current user's ICS/webcal calendar subscriptions, mounted at
// /resources/ics.
func (s *Server) handleICSResource(w http.ResponseWriter, r *http.Request) {
username := userFromContext(r.Context())
// htmx v2 sends DELETE request parameters as URL query parameters, not
// a request body (see internal/web/resources.go's handleResource).
if r.Method == http.MethodDelete {
r.PostForm = r.URL.Query()
} else if err := r.ParseForm(); err != nil {
http.Error(w, "invalid form", http.StatusBadRequest)
return
}
name := strings.TrimSpace(r.PostForm.Get("name"))
if !resourceNameRe.MatchString(name) {
http.Error(w, "name must be 1-64 letters, digits, '-' or '_'", http.StatusBadRequest)
return
}
switch r.Method {
case http.MethodPost:
url := strings.TrimSpace(r.PostForm.Get("url"))
if !icsURLValid(url) {
http.Error(w, "url must be a http://, https:// or webcal:// address", 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.CreateICSSubscription(username, name, url, color); err != nil {
if err == db.ErrResourceExists {
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 ics subscription", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
case http.MethodDelete:
if err := s.dbase.DeleteICSSubscription(username, name); err != nil && err != db.ErrResourceNotFound {
s.logger.Error("deleting ics subscription", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
default:
w.Header().Set("Allow", "POST, DELETE")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// The set of cards changed (one added/removed), so re-render the
// whole #resources list rather than a single card.
resources, err := s.resourceCards(username)
if err != nil {
s.logger.Error("listing resources", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.ResourceList(resources).Render(context.Background(), w)
}
// handleICSColor updates the color of one of the current user's ICS
// subscriptions, mounted at /resources/ics/color. It re-renders just that
// card (not the whole list), since the set of cards doesn't change.
func (s *Server) handleICSColor(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
}
name := strings.TrimSpace(r.PostForm.Get("name"))
color := strings.TrimSpace(r.PostForm.Get("color"))
if !resourceNameRe.MatchString(name) {
http.Error(w, "name must be 1-64 letters, digits, '-' or '_'", http.StatusBadRequest)
return
}
if color != "" && !hexColorRe.MatchString(color) {
http.Error(w, "color must be a hex value like #3b82f6", http.StatusBadRequest)
return
}
if err := s.dbase.SetICSSubscriptionColor(username, name, color); err != nil && err != db.ErrResourceNotFound {
s.logger.Error("setting ics subscription color", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
sub, err := s.dbase.GetICSSubscription(username, name)
if err != nil {
s.logger.Error("loading ics subscription", "error", err)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
card := templates.ResourceCard{Kind: "ics", Name: sub.Name, Color: sub.Color, URL: sub.URL}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_ = templates.ResourceCardView(card).Render(r.Context(), w)
}
+9 -5
View File
@@ -12,20 +12,22 @@ import (
"github.com/yourusername/caldav-server/internal/config"
"github.com/yourusername/caldav-server/internal/db"
"github.com/yourusername/caldav-server/internal/icssub"
"github.com/yourusername/caldav-server/internal/store"
)
// Server holds the dependencies needed by the web UI handlers.
type Server struct {
cfg *config.Config
store *store.Store
dbase *db.DB
logger *slog.Logger
cfg *config.Config
store *store.Store
dbase *db.DB
logger *slog.Logger
icsCache *icssub.Cache
}
// NewServer constructs a web UI Server.
func NewServer(cfg *config.Config, st *store.Store, dbase *db.DB, logger *slog.Logger) *Server {
return &Server{cfg: cfg, store: st, dbase: dbase, logger: logger}
return &Server{cfg: cfg, store: st, dbase: dbase, logger: logger, icsCache: icssub.NewCache(icssub.DefaultTTL)}
}
// Handler returns the http.Handler serving the web UI, mounted at "/web/"
@@ -46,6 +48,8 @@ func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
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("/resources/ics", s.requireLogin(s.handleICSResource))
mux.HandleFunc("/resources/ics/color", s.requireLogin(s.handleICSColor))
mux.HandleFunc("/files/{path...}", s.requireLogin(s.handleFiles))
mux.HandleFunc("/contacts", s.requireLogin(s.handleContactsHome))
mux.HandleFunc("/contacts/{book}", s.requireLogin(s.handleContactsList))
+47 -6
View File
@@ -11,9 +11,10 @@ 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"
Kind string // "calendar", "addressbook", or "ics" (read-only ICS subscription)
Name string
Color string // hex color like "#3b82f6"; only used for calendars
Color string // hex color like "#3b82f6"; only used for calendars/ics
URL string // remote ICS/webcal URL; only used for kind "ics"
Shares []ShareRow
Virtual bool // true for computed calendars (e.g. birthdays): no delete/sharing, just a color picker
}
@@ -73,6 +74,32 @@ templ Dashboard(username string, resources []ResourceCard, sharedWithMe []Shared
Add
</button>
</form>
<form
class="flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4"
hx-post="/web/resources/ics"
hx-target="#resources"
hx-swap="outerHTML"
hx-on::after-request="if(event.detail.successful) this.reset()"
>
<div class="flex-1">
<label class="block text-xs text-gray-500 mb-1">Subscribe to an ICS/webcal calendar</label>
<input name="name" type="text" required pattern="[a-zA-Z0-9_-]{1,64}"
placeholder="e.g. holidays"
class="w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm mb-2"/>
<input name="url" type="text" required
placeholder="https://example.com/calendar.ics or webcal://..."
class="w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm"/>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Color</label>
<input name="color" type="color" value="#10b981"
class="w-12 h-9 rounded-md border-gray-300 border p-0.5"/>
</div>
<button type="submit"
class="w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
Add
</button>
</form>
</div>
@ResourceList(resources)
@@ -114,7 +141,7 @@ templ ResourceCardView(r ResourceCard) {
<div id={ "resource-" + r.Kind + "-" + r.Name } class="bg-white rounded-lg border border-gray-200 p-5">
<div class="flex items-center justify-between mb-3">
<h2 class="font-medium flex items-center gap-2">
if r.Kind == "calendar" {
if r.Kind == "calendar" || r.Kind == "ics" {
<form
hx-post={ colorEndpoint(r) }
hx-target={ "#resource-" + r.Kind + "-" + r.Name }
@@ -136,6 +163,9 @@ templ ResourceCardView(r ResourceCard) {
if r.Virtual {
<span class="text-xs text-gray-400">(computed from contacts)</span>
}
if r.Kind == "ics" {
<span class="text-xs text-gray-400">(read-only subscription)</span>
}
</h2>
if !r.Virtual {
<button
@@ -151,7 +181,11 @@ templ ResourceCardView(r ResourceCard) {
}
</div>
if !r.Virtual {
if r.Kind == "ics" {
<p class="text-xs text-gray-500 mb-4 break-all">{ r.URL }</p>
}
if !r.Virtual && r.Kind != "ics" {
<ul class="divide-y divide-gray-100 mb-4">
for _, sh := range r.Shares {
<li class="py-2 flex items-center justify-between text-sm">
@@ -208,6 +242,9 @@ func colorEndpoint(r ResourceCard) string {
if r.Virtual {
return "/web/resources/birthdays/color"
}
if r.Kind == "ics" {
return "/web/resources/ics/color"
}
return "/web/resources/calendar/color"
}
@@ -223,10 +260,14 @@ func shareVals(resource, sharedWith string) string {
}
func resourceEndpoint(kind string) string {
if kind == "calendar" {
switch kind {
case "calendar":
return "/web/resources/calendar"
case "ics":
return "/web/resources/ics"
default:
return "/web/resources/addressbook"
}
return "/web/resources/addressbook"
}
func resourceVals(name string) string {
+114 -81
View File
@@ -19,9 +19,10 @@ 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"
Kind string // "calendar", "addressbook", or "ics" (read-only ICS subscription)
Name string
Color string // hex color like "#3b82f6"; only used for calendars
Color string // hex color like "#3b82f6"; only used for calendars/ics
URL string // remote ICS/webcal URL; only used for kind "ics"
Shares []ShareRow
Virtual bool // true for computed calendars (e.g. birthdays): no delete/sharing, just a color picker
}
@@ -68,7 +69,7 @@ func Dashboard(username string, resources []ResourceCard, sharedWithMe []SharedW
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"text-2xl font-semibold mb-6\">Your calendars &amp; address books</h1><div class=\"flex flex-col sm:flex-row gap-4 mb-6\"><form class=\"flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4\" hx-post=\"/web/resources/calendar\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) this.reset()\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">New calendar</label> <input name=\"name\" type=\"text\" required pattern=\"[a-zA-Z0-9_-]{1,64}\" placeholder=\"e.g. work\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><div><label class=\"block text-xs text-gray-500 mb-1\">Color</label> <input name=\"color\" type=\"color\" value=\"#3b82f6\" class=\"w-12 h-9 rounded-md border-gray-300 border p-0.5\"></div><button type=\"submit\" class=\"w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Add</button></form><form class=\"flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4\" hx-post=\"/web/resources/addressbook\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) this.reset()\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">New address book</label> <input name=\"name\" type=\"text\" required pattern=\"[a-zA-Z0-9_-]{1,64}\" placeholder=\"e.g. contacts\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><button type=\"submit\" class=\"w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Add</button></form></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"text-2xl font-semibold mb-6\">Your calendars &amp; address books</h1><div class=\"flex flex-col sm:flex-row gap-4 mb-6\"><form class=\"flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4\" hx-post=\"/web/resources/calendar\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) this.reset()\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">New calendar</label> <input name=\"name\" type=\"text\" required pattern=\"[a-zA-Z0-9_-]{1,64}\" placeholder=\"e.g. work\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><div><label class=\"block text-xs text-gray-500 mb-1\">Color</label> <input name=\"color\" type=\"color\" value=\"#3b82f6\" class=\"w-12 h-9 rounded-md border-gray-300 border p-0.5\"></div><button type=\"submit\" class=\"w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Add</button></form><form class=\"flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4\" hx-post=\"/web/resources/addressbook\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) this.reset()\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">New address book</label> <input name=\"name\" type=\"text\" required pattern=\"[a-zA-Z0-9_-]{1,64}\" placeholder=\"e.g. contacts\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><button type=\"submit\" class=\"w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Add</button></form><form class=\"flex flex-col sm:flex-row sm:items-end gap-2 bg-white rounded-lg border border-gray-200 p-4\" hx-post=\"/web/resources/ics\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-on::after-request=\"if(event.detail.successful) this.reset()\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">Subscribe to an ICS/webcal calendar</label> <input name=\"name\" type=\"text\" required pattern=\"[a-zA-Z0-9_-]{1,64}\" placeholder=\"e.g. holidays\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm mb-2\"> <input name=\"url\" type=\"text\" required placeholder=\"https://example.com/calendar.ics or webcal://...\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><div><label class=\"block text-xs text-gray-500 mb-1\">Color</label> <input name=\"color\" type=\"color\" value=\"#10b981\" class=\"w-12 h-9 rounded-md border-gray-300 border p-0.5\"></div><button type=\"submit\" class=\"w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Add</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -93,7 +94,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: 86, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 113, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
@@ -106,7 +107,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: 86, Col: 68}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 113, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -119,7 +120,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: 87, Col: 47}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 114, Col: 47}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
@@ -132,7 +133,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: 89, Col: 83}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 116, Col: 83}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -234,7 +235,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: 114, Col: 46}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 141, Col: 46}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
if templ_7745c5c3_Err != nil {
@@ -244,7 +245,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if r.Kind == "calendar" {
if r.Kind == "calendar" || r.Kind == "ics" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<form hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
@@ -252,7 +253,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(colorEndpoint(r))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 119, Col: 32}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 146, Col: 32}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil {
@@ -265,7 +266,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, 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: 120, Col: 54}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 147, Col: 54}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil {
@@ -278,7 +279,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 124, Col: 53}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 151, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil {
@@ -291,7 +292,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(colorOrDefault(r.Color))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 128, Col: 38}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 155, Col: 38}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
if templ_7745c5c3_Err != nil {
@@ -305,7 +306,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
var templ_7745c5c3_Var14 string
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: 12}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 161, Col: 12}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
@@ -318,7 +319,7 @@ func ResourceCardView(r ResourceCard) templ.Component {
var templ_7745c5c3_Var15 string
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(r.Kind)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 135, Col: 77}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 162, Col: 77}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
if templ_7745c5c3_Err != nil {
@@ -329,204 +330,229 @@ func ResourceCardView(r ResourceCard) templ.Component {
return templ_7745c5c3_Err
}
if r.Virtual {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"text-xs text-gray-400\">(computed from contacts)</span>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span class=\"text-xs text-gray-400\">(computed from contacts)</span> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</h2>")
if r.Kind == "ics" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"text-xs text-gray-400\">(read-only subscription)</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !r.Virtual {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<button class=\"text-red-600 hover:underline text-xs\" hx-delete=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<button class=\"text-red-600 hover:underline text-xs\" hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(resourceEndpoint(r.Kind))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 143, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 173, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" hx-vals=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" hx-vals=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(resourceVals(r.Name))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 144, Col: 35}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 174, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-confirm=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" hx-target=\"#resources\" hx-swap=\"outerHTML\" hx-confirm=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var18 string
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("Delete " + r.Kind + " " + r.Name + "? This removes all its data and cannot be undone.")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 147, Col: 105}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 177, Col: 105}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\">Delete</button>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\">Delete</button>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !r.Virtual {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<ul class=\"divide-y divide-gray-100 mb-4\">")
if r.Kind == "ics" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<p class=\"text-xs text-gray-500 mb-4 break-all\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(r.URL)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 185, Col: 58}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if !r.Virtual && r.Kind != "ics" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<ul class=\"divide-y divide-gray-100 mb-4\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, sh := range r.Shares {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<li class=\"py-2 flex items-center justify-between text-sm\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(sh.SharedWith)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 158, Col: 27}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</span> <span class=\"flex items-center gap-3\"><span class=\"text-xs uppercase tracking-wide text-gray-500\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<li class=\"py-2 flex items-center justify-between text-sm\"><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(sh.Permission)
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(sh.SharedWith)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 160, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 192, Col: 27}
}
_, 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, 32, "</span> <button class=\"text-red-600 hover:underline text-xs\" hx-delete=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span> <span class=\"flex items-center gap-3\"><span class=\"text-xs uppercase tracking-wide text-gray-500\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 string
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(sh.Permission)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 163, Col: 41}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 194, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
_, 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, 33, "\" hx-vals=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span> <button class=\"text-red-600 hover:underline text-xs\" hx-delete=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareVals(r.Name, sh.SharedWith))
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 164, Col: 50}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 197, Col: 41}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" hx-target=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\" hx-vals=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("#resource-" + r.Kind + "-" + r.Name)
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareVals(r.Name, sh.SharedWith))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 165, Col: 56}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 198, Col: 50}
}
_, 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, 35, "\" hx-swap=\"outerHTML\" hx-confirm=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove access for " + sh.SharedWith + "?")
templ_7745c5c3_Var24, 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: 167, Col: 63}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 199, Col: 56}
}
_, 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, 36, "\">Remove</button></span></li>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "\" hx-swap=\"outerHTML\" hx-confirm=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue("Remove access for " + sh.SharedWith + "?")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 201, Col: 63}
}
_, 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, 39, "\">Remove</button></span></li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if len(r.Shares) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<li class=\"py-2 text-sm text-gray-400\">Not shared with anyone yet.</li>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<li class=\"py-2 text-sm text-gray-400\">Not shared with anyone yet.</li>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</ul><form class=\"flex flex-col sm:flex-row sm:items-end gap-2\" hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 181, Col: 35}
}
_, 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, 39, "\" hx-target=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</ul><form class=\"flex flex-col sm:flex-row sm:items-end gap-2\" hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue("#resource-" + r.Kind + "-" + r.Name)
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(shareEndpoint(r.Kind))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 182, Col: 52}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 215, Col: 35}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" hx-swap=\"outerHTML\"><input type=\"hidden\" name=\"resource\" value=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name)
templ_7745c5c3_Var27, 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: 185, Col: 55}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 216, Col: 52}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">Username</label> <input name=\"shared_with\" type=\"text\" required class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><div><label class=\"block text-xs text-gray-500 mb-1\">Permission</label> <select name=\"permission\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"><option value=\"read\">read</option> <option value=\"write\">write</option></select></div><button type=\"submit\" class=\"w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Share</button></form>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" hx-swap=\"outerHTML\"><input type=\"hidden\" name=\"resource\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/dashboard.templ`, Line: 219, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><div class=\"flex-1\"><label class=\"block text-xs text-gray-500 mb-1\">Username</label> <input name=\"shared_with\" type=\"text\" required class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"></div><div><label class=\"block text-xs text-gray-500 mb-1\">Permission</label> <select name=\"permission\" class=\"w-full rounded-md border-gray-300 border px-2 py-1.5 text-sm\"><option value=\"read\">read</option> <option value=\"write\">write</option></select></div><button type=\"submit\" class=\"w-full sm:w-auto bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700\">Share</button></form>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -538,6 +564,9 @@ func colorEndpoint(r ResourceCard) string {
if r.Virtual {
return "/web/resources/birthdays/color"
}
if r.Kind == "ics" {
return "/web/resources/ics/color"
}
return "/web/resources/calendar/color"
}
@@ -553,10 +582,14 @@ func shareVals(resource, sharedWith string) string {
}
func resourceEndpoint(kind string) string {
if kind == "calendar" {
switch kind {
case "calendar":
return "/web/resources/calendar"
case "ics":
return "/web/resources/ics"
default:
return "/web/resources/addressbook"
}
return "/web/resources/addressbook"
}
func resourceVals(name string) string {