Release: one log page, grouped dishes, live search #1
@@ -138,7 +138,7 @@ func TestSoftDeleteSideHidesItFromPickers(t *testing.T) {
|
|||||||
if err := softDeleteSide(h.db, id); err != nil {
|
if err := softDeleteSide(h.db, id); err != nil {
|
||||||
t.Fatalf("softDeleteSide: %v", err)
|
t.Fatalf("softDeleteSide: %v", err)
|
||||||
}
|
}
|
||||||
sides, err := listSides(h.db)
|
sides, err := listSides(h.db, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("listSides: %v", err)
|
t.Fatalf("listSides: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+99
-11
@@ -11,17 +11,34 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/a-h/templ"
|
"github.com/a-h/templ"
|
||||||
|
"github.com/starfederation/datastar-go/datastar"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxUpload caps a pasted or uploaded bundle. A household catalog is a few
|
// maxUpload caps a pasted or uploaded bundle. A household catalog is a few
|
||||||
// kilobytes; a megabyte is already absurd generosity.
|
// kilobytes; a megabyte is already absurd generosity.
|
||||||
const maxUpload = 1 << 20
|
const maxUpload = 1 << 20
|
||||||
|
|
||||||
// historyDays is how far back the history under the logger walks.
|
const (
|
||||||
//
|
// historyDays is one window of the history under the logger, and the step
|
||||||
// ponytail: a fixed window. After a year of daily entries this list is the
|
// that "show more" grows it by. Older days arrive a window at a time
|
||||||
// thing that needs paging; load more on scroll when it actually hurts.
|
// rather than all at once.
|
||||||
const historyDays = 60
|
historyDays = 30
|
||||||
|
|
||||||
|
// maxHistoryDays caps what a hand-edited URL can ask for, so ?paivat=
|
||||||
|
// cannot be turned into a request to render a decade of rows.
|
||||||
|
maxHistoryDays = 366 * 5
|
||||||
|
)
|
||||||
|
|
||||||
|
// historyWindow reads ?paivat=, the number of days of history to show.
|
||||||
|
func historyWindow(r *http.Request) int {
|
||||||
|
days := historyDays
|
||||||
|
if raw := r.URL.Query().Get("paivat"); raw != "" {
|
||||||
|
if n, err := strconv.Atoi(raw); err == nil && n > days {
|
||||||
|
days = min(n, maxHistoryDays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
}
|
||||||
|
|
||||||
type app struct {
|
type app struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
@@ -71,7 +88,9 @@ type logView struct {
|
|||||||
Groups []DishGroup // what the board actually renders
|
Groups []DishGroup // what the board actually renders
|
||||||
Sides []Side
|
Sides []Side
|
||||||
New mainForm // inline "add the dish you were looking for"
|
New mainForm // inline "add the dish you were looking for"
|
||||||
History []HistoryRow
|
History HistoryPage
|
||||||
|
HistoryDays int // size of the window currently shown
|
||||||
|
HistoryMore int // the window size the "show more" link asks for
|
||||||
|
|
||||||
// Deleting a logged meal drops the row outright, unlike a dish which is
|
// Deleting a logged meal drops the row outright, unlike a dish which is
|
||||||
// only soft-deleted, so it asks first.
|
// only soft-deleted, so it asks first.
|
||||||
@@ -128,18 +147,83 @@ func (a *app) index(w http.ResponseWriter, r *http.Request) {
|
|||||||
v.New = mainForm{Name: v.Search, Categories: map[string]bool{}, HasSides: true}
|
v.New = mainForm{Name: v.Search, Categories: map[string]bool{}, HasSides: true}
|
||||||
}
|
}
|
||||||
if v.Chosen != nil && v.Chosen.HasSides {
|
if v.Chosen != nil && v.Chosen.HasSides {
|
||||||
if v.Sides, err = listSides(a.db); err != nil {
|
if v.Sides, err = listSides(a.db, ""); err != nil {
|
||||||
log.Printf("list sides: %v", err)
|
log.Printf("list sides: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.History, err = history(a.db, a.loc, historyDays); err != nil {
|
v.HistoryDays = historyWindow(r)
|
||||||
|
v.HistoryMore = v.HistoryDays + historyDays
|
||||||
|
if v.History, err = history(a.db, a.loc, today(a.loc), v.HistoryDays); err != nil {
|
||||||
log.Printf("history: %v", err)
|
log.Printf("history: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
render(w, r, logPage(v))
|
render(w, r, logPage(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// searchSignals is what Datastar sends back: for a GET it JSON-encodes the
|
||||||
|
// signals into the `datastar` query parameter.
|
||||||
|
type searchSignals struct {
|
||||||
|
Haku string `json:"haku"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// fragment renders a piece of a page for Datastar to patch in. A plain
|
||||||
|
// text/html response is enough — Datastar matches the returned element by its
|
||||||
|
// id and replaces it, so there is no SSE stream to manage.
|
||||||
|
func fragment(w http.ResponseWriter, r *http.Request, c templ.Component) {
|
||||||
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
if err := c.Render(r.Context(), w); err != nil {
|
||||||
|
log.Printf("fragment %s: %v", r.URL.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchBoard re-renders the dish board as the search box is typed into.
|
||||||
|
func (a *app) searchBoard(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var signals searchSignals
|
||||||
|
if err := datastar.ReadSignals(r, &signals); err != nil {
|
||||||
|
http.Error(w, "bad signals", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v := logView{
|
||||||
|
Date: a.date(r),
|
||||||
|
Today: today(a.loc),
|
||||||
|
Search: strings.TrimSpace(signals.Haku),
|
||||||
|
}
|
||||||
|
dishes, err := listDishes(a.db, v.Search)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("search dishes: %v", err)
|
||||||
|
}
|
||||||
|
v.Dishes = dishes
|
||||||
|
v.Groups = groupDishes(dishes)
|
||||||
|
v.New = mainForm{Name: v.Search, Categories: map[string]bool{}, HasSides: true}
|
||||||
|
|
||||||
|
fragment(w, r, boardList(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchCatalog re-renders the catalog lists as the search box is typed into.
|
||||||
|
func (a *app) searchCatalog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var signals searchSignals
|
||||||
|
if err := datastar.ReadSignals(r, &signals); err != nil {
|
||||||
|
http.Error(w, "bad signals", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
v := catalogView{Search: strings.TrimSpace(signals.Haku)}
|
||||||
|
mains, err := listDishes(a.db, v.Search)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("search catalog: %v", err)
|
||||||
|
}
|
||||||
|
v.Mains = len(mains)
|
||||||
|
sortByName(mains)
|
||||||
|
v.Groups = groupDishes(mains)
|
||||||
|
if v.Sides, err = listSides(a.db, v.Search); err != nil {
|
||||||
|
log.Printf("search sides: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment(w, r, catalogList(v))
|
||||||
|
}
|
||||||
|
|
||||||
// quickAdd creates a dish from the Kirjaa screen and goes straight on to
|
// quickAdd creates a dish from the Kirjaa screen and goes straight on to
|
||||||
// logging it. Hunting for something that is not in the catalog yet should not
|
// logging it. Hunting for something that is not in the catalog yet should not
|
||||||
// mean a detour through Ruoat and a lost train of thought.
|
// mean a detour through Ruoat and a lost train of thought.
|
||||||
@@ -195,7 +279,9 @@ func (a *app) quickAdd(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("list dishes: %v", err)
|
log.Printf("list dishes: %v", err)
|
||||||
}
|
}
|
||||||
v.Groups = groupDishes(v.Dishes)
|
v.Groups = groupDishes(v.Dishes)
|
||||||
if v.History, err = history(a.db, a.loc, historyDays); err != nil {
|
v.HistoryDays = historyWindow(r)
|
||||||
|
v.HistoryMore = v.HistoryDays + historyDays
|
||||||
|
if v.History, err = history(a.db, a.loc, today(a.loc), v.HistoryDays); err != nil {
|
||||||
log.Printf("history: %v", err)
|
log.Printf("history: %v", err)
|
||||||
}
|
}
|
||||||
render(w, r, logPage(v))
|
render(w, r, logPage(v))
|
||||||
@@ -276,6 +362,7 @@ type catalogView struct {
|
|||||||
Side sideForm
|
Side sideForm
|
||||||
Report *ImportReport
|
Report *ImportReport
|
||||||
Mains int // count, for the header
|
Mains int // count, for the header
|
||||||
|
Search string
|
||||||
|
|
||||||
// The row awaiting a delete confirmation, if any. A trash icon is easy to
|
// The row awaiting a delete confirmation, if any. A trash icon is easy to
|
||||||
// hit by accident, so the row asks before anything happens.
|
// hit by accident, so the row asks before anything happens.
|
||||||
@@ -286,6 +373,7 @@ type catalogView struct {
|
|||||||
func (a *app) catalog(w http.ResponseWriter, r *http.Request) {
|
func (a *app) catalog(w http.ResponseWriter, r *http.Request) {
|
||||||
v := catalogView{
|
v := catalogView{
|
||||||
Main: mainForm{Categories: map[string]bool{}, HasSides: true},
|
Main: mainForm{Categories: map[string]bool{}, HasSides: true},
|
||||||
|
Search: strings.TrimSpace(r.URL.Query().Get("haku")),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ?muokkaa= loads a dish into its form; the same form adds and edits.
|
// ?muokkaa= loads a dish into its form; the same form adds and edits.
|
||||||
@@ -326,7 +414,7 @@ func (a *app) renderCatalog(w http.ResponseWriter, r *http.Request, v catalogVie
|
|||||||
v.Main.Categories = map[string]bool{}
|
v.Main.Categories = map[string]bool{}
|
||||||
}
|
}
|
||||||
|
|
||||||
mains, err := listDishes(a.db, "")
|
mains, err := listDishes(a.db, v.Search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("list mains: %v", err)
|
log.Printf("list mains: %v", err)
|
||||||
}
|
}
|
||||||
@@ -334,7 +422,7 @@ func (a *app) renderCatalog(w http.ResponseWriter, r *http.Request, v catalogVie
|
|||||||
sortByName(mains) // the catalog is managed, so position should be predictable
|
sortByName(mains) // the catalog is managed, so position should be predictable
|
||||||
v.Groups = groupDishes(mains)
|
v.Groups = groupDishes(mains)
|
||||||
|
|
||||||
if v.Sides, err = listSides(a.db); err != nil {
|
if v.Sides, err = listSides(a.db, v.Search); err != nil {
|
||||||
log.Printf("list sides: %v", err)
|
log.Printf("list sides: %v", err)
|
||||||
}
|
}
|
||||||
render(w, r, catalogPage(v))
|
render(w, r, catalogPage(v))
|
||||||
|
|||||||
@@ -158,8 +158,10 @@ func routes(db *sql.DB, loc *time.Location, password string) http.Handler {
|
|||||||
mux.HandleFunc("GET /{$}", a.index)
|
mux.HandleFunc("GET /{$}", a.index)
|
||||||
mux.HandleFunc("POST /kirjaa", a.save)
|
mux.HandleFunc("POST /kirjaa", a.save)
|
||||||
mux.HandleFunc("POST /lisaa", a.quickAdd)
|
mux.HandleFunc("POST /lisaa", a.quickAdd)
|
||||||
|
mux.HandleFunc("GET /etsi", a.searchBoard)
|
||||||
mux.HandleFunc("POST /poista", a.delete)
|
mux.HandleFunc("POST /poista", a.delete)
|
||||||
mux.HandleFunc("GET /ruuat", a.catalog)
|
mux.HandleFunc("GET /ruuat", a.catalog)
|
||||||
|
mux.HandleFunc("GET /ruuat/etsi", a.searchCatalog)
|
||||||
mux.HandleFunc("POST /ruuat/paaruoka", a.saveMain)
|
mux.HandleFunc("POST /ruuat/paaruoka", a.saveMain)
|
||||||
mux.HandleFunc("POST /ruuat/lisuke", a.saveSide)
|
mux.HandleFunc("POST /ruuat/lisuke", a.saveSide)
|
||||||
mux.HandleFunc("POST /ruuat/poista", a.deleteDish)
|
mux.HandleFunc("POST /ruuat/poista", a.deleteDish)
|
||||||
|
|||||||
@@ -328,6 +328,19 @@ html[data-theme="light"] .themetoggle .i-moon { display: none; }
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
.more {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: var(--tap);
|
||||||
|
margin-top: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
.gapline { border-bottom-style: dashed; font-size: 13.5px; color: var(--muted); }
|
.gapline { border-bottom-style: dashed; font-size: 13.5px; color: var(--muted); }
|
||||||
.entry time, .gapline time {
|
.entry time, .gapline time {
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|||||||
+43
-26
@@ -128,9 +128,12 @@ func dishByID(db *sql.DB, id int64) (*Dish, error) {
|
|||||||
return &d, nil
|
return &d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listSides(db *sql.DB) ([]Side, error) {
|
func listSides(db *sql.DB, search string) ([]Side, error) {
|
||||||
rows, err := db.Query(
|
rows, err := db.Query(`
|
||||||
`SELECT id, name FROM side_dishes WHERE deleted_at IS NULL ORDER BY name`)
|
SELECT id, name FROM side_dishes
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND (? = '' OR lower(name) LIKE '%' || lower(?) || '%')
|
||||||
|
ORDER BY name`, search, search)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -395,36 +398,50 @@ type HistoryRow struct {
|
|||||||
Entry *Entry
|
Entry *Entry
|
||||||
}
|
}
|
||||||
|
|
||||||
// history walks back day by day from today, so a day nobody wrote down shows
|
// HistoryPage is one window of history plus where to continue from. After a
|
||||||
// up as an explicit gap rather than silently missing. It stops at the first
|
// few years of daily entries the whole log is far too much to render at once.
|
||||||
// entry ever recorded — before that there is no history to be missing.
|
type HistoryPage struct {
|
||||||
func history(db *sql.DB, loc *time.Location, days int) ([]HistoryRow, error) {
|
Rows []HistoryRow
|
||||||
var first string
|
More bool // older entries exist beyond this window
|
||||||
err := db.QueryRow(`SELECT min(date) FROM meal_log`).Scan(&first)
|
Next time.Time // the day the next window starts at
|
||||||
if err == sql.ErrNoRows || first == "" {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
firstDate, err := time.ParseInLocation(dateLayout, first, loc)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
now := today(loc)
|
// history walks back day by day from a given day, so a day nobody wrote down
|
||||||
oldest := now.AddDate(0, 0, -days)
|
// shows up as an explicit gap rather than silently missing. It stops at the
|
||||||
if firstDate.After(oldest) {
|
// first entry ever recorded — before that there is no history to be missing.
|
||||||
|
func history(db *sql.DB, loc *time.Location, from time.Time, days int) (HistoryPage, error) {
|
||||||
|
var first sql.NullString
|
||||||
|
if err := db.QueryRow(`SELECT min(date) FROM meal_log`).Scan(&first); err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return HistoryPage{}, nil
|
||||||
|
}
|
||||||
|
return HistoryPage{}, err
|
||||||
|
}
|
||||||
|
if !first.Valid || first.String == "" {
|
||||||
|
return HistoryPage{}, nil
|
||||||
|
}
|
||||||
|
firstDate, err := time.ParseInLocation(dateLayout, first.String, loc)
|
||||||
|
if err != nil {
|
||||||
|
return HistoryPage{}, err
|
||||||
|
}
|
||||||
|
if from.Before(firstDate) {
|
||||||
|
return HistoryPage{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oldest := from.AddDate(0, 0, -days+1)
|
||||||
|
page := HistoryPage{More: true}
|
||||||
|
if !firstDate.Before(oldest) {
|
||||||
oldest = firstDate
|
oldest = firstDate
|
||||||
|
page.More = false
|
||||||
}
|
}
|
||||||
|
page.Next = oldest.AddDate(0, 0, -1)
|
||||||
|
|
||||||
var rows []HistoryRow
|
for d := from; !d.Before(oldest); d = d.AddDate(0, 0, -1) {
|
||||||
for d := now; !d.Before(oldest); d = d.AddDate(0, 0, -1) {
|
|
||||||
entry, err := entryFor(db, d)
|
entry, err := entryFor(db, d)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return HistoryPage{}, err
|
||||||
}
|
}
|
||||||
rows = append(rows, HistoryRow{Date: d, Entry: entry})
|
page.Rows = append(page.Rows, HistoryRow{Date: d, Entry: entry})
|
||||||
}
|
}
|
||||||
return rows, nil
|
return page, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,10 +235,11 @@ func TestHistoryMarksUnloggedDaysAsGaps(t *testing.T) {
|
|||||||
t.Fatalf("save -3: %v", err)
|
t.Fatalf("save -3: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
rows, err := history(h.db, loc, 60)
|
page, err := history(h.db, loc, now, 60)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("history: %v", err)
|
t.Fatalf("history: %v", err)
|
||||||
}
|
}
|
||||||
|
rows := page.Rows
|
||||||
// Walks back to the oldest entry only: today, -1, -2, -3.
|
// Walks back to the oldest entry only: today, -1, -2, -3.
|
||||||
if len(rows) != 4 {
|
if len(rows) != 4 {
|
||||||
t.Fatalf("%d rows, want 4", len(rows))
|
t.Fatalf("%d rows, want 4", len(rows))
|
||||||
@@ -252,16 +253,66 @@ func TestHistoryMarksUnloggedDaysAsGaps(t *testing.T) {
|
|||||||
if rows[3].Entry == nil || rows[3].Entry.Main.Name != "Lihapullat" {
|
if rows[3].Entry == nil || rows[3].Entry.Main.Name != "Lihapullat" {
|
||||||
t.Errorf("last row should be Lihapullat, got %+v", rows[3].Entry)
|
t.Errorf("last row should be Lihapullat, got %+v", rows[3].Entry)
|
||||||
}
|
}
|
||||||
|
if page.More {
|
||||||
|
t.Error("More is set although the window reached the oldest entry")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHistoryPagesInWindows(t *testing.T) {
|
||||||
|
h := seeded(t)
|
||||||
|
loc := time.UTC
|
||||||
|
now := today(loc)
|
||||||
|
|
||||||
|
// Entries today and 9 days back, with a 5-day window over them.
|
||||||
|
if err := saveEntry(h.db, now, h.mainNamed(t, "Lohikeitto"), nil); err != nil {
|
||||||
|
t.Fatalf("save today: %v", err)
|
||||||
|
}
|
||||||
|
if err := saveEntry(h.db, now.AddDate(0, 0, -9), h.mainNamed(t, "Lihapullat"), nil); err != nil {
|
||||||
|
t.Fatalf("save -9: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
first, err := history(h.db, loc, now, 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first window: %v", err)
|
||||||
|
}
|
||||||
|
if len(first.Rows) != 5 {
|
||||||
|
t.Errorf("%d rows in the first window, want 5", len(first.Rows))
|
||||||
|
}
|
||||||
|
if !first.More {
|
||||||
|
t.Error("More should be set: older entries exist")
|
||||||
|
}
|
||||||
|
if want := now.AddDate(0, 0, -5); !first.Next.Equal(want) {
|
||||||
|
t.Errorf("Next = %s, want %s", first.Next.Format(dateLayout), want.Format(dateLayout))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The windows must meet exactly: no day repeated, none skipped.
|
||||||
|
second, err := history(h.db, loc, first.Next, 5)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("second window: %v", err)
|
||||||
|
}
|
||||||
|
if len(second.Rows) != 5 {
|
||||||
|
t.Errorf("%d rows in the second window, want 5", len(second.Rows))
|
||||||
|
}
|
||||||
|
if second.More {
|
||||||
|
t.Error("the second window reaches the oldest entry, so More should be clear")
|
||||||
|
}
|
||||||
|
last := second.Rows[len(second.Rows)-1]
|
||||||
|
if last.Entry == nil || last.Entry.Main.Name != "Lihapullat" {
|
||||||
|
t.Errorf("last row should be the oldest entry, got %+v", last.Entry)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHistoryEmptyWithoutEntries(t *testing.T) {
|
func TestHistoryEmptyWithoutEntries(t *testing.T) {
|
||||||
h := seeded(t)
|
h := seeded(t)
|
||||||
|
|
||||||
rows, err := history(h.db, time.UTC, 60)
|
page, err := history(h.db, time.UTC, today(time.UTC), 60)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("history: %v", err)
|
t.Fatalf("history: %v", err)
|
||||||
}
|
}
|
||||||
if len(rows) != 0 {
|
if len(page.Rows) != 0 {
|
||||||
t.Errorf("%d rows for an empty log, want 0", len(rows))
|
t.Errorf("%d rows for an empty log, want 0", len(page.Rows))
|
||||||
|
}
|
||||||
|
if page.More {
|
||||||
|
t.Error("More is set although there is no history at all")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-18
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -51,6 +52,26 @@ func pickSeparator(v logView) string {
|
|||||||
return "&"
|
return "&"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsString renders a Go string as a JavaScript literal, for the data-signals
|
||||||
|
// attribute that seeds the search box.
|
||||||
|
func jsString(s string) string {
|
||||||
|
b, err := json.Marshal(s)
|
||||||
|
if err != nil {
|
||||||
|
return `""`
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// searchURL is where the live search posts back to. The day travels in the
|
||||||
|
// path so the board keeps rendering links for the right date; the search text
|
||||||
|
// travels as a Datastar signal.
|
||||||
|
func searchURL(v logView) string {
|
||||||
|
if v.Date.Equal(v.Today) {
|
||||||
|
return "/etsi"
|
||||||
|
}
|
||||||
|
return "/etsi?pvm=" + isoDate(v.Date)
|
||||||
|
}
|
||||||
|
|
||||||
// categoryLabels lists a dish's categories in Finnish, for the catalog rows.
|
// categoryLabels lists a dish's categories in Finnish, for the catalog rows.
|
||||||
func categoryLabels(d Dish) string {
|
func categoryLabels(d Dish) string {
|
||||||
names := make([]string, 0, len(d.Categories))
|
names := make([]string, 0, len(d.Categories))
|
||||||
@@ -265,12 +286,12 @@ templ logPage(v logView) {
|
|||||||
templ historyList(v logView) {
|
templ historyList(v logView) {
|
||||||
<section class="history">
|
<section class="history">
|
||||||
<h3 class="sechead">Aiemmin</h3>
|
<h3 class="sechead">Aiemmin</h3>
|
||||||
if len(v.History) == 0 {
|
if len(v.History.Rows) == 0 {
|
||||||
<p class="muted small">Ei vielä merkintöjä.</p>
|
<p class="muted small">Ei vielä merkintöjä.</p>
|
||||||
}
|
}
|
||||||
for i, row := range v.History {
|
for i, row := range v.History.Rows {
|
||||||
if !row.Date.Equal(v.Date) {
|
if !row.Date.Equal(v.Date) {
|
||||||
if i == 0 || v.History[i-1].Date.Month() != row.Date.Month() {
|
if i == 0 || v.History.Rows[i-1].Date.Month() != row.Date.Month() {
|
||||||
<p class="monthrule">{ monthFI(row.Date) }</p>
|
<p class="monthrule">{ monthFI(row.Date) }</p>
|
||||||
}
|
}
|
||||||
if row.Entry != nil {
|
if row.Entry != nil {
|
||||||
@@ -294,6 +315,12 @@ templ historyList(v logView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if v.History.More {
|
||||||
|
<a
|
||||||
|
class="more"
|
||||||
|
href={ templ.SafeURL(dayURL("/", v.Date, v.Today) + pickSeparator(v) + "paivat=" + strconv.Itoa(v.HistoryMore)) }
|
||||||
|
>Näytä lisää</a>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,16 +345,38 @@ templ dayButton(label string, target, selected, now time.Time) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The form still works on its own: submitting reloads the page with ?haku=.
|
||||||
|
// Datastar binds the same box to a signal and re-renders just the list as it
|
||||||
|
// is typed into, so the live version is an enhancement rather than a
|
||||||
|
// requirement.
|
||||||
templ board(v logView) {
|
templ board(v logView) {
|
||||||
|
<div data-signals:haku={ jsString(v.Search) }>
|
||||||
<form method="get" action="/" class="searchrow">
|
<form method="get" action="/" class="searchrow">
|
||||||
if !v.Date.Equal(v.Today) {
|
if !v.Date.Equal(v.Today) {
|
||||||
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
<input type="hidden" name="pvm" value={ isoDate(v.Date) }/>
|
||||||
}
|
}
|
||||||
<input class="filter" type="search" name="haku" value={ v.Search } placeholder="Etsi tai lisää uusi" aria-label="Etsi"/>
|
<input
|
||||||
|
class="filter"
|
||||||
|
type="search"
|
||||||
|
name="haku"
|
||||||
|
value={ v.Search }
|
||||||
|
placeholder="Etsi tai lisää uusi"
|
||||||
|
aria-label="Etsi"
|
||||||
|
data-bind:haku
|
||||||
|
data-on:input__debounce.250ms={ "@get('" + searchURL(v) + "')" }
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
// Grouped by category, and inside each group the most-eaten first — so a
|
@boardList(v)
|
||||||
// dish keeps a predictable neighbourhood while favourites still surface
|
</div>
|
||||||
// at the top of it.
|
}
|
||||||
|
|
||||||
|
// boardList is what Datastar patches: it carries the id, so a plain text/html
|
||||||
|
// response is matched to it and swapped in place.
|
||||||
|
templ boardList(v logView) {
|
||||||
|
<div id="lauta">
|
||||||
|
// Grouped by category, and inside each group the most-eaten first — so
|
||||||
|
// a dish keeps a predictable neighbourhood while favourites still
|
||||||
|
// surface at the top of it.
|
||||||
for _, g := range v.Groups {
|
for _, g := range v.Groups {
|
||||||
<h3 class="sechead">{ g.Label }</h3>
|
<h3 class="sechead">{ g.Label }</h3>
|
||||||
<div class="board">
|
<div class="board">
|
||||||
@@ -339,6 +388,7 @@ templ board(v logView) {
|
|||||||
if len(v.Dishes) == 0 {
|
if len(v.Dishes) == 0 {
|
||||||
@quickAddCard(v)
|
@quickAddCard(v)
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
// quickAddCard turns a search that found nothing into the thing to do next.
|
// quickAddCard turns a search that found nothing into the thing to do next.
|
||||||
@@ -482,12 +532,46 @@ templ catalogPage(v catalogView) {
|
|||||||
@importReport(v.Report)
|
@importReport(v.Report)
|
||||||
}
|
}
|
||||||
@mainForm_(v.Main)
|
@mainForm_(v.Main)
|
||||||
|
@sideForm_(v.Side)
|
||||||
|
<div data-signals:haku={ jsString(v.Search) }>
|
||||||
|
<form method="get" action="/ruuat" class="searchrow">
|
||||||
|
<input
|
||||||
|
class="filter"
|
||||||
|
type="search"
|
||||||
|
name="haku"
|
||||||
|
value={ v.Search }
|
||||||
|
placeholder="Etsi ruokaa"
|
||||||
|
aria-label="Etsi"
|
||||||
|
data-bind:haku
|
||||||
|
data-on:input__debounce.250ms="@get('/ruuat/etsi')"
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
@catalogList(v)
|
||||||
|
</div>
|
||||||
|
<details class="card">
|
||||||
|
<summary>Tuo ruokia tiedostosta</summary>
|
||||||
|
@importForm()
|
||||||
|
</details>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// catalogList carries the id Datastar patches, so typing in the search box
|
||||||
|
// swaps the lists without reloading the forms above them.
|
||||||
|
templ catalogList(v catalogView) {
|
||||||
|
<div id="ruokalista">
|
||||||
if v.Mains == 0 {
|
if v.Mains == 0 {
|
||||||
<h3 class="sechead">Pääruuat</h3>
|
<h3 class="sechead">Pääruuat</h3>
|
||||||
<p class="muted small">Ei vielä pääruokia.</p>
|
<p class="muted small">
|
||||||
|
if v.Search == "" {
|
||||||
|
Ei vielä pääruokia.
|
||||||
|
} else {
|
||||||
|
Ei osumia.
|
||||||
}
|
}
|
||||||
// Grouped by category, alphabetical inside. The catalog is a list
|
</p>
|
||||||
// you manage, so a predictable position beats a useful one.
|
}
|
||||||
|
// Grouped by category, alphabetical inside. The catalog is a list you
|
||||||
|
// manage, so a predictable position beats a useful one.
|
||||||
for _, g := range v.Groups {
|
for _, g := range v.Groups {
|
||||||
<h3 class="sechead">{ g.Label }</h3>
|
<h3 class="sechead">{ g.Label }</h3>
|
||||||
for _, d := range g.Dishes {
|
for _, d := range g.Dishes {
|
||||||
@@ -503,10 +587,15 @@ templ catalogPage(v catalogView) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@sideForm_(v.Side)
|
|
||||||
<h3 class="sechead">Lisukkeet</h3>
|
<h3 class="sechead">Lisukkeet</h3>
|
||||||
if len(v.Sides) == 0 {
|
if len(v.Sides) == 0 {
|
||||||
<p class="muted small">Ei vielä lisukkeita.</p>
|
<p class="muted small">
|
||||||
|
if v.Search == "" {
|
||||||
|
Ei vielä lisukkeita.
|
||||||
|
} else {
|
||||||
|
Ei osumia.
|
||||||
|
}
|
||||||
|
</p>
|
||||||
}
|
}
|
||||||
for _, s := range v.Sides {
|
for _, s := range v.Sides {
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -514,12 +603,7 @@ templ catalogPage(v catalogView) {
|
|||||||
@rowActions(v, "/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke")
|
@rowActions(v, "/ruuat?muokkaa-lisuke="+strconv.FormatInt(s.ID, 10), s.ID, "lisuke")
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<details class="card">
|
</div>
|
||||||
<summary>Tuo ruokia tiedostosta</summary>
|
|
||||||
@importForm()
|
|
||||||
</details>
|
|
||||||
</main>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// rowActions is a pencil and a bin, until the bin is tapped: then the row
|
// rowActions is a pencil and a bin, until the bin is tapped: then the row
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ tool github.com/a-h/templ/cmd/templ
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.3.1020
|
github.com/a-h/templ v0.3.1020
|
||||||
|
github.com/starfederation/datastar-go v1.2.2
|
||||||
golang.org/x/time v0.15.0
|
golang.org/x/time v0.15.0
|
||||||
modernc.org/sqlite v1.58.0
|
modernc.org/sqlite v1.58.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/CAFxX/httpcompression v0.0.9 // indirect
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
@@ -19,11 +21,13 @@ require (
|
|||||||
github.com/fatih/color v1.16.0 // indirect
|
github.com/fatih/color v1.16.0 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||||
github.com/natefinch/atomic v1.0.1 // indirect
|
github.com/natefinch/atomic v1.0.1 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
golang.org/x/mod v0.38.0 // indirect
|
golang.org/x/mod v0.38.0 // indirect
|
||||||
golang.org/x/net v0.57.0 // indirect
|
golang.org/x/net v0.57.0 // indirect
|
||||||
golang.org/x/sync v0.22.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
|
||||||
|
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -16,6 +20,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
|||||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
|
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
|
||||||
|
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||||
@@ -24,6 +30,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
@@ -33,12 +43,27 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0
|
|||||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
|
github.com/starfederation/datastar-go v1.2.2 h1:f+U2y5FY5tXgXaTVXkuKu+Tz9uh7BU1j8jxthYmcC9Q=
|
||||||
|
github.com/starfederation/datastar-go v1.2.2/go.mod h1:stm83LQkhZkwa5GzzdPEN6dLuu8FVwxIv0w1DYkbD3w=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
|
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
|
||||||
|
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
|
||||||
@@ -54,6 +79,8 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
|||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
|
||||||
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||||
|
|||||||
@@ -150,6 +150,28 @@ check "the day is empty again" \
|
|||||||
check "search filters the board" \
|
check "search filters the board" \
|
||||||
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
|
"$(curl -s -u ":$pass" "http://$addr/?haku=keitto")" "keitto"
|
||||||
|
|
||||||
|
# ---- live search: Datastar sends signals as JSON in ?datastar= -----------
|
||||||
|
|
||||||
|
live=$(curl -s -u ":$pass" --get --data-urlencode 'datastar={"haku":"keitto"}' "http://$addr/etsi")
|
||||||
|
check "live search returns the board fragment" "$live" 'id="lauta"'
|
||||||
|
check "live search applies the term" "$live" "keitto"
|
||||||
|
refute "live search excludes non-matches" "$live" "Lihapullat"
|
||||||
|
refute "the fragment is not a whole page" "$live" "<html"
|
||||||
|
|
||||||
|
check "live search is served as html for Datastar to patch" \
|
||||||
|
"$(curl -s -o /dev/null -w '%{content_type}' -u ":$pass" \
|
||||||
|
--get --data-urlencode 'datastar={"haku":"keitto"}' "http://$addr/etsi")" \
|
||||||
|
"text/html"
|
||||||
|
|
||||||
|
cat_live=$(curl -s -u ":$pass" --get --data-urlencode 'datastar={"haku":"riisi"}' "http://$addr/ruuat/etsi")
|
||||||
|
check "catalog live search returns its fragment" "$cat_live" 'id="ruokalista"'
|
||||||
|
check "catalog live search matches sides too" "$cat_live" "Riisi"
|
||||||
|
refute "catalog live search excludes non-matches" "$cat_live" "Lihapullat"
|
||||||
|
|
||||||
|
# The plain form still works without JavaScript.
|
||||||
|
check "catalog search works as a plain form too" \
|
||||||
|
"$(curl -s -u ":$pass" "http://$addr/ruuat?haku=riisi")" "Riisi"
|
||||||
|
|
||||||
# Nothing was eaten tomorrow. A future date is clamped rather than logged.
|
# Nothing was eaten tomorrow. A future date is clamped rather than logged.
|
||||||
future=$(date -d '+30 days' +%Y-%m-%d)
|
future=$(date -d '+30 days' +%Y-%m-%d)
|
||||||
check "a future date falls back to today" \
|
check "a future date falls back to today" \
|
||||||
|
|||||||
Reference in New Issue
Block a user