xtablo-source/backend/templates/planning.templ
Arthur Belleville eaaec7a89d
Some checks are pending
xtablo-ci / Checks (pull_request) Waiting to run
go-backend-ci / Check go-backend (pull_request) Waiting to run
style(planning): implement Month/Week/Day calendar views from sketch 005
- 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)
2026-05-18 16:56:44 +02:00

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>
}