Add web UI for CalDAV calendars (web/calendar)
- List of the user's own calendars (web/calendar) - Month view per calendar with a Monday-first 6-week grid, showing all-day and timed events, colored dot matching the calendar's color - Navigation between months (prev/next/today) via query params - Create/edit/delete events: title, description, location, all-day toggle, start/end date+time - Clicking a day cell prefills a new all-day event on that date; the "New event" button defaults to a one-hour slot starting next full hour - Deleting an event or invalid time ranges (end before/equal start) are validated with inline error messages - Verified round-trip with the real CalDAV protocol handler (events created via the web UI are correctly visible to a REPORT query) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,628 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
ical "github.com/emersion/go-ical"
|
||||
"github.com/yourusername/caldav-server/internal/web/templates"
|
||||
)
|
||||
|
||||
// eventIDRe validates an event's object ID as it appears in a URL path
|
||||
// segment: a filename like "3f9a2b8c1d4e5f60ab12cd34ef56ab78.ics".
|
||||
var eventIDRe = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,128}\.ics$`)
|
||||
|
||||
// dateLayout/timeLayout are the HTML date/time input formats used
|
||||
// throughout the event form.
|
||||
const dateLayout = "2006-01-02"
|
||||
const timeLayout = "15:04"
|
||||
|
||||
// newEventID generates a random filename for a new event object,
|
||||
// mirroring the "<id>.ics" convention used by the CalDAV backend
|
||||
// (internal/caldav).
|
||||
func newEventID() (string, error) {
|
||||
buf := make([]byte, 16)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(buf) + ".ics", nil
|
||||
}
|
||||
|
||||
// ownsCalendar reports whether cal is one of username's own calendars
|
||||
// (never one shared with them by another user — the web calendar UI only
|
||||
// manages a user's own calendars, consistent with contacts/files).
|
||||
func (s *Server) ownsCalendar(username, cal string) (bool, error) {
|
||||
if !resourceNameRe.MatchString(cal) {
|
||||
return false, nil
|
||||
}
|
||||
cals, err := s.dbase.ListCalendars(username)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, c := range cals {
|
||||
if c.Name == cal {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleCalendarsHome(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", "GET")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
username := userFromContext(r.Context())
|
||||
cals, err := s.dbase.ListCalendars(username)
|
||||
if err != nil {
|
||||
s.logger.Error("listing calendars", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sort.Slice(cals, func(i, j int) bool { return cals[i].Name < cals[j].Name })
|
||||
|
||||
var summaries []templates.CalendarSummary
|
||||
for _, c := range cals {
|
||||
summaries = append(summaries, templates.CalendarSummary{Name: c.Name, Color: c.Color})
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.CalendarsHome(username, summaries).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
func (s *Server) handleCalendarMonth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
w.Header().Set("Allow", "GET")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
username := userFromContext(r.Context())
|
||||
cal := r.PathValue("cal")
|
||||
ok, err := s.ownsCalendar(username, cal)
|
||||
if err != nil {
|
||||
s.logger.Error("checking calendar ownership", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
year, month := parseYearMonth(r)
|
||||
color, _ := s.dbase.GetCalendarColor(username, cal)
|
||||
|
||||
data, err := s.buildMonthView(username, cal, color, year, month)
|
||||
if err != nil {
|
||||
s.logger.Error("building month view", "calendar", cal, "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.MonthView(username, data).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
// parseYearMonth reads ?year=&month= query params, defaulting to the
|
||||
// current month if absent or invalid.
|
||||
func parseYearMonth(r *http.Request) (year int, month time.Month) {
|
||||
now := time.Now()
|
||||
year, month = now.Year(), now.Month()
|
||||
if y, err := strconv.Atoi(r.URL.Query().Get("year")); err == nil && y >= 1 && y <= 9999 {
|
||||
year = y
|
||||
}
|
||||
if m, err := strconv.Atoi(r.URL.Query().Get("month")); err == nil && m >= 1 && m <= 12 {
|
||||
month = time.Month(m)
|
||||
}
|
||||
return year, month
|
||||
}
|
||||
|
||||
// buildMonthView loads every event in cal, then places each occurrence's
|
||||
// days onto a 6-week grid covering the requested month (plus enough
|
||||
// leading/trailing days of neighboring months to fill full weeks).
|
||||
// Recurring events (RRULE) are not expanded — only an event's own
|
||||
// DTSTART/DTEND span is considered.
|
||||
func (s *Server) buildMonthView(username, cal, color string, year int, month time.Month) (templates.MonthViewData, error) {
|
||||
loc := time.Local
|
||||
first := time.Date(year, month, 1, 0, 0, 0, 0, loc)
|
||||
// Monday-first offset: time.Weekday has Sunday=0..Saturday=6.
|
||||
offset := (int(first.Weekday()) + 6) % 7
|
||||
gridStart := first.AddDate(0, 0, -offset)
|
||||
const totalDays = 42 // 6 weeks
|
||||
|
||||
today := time.Now().In(loc).Format(dateLayout)
|
||||
|
||||
days := make([]templates.MonthDay, totalDays)
|
||||
dayIndex := make(map[string]int, totalDays)
|
||||
for i := 0; i < totalDays; i++ {
|
||||
d := gridStart.AddDate(0, 0, i)
|
||||
dateStr := d.Format(dateLayout)
|
||||
days[i] = templates.MonthDay{
|
||||
Date: dateStr,
|
||||
Day: d.Day(),
|
||||
InMonth: d.Month() == month,
|
||||
IsToday: dateStr == today,
|
||||
}
|
||||
dayIndex[dateStr] = i
|
||||
}
|
||||
gridEnd := gridStart.AddDate(0, 0, totalDays-1)
|
||||
|
||||
ids, err := s.store.ListObjects(username, "cal-"+cal)
|
||||
if err != nil {
|
||||
return templates.MonthViewData{}, err
|
||||
}
|
||||
for _, id := range ids {
|
||||
data, err := s.store.GetObject(username, "cal-"+cal, id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
form, err := eventFormFromICS(id, data)
|
||||
if err != nil {
|
||||
s.logger.Warn("decoding calendar object", "calendar", cal, "id", id, "error", err)
|
||||
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
|
||||
}
|
||||
// Cap the number of days a single event can add to the grid, in
|
||||
// case of malformed data with a wildly distant DTEND.
|
||||
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,
|
||||
Summary: form.Summary,
|
||||
TimeText: timeText,
|
||||
AllDay: form.AllDay,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Keep each day's events in a stable, readable order: all-day events
|
||||
// first, then timed events sorted by start time.
|
||||
for i := range days {
|
||||
evs := days[i].Events
|
||||
sort.SliceStable(evs, func(a, b int) bool {
|
||||
if evs[a].AllDay != evs[b].AllDay {
|
||||
return evs[a].AllDay
|
||||
}
|
||||
return evs[a].TimeText < evs[b].TimeText
|
||||
})
|
||||
}
|
||||
|
||||
weeks := make([][]templates.MonthDay, 0, 6)
|
||||
for i := 0; i < totalDays; i += 7 {
|
||||
weeks = append(weeks, days[i:i+7])
|
||||
}
|
||||
|
||||
prevMonth := first.AddDate(0, -1, 0)
|
||||
nextMonth := first.AddDate(0, 1, 0)
|
||||
monthURL := func(y int, m time.Month) string {
|
||||
return fmt.Sprintf("/web/calendar/%s?year=%d&month=%d", cal, y, int(m))
|
||||
}
|
||||
|
||||
return templates.MonthViewData{
|
||||
Cal: cal,
|
||||
Color: color,
|
||||
MonthLabel: first.Format("January 2006"),
|
||||
Weeks: weeks,
|
||||
PrevMonthURL: monthURL(prevMonth.Year(), prevMonth.Month()),
|
||||
NextMonthURL: monthURL(nextMonth.Year(), nextMonth.Month()),
|
||||
TodayURL: "/web/calendar/" + cal,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// eventDayRange returns the inclusive [start, end] calendar-day span an
|
||||
// event occupies, in loc, for placing it on the month grid.
|
||||
func eventDayRange(form templates.EventFormData, loc *time.Location) (start, end time.Time, err error) {
|
||||
start, err = time.ParseInLocation(dateLayout, form.StartDate, loc)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
endDate := form.EndDate
|
||||
if endDate == "" {
|
||||
endDate = form.StartDate
|
||||
}
|
||||
end, err = time.ParseInLocation(dateLayout, endDate, loc)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, err
|
||||
}
|
||||
if end.Before(start) {
|
||||
end = start
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleEventNew(w http.ResponseWriter, r *http.Request) {
|
||||
username := userFromContext(r.Context())
|
||||
cal := r.PathValue("cal")
|
||||
ok, err := s.ownsCalendar(username, cal)
|
||||
if err != nil {
|
||||
s.logger.Error("checking calendar ownership", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
form := newEventFormDefaults(cal, r.URL.Query().Get("date"))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.EventForm(username, form, "").Render(context.Background(), w)
|
||||
case http.MethodPost:
|
||||
s.saveEventFromForm(w, r, username, cal, "")
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, POST")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
// newEventFormDefaults builds the initial form values for a brand new
|
||||
// event. If dateParam is a valid "YYYY-MM-DD" (e.g. from clicking a day
|
||||
// cell in the month grid), the event defaults to an all-day event on that
|
||||
// date; otherwise it defaults to a one-hour timed event starting at the
|
||||
// next full hour today.
|
||||
func newEventFormDefaults(cal, dateParam string) templates.EventFormData {
|
||||
if _, err := time.Parse(dateLayout, dateParam); err == nil {
|
||||
return templates.EventFormData{Cal: cal, AllDay: true, StartDate: dateParam, EndDate: dateParam}
|
||||
}
|
||||
now := time.Now()
|
||||
start := now.Truncate(time.Hour).Add(time.Hour)
|
||||
end := start.Add(time.Hour)
|
||||
return templates.EventFormData{
|
||||
Cal: cal,
|
||||
StartDate: start.Format(dateLayout),
|
||||
StartTime: start.Format(timeLayout),
|
||||
EndDate: end.Format(dateLayout),
|
||||
EndTime: end.Format(timeLayout),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleEventEdit(w http.ResponseWriter, r *http.Request) {
|
||||
username := userFromContext(r.Context())
|
||||
cal := r.PathValue("cal")
|
||||
id := r.PathValue("id")
|
||||
ok, err := s.ownsCalendar(username, cal)
|
||||
if err != nil {
|
||||
s.logger.Error("checking calendar ownership", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok || !eventIDRe.MatchString(id) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
data, err := s.store.GetObject(username, "cal-"+cal, id)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
form, err := eventFormFromICS(id, data)
|
||||
if err != nil {
|
||||
s.logger.Error("decoding event", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
form.Cal = cal
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.EventForm(username, form, "").Render(context.Background(), w)
|
||||
case http.MethodPost:
|
||||
s.saveEventFromForm(w, r, username, cal, id)
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, POST")
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleEventDelete(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())
|
||||
cal := r.PathValue("cal")
|
||||
id := r.PathValue("id")
|
||||
ok, err := s.ownsCalendar(username, cal)
|
||||
if err != nil {
|
||||
s.logger.Error("checking calendar ownership", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if !ok || !eventIDRe.MatchString(id) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteObject(username, "cal-"+cal, id); err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, "/web/calendar/"+cal, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// eventFormInput holds the parsed, not-yet-validated values submitted by
|
||||
// the event form.
|
||||
type eventFormInput struct {
|
||||
Summary string
|
||||
Description string
|
||||
Location string
|
||||
AllDay bool
|
||||
StartDate string
|
||||
StartTime string
|
||||
EndDate string
|
||||
EndTime string
|
||||
}
|
||||
|
||||
func parseEventForm(r *http.Request) eventFormInput {
|
||||
return eventFormInput{
|
||||
Summary: strings.TrimSpace(r.PostForm.Get("summary")),
|
||||
Description: strings.TrimSpace(r.PostForm.Get("description")),
|
||||
Location: strings.TrimSpace(r.PostForm.Get("location")),
|
||||
AllDay: r.PostForm.Get("all_day") != "",
|
||||
StartDate: strings.TrimSpace(r.PostForm.Get("start_date")),
|
||||
StartTime: strings.TrimSpace(r.PostForm.Get("start_time")),
|
||||
EndDate: strings.TrimSpace(r.PostForm.Get("end_date")),
|
||||
EndTime: strings.TrimSpace(r.PostForm.Get("end_time")),
|
||||
}
|
||||
}
|
||||
|
||||
// saveEventFromForm handles both event creation (id == "") and editing
|
||||
// (id preserves the existing event's UID).
|
||||
func (s *Server) saveEventFromForm(w http.ResponseWriter, r *http.Request, username, cal, id string) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
http.Error(w, "invalid form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
in := parseEventForm(r)
|
||||
|
||||
reRender := func(errMsg string) {
|
||||
form := templates.EventFormData{
|
||||
Cal: cal, ID: id, Summary: in.Summary, Description: in.Description, Location: in.Location,
|
||||
AllDay: in.AllDay, StartDate: in.StartDate, StartTime: in.StartTime, EndDate: in.EndDate, EndTime: in.EndTime,
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_ = templates.EventForm(username, form, errMsg).Render(context.Background(), w)
|
||||
}
|
||||
|
||||
if in.Summary == "" {
|
||||
reRender("Title is required")
|
||||
return
|
||||
}
|
||||
|
||||
start, end, err := computeEventTimes(in)
|
||||
if err != nil {
|
||||
reRender(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
var uid string
|
||||
if id == "" {
|
||||
newID, err := newEventID()
|
||||
if err != nil {
|
||||
s.logger.Error("generating event id", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
id = newID
|
||||
uid = strings.TrimSuffix(id, ".ics")
|
||||
} else {
|
||||
data, err := s.store.GetObject(username, "cal-"+cal, id)
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
existing, err := ical.NewDecoder(strings.NewReader(string(data))).Decode()
|
||||
if err != nil {
|
||||
s.logger.Error("decoding existing event", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if events := existing.Events(); len(events) > 0 {
|
||||
if uidProp := events[0].Props.Get(ical.PropUID); uidProp != nil {
|
||||
uid = uidProp.Value
|
||||
}
|
||||
}
|
||||
if uid == "" {
|
||||
uid = strings.TrimSuffix(id, ".ics")
|
||||
}
|
||||
}
|
||||
|
||||
newCal := buildEventCalendar(uid, in, start, end)
|
||||
if err := s.saveEvent(username, cal, id, newCal); err != nil {
|
||||
s.logger.Error("saving event", "error", err)
|
||||
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/web/calendar/"+cal, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// computeEventTimes validates and converts the submitted form fields into
|
||||
// concrete start/end instants (for timed events) or dates (for all-day
|
||||
// events, where end is returned as the exclusive DTEND day).
|
||||
func computeEventTimes(in eventFormInput) (start, end time.Time, err error) {
|
||||
if in.StartDate == "" {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("start date is required")
|
||||
}
|
||||
endDate := in.EndDate
|
||||
if endDate == "" {
|
||||
endDate = in.StartDate
|
||||
}
|
||||
|
||||
if in.AllDay {
|
||||
start, err = time.ParseInLocation(dateLayout, in.StartDate, time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid start date")
|
||||
}
|
||||
end, err = time.ParseInLocation(dateLayout, endDate, time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid end date")
|
||||
}
|
||||
if end.Before(start) {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("end date must not be before start date")
|
||||
}
|
||||
// DTEND for all-day events is exclusive: the end date the user
|
||||
// picks is the last day the event covers, so add one day.
|
||||
end = end.AddDate(0, 0, 1)
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
if in.StartTime == "" || in.EndTime == "" {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("start and end time are required for timed events")
|
||||
}
|
||||
start, err = time.ParseInLocation(dateLayout+"T"+timeLayout, in.StartDate+"T"+in.StartTime, time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid start date/time")
|
||||
}
|
||||
end, err = time.ParseInLocation(dateLayout+"T"+timeLayout, endDate+"T"+in.EndTime, time.Local)
|
||||
if err != nil {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("invalid end date/time")
|
||||
}
|
||||
if !end.After(start) {
|
||||
return time.Time{}, time.Time{}, fmt.Errorf("end must be after start")
|
||||
}
|
||||
return start, end, nil
|
||||
}
|
||||
|
||||
// buildEventCalendar builds a fresh VCALENDAR/VEVENT from the submitted
|
||||
// form input and resolved start/end instants.
|
||||
func buildEventCalendar(uid string, in eventFormInput, start, end time.Time) *ical.Calendar {
|
||||
cal := ical.NewCalendar()
|
||||
cal.Props.SetText(ical.PropVersion, "2.0")
|
||||
cal.Props.SetText(ical.PropProductID, "-//nidus//web calendar//EN")
|
||||
|
||||
ev := ical.NewEvent()
|
||||
ev.Props.SetText(ical.PropUID, uid)
|
||||
ev.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
|
||||
ev.Props.SetText(ical.PropSummary, in.Summary)
|
||||
if in.Description != "" {
|
||||
ev.Props.SetText(ical.PropDescription, in.Description)
|
||||
}
|
||||
if in.Location != "" {
|
||||
ev.Props.SetText(ical.PropLocation, in.Location)
|
||||
}
|
||||
|
||||
if in.AllDay {
|
||||
ev.Props.SetDate(ical.PropDateTimeStart, start)
|
||||
ev.Props.SetDate(ical.PropDateTimeEnd, end)
|
||||
} else {
|
||||
ev.Props.SetDateTime(ical.PropDateTimeStart, start.UTC())
|
||||
ev.Props.SetDateTime(ical.PropDateTimeEnd, end.UTC())
|
||||
}
|
||||
|
||||
cal.Children = append(cal.Children, ev.Component)
|
||||
return cal
|
||||
}
|
||||
|
||||
func (s *Server) saveEvent(username, cal, id string, calendar *ical.Calendar) error {
|
||||
var buf strings.Builder
|
||||
enc := ical.NewEncoder(&buf)
|
||||
if err := enc.Encode(calendar); err != nil {
|
||||
return fmt.Errorf("encoding ical: %w", err)
|
||||
}
|
||||
return s.store.PutObject(username, "cal-"+cal, id, []byte(buf.String()))
|
||||
}
|
||||
|
||||
// eventFormFromICS decodes a single-VEVENT .ics object into the shared
|
||||
// EventFormData shape used both for pre-filling the edit form and for
|
||||
// placing the event on the month grid.
|
||||
func eventFormFromICS(id string, data []byte) (templates.EventFormData, error) {
|
||||
calendar, err := ical.NewDecoder(strings.NewReader(string(data))).Decode()
|
||||
if err != nil {
|
||||
return templates.EventFormData{}, err
|
||||
}
|
||||
events := calendar.Events()
|
||||
if len(events) == 0 {
|
||||
return templates.EventFormData{}, fmt.Errorf("no VEVENT in %s", id)
|
||||
}
|
||||
ev := events[0]
|
||||
|
||||
form := templates.EventFormData{ID: id}
|
||||
if p := ev.Props.Get(ical.PropSummary); p != nil {
|
||||
form.Summary = p.Value
|
||||
}
|
||||
if p := ev.Props.Get(ical.PropDescription); p != nil {
|
||||
form.Description = p.Value
|
||||
}
|
||||
if p := ev.Props.Get(ical.PropLocation); p != nil {
|
||||
form.Location = p.Value
|
||||
}
|
||||
|
||||
startProp := ev.Props.Get(ical.PropDateTimeStart)
|
||||
endProp := ev.Props.Get(ical.PropDateTimeEnd)
|
||||
if startProp == nil {
|
||||
return templates.EventFormData{}, fmt.Errorf("missing DTSTART in %s", id)
|
||||
}
|
||||
|
||||
allDay := startProp.ValueType() == ical.ValueDate || len(startProp.Value) == 8 // RFC 5545 DATE value, e.g. "20060102"
|
||||
form.AllDay = allDay
|
||||
|
||||
if allDay {
|
||||
start, err := startProp.DateTime(time.Local)
|
||||
if err != nil {
|
||||
return templates.EventFormData{}, err
|
||||
}
|
||||
form.StartDate = start.Format(dateLayout)
|
||||
end := start.AddDate(0, 0, 1)
|
||||
if endProp != nil {
|
||||
if e, err := endProp.DateTime(time.Local); err == nil {
|
||||
end = e
|
||||
}
|
||||
}
|
||||
// Displayed end date is the last inclusive day (DTEND is exclusive).
|
||||
form.EndDate = end.AddDate(0, 0, -1).Format(dateLayout)
|
||||
} else {
|
||||
// go-ical's Prop.DateTime always parses a "Z"-suffixed UTC value
|
||||
// as UTC regardless of the loc argument passed in, so the
|
||||
// returned time must be explicitly converted to the local zone
|
||||
// before formatting for display.
|
||||
start, err := startProp.DateTime(time.Local)
|
||||
if err != nil {
|
||||
return templates.EventFormData{}, err
|
||||
}
|
||||
start = start.In(time.Local)
|
||||
form.StartDate = start.Format(dateLayout)
|
||||
form.StartTime = start.Format(timeLayout)
|
||||
end := start.Add(time.Hour)
|
||||
if endProp != nil {
|
||||
if e, err := endProp.DateTime(time.Local); err == nil {
|
||||
end = e.In(time.Local)
|
||||
}
|
||||
}
|
||||
form.EndDate = end.Format(dateLayout)
|
||||
form.EndTime = end.Format(timeLayout)
|
||||
}
|
||||
|
||||
return form, nil
|
||||
}
|
||||
@@ -54,6 +54,11 @@ func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
|
||||
mux.HandleFunc("/contacts/{book}/{id}/edit", s.requireLogin(s.handleContactEdit))
|
||||
mux.HandleFunc("/contacts/{book}/{id}/delete", s.requireLogin(s.handleContactDelete))
|
||||
mux.HandleFunc("/contacts/{book}/{id}/export", s.requireLogin(s.handleContactExportOne))
|
||||
mux.HandleFunc("/calendar", s.requireLogin(s.handleCalendarsHome))
|
||||
mux.HandleFunc("/calendar/{cal}", s.requireLogin(s.handleCalendarMonth))
|
||||
mux.HandleFunc("/calendar/{cal}/new", s.requireLogin(s.handleEventNew))
|
||||
mux.HandleFunc("/calendar/{cal}/{id}/edit", s.requireLogin(s.handleEventEdit))
|
||||
mux.HandleFunc("/calendar/{cal}/{id}/delete", s.requireLogin(s.handleEventDelete))
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package templates
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CalendarSummary is one of the user's own calendars, listed on the
|
||||
// calendar home page.
|
||||
type CalendarSummary struct {
|
||||
Name string
|
||||
Color string // hex color like "#3b82f6", "" if unset
|
||||
}
|
||||
|
||||
// EventSummary is a single event shown inside a month-view day cell.
|
||||
type EventSummary struct {
|
||||
ID string
|
||||
Summary string
|
||||
TimeText string // e.g. "14:00" or "" for all-day events
|
||||
AllDay bool
|
||||
}
|
||||
|
||||
// MonthDay is one day cell in the month grid.
|
||||
type MonthDay struct {
|
||||
Date string // "YYYY-MM-DD", used for links and the "new event" date
|
||||
Day int // day-of-month number shown in the cell
|
||||
InMonth bool // false for the leading/trailing days of neighboring months
|
||||
IsToday bool
|
||||
Events []EventSummary
|
||||
}
|
||||
|
||||
// MonthViewData is everything the month grid template needs to render one
|
||||
// month of a single calendar.
|
||||
type MonthViewData struct {
|
||||
Cal string
|
||||
Color string
|
||||
MonthLabel string // e.g. "August 2026"
|
||||
Weeks [][]MonthDay
|
||||
PrevMonthURL string
|
||||
NextMonthURL string
|
||||
TodayURL string
|
||||
}
|
||||
|
||||
// EventFormData pre-fills the create/edit event form.
|
||||
type EventFormData struct {
|
||||
Cal string
|
||||
ID string // empty when creating a new event
|
||||
Summary string
|
||||
Description string
|
||||
Location string
|
||||
AllDay bool
|
||||
StartDate string // "YYYY-MM-DD"
|
||||
StartTime string // "HH:MM", empty when AllDay
|
||||
EndDate string // "YYYY-MM-DD"
|
||||
EndTime string // "HH:MM", empty when AllDay
|
||||
}
|
||||
|
||||
templ CalendarsHome(username string, calendars []CalendarSummary) {
|
||||
@Layout("Calendar", username) {
|
||||
<h1 class="text-2xl font-semibold mb-6">Calendar</h1>
|
||||
if len(calendars) == 0 {
|
||||
<p class="text-sm text-gray-500">
|
||||
You don't have any calendars yet — create one from the
|
||||
<a href="/web/" class="text-indigo-600 hover:underline">dashboard</a> first.
|
||||
</p>
|
||||
} else {
|
||||
<ul class="divide-y divide-gray-200 bg-white rounded-lg border border-gray-200">
|
||||
for _, c := range calendars {
|
||||
<li class="px-4 py-3 flex items-center gap-3 text-sm">
|
||||
<span class="w-3 h-3 rounded-full shrink-0" style={ "background-color: " + colorOrDefault(c.Color) }></span>
|
||||
<a href={ templ.URL("/web/calendar/" + c.Name) } class="font-medium text-indigo-600 hover:underline">{ c.Name }</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
templ MonthView(username string, data MonthViewData) {
|
||||
@Layout("Calendar", username) {
|
||||
<div class="flex items-center justify-between mb-6 gap-4 flex-wrap">
|
||||
<div>
|
||||
<a href="/web/calendar" class="text-sm text-indigo-600 hover:underline">← Calendars</a>
|
||||
<h1 class="text-2xl font-semibold flex items-center gap-2">
|
||||
<span class="w-3 h-3 rounded-full shrink-0" style={ "background-color: " + colorOrDefault(data.Color) }></span>
|
||||
{ data.Cal }
|
||||
</h1>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center flex-wrap">
|
||||
<a href={ templ.URL(data.PrevMonthURL) } class="bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50">←</a>
|
||||
<a href={ templ.URL(data.TodayURL) } class="bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50">Today</a>
|
||||
<a href={ templ.URL(data.NextMonthURL) } class="bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50">→</a>
|
||||
<span class="text-lg font-medium ml-2">{ data.MonthLabel }</span>
|
||||
<a href={ templ.URL("/web/calendar/" + data.Cal + "/new") }
|
||||
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700 ml-2">
|
||||
New event
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden text-sm">
|
||||
for _, wd := range weekdayLabels() {
|
||||
<div class="bg-gray-50 px-2 py-1.5 font-medium text-gray-500 text-xs text-center">{ wd }</div>
|
||||
}
|
||||
for _, week := range data.Weeks {
|
||||
for _, day := range week {
|
||||
@monthDayCell(data.Cal, day)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ monthDayCell(cal string, day MonthDay) {
|
||||
<div class={ "bg-white min-h-[6rem] p-1.5 flex flex-col gap-1", templ.KV("bg-gray-50 text-gray-400", !day.InMonth) }>
|
||||
<div class="flex items-center justify-between">
|
||||
<a
|
||||
href={ templ.URL("/web/calendar/" + cal + "/new?date=" + day.Date) }
|
||||
class={ "text-xs font-medium rounded-full w-5 h-5 flex items-center justify-center", templ.KV("bg-indigo-600 text-white", day.IsToday), templ.KV("hover:bg-gray-100", !day.IsToday) }
|
||||
title="New event"
|
||||
>
|
||||
{ fmt.Sprint(day.Day) }
|
||||
</a>
|
||||
</div>
|
||||
for _, ev := range day.Events {
|
||||
<a
|
||||
href={ templ.URL("/web/calendar/" + cal + "/" + ev.ID + "/edit") }
|
||||
class="block truncate rounded bg-indigo-50 text-indigo-700 px-1.5 py-0.5 text-xs hover:bg-indigo-100"
|
||||
title={ ev.Summary }
|
||||
>
|
||||
if !ev.AllDay && ev.TimeText != "" {
|
||||
<span class="font-medium">{ ev.TimeText }</span>
|
||||
}
|
||||
{ " " + ev.Summary }
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
templ EventForm(username string, data EventFormData, errMsg string) {
|
||||
@Layout("Calendar", username) {
|
||||
<a href={ templ.URL("/web/calendar/" + data.Cal) } class="text-sm text-indigo-600 hover:underline">← { data.Cal }</a>
|
||||
<h1 class="text-2xl font-semibold mt-2 mb-6">
|
||||
if data.ID == "" {
|
||||
New event
|
||||
} else {
|
||||
Edit event
|
||||
}
|
||||
</h1>
|
||||
if errMsg != "" {
|
||||
<p class="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">{ errMsg }</p>
|
||||
}
|
||||
<form
|
||||
method="POST"
|
||||
action={ eventFormAction(data) }
|
||||
class="bg-white rounded-lg border border-gray-200 p-6 space-y-6 max-w-xl"
|
||||
>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Title</label>
|
||||
<input name="summary" type="text" required value={ data.Summary }
|
||||
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" checked?={ data.AllDay }/>
|
||||
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={ data.StartDate }
|
||||
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm"/>
|
||||
</div>
|
||||
<div id="start-time-field" class={ templ.KV("hidden", data.AllDay) }>
|
||||
<label class="block text-sm font-medium text-gray-700">Start time</label>
|
||||
<input name="start_time" type="time" value={ data.StartTime }
|
||||
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={ data.EndDate }
|
||||
class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm"/>
|
||||
</div>
|
||||
<div id="end-time-field" class={ templ.KV("hidden", data.AllDay) }>
|
||||
<label class="block text-sm font-medium text-gray-700">End time</label>
|
||||
<input name="end_time" type="time" value={ data.EndTime }
|
||||
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={ data.Location }
|
||||
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">{ data.Description }</textarea>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between pt-2">
|
||||
<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 data.ID != "" {
|
||||
<form method="POST" action={ templ.URL("/web/calendar/" + data.Cal + "/" + data.ID + "/delete") } onsubmit="return confirm('Delete this event?')">
|
||||
<button type="submit" class="text-red-600 hover:underline text-sm">Delete event</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script type="module" src="/web/static/calendar.js"></script>
|
||||
}
|
||||
}
|
||||
|
||||
func eventFormAction(data EventFormData) templ.SafeURL {
|
||||
if data.ID == "" {
|
||||
return templ.URL("/web/calendar/" + data.Cal + "/new")
|
||||
}
|
||||
return templ.URL("/web/calendar/" + data.Cal + "/" + data.ID + "/edit")
|
||||
}
|
||||
|
||||
func weekdayLabels() []string {
|
||||
return []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
|
||||
}
|
||||
@@ -0,0 +1,805 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "fmt"
|
||||
|
||||
// CalendarSummary is one of the user's own calendars, listed on the
|
||||
// calendar home page.
|
||||
type CalendarSummary struct {
|
||||
Name string
|
||||
Color string // hex color like "#3b82f6", "" if unset
|
||||
}
|
||||
|
||||
// EventSummary is a single event shown inside a month-view day cell.
|
||||
type EventSummary struct {
|
||||
ID string
|
||||
Summary string
|
||||
TimeText string // e.g. "14:00" or "" for all-day events
|
||||
AllDay bool
|
||||
}
|
||||
|
||||
// MonthDay is one day cell in the month grid.
|
||||
type MonthDay struct {
|
||||
Date string // "YYYY-MM-DD", used for links and the "new event" date
|
||||
Day int // day-of-month number shown in the cell
|
||||
InMonth bool // false for the leading/trailing days of neighboring months
|
||||
IsToday bool
|
||||
Events []EventSummary
|
||||
}
|
||||
|
||||
// MonthViewData is everything the month grid template needs to render one
|
||||
// month of a single calendar.
|
||||
type MonthViewData struct {
|
||||
Cal string
|
||||
Color string
|
||||
MonthLabel string // e.g. "August 2026"
|
||||
Weeks [][]MonthDay
|
||||
PrevMonthURL string
|
||||
NextMonthURL string
|
||||
TodayURL string
|
||||
}
|
||||
|
||||
// EventFormData pre-fills the create/edit event form.
|
||||
type EventFormData struct {
|
||||
Cal string
|
||||
ID string // empty when creating a new event
|
||||
Summary string
|
||||
Description string
|
||||
Location string
|
||||
AllDay bool
|
||||
StartDate string // "YYYY-MM-DD"
|
||||
StartTime string // "HH:MM", empty when AllDay
|
||||
EndDate string // "YYYY-MM-DD"
|
||||
EndTime string // "HH:MM", empty when AllDay
|
||||
}
|
||||
|
||||
func CalendarsHome(username string, calendars []CalendarSummary) 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
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<h1 class=\"text-2xl font-semibold mb-6\">Calendar</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if len(calendars) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<p class=\"text-sm text-gray-500\">You don't have any calendars yet — create one from the <a href=\"/web/\" class=\"text-indigo-600 hover:underline\">dashboard</a> first.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<ul class=\"divide-y divide-gray-200 bg-white rounded-lg border border-gray-200\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, c := range calendars {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<li class=\"px-4 py-3 flex items-center gap-3 text-sm\"><span class=\"w-3 h-3 rounded-full shrink-0\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, 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: 67, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"></span> <a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 templ.SafeURL
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + c.Name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 68, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"font-medium text-indigo-600 hover:underline\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, 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: 68, Col: 115}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</a></li>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</ul>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = Layout("Calendar", username).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func MonthView(username string, data MonthViewData) 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
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var6 == nil {
|
||||
templ_7745c5c3_Var6 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var7 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"flex items-center justify-between mb-6 gap-4 flex-wrap\"><div><a href=\"/web/calendar\" class=\"text-sm text-indigo-600 hover:underline\">← Calendars</a><h1 class=\"text-2xl font-semibold flex items-center gap-2\"><span class=\"w-3 h-3 rounded-full shrink-0\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + colorOrDefault(data.Color))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 82, Col: 106}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\"></span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.Cal)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 83, Col: 15}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</h1></div><div class=\"flex gap-2 items-center flex-wrap\"><a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 templ.SafeURL
|
||||
templ_7745c5c3_Var10, 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: 87, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\" class=\"bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50\">←</a> <a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 templ.SafeURL
|
||||
templ_7745c5c3_Var11, 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: 88, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" class=\"bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50\">Today</a> <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(data.NextMonthURL))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 89, Col: 42}
|
||||
}
|
||||
_, 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, 14, "\" class=\"bg-white border border-gray-300 rounded-md px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-50\">→</a> <span class=\"text-lg font-medium ml-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, 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: 90, Col: 60}
|
||||
}
|
||||
_, 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, 15, "</span> <a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 templ.SafeURL
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + data.Cal + "/new"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 91, Col: 61}
|
||||
}
|
||||
_, 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, 16, "\" class=\"bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700 ml-2\">New event</a></div></div><div class=\"grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, wd := range weekdayLabels() {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"bg-gray-50 px-2 py-1.5 font-medium text-gray-500 text-xs text-center\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, 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: 100, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, week := range data.Weeks {
|
||||
for _, day := range week {
|
||||
templ_7745c5c3_Err = monthDayCell(data.Cal, day).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = Layout("Calendar", username).Render(templ.WithChildren(ctx, templ_7745c5c3_Var7), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func monthDayCell(cal string, day MonthDay) 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
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var16 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var16 == nil {
|
||||
templ_7745c5c3_Var16 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
var templ_7745c5c3_Var17 = []any{"bg-white min-h-[6rem] p-1.5 flex flex-col gap-1", templ.KV("bg-gray-50 text-gray-400", !day.InMonth)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var17).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, 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, 21, "\"><div class=\"flex items-center justify-between\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 = []any{"text-xs font-medium rounded-full w-5 h-5 flex items-center justify-center", templ.KV("bg-indigo-600 text-white", day.IsToday), templ.KV("hover:bg-gray-100", !day.IsToday)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 templ.SafeURL
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + cal + "/new?date=" + day.Date))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 115, Col: 70}
|
||||
}
|
||||
_, 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, 23, "\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var19).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" title=\"New event\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, 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: 119, Col: 25}
|
||||
}
|
||||
_, 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, 25, "</a></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, ev := range day.Events {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 templ.SafeURL
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + cal + "/" + ev.ID + "/edit"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 124, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"block truncate rounded bg-indigo-50 text-indigo-700 px-1.5 py-0.5 text-xs hover:bg-indigo-100\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, 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: 126, Col: 22}
|
||||
}
|
||||
_, 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, 28, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !ev.AllDay && ev.TimeText != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<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: 129, 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, 30, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
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: 131, 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, 31, "</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var27 == nil {
|
||||
templ_7745c5c3_Var27 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Var28 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<a href=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 templ.SafeURL
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + data.Cal))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 139, Col: 50}
|
||||
}
|
||||
_, 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, 34, "\" class=\"text-sm text-indigo-600 hover:underline\">← ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(data.Cal)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 139, Col: 118}
|
||||
}
|
||||
_, 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, 35, "</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, 36, "New event")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Edit event")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</h1>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errMsg != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<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_Var31 string
|
||||
templ_7745c5c3_Var31, 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: 148, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, " <form method=\"POST\" action=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 templ.SafeURL
|
||||
templ_7745c5c3_Var32, 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: 152, Col: 33}
|
||||
}
|
||||
_, 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, 42, "\" class=\"bg-white rounded-lg border border-gray-200 p-6 space-y-6 max-w-xl\"><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_Var33 string
|
||||
templ_7745c5c3_Var33, 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: 157, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" 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, 44, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "> 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_Var34 string
|
||||
templ_7745c5c3_Var34, 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: 169, Col: 73}
|
||||
}
|
||||
_, 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, 46, "\" 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
|
||||
}
|
||||
var templ_7745c5c3_Var35 = []any{templ.KV("hidden", data.AllDay)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var35...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<div id=\"start-time-field\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var35).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\"><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_Var37 string
|
||||
templ_7745c5c3_Var37, 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: 174, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\" 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_Var38 string
|
||||
templ_7745c5c3_Var38, 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: 179, Col: 69}
|
||||
}
|
||||
_, 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, 50, "\" 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
|
||||
}
|
||||
var templ_7745c5c3_Var39 = []any{templ.KV("hidden", data.AllDay)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var39...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div id=\"end-time-field\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var39).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\"><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_Var41 string
|
||||
templ_7745c5c3_Var41, 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: 184, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" 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_Var42 string
|
||||
templ_7745c5c3_Var42, 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: 191, 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, 54, "\" 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_Var43 string
|
||||
templ_7745c5c3_Var43, 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: 197, Col: 103}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</textarea></div><div class=\"flex items-center justify-between pt-2\"><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 != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<form method=\"POST\" action=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var44 templ.SafeURL
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL("/web/calendar/" + data.Cal + "/" + data.ID + "/delete"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/calendar.templ`, Line: 205, Col: 100}
|
||||
}
|
||||
_, 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, 57, "\" 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
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</div></form><script type=\"module\" src=\"/web/static/calendar.js\"></script>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
templ_7745c5c3_Err = Layout("Calendar", username).Render(templ.WithChildren(ctx, templ_7745c5c3_Var28), templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func eventFormAction(data EventFormData) templ.SafeURL {
|
||||
if data.ID == "" {
|
||||
return templ.URL("/web/calendar/" + data.Cal + "/new")
|
||||
}
|
||||
return templ.URL("/web/calendar/" + data.Cal + "/" + data.ID + "/edit")
|
||||
}
|
||||
|
||||
func weekdayLabels() []string {
|
||||
return []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
@@ -18,6 +18,7 @@ templ Layout(title string, username string) {
|
||||
if username != "" {
|
||||
<a href="/web/files/" class="text-sm text-gray-600 hover:text-indigo-600">Files</a>
|
||||
<a href="/web/contacts" class="text-sm text-gray-600 hover:text-indigo-600">Contacts</a>
|
||||
<a href="/web/calendar" class="text-sm text-gray-600 hover:text-indigo-600">Calendar</a>
|
||||
}
|
||||
</div>
|
||||
if username != "" {
|
||||
|
||||
@@ -47,7 +47,7 @@ func Layout(title string, username string) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if username != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/web/files/\" class=\"text-sm text-gray-600 hover:text-indigo-600\">Files</a> <a href=\"/web/contacts\" class=\"text-sm text-gray-600 hover:text-indigo-600\">Contacts</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/web/files/\" class=\"text-sm text-gray-600 hover:text-indigo-600\">Files</a> <a href=\"/web/contacts\" class=\"text-sm text-gray-600 hover:text-indigo-600\">Contacts</a> <a href=\"/web/calendar\" class=\"text-sm text-gray-600 hover:text-indigo-600\">Calendar</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func Layout(title string, username string) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 25, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/web/templates/layout.templ`, Line: 26, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
Reference in New Issue
Block a user