Make seasons’ calendar dynamic and allow to set/unset ranges
The CSS is not very good, but for testing purposes it will work.
This commit is contained in:
parent
ea2fe8848b
commit
e584e29f46
|
@ -12,6 +12,7 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/locale"
|
"dev.tandem.ws/tandem/camper/pkg/locale"
|
||||||
|
@ -83,6 +84,11 @@ func (v *Validator) CheckValidPostalCode(ctx context.Context, conn *database.Con
|
||||||
return v.Check(input, match, message), nil
|
return v.Check(input, match, message), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *Validator) CheckValidDate(field *Input, message string) bool {
|
||||||
|
_, err := time.Parse("2006-01-02", field.Val)
|
||||||
|
return v.Check(field, err == nil, message)
|
||||||
|
}
|
||||||
|
|
||||||
func (v *Validator) CheckPasswordConfirmation(password *Input, confirm *Input, message string) bool {
|
func (v *Validator) CheckPasswordConfirmation(password *Input, confirm *Input, message string) bool {
|
||||||
return v.Check(confirm, password.Val == confirm.Val, message)
|
return v.Check(confirm, password.Val == confirm.Val, message)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,9 +7,12 @@ package season
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
|
|
||||||
"dev.tandem.ws/tandem/camper/pkg/auth"
|
"dev.tandem.ws/tandem/camper/pkg/auth"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/database"
|
"dev.tandem.ws/tandem/camper/pkg/database"
|
||||||
"dev.tandem.ws/tandem/camper/pkg/form"
|
"dev.tandem.ws/tandem/camper/pkg/form"
|
||||||
|
@ -19,6 +22,8 @@ import (
|
||||||
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
"dev.tandem.ws/tandem/camper/pkg/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const unsetColor = 13750495
|
||||||
|
|
||||||
type AdminHandler struct {
|
type AdminHandler struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,14 +37,6 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
||||||
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
head, r.URL.Path = httplib.ShiftPath(r.URL.Path)
|
||||||
|
|
||||||
switch head {
|
switch head {
|
||||||
case "new":
|
|
||||||
switch r.Method {
|
|
||||||
case http.MethodGet:
|
|
||||||
f := newSeasonForm()
|
|
||||||
f.MustRender(w, r, user, company)
|
|
||||||
default:
|
|
||||||
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
|
||||||
}
|
|
||||||
case "":
|
case "":
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
@ -49,6 +46,21 @@ func (h *AdminHandler) Handler(user *auth.User, company *auth.Company, conn *dat
|
||||||
default:
|
default:
|
||||||
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
httplib.MethodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
||||||
}
|
}
|
||||||
|
case "new":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
f := newSeasonForm()
|
||||||
|
f.MustRender(w, r, user, company)
|
||||||
|
default:
|
||||||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||||
|
}
|
||||||
|
case "range":
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodPut:
|
||||||
|
updateSeasonCalendar(w, r, user, company, conn)
|
||||||
|
default:
|
||||||
|
httplib.MethodNotAllowed(w, r, http.MethodGet)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if !uuid.Valid(head) {
|
if !uuid.Valid(head) {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
@ -83,6 +95,7 @@ func serveSeasonIndex(w http.ResponseWriter, r *http.Request, user *auth.User, c
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
calendar.Form = newCalendarForm(r.Context(), company, conn)
|
||||||
page := &seasonIndex{
|
page := &seasonIndex{
|
||||||
Seasons: seasons,
|
Seasons: seasons,
|
||||||
Calendar: calendar,
|
Calendar: calendar,
|
||||||
|
@ -138,23 +151,23 @@ var longMonthNames = []string{
|
||||||
locale.PgettextNoop("December", "month"),
|
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) (*seasonCalendar, error) {
|
||||||
firstDay := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
|
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)
|
lastDay := time.Date(year, time.December, 31, 23, 59, 59, 0, time.UTC)
|
||||||
rows, err := conn.Query(ctx, `
|
rows, err := conn.Query(ctx, `
|
||||||
select t.day::date
|
select t.day::date
|
||||||
, to_color(coalesce(color, 13750495)) as color
|
, to_color(coalesce(color, $1)) as color
|
||||||
from generate_series($1, $2, interval '1 day') as t(day)
|
from generate_series($2, $3, interval '1 day') as t(day)
|
||||||
left join season_calendar on season_range @> t.day::date
|
left join season_calendar on season_range @> t.day::date
|
||||||
left join season on season.season_id = season_calendar.season_id and company_id = $3
|
left join season on season.season_id = season_calendar.season_id and company_id = $4
|
||||||
`, firstDay, lastDay, company.ID)
|
`, unsetColor, firstDay, lastDay, company.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var month *seasonMonth
|
var month *seasonMonth
|
||||||
var week seasonWeek
|
var week seasonWeek
|
||||||
var calendar seasonCalendar
|
calendar := &seasonCalendar{}
|
||||||
weekday := int(time.Monday)
|
weekday := int(time.Monday)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
day := &seasonDay{}
|
day := &seasonDay{}
|
||||||
|
@ -168,7 +181,7 @@ func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat
|
||||||
week = append(week, &seasonDay{})
|
week = append(week, &seasonDay{})
|
||||||
}
|
}
|
||||||
month.Weeks = append(month.Weeks, week)
|
month.Weeks = append(month.Weeks, week)
|
||||||
calendar = append(calendar, month)
|
calendar.Months = append(calendar.Months, month)
|
||||||
}
|
}
|
||||||
month = &seasonMonth{
|
month = &seasonMonth{
|
||||||
Month: dayMonth,
|
Month: dayMonth,
|
||||||
|
@ -193,13 +206,16 @@ func collectSeasonCalendar(ctx context.Context, company *auth.Company, conn *dat
|
||||||
week = append(week, &seasonDay{})
|
week = append(week, &seasonDay{})
|
||||||
}
|
}
|
||||||
month.Weeks = append(month.Weeks, week)
|
month.Weeks = append(month.Weeks, week)
|
||||||
calendar = append(calendar, month)
|
calendar.Months = append(calendar.Months, month)
|
||||||
}
|
}
|
||||||
|
|
||||||
return calendar, nil
|
return calendar, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type seasonCalendar []*seasonMonth
|
type seasonCalendar struct {
|
||||||
|
Months []*seasonMonth
|
||||||
|
Form *calendarForm
|
||||||
|
}
|
||||||
|
|
||||||
type seasonMonth struct {
|
type seasonMonth struct {
|
||||||
Month time.Month
|
Month time.Month
|
||||||
|
@ -216,11 +232,11 @@ type seasonDay struct {
|
||||||
|
|
||||||
type seasonIndex struct {
|
type seasonIndex struct {
|
||||||
Seasons []*seasonEntry
|
Seasons []*seasonEntry
|
||||||
Calendar seasonCalendar
|
Calendar *seasonCalendar
|
||||||
}
|
}
|
||||||
|
|
||||||
func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
func (page *seasonIndex) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
template.MustRenderAdmin(w, r, user, company, "season/index.gohtml", page)
|
template.MustRenderAdminFiles(w, r, user, company, page, "season/index.gohtml", "season/calendar.gohtml")
|
||||||
}
|
}
|
||||||
|
|
||||||
func processSeasonForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm, act func()) {
|
func processSeasonForm(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn, f *seasonForm, act func()) {
|
||||||
|
@ -310,3 +326,110 @@ func (f *seasonForm) Valid(ctx context.Context, conn *database.Conn, l *locale.L
|
||||||
func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
func (f *seasonForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company) {
|
||||||
template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f)
|
template.MustRenderAdmin(w, r, user, company, "season/form.gohtml", f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateSeasonCalendar(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||||
|
f := newCalendarForm(r.Context(), company, conn)
|
||||||
|
if ok, err := form.Handle(f, w, r, user); err != nil {
|
||||||
|
return
|
||||||
|
} else if ok {
|
||||||
|
var seasonRange pgtype.Daterange
|
||||||
|
if err = seasonRange.Set(fmt.Sprintf("[%s,%s]", f.StartDate.Val, f.EndDate.Val)); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if f.SeasonID.Val == "" {
|
||||||
|
conn.MustExec(r.Context(), "select unset_season_range($1)", seasonRange)
|
||||||
|
} else {
|
||||||
|
conn.MustExec(r.Context(), "select set_season_range($1, $2)", f.SeasonID, seasonRange)
|
||||||
|
}
|
||||||
|
f.StartDate.Val = ""
|
||||||
|
f.EndDate.Val = ""
|
||||||
|
}
|
||||||
|
f.MustRender(w, r, user, company, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
type calendarForm struct {
|
||||||
|
Seasons []*seasonEntry
|
||||||
|
StartDate *form.Input
|
||||||
|
EndDate *form.Input
|
||||||
|
SeasonID *form.Input
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCalendarForm(ctx context.Context, company *auth.Company, conn *database.Conn) *calendarForm {
|
||||||
|
return &calendarForm{
|
||||||
|
Seasons: mustCollectCalendarSeasons(ctx, company, conn),
|
||||||
|
StartDate: &form.Input{
|
||||||
|
Name: "start_date",
|
||||||
|
},
|
||||||
|
EndDate: &form.Input{
|
||||||
|
Name: "end_date",
|
||||||
|
},
|
||||||
|
SeasonID: &form.Input{
|
||||||
|
Name: "season_id",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustCollectCalendarSeasons(ctx context.Context, company *auth.Company, conn *database.Conn) []*seasonEntry {
|
||||||
|
rows, err := conn.Query(ctx, `
|
||||||
|
select '' as slug
|
||||||
|
, $1 as name
|
||||||
|
, to_color($2)::text
|
||||||
|
, true
|
||||||
|
, 0 as sort
|
||||||
|
union all
|
||||||
|
select season_id::text
|
||||||
|
, name
|
||||||
|
, to_color(color)::text
|
||||||
|
, active
|
||||||
|
, 1 as sort
|
||||||
|
from season
|
||||||
|
where company_id = $3
|
||||||
|
and active
|
||||||
|
order by sort, name`, locale.PgettextNoop("Unset", "action"), unsetColor, company.ID)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var seasons []*seasonEntry
|
||||||
|
for rows.Next() {
|
||||||
|
entry := &seasonEntry{}
|
||||||
|
var sort int
|
||||||
|
if err = rows.Scan(&entry.Slug, &entry.Name, &entry.Color, &entry.Active, &sort); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
seasons = append(seasons, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
return seasons
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *calendarForm) Parse(r *http.Request) error {
|
||||||
|
if err := r.ParseForm(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.StartDate.FillValue(r)
|
||||||
|
f.EndDate.FillValue(r)
|
||||||
|
f.SeasonID.FillValue(r)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *calendarForm) Valid(l *locale.Locale) bool {
|
||||||
|
v := form.NewValidator(l)
|
||||||
|
if v.CheckRequired(f.StartDate, l.GettextNoop("Start date can not be empty.")) {
|
||||||
|
v.CheckValidDate(f.StartDate, l.GettextNoop("Start date must be a valid date."))
|
||||||
|
}
|
||||||
|
if v.CheckRequired(f.EndDate, l.GettextNoop("End date can not be empty.")) {
|
||||||
|
v.CheckValidDate(f.EndDate, l.GettextNoop("End date must be a valid date."))
|
||||||
|
}
|
||||||
|
return v.AllOK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *calendarForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User, company *auth.Company, conn *database.Conn) {
|
||||||
|
calendar, err := collectSeasonCalendar(r.Context(), company, conn, 2023)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
calendar.Form = f
|
||||||
|
template.MustRenderNoLayout(w, r, user, company, "season/calendar.gohtml", calendar)
|
||||||
|
}
|
||||||
|
|
|
@ -127,10 +127,61 @@ a.missing-translation {
|
||||||
.season-calendar {
|
.season-calendar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, auto);
|
grid-template-columns: repeat(3, auto);
|
||||||
|
grid-auto-rows: 1fr;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: start;
|
||||||
gap: 2em;
|
gap: 2em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.season-calendar svg {
|
@media (max-width: 48rem) {
|
||||||
max-width: 5rem;
|
.season-calendar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar td {
|
||||||
|
width: calc(100% / 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar time {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 3rem;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
text-indent: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar [aria-checked] {
|
||||||
|
border: 2px solid black;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar [aria-checked]::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: block;
|
||||||
|
background-color: black;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: .8rem;
|
||||||
|
height: .8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar form {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.season-calendar button {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,41 @@ export function setupIconInput(icon) {
|
||||||
updateValue(input.value);
|
updateValue(input.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setupCalendar(calendar) {
|
||||||
|
const startDate = calendar.querySelector('input[name="start_date"]');
|
||||||
|
const endDate = calendar.querySelector('input[name="end_date"]');
|
||||||
|
const days = Array.from(calendar.querySelectorAll('time'));
|
||||||
|
|
||||||
|
const selectDate = function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const date = e.currentTarget.dateTime;
|
||||||
|
if (!date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!startDate.value) {
|
||||||
|
startDate.value = date;
|
||||||
|
e.currentTarget.setAttribute('aria-checked', true);
|
||||||
|
return;
|
||||||
|
} else if (startDate.value > date) {
|
||||||
|
endDate.value = startDate.value;
|
||||||
|
startDate.value = date;
|
||||||
|
} else {
|
||||||
|
endDate.value = date;
|
||||||
|
}
|
||||||
|
for (const day of days) {
|
||||||
|
if (day.dateTime >= startDate.value && day.dateTime <= endDate.value) {
|
||||||
|
day.setAttribute('aria-checked', true);
|
||||||
|
} else {
|
||||||
|
day.removeAttribute('aria-checked');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const day of days) {
|
||||||
|
day.addEventListener('click', selectDate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
htmx.onLoad((target) => {
|
htmx.onLoad((target) => {
|
||||||
if (target.tagName === 'DIALOG') {
|
if (target.tagName === 'DIALOG') {
|
||||||
target.showModal();
|
target.showModal();
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
<div class="season-calendar" data-hx-target="this" data-hx-swap="outerHTML">
|
||||||
|
{{- /*gotype: dev.tandem.ws/tandem/camper/pkg/season.seasonCalendar*/ -}}
|
||||||
|
{{ range .Months -}}
|
||||||
|
<table>
|
||||||
|
<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="background-color: {{ .Color }}"
|
||||||
|
datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2" }}</time>
|
||||||
|
{{- end -}}
|
||||||
|
</td>
|
||||||
|
{{- end }}
|
||||||
|
</tr>
|
||||||
|
{{- end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{- end }}
|
||||||
|
{{ with .Form }}
|
||||||
|
<form data-hx-put="/admin/seasons/range">
|
||||||
|
{{ CSRFInput }}
|
||||||
|
{{ with .StartDate }}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{ end }}
|
||||||
|
{{ with .EndDate }}<input type="hidden" name="{{ .Name }}" value="{{ .Val }}">{{ end }}
|
||||||
|
<footer>
|
||||||
|
{{ 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>
|
||||||
|
{{ end }}
|
||||||
|
<script type="module">
|
||||||
|
import {setupCalendar} from "/static/camper.js";
|
||||||
|
|
||||||
|
setupCalendar(document.querySelector('.season-calendar'))
|
||||||
|
</script>
|
||||||
|
</div>
|
|
@ -38,37 +38,5 @@
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
<h2>{{( pgettext "Calendar" "title" )}}</h2>
|
<h2>{{( pgettext "Calendar" "title" )}}</h2>
|
||||||
<div class="season-calendar">
|
{{ template "calendar.gohtml" .Calendar }}
|
||||||
{{ range .Calendar -}}
|
|
||||||
<table>
|
|
||||||
<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 -}}
|
|
||||||
<svg viewBox="0 0 100 100">
|
|
||||||
<rect x="0" y="0" width="100" height="100" fill="{{ .Color }}"/>
|
|
||||||
</svg>
|
|
||||||
{{- end -}}
|
|
||||||
</td>
|
|
||||||
{{- end }}
|
|
||||||
</tr>
|
|
||||||
{{- end }}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{{- end }}
|
|
||||||
</div>
|
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
Loading…
Reference in New Issue