Show the campsite type’s calendar in an “infinite scroll” carousel
Oriol does not want to waste so much vertical space for the calendar, and wants it to show in a carousel, initially with only 6 months, and loading the next three each time the user scrolls past the last. I now use HTMx in the public page too for this auto-loading behavior, based on their “infinite scroll” example[0]. Had to put the /calendar URI inside campsites because in the calendar.gohtml i do not know the current type’s UUID, and can not use a relative URL to “add subdirectories”, because the type does not end with a slash. Had to change season.CollectCalendar to expect the first month and a number of months to show, to be able to load only 6 or 3 months after the current, for the initial carousel content, or after the last month of the carousel. [0]: https://htmx.org/examples/infinite-scroll/
This commit is contained in:
parent
6e7df4ca79
commit
96fb253354
|
@ -7,11 +7,14 @@ package campsite
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
"dev.tandem.ws/tandem/camper/pkg/campsite/types"
|
||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
"dev.tandem.ws/tandem/camper/pkg/season"
|
||||
"dev.tandem.ws/tandem/camper/pkg/template"
|
||||
)
|
||||
|
||||
type PublicHandler struct {
|
||||
|
@ -32,6 +35,33 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da
|
|||
switch head {
|
||||
case "types":
|
||||
h.types.Handler(user, company, conn).ServeHTTP(w, r)
|
||||
case "calendar":
|
||||
h.calendarHandler(user, company, conn).ServeHTTP(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PublicHandler) calendarHandler(user *auth.User, company *auth.Company, conn *database.Conn) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var head string
|
||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||
switch head {
|
||||
case "":
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
year := season.GetCalendarYear(r.URL.Query())
|
||||
month := season.GetCalendarMonth(r.URL.Query())
|
||||
date := time.Date(year, month, 1, 0, 0, 0, 0, time.UTC).AddDate(0, 1, 0)
|
||||
calendar, err := season.CollectSeasonCalendar(r.Context(), company, conn, date.Year(), date.Month(), 3)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
template.MustRenderPublicNoLayout(w, r, user, company, "campsite/calendar.gohtml", calendar)
|
||||
default:
|
||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
|
|
@ -94,7 +94,8 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
calendar, err := season.CollectSeasonCalendar(ctx, company, conn, time.Now().Year())
|
||||
now := time.Now()
|
||||
calendar, err := season.CollectSeasonCalendar(ctx, company, conn, now.Year(), now.Month(), 6)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -197,11 +197,11 @@ func newMediaPickerWithUploadForm(f *uploadForm, query url.Values) *mediaPicker
|
|||
}
|
||||
|
||||
func (picker *mediaPicker) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderNoLayout(w, r, user, company, "media/picker.gohtml", picker)
|
||||
template.MustRenderAdminNoLayout(w, r, user, company, "media/picker.gohtml", picker)
|
||||
}
|
||||
|
||||
func (picker *mediaPicker) MustRenderField(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||
template.MustRenderNoLayout(w, r, user, company, "media/field.gohtml", picker.Field)
|
||||
template.MustRenderAdminNoLayout(w, r, user, company, "media/field.gohtml", picker.Field)
|
||||
}
|
||||
|
||||
func uploadMedia(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
|
|
|
@ -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()), time.January, 12)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -136,7 +136,7 @@ func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, c
|
|||
page.MustRender(w, r, user, company)
|
||||
}
|
||||
|
||||
func getCalendarYear(query url.Values) int {
|
||||
func GetCalendarYear(query url.Values) int {
|
||||
yearStr := strings.TrimSpace(query.Get("year"))
|
||||
if yearStr != "" {
|
||||
if year, err := strconv.Atoi(yearStr); err == nil {
|
||||
|
@ -146,6 +146,16 @@ func getCalendarYear(query url.Values) int {
|
|||
return time.Now().Year()
|
||||
}
|
||||
|
||||
func GetCalendarMonth(query url.Values) time.Month {
|
||||
monthStr := strings.TrimSpace(query.Get("month"))
|
||||
if monthStr != "" {
|
||||
if month, err := strconv.Atoi(monthStr); err == nil {
|
||||
return time.Month(month)
|
||||
}
|
||||
}
|
||||
return time.Now().Month()
|
||||
}
|
||||
|
||||
func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) {
|
||||
rows, err := conn.Query(ctx, `
|
||||
select '/admin/seasons/' || season.slug
|
||||
|
@ -213,9 +223,9 @@ var longMonthNames = []string{
|
|||
locale.PgettextNoop("December", "month"),
|
||||
}
|
||||
|
||||
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)
|
||||
func CollectSeasonCalendar(ctx context.Context, company *auth.Company, conn *database.Conn, year int, firstMonth time.Month, monthCount int) (*Calendar, error) {
|
||||
firstDay := time.Date(year, firstMonth, 1, 0, 0, 0, 0, time.UTC)
|
||||
lastDay := firstDay.AddDate(0, monthCount, 0).Add(-1 * time.Second)
|
||||
rows, err := conn.Query(ctx, `
|
||||
select t.day::date
|
||||
, to_color(coalesce(color, $1)) as color
|
||||
|
@ -248,6 +258,7 @@ func CollectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat
|
|||
calendar.Months = append(calendar.Months, month)
|
||||
}
|
||||
month = &Month{
|
||||
Year: day.Date.Year(),
|
||||
Month: dayMonth,
|
||||
Name: longMonthNames[dayMonth-1],
|
||||
}
|
||||
|
@ -283,6 +294,7 @@ type Calendar struct {
|
|||
}
|
||||
|
||||
type Month struct {
|
||||
Year int
|
||||
Month time.Month
|
||||
Name string
|
||||
Weeks []Week
|
||||
|
@ -394,7 +406,7 @@ func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *au
|
|||
|
||||
func serveSeasonCalendar(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
f := newCalendarForm(r.Context(), company, conn)
|
||||
f.MustRender(w, r, user, company, conn, getCalendarYear(r.URL.Query()))
|
||||
f.MustRender(w, r, user, company, conn, GetCalendarYear(r.URL.Query()))
|
||||
}
|
||||
|
||||
func updateSeasonCalendar(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||
|
@ -416,7 +428,7 @@ func updateSeasonCalendar(w http.ResponseWriter, r *http.Request, user *auth.Use
|
|||
f.StartDate.Val = ""
|
||||
f.EndDate.Val = ""
|
||||
}
|
||||
f.MustRender(w, r, user, company, conn, getCalendarYear(r.Form))
|
||||
f.MustRender(w, r, user, company, conn, GetCalendarYear(r.Form))
|
||||
}
|
||||
|
||||
type calendarForm struct {
|
||||
|
@ -498,10 +510,10 @@ 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, time.January, 12)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
calendar.Form = f
|
||||
template.MustRenderNoLayout(w, r, user, company, "season/calendar.gohtml", calendar)
|
||||
template.MustRenderAdminNoLayout(w, r, user, company, "season/calendar.gohtml", calendar)
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
||||
|
@ -44,7 +45,7 @@ func MustRenderAdminFiles(w io.Writer, r *http.Request, user *auth.User, company
|
|||
mustRenderLayout(w, user, company, adminTemplateFile, data, filenames...)
|
||||
}
|
||||
|
||||
func MustRenderNoLayout(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) {
|
||||
func MustRenderAdminNoLayout(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) {
|
||||
mustRenderLayout(w, user, company, adminTemplateFile, data, filename)
|
||||
}
|
||||
|
||||
|
@ -53,6 +54,10 @@ func MustRenderPublic(w io.Writer, r *http.Request, user *auth.User, company *au
|
|||
mustRenderLayout(w, user, company, publicTemplateFile, data, layout, filename)
|
||||
}
|
||||
|
||||
func MustRenderPublicNoLayout(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, filename string, data interface{}) {
|
||||
mustRenderLayout(w, user, company, publicTemplateFile, data, filename)
|
||||
}
|
||||
|
||||
func MustRenderPublicFiles(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, data interface{}, filenames ...string) {
|
||||
layout := "layout.gohtml"
|
||||
filenames = append([]string{layout}, filenames...)
|
||||
|
@ -95,6 +100,18 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
|
|||
"dec": func(i int) int {
|
||||
return i - 1
|
||||
},
|
||||
"int": func(v interface{}) int {
|
||||
switch v := v.(type) {
|
||||
case int:
|
||||
return v
|
||||
case time.Weekday:
|
||||
return int(v)
|
||||
case time.Month:
|
||||
return int(v)
|
||||
default:
|
||||
panic(fmt.Errorf("Could not convert to integer"))
|
||||
}
|
||||
},
|
||||
"hexToDec": func(s string) int {
|
||||
num, _ := strconv.ParseInt(s, 16, 0)
|
||||
return int(num)
|
||||
|
|
|
@ -835,14 +835,10 @@ dt {
|
|||
.campsite_type_calendar > header {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.campsite_type_calendar > header button:first-of-type {
|
||||
order: -1;
|
||||
}
|
||||
|
||||
.campsite_type_calendar > header button:first-of-type::before {
|
||||
content: "←";
|
||||
}
|
||||
|
@ -852,27 +848,20 @@ dt {
|
|||
}
|
||||
|
||||
.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;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.campsite_type_calendar > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.campsite_type_calendar table {
|
||||
width: 100%;
|
||||
}
|
||||
overflow-x: scroll;
|
||||
scroll-behavior: smooth;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||
}
|
||||
|
||||
.campsite_type_calendar table {
|
||||
border-collapse: collapse;
|
||||
flex: 0 0 27rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.campsite_type_calendar td {
|
||||
|
|
|
@ -2,48 +2,42 @@
|
|||
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
<article class="campsite_type_calendar">
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.Calendar*/ -}}
|
||||
<header>
|
||||
<h3>{{ .Year }}</h3>
|
||||
<button type="button"
|
||||
data-hx-get="/{{ currentLocale }}/seasons/calendar?year={{ dec .Year }}"
|
||||
><span class="sr-only">{{ pgettext "Prev" "navigation" }}</span></button>
|
||||
<button type="button"
|
||||
data-hx-get="/{{ currentLocale }}/seasons/calendar?year={{ inc .Year }}"
|
||||
><span class="sr-only">{{ pgettext "Next" "navigation" }}</span></button>
|
||||
</header>
|
||||
<div>
|
||||
{{ range .Months -}}
|
||||
<table class="month">
|
||||
<caption>{{ pgettext .Name "month" }}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{(pgettext "Mon" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Tue" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Wed" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Thu" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Fri" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Sat" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Sun" "day" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Weeks }}
|
||||
<tr>
|
||||
{{- range . }}
|
||||
<td>
|
||||
{{- if .Color -}}
|
||||
<time style="--red: {{ slice .Color 1 3 | hexToDec }}; --green: {{ slice .Color 3 5 | hexToDec }}; --blue: {{ slice .Color 5 7 | hexToDec }}"
|
||||
datetime="{{ .Date.Format "2006-01-02" }}"
|
||||
>{{ .Date.Format "2" }}</time>
|
||||
{{- end -}}
|
||||
</td>
|
||||
{{- end }}
|
||||
</tr>
|
||||
{{ $last := dec (len .Months)}}
|
||||
{{ range $index, $month := .Months -}}
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.Month */ -}}
|
||||
<table class="month"
|
||||
{{ if eq $index $last -}}
|
||||
data-hx-get="/{{ currentLocale }}/campsites/calendar?year={{ .Year }}&month={{ .Month | int }}"
|
||||
data-hx-trigger="intersect once"
|
||||
data-hx-swap="afterend"
|
||||
{{- end }}
|
||||
>
|
||||
<caption>{{ pgettext .Name "month" }} {{ .Year }}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{(pgettext "Mon" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Tue" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Wed" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Thu" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Fri" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Sat" "day" )}}</th>
|
||||
<th scope="col">{{(pgettext "Sun" "day" )}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Weeks }}
|
||||
<tr>
|
||||
{{- range . }}
|
||||
<td>
|
||||
{{- if .Color -}}
|
||||
<time style="--red: {{ slice .Color 1 3 | hexToDec }}; --green: {{ slice .Color 3 5 | hexToDec }}; --blue: {{ slice .Color 5 7 | hexToDec }}"
|
||||
datetime="{{ .Date.Format "2006-01-02" }}"
|
||||
>{{ .Date.Format "2" }}</time>
|
||||
{{- end -}}
|
||||
</td>
|
||||
{{- end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</tr>
|
||||
{{- end }}
|
||||
</div>
|
||||
</article>
|
||||
</tbody>
|
||||
</table>
|
||||
{{- end }}
|
||||
|
|
|
@ -82,7 +82,17 @@
|
|||
{{- end }}
|
||||
</div>
|
||||
|
||||
{{ template "calendar.gohtml" .Calendar }}
|
||||
<article class="campsite_type_calendar">
|
||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.Calendar*/ -}}
|
||||
<header>
|
||||
<h3 class="sr-only">{{( pgettext "Calendar" "title" )}}</h3>
|
||||
<button type="button"><span class="sr-only">{{ pgettext "Prev" "navigation" }}</span></button>
|
||||
<button type="button"><span class="sr-only">{{ pgettext "Next" "navigation" }}</span></button>
|
||||
</header>
|
||||
<div>
|
||||
{{ template "calendar.gohtml" .Calendar }}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{{ with .Features -}}
|
||||
<article class="campsite_type_features">
|
||||
|
@ -112,4 +122,20 @@
|
|||
|
||||
{{ template "carouselInit" }}
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const calendar = document.querySelector('.campsite_type_calendar');
|
||||
const carousel = calendar.querySelector('div');
|
||||
const month = carousel.querySelector('table');
|
||||
|
||||
const left = calendar.querySelector('header button:first-of-type');
|
||||
left.addEventListener('click', () => carousel.scrollLeft -= month.clientWidth);
|
||||
|
||||
const right = calendar.querySelector('header button:last-of-type');
|
||||
right.addEventListener('click', () => carousel.scrollLeft += month.clientWidth);
|
||||
})();
|
||||
</script>
|
||||
|
||||
{{- end }}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
{{ range .LocalizedAlternates -}}
|
||||
<link rel="alternate" hreflang="{{ .Lang }}" href="{{ .HRef }}"/>
|
||||
{{ end }}
|
||||
<script src="/static/htmx@1.9.3.min.js"></script>
|
||||
{{- block "head" . }}{{ end }}
|
||||
</head>
|
||||
<body>
|
||||
|
|
Loading…
Reference in New Issue