Add a year navigator to the seasons’ calendar
This commit is contained in:
parent
47ec317010
commit
6939670dfc
|
@ -9,6 +9,9 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgtype"
|
"github.com/jackc/pgtype"
|
||||||
|
@ -56,10 +59,12 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
||||||
}
|
}
|
||||||
case "range":
|
case "range":
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
serveSeasonCalendar(w, r, user, company, conn)
|
||||||
case http.MethodPut:
|
case http.MethodPut:
|
||||||
updateSeasonCalendar(w, r, user, company, conn)
|
updateSeasonCalendar(w, r, user, company, conn)
|
||||||
default:
|
default:
|
||||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPut)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if !uuid.Valid(head) {
|
if !uuid.Valid(head) {
|
||||||
|
@ -91,7 +96,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, 2023)
|
calendar, err := collectSeasonCalendar(r.Context(), company, conn, getCalendarYear(r.URL.Query()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -103,6 +108,16 @@ 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 {
|
||||||
|
yearStr := strings.TrimSpace(query.Get("year"))
|
||||||
|
if yearStr != "" {
|
||||||
|
if year, err := strconv.Atoi(yearStr); err == nil {
|
||||||
|
return year
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Now().Year()
|
||||||
|
}
|
||||||
|
|
||||||
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 slug
|
select slug
|
||||||
|
@ -167,7 +182,9 @@ func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat
|
||||||
|
|
||||||
var month *seasonMonth
|
var month *seasonMonth
|
||||||
var week seasonWeek
|
var week seasonWeek
|
||||||
calendar := &seasonCalendar{}
|
calendar := &seasonCalendar{
|
||||||
|
Year: year,
|
||||||
|
}
|
||||||
weekday := int(time.Monday)
|
weekday := int(time.Monday)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
day := &seasonDay{}
|
day := &seasonDay{}
|
||||||
|
@ -213,6 +230,7 @@ func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat
|
||||||
}
|
}
|
||||||
|
|
||||||
type seasonCalendar struct {
|
type seasonCalendar struct {
|
||||||
|
Year int
|
||||||
Months []*seasonMonth
|
Months []*seasonMonth
|
||||||
Form *calendarForm
|
Form *calendarForm
|
||||||
}
|
}
|
||||||
|
@ -327,6 +345,11 @@ func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *au
|
||||||
template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f)
|
template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
f := newCalendarForm(r.Context(), company, conn)
|
f := newCalendarForm(r.Context(), company, conn)
|
||||||
if ok, err := form.Handle(f, w, r, user); err != nil {
|
if ok, err := form.Handle(f, w, r, user); err != nil {
|
||||||
|
@ -346,7 +369,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)
|
f.MustRender(w, r, user, company, conn, getCalendarYear(r.Form))
|
||||||
}
|
}
|
||||||
|
|
||||||
type calendarForm struct {
|
type calendarForm struct {
|
||||||
|
@ -427,8 +450,8 @@ func (f *calendarForm) Valid(l *locale.Locale) bool {
|
||||||
return v.AllOK
|
return v.AllOK
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *calendarForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
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, 2023)
|
calendar, err := collectSeasonCalendar(r.Context(), company, conn, year)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,12 @@ func mustRenderLayout(w io.Writer, user *auth.User, company *auth.Company, templ
|
||||||
"queryEscape": func(s string) string {
|
"queryEscape": func(s string) string {
|
||||||
return url.QueryEscape(s)
|
return url.QueryEscape(s)
|
||||||
},
|
},
|
||||||
|
"inc": func(i int) int {
|
||||||
|
return i + 1
|
||||||
|
},
|
||||||
|
"dec": func(i int) int {
|
||||||
|
return i - 1
|
||||||
|
},
|
||||||
})
|
})
|
||||||
templates = append(templates, "form.gohtml")
|
templates = append(templates, "form.gohtml")
|
||||||
files := make([]string, len(templates))
|
files := make([]string, len(templates))
|
||||||
|
|
|
@ -228,14 +228,14 @@ body > a[href="#content"]:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* header */
|
/* header */
|
||||||
header {
|
body > header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--camper--header--background-color);
|
background-color: var(--camper--header--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
header, body > nav a {
|
body > header, body > nav a {
|
||||||
padding: 0 3rem;
|
padding: 0 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,7 +497,38 @@ textarea {
|
||||||
background-color: #ffeeaa;
|
background-color: #ffeeaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.season-calendar {
|
/* calendar */
|
||||||
|
.season-calendar button {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar form button:first-child, .season-calendar > header button {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar > header {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar > header button:first-of-type {
|
||||||
|
order: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar > header button:first-of-type::before {
|
||||||
|
content: "←";
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar > header button:last-of-type::before {
|
||||||
|
content: "→";
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar > div {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, auto);
|
grid-template-columns: repeat(3, auto);
|
||||||
grid-auto-rows: 1fr;
|
grid-auto-rows: 1fr;
|
||||||
|
@ -507,11 +538,11 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 48rem) {
|
@media (max-width: 48rem) {
|
||||||
.season-calendar {
|
.season-calendar > div {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.season-calendar table {
|
.season-calendar table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -553,22 +584,14 @@ textarea {
|
||||||
height: .8rem;
|
height: .8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.season-calendar form button {
|
|
||||||
display: flex;
|
|
||||||
gap: 1em;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.season-calendar form button:first-child {
|
.season-calendar form button:first-child {
|
||||||
min-width: 0;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.season-calendar form button:hover, .season-calendar form button:first-child:hover {
|
.season-calendar form button:hover, .season-calendar form button:first-child:hover {
|
||||||
background-color: var(--camper--color--hay);
|
background-color: var(--camper--color--hay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,58 +1,69 @@
|
||||||
<div class="season-calendar" data-hx-target="this" data-hx-swap="outerHTML">
|
<article class="season-calendar" data-hx-target="this" data-hx-swap="outerHTML">
|
||||||
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonCalendar*/ -}}
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonCalendar*/ -}}
|
||||||
{{ range .Months -}}
|
<header>
|
||||||
<table class="month">
|
<h3>{{ .Year }}</h3>
|
||||||
<caption>{{ pgettext .Name "month" }}</caption>
|
<button type="button"
|
||||||
<thead>
|
data-hx-get="/admin/seasons/range?year={{ dec .Year }}"><span class="sr-only">{{ pgettext "Prev" "navigation" }}</span></button>
|
||||||
<tr>
|
<button type="button"
|
||||||
<th scope="col">{{(pgettext "Mon" "day" )}}</th>
|
data-hx-get="/admin/seasons/range?year={{ inc .Year }}"><span class="sr-only">{{ pgettext "Next" "navigation" }}</span></button>
|
||||||
<th scope="col">{{(pgettext "Tue" "day" )}}</th>
|
</header>
|
||||||
<th scope="col">{{(pgettext "Wed" "day" )}}</th>
|
<div>
|
||||||
<th scope="col">{{(pgettext "Thu" "day" )}}</th>
|
{{ range .Months -}}
|
||||||
<th scope="col">{{(pgettext "Fri" "day" )}}</th>
|
<table class="month">
|
||||||
<th scope="col">{{(pgettext "Sat" "day" )}}</th>
|
<caption>{{ pgettext .Name "month" }}</caption>
|
||||||
<th scope="col">{{(pgettext "Sun" "day" )}}</th>
|
<thead>
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{{ range .Weeks }}
|
|
||||||
<tr>
|
<tr>
|
||||||
{{- range . }}
|
<th scope="col">{{(pgettext "Mon" "day" )}}</th>
|
||||||
<td>
|
<th scope="col">{{(pgettext "Tue" "day" )}}</th>
|
||||||
{{- if .Color -}}
|
<th scope="col">{{(pgettext "Wed" "day" )}}</th>
|
||||||
<time style="background-color: {{ .Color }}"
|
<th scope="col">{{(pgettext "Thu" "day" )}}</th>
|
||||||
datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2" }}</time>
|
<th scope="col">{{(pgettext "Fri" "day" )}}</th>
|
||||||
{{- end -}}
|
<th scope="col">{{(pgettext "Sat" "day" )}}</th>
|
||||||
</td>
|
<th scope="col">{{(pgettext "Sun" "day" )}}</th>
|
||||||
{{- end }}
|
|
||||||
</tr>
|
</tr>
|
||||||
{{- end }}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{{ range .Weeks }}
|
||||||
{{- end }}
|
<tr>
|
||||||
{{ with .Form }}
|
{{- range . }}
|
||||||
<dialog>
|
<td>
|
||||||
<form data-hx-put="/admin/seasons/range">
|
{{- if .Color -}}
|
||||||
{{ CSRFInput }}
|
<time style="background-color: {{ .Color }}"
|
||||||
{{ with .StartDate }}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{ end }}
|
datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2" }}</time>
|
||||||
{{ with .EndDate }}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{ end }}
|
{{- end -}}
|
||||||
<footer>
|
</td>
|
||||||
<button type="submit" name="season_id" value=""><span class="sr-only">{{( pgettext "Cancel" "action" )}}</span></button>
|
{{- end }}
|
||||||
{{ range .Seasons -}}
|
</tr>
|
||||||
<button type="submit" name="season_id" value="{{ .Slug }}">
|
{{- end }}
|
||||||
<svg width="20px" height="20px">
|
</tbody>
|
||||||
<circle cx="50%" cy="50%" r="49%" fill="{{ .Color }}" stroke="#000" stroke-width=".5"/>
|
</table>
|
||||||
</svg>
|
{{- end }}
|
||||||
{{ .Name }}
|
{{ with .Form }}
|
||||||
</button>
|
<dialog>
|
||||||
{{- end }}
|
<form data-hx-put="/admin/seasons/range">
|
||||||
</footer>
|
{{ CSRFInput }}
|
||||||
</form>
|
<input type="hidden" name="year" value="{{ $.Year }}">
|
||||||
</dialog>
|
{{ with .StartDate }}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{ end }}
|
||||||
{{ end }}
|
{{ with .EndDate }}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{ end }}
|
||||||
|
<footer>
|
||||||
|
<button type="submit"><span class="sr-only">{{( pgettext "Cancel" "action" )}}</span></button>
|
||||||
|
{{ range .Seasons -}}
|
||||||
|
<button type="submit" name="season_id" value="{{ .Slug }}">
|
||||||
|
<svg width="20px" height="20px">
|
||||||
|
<circle cx="50%" cy="50%" r="49%" fill="{{ .Color }}" stroke="#000"
|
||||||
|
stroke-width=".5"/>
|
||||||
|
</svg>
|
||||||
|
{{ .Name }}
|
||||||
|
</button>
|
||||||
|
{{- end }}
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import {setupCalendar} from "/static/camper.js";
|
import {setupCalendar} from "/static/camper.js";
|
||||||
|
|
||||||
setupCalendar(document.querySelector('.season-calendar'))
|
setupCalendar(document.querySelector('.season-calendar'))
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</article>
|
||||||
|
|
Loading…
Reference in New Issue