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:
@@ -50,8 +50,33 @@ type MonthViewData struct {
|
||||
PrevMonthURL string
|
||||
NextMonthURL 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>.
|
||||
type CalendarOption struct {
|
||||
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.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>
|
||||
@viewSwitcher("month", "/web/calendar", data.WeekURL)
|
||||
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">
|
||||
@@ -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) {
|
||||
<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>
|
||||
@@ -170,6 +216,83 @@ templ monthDayCell(day MonthDay) {
|
||||
</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 {
|
||||
if ev.LinkURL != "" {
|
||||
return ev.LinkURL
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user