package templates import ( "encoding/json" "fmt" "time" "veola/internal/db" "veola/internal/models" ) type ItemResultsData struct { Page Item models.Item Badge BadgeData History []models.PricePoint Results []models.Result Page_ int TotalPages int Order string HistoryChartJSON string // RunMsg / RunError carry feedback from a "Run Now" poll. Both empty on a // normal page load; PostRunItem sets exactly one. RunMsg string RunError string // EndingSoon, when non-nil, surfaces the soonest-ending auction across this // item's results so users can act before close. EndingSoon *db.EndingSoon } type BadgeData struct { Label string Class string // v-badge-low / v-badge-avg / v-badge-target / "" } type GlobalResultsData struct { Page Items []models.Item Results []ItemResultRow ItemID int64 From string To string // EndingSoon mirrors the per-item field but spans every watched item. EndingSoon *db.EndingSoon } type ItemResultRow struct { models.Result ItemName string } // endingSoonStrip surfaces the next auction about to close. Hidden when nil // (the handler decides cutoff: 24h by default). The data-ends-at attribute // drives the live countdown in flair.js. templ endingSoonStrip(e *db.EndingSoon) { if e != nil {
Ending soon
if e.URL != "" { { e.Title } } else { { e.Title } } { e.ItemName }
} } // endsInCell renders the per-row countdown for auction listings only. Fixed- // price rows (EndsAt == nil) get an em-dash placeholder so column widths stay // consistent. templ endsInCell(endsAt *time.Time) { if endsAt != nil { } else { } } templ itemResultsBody(d ItemResultsData) {
@endingSoonStrip(d.EndingSoon)

{ d.Item.Name }

if d.Item.Category != "" {
{ d.Item.Category }
}
if d.Item.BestPrice != nil {
{ fmtPrice(d.Item.BestPrice, d.Item.BestPriceCurrency) }
if d.Item.BestPriceURL != "" { { d.Item.BestPriceStore } } } else {
no price yet
} if d.Badge.Label != "" {
{ d.Badge.Label }
}
Running...

Price History

if len(d.History) < 2 {
Not enough history yet.
} else { // Chart data rides on a data- attribute, not }
@ItemResultsTable(d)
} // ItemResultsTable is the results listing + pagination, plus optional "Run // Now" feedback. It is both part of the initial page (via itemResultsBody) // and the standalone response to POST /items/{id}/run from the results page, // so the Run Now button targets #item-results-table with hx-swap="outerHTML". templ ItemResultsTable(d ItemResultsData) {
if d.RunError != "" {
{ d.RunError }
} else if d.RunMsg != "" {
{ d.RunMsg }
}
for _, r := range d.Results { }
Title Price Store Ends Found Alert
if r.ImageURL != "" { } if r.URL != "" { { r.Title } } else { { r.Title } } if r.MatchedQuery != "" {
via "{ r.MatchedQuery }"
}
{ fmtPrice(r.Price, r.Currency) } { r.Source } @endsInCell(r.EndsAt) { humanTime(r.FoundAt) } if r.Alerted { sent }
if d.TotalPages > 1 {
for i := 1; i <= d.TotalPages; i++ { { fmt.Sprintf("%d", i) } }
}
} func pageClass(i, current int) string { if i == current { return "v-btn" } return "v-btn-ghost" } func toggleOrder(current, axis string) string { switch axis { case "price": if current == "price_asc" { return "price_desc" } return "price_asc" case "found": if current == "found_desc" || current == "" { return "found_asc" } return "found_desc" } return "" } templ ItemResults(d ItemResultsData) { @Layout(d.Page, itemResultsBody(d)) } templ globalResultsBody(d GlobalResultsData) {

All Results

@endingSoonStrip(d.EndingSoon)
for _, r := range d.Results { }
ItemTitlePriceStoreEndsFoundAlert
{ r.ItemName } if r.URL != "" { { r.Title } } else { { r.Title } } if r.MatchedQuery != "" {
via "{ r.MatchedQuery }"
}
{ fmtPrice(r.Price, r.Currency) } { r.Source } @endsInCell(r.EndsAt) { humanTime(r.FoundAt) } if r.Alerted { sent }
} templ GlobalResults(d GlobalResultsData) { @Layout(d.Page, globalResultsBody(d)) } // ChartJSON helper for handlers. type ChartJSON struct { Labels []string `json:"labels"` Points []float64 `json:"points"` } func MustChartJSON(c ChartJSON) string { b, _ := json.Marshal(c) return string(b) }