diff --git a/pkg/campsite/types/public.go b/pkg/campsite/types/public.go index 8ec21c0..92c5ccd 100644 --- a/pkg/campsite/types/public.go +++ b/pkg/campsite/types/public.go @@ -9,6 +9,7 @@ import ( "context" gotemplate "html/template" "net/http" + "time" "golang.org/x/text/language" @@ -17,6 +18,7 @@ import ( "dev.tandem.ws/tandem/camper/pkg/database" httplib "dev.tandem.ws/tandem/camper/pkg/http" "dev.tandem.ws/tandem/camper/pkg/locale" + "dev.tandem.ws/tandem/camper/pkg/season" "dev.tandem.ws/tandem/camper/pkg/template" "dev.tandem.ws/tandem/camper/pkg/uuid" ) @@ -35,7 +37,7 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da http.NotFound(w, r) return } - page, err := newPublicPage(r.Context(), conn, user.Locale, head) + page, err := newPublicPage(r.Context(), company, conn, user.Locale, head) if database.ErrorIsNotFound(err) { http.NotFound(w, r) return @@ -55,6 +57,7 @@ type publicPage struct { Carousel []*carousel.Slide Prices []*typePrice Features []*typeFeature + Calendar *season.Calendar Spiel gotemplate.HTML Info gotemplate.HTML Facilities gotemplate.HTML @@ -74,7 +77,7 @@ type typeFeature struct { Name string } -func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale, slug string) (*publicPage, error) { +func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Conn, loc *locale.Locale, slug string) (*publicPage, error) { prices, err := collectPrices(ctx, conn, loc.Language, slug) if err != nil { return nil, err @@ -83,9 +86,14 @@ func newPublicPage(ctx context.Context, conn *database.Conn, loc *locale.Locale, if err != nil { return nil, err } + calendar, err := season.CollectSeasonCalendar(ctx, company, conn, time.Now().Year()) + if err != nil { + return nil, err + } page := &publicPage{ PublicPage: template.NewPublicPage(), Carousel: mustCollectSlides(ctx, conn, loc, slug), + Calendar: calendar, Prices: prices, Features: features, } @@ -129,7 +137,13 @@ func collectPrices(ctx context.Context, conn *database.Conn, language language.T group by season_id ) as option on option.season_id = season.season_id where season.active - `, language, slug) + union all + select $3 + , to_color($4)::text + , 1 + , '' + , false + `, language, slug, locale.PgettextNoop("Closed", "season"), season.UnsetColor) if err != nil { return nil, err } @@ -171,5 +185,5 @@ func collectFeatures(ctx context.Context, conn *database.Conn, language language func (p *publicPage) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) { p.Setup(r, user, company, conn) - template.MustRenderPublic(w, r, user, company, "campsite/type.gohtml", p) + template.MustRenderPublicFiles(w, r, user, company, p, "campsite/type.gohtml", "campsite/calendar.gohtml") } diff --git a/pkg/season/admin.go b/pkg/season/admin.go index 834a080..85dc484 100644 --- a/pkg/season/admin.go +++ b/pkg/season/admin.go @@ -26,7 +26,7 @@ import ( "dev.tandem.ws/tandem/camper/pkg/uuid" ) -const unsetColor = 0xd1d0df +const UnsetColor = 0xd1d0df type AdminHandler struct { locales locale.Locales @@ -124,7 +124,7 @@ func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, c if err != nil { panic(err) } - calendar, err := collectSeasonCalendar(r.Context(), company, conn, getCalendarYear(r.URL.Query())) + calendar, err := CollectSeasonCalendar(r.Context(), company, conn, getCalendarYear(r.URL.Query())) if err != nil { panic(err) } @@ -213,7 +213,7 @@ var longMonthNames = []string{ locale.PgettextNoop("December", "month"), } -func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *database.Conn, year int) (*seasonCalendar, error) { +func CollectSeasonCalendar(ctx context.Context, company *auth.Company, conn *database.Conn, year int) (*Calendar, error) { firstDay := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC) lastDay := time.Date(year, time.December, 31, 23, 59, 59, 0, time.UTC) rows, err := conn.Query(ctx, ` @@ -222,19 +222,19 @@ func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat from generate_series($2, $3, interval '1 day') as t(day) left join season_calendar on season_range @> t.day::date left join season on season.season_id = season_calendar.season_id and company_id = $4 - `, unsetColor, firstDay, lastDay, company.ID) + `, UnsetColor, firstDay, lastDay, company.ID) if err != nil { return nil, err } - var month *seasonMonth - var week seasonWeek - calendar := &seasonCalendar{ + var month *Month + var week Week + calendar := &Calendar{ Year: year, } weekday := int(time.Monday) for rows.Next() { - day := &seasonDay{} + day := &Day{} if err = rows.Scan(&day.Date, &day.Color); err != nil { return nil, err } @@ -242,32 +242,32 @@ func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat if month == nil || month.Month != dayMonth { if month != nil { for ; weekday != int(time.Sunday); weekday = (weekday + 1) % 7 { - week = append(week, &seasonDay{}) + week = append(week, &Day{}) } month.Weeks = append(month.Weeks, week) calendar.Months = append(calendar.Months, month) } - month = &seasonMonth{ + month = &Month{ Month: dayMonth, Name: longMonthNames[dayMonth-1], } - week = seasonWeek{} + week = Week{} weekday = int(time.Monday) dayWeekday := int(day.Date.Weekday()) for ; weekday != dayWeekday; weekday = (weekday + 1) % 7 { - week = append(week, &seasonDay{}) + week = append(week, &Day{}) } } week = append(week, day) weekday = (weekday + 1) % 7 if weekday == int(time.Monday) { month.Weeks = append(month.Weeks, week) - week = seasonWeek{} + week = Week{} } } if month != nil { for ; weekday != int(time.Sunday); weekday = (weekday + 1) % 7 { - week = append(week, &seasonDay{}) + week = append(week, &Day{}) } month.Weeks = append(month.Weeks, week) calendar.Months = append(calendar.Months, month) @@ -276,28 +276,28 @@ func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat return calendar, nil } -type seasonCalendar struct { +type Calendar struct { Year int - Months []*seasonMonth + Months []*Month Form *calendarForm } -type seasonMonth struct { +type Month struct { Month time.Month Name string - Weeks []seasonWeek + Weeks []Week } -type seasonWeek []*seasonDay +type Week []*Day -type seasonDay struct { +type Day struct { Date time.Time Color string } type seasonIndex struct { Seasons []*seasonEntry - Calendar *seasonCalendar + Calendar *Calendar } func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) { @@ -457,7 +457,7 @@ func mustCollectCalendarSeasons(ctx context.Context, company *auth.Company, conn from season where company_id = $3 and active - order by sort, name`, locale.PgettextNoop("Unset", "action"), unsetColor, company.ID) + order by sort, name`, locale.PgettextNoop("Unset", "action"), UnsetColor, company.ID) if err != nil { panic(err) } @@ -498,7 +498,7 @@ func (f *calendarForm) Valid(l *locale.Locale) bool { } func (f *calendarForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, year int) { - calendar, err := collectSeasonCalendar(r.Context(), company, conn, year) + calendar, err := CollectSeasonCalendar(r.Context(), company, conn, year) if err != nil { panic(err) } diff --git a/pkg/template/render.go b/pkg/template/render.go index 41937e9..3d882eb 100644 --- a/pkg/template/render.go +++ b/pkg/template/render.go @@ -12,6 +12,7 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" "dev.tandem.ws/tandem/camper/pkg/auth" @@ -94,6 +95,10 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ "dec": func(i int) int { return i - 1 }, + "hexToDec": func(s string) int { + num, _ := strconv.ParseInt(s, 16, 0) + return int(num) + }, }) templates = append(templates, "form.gohtml") files := make([]string, len(templates)) diff --git a/web/static/public.css b/web/static/public.css index eba288b..33ff9c4 100644 --- a/web/static/public.css +++ b/web/static/public.css @@ -821,6 +821,94 @@ dt { } } +.campsite_type_calendar button { + display: flex; + gap: 1em; + border: none; + cursor: pointer; +} + +.campsite_type_calendar form button:first-child, .campsite_type_calendar > header button { + min-width: 0; +} + +.campsite_type_calendar > header { + display: flex; + gap: 2rem; + justify-content: center; + align-items: center; +} + +.campsite_type_calendar > header button:first-of-type { + order: -1; +} + +.campsite_type_calendar > header button:first-of-type::before { + content: "←"; +} + +.campsite_type_calendar > header button:last-of-type::before { + content: "→"; +} + +.campsite_type_calendar > div { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(27rem, 1fr)); + grid-auto-rows: 1fr; + justify-content: center; + align-items: start; + gap: 1em; +} + +@media (max-width: 48rem) { + .campsite_type_calendar > div { + display: flex; + flex-direction: column; + } + + .campsite_type_calendar table { + width: 100%; + } +} + +.campsite_type_calendar table { + border-collapse: collapse; +} + +.campsite_type_calendar td { + width: calc(100% / 7); +} + +.campsite_type_calendar time { + --aa-brightness: calc(((var(--red) * 299) + (var(--green) * 587) + (var(--blue) * 114)) / 1000); + --aa-color: calc((var(--aa-brightness) - 128) * -1000); + background: rgb(var(--red), var(--green), var(--blue)); + color: rgb(var(--aa-color), var(--aa-color), var(--aa-color)); + display: flex; + width: 100%; + min-width: 3rem; + aspect-ratio: 1; + justify-content: center; + align-items: center; +} + +.campsite_type_calendar [aria-checked] { + border: 2px solid var(--camper--color--black); + position: relative; +} + +.campsite_type_calendar [aria-checked]::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: block; + background-color: var(--camper--color--black); + border-radius: 50%; + width: .8rem; + height: .8rem; +} body > footer { display: flex; diff --git a/web/templates/admin/season/calendar.gohtml b/web/templates/admin/season/calendar.gohtml index e331ffd..265d84f 100644 --- a/web/templates/admin/season/calendar.gohtml +++ b/web/templates/admin/season/calendar.gohtml @@ -1,5 +1,5 @@
- {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonCalendar*/ -}} + {{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.Calendar*/ -}}

{{ .Year }}

+ +
+
+ {{ range .Months -}} + + + + + + + + + + + + + + + {{ range .Weeks }} + + {{- range . }} + + {{- end }} + + {{- end }} + +
{{ pgettext .Name "month" }}
{{(pgettext "Mon" "day" )}}{{(pgettext "Tue" "day" )}}{{(pgettext "Wed" "day" )}}{{(pgettext "Thu" "day" )}}{{(pgettext "Fri" "day" )}}{{(pgettext "Sat" "day" )}}{{(pgettext "Sun" "day" )}}
+ {{- if .Color -}} + + {{- end -}} +
+ {{- end }} +
+
diff --git a/web/templates/public/campsite/type.gohtml b/web/templates/public/campsite/type.gohtml index f1f9ac9..4ece1c5 100644 --- a/web/templates/public/campsite/type.gohtml +++ b/web/templates/public/campsite/type.gohtml @@ -69,7 +69,7 @@ {{ if .HasOptions -}}
{{ printf (gettext "Starting from %s €/night") .PricePerNight }}
- {{- else -}} + {{- else if .PricePerNight -}}
{{ printf (gettext "%s €/night") .PricePerNight }}
{{- end }} {{ if gt .MinNights 1 -}} @@ -82,8 +82,7 @@ {{- end }} -
-
+ {{ template "calendar.gohtml" .Calendar }} {{ with .Features -}}