Items-list sparklines, retro CSS, pinned tooling, deploy docs

- Bulk-load recent price points per item and render a sparkline in
  the items list (new LoadRecentPriceHistory query avoids N+1).
- Add retro.css visual layer and refreshed login/items/layout styling.
- Swap the logo from webp to avif.
- Pin htmx/Chart.js/Tailwind/templ versions in the Makefile with
  vendor / tools / update-deps targets; README documents the
  dependency-bump flow and the hardened systemd deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-15 19:10:56 -07:00
parent 0ec97afafb
commit ea3577a45e
17 changed files with 1174 additions and 343 deletions

View File

@@ -1,18 +1,28 @@
# Veola build. # Veola build.
# #
# Requires the `templ` CLI (go install github.com/a-h/templ/cmd/templ@latest). # Tool dependencies the `*_VERSION` variables below are the single source of
# The Tailwind standalone CLI is fetched on demand into bin/ (gitignored) — no # truth for every non-Go thing Veola pulls in at build time. To upgrade one,
# node toolchain required. static/css/tailwind.css is a committed build # bump its version and run `make vendor` (vendored JS) or just `make css`
# artifact so a plain `go build` deploy still has styles; run `make css` # (Tailwind) on a clean `bin/` to refetch.
# (or `make build`) after touching templates or static/css/input.css. #
# Go module dependencies live in go.mod and are bumped via `make update-deps`.
TAILWIND_VERSION := v3.4.17 TAILWIND_VERSION := v3.4.17
HTMX_VERSION := 2.0.4
CHARTJS_VERSION := 4.4.6
TEMPL_VERSION := v0.3.1020
TAILWIND_BIN := bin/tailwindcss TAILWIND_BIN := bin/tailwindcss
# linux-x64 only; change the asset name for other platforms. # Override TAILWIND_URL for non-linux-x64 platforms — see README "Build-time
TAILWIND_URL := https://github.com/tailwindlabs/tailwindcss/releases/download/$(TAILWIND_VERSION)/tailwindcss-linux-x64 # tools" for the matching asset names on the Tailwind releases page.
TAILWIND_URL ?= https://github.com/tailwindlabs/tailwindcss/releases/download/$(TAILWIND_VERSION)/tailwindcss-linux-x64
HTMX_URL := https://unpkg.com/htmx.org@$(HTMX_VERSION)/dist/htmx.min.js
CHARTJS_URL := https://cdn.jsdelivr.net/npm/chart.js@$(CHARTJS_VERSION)/dist/chart.umd.min.js
TEMPL := $(shell go env GOPATH)/bin/templ TEMPL := $(shell go env GOPATH)/bin/templ
.PHONY: all generate css build run test clean .PHONY: all generate css build run test clean tools vendor update-deps
all: build all: build
@@ -41,3 +51,32 @@ test:
clean: clean:
rm -f veola-bin rm -f veola-bin
# Install the pinned templ CLI into $(go env GOPATH)/bin. Pinning matters
# because two contributors on different templ versions can produce
# differently-formatted generated Go for the same .templ source.
tools:
go install github.com/a-h/templ/cmd/templ@$(TEMPL_VERSION)
# Re-fetch vendored third-party JS at the currently pinned versions,
# overwriting whatever is on disk. Also clears bin/tailwindcss so the next
# `make css` pulls the pinned Tailwind binary. Use after bumping any of
# HTMX_VERSION / CHARTJS_VERSION / TAILWIND_VERSION.
vendor:
curl -sL --fail $(HTMX_URL) -o static/vendor/htmx.min.js
curl -sL --fail $(CHARTJS_URL) -o static/vendor/chart.umd.min.js
rm -f $(TAILWIND_BIN)
$(MAKE) $(TAILWIND_BIN)
# Bump Go module dependencies to their newest compatible versions, then
# print upstream-release URLs for the non-Go tools so you can decide
# whether to bump those pins too. Does not modify the pinned versions.
update-deps:
go get -u ./...
go mod tidy
@echo
@echo "Pinned tool versions (current → upstream releases):"
@printf " Tailwind %s → https://github.com/tailwindlabs/tailwindcss/releases/latest\n" "$(TAILWIND_VERSION)"
@printf " htmx %s → https://github.com/bigskysoftware/htmx/releases/latest\n" "$(HTMX_VERSION)"
@printf " Chart.js %s → https://github.com/chartjs/Chart.js/releases/latest\n" "$(CHARTJS_VERSION)"
@printf " templ %s → https://github.com/a-h/templ/releases/latest\n" "$(TEMPL_VERSION)"

111
README.md
View File

@@ -18,18 +18,68 @@ See [`veola-spec.md`](veola-spec.md) for the full specification.
## Requirements ## Requirements
- Go 1.22+ (developed against 1.25) - Go 1.22+ (developed against 1.25)
- `make`, `curl`, and a POSIX shell (for the Makefile)
- A reachable [ntfy](https://ntfy.sh) instance (self-hosted or ntfy.sh) - A reachable [ntfy](https://ntfy.sh) instance (self-hosted or ntfy.sh)
- An [eBay developer](https://developer.ebay.com) keyset (App ID + Cert ID) — for eBay marketplaces - An [eBay developer](https://developer.ebay.com) keyset (App ID + Cert ID) — for eBay marketplaces
- An [Apify](https://apify.com) account + API key — for the non-eBay marketplaces - An [Apify](https://apify.com) account + API key — for the non-eBay marketplaces
- To build from source: the [`templ`](https://templ.guide) CLI. The Tailwind standalone CLI is fetched automatically by the Makefile — no Node toolchain required.
### Build-time tools
All non-Go tool versions are pinned in the Makefile at the top:
```make
TAILWIND_VERSION := v3.4.17
HTMX_VERSION := 2.0.4
CHARTJS_VERSION := 4.4.6
TEMPL_VERSION := v0.3.1020
```
Install `templ` yourself at the pinned version; Tailwind is downloaded for you on first `make css`. htmx and Chart.js are already vendored in `static/vendor/` at the pinned versions above and committed to the repo.
```sh
# Install the pinned templ CLI.
make tools
# (or, equivalently)
go install github.com/a-h/templ/cmd/templ@v0.3.1020
# Confirm it's on PATH (typically $(go env GOPATH)/bin).
templ --version
```
You **do not need Node, npm, or Yarn**. Tailwind ships as a self-contained binary — the Makefile fetches the [standalone Tailwind CLI](https://github.com/tailwindlabs/tailwindcss/releases) into `./bin/tailwindcss` on first `make css`. The `bin/` directory is gitignored.
By default the Makefile downloads the **linux-x64** build. For other platforms, override `TAILWIND_URL` on the command line — pick the matching asset from the [Tailwind releases page](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.4.17):
```sh
# macOS Apple Silicon
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-macos-arm64
# macOS Intel
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-macos-x64
# linux-arm64
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-linux-arm64
```
Or drop a binary at `./bin/tailwindcss` yourself and the Makefile will use it.
## Build ## Build
First time:
```sh
go install github.com/a-h/templ/cmd/templ@latest # if not already
make build # fetches Tailwind, runs templ, compiles
```
Subsequent builds:
```sh ```sh
make build make build
``` ```
This runs `templ generate`, compiles Tailwind, and produces `veola-bin`. Makefile targets: This runs `templ generate`, compiles Tailwind utilities into `static/css/tailwind.css`, and produces `veola-bin`. Makefile targets:
| Target | What it does | | Target | What it does |
| --- | --- | | --- | --- |
@@ -38,10 +88,11 @@ This runs `templ generate`, compiles Tailwind, and produces `veola-bin`. Makefil
| `make build` | `generate` + `css` + `go build -o veola-bin .` | | `make build` | `generate` + `css` + `go build -o veola-bin .` |
| `make run` | `build`, then run against `config.toml` | | `make run` | `build`, then run against `config.toml` |
| `make test` | `go test ./...` | | `make test` | `go test ./...` |
| `make clean` | Remove `veola-bin` |
The binary is named `veola-bin` rather than `veola` because the module is also `veola``go build` cannot write a binary with the same name as the module dir. The binary is named `veola-bin` rather than `veola` because the module is also `veola``go build` cannot write a binary with the same name as the module dir.
`static/css/tailwind.css` is committed, so a deploy box can `go build -o veola-bin .` without the Tailwind CLI. Re-run `make css` whenever you change templates or `static/css/input.css`. The hand-written component layer in `static/css/app.css` is loaded separately and needs no rebuild. `static/css/tailwind.css` is committed, so a deploy box can `go build -o veola-bin .` without the Tailwind CLI or even `make`. Re-run `make css` whenever you change templates or `static/css/input.css`. The hand-written component layer in `static/css/app.css` is loaded separately and needs no rebuild.
## Configure ## Configure
@@ -70,13 +121,22 @@ Other notable config:
./veola-bin -config config.toml ./veola-bin -config config.toml
``` ```
CLI flags:
| Flag | Default | Notes |
| --- | --- | --- |
| `-config <path>` | `config.toml` | Path to the TOML config file |
| `-debug` | off | Verbose `log/slog` at `LevelDebug`. Logs raw external API payloads (eBay / ZenMarket / etc.) — useful when diagnosing parse failures. Not for production. |
First-run flow: First-run flow:
1. Visit `http://localhost:8080/`. With no users, you are redirected to `/setup`. 1. Visit `http://localhost:8080/`. With no users, you are redirected to `/setup`.
2. Create the admin account. 2. Create the admin account. The first user is always an admin.
3. Log in at `/login`. 3. Log in at `/login`.
4. Add items at `/items/new`. Optionally fill in your eBay/Apify credentials and ntfy URL via `/settings` if you didn't put them in `config.toml`. The Settings page also shows the running eBay API call count for the day. 4. Add items at `/items/new`. Optionally fill in your eBay/Apify credentials and ntfy URL via `/settings` if you didn't put them in `config.toml`. The Settings page also shows the running eBay API call count for the day.
Account registration is admin-only — there is no public signup. Once at least one user exists, `/setup` returns 404. New users are created from the Settings page by an admin.
The scheduler starts with the server and polls each active item on its configured interval. The bottom-of-hour global poll runs every `scheduler.global_poll_interval_minutes`. The scheduler starts with the server and polls each active item on its configured interval. The bottom-of-hour global poll runs every `scheduler.global_poll_interval_minutes`.
## Layout ## Layout
@@ -108,6 +168,30 @@ go test ./...
Unit tests cover crypto round-trip, db round-trip and dedup, scheduler alert/badge logic, and eBay marketplace/filter mapping. No handler-level tests yet. Unit tests cover crypto round-trip, db round-trip and dedup, scheduler alert/badge logic, and eBay marketplace/filter mapping. No handler-level tests yet.
## Keeping dependencies current
Veola pulls from four sources, all version-pinned for reproducibility.
```sh
# Bump Go module deps to their newest compatible versions, then prints
# upstream-release URLs for the four pinned tools so you can spot bumps.
make update-deps
# After bumping any of TAILWIND_VERSION / HTMX_VERSION / CHARTJS_VERSION
# in the Makefile, refetch the vendored assets at the new pins:
make vendor
# After bumping TEMPL_VERSION:
make tools
# Then rebuild and run the test suite:
make build && make test
```
The pinned tool versions live at the top of the Makefile. The vendored JS at `static/vendor/htmx.min.js` and `static/vendor/chart.umd.min.js` is committed and updated only by `make vendor`. Tailwind v3.4.17 is intentionally pinned — v4 is a breaking release that drops the `tailwind.config.js` format Veola uses.
For automated tracking, point [Dependabot](https://docs.github.com/en/code-security/dependabot) or [Renovate](https://docs.renovatebot.com/) at the repo. Both can watch `go.mod` natively; Renovate's custom-regex managers can also track the `*_VERSION` lines in the Makefile.
## Operate ## Operate
- The SQLite file lives at `server.db_path` (default `./veola.db`). Back this up — it holds your watched items, history, encrypted secrets, and user accounts. - The SQLite file lives at `server.db_path` (default `./veola.db`). Back this up — it holds your watched items, history, encrypted secrets, and user accounts.
@@ -122,6 +206,25 @@ Veola speaks plain HTTP and is meant to sit behind a TLS-terminating reverse pro
- Keep `server.secure_cookies = true` (the default). - Keep `server.secure_cookies = true` (the default).
- Terminate TLS at the proxy and set HSTS there — Veola does not emit HSTS itself. - Terminate TLS at the proxy and set HSTS there — Veola does not emit HSTS itself.
- Veola sets `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, and `Referrer-Policy` on every response, and trusts `X-Forwarded-For` for client IPs — configure the proxy to strip client-supplied `X-Forwarded-*` headers so they cannot be spoofed. - Veola sets `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, and `Referrer-Policy` on every response, and trusts `X-Forwarded-For` for client IPs — configure the proxy to strip client-supplied `X-Forwarded-*` headers so they cannot be spoofed.
- `GET /healthz` returns 200 `ok` with no auth — wire it up to your proxy/uptime probe.
A hardened systemd unit template lives at [`deploy/veola.service`](deploy/veola.service). It assumes:
- Binary at `/usr/local/bin/veola-bin`
- Config at `/etc/veola/config.toml`
- A `veola` system user with `/var/lib/veola` as its working / data directory (the only writable path under `ReadWritePaths`)
Install sketch:
```sh
sudo useradd --system --home /var/lib/veola --shell /usr/sbin/nologin veola
sudo install -d -o veola -g veola -m 0750 /var/lib/veola /etc/veola
sudo install -m 0755 veola-bin /usr/local/bin/veola-bin
sudo install -m 0640 -o veola -g veola config.toml /etc/veola/config.toml
sudo install -m 0644 deploy/veola.service /etc/systemd/system/veola.service
sudo systemctl daemon-reload
sudo systemctl enable --now veola
```
## Aesthetic ## Aesthetic

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -798,6 +798,49 @@ func (s *Store) InsertPricePoint(ctx context.Context, p *models.PricePoint) erro
return err return err
} }
// LoadRecentPriceHistory bulk-loads the last `perItem` price points (oldest →
// newest) for each item id. Returns a map keyed by item id; items with no
// history are absent from the map. Used by the items list page to render a
// sparkline per row without N+1 queries.
func (s *Store) LoadRecentPriceHistory(ctx context.Context, ids []int64, perItem int) (map[int64][]models.PricePoint, error) {
out := make(map[int64][]models.PricePoint, len(ids))
if len(ids) == 0 || perItem <= 0 {
return out, nil
}
placeholders := make([]string, len(ids))
args := make([]any, 0, len(ids)+1)
for i, id := range ids {
placeholders[i] = "?"
args = append(args, id)
}
args = append(args, perItem)
q := `
WITH ranked AS (
SELECT item_id, price, store, polled_at,
ROW_NUMBER() OVER (PARTITION BY item_id ORDER BY polled_at DESC) AS rn
FROM price_history
WHERE item_id IN (` + strings.Join(placeholders, ",") + `)
)
SELECT item_id, price, store, polled_at FROM ranked WHERE rn <= ?
ORDER BY item_id, polled_at ASC
`
rows, err := s.DB.QueryContext(ctx, q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var p models.PricePoint
var store sql.NullString
if err := rows.Scan(&p.ItemID, &p.Price, &store, &p.PolledAt); err != nil {
return nil, err
}
p.Store = s.dec(store.String)
out[p.ItemID] = append(out[p.ItemID], p)
}
return out, rows.Err()
}
func (s *Store) ListPriceHistory(ctx context.Context, itemID int64) ([]models.PricePoint, error) { func (s *Store) ListPriceHistory(ctx context.Context, itemID int64) ([]models.PricePoint, error) {
rows, err := s.DB.QueryContext(ctx, rows, err := s.DB.QueryContext(ctx,
`SELECT id, item_id, price, store, polled_at FROM price_history WHERE item_id = ? ORDER BY polled_at ASC`, `SELECT id, item_id, price, store, polled_at FROM price_history WHERE item_id = ? ORDER BY polled_at ASC`,

View File

@@ -30,11 +30,22 @@ func (a *App) GetItems(w http.ResponseWriter, r *http.Request) {
} }
} }
cats, _ := a.Store.ListCategories(r.Context()) cats, _ := a.Store.ListCategories(r.Context())
// Bulk-load recent price history so each row can render a sparkline
// without N+1 queries. 20 points is enough for a meaningful trend line
// at 80px wide and stays cheap on the largest realistic watchlists.
ids := make([]int64, 0, len(items))
for _, it := range items {
ids = append(ids, it.ID)
}
history, _ := a.Store.LoadRecentPriceHistory(r.Context(), ids, 20)
render(w, r, templates.Items(templates.ItemsData{ render(w, r, templates.Items(templates.ItemsData{
Page: a.page(r, "Items", "items"), Page: a.page(r, "Items", "items"),
Items: items, Items: items,
Categories: cats, Categories: cats,
SelectedCategory: cat, SelectedCategory: cat,
PriceHistory: history,
})) }))
} }
@@ -387,7 +398,8 @@ func (a *App) PostToggleItem(w http.ResponseWriter, r *http.Request) {
return return
} }
a.Scheduler.SyncItem(*it) a.Scheduler.SyncItem(*it)
render(w, r, templates.ItemRow(*it, a.Auth.CSRFToken(r.Context()))) hist, _ := a.Store.LoadRecentPriceHistory(r.Context(), []int64{id}, 20)
render(w, r, templates.ItemRow(*it, a.Auth.CSRFToken(r.Context()), hist[id]))
} }
func (a *App) PostDeleteItem(w http.ResponseWriter, r *http.Request) { func (a *App) PostDeleteItem(w http.ResponseWriter, r *http.Request) {

View File

@@ -176,6 +176,19 @@ a:hover { text-decoration: underline; }
} }
.v-side-nav a:hover { color: white; } .v-side-nav a:hover { color: white; }
/* The brand wordmark at the top of the sidebar is also an anchor (→ /), but
shouldn't pick up the active-item border-left / padding treatment that
the nav links get. Higher specificity overrides .v-side-nav a defaults. */
.v-side-nav a.v-side-brand {
border-left: 0;
color: var(--text);
text-decoration: none;
}
.v-side-nav a.v-side-brand:hover {
text-decoration: none;
filter: brightness(1.15);
}
.v-veola-portrait { .v-veola-portrait {
background: #f3ead8; background: #f3ead8;
border-radius: 12px; border-radius: 12px;
@@ -304,6 +317,156 @@ table.v-table tbody tr:hover td { background: rgba(0, 164, 228, 0.08); }
50% { opacity: 0.55; } 50% { opacity: 0.55; }
} }
/* Sparkline cells in the items list. Color follows trend: green when the
latest price is meaningfully below the running average (good news for a
watchlist), red when it's risen, neutral otherwise. */
.v-sparkline { display: block; overflow: visible; }
.v-spark-down { color: var(--success); filter: drop-shadow(0 0 4px rgba(0, 228, 164, 0.45)); }
.v-spark-up { color: var(--danger); filter: drop-shadow(0 0 4px rgba(232, 64, 64, 0.40)); }
.v-spark-flat { color: var(--text-2); }
/* Trend arrow rendered next to Best Price. Same palette as the sparkline,
so a glance at the column reads consistently. */
.v-trend { font-size: 0.95rem; font-weight: 700; }
.v-trend-down { color: var(--success); }
.v-trend-up { color: var(--danger); }
.v-trend-flat { color: var(--text-2); }
/* Mascot "deal" moment: Veola appears next to an item's name only when the
current best price is at or below target. Small, animated, decorative —
purely a delight hit on top of the existing "Deal" badge. */
.v-deal-mascot {
width: 24px;
height: 24px;
border-radius: 6px;
object-fit: cover;
background: #f3ead8;
padding: 1px;
box-shadow: 0 0 0 1px rgba(0, 228, 164, 0.6), 0 0 12px rgba(0, 228, 164, 0.45);
animation: v-deal-bob 2.4s ease-in-out infinite;
}
@keyframes v-deal-bob {
0%, 100% { transform: translateY(0) rotate(-2deg); }
50% { transform: translateY(-2px) rotate(2deg); }
}
@media (prefers-reduced-motion: reduce) {
.v-deal-mascot { animation: none; }
}
/* --- Login / Setup chrome -----------------------------------------------
The auth pages use the Bare layout (no sidebar) so the form has to carry
its own visual weight. The v-auth-* classes give it that: a glassy card
for the form, a softly-rotating conic halo behind the mascot, a
typeset wordmark + tagline, and the right column is left translucent so
the global aurora reaches edge-to-edge instead of stopping at a flat
blue block. */
.v-auth-wordmark {
font-family: 'Outfit', system-ui, sans-serif;
text-shadow: 0 2px 14px rgba(0, 164, 228, 0.30);
}
.v-auth-card {
/* Slightly heavier shadow + inner glow ring since the card stands
alone on the page with no sidebar context. */
box-shadow:
0 16px 44px rgba(0, 0, 80, 0.55),
0 0 0 1px rgba(0, 164, 228, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.v-auth-tagline {
margin-top: 1.5rem;
text-align: center;
letter-spacing: 0.34em;
font-size: 0.72rem;
text-transform: uppercase;
color: var(--text-2);
text-shadow: 0 0 12px rgba(0, 164, 228, 0.35);
}
.v-auth-portrait-col {
/* Translucent overlay instead of a solid block: a soft inward vignette
plus a faint diagonal sheen, keeping the column distinct from the
form side without hiding the aurora behind it. */
background:
radial-gradient(ellipse at center, rgba(0, 0, 0, 0.0) 30%, rgba(0, 0, 0, 0.25) 100%),
linear-gradient(135deg, rgba(0, 164, 228, 0.05) 0%, rgba(245, 196, 0, 0.04) 100%);
position: relative;
overflow: hidden;
}
.v-auth-portrait-halo {
position: relative;
isolation: isolate;
width: min(420px, 80%);
display: flex;
align-items: center;
justify-content: center;
animation: v-auth-portrait-float 7s ease-in-out infinite;
will-change: transform;
}
/* Rotating conic-gradient halo. Sits behind the portrait via z-index -1
inside the isolated stacking context, blurred so it reads as a glow
rather than a hard rainbow. */
.v-auth-portrait-halo::before {
content: "";
position: absolute;
inset: -15%;
border-radius: 50%;
background: conic-gradient(
from 180deg,
rgba(0, 164, 228, 0.55),
rgba(245, 196, 0, 0.40),
rgba(232, 64, 64, 0.30),
rgba(0, 228, 164, 0.40),
rgba(0, 164, 228, 0.55)
);
filter: blur(50px);
z-index: -1;
animation: v-auth-portrait-spin 22s linear infinite;
will-change: transform;
}
/* A second, slower halo gives the impression of depth: two layers of
light rotating at different speeds keep the eye from locking onto a
single repeating pattern. */
.v-auth-portrait-halo::after {
content: "";
position: absolute;
inset: -25%;
border-radius: 50%;
background: conic-gradient(
from 0deg,
rgba(0, 228, 164, 0.18),
rgba(0, 164, 228, 0.10),
rgba(245, 196, 0, 0.14),
rgba(0, 228, 164, 0.18)
);
filter: blur(70px);
z-index: -1;
animation: v-auth-portrait-spin 42s linear infinite reverse;
will-change: transform;
opacity: 0.85;
}
@keyframes v-auth-portrait-spin {
to { transform: rotate(360deg); }
}
@keyframes v-auth-portrait-float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-6px); }
}
@media (prefers-reduced-motion: reduce) {
.v-auth-portrait-halo,
.v-auth-portrait-halo::before,
.v-auth-portrait-halo::after { animation: none; }
}
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.v-card { transition: none; } .v-card { transition: none; }
.v-card:hover { transform: none; } .v-card:hover { transform: none; }

118
static/css/retro.css Normal file
View File

@@ -0,0 +1,118 @@
/* retro.css — optional "retro-arcade identity" layer.
*
* Entirely additive. To revert, delete this file and remove its <link> tag
* from templates/layout.templ. Nothing else depends on it.
*
* What this adds:
* 1. Two independently-animated aurora blobs that drift across the
* viewport on different paths — the real "shifting light" feel,
* not a synchronized slide.
* 2. Dot-grid overlay over the whole viewport.
* 3. Heavier display headings with a soft accent glow.
*
* Both blobs live on html pseudo-elements at z-index -2 / -1, sitting
* behind everything else so card glassiness still reads on top.
*/
body {
/* Let the html-level aurora show through. app.css set a solid blue here;
dropping it here unlocks the gradient. Removing retro.css restores the
original solid background automatically. */
background: transparent;
}
html {
background-color: var(--bg);
/* Static dot-grid baked into the root background so the two pseudo-element
slots stay free for the animated blobs. */
background-image: radial-gradient(
circle,
rgba(255, 255, 255, 0.045) 1px,
transparent 1px
);
background-size: 26px 26px;
background-attachment: fixed;
}
/* Two aurora blobs animated with seven waypoints each (no `alternate`, so
the second half doesn't mirror the first) on incommensurate periods —
the two cycles drift in and out of phase, so the eye never locks onto a
repeating pattern even though each blob is technically on a loop.
Opacity also wobbles so the lights "breathe" instead of just sliding. */
html::before {
content: "";
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
background: radial-gradient(
closest-side,
rgba(0, 164, 228, 0.60),
rgba(0, 164, 228, 0.0) 70%
);
width: 70vmax;
height: 70vmax;
border-radius: 50%;
filter: blur(60px);
/* Centered base; keyframes drive all motion from here. */
top: 50%; left: 50%;
margin-top: -35vmax; margin-left: -35vmax;
animation: v-retro-drift-a 41s cubic-bezier(.6,.1,.4,.9) infinite;
will-change: transform, opacity;
}
html::after {
content: "";
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
background: radial-gradient(
closest-side,
rgba(245, 196, 0, 0.50),
rgba(245, 196, 0, 0.0) 70%
);
width: 55vmax;
height: 55vmax;
border-radius: 50%;
filter: blur(70px);
top: 50%; left: 50%;
margin-top: -27.5vmax; margin-left: -27.5vmax;
animation: v-retro-drift-b 29s cubic-bezier(.5,.2,.3,.8) infinite;
will-change: transform, opacity;
}
@keyframes v-retro-drift-a {
0% { transform: translate3d(-30vw, -20vh, 0) scale(1.00); opacity: 0.85; }
14% { transform: translate3d( 5vw, -28vh, 0) scale(1.18); opacity: 1.00; }
29% { transform: translate3d( 32vw, -10vh, 0) scale(0.92); opacity: 0.75; }
43% { transform: translate3d( 18vw, 22vh, 0) scale(1.10); opacity: 0.95; }
58% { transform: translate3d(-15vw, 30vh, 0) scale(1.05); opacity: 0.80; }
72% { transform: translate3d(-38vw, 8vh, 0) scale(0.95); opacity: 1.00; }
86% { transform: translate3d(-20vw, -12vh, 0) scale(1.12); opacity: 0.90; }
100% { transform: translate3d(-30vw, -20vh, 0) scale(1.00); opacity: 0.85; }
}
@keyframes v-retro-drift-b {
0% { transform: translate3d( 28vw, -25vh, 0) scale(1.00); opacity: 0.70; }
17% { transform: translate3d( 0vw, -10vh, 0) scale(1.20); opacity: 0.95; }
33% { transform: translate3d(-30vw, 8vh, 0) scale(0.90); opacity: 0.80; }
48% { transform: translate3d(-12vw, 28vh, 0) scale(1.10); opacity: 1.00; }
64% { transform: translate3d( 22vw, 18vh, 0) scale(0.95); opacity: 0.75; }
80% { transform: translate3d( 36vw, -2vh, 0) scale(1.15); opacity: 0.90; }
100% { transform: translate3d( 28vw, -25vh, 0) scale(1.00); opacity: 0.70; }
}
/* Display headings: heavier, slightly tighter, with an accent glow that ties
into the card border gradient. Class-free selectors so existing Tailwind
utilities (text-3xl etc.) stack on top untouched. */
h1, h2 {
font-weight: 800;
letter-spacing: -0.02em;
text-shadow: 0 2px 14px rgba(0, 164, 228, 0.30);
}
@media (prefers-reduced-motion: reduce) {
html::before, html::after { animation: none; }
}

File diff suppressed because one or more lines are too long

BIN
static/img/veola.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -9,7 +9,29 @@
return; return;
} }
var data = JSON.parse(canvas.dataset.chart); var data = JSON.parse(canvas.dataset.chart);
new Chart(canvas.getContext('2d'), { var ctx = canvas.getContext('2d');
// Vertical gradient fill — bright accent at the top, transparent at the
// baseline — makes the chart read at a glance instead of as a thin line.
var fill = ctx.createLinearGradient(0, 0, 0, canvas.clientHeight || 200);
fill.addColorStop(0, 'rgba(0, 228, 164, 0.45)');
fill.addColorStop(1, 'rgba(0, 228, 164, 0.00)');
// Chart.js plugin that paints a soft glow under the line stroke before
// the dataset draws. Cheap enough to keep on by default; respects
// prefers-reduced-motion only insofar as nothing animates here.
var glowPlugin = {
id: 'priceLineGlow',
beforeDatasetDraw: function (chart) {
var c = chart.ctx;
c.save();
c.shadowColor = 'rgba(0, 228, 164, 0.55)';
c.shadowBlur = 12;
},
afterDatasetDraw: function (chart) { chart.ctx.restore(); }
};
new Chart(ctx, {
type: 'line', type: 'line',
data: { data: {
labels: data.labels, labels: data.labels,
@@ -17,19 +39,37 @@
label: 'Best price', label: 'Best price',
data: data.points, data: data.points,
borderColor: '#00e4a4', borderColor: '#00e4a4',
backgroundColor: 'rgba(0,228,164,0.15)', borderWidth: 2.5,
pointBackgroundColor: '#e84040', backgroundColor: fill,
pointBackgroundColor: '#ffffff',
pointBorderColor: '#00e4a4',
pointBorderWidth: 1.5,
pointRadius: 3, pointRadius: 3,
tension: 0.25, pointHoverRadius: 5,
tension: 0.3,
fill: true fill: true
}] }]
}, },
options: { options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: { scales: {
x: { ticks: { color: '#a8c0f0' }, grid: { color: 'rgba(255,255,255,0.07)' } }, x: { ticks: { color: '#a8c0f0' }, grid: { color: 'rgba(255,255,255,0.06)' } },
y: { ticks: { color: '#a8c0f0' }, grid: { color: 'rgba(255,255,255,0.07)' } } y: { ticks: { color: '#a8c0f0' }, grid: { color: 'rgba(255,255,255,0.06)' } }
}, },
plugins: { legend: { labels: { color: '#ffffff' } } } plugins: {
legend: { labels: { color: '#ffffff' } },
tooltip: {
backgroundColor: 'rgba(20, 32, 80, 0.95)',
borderColor: 'rgba(0, 164, 228, 0.6)',
borderWidth: 1,
titleColor: '#ffffff',
bodyColor: '#a8c0f0',
padding: 10
} }
}
},
plugins: [glowPlugin]
}); });
})(); })();

View File

@@ -2,6 +2,8 @@ package templates
import ( import (
"fmt" "fmt"
"math"
"strings"
"veola/internal/models" "veola/internal/models"
) )
@@ -11,6 +13,85 @@ type ItemsData struct {
Items []models.Item Items []models.Item
Categories []string Categories []string
SelectedCategory string SelectedCategory string
// PriceHistory holds the last ~20 points per item id, ascending in time.
// Nil/missing entries render an empty sparkline cell.
PriceHistory map[int64][]models.PricePoint
}
// sparklinePoints turns a price-history slice into an SVG polyline "points"
// attribute, normalized to a 80x24 viewBox with 2px padding so endpoints
// don't clip the stroke. Returns "" when there isn't enough history to draw.
func sparklinePoints(history []models.PricePoint) string {
if len(history) < 2 {
return ""
}
const w, h, pad = 80.0, 24.0, 2.0
minP, maxP := math.Inf(1), math.Inf(-1)
for _, p := range history {
if p.Price < minP {
minP = p.Price
}
if p.Price > maxP {
maxP = p.Price
}
}
span := maxP - minP
if span == 0 {
// All-equal series: draw a flat line through the middle.
span = 1
}
step := (w - 2*pad) / float64(len(history)-1)
parts := make([]string, len(history))
for i, p := range history {
x := pad + float64(i)*step
y := h - pad - ((p.Price-minP)/span)*(h-2*pad)
parts[i] = fmt.Sprintf("%.1f,%.1f", x, y)
}
return strings.Join(parts, " ")
}
// sparklineTrendClass compares the last point to the average of the prior
// points and returns a CSS class so the sparkline tints green (price down),
// red (up), or neutral. Threshold is ±3% to ignore tiny wobbles.
func sparklineTrendClass(history []models.PricePoint) string {
if len(history) < 2 {
return ""
}
last := history[len(history)-1].Price
var sum float64
for _, p := range history[:len(history)-1] {
sum += p.Price
}
avg := sum / float64(len(history)-1)
if avg == 0 {
return "v-spark-flat"
}
switch {
case last <= avg*0.97:
return "v-spark-down"
case last >= avg*1.03:
return "v-spark-up"
}
return "v-spark-flat"
}
// trendArrow returns the unicode arrow + a CSS class describing the direction.
// Same threshold logic as the sparkline. Empty when there's no trend signal.
func trendArrow(history []models.PricePoint) (glyph, class string) {
switch sparklineTrendClass(history) {
case "v-spark-down":
return "↓", "v-trend-down"
case "v-spark-up":
return "↑", "v-trend-up"
case "v-spark-flat":
return "→", "v-trend-flat"
}
return "", ""
}
// isDeal is the gate for the mascot "deal" moment on a row.
func isDeal(it models.Item) bool {
return it.BestPrice != nil && it.TargetPrice != nil && *it.BestPrice <= *it.TargetPrice
} }
templ itemsBody(d ItemsData) { templ itemsBody(d ItemsData) {
@@ -41,6 +122,7 @@ templ itemsBody(d ItemsData) {
<th>Category</th> <th>Category</th>
<th>Target</th> <th>Target</th>
<th>Best Price</th> <th>Best Price</th>
<th>Trend</th>
<th>Last Polled</th> <th>Last Polled</th>
<th>Status</th> <th>Status</th>
<th></th> <th></th>
@@ -48,7 +130,7 @@ templ itemsBody(d ItemsData) {
</thead> </thead>
<tbody id="items-tbody"> <tbody id="items-tbody">
for _, it := range d.Items { for _, it := range d.Items {
@itemRow(it, d.CSRFToken) @itemRow(it, d.CSRFToken, d.PriceHistory[it.ID])
} }
</tbody> </tbody>
</table> </table>
@@ -60,7 +142,7 @@ templ itemsBody(d ItemsData) {
templ itemsEmpty() { templ itemsEmpty() {
<div class="v-card p-8 flex flex-col md:flex-row items-center gap-6"> <div class="v-card p-8 flex flex-col md:flex-row items-center gap-6">
<div class="v-veola-portrait w-48 shrink-0"> <div class="v-veola-portrait w-48 shrink-0">
<img src="/static/img/veola.webp" alt="Veola"/> <img src="/static/img/veola.avif" alt="Veola"/>
</div> </div>
<div> <div>
<h2 class="text-xl font-semibold mb-2">Nothing on the watchlist.</h2> <h2 class="text-xl font-semibold mb-2">Nothing on the watchlist.</h2>
@@ -70,10 +152,15 @@ templ itemsEmpty() {
</div> </div>
} }
templ itemRow(it models.Item, csrf string) { templ itemRow(it models.Item, csrf string, history []models.PricePoint) {
<tr id={ fmt.Sprintf("item-row-%d", it.ID) }> <tr id={ fmt.Sprintf("item-row-%d", it.ID) }>
<td> <td>
<div class="flex items-center gap-2">
if isDeal(it) {
<img class="v-deal-mascot" src="/static/img/veola.avif" alt="" title={ fmt.Sprintf("Deal! Best %s ≤ target %s", fmtPrice(it.BestPrice, it.BestPriceCurrency), fmtPrice(it.TargetPrice, "USD")) }/>
}
<a href={ templ.SafeURL(fmt.Sprintf("/items/%d/results", it.ID)) }>{ it.Name }</a> <a href={ templ.SafeURL(fmt.Sprintf("/items/%d/results", it.ID)) }>{ it.Name }</a>
</div>
if it.LastPollError != "" { if it.LastPollError != "" {
<button class="v-pill v-pill-error ml-2" hx-get={ fmt.Sprintf("/items/%d/error", it.ID) } hx-target={ fmt.Sprintf("#item-error-%d", it.ID) } hx-swap="innerHTML">!</button> <button class="v-pill v-pill-error ml-2" hx-get={ fmt.Sprintf("/items/%d/error", it.ID) } hx-target={ fmt.Sprintf("#item-error-%d", it.ID) } hx-swap="innerHTML">!</button>
<div id={ fmt.Sprintf("item-error-%d", it.ID) } class="v-error-text mt-1"></div> <div id={ fmt.Sprintf("item-error-%d", it.ID) } class="v-error-text mt-1"></div>
@@ -89,7 +176,12 @@ templ itemRow(it models.Item, csrf string) {
</td> </td>
<td> <td>
if it.BestPrice != nil { if it.BestPrice != nil {
<div class={ "font-mono text-lg", priceClass(it.BestPrice, it.TargetPrice) }>{ fmtPrice(it.BestPrice, it.BestPriceCurrency) }</div> <div class="flex items-center gap-1.5">
<span class={ "font-mono text-lg", priceClass(it.BestPrice, it.TargetPrice) }>{ fmtPrice(it.BestPrice, it.BestPriceCurrency) }</span>
if glyph, cls := trendArrow(history); glyph != "" {
<span class={ "v-trend", cls }>{ glyph }</span>
}
</div>
if it.BestPriceURL != "" { if it.BestPriceURL != "" {
<a class="text-xs" href={ templ.SafeURL(it.BestPriceURL) } target="_blank" rel="noopener">{ it.BestPriceStore }</a> <a class="text-xs" href={ templ.SafeURL(it.BestPriceURL) } target="_blank" rel="noopener">{ it.BestPriceStore }</a>
} else if it.BestPriceStore != "" { } else if it.BestPriceStore != "" {
@@ -99,6 +191,15 @@ templ itemRow(it models.Item, csrf string) {
<span class="v-muted">not yet</span> <span class="v-muted">not yet</span>
} }
</td> </td>
<td>
if pts := sparklinePoints(history); pts != "" {
<svg class={ "v-sparkline", sparklineTrendClass(history) } viewBox="0 0 80 24" width="80" height="24" aria-hidden="true">
<polyline points={ pts } fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
} else {
<span class="v-muted text-xs">—</span>
}
</td>
<td class="v-muted text-sm"> <td class="v-muted text-sm">
if it.LastPolledAt != nil { if it.LastPolledAt != nil {
<span title={ it.LastPolledAt.Format("2006-01-02 15:04:05") }>{ humanTime(*it.LastPolledAt) }</span> <span title={ it.LastPolledAt.Format("2006-01-02 15:04:05") }>{ humanTime(*it.LastPolledAt) }</span>
@@ -154,9 +255,11 @@ templ Items(d ItemsData) {
@Layout(d.Page, itemsBody(d)) @Layout(d.Page, itemsBody(d))
} }
// ItemRow renders a single row partial, used by HTMX endpoints. // ItemRow renders a single row partial, used by HTMX endpoints. Callers
templ ItemRow(it models.Item, csrf string) { // that don't have history cheaply on hand pass nil; the sparkline cell
@itemRow(it, csrf) // degrades to an em-dash placeholder.
templ ItemRow(it models.Item, csrf string, history []models.PricePoint) {
@itemRow(it, csrf, history)
} }
// EmptyRow lets a delete handler return a row replacement that vanishes. // EmptyRow lets a delete handler return a row replacement that vanishes.

View File

@@ -10,6 +10,8 @@ import templruntime "github.com/a-h/templ/runtime"
import ( import (
"fmt" "fmt"
"math"
"strings"
"veola/internal/models" "veola/internal/models"
) )
@@ -19,6 +21,85 @@ type ItemsData struct {
Items []models.Item Items []models.Item
Categories []string Categories []string
SelectedCategory string SelectedCategory string
// PriceHistory holds the last ~20 points per item id, ascending in time.
// Nil/missing entries render an empty sparkline cell.
PriceHistory map[int64][]models.PricePoint
}
// sparklinePoints turns a price-history slice into an SVG polyline "points"
// attribute, normalized to a 80x24 viewBox with 2px padding so endpoints
// don't clip the stroke. Returns "" when there isn't enough history to draw.
func sparklinePoints(history []models.PricePoint) string {
if len(history) < 2 {
return ""
}
const w, h, pad = 80.0, 24.0, 2.0
minP, maxP := math.Inf(1), math.Inf(-1)
for _, p := range history {
if p.Price < minP {
minP = p.Price
}
if p.Price > maxP {
maxP = p.Price
}
}
span := maxP - minP
if span == 0 {
// All-equal series: draw a flat line through the middle.
span = 1
}
step := (w - 2*pad) / float64(len(history)-1)
parts := make([]string, len(history))
for i, p := range history {
x := pad + float64(i)*step
y := h - pad - ((p.Price-minP)/span)*(h-2*pad)
parts[i] = fmt.Sprintf("%.1f,%.1f", x, y)
}
return strings.Join(parts, " ")
}
// sparklineTrendClass compares the last point to the average of the prior
// points and returns a CSS class so the sparkline tints green (price down),
// red (up), or neutral. Threshold is ±3% to ignore tiny wobbles.
func sparklineTrendClass(history []models.PricePoint) string {
if len(history) < 2 {
return ""
}
last := history[len(history)-1].Price
var sum float64
for _, p := range history[:len(history)-1] {
sum += p.Price
}
avg := sum / float64(len(history)-1)
if avg == 0 {
return "v-spark-flat"
}
switch {
case last <= avg*0.97:
return "v-spark-down"
case last >= avg*1.03:
return "v-spark-up"
}
return "v-spark-flat"
}
// trendArrow returns the unicode arrow + a CSS class describing the direction.
// Same threshold logic as the sparkline. Empty when there's no trend signal.
func trendArrow(history []models.PricePoint) (glyph, class string) {
switch sparklineTrendClass(history) {
case "v-spark-down":
return "↓", "v-trend-down"
case "v-spark-up":
return "↑", "v-trend-up"
case "v-spark-flat":
return "→", "v-trend-flat"
}
return "", ""
}
// isDeal is the gate for the mascot "deal" moment on a row.
func isDeal(it models.Item) bool {
return it.BestPrice != nil && it.TargetPrice != nil && *it.BestPrice <= *it.TargetPrice
} }
func itemsBody(d ItemsData) templ.Component { func itemsBody(d ItemsData) templ.Component {
@@ -59,7 +140,7 @@ func itemsBody(d ItemsData) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(c) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(c)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 28, Col: 23} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 109, Col: 23}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -82,7 +163,7 @@ func itemsBody(d ItemsData) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(c) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(c)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 28, Col: 67} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 109, Col: 67}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -104,12 +185,12 @@ func itemsBody(d ItemsData) templ.Component {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"v-card p-0 overflow-hidden\"><table class=\"v-table\"><thead><tr><th>Name</th><th>Category</th><th>Target</th><th>Best Price</th><th>Last Polled</th><th>Status</th><th></th></tr></thead> <tbody id=\"items-tbody\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"v-card p-0 overflow-hidden\"><table class=\"v-table\"><thead><tr><th>Name</th><th>Category</th><th>Target</th><th>Best Price</th><th>Trend</th><th>Last Polled</th><th>Status</th><th></th></tr></thead> <tbody id=\"items-tbody\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
for _, it := range d.Items { for _, it := range d.Items {
templ_7745c5c3_Err = itemRow(it, d.CSRFToken).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = itemRow(it, d.CSRFToken, d.PriceHistory[it.ID]).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -148,7 +229,7 @@ func itemsEmpty() templ.Component {
templ_7745c5c3_Var4 = templ.NopComponent templ_7745c5c3_Var4 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"v-card p-8 flex flex-col md:flex-row items-center gap-6\"><div class=\"v-veola-portrait w-48 shrink-0\"><img src=\"/static/img/veola.webp\" alt=\"Veola\"></div><div><h2 class=\"text-xl font-semibold mb-2\">Nothing on the watchlist.</h2><p class=\"v-muted mb-4\">Add an item and Veola will keep an eye on it.</p><a class=\"v-btn\" href=\"/items/new\">Add the first item</a></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"v-card p-8 flex flex-col md:flex-row items-center gap-6\"><div class=\"v-veola-portrait w-48 shrink-0\"><img src=\"/static/img/veola.avif\" alt=\"Veola\"></div><div><h2 class=\"text-xl font-semibold mb-2\">Nothing on the watchlist.</h2><p class=\"v-muted mb-4\">Add an item and Veola will keep an eye on it.</p><a class=\"v-btn\" href=\"/items/new\">Add the first item</a></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -156,7 +237,7 @@ func itemsEmpty() templ.Component {
}) })
} }
func itemRow(it models.Item, csrf string) templ.Component { func itemRow(it models.Item, csrf string, history []models.PricePoint) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -184,418 +265,532 @@ func itemRow(it models.Item, csrf string) templ.Component {
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("item-row-%d", it.ID)) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("item-row-%d", it.ID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 74, Col: 43} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 156, Col: 43}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"><td><a href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\"><td><div class=\"flex items-center gap-2\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var7 templ.SafeURL if isDeal(it) {
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/results", it.ID))) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<img class=\"v-deal-mascot\" src=\"/static/img/veola.avif\" alt=\"\" title=\"")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 76, Col: 67}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">") var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("Deal! Best %s ≤ target %s", fmtPrice(it.BestPrice, it.BestPriceCurrency), fmtPrice(it.TargetPrice, "USD")))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 160, Col: 197}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var8 string templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"> ")
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(it.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 76, Col: 79} return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<a href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/results", it.ID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 162, Col: 68}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a> ") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.LastPollError != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<button class=\"v-pill v-pill-error ml-2\" hx-get=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var9 string var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/error", it.ID)) templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(it.Name)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 78, Col: 91} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 162, Col: 80}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\" hx-target=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.LastPollError != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<button class=\"v-pill v-pill-error ml-2\" hx-get=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var10 string var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-error-%d", it.ID)) templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/error", it.ID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 78, Col: 142} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 165, Col: 91}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" hx-swap=\"innerHTML\">!</button><div id=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" hx-target=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var11 string var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("item-error-%d", it.ID)) templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-error-%d", it.ID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 79, Col: 49} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 165, Col: 142}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"v-error-text mt-1\"></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" hx-swap=\"innerHTML\">!</button><div id=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><td class=\"v-muted\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var12 string var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(it.Category) templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("item-error-%d", it.ID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 82, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 166, Col: 49}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</td><td class=\"font-mono\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"v-error-text mt-1\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td class=\"v-muted\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if it.TargetPrice != nil {
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(it.TargetPrice, "USD")) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(it.Category)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 85, Col: 37} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 169, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</td><td class=\"font-mono\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.TargetPrice != nil {
var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(it.TargetPrice, "USD"))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 172, Col: 37}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<span class=\"v-muted\">—</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"v-muted\">—</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if it.BestPrice != nil { if it.BestPrice != nil {
var templ_7745c5c3_Var14 = []any{"font-mono text-lg", priceClass(it.BestPrice, it.TargetPrice)} templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"flex items-center gap-1.5\">")
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"") var templ_7745c5c3_Var15 = []any{"font-mono text-lg", priceClass(it.BestPrice, it.TargetPrice)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var15 string templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<span class=\"")
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var14).String())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 1, Col: 0}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var16 string var templ_7745c5c3_Var16 string
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(it.BestPrice, it.BestPriceCurrency)) templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var15).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 92, Col: 127} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 1, Col: 0}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
if it.BestPriceURL != "" { var templ_7745c5c3_Var17 string
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<a class=\"text-xs\" href=\"") templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(it.BestPrice, it.BestPriceCurrency))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 180, Col: 129}
}
var templ_7745c5c3_Var17 templ.SafeURL
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(it.BestPriceURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 94, Col: 61}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" target=\"_blank\" rel=\"noopener\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</span> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var18 string if glyph, cls := trendArrow(history); glyph != "" {
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(it.BestPriceStore) var templ_7745c5c3_Var18 = []any{"v-trend", cls}
if templ_7745c5c3_Err != nil { templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 94, Col: 114}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<span class=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if it.BestPriceStore != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<span class=\"text-xs v-muted\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var19 string var templ_7745c5c3_Var19 string
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(it.BestPriceStore) templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 96, Col: 54} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 1, Col: 0}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<span class=\"v-muted\">not yet</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</td><td class=\"v-muted text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.LastPolledAt != nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<span title=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var20 string var templ_7745c5c3_Var20 string
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(it.LastPolledAt.Format("2006-01-02 15:04:05")) templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(glyph)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 104, Col: 63} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 182, Col: 44}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var21 string }
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(humanTime(*it.LastPolledAt)) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 104, Col: 95} return templ_7745c5c3_Err
}
if it.BestPriceURL != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<a class=\"text-xs\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var21 templ.SafeURL
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(it.BestPriceURL))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 186, Col: 61}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" target=\"_blank\" rel=\"noopener\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "—")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.Active {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<span class=\"v-pill v-pill-active\">active</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<span class=\"v-pill v-pill-paused\">paused</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</td><td class=\"text-right whitespace-nowrap\"><form class=\"inline\" hx-post=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var22 string var templ_7745c5c3_Var22 string
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/toggle", it.ID)) templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(it.BestPriceStore)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 117, Col: 72} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 186, Col: 114}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\" hx-target=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if it.BestPriceStore != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<span class=\"text-xs v-muted\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var23 string var templ_7745c5c3_Var23 string
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-row-%d", it.ID)) templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(it.BestPriceStore)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 117, Col: 121} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 188, Col: 54}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" hx-swap=\"outerHTML\"><input type=\"hidden\" name=\"csrf_token\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(csrf)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 118, Col: 55}
}
_, 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, "\"> <button class=\"v-btn-ghost text-sm\" type=\"submit\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.Active {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "Pause")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
} }
} else { } else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "Resume") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<span class=\"v-muted\">not yet</span>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</button></form><form class=\"inline\" hx-post=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if pts := sparklinePoints(history); pts != "" {
var templ_7745c5c3_Var24 = []any{"v-sparkline", sparklineTrendClass(history)}
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var24...)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<svg class=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var25 string var templ_7745c5c3_Var25 string
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/run", it.ID)) templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var24).String())
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 127, Col: 69} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 1, Col: 0}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\" hx-target=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\" viewBox=\"0 0 80 24\" width=\"80\" height=\"24\" aria-hidden=\"true\"><polyline points=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var26 string var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-row-%d", it.ID)) templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(pts)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 127, Col: 118} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 197, Col: 27}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\" hx-swap=\"outerHTML\" hx-disabled-elt=\"find button\"><input type=\"hidden\" name=\"csrf_token\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></polyline></svg>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<span class=\"v-muted text-xs\">—</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</td><td class=\"v-muted text-sm\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.LastPolledAt != nil {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<span title=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var27 string var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(csrf) templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(it.LastPolledAt.Format("2006-01-02 15:04:05"))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 128, Col: 55} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 205, Col: 63}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\"> <button class=\"v-btn-ghost text-sm\" type=\"submit\">Run Now <span class=\"v-spinner htmx-indicator ml-1\"></span></button></form><a class=\"v-btn-ghost text-sm\" href=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var28 templ.SafeURL var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/edit", it.ID))) templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(humanTime(*it.LastPolledAt))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 134, Col: 92} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 205, Col: 95}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "\">Edit</a><form class=\"inline\" hx-post=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "—")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.Active {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "<span class=\"v-pill v-pill-active\">active</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<span class=\"v-pill v-pill-paused\">paused</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</td><td class=\"text-right whitespace-nowrap\"><form class=\"inline\" hx-post=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var29 string var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/delete", it.ID)) templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/toggle", it.ID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 135, Col: 72} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 218, Col: 72}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\" hx-target=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "\" hx-target=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var30 string var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-row-%d", it.ID)) templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-row-%d", it.ID))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 135, Col: 121} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 218, Col: 121}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\" hx-swap=\"outerHTML\" hx-confirm=\"Delete this item?\"><input type=\"hidden\" name=\"csrf_token\" value=\"") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\" hx-swap=\"outerHTML\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
var templ_7745c5c3_Var31 string var templ_7745c5c3_Var31 string
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(csrf) templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(csrf)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 136, Col: 55} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 219, Col: 55}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"> <button class=\"v-btn-ghost text-sm\" type=\"submit\">Delete</button></form></td></tr>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "\"> <button class=\"v-btn-ghost text-sm\" type=\"submit\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if it.Active {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "Pause")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "Resume")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</button></form><form class=\"inline\" hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var32 string
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/run", it.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 228, Col: 69}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var33 string
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-row-%d", it.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 228, Col: 118}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\" hx-swap=\"outerHTML\" hx-disabled-elt=\"find button\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var34 string
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(csrf)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 229, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "\"> <button class=\"v-btn-ghost text-sm\" type=\"submit\">Run Now <span class=\"v-spinner htmx-indicator ml-1\"></span></button></form><a class=\"v-btn-ghost text-sm\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var35 templ.SafeURL
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/edit", it.ID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 235, Col: 92}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "\">Edit</a><form class=\"inline\" hx-post=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var36 string
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/items/%d/delete", it.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 236, Col: 72}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "\" hx-target=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var37 string
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("#item-row-%d", it.ID))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 236, Col: 121}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\" hx-swap=\"outerHTML\" hx-confirm=\"Delete this item?\"><input type=\"hidden\" name=\"csrf_token\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var38 string
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(csrf)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/items.templ`, Line: 237, Col: 55}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "\"> <button class=\"v-btn-ghost text-sm\" type=\"submit\">Delete</button></form></td></tr>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -629,9 +824,9 @@ func Items(d ItemsData) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var32 := templ.GetChildren(ctx) templ_7745c5c3_Var39 := templ.GetChildren(ctx)
if templ_7745c5c3_Var32 == nil { if templ_7745c5c3_Var39 == nil {
templ_7745c5c3_Var32 = templ.NopComponent templ_7745c5c3_Var39 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = Layout(d.Page, itemsBody(d)).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = Layout(d.Page, itemsBody(d)).Render(ctx, templ_7745c5c3_Buffer)
@@ -642,8 +837,10 @@ func Items(d ItemsData) templ.Component {
}) })
} }
// ItemRow renders a single row partial, used by HTMX endpoints. // ItemRow renders a single row partial, used by HTMX endpoints. Callers
func ItemRow(it models.Item, csrf string) templ.Component { // that don't have history cheaply on hand pass nil; the sparkline cell
// degrades to an em-dash placeholder.
func ItemRow(it models.Item, csrf string, history []models.PricePoint) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 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 templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -659,12 +856,12 @@ func ItemRow(it models.Item, csrf string) templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var33 := templ.GetChildren(ctx) templ_7745c5c3_Var40 := templ.GetChildren(ctx)
if templ_7745c5c3_Var33 == nil { if templ_7745c5c3_Var40 == nil {
templ_7745c5c3_Var33 = templ.NopComponent templ_7745c5c3_Var40 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = itemRow(it, csrf).Render(ctx, templ_7745c5c3_Buffer) templ_7745c5c3_Err = itemRow(it, csrf, history).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -689,12 +886,12 @@ func EmptyRow() templ.Component {
}() }()
} }
ctx = templ.InitializeContext(ctx) ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var34 := templ.GetChildren(ctx) templ_7745c5c3_Var41 := templ.GetChildren(ctx)
if templ_7745c5c3_Var34 == nil { if templ_7745c5c3_Var41 == nil {
templ_7745c5c3_Var34 = templ.NopComponent templ_7745c5c3_Var41 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<tr></tr>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "<tr></tr>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }

View File

@@ -23,6 +23,9 @@ templ head(title string) {
<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 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/tailwind.css"/>
<link rel="stylesheet" href="/static/css/app.css"/> <link rel="stylesheet" href="/static/css/app.css"/>
<!-- retro.css is an additive flair layer. Delete this <link> + the
file to revert to the plainer look. See retro.css for details. -->
<link rel="stylesheet" href="/static/css/retro.css"/>
<script src="/static/vendor/htmx.min.js" defer></script> <script src="/static/vendor/htmx.min.js" defer></script>
<script src="/static/js/flair.js" defer></script> <script src="/static/js/flair.js" defer></script>
</head> </head>
@@ -30,10 +33,10 @@ templ head(title string) {
templ Sidebar(active string) { templ Sidebar(active string) {
<nav class="v-side-nav flex flex-col"> <nav class="v-side-nav flex flex-col">
<div class="px-4 py-5 flex items-center gap-2"> <a href="/" class="v-side-brand px-4 py-5 flex items-center gap-2">
<span class="text-2xl">🐝</span> <span class="text-2xl">🐝</span>
<span class="text-xl font-semibold tracking-wide">Veola</span> <span class="text-xl font-semibold tracking-wide">Veola</span>
</div> </a>
<a href="/" class={ navClass("dashboard", active) }>Dashboard</a> <a href="/" class={ navClass("dashboard", active) }>Dashboard</a>
<a href="/items" class={ navClass("items", active) }>Items</a> <a href="/items" class={ navClass("items", active) }>Items</a>
<a href="/results" class={ navClass("results", active) }>Results</a> <a href="/results" class={ navClass("results", active) }>Results</a>

View File

@@ -55,7 +55,7 @@ func head(title string) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err 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><script src=\"/static/js/flair.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\"><!-- retro.css is an additive flair layer. Delete this <link> + the\n\t\t file to revert to the plainer look. See retro.css for details. --><link rel=\"stylesheet\" href=\"/static/css/retro.css\"><script src=\"/static/vendor/htmx.min.js\" defer></script><script src=\"/static/js/flair.js\" defer></script></head>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -84,7 +84,7 @@ func Sidebar(active string) templ.Component {
templ_7745c5c3_Var3 = templ.NopComponent templ_7745c5c3_Var3 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<nav class=\"v-side-nav flex flex-col\"><div class=\"px-4 py-5 flex items-center gap-2\"><span class=\"text-2xl\">🐝</span> <span class=\"text-xl font-semibold tracking-wide\">Veola</span></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<nav class=\"v-side-nav flex flex-col\"><a href=\"/\" class=\"v-side-brand px-4 py-5 flex items-center gap-2\"><span class=\"text-2xl\">🐝</span> <span class=\"text-xl font-semibold tracking-wide\">Veola</span></a> ")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -236,7 +236,7 @@ func Layout(p Page, body templ.Component) templ.Component {
var templ_7745c5c3_Var13 string var templ_7745c5c3_Var13 string
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.Flash) templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(p.Flash)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 62, Col: 35} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 65, Col: 35}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -255,7 +255,7 @@ func Layout(p Page, body templ.Component) templ.Component {
var templ_7745c5c3_Var14 string var templ_7745c5c3_Var14 string
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(p.FlashError) templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(p.FlashError)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 65, Col: 46} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 68, Col: 46}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -353,7 +353,7 @@ func CSRFInput(token string) templ.Component {
var templ_7745c5c3_Var17 string var templ_7745c5c3_Var17 string
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(token) templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(token)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 86, Col: 53} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/layout.templ`, Line: 89, Col: 53}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {

View File

@@ -7,13 +7,14 @@ type LoginData struct {
} }
templ loginBody(d LoginData) { templ loginBody(d LoginData) {
<div class="min-h-screen grid md:grid-cols-2"> <div class="min-h-screen grid md:grid-cols-2 v-auth-grid">
<div class="flex items-center justify-center p-8"> <div class="flex items-center justify-center p-8">
<div class="w-full max-w-sm"> <div class="w-full max-w-sm">
<div class="flex items-center gap-2 mb-6"> <div class="flex items-center gap-2 mb-8 v-auth-wordmark">
<span class="text-3xl">🐝</span> <span class="text-3xl">🐝</span>
<span class="text-2xl font-semibold tracking-wide">Veola</span> <span class="text-2xl font-semibold tracking-wide">Veola</span>
</div> </div>
<div class="v-card v-auth-card p-7">
<h1 class="text-3xl font-semibold mb-2">Open the door.</h1> <h1 class="text-3xl font-semibold mb-2">Open the door.</h1>
<p class="v-muted mb-6">Sign in to continue.</p> <p class="v-muted mb-6">Sign in to continue.</p>
if d.Error != "" { if d.Error != "" {
@@ -32,10 +33,14 @@ templ loginBody(d LoginData) {
<button class="v-btn w-full justify-center" type="submit">Sign In</button> <button class="v-btn w-full justify-center" type="submit">Sign In</button>
</form> </form>
</div> </div>
<p class="v-auth-tagline">Track · Watch · Notice</p>
</div> </div>
<div class="hidden md:flex items-end justify-center p-8 bg-[#152560]"> </div>
<div class="hidden md:flex flex-col items-center justify-center p-8 v-auth-portrait-col">
<div class="v-auth-portrait-halo">
<div class="v-veola-portrait max-w-md"> <div class="v-veola-portrait max-w-md">
<img src="/static/img/veola.webp" alt="Veola"/> <img src="/static/img/veola.avif" alt="Veola"/>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -52,13 +57,14 @@ type SetupData struct {
} }
templ setupBody(d SetupData) { templ setupBody(d SetupData) {
<div class="min-h-screen grid md:grid-cols-2"> <div class="min-h-screen grid md:grid-cols-2 v-auth-grid">
<div class="flex items-center justify-center p-8"> <div class="flex items-center justify-center p-8">
<div class="w-full max-w-sm"> <div class="w-full max-w-sm">
<div class="flex items-center gap-2 mb-6"> <div class="flex items-center gap-2 mb-8 v-auth-wordmark">
<span class="text-3xl">🐝</span> <span class="text-3xl">🐝</span>
<span class="text-2xl font-semibold tracking-wide">Veola</span> <span class="text-2xl font-semibold tracking-wide">Veola</span>
</div> </div>
<div class="v-card v-auth-card p-7">
<h1 class="text-3xl font-semibold mb-2">First time here.</h1> <h1 class="text-3xl font-semibold mb-2">First time here.</h1>
<p class="v-muted mb-6">Create the admin account. Password must be at least 12 characters.</p> <p class="v-muted mb-6">Create the admin account. Password must be at least 12 characters.</p>
if d.Error != "" { if d.Error != "" {
@@ -81,10 +87,14 @@ templ setupBody(d SetupData) {
<button class="v-btn w-full justify-center" type="submit">Create Admin</button> <button class="v-btn w-full justify-center" type="submit">Create Admin</button>
</form> </form>
</div> </div>
<p class="v-auth-tagline">Track · Watch · Notice</p>
</div> </div>
<div class="hidden md:flex items-end justify-center p-8 bg-[#152560]"> </div>
<div class="hidden md:flex flex-col items-center justify-center p-8 v-auth-portrait-col">
<div class="v-auth-portrait-halo">
<div class="v-veola-portrait max-w-md"> <div class="v-veola-portrait max-w-md">
<img src="/static/img/veola.webp" alt="Veola"/> <img src="/static/img/veola.avif" alt="Veola"/>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -35,7 +35,7 @@ func loginBody(d LoginData) templ.Component {
templ_7745c5c3_Var1 = templ.NopComponent templ_7745c5c3_Var1 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"min-h-screen grid md:grid-cols-2\"><div class=\"flex items-center justify-center p-8\"><div class=\"w-full max-w-sm\"><div class=\"flex items-center gap-2 mb-6\"><span class=\"text-3xl\">🐝</span> <span class=\"text-2xl font-semibold tracking-wide\">Veola</span></div><h1 class=\"text-3xl font-semibold mb-2\">Open the door.</h1><p class=\"v-muted mb-6\">Sign in to continue.</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"min-h-screen grid md:grid-cols-2 v-auth-grid\"><div class=\"flex items-center justify-center p-8\"><div class=\"w-full max-w-sm\"><div class=\"flex items-center gap-2 mb-8 v-auth-wordmark\"><span class=\"text-3xl\">🐝</span> <span class=\"text-2xl font-semibold tracking-wide\">Veola</span></div><div class=\"v-card v-auth-card p-7\"><h1 class=\"text-3xl font-semibold mb-2\">Open the door.</h1><p class=\"v-muted mb-6\">Sign in to continue.</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -47,7 +47,7 @@ func loginBody(d LoginData) templ.Component {
var templ_7745c5c3_Var2 string var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(d.Error) templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(d.Error)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 20, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 21, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -73,13 +73,13 @@ func loginBody(d LoginData) templ.Component {
var templ_7745c5c3_Var3 string var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Username) templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 26, Col: 97} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 27, Col: 98}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></div><div><label class=\"v-label\">Password</label> <input class=\"v-input\" type=\"password\" name=\"password\" autocomplete=\"current-password\"></div><button class=\"v-btn w-full justify-center\" type=\"submit\">Sign In</button></form></div></div><div class=\"hidden md:flex items-end justify-center p-8 bg-[#152560]\"><div class=\"v-veola-portrait max-w-md\"><img src=\"/static/img/veola.webp\" alt=\"Veola\"></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\"></div><div><label class=\"v-label\">Password</label> <input class=\"v-input\" type=\"password\" name=\"password\" autocomplete=\"current-password\"></div><button class=\"v-btn w-full justify-center\" type=\"submit\">Sign In</button></form></div><p class=\"v-auth-tagline\">Track · Watch · Notice</p></div></div><div class=\"hidden md:flex flex-col items-center justify-center p-8 v-auth-portrait-col\"><div class=\"v-auth-portrait-halo\"><div class=\"v-veola-portrait max-w-md\"><img src=\"/static/img/veola.avif\" alt=\"Veola\"></div></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -143,7 +143,7 @@ func setupBody(d SetupData) templ.Component {
templ_7745c5c3_Var5 = templ.NopComponent templ_7745c5c3_Var5 = templ.NopComponent
} }
ctx = templ.ClearChildren(ctx) ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"min-h-screen grid md:grid-cols-2\"><div class=\"flex items-center justify-center p-8\"><div class=\"w-full max-w-sm\"><div class=\"flex items-center gap-2 mb-6\"><span class=\"text-3xl\">🐝</span> <span class=\"text-2xl font-semibold tracking-wide\">Veola</span></div><h1 class=\"text-3xl font-semibold mb-2\">First time here.</h1><p class=\"v-muted mb-6\">Create the admin account. Password must be at least 12 characters.</p>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"min-h-screen grid md:grid-cols-2 v-auth-grid\"><div class=\"flex items-center justify-center p-8\"><div class=\"w-full max-w-sm\"><div class=\"flex items-center gap-2 mb-8 v-auth-wordmark\"><span class=\"text-3xl\">🐝</span> <span class=\"text-2xl font-semibold tracking-wide\">Veola</span></div><div class=\"v-card v-auth-card p-7\"><h1 class=\"text-3xl font-semibold mb-2\">First time here.</h1><p class=\"v-muted mb-6\">Create the admin account. Password must be at least 12 characters.</p>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
@@ -155,7 +155,7 @@ func setupBody(d SetupData) templ.Component {
var templ_7745c5c3_Var6 string var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(d.Error) templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(d.Error)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 65, Col: 41} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 71, Col: 42}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
@@ -181,13 +181,13 @@ func setupBody(d SetupData) templ.Component {
var templ_7745c5c3_Var7 string var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Username) templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(d.Username)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 71, Col: 73} return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 77, Col: 74}
} }
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7) _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"></div><div><label class=\"v-label\">Password</label> <input class=\"v-input\" type=\"password\" name=\"password\"></div><div><label class=\"v-label\">Confirm Password</label> <input class=\"v-input\" type=\"password\" name=\"password_confirm\"></div><button class=\"v-btn w-full justify-center\" type=\"submit\">Create Admin</button></form></div></div><div class=\"hidden md:flex items-end justify-center p-8 bg-[#152560]\"><div class=\"v-veola-portrait max-w-md\"><img src=\"/static/img/veola.webp\" alt=\"Veola\"></div></div></div>") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\"></div><div><label class=\"v-label\">Password</label> <input class=\"v-input\" type=\"password\" name=\"password\"></div><div><label class=\"v-label\">Confirm Password</label> <input class=\"v-input\" type=\"password\" name=\"password_confirm\"></div><button class=\"v-btn w-full justify-center\" type=\"submit\">Create Admin</button></form></div><p class=\"v-auth-tagline\">Track · Watch · Notice</p></div></div><div class=\"hidden md:flex flex-col items-center justify-center p-8 v-auth-portrait-col\"><div class=\"v-auth-portrait-halo\"><div class=\"v-veola-portrait max-w-md\"><img src=\"/static/img/veola.avif\" alt=\"Veola\"></div></div></div></div>")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }