- Replace 14-day agenda list with three-view calendar (month/week/day) - Add PlanningCalendar, CalendarDay, CalendarTimeEvent, MiniCalDay data structs - Add BuildMonthCalendar, BuildWeekCalendar, BuildDayCalendar builder helpers - Add MondayOf, PlanningMonthURL, PlanningWeekURL, PlanningDayURL URL helpers - Handler now parses ?view=month|week|day with matching date params - Month view: 7-col grid with event chips using tablo color via color-mix() - Week/Day view: split layout with mini-month panel and hour timeline (07-20) - Timeline events positioned via TopPx/HeightPx from start_time microseconds - Append all calendar CSS to app.css (view-toggle, cal-grid, tl-* classes)
271 lines
8.3 KiB
Text
271 lines
8.3 KiB
Text
package templates
|
|
|
|
import (
|
|
"fmt"
|
|
"backend/internal/auth"
|
|
"backend/internal/db/sqlc"
|
|
)
|
|
|
|
// PlanningCalendarPage is the top-level page template for the /planning route.
|
|
// It renders the appropriate view (month / week / day) based on cal.View.
|
|
templ PlanningCalendarPage(user *auth.User, csrfToken string, activePath string, sidebarTablos []sqlc.Tablo, cal PlanningCalendar, pageTitle string, breadcrumb []BreadcrumbItem) {
|
|
@AppLayout("Planning - Xtablo", user, csrfToken, activePath, sidebarTablos, pageTitle, breadcrumb, nil) {
|
|
<div class="planning-page">
|
|
@PlanningHeader(cal)
|
|
if cal.View == "month" {
|
|
@PlanningMonthView(cal)
|
|
} else {
|
|
@PlanningWeekDayView(cal)
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
// PlanningHeader renders the navigation row: prev/today/next + period label + view toggle.
|
|
templ PlanningHeader(cal PlanningCalendar) {
|
|
<div class="planning-header">
|
|
<nav class="flex items-center gap-1" aria-label="Period navigation">
|
|
<a href={ templ.SafeURL(cal.PrevURL) } class="icon-btn" aria-label="Previous period">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7"/></svg>
|
|
</a>
|
|
<a href={ templ.SafeURL(cal.TodayURL) } class="ui-button ui-button-soft ui-button-neutral ui-button-sm">Today</a>
|
|
<a href={ templ.SafeURL(cal.NextURL) } class="icon-btn" aria-label="Next period">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/></svg>
|
|
</a>
|
|
</nav>
|
|
<span class="planning-period">{ cal.Label }</span>
|
|
<div class="view-toggle" role="group" aria-label="Calendar view">
|
|
<a
|
|
href={ templ.SafeURL(cal.PrevURL) + "?view_switch=month" }
|
|
hx-get={ "/planning?view=month" }
|
|
hx-push-url="true"
|
|
if cal.View == "month" {
|
|
class="view-btn active"
|
|
} else {
|
|
class="view-btn"
|
|
}
|
|
aria-label="Month view"
|
|
>Month</a>
|
|
<a
|
|
hx-get={ "/planning?view=week" }
|
|
hx-push-url="true"
|
|
if cal.View == "week" {
|
|
class="view-btn active"
|
|
} else {
|
|
class="view-btn"
|
|
}
|
|
aria-label="Week view"
|
|
>Week</a>
|
|
<a
|
|
hx-get={ "/planning?view=day" }
|
|
hx-push-url="true"
|
|
if cal.View == "day" {
|
|
class="view-btn active"
|
|
} else {
|
|
class="view-btn"
|
|
}
|
|
aria-label="Day view"
|
|
>Day</a>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Month view
|
|
// ---------------------------------------------------------------------------
|
|
|
|
templ PlanningMonthView(cal PlanningCalendar) {
|
|
<div class="planning-month-body">
|
|
<div class="cal-header">
|
|
<div class="cal-dow">Mon</div>
|
|
<div class="cal-dow">Tue</div>
|
|
<div class="cal-dow">Wed</div>
|
|
<div class="cal-dow">Thu</div>
|
|
<div class="cal-dow">Fri</div>
|
|
<div class="cal-dow">Sat</div>
|
|
<div class="cal-dow">Sun</div>
|
|
</div>
|
|
<div class="cal-grid">
|
|
for _, week := range cal.Weeks {
|
|
for _, day := range week {
|
|
@MonthDayCell(day)
|
|
}
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ MonthDayCell(day CalendarDay) {
|
|
if day.OtherMonth {
|
|
<div class="cal-day other-month">
|
|
<span class="day-num">{ fmt.Sprintf("%d", day.DayNum) }</span>
|
|
</div>
|
|
} else if day.IsToday {
|
|
<div class="cal-day today">
|
|
<span class="day-num">{ fmt.Sprintf("%d", day.DayNum) }</span>
|
|
for _, ev := range day.Events {
|
|
@CalEventChip(ev)
|
|
}
|
|
</div>
|
|
} else {
|
|
<div class="cal-day">
|
|
<span class="day-num">{ fmt.Sprintf("%d", day.DayNum) }</span>
|
|
for _, ev := range day.Events {
|
|
@CalEventChip(ev)
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
templ CalEventChip(ev CalendarEvent) {
|
|
if ev.Style != "" {
|
|
<a href={ templ.SafeURL(ev.URL) } class="cal-event" style={ ev.Style } title={ ev.Title }>
|
|
if ev.TimeLabel != "" {
|
|
<span>{ ev.TimeLabel } </span>
|
|
}
|
|
{ ev.Title }
|
|
</a>
|
|
} else {
|
|
<a href={ templ.SafeURL(ev.URL) } class={ "cal-event " + ev.ColorClass } title={ ev.Title }>
|
|
if ev.TimeLabel != "" {
|
|
<span>{ ev.TimeLabel } </span>
|
|
}
|
|
{ ev.Title }
|
|
</a>
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Week / Day view (shared layout with mini-month panel + timeline)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
templ PlanningWeekDayView(cal PlanningCalendar) {
|
|
<div class="planning-split">
|
|
@MiniMonthPanel(cal)
|
|
<div class="timeline-outer">
|
|
<div class="timeline-col-header">
|
|
<div class="tl-gutter"></div>
|
|
for _, day := range cal.Days {
|
|
if day.IsToday {
|
|
<div class="tl-day-header today">{ day.Label }</div>
|
|
} else {
|
|
<div class="tl-day-header">{ day.Label }</div>
|
|
}
|
|
}
|
|
</div>
|
|
<div class="timeline-body">
|
|
<div class="tl-time-col">
|
|
for _, slot := range cal.HourSlots {
|
|
<div class="tl-hour-label">{ slot }</div>
|
|
}
|
|
</div>
|
|
for _, day := range cal.Days {
|
|
@TimelineDayCol(day)
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
templ TimelineDayCol(day CalendarDayColumn) {
|
|
<div class="tl-day-col">
|
|
for range day.Events {
|
|
<!-- hour rows for visual grid -->
|
|
}
|
|
for i := range [14]struct{}{} {
|
|
<div class="tl-hour-row" data-hour={ fmt.Sprintf("%d", 7+i) }></div>
|
|
}
|
|
for _, ev := range day.Events {
|
|
@TimelineEventBlock(ev)
|
|
}
|
|
</div>
|
|
}
|
|
|
|
templ TimelineEventBlock(ev CalendarTimeEvent) {
|
|
if ev.Style != "" {
|
|
<a
|
|
href={ templ.SafeURL(ev.URL) }
|
|
class="tl-event"
|
|
style={ fmt.Sprintf("%s;top:%dpx;height:%dpx", ev.Style, ev.TopPx, ev.HeightPx) }
|
|
title={ ev.Title }
|
|
>{ ev.Title }</a>
|
|
} else {
|
|
<a
|
|
href={ templ.SafeURL(ev.URL) }
|
|
class={ "tl-event " + ev.ColorClass }
|
|
style={ fmt.Sprintf("top:%dpx;height:%dpx", ev.TopPx, ev.HeightPx) }
|
|
title={ ev.Title }
|
|
>{ ev.Title }</a>
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mini-month panel
|
|
// ---------------------------------------------------------------------------
|
|
|
|
templ MiniMonthPanel(cal PlanningCalendar) {
|
|
<div class="mini-panel">
|
|
<p class="mini-month-label">{ cal.MiniMonthLabel }</p>
|
|
<div class="mini-grid">
|
|
<div class="mini-dow">M</div>
|
|
<div class="mini-dow">T</div>
|
|
<div class="mini-dow">W</div>
|
|
<div class="mini-dow">T</div>
|
|
<div class="mini-dow">F</div>
|
|
<div class="mini-dow">S</div>
|
|
<div class="mini-dow">S</div>
|
|
</div>
|
|
for _, week := range cal.MiniMonth {
|
|
<div class="mini-grid">
|
|
for _, d := range week {
|
|
@MiniDayCell(d)
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
templ MiniDayCell(d MiniCalDay) {
|
|
if d.IsToday {
|
|
<a href={ templ.SafeURL(d.URL) } class="mini-day today">{ fmt.Sprintf("%d", d.DayNum) }</a>
|
|
} else if d.InWeek {
|
|
<a href={ templ.SafeURL(d.URL) } class="mini-day in-week">{ fmt.Sprintf("%d", d.DayNum) }</a>
|
|
} else {
|
|
<a href={ templ.SafeURL(d.URL) } class="mini-day">{ fmt.Sprintf("%d", d.DayNum) }</a>
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Legacy agenda templates kept for reference (no longer used by handler)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
templ PlanningDaySeparator(label string) {
|
|
<li class="bg-slate-50 px-4 py-2 text-sm font-semibold text-slate-600 border-t border-slate-200" data-day-separator="true">
|
|
{ label }
|
|
</li>
|
|
}
|
|
|
|
templ PlanningEventListItem(event PlanningEventRow) {
|
|
<li class="bg-white px-4 py-3 hover:bg-slate-50 border-t border-slate-200">
|
|
<a href={ templ.SafeURL(event.URL) } class="block focus:outline-none focus:ring-2 focus:ring-blue-600 focus:ring-offset-2" aria-label={ "Open event in tablo: " + event.Title }>
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex min-w-0 flex-col gap-1 sm:flex-row sm:items-center sm:gap-3">
|
|
<span class="text-sm font-medium text-slate-700">{ event.TimeRange }</span>
|
|
<span class="text-base font-semibold text-slate-900">{ event.Title }</span>
|
|
</div>
|
|
<div class="flex flex-wrap items-center gap-2 text-sm text-slate-600">
|
|
<span class="inline-flex items-center gap-1">
|
|
if event.HasColor {
|
|
<span class="inline-block h-2.5 w-2.5 rounded-full" style={ "background-color: " + event.TabloColor }></span>
|
|
}
|
|
<span>{ event.TabloTitle }</span>
|
|
</span>
|
|
if event.HasLocation {
|
|
<span aria-hidden="true">·</span>
|
|
<span>{ event.Location }</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</a>
|
|
</li>
|
|
}
|