aboutsummaryrefslogtreecommitdiff
path: root/internal/web
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:56:07 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 16:56:07 +0200
commit378926240f34759116b19e078d2b2504965f5329 (patch)
treec8e8f2288869882ccc01845eaead8f3c11f067dd /internal/web
parent29e244c48222278c2f255ff2adf45e3af7c9f895 (diff)
downloadlectio-378926240f34759116b19e078d2b2504965f5329.tar.gz
lectio-378926240f34759116b19e078d2b2504965f5329.zip
export: month calendar PDF (A4 landscape, Monday-first grid, feast name + gospel ref + liturgical colour per day); cli --calendar YYYY-MM; web /calendar + link; readings.GospelCitation; v0.23.0
Diffstat (limited to 'internal/web')
-rw-r--r--internal/web/server.go36
-rw-r--r--internal/web/server_test.go18
-rw-r--r--internal/web/templates/index.html1
3 files changed, 55 insertions, 0 deletions
diff --git a/internal/web/server.go b/internal/web/server.go
index 45ab06f..5ebed38 100644
--- a/internal/web/server.go
+++ b/internal/web/server.go
@@ -69,6 +69,7 @@ func NewServer(cfg config.Config) http.Handler {
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { indexHandler(s.get())(w, r) })
mux.HandleFunc("GET /readings", func(w http.ResponseWriter, r *http.Request) { readingsHandler(s.get())(w, r) })
mux.HandleFunc("GET /export", func(w http.ResponseWriter, r *http.Request) { exportHandler(s.get())(w, r) })
+ mux.HandleFunc("GET /calendar", func(w http.ResponseWriter, r *http.Request) { calendarHandler(s.get())(w, r) })
mux.HandleFunc("GET /reader", func(w http.ResponseWriter, r *http.Request) { readerHandler(s.get(), s.table())(w, r) })
mux.HandleFunc("GET /theme.css", func(w http.ResponseWriter, r *http.Request) { themeCSSHandler(s.get())(w, r) })
mux.HandleFunc("GET /settings", settingsGet(s))
@@ -381,6 +382,41 @@ func exportHandler(cfg config.Config) http.HandlerFunc {
}
}
+// calendarHandler serves GET /calendar?month=YYYY-MM&lectionary=: a printable
+// A4 PDF month calendar of the celebration names + gospel references. Missing/
+// bad month defaults to the current month.
+func calendarHandler(cfg config.Config) http.HandlerFunc {
+ return func(w http.ResponseWriter, r *http.Request) {
+ t, err := time.Parse("2006-01", r.URL.Query().Get("month"))
+ if err != nil {
+ t = time.Now()
+ }
+ year, month := t.Year(), int(t.Month())
+ cfg.Lectionary = requestLectionary(cfg, r)
+ first := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC)
+
+ var days []export.CalendarDay
+ for d := first; int(d.Month()) == month; d = d.AddDate(0, 0, 1) {
+ cd := export.CalendarDay{Day: d.Day()}
+ if secs, info, lerr := readings.Load(cfg, readings.Options{Date: d.Format("2006-01-02"), Offline: cfg.Offline, All: false}); lerr == nil {
+ cd.Name = info.Name
+ cd.Colour = info.Colour
+ cd.Citation = readings.GospelCitation(secs)
+ }
+ days = append(days, cd)
+ }
+
+ data, perr := export.CalendarPDF(year, month, days, cfg.Lectionary, cfg.UILanguage)
+ if perr != nil {
+ http.Error(w, perr.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.Header().Set("Content-Type", "application/pdf")
+ w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="lectio-%04d-%02d-calendar.pdf"`, year, month))
+ w.Write(data)
+ }
+}
+
// renderOrError returns RenderReadings' fragment, or (on a readings.Load
// error, or no sections at all for the date) a small escaped error
// paragraph, localised via lang -- readings.Load errors are expected in
diff --git a/internal/web/server_test.go b/internal/web/server_test.go
index 43508ba..284a454 100644
--- a/internal/web/server_test.go
+++ b/internal/web/server_test.go
@@ -645,3 +645,21 @@ func TestExportNoReadings(t *testing.T) {
t.Errorf("export with no readings status=%d (want 404/500)", rec.Code)
}
}
+
+func TestCalendarPDF(t *testing.T) {
+ t.Setenv("XDG_CACHE_HOME", t.TempDir())
+ cfg := config.Default()
+ cfg.Offline = true // no network; empty cells are fine, PDF still renders
+ srv := NewServer(cfg)
+ rec := httptest.NewRecorder()
+ srv.ServeHTTP(rec, httptest.NewRequest("GET", "/calendar?month=2026-07", nil))
+ if rec.Code != 200 {
+ t.Fatalf("calendar status %d", rec.Code)
+ }
+ if ct := rec.Header().Get("Content-Type"); ct != "application/pdf" {
+ t.Errorf("content-type %q", ct)
+ }
+ if b := rec.Body.Bytes(); len(b) < 1000 || string(b[:4]) != "%PDF" {
+ t.Errorf("not a pdf (len=%d)", len(b))
+ }
+}
diff --git a/internal/web/templates/index.html b/internal/web/templates/index.html
index cd30208..1e43df4 100644
--- a/internal/web/templates/index.html
+++ b/internal/web/templates/index.html
@@ -84,6 +84,7 @@
<a href="#" onclick="location='/export?fmt=txt&'+new URLSearchParams(new FormData(document.getElementById('controls'))).toString();return false;">txt</a>
<a href="#" onclick="location='/export?fmt=md&'+new URLSearchParams(new FormData(document.getElementById('controls'))).toString();return false;">md</a>
<a href="#" onclick="location='/export?fmt=pdf&'+new URLSearchParams(new FormData(document.getElementById('controls'))).toString();return false;">pdf</a>
+ ยท <a href="#" onclick="var d=(document.querySelector('#controls [name=date]').value||'').slice(0,7);location='/calendar?month='+d+'&lectionary='+document.querySelector('#controls [name=lectionary]').value;return false;">{{.L.Calendar}}</a>
</span>
<div id="pane">{{.Reading}}</div>