Auction end times, visual flair, and pre-launch cleanup
Auction handling: - Capture itemEndDate from eBay Browse API and ending_date from ZenMarket (Yahoo JP); plumb through results.ends_at column. Permissive ZenMarket parser (multiple layouts, JST when offset missing). - Per-row "Ends" countdown column + "Ending soon" banner on results pages, live-ticked by flair.js with urgent/critical tinting under 1h/5m. - Backfill ends_at for known auctions when their URL reappears in a poll (dedup hit no longer drops the new end time). - Hide ended auctions from result listings by default via ResultsQuery.ExcludeEnded; rows stay in the DB. Visual flair: - Glassy backdrop-blur v-cards with gradient-mask borders and hover-lift. - htmx swap fade-in via transient .v-just-swapped class. - Count-up animation on dashboard stats. All animations gated behind prefers-reduced-motion. eBay condition + region filters (auctions-style scoping): - items.condition and items.region columns; threaded through item form, CreateItem/UpdateItem, scheduler eBay plan input, and previewKey so cache invalidates when these change. - ebay.SearchParams gains conditionIds and itemLocationCountry filters. Run Now reload + countdown engine: - Run Now now sets HX-Refresh: true (non-htmx fallback: 303 redirect) so the entire results view — best price, chart, badge, last polled — reflects the new poll, instead of swapping just one partial. Pre-launch hardening (P1 set): - auth.EqualizeLoginTiming on no-such-user branch. - (*App).serverError centralizes 500s; replaces err.Error() leaks across results/settings/items/users/dashboard handlers. - main.go server: ReadTimeout 30s / WriteTimeout 60s / IdleTimeout 120s alongside the existing ReadHeaderTimeout. - noListFS wrapper blocks static directory listings. - Credential fields in settings no longer render value=; blank submission preserves the saved value, with per-field "Saved in settings / Set in config.toml / Not set" status indicator. Misc: - -debug flag wires slog to LevelDebug; raw ZenMarket items logged for format diagnosis. - /healthz public endpoint for reverse-proxy probes. - deploy/veola.service systemd unit template (hardening flags, single ReadWritePaths=/var/lib/veola). - handlers_test.go covers /healthz, setup-gate redirect, auth gate, and /login render with httptest + in-memory sqlite. - best_price_currency on items; templates pick the right symbol per row. - .gitignore now excludes *.log / veola-debug.log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -19,3 +19,7 @@ config.toml
|
||||
*.swp
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
# Debug log output from `-debug` runs
|
||||
veola-debug.log
|
||||
*.log
|
||||
|
||||
52
deploy/veola.service
Normal file
52
deploy/veola.service
Normal file
@@ -0,0 +1,52 @@
|
||||
[Unit]
|
||||
Description=Veola price tracker
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
# --- Edit these for your host ---------------------------------------------
|
||||
# User must be able to read config.toml and write WorkingDirectory (sqlite WAL).
|
||||
User=veola
|
||||
Group=veola
|
||||
WorkingDirectory=/var/lib/veola
|
||||
ExecStart=/usr/local/bin/veola-bin -config /etc/veola/config.toml
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
# SIGINT triggers the graceful-shutdown path in main.go (matches Ctrl-C).
|
||||
KillSignal=SIGINT
|
||||
TimeoutStopSec=45s
|
||||
|
||||
# Hardening. Veola only needs to read its config, write its sqlite db, and
|
||||
# reach the network. Everything else can be locked down.
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectControlGroups=true
|
||||
ProtectClock=true
|
||||
ProtectHostname=true
|
||||
ProtectProc=invisible
|
||||
RestrictNamespaces=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service
|
||||
SystemCallFilter=~@privileged @resources
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
CapabilityBoundingSet=
|
||||
AmbientCapabilities=
|
||||
# Allow writes only to the sqlite db directory.
|
||||
ReadWritePaths=/var/lib/veola
|
||||
UMask=0027
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -2,10 +2,70 @@ package apify
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// jstLocation is the timezone ZenMarket reports Yahoo Auctions JP end times
|
||||
// in when the value lacks an explicit offset. Falls back to UTC if the
|
||||
// embedded tzdata lookup fails (main.go imports time/tzdata so in practice
|
||||
// this always resolves).
|
||||
var jstLocation = func() *time.Location {
|
||||
loc, err := time.LoadLocation("Asia/Tokyo")
|
||||
if err != nil {
|
||||
return time.UTC
|
||||
}
|
||||
return loc
|
||||
}()
|
||||
|
||||
// yahooEndingDateLayouts is the ordered list of layouts attempted when
|
||||
// parsing ZenMarket's `ending_date`. Some entries use Local-as-JST (no
|
||||
// offset in the string) — see parseYahooEndingDate for the routing.
|
||||
var yahooEndingDateLayouts = []struct {
|
||||
layout string
|
||||
jst bool // true: parse as JST; false: parse with offset/zone from string
|
||||
}{
|
||||
{time.RFC3339, false},
|
||||
{"2006-01-02T15:04:05", true},
|
||||
{"2006-01-02 15:04:05", true},
|
||||
{"2006/01/02 15:04:05", true},
|
||||
{"2006/01/02 15:04", true},
|
||||
{"2006-01-02 15:04", true},
|
||||
}
|
||||
|
||||
// parseYahooEndingDate is a permissive parser for ZenMarket's ending_date
|
||||
// field. The actor's output format has shifted over time and isn't documented;
|
||||
// rather than wedging on one layout, try the known shapes in order and treat
|
||||
// anything without an explicit zone as JST (Yahoo Auctions runs in Japan).
|
||||
// Returns nil + logs a warning when nothing parses, so operators see the raw
|
||||
// value and can extend this list.
|
||||
func parseYahooEndingDate(s string) *time.Time {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
for _, l := range yahooEndingDateLayouts {
|
||||
var (
|
||||
t time.Time
|
||||
err error
|
||||
)
|
||||
if l.jst {
|
||||
t, err = time.ParseInLocation(l.layout, s, jstLocation)
|
||||
} else {
|
||||
t, err = time.Parse(l.layout, s)
|
||||
}
|
||||
if err == nil {
|
||||
slog.Debug("yahoo ending_date parsed",
|
||||
"raw", s, "layout", l.layout, "jst_assumed", l.jst, "parsed", t.UTC().Format(time.RFC3339))
|
||||
return &t
|
||||
}
|
||||
}
|
||||
slog.Warn("yahoo ending_date: no layout matched", "raw", s)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ActiveListingInput is the input schema for `automation-lab/ebay-scraper`.
|
||||
// The actor accepts keyword searches and standard filters; it targets
|
||||
// ebay.com only (no per-marketplace routing in the actor itself), so
|
||||
@@ -145,6 +205,10 @@ type UnifiedResult struct {
|
||||
// MatchedQuery records which alias from the item's query list produced
|
||||
// this row. Empty for URL-only items or rows from non-search sources.
|
||||
MatchedQuery string
|
||||
// EndsAt is the auction end time, if known. Auction-format eBay listings
|
||||
// and Yahoo Auctions JP listings populate this; fixed-price listings
|
||||
// and most Apify scraper outputs leave it nil.
|
||||
EndsAt *time.Time
|
||||
}
|
||||
|
||||
// Decode unmarshals a list of raw JSON items into UnifiedResult slices using
|
||||
@@ -197,8 +261,10 @@ func Decode(items []json.RawMessage, source string) ([]UnifiedResult, error) {
|
||||
}
|
||||
case SourceYahooJP:
|
||||
for _, raw := range items {
|
||||
slog.Debug("yahoo raw item", "json", string(raw))
|
||||
var r YahooAuctionsJPResult
|
||||
if err := json.Unmarshal(raw, &r); err != nil {
|
||||
slog.Debug("yahoo decode failed", "err", err, "json", string(raw))
|
||||
continue
|
||||
}
|
||||
img := ""
|
||||
@@ -213,6 +279,7 @@ func Decode(items []json.RawMessage, source string) ([]UnifiedResult, error) {
|
||||
Store: "yahoo-auctions-jp (via zenmarket)",
|
||||
ImageURL: img,
|
||||
Source: source,
|
||||
EndsAt: parseYahooEndingDate(r.EndingDate),
|
||||
})
|
||||
}
|
||||
case SourceMercariJP:
|
||||
|
||||
@@ -73,6 +73,19 @@ func CheckPassword(hash, plain string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(plain)) == nil
|
||||
}
|
||||
|
||||
// dummyHash is a valid bcrypt hash of a throwaway value. It exists only so a
|
||||
// login attempt for a non-existent user can still run a real bcrypt
|
||||
// comparison, equalizing response time and closing the user-enumeration
|
||||
// timing oracle.
|
||||
var dummyHash, _ = bcrypt.GenerateFromPassword([]byte("veola-timing-equalizer"), BcryptCost)
|
||||
|
||||
// EqualizeLoginTiming performs a bcrypt comparison against a throwaway hash so
|
||||
// that a missing username costs roughly the same wall-clock time as a wrong
|
||||
// password. Call it on the no-such-user branch of login.
|
||||
func EqualizeLoginTiming() {
|
||||
_ = bcrypt.CompareHashAndPassword(dummyHash, []byte("veola-timing-equalizer-x"))
|
||||
}
|
||||
|
||||
// LogIn writes the user id into the session and rotates the token.
|
||||
func (m *Manager) LogIn(ctx context.Context, userID int64) error {
|
||||
if err := m.Sessions.RenewToken(ctx); err != nil {
|
||||
|
||||
@@ -37,6 +37,22 @@ func Open(path string) (*sql.DB, error) {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := addColumnIfMissing(conn, "items", "condition", "TEXT"); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := addColumnIfMissing(conn, "items", "region", "TEXT"); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := addColumnIfMissing(conn, "items", "best_price_currency", "TEXT"); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
if err := addColumnIfMissing(conn, "results", "ends_at", "DATETIME"); err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -274,20 +274,20 @@ func (s *Store) CreateItem(ctx context.Context, it *models.Item) (int64, error)
|
||||
INSERT INTO items (
|
||||
name, search_query, url, category, target_price, ntfy_topic, ntfy_priority,
|
||||
poll_interval_minutes, include_out_of_stock, min_price, exclude_keywords,
|
||||
listing_type,
|
||||
listing_type, condition, region,
|
||||
actor_active, actor_sold, actor_price_compare, use_price_comparison,
|
||||
active, best_price, best_price_store, best_price_url, best_price_image_url,
|
||||
active, best_price, best_price_currency, best_price_store, best_price_url, best_price_image_url,
|
||||
best_price_title, last_polled_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
it.Name, s.enc(it.SearchQuery), nullStr(it.URL), nullStr(it.Category),
|
||||
nullFloat(it.TargetPrice), s.enc(it.NtfyTopic), it.NtfyPriority,
|
||||
it.PollIntervalMinutes, boolToInt(it.IncludeOutOfStock),
|
||||
nullFloat(it.MinPrice), nullStr(s.enc(it.ExcludeKeywords)),
|
||||
nullStr(it.ListingType),
|
||||
nullStr(it.ListingType), nullStr(it.Condition), nullStr(it.Region),
|
||||
nullStr(it.ActorActive), nullStr(it.ActorSold), nullStr(it.ActorPriceCompare),
|
||||
boolToInt(it.UsePriceComparison), boolToInt(it.Active),
|
||||
nullFloat(it.BestPrice), nullStr(it.BestPriceStore),
|
||||
nullFloat(it.BestPrice), nullStr(it.BestPriceCurrency), nullStr(it.BestPriceStore),
|
||||
nullStr(s.enc(it.BestPriceURL)), nullStr(s.enc(it.BestPriceImageURL)),
|
||||
nullStr(s.enc(it.BestPriceTitle)), nullTime(it.LastPolledAt),
|
||||
)
|
||||
@@ -310,7 +310,7 @@ func (s *Store) UpdateItem(ctx context.Context, it *models.Item) error {
|
||||
name = ?, search_query = ?, url = ?, category = ?, target_price = ?,
|
||||
ntfy_topic = ?, ntfy_priority = ?, poll_interval_minutes = ?,
|
||||
include_out_of_stock = ?, min_price = ?, exclude_keywords = ?,
|
||||
listing_type = ?,
|
||||
listing_type = ?, condition = ?, region = ?,
|
||||
actor_active = ?, actor_sold = ?, actor_price_compare = ?,
|
||||
use_price_comparison = ?, active = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
@@ -319,7 +319,7 @@ func (s *Store) UpdateItem(ctx context.Context, it *models.Item) error {
|
||||
nullFloat(it.TargetPrice), s.enc(it.NtfyTopic), it.NtfyPriority,
|
||||
it.PollIntervalMinutes, boolToInt(it.IncludeOutOfStock),
|
||||
nullFloat(it.MinPrice), nullStr(s.enc(it.ExcludeKeywords)),
|
||||
nullStr(it.ListingType),
|
||||
nullStr(it.ListingType), nullStr(it.Condition), nullStr(it.Region),
|
||||
nullStr(it.ActorActive), nullStr(it.ActorSold), nullStr(it.ActorPriceCompare),
|
||||
boolToInt(it.UsePriceComparison), boolToInt(it.Active),
|
||||
it.ID,
|
||||
@@ -492,10 +492,11 @@ func (s *Store) ListCategories(ctx context.Context) ([]string, error) {
|
||||
func (s *Store) UpdateItemPollResult(ctx context.Context, id int64, best *models.Item, errMsg string) error {
|
||||
var (
|
||||
bestPrice sql.NullFloat64
|
||||
bestStore, bestURL, bestImage, bestTitle, errField sql.NullString
|
||||
bestCurrency, bestStore, bestURL, bestImage, bestTitle, errField sql.NullString
|
||||
)
|
||||
if best != nil {
|
||||
bestPrice = nullFloat(best.BestPrice)
|
||||
bestCurrency = nullStr(best.BestPriceCurrency)
|
||||
bestStore = nullStr(best.BestPriceStore)
|
||||
bestURL = nullStr(s.enc(best.BestPriceURL))
|
||||
bestImage = nullStr(s.enc(best.BestPriceImageURL))
|
||||
@@ -506,20 +507,20 @@ func (s *Store) UpdateItemPollResult(ctx context.Context, id int64, best *models
|
||||
}
|
||||
_, err := s.DB.ExecContext(ctx, `
|
||||
UPDATE items SET
|
||||
best_price = ?, best_price_store = ?, best_price_url = ?,
|
||||
best_price = ?, best_price_currency = ?, best_price_store = ?, best_price_url = ?,
|
||||
best_price_image_url = ?, best_price_title = ?,
|
||||
last_polled_at = CURRENT_TIMESTAMP, last_poll_error = ?
|
||||
WHERE id = ?
|
||||
`, bestPrice, bestStore, bestURL, bestImage, bestTitle, errField, id)
|
||||
`, bestPrice, bestCurrency, bestStore, bestURL, bestImage, bestTitle, errField, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const itemSelect = `
|
||||
SELECT id, name, search_query, url, category, target_price, ntfy_topic, ntfy_priority,
|
||||
poll_interval_minutes, include_out_of_stock, min_price, exclude_keywords,
|
||||
listing_type,
|
||||
listing_type, condition, region,
|
||||
actor_active, actor_sold, actor_price_compare, use_price_comparison,
|
||||
active, last_polled_at, last_poll_error, best_price, best_price_store,
|
||||
active, last_polled_at, last_poll_error, best_price, best_price_currency, best_price_store,
|
||||
best_price_url, best_price_image_url, best_price_title, created_at, updated_at
|
||||
FROM items
|
||||
`
|
||||
@@ -532,10 +533,10 @@ func scanItem(r rowScanner) (*models.Item, error) {
|
||||
var (
|
||||
it models.Item
|
||||
searchQuery, urlS, category, listingType sql.NullString
|
||||
excludeKw sql.NullString
|
||||
excludeKw, condition, region sql.NullString
|
||||
actorA, actorS, actorP sql.NullString
|
||||
ntfyTopic, lastPollErr sql.NullString
|
||||
bestStore, bestURL, bestImage, bestTitle sql.NullString
|
||||
bestCurrency, bestStore, bestURL, bestImage, bestTitle sql.NullString
|
||||
targetPrice, minPrice, bestPrice sql.NullFloat64
|
||||
includeOOS, usePC, active int
|
||||
lastPolledAt sql.NullTime
|
||||
@@ -543,9 +544,9 @@ func scanItem(r rowScanner) (*models.Item, error) {
|
||||
if err := r.Scan(
|
||||
&it.ID, &it.Name, &searchQuery, &urlS, &category, &targetPrice, &ntfyTopic, &it.NtfyPriority,
|
||||
&it.PollIntervalMinutes, &includeOOS, &minPrice, &excludeKw,
|
||||
&listingType,
|
||||
&listingType, &condition, ®ion,
|
||||
&actorA, &actorS, &actorP, &usePC,
|
||||
&active, &lastPolledAt, &lastPollErr, &bestPrice, &bestStore,
|
||||
&active, &lastPolledAt, &lastPollErr, &bestPrice, &bestCurrency, &bestStore,
|
||||
&bestURL, &bestImage, &bestTitle, &it.CreatedAt, &it.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
@@ -556,11 +557,14 @@ func scanItem(r rowScanner) (*models.Item, error) {
|
||||
it.URL = urlS.String
|
||||
it.Category = category.String
|
||||
it.ListingType = listingType.String
|
||||
it.Condition = condition.String
|
||||
it.Region = region.String
|
||||
it.ActorActive = actorA.String
|
||||
it.ActorSold = actorS.String
|
||||
it.ActorPriceCompare = actorP.String
|
||||
it.NtfyTopic = ntfyTopic.String
|
||||
it.LastPollError = lastPollErr.String
|
||||
it.BestPriceCurrency = bestCurrency.String
|
||||
it.BestPriceStore = bestStore.String
|
||||
it.BestPriceURL = bestURL.String
|
||||
it.BestPriceImageURL = bestImage.String
|
||||
@@ -589,13 +593,14 @@ func (s *Store) decryptItem(it *models.Item) *models.Item {
|
||||
|
||||
func (s *Store) InsertResult(ctx context.Context, r *models.Result) (int64, error) {
|
||||
res, err := s.DB.ExecContext(ctx, `
|
||||
INSERT INTO results (item_id, title, price, currency, url, source, image_url, matched_query, alerted, found_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
INSERT INTO results (item_id, title, price, currency, url, source, image_url, matched_query, alerted, found_at, ends_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, ?)
|
||||
`,
|
||||
r.ItemID, s.enc(r.Title), nullFloat(r.Price), r.Currency,
|
||||
nullStr(r.URL), nullStr(r.Source), s.enc(r.ImageURL),
|
||||
nullStr(s.enc(r.MatchedQuery)),
|
||||
boolToInt(r.Alerted),
|
||||
nullTime(r.EndsAt),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
@@ -624,11 +629,28 @@ func (s *Store) MarkResultAlerted(ctx context.Context, id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// BackfillResultEndsAt sets ends_at on an existing result row that's currently
|
||||
// missing one. Used during polling: when a known auction listing reappears in
|
||||
// a poll result, we still want its end time recorded even though the row
|
||||
// itself isn't being re-inserted (URL dedup).
|
||||
func (s *Store) BackfillResultEndsAt(ctx context.Context, itemID int64, urlStr string, endsAt time.Time) error {
|
||||
if urlStr == "" {
|
||||
return nil
|
||||
}
|
||||
_, err := s.DB.ExecContext(ctx,
|
||||
`UPDATE results SET ends_at = ? WHERE item_id = ? AND url = ? AND ends_at IS NULL`,
|
||||
endsAt, itemID, urlStr)
|
||||
return err
|
||||
}
|
||||
|
||||
type ResultsQuery struct {
|
||||
ItemID int64 // 0 = all items
|
||||
Limit int
|
||||
Offset int
|
||||
Order string // "price_asc", "price_desc", "found_desc" (default), "found_asc"
|
||||
// ExcludeEnded drops rows whose ends_at is in the past. Fixed-price
|
||||
// listings (ends_at IS NULL) are kept regardless: they don't expire.
|
||||
ExcludeEnded bool
|
||||
}
|
||||
|
||||
func (s *Store) ListResults(ctx context.Context, q ResultsQuery) ([]models.Result, error) {
|
||||
@@ -646,14 +668,22 @@ func (s *Store) ListResults(ctx context.Context, q ResultsQuery) ([]models.Resul
|
||||
limit = 20
|
||||
}
|
||||
args := []any{}
|
||||
where := ""
|
||||
var conds []string
|
||||
if q.ItemID != 0 {
|
||||
where = `WHERE item_id = ?`
|
||||
conds = append(conds, `item_id = ?`)
|
||||
args = append(args, q.ItemID)
|
||||
}
|
||||
if q.ExcludeEnded {
|
||||
conds = append(conds, `(ends_at IS NULL OR ends_at > ?)`)
|
||||
args = append(args, time.Now().UTC())
|
||||
}
|
||||
where := ""
|
||||
if len(conds) > 0 {
|
||||
where = `WHERE ` + strings.Join(conds, ` AND `)
|
||||
}
|
||||
args = append(args, limit, q.Offset)
|
||||
rows, err := s.DB.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT id, item_id, title, price, currency, url, source, image_url, matched_query, alerted, found_at
|
||||
SELECT id, item_id, title, price, currency, url, source, image_url, matched_query, alerted, found_at, ends_at
|
||||
FROM results %s ORDER BY %s LIMIT ? OFFSET ?
|
||||
`, where, order), args...)
|
||||
if err != nil {
|
||||
@@ -667,8 +697,9 @@ func (s *Store) ListResults(ctx context.Context, q ResultsQuery) ([]models.Resul
|
||||
title, urlS, source, imageS, matchQ sql.NullString
|
||||
price sql.NullFloat64
|
||||
alerted int
|
||||
endsAt sql.NullTime
|
||||
)
|
||||
if err := rows.Scan(&r.ID, &r.ItemID, &title, &price, &r.Currency, &urlS, &source, &imageS, &matchQ, &alerted, &r.FoundAt); err != nil {
|
||||
if err := rows.Scan(&r.ID, &r.ItemID, &title, &price, &r.Currency, &urlS, &source, &imageS, &matchQ, &alerted, &r.FoundAt, &endsAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Title = s.dec(title.String)
|
||||
@@ -678,19 +709,76 @@ func (s *Store) ListResults(ctx context.Context, q ResultsQuery) ([]models.Resul
|
||||
r.MatchedQuery = s.dec(matchQ.String)
|
||||
r.Price = ptrFloat(price)
|
||||
r.Alerted = alerted != 0
|
||||
r.EndsAt = ptrTime(endsAt)
|
||||
out = append(out, r)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) CountResults(ctx context.Context, itemID int64) (int, error) {
|
||||
// EndingSoon is a compact projection for the "ending soon" strip: the single
|
||||
// nearest-to-end auction across the user's results, with enough context to
|
||||
// render and link to it.
|
||||
type EndingSoon struct {
|
||||
ItemID int64
|
||||
ItemName string
|
||||
Title string
|
||||
URL string
|
||||
EndsAt time.Time
|
||||
}
|
||||
|
||||
// NextEndingResult returns the soonest-ending result whose ends_at lies in the
|
||||
// window (now, now+within]. If itemID is 0 the search spans all items; nil is
|
||||
// returned when no auction falls inside the window.
|
||||
func (s *Store) NextEndingResult(ctx context.Context, itemID int64, within time.Duration) (*EndingSoon, error) {
|
||||
now := time.Now().UTC()
|
||||
cutoff := now.Add(within)
|
||||
q := `SELECT r.item_id, r.title, r.url, r.ends_at, i.name
|
||||
FROM results r JOIN items i ON r.item_id = i.id
|
||||
WHERE r.ends_at IS NOT NULL AND r.ends_at > ? AND r.ends_at <= ?`
|
||||
args := []any{now, cutoff}
|
||||
if itemID != 0 {
|
||||
q += ` AND r.item_id = ?`
|
||||
args = append(args, itemID)
|
||||
}
|
||||
q += ` ORDER BY r.ends_at ASC LIMIT 1`
|
||||
row := s.DB.QueryRowContext(ctx, q, args...)
|
||||
var (
|
||||
e EndingSoon
|
||||
title sql.NullString
|
||||
urlS sql.NullString
|
||||
endsAt time.Time
|
||||
)
|
||||
if err := row.Scan(&e.ItemID, &title, &urlS, &endsAt, &e.ItemName); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
e.Title = s.dec(title.String)
|
||||
e.URL = urlS.String
|
||||
e.EndsAt = endsAt
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// CountResults returns the row count matching the same filters ListResults
|
||||
// applies. Pagination relies on this matching the visible list, so it must
|
||||
// honor ExcludeEnded too.
|
||||
func (s *Store) CountResults(ctx context.Context, itemID int64, excludeEnded bool) (int, error) {
|
||||
var n int
|
||||
q := `SELECT COUNT(*) FROM results`
|
||||
args := []any{}
|
||||
var conds []string
|
||||
if itemID != 0 {
|
||||
q += ` WHERE item_id = ?`
|
||||
conds = append(conds, `item_id = ?`)
|
||||
args = append(args, itemID)
|
||||
}
|
||||
if excludeEnded {
|
||||
conds = append(conds, `(ends_at IS NULL OR ends_at > ?)`)
|
||||
args = append(args, time.Now().UTC())
|
||||
}
|
||||
if len(conds) > 0 {
|
||||
q += ` WHERE ` + strings.Join(conds, ` AND `)
|
||||
}
|
||||
err := s.DB.QueryRowContext(ctx, q, args...).Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ CREATE TABLE IF NOT EXISTS items (
|
||||
min_price REAL,
|
||||
exclude_keywords TEXT,
|
||||
listing_type TEXT,
|
||||
condition TEXT,
|
||||
region TEXT,
|
||||
actor_active TEXT,
|
||||
actor_sold TEXT,
|
||||
actor_price_compare TEXT,
|
||||
@@ -31,6 +33,7 @@ CREATE TABLE IF NOT EXISTS items (
|
||||
last_polled_at DATETIME,
|
||||
last_poll_error TEXT,
|
||||
best_price REAL,
|
||||
best_price_currency TEXT,
|
||||
best_price_store TEXT,
|
||||
best_price_url TEXT,
|
||||
best_price_image_url TEXT,
|
||||
@@ -61,7 +64,8 @@ CREATE TABLE IF NOT EXISTS results (
|
||||
image_url TEXT,
|
||||
matched_query TEXT,
|
||||
alerted INTEGER DEFAULT 0,
|
||||
found_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
found_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
ends_at DATETIME
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_results_item ON results(item_id, found_at DESC);
|
||||
|
||||
@@ -149,6 +149,8 @@ type browseItemSummary struct {
|
||||
Seller struct {
|
||||
Username string `json:"username"`
|
||||
} `json:"seller"`
|
||||
// itemEndDate is present only on auction-format listings.
|
||||
ItemEndDate string `json:"itemEndDate"`
|
||||
}
|
||||
|
||||
// Search runs one item_summary/search call and returns normalized listings.
|
||||
@@ -174,8 +176,20 @@ func (c *Client) Search(ctx context.Context, p SearchParams) ([]Listing, error)
|
||||
q := url.Values{}
|
||||
q.Set("q", query)
|
||||
q.Set("limit", strconv.Itoa(limit))
|
||||
// The Browse API "filter" parameter takes a comma-separated list of
|
||||
// filter clauses; assemble whichever ones the caller requested.
|
||||
var filters []string
|
||||
if f := buyingOptionsFilter(p.ListingType); f != "" {
|
||||
q.Set("filter", f)
|
||||
filters = append(filters, f)
|
||||
}
|
||||
if f := conditionIDsFilter(p.Condition); f != "" {
|
||||
filters = append(filters, f)
|
||||
}
|
||||
if f := itemLocationFilter(p.Region); f != "" {
|
||||
filters = append(filters, f)
|
||||
}
|
||||
if len(filters) > 0 {
|
||||
q.Set("filter", strings.Join(filters, ","))
|
||||
}
|
||||
reqURL := c.ends.browse + "/item_summary/search?" + q.Encode()
|
||||
|
||||
@@ -214,6 +228,12 @@ func (c *Client) Search(ctx context.Context, p SearchParams) ([]Listing, error)
|
||||
if s.Seller.Username != "" {
|
||||
store = "ebay (" + s.Seller.Username + ")"
|
||||
}
|
||||
var endsAt *time.Time
|
||||
if s.ItemEndDate != "" {
|
||||
if t, err := time.Parse(time.RFC3339, s.ItemEndDate); err == nil {
|
||||
endsAt = &t
|
||||
}
|
||||
}
|
||||
out = append(out, Listing{
|
||||
Title: s.Title,
|
||||
Price: price,
|
||||
@@ -221,6 +241,7 @@ func (c *Client) Search(ctx context.Context, p SearchParams) ([]Listing, error)
|
||||
URL: s.ItemWebURL,
|
||||
Store: store,
|
||||
ImageURL: img,
|
||||
EndsAt: endsAt,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
|
||||
@@ -43,3 +43,35 @@ func TestBuyingOptionsFilter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionIDsFilter(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "",
|
||||
"anything": "",
|
||||
"new": "conditionIds:{1000|1500}",
|
||||
"NEW": "conditionIds:{1000|1500}",
|
||||
" used ": "conditionIds:{3000}",
|
||||
"refurbished": "conditionIds:{2000|2010|2020|2030|2500}",
|
||||
"parts": "conditionIds:{7000}",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := conditionIDsFilter(in); got != want {
|
||||
t.Errorf("conditionIDsFilter(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestItemLocationFilter(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"": "",
|
||||
" ": "",
|
||||
"us": "itemLocationCountry:US",
|
||||
"GB": "itemLocationCountry:GB",
|
||||
" jp ": "itemLocationCountry:JP",
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := itemLocationFilter(in); got != want {
|
||||
t.Errorf("itemLocationFilter(%q) = %q, want %q", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package ebay
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SearchParams is the input to a single Browse API item_summary/search call.
|
||||
// It is provider-specific and is carried as the opaque input payload of a
|
||||
@@ -14,6 +17,14 @@ type SearchParams struct {
|
||||
// ListingType is Veola's vocabulary ("all", "bin"/"buy_it_now",
|
||||
// "auction"); it is mapped to a buyingOptions filter.
|
||||
ListingType string
|
||||
// Condition is Veola's condition vocabulary ("new", "used",
|
||||
// "refurbished", "parts"); it is mapped to a conditionIds filter. Empty
|
||||
// means no condition filter.
|
||||
Condition string
|
||||
// Region is an ISO 3166-1 alpha-2 country code constraining item
|
||||
// location (mapped to the itemLocationCountry filter). Empty means no
|
||||
// location filter.
|
||||
Region string
|
||||
// Limit caps the number of results requested (Browse API max is 200).
|
||||
Limit int
|
||||
}
|
||||
@@ -28,6 +39,10 @@ type Listing struct {
|
||||
URL string
|
||||
Store string
|
||||
ImageURL string
|
||||
// EndsAt is the auction end time as reported by the Browse API
|
||||
// (itemEndDate). Nil for fixed-price ("Buy It Now") listings, which
|
||||
// don't have one.
|
||||
EndsAt *time.Time
|
||||
}
|
||||
|
||||
// MarketplaceID maps a Veola marketplace string (e.g. "ebay.com",
|
||||
@@ -92,3 +107,34 @@ func buyingOptionsFilter(listingType string) string {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// conditionIDsFilter maps Veola's condition vocabulary to a Browse API
|
||||
// "conditionIds" filter clause. Each Veola value expands to the set of eBay
|
||||
// condition IDs that belong to it (e.g. "new" covers both brand-new and
|
||||
// new-other). An empty or unknown value yields no filter.
|
||||
func conditionIDsFilter(condition string) string {
|
||||
var ids string
|
||||
switch strings.ToLower(strings.TrimSpace(condition)) {
|
||||
case "new":
|
||||
ids = "1000|1500"
|
||||
case "used":
|
||||
ids = "3000"
|
||||
case "refurbished":
|
||||
ids = "2000|2010|2020|2030|2500"
|
||||
case "parts":
|
||||
ids = "7000"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
return "conditionIds:{" + ids + "}"
|
||||
}
|
||||
|
||||
// itemLocationFilter maps an ISO 3166-1 alpha-2 country code to a Browse API
|
||||
// "itemLocationCountry" filter clause. An empty value yields no filter.
|
||||
func itemLocationFilter(region string) string {
|
||||
r := strings.ToUpper(strings.TrimSpace(region))
|
||||
if r == "" {
|
||||
return ""
|
||||
}
|
||||
return "itemLocationCountry:" + r
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ func (a *App) PostLogin(w http.ResponseWriter, r *http.Request) {
|
||||
username := strings.TrimSpace(r.PostFormValue("username"))
|
||||
password := r.PostFormValue("password")
|
||||
u, err := a.Store.GetUserByUsername(r.Context(), username)
|
||||
if err != nil || u == nil {
|
||||
// Run a bcrypt comparison anyway so a missing username takes the
|
||||
// same time as a wrong password (no user-enumeration oracle).
|
||||
auth.EqualizeLoginTiming()
|
||||
}
|
||||
if err != nil || u == nil || !auth.CheckPassword(u.PasswordHash, password) {
|
||||
render(w, r, templates.Login(templates.LoginData{
|
||||
Page: a.page(r, "Sign in", ""),
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func (a *App) GetDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
d, err := a.dashboardData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
render(w, r, templates.Dashboard(d))
|
||||
@@ -21,7 +21,7 @@ func (a *App) GetDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *App) GetDashboardRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
d, err := a.dashboardData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
// Render ONLY the inner body. The hx-swap="outerHTML" on DashboardBody's
|
||||
@@ -36,7 +36,7 @@ func (a *App) dashboardData(r *http.Request) (templates.DashboardData, error) {
|
||||
if err != nil {
|
||||
return templates.DashboardData{}, err
|
||||
}
|
||||
results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{Limit: 20})
|
||||
results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{Limit: 20, ExcludeEnded: true})
|
||||
if err != nil {
|
||||
return templates.DashboardData{}, err
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -52,9 +53,19 @@ func (a *App) Routes() http.Handler {
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(securityHeaders)
|
||||
|
||||
fs := http.FileServer(http.Dir("./static"))
|
||||
// noListFS denies directory requests, so http.FileServer can't render
|
||||
// an index listing of static/ if an index.html is ever absent.
|
||||
fs := http.FileServer(noListFS{http.Dir("./static")})
|
||||
r.Handle("/static/*", http.StripPrefix("/static/", fs))
|
||||
|
||||
// Health check for reverse-proxy/uptime probes. No session, no setup
|
||||
// gate, no auth — just a 200 to confirm the process is serving.
|
||||
r.Get("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
// All other routes pass through session loading + setup gate.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(a.Auth.Sessions.LoadAndSave)
|
||||
@@ -169,6 +180,34 @@ func (a *App) page(r *http.Request, title, active string) templates.Page {
|
||||
}
|
||||
}
|
||||
|
||||
// noListFS wraps an http.FileSystem and refuses to open directories, which
|
||||
// stops http.FileServer from emitting an auto-generated directory listing.
|
||||
type noListFS struct{ fs http.FileSystem }
|
||||
|
||||
func (n noListFS) Open(name string) (http.File, error) {
|
||||
f, err := n.fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
if info.IsDir() {
|
||||
f.Close()
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// serverError logs the underlying error and returns a generic 500 to the
|
||||
// client, so internal details (DB errors, file paths) never reach the browser.
|
||||
func (a *App) serverError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
slog.Error("handler error", "path", r.URL.Path, "err", err)
|
||||
http.Error(w, "internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
func render(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 {
|
||||
|
||||
115
internal/handlers/handlers_test.go
Normal file
115
internal/handlers/handlers_test.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"veola/internal/apify"
|
||||
"veola/internal/auth"
|
||||
"veola/internal/config"
|
||||
"veola/internal/crypto"
|
||||
"veola/internal/db"
|
||||
"veola/internal/models"
|
||||
"veola/internal/ntfy"
|
||||
"veola/internal/scheduler"
|
||||
)
|
||||
|
||||
// newTestApp builds an App backed by a fresh sqlite db in t.TempDir(). The
|
||||
// scheduler, apify, and ntfy clients are wired but unused by the routes we
|
||||
// hit here. The returned http.Handler is App.Routes().
|
||||
func newTestApp(t *testing.T) (*App, http.Handler) {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "test.db")
|
||||
sqlDB, err := db.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("db.Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { sqlDB.Close() })
|
||||
|
||||
key, err := crypto.DeriveKey([]byte("test-encryption-key-32-bytes-min-aaaaaa"))
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveKey: %v", err)
|
||||
}
|
||||
store := db.NewStore(sqlDB, key)
|
||||
|
||||
am, err := auth.NewManager(sqlDB, store, strings.Repeat("a", 32), false)
|
||||
if err != nil {
|
||||
t.Fatalf("auth.NewManager: %v", err)
|
||||
}
|
||||
|
||||
cfg := &config.Config{}
|
||||
ap := apify.New("")
|
||||
nt := ntfy.New("")
|
||||
sc := scheduler.New(cfg, store, ap, nt)
|
||||
|
||||
app := New(cfg, store, am, ap, nt, sc)
|
||||
return app, app.Routes()
|
||||
}
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
_, h := newTestApp(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
if got := rec.Body.String(); got != "ok" {
|
||||
t.Fatalf("body = %q, want %q", got, "ok")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupGateRedirectsWhenNoUsers(t *testing.T) {
|
||||
_, h := newTestApp(t)
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("status = %d, want 303", rec.Code)
|
||||
}
|
||||
if loc := rec.Header().Get("Location"); loc != "/setup" {
|
||||
t.Fatalf("Location = %q, want /setup", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAuthRedirectsToLogin(t *testing.T) {
|
||||
app, h := newTestApp(t)
|
||||
hash, err := auth.HashPassword("a-long-enough-password")
|
||||
if err != nil {
|
||||
t.Fatalf("HashPassword: %v", err)
|
||||
}
|
||||
if _, err := app.Store.CreateUser(context.Background(), "admin", hash, models.RoleAdmin); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("status = %d, want 303", rec.Code)
|
||||
}
|
||||
if loc := rec.Header().Get("Location"); loc != "/login" {
|
||||
t.Fatalf("Location = %q, want /login", loc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginPageRenders(t *testing.T) {
|
||||
app, h := newTestApp(t)
|
||||
hash, _ := auth.HashPassword("a-long-enough-password")
|
||||
if _, err := app.Store.CreateUser(context.Background(), "admin", hash, models.RoleAdmin); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/login", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200", rec.Code)
|
||||
}
|
||||
if !strings.Contains(rec.Body.String(), "<form") {
|
||||
t.Fatalf("body missing <form>")
|
||||
}
|
||||
}
|
||||
@@ -93,6 +93,8 @@ func parseItemForm(r *http.Request) (models.Item, []string) {
|
||||
}
|
||||
it.Marketplaces = collectMarketplaces(r.PostForm["marketplace"], r.PostFormValue("marketplace_custom"))
|
||||
it.ListingType = strings.TrimSpace(r.PostFormValue("listing_type"))
|
||||
it.Condition = strings.TrimSpace(r.PostFormValue("condition"))
|
||||
it.Region = strings.ToUpper(strings.TrimSpace(r.PostFormValue("region")))
|
||||
it.ActorActive = strings.TrimSpace(r.PostFormValue("actor_active"))
|
||||
it.ActorSold = strings.TrimSpace(r.PostFormValue("actor_sold"))
|
||||
it.ActorPriceCompare = strings.TrimSpace(r.PostFormValue("actor_price_compare"))
|
||||
@@ -253,6 +255,8 @@ func (a *App) runPreview(ctx context.Context, it models.Item) ([]apify.UnifiedRe
|
||||
Marketplace: previewMarket,
|
||||
ListingType: it.ListingType,
|
||||
ActorIDs: strings.Join(actorIDs, ","),
|
||||
Condition: it.Condition,
|
||||
Region: it.Region,
|
||||
MaxResults: 30,
|
||||
}
|
||||
if cached, src, ok := a.Preview.Get(key); ok {
|
||||
@@ -304,6 +308,8 @@ func formValuesFromItem(it models.Item, r *http.Request) templates.FormValues {
|
||||
IncludeOutOfStock: it.IncludeOutOfStock,
|
||||
Marketplaces: it.Marketplaces,
|
||||
ListingType: it.ListingType,
|
||||
Condition: it.Condition,
|
||||
Region: it.Region,
|
||||
ActorActive: it.ActorActive,
|
||||
ActorSold: it.ActorSold,
|
||||
ActorPriceCompare: it.ActorPriceCompare,
|
||||
@@ -319,7 +325,7 @@ func (a *App) PostCreateItem(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
id, err := a.Store.CreateItem(r.Context(), &it)
|
||||
if err != nil {
|
||||
http.Error(w, "could not save item: "+err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
it.ID = id
|
||||
@@ -361,7 +367,7 @@ func (a *App) PostUpdateItem(w http.ResponseWriter, r *http.Request) {
|
||||
updated.ID = id
|
||||
updated.Active = existing.Active
|
||||
if err := a.Store.UpdateItem(r.Context(), &updated); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.Scheduler.SyncItem(updated)
|
||||
@@ -377,7 +383,7 @@ func (a *App) PostToggleItem(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
it.Active = !it.Active
|
||||
if err := a.Store.SetItemActive(r.Context(), id, it.Active); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.Scheduler.SyncItem(*it)
|
||||
@@ -387,7 +393,7 @@ func (a *App) PostToggleItem(w http.ResponseWriter, r *http.Request) {
|
||||
func (a *App) PostDeleteItem(w http.ResponseWriter, r *http.Request) {
|
||||
id := intParam(r, "id")
|
||||
if err := a.Store.DeleteItem(r.Context(), id); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
a.Scheduler.RemoveItem(id)
|
||||
@@ -410,31 +416,22 @@ func (a *App) PostRunItem(w http.ResponseWriter, r *http.Request) {
|
||||
defer cancel()
|
||||
a.Scheduler.RunPoll(ctx, *it)
|
||||
|
||||
// RunPoll writes best price, last_polled_at, and last_poll_error; re-fetch
|
||||
// so the rendered partial shows the post-poll state.
|
||||
fresh, err := a.Store.GetItem(r.Context(), id)
|
||||
if err != nil || fresh == nil {
|
||||
http.Error(w, "could not reload item after run", http.StatusInternalServerError)
|
||||
// A partial swap (single row or just the results table) leaves the rest
|
||||
// of the page — best-price card, price chart, "last polled" time, badge —
|
||||
// looking stale, so the run reads as a no-op. Tell htmx to do a full
|
||||
// reload so every derived view picks up the post-poll state.
|
||||
if r.Header.Get("HX-Request") != "" {
|
||||
w.Header().Set("HX-Refresh", "true")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
// The results page asks for a refreshed listing table; the items list
|
||||
// asks for a refreshed row. Both POST to this same endpoint.
|
||||
// Non-htmx fallback: redirect back to the originating page.
|
||||
target := "/items"
|
||||
if r.PostFormValue("from") == "results" {
|
||||
d, err := a.buildItemResultsData(r, fresh, 1, "found_desc")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
target = fmt.Sprintf("/items/%d/results", id)
|
||||
}
|
||||
if fresh.LastPollError != "" {
|
||||
d.RunError = "Run finished with errors: " + fresh.LastPollError
|
||||
} else {
|
||||
d.RunMsg = fmt.Sprintf("Run complete. Showing %d listing(s).", len(d.Results))
|
||||
}
|
||||
render(w, r, templates.ItemResultsTable(d))
|
||||
return
|
||||
}
|
||||
render(w, r, templates.ItemRow(*fresh, a.Auth.CSRFToken(r.Context())))
|
||||
http.Redirect(w, r, target, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (a *App) GetItemError(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -10,8 +10,13 @@ import (
|
||||
// previewKey caches the *raw* apify result set (post-decode, post-merge,
|
||||
// pre-filter). Filters like min_price and exclude_keywords are applied after
|
||||
// the cache lookup so the operator can iterate on them without burning credits.
|
||||
//
|
||||
// Condition and Region are part of the key, not post-filters: they are
|
||||
// server-side eBay Browse API filters that change the result set the API
|
||||
// returns, so a different condition/region must miss the cache.
|
||||
type previewKey struct {
|
||||
Queries, URL, Marketplace, ListingType, ActorIDs string
|
||||
Condition, Region string
|
||||
MaxResults int
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ func (a *App) GetItemResults(w http.ResponseWriter, r *http.Request) {
|
||||
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
||||
d, err := a.buildItemResultsData(r, it, page, r.URL.Query().Get("order"))
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
render(w, r, templates.ItemResults(d))
|
||||
@@ -41,7 +41,7 @@ func (a *App) buildItemResultsData(r *http.Request, it *models.Item, page int, o
|
||||
page = 1
|
||||
}
|
||||
|
||||
total, err := a.Store.CountResults(r.Context(), it.ID)
|
||||
total, err := a.Store.CountResults(r.Context(), it.ID, true)
|
||||
if err != nil {
|
||||
return templates.ItemResultsData{}, err
|
||||
}
|
||||
@@ -58,6 +58,7 @@ func (a *App) buildItemResultsData(r *http.Request, it *models.Item, page int, o
|
||||
Limit: resultsPerPage,
|
||||
Offset: (page - 1) * resultsPerPage,
|
||||
Order: order,
|
||||
ExcludeEnded: true,
|
||||
})
|
||||
if err != nil {
|
||||
return templates.ItemResultsData{}, err
|
||||
@@ -68,6 +69,10 @@ func (a *App) buildItemResultsData(r *http.Request, it *models.Item, page int, o
|
||||
return templates.ItemResultsData{}, err
|
||||
}
|
||||
|
||||
// 24h surface for the "ending soon" strip — beyond that, a static
|
||||
// "ends in 4 days" in the per-row cell carries enough signal on its own.
|
||||
endingSoon, _ := a.Store.NextEndingResult(r.Context(), it.ID, 24*time.Hour)
|
||||
|
||||
return templates.ItemResultsData{
|
||||
Page: a.page(r, it.Name, "items"),
|
||||
Item: *it,
|
||||
@@ -78,6 +83,7 @@ func (a *App) buildItemResultsData(r *http.Request, it *models.Item, page int, o
|
||||
TotalPages: totalPages,
|
||||
Order: order,
|
||||
HistoryChartJSON: buildChartJSON(history),
|
||||
EndingSoon: endingSoon,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -101,7 +107,7 @@ func (a *App) GetGlobalResults(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
items, err := a.Store.ListItems(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
names := make(map[int64]string, len(items))
|
||||
@@ -112,9 +118,10 @@ func (a *App) GetGlobalResults(w http.ResponseWriter, r *http.Request) {
|
||||
results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{
|
||||
ItemID: itemID,
|
||||
Limit: 200,
|
||||
ExcludeEnded: true,
|
||||
})
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -138,6 +145,8 @@ func (a *App) GetGlobalResults(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
endingSoon, _ := a.Store.NextEndingResult(r.Context(), itemID, 24*time.Hour)
|
||||
|
||||
render(w, r, templates.GlobalResults(templates.GlobalResultsData{
|
||||
Page: a.page(r, "Results", "results"),
|
||||
Items: items,
|
||||
@@ -145,5 +154,6 @@ func (a *App) GetGlobalResults(w http.ResponseWriter, r *http.Request) {
|
||||
ItemID: itemID,
|
||||
From: from,
|
||||
To: to,
|
||||
EndingSoon: endingSoon,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -26,6 +26,40 @@ var settingsKeys = []string{
|
||||
"match_confidence_threshold",
|
||||
}
|
||||
|
||||
// secretSettingsKeys are credential fields. Their values are never rendered
|
||||
// back into the form, so a blank submission means "leave unchanged" rather
|
||||
// than "clear" — see PostSettings.
|
||||
var secretSettingsKeys = map[string]bool{
|
||||
"apify_api_key": true,
|
||||
"ebay_client_id": true,
|
||||
"ebay_client_secret": true,
|
||||
"ntfy_token": true,
|
||||
}
|
||||
|
||||
// credentialStatus reports, per secret key, whether a value is saved in the
|
||||
// settings table, inherited from config.toml, or absent — without exposing
|
||||
// the secret itself.
|
||||
func (a *App) credentialStatus(values map[string]string) map[string]string {
|
||||
configVals := map[string]string{
|
||||
"apify_api_key": a.Cfg.Apify.APIKey,
|
||||
"ebay_client_id": a.Cfg.Ebay.ClientID,
|
||||
"ebay_client_secret": a.Cfg.Ebay.ClientSecret,
|
||||
"ntfy_token": "",
|
||||
}
|
||||
status := make(map[string]string, len(secretSettingsKeys))
|
||||
for k := range secretSettingsKeys {
|
||||
switch {
|
||||
case strings.TrimSpace(values[k]) != "":
|
||||
status[k] = "Saved in settings"
|
||||
case strings.TrimSpace(configVals[k]) != "":
|
||||
status[k] = "Set in config.toml"
|
||||
default:
|
||||
status[k] = "Not set"
|
||||
}
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) {
|
||||
values, err := a.Store.GetAllSettings(r.Context())
|
||||
if err != nil {
|
||||
@@ -40,6 +74,7 @@ func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) {
|
||||
return templates.SettingsData{
|
||||
Page: a.page(r, "Settings", "settings"),
|
||||
Values: values,
|
||||
CredentialStatus: a.credentialStatus(values),
|
||||
IsAdmin: cur != nil && cur.Role == models.RoleAdmin,
|
||||
Users: users,
|
||||
EbayUsedToday: ebayUsed,
|
||||
@@ -50,7 +85,7 @@ func (a *App) settingsData(r *http.Request) (templates.SettingsData, error) {
|
||||
func (a *App) GetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
d, err := a.settingsData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
render(w, r, templates.Settings(d))
|
||||
@@ -68,8 +103,14 @@ func (a *App) PostSettings(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
for _, k := range settingsKeys {
|
||||
v := strings.TrimSpace(r.PostFormValue(k))
|
||||
// Secret fields are never rendered back into the form, so a blank
|
||||
// submission is the normal state and means "leave unchanged" — not
|
||||
// "clear". (To clear a stored credential, edit the settings table.)
|
||||
if v == "" && secretSettingsKeys[k] {
|
||||
continue
|
||||
}
|
||||
if err := a.Store.SetSetting(r.Context(), k, v); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -92,7 +133,7 @@ func (a *App) PostPasswordChange(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
d, err := a.settingsData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -115,7 +156,7 @@ func (a *App) PostPasswordChange(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if err := a.Store.UpdateUserPassword(r.Context(), cur.ID, hash); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
d.PasswordMsg = "Password updated"
|
||||
@@ -130,7 +171,7 @@ func (a *App) PostTestNtfy(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
d, err := a.settingsData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
baseURL := strings.TrimSpace(d.Values["ntfy_base_url"])
|
||||
@@ -164,7 +205,7 @@ func (a *App) PostTestApify(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
d, err := a.settingsData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
apiKey := strings.TrimSpace(d.Values["apify_api_key"])
|
||||
@@ -210,7 +251,7 @@ func (a *App) PostTestEbay(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
d, err := a.settingsData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
// Settings-table values win over config.toml. Both paths are trimmed:
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func (a *App) renderSettingsWithUserMsg(w http.ResponseWriter, r *http.Request, msg, errMsg string) {
|
||||
d, err := a.settingsData(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
a.serverError(w, r, err)
|
||||
return
|
||||
}
|
||||
d.UserMsg = msg
|
||||
|
||||
@@ -35,6 +35,12 @@ type Item struct {
|
||||
ExcludeKeywords string
|
||||
Marketplaces []string
|
||||
ListingType string
|
||||
// Condition and Region are eBay-only search filters. Condition is
|
||||
// Veola's vocabulary ("new", "used", "refurbished", "parts"); Region is
|
||||
// an ISO 3166-1 alpha-2 country code constraining item location. Both
|
||||
// empty means no filter, and both are ignored for non-eBay marketplaces.
|
||||
Condition string
|
||||
Region string
|
||||
ActorActive string
|
||||
ActorSold string
|
||||
ActorPriceCompare string
|
||||
@@ -43,6 +49,7 @@ type Item struct {
|
||||
LastPolledAt *time.Time
|
||||
LastPollError string
|
||||
BestPrice *float64
|
||||
BestPriceCurrency string
|
||||
BestPriceStore string
|
||||
BestPriceURL string
|
||||
BestPriceImageURL string
|
||||
@@ -63,6 +70,10 @@ type Result struct {
|
||||
MatchedQuery string
|
||||
Alerted bool
|
||||
FoundAt time.Time
|
||||
// EndsAt is populated for auction-format listings (eBay auctions,
|
||||
// Yahoo Auctions JP). Nil for fixed-price listings and for any source
|
||||
// that doesn't report an end time.
|
||||
EndsAt *time.Time
|
||||
}
|
||||
|
||||
// SearchQueries returns the item's alias list. Splits on newline, comma, and
|
||||
|
||||
@@ -220,6 +220,14 @@ func (s *Scheduler) RunPoll(ctx context.Context, it models.Item) {
|
||||
continue
|
||||
}
|
||||
if exists {
|
||||
// Row already stored — but if this poll surfaced an end time we
|
||||
// didn't have before (or the row predates the ends_at column),
|
||||
// backfill it so countdowns light up for known auctions.
|
||||
if r.EndsAt != nil {
|
||||
if err := s.store.BackfillResultEndsAt(ctx, it.ID, r.URL, *r.EndsAt); err != nil {
|
||||
slog.Error("backfill ends_at failed", "err", err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
alerted := false
|
||||
@@ -242,6 +250,7 @@ func (s *Scheduler) RunPoll(ctx context.Context, it models.Item) {
|
||||
ImageURL: r.ImageURL,
|
||||
MatchedQuery: r.MatchedQuery,
|
||||
Alerted: alerted,
|
||||
EndsAt: r.EndsAt,
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("insert result failed", "err", err)
|
||||
@@ -257,6 +266,7 @@ func (s *Scheduler) RunPoll(ctx context.Context, it models.Item) {
|
||||
bp := best.Price
|
||||
_ = s.store.UpdateItemPollResult(ctx, it.ID, &models.Item{
|
||||
BestPrice: &bp,
|
||||
BestPriceCurrency: best.Currency,
|
||||
BestPriceStore: best.Store,
|
||||
BestPriceURL: best.URL,
|
||||
BestPriceImageURL: best.ImageURL,
|
||||
@@ -365,6 +375,7 @@ func (s *Scheduler) ExecutePlan(ctx context.Context, p actorPlan) ([]apify.Unifi
|
||||
Store: l.Store,
|
||||
ImageURL: l.ImageURL,
|
||||
Source: apify.SourceActiveEbay,
|
||||
EndsAt: l.EndsAt,
|
||||
})
|
||||
}
|
||||
default:
|
||||
@@ -549,6 +560,8 @@ func (s *Scheduler) buildInputsForQuery(it models.Item, query string, markets []
|
||||
MarketplaceID: ebay.MarketplaceID(mk),
|
||||
Query: query,
|
||||
ListingType: it.ListingType,
|
||||
Condition: it.Condition,
|
||||
Region: it.Region,
|
||||
Limit: 30,
|
||||
},
|
||||
})
|
||||
|
||||
14
main.go
14
main.go
@@ -27,8 +27,19 @@ import (
|
||||
|
||||
func main() {
|
||||
configPath := flag.String("config", "config.toml", "path to config TOML file")
|
||||
debug := flag.Bool("debug", false, "enable debug-level logging (verbose; raw external payloads logged)")
|
||||
flag.Parse()
|
||||
|
||||
level := slog.LevelInfo
|
||||
if *debug {
|
||||
level = slog.LevelDebug
|
||||
}
|
||||
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: level})))
|
||||
|
||||
if *debug {
|
||||
slog.Debug("debug logging enabled")
|
||||
}
|
||||
|
||||
if err := run(*configPath); err != nil {
|
||||
slog.Error("fatal", "err", err)
|
||||
os.Exit(1)
|
||||
@@ -74,6 +85,9 @@ func run(configPath string) error {
|
||||
Addr: addr,
|
||||
Handler: app.Routes(),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 60 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
@@ -26,16 +26,54 @@ a { color: var(--accent); }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
.v-card {
|
||||
background: var(--surface);
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(36, 58, 147, 0.82), rgba(31, 51, 128, 0.82));
|
||||
backdrop-filter: blur(10px) saturate(140%);
|
||||
-webkit-backdrop-filter: blur(10px) saturate(140%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--shadow);
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
box-shadow 180ms ease,
|
||||
border-color 180ms ease;
|
||||
}
|
||||
.v-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(135deg,
|
||||
rgba(0, 164, 228, 0.65) 0%,
|
||||
rgba(245, 196, 0, 0.30) 45%,
|
||||
rgba(255, 255, 255, 0.04) 100%);
|
||||
-webkit-mask:
|
||||
linear-gradient(#000 0 0) content-box,
|
||||
linear-gradient(#000 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
.v-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 10px 28px rgba(0, 0, 80, 0.55),
|
||||
0 0 0 1px rgba(0, 164, 228, 0.30);
|
||||
}
|
||||
|
||||
.v-card-flat {
|
||||
background: var(--surface);
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(36, 58, 147, 0.70), rgba(31, 51, 128, 0.70));
|
||||
backdrop-filter: blur(8px) saturate(130%);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(130%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.v-divider { border-top: 1px solid var(--border); }
|
||||
@@ -173,7 +211,9 @@ table.v-table td {
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
table.v-table tr:hover td { background: rgba(255,255,255,0.03); }
|
||||
table.v-table td { transition: background 140ms ease; }
|
||||
table.v-table tr { transition: transform 140ms ease; }
|
||||
table.v-table tbody tr:hover td { background: rgba(0, 164, 228, 0.08); }
|
||||
|
||||
.v-error-text { color: var(--danger); font-size: 0.85rem; }
|
||||
.v-muted { color: var(--text-2); }
|
||||
@@ -210,3 +250,64 @@ table.v-table tr:hover td { background: rgba(255,255,255,0.03); }
|
||||
.htmx-indicator { display: none; }
|
||||
.htmx-request .htmx-indicator,
|
||||
.htmx-request.htmx-indicator { display: inline-flex; }
|
||||
|
||||
/* flair.js adds .v-just-swapped to any htmx swap target for ~400ms, giving
|
||||
refreshed regions a soft fade-in instead of an abrupt content jump. */
|
||||
@keyframes v-fade-in-up {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.v-just-swapped { animation: v-fade-in-up 240ms ease-out; }
|
||||
|
||||
/* Ending-soon strip: one global "next auction to close" banner. flair.js
|
||||
keeps the countdown live. Subtle by default; pulses red when inside the
|
||||
last 5 minutes via .v-countdown-critical on the inner counter. */
|
||||
.v-ending-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.7rem 1rem;
|
||||
border-radius: 8px;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(245, 196, 0, 0.12), rgba(0, 164, 228, 0.08));
|
||||
border: 1px solid rgba(245, 196, 0, 0.35);
|
||||
}
|
||||
.v-ending-label {
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.v-ending-title { flex: 1; min-width: 0; }
|
||||
.v-ending-countdown {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
/* Per-row + strip countdown urgency states. Default reads as neutral muted
|
||||
text so 6-day countdowns don't shout; urgency tints kick in only when
|
||||
flair.js flips the class. */
|
||||
.v-countdown { color: var(--text-2); }
|
||||
.v-countdown-urgent { color: var(--yellow); font-weight: 600; }
|
||||
.v-countdown-critical {
|
||||
color: var(--danger);
|
||||
font-weight: 700;
|
||||
animation: v-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
.v-countdown-ended { color: var(--text-2); font-style: italic; }
|
||||
|
||||
@keyframes v-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.55; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.v-card { transition: none; }
|
||||
.v-card:hover { transform: none; }
|
||||
table.v-table td, table.v-table tr { transition: none; }
|
||||
.v-just-swapped { animation: none; }
|
||||
.v-countdown-critical { animation: none; }
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
130
static/js/flair.js
Normal file
130
static/js/flair.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// Lightweight visual flourishes:
|
||||
// - count-up animation on [data-countup] elements at page load
|
||||
// - fade-in on htmx swap targets via a transient .v-just-swapped class
|
||||
// Respects prefers-reduced-motion by no-oping both effects.
|
||||
|
||||
(function () {
|
||||
const prefersReduced =
|
||||
window.matchMedia &&
|
||||
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
|
||||
function easeOutCubic(t) {
|
||||
return 1 - Math.pow(1 - t, 3);
|
||||
}
|
||||
|
||||
function animateCount(el) {
|
||||
if (prefersReduced) return;
|
||||
const raw = el.textContent.trim();
|
||||
const m = raw.match(/^(\$|£|€|¥)?(-?[\d,]+(?:\.\d+)?)$/);
|
||||
if (!m) return;
|
||||
const prefix = m[1] || "";
|
||||
const numeric = m[2].replace(/,/g, "");
|
||||
const target = parseFloat(numeric);
|
||||
if (!isFinite(target)) return;
|
||||
const decimals = numeric.includes(".") ? numeric.split(".")[1].length : 0;
|
||||
const format = (v) =>
|
||||
prefix + (decimals > 0 ? v.toFixed(decimals) : Math.floor(v).toString());
|
||||
const duration = 650;
|
||||
const start = performance.now();
|
||||
el.textContent = format(0);
|
||||
function tick(now) {
|
||||
const t = Math.min(1, (now - start) / duration);
|
||||
const v = target * easeOutCubic(t);
|
||||
el.textContent = format(v);
|
||||
if (t < 1) {
|
||||
requestAnimationFrame(tick);
|
||||
} else {
|
||||
el.textContent = format(target);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
function runCountUps() {
|
||||
document.querySelectorAll("[data-countup]").forEach(animateCount);
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", runCountUps);
|
||||
} else {
|
||||
runCountUps();
|
||||
}
|
||||
|
||||
document.addEventListener("htmx:afterSwap", function (evt) {
|
||||
if (prefersReduced) return;
|
||||
const target = evt.detail && evt.detail.target;
|
||||
if (!target || !target.classList) return;
|
||||
target.classList.add("v-just-swapped");
|
||||
setTimeout(() => target.classList.remove("v-just-swapped"), 400);
|
||||
});
|
||||
|
||||
// Auction countdowns. Each [data-countdown] reads its sibling/ancestor's
|
||||
// data-ends-at ISO timestamp. Above 1h remaining we update once a minute
|
||||
// (text barely changes); inside the last hour we tick every second and tint
|
||||
// urgent / critical so the eye lands on it. Past the end we render "ended"
|
||||
// and stop ticking that node.
|
||||
function formatRemaining(ms) {
|
||||
if (ms <= 0) return { text: "ended", urgent: false, critical: false };
|
||||
const s = Math.floor(ms / 1000);
|
||||
const days = Math.floor(s / 86400);
|
||||
const hours = Math.floor((s % 86400) / 3600);
|
||||
const minutes = Math.floor((s % 3600) / 60);
|
||||
const seconds = s % 60;
|
||||
const urgent = ms < 60 * 60 * 1000;
|
||||
const critical = ms < 5 * 60 * 1000;
|
||||
let text;
|
||||
if (days > 0) text = days + "d " + hours + "h";
|
||||
else if (hours > 0) text = hours + "h " + minutes + "m";
|
||||
else if (minutes > 0) text = minutes + "m " + String(seconds).padStart(2, "0") + "s";
|
||||
else text = seconds + "s";
|
||||
return { text, urgent, critical };
|
||||
}
|
||||
|
||||
function resolveEndsAt(el) {
|
||||
let cur = el;
|
||||
while (cur && cur.dataset && !cur.dataset.endsAt) cur = cur.parentElement;
|
||||
return cur && cur.dataset ? cur.dataset.endsAt : null;
|
||||
}
|
||||
|
||||
function tickCountdown(el, endsAtMs) {
|
||||
const remaining = endsAtMs - Date.now();
|
||||
const f = formatRemaining(remaining);
|
||||
el.textContent = f.text;
|
||||
el.classList.toggle("v-countdown-urgent", f.urgent && !f.critical);
|
||||
el.classList.toggle("v-countdown-critical", f.critical && remaining > 0);
|
||||
el.classList.toggle("v-countdown-ended", remaining <= 0);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
function startCountdowns() {
|
||||
document.querySelectorAll("[data-countdown]").forEach(function (el) {
|
||||
const iso = resolveEndsAt(el);
|
||||
if (!iso) return;
|
||||
const endsAtMs = Date.parse(iso);
|
||||
if (!isFinite(endsAtMs)) return;
|
||||
|
||||
let timer = null;
|
||||
function loop() {
|
||||
const remaining = tickCountdown(el, endsAtMs);
|
||||
if (remaining <= 0) {
|
||||
if (timer) clearTimeout(timer);
|
||||
return;
|
||||
}
|
||||
// Tick every second below an hour, every 30 seconds otherwise. The
|
||||
// long-interval path keeps "3d 4h" from causing constant repaints.
|
||||
const interval = remaining < 60 * 60 * 1000 ? 1000 : 30000;
|
||||
timer = setTimeout(loop, interval);
|
||||
}
|
||||
loop();
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", startCountdowns);
|
||||
} else {
|
||||
startCountdowns();
|
||||
}
|
||||
|
||||
// After an htmx swap, freshly-inserted countdowns need to be started too.
|
||||
document.addEventListener("htmx:afterSwap", startCountdowns);
|
||||
})();
|
||||
@@ -51,7 +51,7 @@ templ DashboardBody(d DashboardData) {
|
||||
<div class="grid md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="v-card p-5">
|
||||
<div class="v-muted text-sm uppercase tracking-wide">Potential Spend</div>
|
||||
<div class="font-mono text-4xl mt-2">{ fmt.Sprintf("$%.2f", d.Stats.PotentialSpend) }</div>
|
||||
<div class="font-mono text-4xl mt-2" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.PotentialSpend) }</div>
|
||||
<div class="v-muted text-sm mt-1">across { fmt.Sprintf("%d", d.Stats.PricedItemCount) } items</div>
|
||||
if d.Stats.UnpricedCount > 0 {
|
||||
<div class="v-muted text-xs mt-1">{ fmt.Sprintf("%d items not yet priced.", d.Stats.UnpricedCount) }</div>
|
||||
@@ -59,7 +59,7 @@ templ DashboardBody(d DashboardData) {
|
||||
</div>
|
||||
<div class="v-card p-5">
|
||||
<div class="v-muted text-sm uppercase tracking-wide">Money Saved</div>
|
||||
<div class="font-mono text-4xl mt-2 v-price-deal">{ fmt.Sprintf("$%.2f", d.Stats.MoneySaved) }</div>
|
||||
<div class="font-mono text-4xl mt-2 v-price-deal" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.MoneySaved) }</div>
|
||||
<div class="v-muted text-sm mt-1">across { fmt.Sprintf("%d", d.Stats.SavedItemCount) } items</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -108,7 +108,7 @@ templ DashboardBody(d DashboardData) {
|
||||
templ statCard(label, value, sub string) {
|
||||
<div class="v-card p-4">
|
||||
<div class="v-muted text-xs uppercase tracking-wide">{ label }</div>
|
||||
<div class="font-mono text-3xl mt-1">{ value }</div>
|
||||
<div class="font-mono text-3xl mt-1" data-countup>{ value }</div>
|
||||
if sub != "" {
|
||||
<div class="v-muted text-xs mt-1">{ sub }</div>
|
||||
}
|
||||
|
||||
@@ -88,14 +88,14 @@ func DashboardBody(d DashboardData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"grid md:grid-cols-2 gap-4 mb-6\"><div class=\"v-card p-5\"><div class=\"v-muted text-sm uppercase tracking-wide\">Potential Spend</div><div class=\"font-mono text-4xl mt-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"grid md:grid-cols-2 gap-4 mb-6\"><div class=\"v-card p-5\"><div class=\"v-muted text-sm uppercase tracking-wide\">Potential Spend</div><div class=\"font-mono text-4xl mt-2\" data-countup>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("$%.2f", d.Stats.PotentialSpend))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 54, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 54, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -137,14 +137,14 @@ func DashboardBody(d DashboardData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div class=\"v-card p-5\"><div class=\"v-muted text-sm uppercase tracking-wide\">Money Saved</div><div class=\"font-mono text-4xl mt-2 v-price-deal\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div class=\"v-card p-5\"><div class=\"v-muted text-sm uppercase tracking-wide\">Money Saved</div><div class=\"font-mono text-4xl mt-2 v-price-deal\" data-countup>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("$%.2f", d.Stats.MoneySaved))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 62, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 62, Col: 109}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -346,14 +346,14 @@ func statCard(label, value, sub string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div><div class=\"font-mono text-3xl mt-1\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div><div class=\"font-mono text-3xl mt-1\" data-countup>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 111, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 111, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -16,6 +16,43 @@ type ItemFormData struct {
|
||||
|
||||
func itemSelected(have, want string) bool { return have == want }
|
||||
|
||||
type selectOpt struct {
|
||||
Value string
|
||||
Label string
|
||||
}
|
||||
|
||||
// conditionOptions are the eBay-only item-condition filters. Values match the
|
||||
// vocabulary mapped to Browse API condition IDs in internal/ebay.
|
||||
func conditionOptions() []selectOpt {
|
||||
return []selectOpt{
|
||||
{"", "— any —"},
|
||||
{"new", "New"},
|
||||
{"used", "Used"},
|
||||
{"refurbished", "Refurbished"},
|
||||
{"parts", "For parts / not working"},
|
||||
}
|
||||
}
|
||||
|
||||
// regionOptions are the eBay-only item-location filters, keyed by ISO 3166-1
|
||||
// alpha-2 country code (the value the Browse API itemLocationCountry filter
|
||||
// expects).
|
||||
func regionOptions() []selectOpt {
|
||||
return []selectOpt{
|
||||
{"", "— any —"},
|
||||
{"US", "United States"},
|
||||
{"GB", "United Kingdom"},
|
||||
{"DE", "Germany"},
|
||||
{"FR", "France"},
|
||||
{"IT", "Italy"},
|
||||
{"ES", "Spain"},
|
||||
{"CA", "Canada"},
|
||||
{"AU", "Australia"},
|
||||
{"JP", "Japan"},
|
||||
{"CN", "China"},
|
||||
{"HK", "Hong Kong"},
|
||||
}
|
||||
}
|
||||
|
||||
type marketplaceOpt struct {
|
||||
Value string
|
||||
Label string
|
||||
@@ -226,6 +263,26 @@ templ itemFormInner(d ItemFormData) {
|
||||
<div class="v-muted text-xs mt-1">Drop results whose title contains any of these. Case-insensitive substring match.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="v-label">Condition</label>
|
||||
<select class="v-select" name="condition">
|
||||
for _, o := range conditionOptions() {
|
||||
<option value={ o.Value } selected?={ itemSelected(d.Item.Condition, o.Value) }>{ o.Label }</option>
|
||||
}
|
||||
</select>
|
||||
<div class="v-muted text-xs mt-1">eBay marketplaces only. Ignored for Yahoo JP, Mercari, and custom actors.</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="v-label">Item Location</label>
|
||||
<select class="v-select" name="region">
|
||||
for _, o := range regionOptions() {
|
||||
<option value={ o.Value } selected?={ itemSelected(d.Item.Region, o.Value) }>{ o.Label }</option>
|
||||
}
|
||||
</select>
|
||||
<div class="v-muted text-xs mt-1">Restrict to items located in this country. eBay marketplaces only.</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="include_out_of_stock" checked?={ d.Item.IncludeOutOfStock } value="1"/>
|
||||
<span>Include out-of-stock results</span>
|
||||
|
||||
@@ -24,6 +24,43 @@ type ItemFormData struct {
|
||||
|
||||
func itemSelected(have, want string) bool { return have == want }
|
||||
|
||||
type selectOpt struct {
|
||||
Value string
|
||||
Label string
|
||||
}
|
||||
|
||||
// conditionOptions are the eBay-only item-condition filters. Values match the
|
||||
// vocabulary mapped to Browse API condition IDs in internal/ebay.
|
||||
func conditionOptions() []selectOpt {
|
||||
return []selectOpt{
|
||||
{"", "— any —"},
|
||||
{"new", "New"},
|
||||
{"used", "Used"},
|
||||
{"refurbished", "Refurbished"},
|
||||
{"parts", "For parts / not working"},
|
||||
}
|
||||
}
|
||||
|
||||
// regionOptions are the eBay-only item-location filters, keyed by ISO 3166-1
|
||||
// alpha-2 country code (the value the Browse API itemLocationCountry filter
|
||||
// expects).
|
||||
func regionOptions() []selectOpt {
|
||||
return []selectOpt{
|
||||
{"", "— any —"},
|
||||
{"US", "United States"},
|
||||
{"GB", "United Kingdom"},
|
||||
{"DE", "Germany"},
|
||||
{"FR", "France"},
|
||||
{"IT", "Italy"},
|
||||
{"ES", "Spain"},
|
||||
{"CA", "Canada"},
|
||||
{"AU", "Australia"},
|
||||
{"JP", "Japan"},
|
||||
{"CN", "China"},
|
||||
{"HK", "Hong Kong"},
|
||||
}
|
||||
}
|
||||
|
||||
type marketplaceOpt struct {
|
||||
Value string
|
||||
Label string
|
||||
@@ -138,7 +175,7 @@ func itemFormBody(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(d.Item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 104, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 141, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -167,7 +204,7 @@ func itemFormBody(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(e)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 113, Col: 13}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 150, Col: 13}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -223,7 +260,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(formAction(d))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 125, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 162, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -254,7 +291,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 139, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 176, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -282,7 +319,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(c)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 146, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 183, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -305,7 +342,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(c)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 146, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 183, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -323,7 +360,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(newCategory(d.Item.Category, d.Categories))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 149, Col: 110}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 186, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -336,7 +373,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(d.Item.SearchQuery)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 154, Col: 150}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 191, Col: 150}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -349,7 +386,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.URL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 159, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 196, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -362,7 +399,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(optFloat(d.Item.TargetPrice))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 165, Col: 119}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 202, Col: 119}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -375,7 +412,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.NtfyTopic)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 170, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 207, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -393,7 +430,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(p)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 176, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 213, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -416,7 +453,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(p)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 176, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 213, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -439,7 +476,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", opt.Minutes))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 186, Col: 52}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 223, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -462,7 +499,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 186, Col: 122}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 223, Col: 122}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -485,7 +522,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(m.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 195, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 232, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -508,7 +545,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(m.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 196, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 233, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -526,7 +563,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(customMarketplacesCSV(d.Item))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 200, Col: 103}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 237, Col: 103}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -554,7 +591,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(lt)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 212, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 249, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -577,7 +614,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(lt)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 212, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 249, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -595,7 +632,7 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(optFloat(d.Item.MinPrice))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 220, Col: 113}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 257, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -608,92 +645,184 @@ func itemFormInner(d ItemFormData) templ.Component {
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(d.Item.ExcludeKeywords)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 225, Col: 137}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 262, Col: 137}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</textarea><div class=\"v-muted text-xs mt-1\">Drop results whose title contains any of these. Case-insensitive substring match.</div></div></div><label class=\"flex items-center gap-2\"><input type=\"checkbox\" name=\"include_out_of_stock\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</textarea><div class=\"v-muted text-xs mt-1\">Drop results whose title contains any of these. Case-insensitive substring match.</div></div></div><div class=\"grid md:grid-cols-2 gap-4\"><div><label class=\"v-label\">Condition</label> <select class=\"v-select\" name=\"condition\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.Item.IncludeOutOfStock {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, " value=\"1\"> <span>Include out-of-stock results</span></label> <details class=\"v-card-flat p-4\"><summary class=\"cursor-pointer font-semibold\">Advanced</summary><div class=\"v-muted text-sm mt-2\">Leave blank to use the configured default for the selected marketplace.</div><div class=\"grid md:grid-cols-2 gap-4 mt-3\"><div><label class=\"v-label\">Active Listings Actor</label> <input class=\"v-input\" name=\"actor_active\" value=\"")
|
||||
for _, o := range conditionOptions() {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.ActorActive)
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(o.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 239, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 271, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "\" placeholder=\"from config\"></div><div><label class=\"v-label\">Sold Listings Actor</label> <input class=\"v-input\" name=\"actor_sold\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if itemSelected(d.Item.Condition, o.Value) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.ActorSold)
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(o.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 243, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 271, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "\" placeholder=\"from config\"></div><div><label class=\"v-label\">Price Comparison Actor</label> <input class=\"v-input\" name=\"actor_price_compare\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "</select><div class=\"v-muted text-xs mt-1\">eBay marketplaces only. Ignored for Yahoo JP, Mercari, and custom actors.</div></div><div><label class=\"v-label\">Item Location</label> <select class=\"v-select\" name=\"region\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, o := range regionOptions() {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<option value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.ActorPriceCompare)
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(o.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 247, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 280, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" placeholder=\"from config\"></div><label class=\"flex items-center gap-2 mt-6\"><input type=\"checkbox\" name=\"use_price_comparison\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if itemSelected(d.Item.Region, o.Value) {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, ">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(o.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 280, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "</option>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "</select><div class=\"v-muted text-xs mt-1\">Restrict to items located in this country. eBay marketplaces only.</div></div></div><label class=\"flex items-center gap-2\"><input type=\"checkbox\" name=\"include_out_of_stock\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.Item.IncludeOutOfStock {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, " value=\"1\"> <span>Include out-of-stock results</span></label> <details class=\"v-card-flat p-4\"><summary class=\"cursor-pointer font-semibold\">Advanced</summary><div class=\"v-muted text-sm mt-2\">Leave blank to use the configured default for the selected marketplace.</div><div class=\"grid md:grid-cols-2 gap-4 mt-3\"><div><label class=\"v-label\">Active Listings Actor</label> <input class=\"v-input\" name=\"actor_active\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.ActorActive)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 296, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "\" placeholder=\"from config\"></div><div><label class=\"v-label\">Sold Listings Actor</label> <input class=\"v-input\" name=\"actor_sold\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.ActorSold)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 300, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "\" placeholder=\"from config\"></div><div><label class=\"v-label\">Price Comparison Actor</label> <input class=\"v-input\" name=\"actor_price_compare\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Item.ActorPriceCompare)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_form.templ`, Line: 304, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "\" placeholder=\"from config\"></div><label class=\"flex items-center gap-2 mt-6\"><input type=\"checkbox\" name=\"use_price_comparison\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.Item.UsePriceComparison {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, " checked")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " value=\"1\"> <span>Use price comparison actor in addition to active listings</span></label></div></details><div class=\"flex items-center gap-3 pt-2\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, " value=\"1\"> <span>Use price comparison actor in addition to active listings</span></label></div></details><div class=\"flex items-center gap-3 pt-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.IsEdit {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<button class=\"v-btn\" type=\"submit\">Save</button> <a class=\"v-btn-ghost\" href=\"/items\">Cancel</a>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<button class=\"v-btn\" type=\"submit\">Save</button> <a class=\"v-btn-ghost\" href=\"/items\">Cancel</a>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "<button class=\"v-btn\" type=\"submit\">Preview</button> <a class=\"v-btn-ghost\" href=\"/items\">Cancel</a> <span id=\"preview-loading\" class=\"htmx-indicator v-muted text-sm flex items-center gap-2\"><span class=\"v-spinner\"></span> Running preview… apify runs can take 30–60s.</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "<button class=\"v-btn\" type=\"submit\">Preview</button> <a class=\"v-btn-ghost\" href=\"/items\">Cancel</a> <span id=\"preview-loading\" class=\"htmx-indicator v-muted text-sm flex items-center gap-2\"><span class=\"v-spinner\"></span> Running preview… apify runs can take 30–60s.</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "</div></form>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</div></form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !d.IsEdit {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "<div id=\"preview-target\" class=\"mt-6\"></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "<div id=\"preview-target\" class=\"mt-6\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -744,9 +873,9 @@ func ItemForm(d ItemFormData) templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var28 == nil {
|
||||
templ_7745c5c3_Var28 = templ.NopComponent
|
||||
templ_7745c5c3_Var32 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var32 == nil {
|
||||
templ_7745c5c3_Var32 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = Layout(d.Page, itemFormBody(d)).Render(ctx, templ_7745c5c3_Buffer)
|
||||
|
||||
@@ -35,6 +35,8 @@ type FormValues struct {
|
||||
IncludeOutOfStock bool
|
||||
Marketplaces []string
|
||||
ListingType string
|
||||
Condition string
|
||||
Region string
|
||||
ActorActive string
|
||||
ActorSold string
|
||||
ActorPriceCompare string
|
||||
@@ -136,6 +138,8 @@ templ confirmForm(d PreviewData) {
|
||||
@hidden("marketplace", m)
|
||||
}
|
||||
@hidden("listing_type", d.Form.ListingType)
|
||||
@hidden("condition", d.Form.Condition)
|
||||
@hidden("region", d.Form.Region)
|
||||
@hidden("actor_active", d.Form.ActorActive)
|
||||
@hidden("actor_sold", d.Form.ActorSold)
|
||||
@hidden("actor_price_compare", d.Form.ActorPriceCompare)
|
||||
|
||||
@@ -43,6 +43,8 @@ type FormValues struct {
|
||||
IncludeOutOfStock bool
|
||||
Marketplaces []string
|
||||
ListingType string
|
||||
Condition string
|
||||
Region string
|
||||
ActorActive string
|
||||
ActorSold string
|
||||
ActorPriceCompare string
|
||||
@@ -78,7 +80,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(d.Error)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 48, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 50, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -101,7 +103,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Found %d results for '%s'", len(d.Results), d.Form.SearchQuery))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 60, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 62, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -144,7 +146,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(r.ImageURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 75, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 77, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -162,7 +164,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(r.URL))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 78, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 80, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -175,7 +177,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 78, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 80, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -188,7 +190,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(r.Store)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 79, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 81, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -201,7 +203,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmtNumber(r.Price, r.Currency))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 81, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 83, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -225,7 +227,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("and %d more", len(d.Results)-6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 87, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 89, Col: 86}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -248,7 +250,7 @@ func ItemPreview(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Prices range from %s to %s across %d stores", fmtNumber(d.MinPrice, d.Currency), fmtNumber(d.MaxPrice, d.Currency), d.StoreCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 93, Col: 148}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 95, Col: 148}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -301,7 +303,7 @@ func previewBest(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Results[d.BestIndex].ImageURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 104, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 106, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -324,7 +326,7 @@ func previewBest(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmtNumber(d.Results[d.BestIndex].Price, d.Currency))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 110, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 112, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -337,7 +339,7 @@ func previewBest(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var14 templ.SafeURL
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(d.Results[d.BestIndex].URL))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 111, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 113, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -350,7 +352,7 @@ func previewBest(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(d.Results[d.BestIndex].Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 111, Col: 141}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 113, Col: 141}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -363,7 +365,7 @@ func previewBest(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(d.Results[d.BestIndex].Store)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 112, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 114, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -381,7 +383,7 @@ func previewBest(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(d.Results[d.BestIndex].MatchedQuery)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 114, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 116, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -429,7 +431,7 @@ func confirmForm(d PreviewData) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 123, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 125, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -493,6 +495,14 @@ func confirmForm(d PreviewData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = hidden("condition", d.Form.Condition).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = hidden("region", d.Form.Region).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = hidden("actor_active", d.Form.ActorActive).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
@@ -555,7 +565,7 @@ func hidden(name, value string) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 149, Col: 33}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 153, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -568,7 +578,7 @@ func hidden(name, value string) templ.Component {
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 149, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 153, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -611,7 +621,7 @@ func hiddenBool(name string, value bool) templ.Component {
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 154, Col: 34}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/item_preview.templ`, Line: 158, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -89,7 +89,7 @@ templ itemRow(it models.Item, csrf string) {
|
||||
</td>
|
||||
<td>
|
||||
if it.BestPrice != nil {
|
||||
<div class={ "font-mono text-lg", priceClass(it.BestPrice, it.TargetPrice) }>{ fmtPrice(it.BestPrice, "USD") }</div>
|
||||
<div class={ "font-mono text-lg", priceClass(it.BestPrice, it.TargetPrice) }>{ fmtPrice(it.BestPrice, it.BestPriceCurrency) }</div>
|
||||
if it.BestPriceURL != "" {
|
||||
<a class="text-xs" href={ templ.SafeURL(it.BestPriceURL) } target="_blank" rel="noopener">{ it.BestPriceStore }</a>
|
||||
} else if it.BestPriceStore != "" {
|
||||
|
||||
@@ -326,9 +326,9 @@ func itemRow(it models.Item, csrf string) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(it.BestPrice, "USD"))
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(it.BestPrice, it.BestPriceCurrency))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 92, Col: 112}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 92, Col: 127}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -24,6 +24,7 @@ templ head(title string) {
|
||||
<link rel="stylesheet" href="/static/css/tailwind.css"/>
|
||||
<link rel="stylesheet" href="/static/css/app.css"/>
|
||||
<script src="/static/vendor/htmx.min.js" defer></script>
|
||||
<script src="/static/js/flair.js" defer></script>
|
||||
</head>
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ func head(title string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " · Veola</title><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"/static/css/tailwind.css\"><link rel=\"stylesheet\" href=\"/static/css/app.css\"><script src=\"/static/vendor/htmx.min.js\" defer></script></head>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " · Veola</title><link rel=\"preconnect\" href=\"https://fonts.googleapis.com\"><link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin><link href=\"https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"/static/css/tailwind.css\"><link rel=\"stylesheet\" href=\"/static/css/app.css\"><script src=\"/static/vendor/htmx.min.js\" defer></script><script src=\"/static/js/flair.js\" defer></script></head>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -236,7 +236,7 @@ func Layout(p Page, body templ.Component) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.Flash)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 61, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 62, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -255,7 +255,7 @@ func Layout(p Page, body templ.Component) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(p.FlashError)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 64, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 65, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -353,7 +353,7 @@ func CSRFInput(token string) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(token)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 85, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 86, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -3,7 +3,9 @@ package templates
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"veola/internal/db"
|
||||
"veola/internal/models"
|
||||
)
|
||||
|
||||
@@ -21,6 +23,9 @@ type ItemResultsData struct {
|
||||
// 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 {
|
||||
@@ -35,6 +40,8 @@ type GlobalResultsData struct {
|
||||
ItemID int64
|
||||
From string
|
||||
To string
|
||||
// EndingSoon mirrors the per-item field but spans every watched item.
|
||||
EndingSoon *db.EndingSoon
|
||||
}
|
||||
|
||||
type ItemResultRow struct {
|
||||
@@ -42,8 +49,40 @@ type ItemResultRow struct {
|
||||
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 {
|
||||
<div class="v-ending-strip mb-5" data-ends-at={ e.EndsAt.UTC().Format(time.RFC3339) }>
|
||||
<div class="v-ending-label">Ending soon</div>
|
||||
<div class="v-ending-title">
|
||||
if e.URL != "" {
|
||||
<a href={ templ.SafeURL(e.URL) } target="_blank" rel="noopener">{ e.Title }</a>
|
||||
} else {
|
||||
{ e.Title }
|
||||
}
|
||||
<span class="v-muted text-xs ml-2">{ e.ItemName }</span>
|
||||
</div>
|
||||
<div class="v-ending-countdown font-mono" data-countdown></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
<span class="v-countdown font-mono text-sm" data-ends-at={ endsAt.UTC().Format(time.RFC3339) } data-countdown></span>
|
||||
} else {
|
||||
<span class="v-muted">—</span>
|
||||
}
|
||||
}
|
||||
|
||||
templ itemResultsBody(d ItemResultsData) {
|
||||
<div>
|
||||
@endingSoonStrip(d.EndingSoon)
|
||||
<div class="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold">{ d.Item.Name }</h1>
|
||||
@@ -53,7 +92,7 @@ templ itemResultsBody(d ItemResultsData) {
|
||||
</div>
|
||||
<div class="text-right">
|
||||
if d.Item.BestPrice != nil {
|
||||
<div class="font-mono text-3xl">{ fmtPrice(d.Item.BestPrice, "USD") }</div>
|
||||
<div class="font-mono text-3xl">{ fmtPrice(d.Item.BestPrice, d.Item.BestPriceCurrency) }</div>
|
||||
if d.Item.BestPriceURL != "" {
|
||||
<a class="text-sm" href={ templ.SafeURL(d.Item.BestPriceURL) } target="_blank" rel="noopener">{ d.Item.BestPriceStore }</a>
|
||||
}
|
||||
@@ -120,6 +159,7 @@ templ ItemResultsTable(d ItemResultsData) {
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/items/%d/results?order=%s", d.Item.ID, toggleOrder(d.Order, "price"))) }>Price</a>
|
||||
</th>
|
||||
<th>Store</th>
|
||||
<th>Ends</th>
|
||||
<th>
|
||||
<a href={ templ.SafeURL(fmt.Sprintf("/items/%d/results?order=%s", d.Item.ID, toggleOrder(d.Order, "found"))) }>Found</a>
|
||||
</th>
|
||||
@@ -146,6 +186,7 @@ templ ItemResultsTable(d ItemResultsData) {
|
||||
</td>
|
||||
<td class={ "font-mono", priceClass(r.Price, d.Item.TargetPrice) }>{ fmtPrice(r.Price, r.Currency) }</td>
|
||||
<td class="v-muted">{ r.Source }</td>
|
||||
<td>@endsInCell(r.EndsAt)</td>
|
||||
<td class="v-muted text-sm">{ humanTime(r.FoundAt) }</td>
|
||||
<td>
|
||||
if r.Alerted {
|
||||
@@ -197,6 +238,7 @@ templ ItemResults(d ItemResultsData) {
|
||||
templ globalResultsBody(d GlobalResultsData) {
|
||||
<div>
|
||||
<h1 class="text-3xl font-semibold mb-6">All Results</h1>
|
||||
@endingSoonStrip(d.EndingSoon)
|
||||
<form method="get" action="/results" class="flex items-end gap-3 mb-4">
|
||||
<div>
|
||||
<label class="v-label">Item</label>
|
||||
@@ -220,7 +262,7 @@ templ globalResultsBody(d GlobalResultsData) {
|
||||
<div class="v-card p-0 overflow-hidden">
|
||||
<table class="v-table">
|
||||
<thead>
|
||||
<tr><th>Item</th><th>Title</th><th>Price</th><th>Store</th><th>Found</th><th>Alert</th></tr>
|
||||
<tr><th>Item</th><th>Title</th><th>Price</th><th>Store</th><th>Ends</th><th>Found</th><th>Alert</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
for _, r := range d.Results {
|
||||
@@ -238,6 +280,7 @@ templ globalResultsBody(d GlobalResultsData) {
|
||||
</td>
|
||||
<td class="font-mono">{ fmtPrice(r.Price, r.Currency) }</td>
|
||||
<td class="v-muted">{ r.Source }</td>
|
||||
<td>@endsInCell(r.EndsAt)</td>
|
||||
<td class="v-muted text-sm">{ humanTime(r.FoundAt) }</td>
|
||||
<td>
|
||||
if r.Alerted {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,10 @@ import (
|
||||
type SettingsData struct {
|
||||
Page
|
||||
Values map[string]string
|
||||
// CredentialStatus maps each secret settings key to a human-readable
|
||||
// status ("Saved in settings", "Set in config.toml", "Not set"). Secret
|
||||
// values are never rendered into the form itself.
|
||||
CredentialStatus map[string]string
|
||||
IsAdmin bool
|
||||
Users []models.User
|
||||
TestNtfyOK string
|
||||
@@ -28,6 +32,16 @@ func (d SettingsData) EbayLimitReached() bool {
|
||||
return d.EbayDailyLimit > 0 && d.EbayUsedToday >= d.EbayDailyLimit
|
||||
}
|
||||
|
||||
// credStatus renders the "Saved in settings / Set in config.toml / Not set"
|
||||
// indicator for a secret field without ever printing the secret itself.
|
||||
templ credStatus(d SettingsData, key string) {
|
||||
if s := d.CredentialStatus[key]; s != "" {
|
||||
<div class="v-muted text-xs mt-1">
|
||||
Status: { s }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
templ settingsBody(d SettingsData) {
|
||||
<div class="space-y-8 max-w-3xl">
|
||||
<h1 class="text-3xl font-semibold">Settings</h1>
|
||||
@@ -38,15 +52,18 @@ templ settingsBody(d SettingsData) {
|
||||
@CSRFInput(d.CSRFToken)
|
||||
<div>
|
||||
<label class="v-label">Apify API Key</label>
|
||||
<input class="v-input font-mono" type="password" name="apify_api_key" value={ d.Values["apify_api_key"] }/>
|
||||
<input class="v-input font-mono" type="password" name="apify_api_key" autocomplete="off" placeholder="leave blank to keep current value"/>
|
||||
@credStatus(d, "apify_api_key")
|
||||
</div>
|
||||
<div class="border-t border-white/10 pt-4">
|
||||
<label class="v-label">eBay App ID (Client ID)</label>
|
||||
<input class="v-input font-mono" type="password" name="ebay_client_id" value={ d.Values["ebay_client_id"] } placeholder="used for eBay marketplaces instead of Apify"/>
|
||||
<input class="v-input font-mono" type="password" name="ebay_client_id" autocomplete="off" placeholder="used for eBay marketplaces instead of Apify"/>
|
||||
@credStatus(d, "ebay_client_id")
|
||||
</div>
|
||||
<div>
|
||||
<label class="v-label">eBay Cert ID (Client Secret)</label>
|
||||
<input class="v-input font-mono" type="password" name="ebay_client_secret" value={ d.Values["ebay_client_secret"] }/>
|
||||
<input class="v-input font-mono" type="password" name="ebay_client_secret" autocomplete="off" placeholder="leave blank to keep current value"/>
|
||||
@credStatus(d, "ebay_client_secret")
|
||||
</div>
|
||||
<div>
|
||||
<label class="v-label">eBay Daily Call Limit</label>
|
||||
@@ -73,7 +90,8 @@ templ settingsBody(d SettingsData) {
|
||||
</div>
|
||||
<div>
|
||||
<label class="v-label">Ntfy Token</label>
|
||||
<input class="v-input font-mono" type="password" name="ntfy_token" value={ d.Values["ntfy_token"] } placeholder="tk_... (leave blank if ntfy is unauthenticated)"/>
|
||||
<input class="v-input font-mono" type="password" name="ntfy_token" autocomplete="off" placeholder="tk_... (leave blank to keep current value)"/>
|
||||
@credStatus(d, "ntfy_token")
|
||||
</div>
|
||||
<div>
|
||||
<label class="v-label">Global Poll Interval (minutes)</label>
|
||||
|
||||
@@ -17,6 +17,10 @@ import (
|
||||
type SettingsData struct {
|
||||
Page
|
||||
Values map[string]string
|
||||
// CredentialStatus maps each secret settings key to a human-readable
|
||||
// status ("Saved in settings", "Set in config.toml", "Not set"). Secret
|
||||
// values are never rendered into the form itself.
|
||||
CredentialStatus map[string]string
|
||||
IsAdmin bool
|
||||
Users []models.User
|
||||
TestNtfyOK string
|
||||
@@ -36,7 +40,9 @@ func (d SettingsData) EbayLimitReached() bool {
|
||||
return d.EbayDailyLimit > 0 && d.EbayUsedToday >= d.EbayDailyLimit
|
||||
}
|
||||
|
||||
func settingsBody(d SettingsData) templ.Component {
|
||||
// credStatus renders the "Saved in settings / Set in config.toml / Not set"
|
||||
// indicator for a secret field without ever printing the secret itself.
|
||||
func credStatus(d SettingsData, key string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
@@ -57,7 +63,51 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"space-y-8 max-w-3xl\"><h1 class=\"text-3xl font-semibold\">Settings</h1><section class=\"v-card p-6\"><h2 class=\"font-semibold mb-4\">Apify, eBay and Ntfy</h2><form method=\"post\" action=\"/settings\" class=\"space-y-4\">")
|
||||
if s := d.CredentialStatus[key]; s != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"v-muted text-xs mt-1\">Status: ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(s)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 40, Col: 14}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func settingsBody(d SettingsData) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var3 == nil {
|
||||
templ_7745c5c3_Var3 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"space-y-8 max-w-3xl\"><h1 class=\"text-3xl font-semibold\">Settings</h1><section class=\"v-card p-6\"><h2 class=\"font-semibold mb-4\">Apify, eBay and Ntfy</h2><form method=\"post\" action=\"/settings\" class=\"space-y-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -65,91 +115,58 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div><label class=\"v-label\">Apify API Key</label> <input class=\"v-input font-mono\" type=\"password\" name=\"apify_api_key\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div><label class=\"v-label\">Apify API Key</label> <input class=\"v-input font-mono\" type=\"password\" name=\"apify_api_key\" autocomplete=\"off\" placeholder=\"leave blank to keep current value\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["apify_api_key"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 41, Col: 108}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
templ_7745c5c3_Err = credStatus(d, "apify_api_key").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\"></div><div class=\"border-t border-white/10 pt-4\"><label class=\"v-label\">eBay App ID (Client ID)</label> <input class=\"v-input font-mono\" type=\"password\" name=\"ebay_client_id\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div><div class=\"border-t border-white/10 pt-4\"><label class=\"v-label\">eBay App ID (Client ID)</label> <input class=\"v-input font-mono\" type=\"password\" name=\"ebay_client_id\" autocomplete=\"off\" placeholder=\"used for eBay marketplaces instead of Apify\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ebay_client_id"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 45, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
templ_7745c5c3_Err = credStatus(d, "ebay_client_id").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" placeholder=\"used for eBay marketplaces instead of Apify\"></div><div><label class=\"v-label\">eBay Cert ID (Client Secret)</label> <input class=\"v-input font-mono\" type=\"password\" name=\"ebay_client_secret\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div><label class=\"v-label\">eBay Cert ID (Client Secret)</label> <input class=\"v-input font-mono\" type=\"password\" name=\"ebay_client_secret\" autocomplete=\"off\" placeholder=\"leave blank to keep current value\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = credStatus(d, "ebay_client_secret").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div><div><label class=\"v-label\">eBay Daily Call Limit</label> <input class=\"v-input font-mono\" name=\"ebay_daily_call_limit\" type=\"number\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ebay_client_secret"])
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ebay_daily_call_limit"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 49, Col: 118}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 70, Col: 122}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\"></div><div><label class=\"v-label\">eBay Daily Call Limit</label> <input class=\"v-input font-mono\" name=\"ebay_daily_call_limit\" type=\"number\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ebay_daily_call_limit"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 53, Col: 122}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" placeholder=\"5000 (blank uses config default)\"></div><div class=\"text-sm\"><span class=\"v-muted\">eBay API calls today:</span> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" placeholder=\"5000 (blank uses config default)\"></div><div class=\"text-sm\"><span class=\"v-muted\">eBay API calls today:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.EbayDailyLimit > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span class=\"font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", d.EbayUsedToday, d.EbayDailyLimit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 58, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d (uncapped)", d.EbayUsedToday))
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", d.EbayUsedToday, d.EbayDailyLimit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 60, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 75, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -157,127 +174,121 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"font-mono\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.EbayLimitReached() {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"v-flash-error inline-block ml-2\">Limit reached. eBay polling halted until the next reset (midnight US Pacific).</span>")
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d (uncapped)", d.EbayUsedToday))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 77, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><div class=\"border-t border-white/10 pt-4\"><label class=\"v-label\">Ntfy Base URL</label> <input class=\"v-input\" name=\"ntfy_base_url\" value=\"")
|
||||
if d.EbayLimitReached() {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"v-flash-error inline-block ml-2\">Limit reached. eBay polling halted until the next reset (midnight US Pacific).</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div><div class=\"border-t border-white/10 pt-4\"><label class=\"v-label\">Ntfy Base URL</label> <input class=\"v-input\" name=\"ntfy_base_url\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ntfy_base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 85, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\"></div><div><label class=\"v-label\">Ntfy Default Topic</label> <input class=\"v-input\" name=\"ntfy_default_topic\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ntfy_base_url"])
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ntfy_default_topic"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 68, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 89, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div><div><label class=\"v-label\">Ntfy Default Topic</label> <input class=\"v-input\" name=\"ntfy_default_topic\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"></div><div><label class=\"v-label\">Ntfy Token</label> <input class=\"v-input font-mono\" type=\"password\" name=\"ntfy_token\" autocomplete=\"off\" placeholder=\"tk_... (leave blank to keep current value)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = credStatus(d, "ntfy_token").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div><div><label class=\"v-label\">Global Poll Interval (minutes)</label> <input class=\"v-input font-mono\" name=\"global_poll_interval_minutes\" type=\"number\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ntfy_default_topic"])
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["global_poll_interval_minutes"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 72, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 98, Col: 136}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"></div><div><label class=\"v-label\">Ntfy Token</label> <input class=\"v-input font-mono\" type=\"password\" name=\"ntfy_token\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"></div><div><label class=\"v-label\">Match Confidence Threshold</label> <input class=\"v-input font-mono\" name=\"match_confidence_threshold\" type=\"number\" min=\"0\" max=\"1\" step=\"0.05\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["ntfy_token"])
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["match_confidence_threshold"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 76, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 102, Col: 160}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" placeholder=\"tk_... (leave blank if ntfy is unauthenticated)\"></div><div><label class=\"v-label\">Global Poll Interval (minutes)</label> <input class=\"v-input font-mono\" name=\"global_poll_interval_minutes\" type=\"number\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["global_poll_interval_minutes"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 80, Col: 136}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"></div><div><label class=\"v-label\">Match Confidence Threshold</label> <input class=\"v-input font-mono\" name=\"match_confidence_threshold\" type=\"number\" min=\"0\" max=\"1\" step=\"0.05\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Values["match_confidence_threshold"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 84, Col: 160}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !d.IsAdmin {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"v-muted text-sm\">Read-only for non-admin users.</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div class=\"v-muted text-sm\">Read-only for non-admin users.</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"flex items-center gap-3 pt-1\"><button class=\"v-btn\" type=\"submit\">Save</button> <button class=\"v-btn-ghost\" type=\"submit\" formaction=\"/settings/test-ntfy\">Test Ntfy</button> <button class=\"v-btn-ghost\" type=\"submit\" formaction=\"/settings/test-apify\">Test Apify</button> <button class=\"v-btn-ghost\" type=\"submit\" formaction=\"/settings/test-ebay\">Test eBay</button></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"flex items-center gap-3 pt-1\"><button class=\"v-btn\" type=\"submit\">Save</button> <button class=\"v-btn-ghost\" type=\"submit\" formaction=\"/settings/test-ntfy\">Test Ntfy</button> <button class=\"v-btn-ghost\" type=\"submit\" formaction=\"/settings/test-apify\">Test Apify</button> <button class=\"v-btn-ghost\" type=\"submit\" formaction=\"/settings/test-ebay\">Test eBay</button></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</form>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</form>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.TestNtfyOK != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<div class=\"v-flash mt-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestNtfyOK)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 98, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if d.TestApifyOK != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"v-flash mt-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestApifyOK)
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestNtfyOK)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 101, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 116, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -286,17 +297,17 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if d.TestEbayOK != "" {
|
||||
if d.TestApifyOK != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"v-flash mt-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestEbayOK)
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestApifyOK)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 104, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 119, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -305,40 +316,40 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</section><section class=\"v-card p-6\"><h2 class=\"font-semibold mb-4\">Change Password</h2>")
|
||||
if d.TestEbayOK != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<div class=\"v-flash mt-3\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(d.TestEbayOK)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 122, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</section><section class=\"v-card p-6\"><h2 class=\"font-semibold mb-4\">Change Password</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.PasswordError != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"v-flash-error\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"v-flash-error\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(d.PasswordError)
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(d.PasswordError)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 111, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 129, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if d.PasswordMsg != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<div class=\"v-flash\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(d.PasswordMsg)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 114, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -347,7 +358,26 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<form method=\"post\" action=\"/settings/password\" class=\"space-y-4\">")
|
||||
if d.PasswordMsg != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"v-flash\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(d.PasswordMsg)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 132, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<form method=\"post\" action=\"/settings/password\" class=\"space-y-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -355,45 +385,26 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<div><label class=\"v-label\">Current Password</label> <input class=\"v-input\" type=\"password\" name=\"current_password\"></div><div><label class=\"v-label\">New Password</label> <input class=\"v-input\" type=\"password\" name=\"new_password\"></div><div><label class=\"v-label\">Confirm New Password</label> <input class=\"v-input\" type=\"password\" name=\"new_password_confirm\"></div><button class=\"v-btn\" type=\"submit\">Update Password</button></form></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div><label class=\"v-label\">Current Password</label> <input class=\"v-input\" type=\"password\" name=\"current_password\"></div><div><label class=\"v-label\">New Password</label> <input class=\"v-input\" type=\"password\" name=\"new_password\"></div><div><label class=\"v-label\">Confirm New Password</label> <input class=\"v-input\" type=\"password\" name=\"new_password_confirm\"></div><button class=\"v-btn\" type=\"submit\">Update Password</button></form></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.IsAdmin {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<section class=\"v-card p-6\"><h2 class=\"font-semibold mb-4\">Users</h2>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<section class=\"v-card p-6\"><h2 class=\"font-semibold mb-4\">Users</h2>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if d.UserError != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<div class=\"v-flash-error\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"v-flash-error\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(d.UserError)
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(d.UserError)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 138, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 156, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if d.UserMsg != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<div class=\"v-flash\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(d.UserMsg)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 141, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -402,126 +413,145 @@ func settingsBody(d SettingsData) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<table class=\"v-table mb-4\"><thead><tr><th>Username</th><th>Role</th><th>Created</th><th></th></tr></thead> <tbody>")
|
||||
if d.UserMsg != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"v-flash\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(d.UserMsg)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 159, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<table class=\"v-table mb-4\"><thead><tr><th>Username</th><th>Role</th><th>Created</th><th></th></tr></thead> <tbody>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, u := range d.Users {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<tr><td>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<tr><td>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 166, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</td><td class=\"v-muted\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(string(u.Role))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 167, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</td><td class=\"v-muted text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(u.CreatedAt.Format("2006-01-02"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 148, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 168, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</td><td class=\"v-muted\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</td><td class=\"text-right\"><form class=\"inline\" method=\"post\" action=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(string(u.Role))
|
||||
var templ_7745c5c3_Var21 templ.SafeURL
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/users/%d/reset-password", u.ID)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 149, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 170, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</td><td class=\"v-muted text-sm\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(u.CreatedAt.Format("2006-01-02"))
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 150, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 171, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</td><td class=\"text-right\"><form class=\"inline\" method=\"post\" action=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"> <input type=\"password\" class=\"v-input inline-block max-w-[140px]\" name=\"new_password\" placeholder=\"new password\"> <button class=\"v-btn-ghost\" type=\"submit\">Reset</button></form><form class=\"inline\" method=\"post\" action=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 templ.SafeURL
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/users/%d/reset-password", u.ID)))
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/users/%d/delete", u.ID)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 152, Col: 113}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 175, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "\" onsubmit=\"return confirm('Remove user?')\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 153, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 176, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\"> <input type=\"password\" class=\"v-input inline-block max-w-[140px]\" name=\"new_password\" placeholder=\"new password\"> <button class=\"v-btn-ghost\" type=\"submit\">Reset</button></form><form class=\"inline\" method=\"post\" action=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 templ.SafeURL
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/users/%d/delete", u.ID)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 157, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "\" onsubmit=\"return confirm('Remove user?')\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 158, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"> <button class=\"v-btn-ghost\" type=\"submit\">Remove</button></form></td></tr>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"> <button class=\"v-btn-ghost\" type=\"submit\">Remove</button></form></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</tbody></table><form method=\"post\" action=\"/users\" class=\"grid md:grid-cols-4 gap-3 items-end\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</tbody></table><form method=\"post\" action=\"/users\" class=\"grid md:grid-cols-4 gap-3 items-end\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken)
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.CSRFToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 167, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/settings.templ`, Line: 185, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"><div><label class=\"v-label\">Username</label> <input class=\"v-input\" name=\"username\"></div><div><label class=\"v-label\">Role</label> <select class=\"v-select\" name=\"role\"><option value=\"user\">user</option> <option value=\"admin\">admin</option></select></div><div><label class=\"v-label\">Initial Password</label> <input class=\"v-input\" type=\"password\" name=\"password\"></div><button class=\"v-btn\" type=\"submit\">Add User</button></form></section>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\"><div><label class=\"v-label\">Username</label> <input class=\"v-input\" name=\"username\"></div><div><label class=\"v-label\">Role</label> <select class=\"v-select\" name=\"role\"><option value=\"user\">user</option> <option value=\"admin\">admin</option></select></div><div><label class=\"v-label\">Initial Password</label> <input class=\"v-input\" type=\"password\" name=\"password\"></div><button class=\"v-btn\" type=\"submit\">Add User</button></form></section>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -545,9 +575,9 @@ func Settings(d SettingsData) templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var28 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var28 == nil {
|
||||
templ_7745c5c3_Var28 = templ.NopComponent
|
||||
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var26 == nil {
|
||||
templ_7745c5c3_Var26 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = Layout(d.Page, settingsBody(d)).Render(ctx, templ_7745c5c3_Buffer)
|
||||
|
||||
Reference in New Issue
Block a user