Add week view to web calendar with Month/Week toggle
Extract the shared per-calendar event-collection logic out of buildMonthView into collectCalendarEvents, keyed by day so it can feed either the existing 42-cell month grid or a new 7-cell week grid. Add templates.WeekDay/WeekViewData, a WeekView templ, and a viewSwitcher component shown in both views' headers to switch between Month and Week. Add buildWeekView, handleCalendarWeek, parseWeekStart, and mondayOf in internal/web/calendar.go, and register the new /calendar/week route. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+166
-39
@@ -256,6 +256,41 @@ func parseYearMonth(r *http.Request) (year int, month time.Month) {
|
|||||||
return year, month
|
return year, month
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleCalendarWeek renders the week view, mirroring
|
||||||
|
// handleCalendarMonth but for a single 7-day week.
|
||||||
|
func (s *Server) handleCalendarWeek(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())
|
||||||
|
|
||||||
|
weekStart := parseWeekStart(r)
|
||||||
|
|
||||||
|
data, err := s.buildWeekView(username, weekStart)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("building week view", "error", err)
|
||||||
|
http.Error(w, "internal error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
_ = templates.WeekView(username, data).Render(context.Background(), w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseWeekStart reads ?date=YYYY-MM-DD and returns the Monday of that
|
||||||
|
// date's week, defaulting to the current week if the parameter is
|
||||||
|
// absent or invalid.
|
||||||
|
func parseWeekStart(r *http.Request) time.Time {
|
||||||
|
if v := r.URL.Query().Get("date"); v != "" {
|
||||||
|
if d, err := time.ParseInLocation(dateLayout, v, time.Local); err == nil {
|
||||||
|
return mondayOf(d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mondayOf(time.Now())
|
||||||
|
}
|
||||||
|
|
||||||
// buildMonthView loads every event from every calendar visible to
|
// buildMonthView loads every event from every calendar visible to
|
||||||
// username (own + shared), then places each occurrence's days onto a
|
// username (own + shared), then places each occurrence's days onto a
|
||||||
// 6-week grid covering the requested month (plus enough leading/trailing
|
// 6-week grid covering the requested month (plus enough leading/trailing
|
||||||
@@ -263,11 +298,6 @@ func parseYearMonth(r *http.Request) (year int, month time.Month) {
|
|||||||
// (RRULE) are not expanded — only an event's own DTSTART/DTEND span is
|
// (RRULE) are not expanded — only an event's own DTSTART/DTEND span is
|
||||||
// considered.
|
// considered.
|
||||||
func (s *Server) buildMonthView(username string, year int, month time.Month) (templates.MonthViewData, error) {
|
func (s *Server) buildMonthView(username string, year int, month time.Month) (templates.MonthViewData, error) {
|
||||||
entries, err := s.listCalendarEntries(username)
|
|
||||||
if err != nil {
|
|
||||||
return templates.MonthViewData{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
loc := time.Local
|
loc := time.Local
|
||||||
first := time.Date(year, month, 1, 0, 0, 0, 0, loc)
|
first := time.Date(year, month, 1, 0, 0, 0, 0, loc)
|
||||||
// Monday-first offset: time.Weekday has Sunday=0..Saturday=6.
|
// Monday-first offset: time.Weekday has Sunday=0..Saturday=6.
|
||||||
@@ -278,7 +308,6 @@ func (s *Server) buildMonthView(username string, year int, month time.Month) (te
|
|||||||
today := time.Now().In(loc).Format(dateLayout)
|
today := time.Now().In(loc).Format(dateLayout)
|
||||||
|
|
||||||
days := make([]templates.MonthDay, totalDays)
|
days := make([]templates.MonthDay, totalDays)
|
||||||
dayIndex := make(map[string]int, totalDays)
|
|
||||||
for i := 0; i < totalDays; i++ {
|
for i := 0; i < totalDays; i++ {
|
||||||
d := gridStart.AddDate(0, 0, i)
|
d := gridStart.AddDate(0, 0, i)
|
||||||
dateStr := d.Format(dateLayout)
|
dateStr := d.Format(dateLayout)
|
||||||
@@ -288,12 +317,128 @@ func (s *Server) buildMonthView(username string, year int, month time.Month) (te
|
|||||||
InMonth: d.Month() == month,
|
InMonth: d.Month() == month,
|
||||||
IsToday: dateStr == today,
|
IsToday: dateStr == today,
|
||||||
}
|
}
|
||||||
dayIndex[dateStr] = i
|
|
||||||
}
|
}
|
||||||
gridEnd := gridStart.AddDate(0, 0, totalDays-1)
|
gridEnd := gridStart.AddDate(0, 0, totalDays-1)
|
||||||
|
|
||||||
hasWritable := false
|
eventsByDay, calSummaries, hasWritable, err := s.collectCalendarEvents(username, gridStart, gridEnd, loc)
|
||||||
var calSummaries []templates.CalendarSummary
|
if err != nil {
|
||||||
|
return templates.MonthViewData{}, err
|
||||||
|
}
|
||||||
|
for i := range days {
|
||||||
|
days[i].Events = sortedDayEvents(eventsByDay[days[i].Date])
|
||||||
|
}
|
||||||
|
|
||||||
|
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?year=%d&month=%d", y, int(m))
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.MonthViewData{
|
||||||
|
Calendars: calSummaries,
|
||||||
|
HasWritable: hasWritable,
|
||||||
|
MonthLabel: first.Format("January 2006"),
|
||||||
|
Weeks: weeks,
|
||||||
|
PrevMonthURL: monthURL(prevMonth.Year(), prevMonth.Month()),
|
||||||
|
NextMonthURL: monthURL(nextMonth.Year(), nextMonth.Month()),
|
||||||
|
TodayURL: "/web/calendar",
|
||||||
|
WeekURL: "/web/calendar/week",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWeekView loads every event from every calendar visible to
|
||||||
|
// username (own + shared) that falls within the 7-day week starting on
|
||||||
|
// weekStart (which must already be a Monday — see mondayOf), placing
|
||||||
|
// each occurrence's days onto a single 7-column row.
|
||||||
|
func (s *Server) buildWeekView(username string, weekStart time.Time) (templates.WeekViewData, error) {
|
||||||
|
loc := time.Local
|
||||||
|
gridEnd := weekStart.AddDate(0, 0, 6)
|
||||||
|
today := time.Now().In(loc).Format(dateLayout)
|
||||||
|
|
||||||
|
eventsByDay, calSummaries, hasWritable, err := s.collectCalendarEvents(username, weekStart, gridEnd, loc)
|
||||||
|
if err != nil {
|
||||||
|
return templates.WeekViewData{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
days := make([]templates.WeekDay, 7)
|
||||||
|
for i := 0; i < 7; i++ {
|
||||||
|
d := weekStart.AddDate(0, 0, i)
|
||||||
|
dateStr := d.Format(dateLayout)
|
||||||
|
days[i] = templates.WeekDay{
|
||||||
|
Date: dateStr,
|
||||||
|
Weekday: d.Format("Monday"),
|
||||||
|
Day: d.Day(),
|
||||||
|
Month: d.Format("Jan"),
|
||||||
|
IsToday: dateStr == today,
|
||||||
|
Events: sortedDayEvents(eventsByDay[dateStr]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prevWeek := weekStart.AddDate(0, 0, -7)
|
||||||
|
nextWeek := weekStart.AddDate(0, 0, 7)
|
||||||
|
weekURL := func(t time.Time) string {
|
||||||
|
return "/web/calendar/week?date=" + t.Format(dateLayout)
|
||||||
|
}
|
||||||
|
|
||||||
|
rangeLabel := fmt.Sprintf("%s – %s", weekStart.Format("Jan 2"), gridEnd.Format("Jan 2, 2006"))
|
||||||
|
|
||||||
|
return templates.WeekViewData{
|
||||||
|
Calendars: calSummaries,
|
||||||
|
HasWritable: hasWritable,
|
||||||
|
RangeLabel: rangeLabel,
|
||||||
|
Days: days,
|
||||||
|
PrevWeekURL: weekURL(prevWeek),
|
||||||
|
NextWeekURL: weekURL(nextWeek),
|
||||||
|
TodayURL: "/web/calendar/week",
|
||||||
|
MonthURL: "/web/calendar",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortedDayEvents returns evs (which may be nil) sorted with all-day
|
||||||
|
// events first, then timed events by start time — the stable, readable
|
||||||
|
// order used by both the month and week grids.
|
||||||
|
func sortedDayEvents(evs []templates.EventSummary) []templates.EventSummary {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
return evs
|
||||||
|
}
|
||||||
|
|
||||||
|
// collectCalendarEvents gathers every event, across every calendar
|
||||||
|
// visible to username (own, shared, birthdays, ICS subscriptions), whose
|
||||||
|
// day-span overlaps [gridStart, gridEnd] (inclusive, in loc), keyed by
|
||||||
|
// day (dateLayout) so callers can place them into either a month or a
|
||||||
|
// week grid. It also returns the calendar summary list (used for the
|
||||||
|
// legend/sidebar) and whether any calendar is writable (for "new event"
|
||||||
|
// links).
|
||||||
|
func (s *Server) collectCalendarEvents(username string, gridStart, gridEnd time.Time, loc *time.Location) (eventsByDay map[string][]templates.EventSummary, calSummaries []templates.CalendarSummary, hasWritable bool, err error) {
|
||||||
|
entries, err := s.listCalendarEntries(username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap how many days any single event can add, in case of malformed
|
||||||
|
// data with a wildly distant DTEND.
|
||||||
|
totalDays := int(gridEnd.Sub(gridStart).Hours()/24) + 1
|
||||||
|
|
||||||
|
dayIndex := make(map[string]int, totalDays)
|
||||||
|
days := make([]templates.MonthDay, totalDays)
|
||||||
|
for i := 0; i < totalDays; i++ {
|
||||||
|
d := gridStart.AddDate(0, 0, i)
|
||||||
|
dateStr := d.Format(dateLayout)
|
||||||
|
days[i] = templates.MonthDay{Date: dateStr}
|
||||||
|
dayIndex[dateStr] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
eventsByDay = make(map[string][]templates.EventSummary)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.Writable {
|
if entry.Writable {
|
||||||
hasWritable = true
|
hasWritable = true
|
||||||
@@ -344,8 +489,6 @@ func (s *Server) buildMonthView(username string, year int, month time.Month) (te
|
|||||||
if endDay.After(gridEnd) {
|
if endDay.After(gridEnd) {
|
||||||
endDay = 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 {
|
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)]
|
idx, ok := dayIndex[d.Format(dateLayout)]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -367,39 +510,23 @@ func (s *Server) buildMonthView(username string, year int, month time.Month) (te
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep each day's events in a stable, readable order: all-day events
|
for _, d := range days {
|
||||||
// first, then timed events sorted by start time.
|
if len(d.Events) > 0 {
|
||||||
for i := range days {
|
eventsByDay[d.Date] = d.Events
|
||||||
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
|
}
|
||||||
})
|
return eventsByDay, calSummaries, hasWritable, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
weeks := make([][]templates.MonthDay, 0, 6)
|
// mondayOf returns the Monday (in loc, at midnight) of the week
|
||||||
for i := 0; i < totalDays; i += 7 {
|
// containing t.
|
||||||
weeks = append(weeks, days[i:i+7])
|
func mondayOf(t time.Time) time.Time {
|
||||||
|
// time.Weekday has Sunday=0..Saturday=6; convert to a Monday-first
|
||||||
|
// offset so Monday itself maps to 0.
|
||||||
|
offset := (int(t.Weekday()) + 6) % 7
|
||||||
|
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location()).AddDate(0, 0, -offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
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?year=%d&month=%d", y, int(m))
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates.MonthViewData{
|
|
||||||
Calendars: calSummaries,
|
|
||||||
HasWritable: hasWritable,
|
|
||||||
MonthLabel: first.Format("January 2006"),
|
|
||||||
Weeks: weeks,
|
|
||||||
PrevMonthURL: monthURL(prevMonth.Year(), prevMonth.Month()),
|
|
||||||
NextMonthURL: monthURL(nextMonth.Year(), nextMonth.Month()),
|
|
||||||
TodayURL: "/web/calendar",
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// eventDayRange returns the inclusive [start, end] calendar-day span an
|
// eventDayRange returns the inclusive [start, end] calendar-day span an
|
||||||
// event occupies, in loc, for placing it on the month grid.
|
// event occupies, in loc, for placing it on the month grid.
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func (s *Server) Handler(staticFS http.FileSystem) http.Handler {
|
|||||||
mux.HandleFunc("/contacts/{book}/{id}/delete", s.requireLogin(s.handleContactDelete))
|
mux.HandleFunc("/contacts/{book}/{id}/delete", s.requireLogin(s.handleContactDelete))
|
||||||
mux.HandleFunc("/contacts/{book}/{id}/export", s.requireLogin(s.handleContactExportOne))
|
mux.HandleFunc("/contacts/{book}/{id}/export", s.requireLogin(s.handleContactExportOne))
|
||||||
mux.HandleFunc("/calendar", s.requireLogin(s.handleCalendarMonth))
|
mux.HandleFunc("/calendar", s.requireLogin(s.handleCalendarMonth))
|
||||||
|
mux.HandleFunc("/calendar/week", s.requireLogin(s.handleCalendarWeek))
|
||||||
mux.HandleFunc("/calendar/new", s.requireLogin(s.handleEventNew))
|
mux.HandleFunc("/calendar/new", s.requireLogin(s.handleEventNew))
|
||||||
mux.HandleFunc("/calendar/{ref}/import", s.requireLogin(s.handleCalendarImport))
|
mux.HandleFunc("/calendar/{ref}/import", s.requireLogin(s.handleCalendarImport))
|
||||||
mux.HandleFunc("/calendar/{ref}/export", s.requireLogin(s.handleCalendarExportAll))
|
mux.HandleFunc("/calendar/{ref}/export", s.requireLogin(s.handleCalendarExportAll))
|
||||||
|
|||||||
@@ -50,8 +50,33 @@ type MonthViewData struct {
|
|||||||
PrevMonthURL string
|
PrevMonthURL string
|
||||||
NextMonthURL string
|
NextMonthURL string
|
||||||
TodayURL string
|
TodayURL string
|
||||||
|
WeekURL string // link to switch to the week view
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeekDay is one day column in the week grid.
|
||||||
|
type WeekDay struct {
|
||||||
|
Date string // "YYYY-MM-DD", used for links and the "new event" date
|
||||||
|
Weekday string // full weekday name, e.g. "Monday"
|
||||||
|
Day int // day-of-month number shown in the column header
|
||||||
|
Month string // short month abbreviation, e.g. "Aug" (for cross-month weeks)
|
||||||
|
IsToday bool
|
||||||
|
Events []EventSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeekViewData is everything the week grid template needs to render one
|
||||||
|
// 7-day week across every calendar (own + shared) visible to the viewer.
|
||||||
|
type WeekViewData struct {
|
||||||
|
Calendars []CalendarSummary
|
||||||
|
HasWritable bool
|
||||||
|
RangeLabel string // e.g. "Aug 18 – 24, 2026"
|
||||||
|
Days []WeekDay
|
||||||
|
PrevWeekURL string
|
||||||
|
NextWeekURL string
|
||||||
|
TodayURL string
|
||||||
|
MonthURL string // link to switch to the month view
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// CalendarOption is one entry in the "new event" calendar <select>.
|
// CalendarOption is one entry in the "new event" calendar <select>.
|
||||||
type CalendarOption struct {
|
type CalendarOption struct {
|
||||||
Ref string
|
Ref string
|
||||||
@@ -84,6 +109,7 @@ templ MonthView(username string, data MonthViewData) {
|
|||||||
<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.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>
|
<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>
|
<span class="text-lg font-medium ml-2">{ data.MonthLabel }</span>
|
||||||
|
@viewSwitcher("month", "/web/calendar", data.WeekURL)
|
||||||
if data.HasWritable {
|
if data.HasWritable {
|
||||||
<a href="/web/calendar/new"
|
<a href="/web/calendar/new"
|
||||||
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700 ml-2">
|
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700 ml-2">
|
||||||
@@ -119,6 +145,26 @@ templ MonthView(username string, data MonthViewData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// viewSwitcher renders the Month/Week toggle shown in both views'
|
||||||
|
// headers. active is "month" or "week"; monthURL/weekURL are the
|
||||||
|
// destinations for switching to each view.
|
||||||
|
templ viewSwitcher(active, monthURL, weekURL string) {
|
||||||
|
<div class="inline-flex rounded-md border border-gray-300 overflow-hidden ml-2">
|
||||||
|
<a
|
||||||
|
href={ templ.URL(monthURL) }
|
||||||
|
class={ "px-3 py-1.5 text-sm font-medium", templ.KV("bg-indigo-600 text-white", active == "month"), templ.KV("bg-white text-gray-700 hover:bg-gray-50", active != "month") }
|
||||||
|
>
|
||||||
|
Month
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={ templ.URL(weekURL) }
|
||||||
|
class={ "px-3 py-1.5 text-sm font-medium border-l border-gray-300", templ.KV("bg-indigo-600 text-white", active == "week"), templ.KV("bg-white text-gray-700 hover:bg-gray-50", active != "week") }
|
||||||
|
>
|
||||||
|
Week
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
templ calendarLegendRow(c CalendarSummary) {
|
templ calendarLegendRow(c CalendarSummary) {
|
||||||
<div class="px-4 py-3 flex items-center gap-3 text-sm flex-wrap">
|
<div class="px-4 py-3 flex items-center gap-3 text-sm flex-wrap">
|
||||||
<span class="w-3 h-3 rounded-full shrink-0 ring-1 ring-inset ring-black/10" style={ "background-color: " + colorOrDefault(c.Color) }></span>
|
<span class="w-3 h-3 rounded-full shrink-0 ring-1 ring-inset ring-black/10" style={ "background-color: " + colorOrDefault(c.Color) }></span>
|
||||||
@@ -170,6 +216,83 @@ templ monthDayCell(day MonthDay) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WeekView renders a single 7-day week across every calendar (own +
|
||||||
|
// shared) visible to the viewer, one column per day.
|
||||||
|
templ WeekView(username string, data WeekViewData) {
|
||||||
|
@Layout("Calendar", username) {
|
||||||
|
<div class="flex items-center justify-between mb-6 gap-4 flex-wrap">
|
||||||
|
<h1 class="text-2xl font-semibold">Calendar</h1>
|
||||||
|
<div class="flex gap-2 items-center flex-wrap">
|
||||||
|
<a href={ templ.URL(data.PrevWeekURL) } 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.NextWeekURL) } 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.RangeLabel }</span>
|
||||||
|
@viewSwitcher("week", data.MonthURL, "/web/calendar/week")
|
||||||
|
if data.HasWritable {
|
||||||
|
<a href="/web/calendar/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>
|
||||||
|
|
||||||
|
if len(data.Calendars) == 0 {
|
||||||
|
<p class="text-sm text-gray-500 mb-6">
|
||||||
|
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 {
|
||||||
|
<div class="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden text-sm mb-6">
|
||||||
|
for _, d := range data.Days {
|
||||||
|
@weekDayHeaderCell(d)
|
||||||
|
}
|
||||||
|
for _, d := range data.Days {
|
||||||
|
@weekDayCell(d)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg border border-gray-200 divide-y divide-gray-200">
|
||||||
|
for _, c := range data.Calendars {
|
||||||
|
@calendarLegendRow(c)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templ weekDayHeaderCell(day WeekDay) {
|
||||||
|
<div class={ "bg-gray-50 px-2 py-1.5 text-xs text-center", templ.KV("text-indigo-600 font-semibold", day.IsToday), templ.KV("text-gray-500 font-medium", !day.IsToday) }>
|
||||||
|
{ day.Weekday } <span class="text-gray-400">{ day.Month }</span> { fmt.Sprint(day.Day) }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
templ weekDayCell(day WeekDay) {
|
||||||
|
<div class="bg-white min-h-[24rem] p-1.5 flex flex-col gap-1 align-top">
|
||||||
|
<a
|
||||||
|
href={ templ.URL("/web/calendar/new?date=" + day.Date) }
|
||||||
|
class="self-start text-xs text-gray-400 hover:text-indigo-600 mb-1"
|
||||||
|
title="New event"
|
||||||
|
>
|
||||||
|
+ new
|
||||||
|
</a>
|
||||||
|
for _, ev := range day.Events {
|
||||||
|
<a
|
||||||
|
href={ templ.URL(eventLinkURL(ev)) }
|
||||||
|
class="block truncate rounded px-1.5 py-0.5 text-xs hover:opacity-80"
|
||||||
|
style={ "background-color: " + colorOrDefault(ev.Color) + "22; color: " + eventTextColor(ev.Color) }
|
||||||
|
title={ ev.Summary }
|
||||||
|
>
|
||||||
|
<span class="inline-block w-1.5 h-1.5 rounded-full mr-1 ring-1 ring-inset ring-black/10" style={ "background-color: " + eventTextColor(ev.Color) }></span>
|
||||||
|
if !ev.AllDay && ev.TimeText != "" {
|
||||||
|
<span class="font-medium">{ ev.TimeText }</span>
|
||||||
|
}
|
||||||
|
{ " " + ev.Summary }
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
func eventLinkURL(ev EventSummary) string {
|
func eventLinkURL(ev EventSummary) string {
|
||||||
if ev.LinkURL != "" {
|
if ev.LinkURL != "" {
|
||||||
return ev.LinkURL
|
return ev.LinkURL
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user