594 lines
22 KiB
Templ
594 lines
22 KiB
Templ
package templates
|
||
|
||
import "fmt"
|
||
import "strconv"
|
||
import "strings"
|
||
import "time"
|
||
|
||
// CalendarSummary is one calendar (own or shared) shown in the combined
|
||
// month view's legend.
|
||
type CalendarSummary struct {
|
||
Ref string // path-safe reference: "name" or "owner~name"
|
||
Name string
|
||
Owner string // owner's username
|
||
Color string // hex color like "#3b82f6", "" if unset
|
||
Shared bool // true if owned by someone other than the viewer
|
||
Writable bool
|
||
Virtual bool // true for computed calendars (e.g. birthdays) with no import/export
|
||
}
|
||
|
||
// EventSummary is a single event shown inside a month-view day cell.
|
||
type EventSummary struct {
|
||
ID string
|
||
CalRef string
|
||
Color string
|
||
Summary string
|
||
TimeText string // e.g. "14:00" or "" for all-day events
|
||
AllDay bool
|
||
// LinkURL overrides the default "/web/calendar/{CalRef}/{ID}" (detail
|
||
// view) link target, used by virtual/read-only calendars (e.g.
|
||
// birthdays) that don't have an editable event object of their own.
|
||
LinkURL string
|
||
}
|
||
|
||
// 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 combined month grid template needs to
|
||
// render one month across every calendar (own + shared) visible to the
|
||
// viewer.
|
||
type MonthViewData struct {
|
||
Calendars []CalendarSummary
|
||
HasWritable bool // true if the viewer can create events in at least one calendar
|
||
MonthLabel string // e.g. "August 2026"
|
||
Weeks [][]MonthDay
|
||
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
|
||
Label string
|
||
}
|
||
|
||
// EventFormData pre-fills the create/edit event form.
|
||
type EventFormData struct {
|
||
CalRef string // resolved/fixed calendar reference (edit), or the selected one (new)
|
||
CalendarLabel string // fixed, read-only display label used when editing
|
||
Calendars []CalendarOption // populated only for new-event forms
|
||
Writable bool // false when editing an event in a read-only shared calendar
|
||
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
|
||
}
|
||
|
||
// EventDetailData is everything the read-only event detail view needs: the
|
||
// decoded event itself (Form, including its ID/CalRef/Writable display
|
||
// metadata) plus the calendar's color for the header dot.
|
||
type EventDetailData struct {
|
||
Form EventFormData
|
||
Color string // calendar color, "" if unset
|
||
}
|
||
|
||
templ MonthView(username string, data MonthViewData) {
|
||
@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.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>
|
||
@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">
|
||
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="overflow-x-auto -mx-3 sm:mx-0 px-3 sm:px-0 mb-6">
|
||
<div class="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden text-sm min-w-[640px] sm:min-w-0">
|
||
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(day)
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white rounded-lg border border-gray-200 divide-y divide-gray-200">
|
||
for _, c := range data.Calendars {
|
||
@calendarLegendRow(c)
|
||
}
|
||
</div>
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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>
|
||
<span class="font-medium">{ c.Name }</span>
|
||
if c.Shared {
|
||
<span class="text-xs text-gray-500">shared by { c.Owner }</span>
|
||
}
|
||
if !c.Writable {
|
||
<span class="text-xs text-gray-400">(read-only)</span>
|
||
}
|
||
<span class="flex-1"></span>
|
||
if !c.Virtual {
|
||
<a href={ templ.URL("/web/calendar/" + c.Ref + "/export") } class="text-indigo-600 hover:underline text-xs">Export .ics</a>
|
||
}
|
||
if c.Writable {
|
||
<form method="POST" action={ templ.URL("/web/calendar/" + c.Ref + "/import") } enctype="multipart/form-data" class="flex items-center gap-1">
|
||
<input type="file" name="file" accept=".ics,text/calendar" required class="text-xs"/>
|
||
<button type="submit" class="text-indigo-600 hover:underline text-xs">Import</button>
|
||
</form>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
templ monthDayCell(day MonthDay) {
|
||
<div class={ "bg-white min-h-[4.5rem] sm:min-h-[6rem] p-1 sm: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/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(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>
|
||
}
|
||
|
||
// 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="overflow-x-auto -mx-3 sm:mx-0 px-3 sm:px-0 mb-6">
|
||
<div class="grid grid-cols-7 gap-px bg-gray-200 border border-gray-200 rounded-lg overflow-hidden text-sm min-w-[640px] sm:min-w-0">
|
||
for _, d := range data.Days {
|
||
@weekDayHeaderCell(d)
|
||
}
|
||
for _, d := range data.Days {
|
||
@weekDayCell(d)
|
||
}
|
||
</div>
|
||
</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
|
||
}
|
||
return "/web/calendar/" + ev.CalRef + "/" + ev.ID
|
||
}
|
||
|
||
// eventTextColor returns a color derived from hex, darkened if needed so
|
||
// it stays legible as text on monthDayCell's very light (~13% opacity)
|
||
// tinted event background. Without this, a light calendar color like
|
||
// white or pale yellow renders its own event text as invisible or
|
||
// near-invisible, since the same color is used for both the tint and the
|
||
// text. Colors that are already dark enough are returned unchanged.
|
||
func eventTextColor(hex string) string {
|
||
c := colorOrDefault(hex)
|
||
r, g, b, ok := parseHexColor(c)
|
||
if !ok {
|
||
return c
|
||
}
|
||
// Perceived luminance (ITU-R BT.601).
|
||
luminance := 0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)
|
||
const maxReadableLuminance = 170
|
||
if luminance <= maxReadableLuminance {
|
||
return c
|
||
}
|
||
// Scale each channel down so the color darkens towards black while
|
||
// keeping its hue, ending up comfortably below the threshold.
|
||
scale := maxReadableLuminance / luminance * 0.75
|
||
return fmt.Sprintf("#%02x%02x%02x", clampByte(float64(r)*scale), clampByte(float64(g)*scale), clampByte(float64(b)*scale))
|
||
}
|
||
|
||
// parseHexColor parses a "#rrggbb" string into its red/green/blue
|
||
// components. ok is false for anything else (including 3-digit shorthand
|
||
// hex, which colorOrDefault/hexColorRe never produce).
|
||
func parseHexColor(s string) (r, g, b int, ok bool) {
|
||
s = strings.TrimPrefix(s, "#")
|
||
if len(s) != 6 {
|
||
return 0, 0, 0, false
|
||
}
|
||
v, err := strconv.ParseInt(s, 16, 32)
|
||
if err != nil {
|
||
return 0, 0, 0, false
|
||
}
|
||
return int(v >> 16 & 0xff), int(v >> 8 & 0xff), int(v & 0xff), true
|
||
}
|
||
|
||
func clampByte(v float64) int {
|
||
if v < 0 {
|
||
return 0
|
||
}
|
||
if v > 255 {
|
||
return 255
|
||
}
|
||
return int(v)
|
||
}
|
||
|
||
|
||
|
||
// EventDetail is the read-only detail view for a single event, opened when
|
||
// the user clicks an event in the month or week grid. It shows the
|
||
// event's fields and, when the calendar is writable, offers an Edit link.
|
||
templ EventDetail(username string, data EventDetailData) {
|
||
@Layout("Calendar", username) {
|
||
<div class="flex items-center justify-between gap-3 flex-wrap mb-6">
|
||
<a href="/web/calendar" class="text-sm text-indigo-600 hover:underline">← Calendar</a>
|
||
<div class="flex items-center gap-3">
|
||
if data.Form.Writable {
|
||
<a href={ templ.URL("/web/calendar/" + data.Form.CalRef + "/" + data.Form.ID + "/edit") }
|
||
class="bg-indigo-600 text-white rounded-md px-3 py-1.5 text-sm font-medium hover:bg-indigo-700">
|
||
Edit
|
||
</a>
|
||
}
|
||
<a href={ templ.URL("/web/calendar/" + data.Form.CalRef + "/" + data.Form.ID + "/export") }
|
||
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">
|
||
Export .ics
|
||
</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-2xl">
|
||
<div class="flex items-start gap-3 border-b border-gray-100 pb-5 mb-5">
|
||
<span class="w-3 h-3 rounded-full mt-2 shrink-0 ring-1 ring-inset ring-black/10" style={ "background-color: " + colorOrDefault(data.Color) }></span>
|
||
<div class="min-w-0">
|
||
<h1 class="text-2xl font-semibold break-words leading-tight">{ data.Form.Summary }</h1>
|
||
<p class="text-sm text-gray-500 mt-1">{ data.Form.CalendarLabel }</p>
|
||
</div>
|
||
</div>
|
||
|
||
if !data.Form.Writable {
|
||
<p class="mb-5 text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded px-3 py-2">
|
||
This calendar was shared with you as read-only — you can view this event but not change it.
|
||
</p>
|
||
}
|
||
|
||
<dl class="space-y-4">
|
||
<div>
|
||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-400 mb-1">When</dt>
|
||
<dd class="text-base text-gray-900">{ eventRangeText(data.Form) }</dd>
|
||
</div>
|
||
if data.Form.Location != "" {
|
||
<div>
|
||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-400 mb-1">Location</dt>
|
||
<dd class="text-base text-gray-900 break-words">{ data.Form.Location }</dd>
|
||
</div>
|
||
}
|
||
if data.Form.Description != "" {
|
||
<div>
|
||
<dt class="text-xs font-medium uppercase tracking-wide text-gray-400 mb-1">Description</dt>
|
||
<dd class="text-base text-gray-900 whitespace-pre-wrap break-words">{ data.Form.Description }</dd>
|
||
</div>
|
||
}
|
||
</dl>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
// eventRangeText renders a human-readable "when" string for an event:
|
||
// a single timed event reads "Aug 5, 2026, 14:00 – 15:00"; a multi-day
|
||
// range reads "Aug 5 – 7, 2026"; a single all-day date reads
|
||
// "August 5, 2026".
|
||
func eventRangeText(f EventFormData) string {
|
||
if f.AllDay {
|
||
if f.StartDate != f.EndDate {
|
||
s, err := dateOnly(f.StartDate)
|
||
if err != nil {
|
||
return f.StartDate
|
||
}
|
||
e, err := dateOnly(f.EndDate)
|
||
if err != nil {
|
||
return f.EndDate
|
||
}
|
||
if s.Year() == e.Year() && s.Month() == e.Month() {
|
||
return fmt.Sprintf("%s – %s, %d", s.Format("Jan 2"), e.Format("2"), s.Year())
|
||
}
|
||
return fmt.Sprintf("%s – %s", s.Format("Jan 2, 2006"), e.Format("Jan 2, 2006"))
|
||
}
|
||
d, err := dateOnly(f.StartDate)
|
||
if err != nil {
|
||
return f.StartDate
|
||
}
|
||
return d.Format("January 2, 2006")
|
||
}
|
||
|
||
s, err := dateTime(f.StartDate, f.StartTime)
|
||
if err != nil {
|
||
return f.StartDate
|
||
}
|
||
e, err := dateTime(f.EndDate, f.EndTime)
|
||
if err != nil {
|
||
return s.Format("January 2, 2006, 15:04")
|
||
}
|
||
if s.Day() == e.Day() {
|
||
return fmt.Sprintf("%s, %s – %s", s.Format("January 2, 2006"), s.Format("15:04"), e.Format("15:04"))
|
||
}
|
||
if s.Year() == e.Year() && s.Month() == e.Month() {
|
||
return fmt.Sprintf("%s – %s, %d", s.Format("Jan 2, 15:04"), e.Format("15:04"), s.Year())
|
||
}
|
||
return fmt.Sprintf("%s – %s", s.Format("Jan 2, 2006, 15:04"), e.Format("Jan 2, 2006, 15:04"))
|
||
}
|
||
|
||
func dateOnly(ds string) (time.Time, error) {
|
||
return time.ParseInLocation("2006-01-02", ds, time.Local)
|
||
}
|
||
|
||
func dateTime(ds, ts string) (time.Time, error) {
|
||
if ts == "" {
|
||
return time.ParseInLocation("2006-01-02", ds, time.Local)
|
||
}
|
||
return time.ParseInLocation("2006-01-02T15:04", ds+"T"+ts, time.Local)
|
||
}
|
||
|
||
templ EventForm(username string, data EventFormData, errMsg string) {
|
||
@Layout("Calendar", username) {
|
||
<a href="/web/calendar" class="text-sm text-indigo-600 hover:underline">← Calendar</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>
|
||
}
|
||
if data.ID != "" && !data.Writable {
|
||
<p class="mb-4 text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded px-3 py-2">
|
||
This calendar was shared with you as read-only — you can view this event but not change it.
|
||
</p>
|
||
}
|
||
<form
|
||
method="POST"
|
||
action={ eventFormAction(data) }
|
||
class="bg-white rounded-lg border border-gray-200 p-6 space-y-6 max-w-xl"
|
||
>
|
||
<fieldset disabled?={ data.ID != "" && !data.Writable } class="space-y-6">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700">Calendar</label>
|
||
if data.ID == "" {
|
||
<select name="calendar" class="mt-1 block w-full rounded-md border-gray-300 border px-3 py-2 shadow-sm">
|
||
for _, c := range data.Calendars {
|
||
<option value={ c.Ref } selected?={ c.Ref == data.CalRef }>{ c.Label }</option>
|
||
}
|
||
</select>
|
||
} else {
|
||
<p class="mt-1 text-sm text-gray-600">{ data.CalendarLabel }</p>
|
||
}
|
||
</div>
|
||
|
||
<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>
|
||
</fieldset>
|
||
|
||
<div class="flex items-center justify-between pt-2">
|
||
if data.ID == "" || data.Writable {
|
||
<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 != "" && data.Writable {
|
||
<form method="POST" action={ templ.URL("/web/calendar/" + data.CalRef + "/" + data.ID + "/delete") } onsubmit="return confirm('Delete this event?')">
|
||
<button type="submit" class="text-red-600 hover:underline text-sm">Delete event</button>
|
||
</form>
|
||
}
|
||
if data.ID != "" {
|
||
<a href={ templ.URL("/web/calendar/" + data.CalRef + "/" + data.ID + "/export") } class="text-indigo-600 hover:underline text-sm">Export .ics</a>
|
||
}
|
||
</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/new")
|
||
}
|
||
return templ.URL("/web/calendar/" + data.CalRef + "/" + data.ID + "/edit")
|
||
}
|
||
|
||
func weekdayLabels() []string {
|
||
return []string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}
|
||
}
|