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 (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/campsite/types"
|
"dev.tandem.ws/tandem/camper/pkg/campsite/types"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
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 {
|
type PublicHandler struct {
|
||||||
|
@ -32,6 +35,33 @@ func (h *PublicHandler) Handler(user *auth.User, company *auth.Company, conn *da
|
||||||
switch head {
|
switch head {
|
||||||
case "types":
|
case "types":
|
||||||
h.types.Handler(user, company, conn).ServeHTTP(w, r)
|
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:
|
default:
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -94,7 +94,8 @@ func newPublicPage(ctx context.Context, company *auth.Company, conn *database.Co
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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) {
|
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) {
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -136,7 +136,7 @@ func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, c
|
||||||
page.MustRender(w, r, user, company)
|
page.MustRender(w, r, user, company)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCalendarYear(query url.Values) int {
|
func GetCalendarYear(query url.Values) int {
|
||||||
yearStr := strings.TrimSpace(query.Get("year"))
|
yearStr := strings.TrimSpace(query.Get("year"))
|
||||||
if yearStr != "" {
|
if yearStr != "" {
|
||||||
if year, err := strconv.Atoi(yearStr); err == nil {
|
if year, err := strconv.Atoi(yearStr); err == nil {
|
||||||
|
@ -146,6 +146,16 @@ func getCalendarYear(query url.Values) int {
|
||||||
return time.Now().Year()
|
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) {
|
func collectSeasonEntries(ctx context.Context, company *auth.Company, conn *database.Conn) ([]*seasonEntry, error) {
|
||||||
rows, err := conn.Query(ctx, `
|
rows, err := conn.Query(ctx, `
|
||||||
select '/admin/seasons/' || season.slug
|
select '/admin/seasons/' || season.slug
|
||||||
|
@ -213,9 +223,9 @@ var longMonthNames = []string{
|
||||||
locale.PgettextNoop("December", "month"),
|
locale.PgettextNoop("December", "month"),
|
||||||
}
|
}
|
||||||
|
|
||||||
func CollectSeasonCalendar(ctx context.Context, company *auth.Company, conn *database.Conn, year int) (*Calendar, error) {
|
func CollectSeasonCalendar(ctx context.Context, company *auth.Company, conn *database.Conn, year int, firstMonth time.Month, monthCount int) (*Calendar, error) {
|
||||||
firstDay := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
|
firstDay := time.Date(year, firstMonth, 1, 0, 0, 0, 0, time.UTC)
|
||||||
lastDay := time.Date(year, time.December, 31, 23, 59, 59, 0, time.UTC)
|
lastDay := firstDay.AddDate(0, monthCount, 0).Add(-1 * time.Second)
|
||||||
rows, err := conn.Query(ctx, `
|
rows, err := conn.Query(ctx, `
|
||||||
select t.day::date
|
select t.day::date
|
||||||
, to_color(coalesce(color, $1)) as color
|
, 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)
|
calendar.Months = append(calendar.Months, month)
|
||||||
}
|
}
|
||||||
month = &Month{
|
month = &Month{
|
||||||
|
Year: day.Date.Year(),
|
||||||
Month: dayMonth,
|
Month: dayMonth,
|
||||||
Name: longMonthNames[dayMonth-1],
|
Name: longMonthNames[dayMonth-1],
|
||||||
}
|
}
|
||||||
|
@ -283,6 +294,7 @@ type Calendar struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Month struct {
|
type Month struct {
|
||||||
|
Year int
|
||||||
Month time.Month
|
Month time.Month
|
||||||
Name string
|
Name string
|
||||||
Weeks []Week
|
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) {
|
func serveSeasonCalendar(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||||
f := newCalendarForm(r.Context(), company, 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) {
|
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.StartDate.Val = ""
|
||||||
f.EndDate.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 {
|
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) {
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
calendar.Form = f
|
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"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
httplib "dev.tandem.ws/tandem/camper/pkg/http"
|
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...)
|
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)
|
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)
|
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) {
|
func MustRenderPublicFiles(w io.Writer, r *http.Request, user *auth.User, company *auth.Company, data interface{}, filenames ...string) {
|
||||||
layout := "layout.gohtml"
|
layout := "layout.gohtml"
|
||||||
filenames = append([]string{layout}, filenames...)
|
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 {
|
"dec": func(i int) int {
|
||||||
return i - 1
|
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 {
|
"hexToDec": func(s string) int {
|
||||||
num, _ := strconv.ParseInt(s, 16, 0)
|
num, _ := strconv.ParseInt(s, 16, 0)
|
||||||
return int(num)
|
return int(num)
|
||||||
|
|
|
@ -835,14 +835,10 @@ dt {
|
||||||
.campsite_type_calendar > header {
|
.campsite_type_calendar > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.campsite_type_calendar > header button:first-of-type {
|
|
||||||
order: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.campsite_type_calendar > header button:first-of-type::before {
|
.campsite_type_calendar > header button:first-of-type::before {
|
||||||
content: "←";
|
content: "←";
|
||||||
}
|
}
|
||||||
|
@ -851,28 +847,21 @@ dt {
|
||||||
content: "→";
|
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 {
|
.campsite_type_calendar > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: nowrap;
|
||||||
}
|
gap: 1em;
|
||||||
|
overflow-x: scroll;
|
||||||
.campsite_type_calendar table {
|
scroll-behavior: smooth;
|
||||||
width: 100%;
|
scrollbar-width: none;
|
||||||
}
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: -ms-autohiding-scrollbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
.campsite_type_calendar table {
|
.campsite_type_calendar table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
flex: 0 0 27rem;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.campsite_type_calendar td {
|
.campsite_type_calendar td {
|
||||||
|
|
|
@ -2,21 +2,17 @@
|
||||||
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
SPDX-FileCopyrightText: 2023 jordi fita mas <jordi@tandem.blog>
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
<article class="campsite_type_calendar">
|
{{ $last := dec (len .Months)}}
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.Calendar*/ -}}
|
{{ range $index, $month := .Months -}}
|
||||||
<header>
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.Month */ -}}
|
||||||
<h3>{{ .Year }}</h3>
|
<table class="month"
|
||||||
<button type="button"
|
{{ if eq $index $last -}}
|
||||||
data-hx-get="/{{ currentLocale }}/seasons/calendar?year={{ dec .Year }}"
|
data-hx-get="/{{ currentLocale }}/campsites/calendar?year={{ .Year }}&month={{ .Month | int }}"
|
||||||
><span class="sr-only">{{ pgettext "Prev" "navigation" }}</span></button>
|
data-hx-trigger="intersect once"
|
||||||
<button type="button"
|
data-hx-swap="afterend"
|
||||||
data-hx-get="/{{ currentLocale }}/seasons/calendar?year={{ inc .Year }}"
|
{{- end }}
|
||||||
><span class="sr-only">{{ pgettext "Next" "navigation" }}</span></button>
|
>
|
||||||
</header>
|
<caption>{{ pgettext .Name "month" }} {{ .Year }}</caption>
|
||||||
<div>
|
|
||||||
{{ range .Months -}}
|
|
||||||
<table class="month">
|
|
||||||
<caption>{{ pgettext .Name "month" }}</caption>
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{(pgettext "Mon" "day" )}}</th>
|
<th scope="col">{{(pgettext "Mon" "day" )}}</th>
|
||||||
|
@ -45,5 +41,3 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
|
@ -82,7 +82,17 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<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 }}
|
{{ template "calendar.gohtml" .Calendar }}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
{{ with .Features -}}
|
{{ with .Features -}}
|
||||||
<article class="campsite_type_features">
|
<article class="campsite_type_features">
|
||||||
|
@ -112,4 +122,20 @@
|
||||||
|
|
||||||
{{ template "carouselInit" }}
|
{{ 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 }}
|
{{- end }}
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
{{ range .LocalizedAlternates -}}
|
{{ range .LocalizedAlternates -}}
|
||||||
<link rel="alternate" hreflang="{{ .Lang }}" href="{{ .HRef }}"/>
|
<link rel="alternate" hreflang="{{ .Lang }}" href="{{ .HRef }}"/>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<script src="/static/htmx@1.9.3.min.js"></script>
|
||||||
{{- block "head" . }}{{ end }}
|
{{- block "head" . }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
Loading…
Reference in New Issue