Redesign dashboard: refined-minimal layout with product thumbnails
Move the dashboard from boxed glass cards to an airy, hairline-separated layout where type and whitespace carry the page, then surface listing images as the visual anchor. - Solid surfaces (drop backdrop-filter glass, per spec); calm flat field with a single cool top-glow replacing the drifting aurora and dot-grid. - Recent results / recent alerts are now product feeds with 52px thumbnails, listing titles, and price; image_url threaded through ResultRow/AlertRow (data already stored, just unsurfaced). - ListResults gains an OnlyAlerted filter so the alerts panel reuses its decryption path instead of a raw query. - Dashboard recent results capped at 8 for a balanced snapshot.
This commit is contained in:
6
.gitignore
vendored
6
.gitignore
vendored
@@ -23,3 +23,9 @@ config.toml
|
|||||||
# Debug log output from `-debug` runs
|
# Debug log output from `-debug` runs
|
||||||
veola-debug.log
|
veola-debug.log
|
||||||
*.log
|
*.log
|
||||||
|
|
||||||
|
# dev screenshot + throwaway preview artifacts
|
||||||
|
.shot_*
|
||||||
|
.shots_*
|
||||||
|
config_shot.toml
|
||||||
|
veola_shot.db*
|
||||||
|
|||||||
@@ -791,6 +791,8 @@ type ResultsQuery struct {
|
|||||||
// Exception: yahoo-auctions-jp is auction-only — NULL ends_at there means
|
// Exception: yahoo-auctions-jp is auction-only — NULL ends_at there means
|
||||||
// the actor's ending_date didn't parse, and those rows are also dropped.
|
// the actor's ending_date didn't parse, and those rows are also dropped.
|
||||||
ExcludeEnded bool
|
ExcludeEnded bool
|
||||||
|
// OnlyAlerted restricts to rows that triggered a deal alert.
|
||||||
|
OnlyAlerted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) ListResults(ctx context.Context, q ResultsQuery) ([]models.Result, error) {
|
func (s *Store) ListResults(ctx context.Context, q ResultsQuery) ([]models.Result, error) {
|
||||||
@@ -817,6 +819,9 @@ func (s *Store) ListResults(ctx context.Context, q ResultsQuery) ([]models.Resul
|
|||||||
conds = append(conds, `((ends_at IS NULL AND source != 'yahoo-auctions-jp') OR ends_at > ?)`)
|
conds = append(conds, `((ends_at IS NULL AND source != 'yahoo-auctions-jp') OR ends_at > ?)`)
|
||||||
args = append(args, time.Now().UTC())
|
args = append(args, time.Now().UTC())
|
||||||
}
|
}
|
||||||
|
if q.OnlyAlerted {
|
||||||
|
conds = append(conds, `alerted = 1`)
|
||||||
|
}
|
||||||
where := ""
|
where := ""
|
||||||
if len(conds) > 0 {
|
if len(conds) > 0 {
|
||||||
where = `WHERE ` + strings.Join(conds, ` AND `)
|
where = `WHERE ` + strings.Join(conds, ` AND `)
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
|
||||||
|
|
||||||
"veola/internal/db"
|
"veola/internal/db"
|
||||||
"veola/templates"
|
"veola/templates"
|
||||||
@@ -36,7 +34,7 @@ func (a *App) dashboardData(r *http.Request) (templates.DashboardData, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return templates.DashboardData{}, err
|
return templates.DashboardData{}, err
|
||||||
}
|
}
|
||||||
results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{Limit: 20, ExcludeEnded: true})
|
results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{Limit: 8, ExcludeEnded: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return templates.DashboardData{}, err
|
return templates.DashboardData{}, err
|
||||||
}
|
}
|
||||||
@@ -55,6 +53,7 @@ func (a *App) dashboardData(r *http.Request) (templates.DashboardData, error) {
|
|||||||
Currency: r.Currency,
|
Currency: r.Currency,
|
||||||
Source: r.Source,
|
Source: r.Source,
|
||||||
URL: r.URL,
|
URL: r.URL,
|
||||||
|
ImageURL: r.ImageURL,
|
||||||
FoundAt: r.FoundAt,
|
FoundAt: r.FoundAt,
|
||||||
Alerted: r.Alerted,
|
Alerted: r.Alerted,
|
||||||
})
|
})
|
||||||
@@ -80,36 +79,20 @@ func (a *App) dashboardData(r *http.Request) (templates.DashboardData, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func alertsRecent(a *App, r *http.Request, itemNames map[int64]string) ([]templates.AlertRow, error) {
|
func alertsRecent(a *App, r *http.Request, itemNames map[int64]string) ([]templates.AlertRow, error) {
|
||||||
rows, err := a.Store.DB.QueryContext(r.Context(), `
|
results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{OnlyAlerted: true, Limit: 5})
|
||||||
SELECT item_id, price, currency, found_at FROM results
|
|
||||||
WHERE alerted = 1 ORDER BY found_at DESC LIMIT 5
|
|
||||||
`)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
out := make([]templates.AlertRow, 0, len(results))
|
||||||
var out []templates.AlertRow
|
for _, res := range results {
|
||||||
for rows.Next() {
|
|
||||||
var (
|
|
||||||
itemID int64
|
|
||||||
price sql.NullFloat64
|
|
||||||
currency string
|
|
||||||
foundAt time.Time
|
|
||||||
)
|
|
||||||
if err := rows.Scan(&itemID, &price, ¤cy, &foundAt); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var p *float64
|
|
||||||
if price.Valid {
|
|
||||||
v := price.Float64
|
|
||||||
p = &v
|
|
||||||
}
|
|
||||||
out = append(out, templates.AlertRow{
|
out = append(out, templates.AlertRow{
|
||||||
ItemName: itemNames[itemID],
|
ItemName: itemNames[res.ItemID],
|
||||||
Price: p,
|
Title: res.Title,
|
||||||
Currency: currency,
|
Price: res.Price,
|
||||||
FoundAt: foundAt,
|
Currency: res.Currency,
|
||||||
|
ImageURL: res.ImageURL,
|
||||||
|
FoundAt: res.FoundAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return out, rows.Err()
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* Veola — Sega-blue palette and component overrides for Tailwind play CDN. */
|
/* Veola — Sega-blue palette and component overrides for Tailwind play CDN. */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bg: #1a2b6d;
|
--bg: #16224f;
|
||||||
--surface: #1f3380;
|
--surface: #243a93;
|
||||||
--surface-2: #243a93;
|
--surface-2: #2c47ad;
|
||||||
--accent: #00a4e4;
|
--accent: #00a4e4;
|
||||||
--yellow: #f5c400;
|
--yellow: #f5c400;
|
||||||
--text: #ffffff;
|
--text: #ffffff;
|
||||||
@@ -25,59 +25,175 @@ html, body {
|
|||||||
a { color: var(--accent); }
|
a { color: var(--accent); }
|
||||||
a:hover { text-decoration: underline; }
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
|
/* Solid surfaces, per the spec's "blue mode": no glassmorphism, no blur. A
|
||||||
|
clear value gap between --bg and --surface is what lifts cards off the
|
||||||
|
page; a 1px top highlight + soft blue shadow gives the cartridge edge. */
|
||||||
.v-card {
|
.v-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
background:
|
background: var(--surface);
|
||||||
linear-gradient(180deg, rgba(36, 58, 147, 0.82), rgba(31, 51, 128, 0.82));
|
|
||||||
backdrop-filter: blur(10px) saturate(140%);
|
|
||||||
-webkit-backdrop-filter: blur(10px) saturate(140%);
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 12px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: 0 1px 2px rgba(0, 0, 40, 0.25);
|
||||||
transition:
|
transition:
|
||||||
transform 180ms ease,
|
|
||||||
box-shadow 180ms ease,
|
box-shadow 180ms ease,
|
||||||
border-color 180ms ease;
|
border-color 180ms ease;
|
||||||
}
|
}
|
||||||
.v-card::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
border-radius: inherit;
|
|
||||||
padding: 1px;
|
|
||||||
background: linear-gradient(135deg,
|
|
||||||
rgba(0, 164, 228, 0.65) 0%,
|
|
||||||
rgba(245, 196, 0, 0.30) 45%,
|
|
||||||
rgba(255, 255, 255, 0.04) 100%);
|
|
||||||
-webkit-mask:
|
|
||||||
linear-gradient(#000 0 0) content-box,
|
|
||||||
linear-gradient(#000 0 0);
|
|
||||||
-webkit-mask-composite: xor;
|
|
||||||
mask-composite: exclude;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
.v-card:hover {
|
.v-card:hover {
|
||||||
transform: translateY(-2px);
|
border-color: rgba(255, 255, 255, 0.2);
|
||||||
box-shadow:
|
box-shadow: 0 2px 8px rgba(0, 0, 40, 0.3);
|
||||||
0 10px 28px rgba(0, 0, 80, 0.55),
|
|
||||||
0 0 0 1px rgba(0, 164, 228, 0.30);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-card-flat {
|
.v-card-flat {
|
||||||
position: relative;
|
position: relative;
|
||||||
isolation: isolate;
|
isolation: isolate;
|
||||||
background:
|
background: var(--surface);
|
||||||
linear-gradient(180deg, rgba(36, 58, 147, 0.70), rgba(31, 51, 128, 0.70));
|
|
||||||
backdrop-filter: blur(8px) saturate(130%);
|
|
||||||
-webkit-backdrop-filter: blur(8px) saturate(130%);
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-divider { border-top: 1px solid var(--border); }
|
.v-divider { border-top: 1px solid var(--border); }
|
||||||
|
|
||||||
|
/* --- Console readout / page header system -------------------------------
|
||||||
|
The dashboard's signature: a mono "terminal" kicker over a heavy display
|
||||||
|
title, and the Spend/Saved pair styled as an arcade scoreboard readout.
|
||||||
|
JetBrains Mono is the machine voice here (labels, counts); Outfit carries
|
||||||
|
the title. */
|
||||||
|
|
||||||
|
.v-kicker {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.v-kicker .v-kicker-sep { color: var(--text-2); opacity: 0.6; }
|
||||||
|
.v-kicker .v-kicker-dim { color: var(--text-2); }
|
||||||
|
|
||||||
|
/* --- Refined header + metric system -------------------------------------
|
||||||
|
Calm and spacious: type and whitespace carry the page. No glow, no
|
||||||
|
uppercase-mono labels, no accent rules. Mono is reserved for the numbers
|
||||||
|
themselves; Outfit carries everything else. */
|
||||||
|
|
||||||
|
.v-page-title {
|
||||||
|
font-family: 'Outfit', system-ui, sans-serif;
|
||||||
|
font-size: 1.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.015em;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
.v-page-sub {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hero metric: a small quiet label over a large clean number. Presented in
|
||||||
|
open space with a hairline, not a boxed card. */
|
||||||
|
.v-metric-label {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.v-metric-value {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
font-size: clamp(2rem, 3.8vw, 2.75rem);
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.05;
|
||||||
|
margin-top: 0.45rem;
|
||||||
|
}
|
||||||
|
.v-metric-value.v-price-deal { color: var(--success); }
|
||||||
|
.v-metric-sub {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary stat: smaller label/number pair for the supporting counts. */
|
||||||
|
.v-stat-label {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.v-stat-value {
|
||||||
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Understated section heading for list panels — sized to sit below the page
|
||||||
|
title, sentence case, no glow. */
|
||||||
|
.v-section-title {
|
||||||
|
font-family: 'Outfit', system-ui, sans-serif;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--text);
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hairline used to separate open sections instead of boxing them. */
|
||||||
|
.v-rule {
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quiet empty-state line. */
|
||||||
|
.v-empty {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Product feed (thumbnails) ------------------------------------------
|
||||||
|
Listing thumbnails are what give the data life. A square cropped image,
|
||||||
|
the listing title as the line that earns its space, price on the right. */
|
||||||
|
.v-thumb {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
flex: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
object-fit: cover;
|
||||||
|
background: var(--surface-2);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.v-thumb-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-feed { display: block; }
|
||||||
|
.v-feed-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.85rem;
|
||||||
|
padding: 0.7rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
.v-feed-row:last-child { border-bottom: 0; }
|
||||||
|
.v-feed-name {
|
||||||
|
display: block;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
a.v-feed-name:hover { color: var(--accent); text-decoration: none; }
|
||||||
|
.v-feed-meta {
|
||||||
|
color: var(--text-2);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
}
|
||||||
|
.v-feed-price { flex: none; white-space: nowrap; }
|
||||||
|
|
||||||
.v-btn {
|
.v-btn {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: white;
|
color: white;
|
||||||
@@ -212,21 +328,22 @@ a:hover { text-decoration: underline; }
|
|||||||
table.v-table { width: 100%; border-collapse: collapse; }
|
table.v-table { width: 100%; border-collapse: collapse; }
|
||||||
table.v-table th {
|
table.v-table th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-size: 0.75rem;
|
font-size: 0.74rem;
|
||||||
letter-spacing: 0.05em;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
letter-spacing: 0.01em;
|
||||||
color: var(--text-2);
|
color: var(--text-2);
|
||||||
padding: 0.6rem 0.75rem;
|
padding: 0.5rem 0.75rem 0.7rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
table.v-table td {
|
table.v-table td {
|
||||||
padding: 0.7rem 0.75rem;
|
padding: 0.7rem 0.75rem;
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
/* No zebra — hairline rows and a faint hover keep it calm but scannable. */
|
||||||
table.v-table td { transition: background 140ms ease; }
|
table.v-table td { transition: background 140ms ease; }
|
||||||
table.v-table tr { transition: transform 140ms ease; }
|
table.v-table tbody tr:last-child td { border-bottom: 0; }
|
||||||
table.v-table tbody tr:hover td { background: rgba(0, 164, 228, 0.08); }
|
table.v-table tbody tr:hover td { background: rgba(255, 255, 255, 0.04); }
|
||||||
|
|
||||||
.v-error-text { color: var(--danger); font-size: 0.85rem; }
|
.v-error-text { color: var(--danger); font-size: 0.85rem; }
|
||||||
.v-muted { color: var(--text-2); }
|
.v-muted { color: var(--text-2); }
|
||||||
|
|||||||
@@ -23,17 +23,20 @@ body {
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
background-color: var(--bg);
|
background-color: var(--bg);
|
||||||
/* Static dot-grid baked into the root background so the two pseudo-element
|
/* Refined direction: dot-grid and drifting blobs removed. A single calm,
|
||||||
slots stay free for the animated blobs. */
|
cool top-glow gradient gives the field depth without reading as a smudge
|
||||||
|
or animating in the user's peripheral vision. */
|
||||||
background-image: radial-gradient(
|
background-image: radial-gradient(
|
||||||
circle,
|
120% 75% at 50% -15%,
|
||||||
rgba(255, 255, 255, 0.045) 1px,
|
#1f3074 0%,
|
||||||
transparent 1px
|
var(--bg) 58%
|
||||||
);
|
);
|
||||||
background-size: 26px 26px;
|
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Retire the animated aurora blobs for the refined look. */
|
||||||
|
html::before, html::after { display: none; }
|
||||||
|
|
||||||
/* Two aurora blobs animated with seven waypoints each (no `alternate`, so
|
/* Two aurora blobs animated with seven waypoints each (no `alternate`, so
|
||||||
the second half doesn't mirror the first) on incommensurate periods —
|
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
|
the two cycles drift in and out of phase, so the eye never locks onto a
|
||||||
@@ -48,13 +51,13 @@ html::before {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
closest-side,
|
closest-side,
|
||||||
rgba(0, 164, 228, 0.60),
|
rgba(0, 164, 228, 0.13),
|
||||||
rgba(0, 164, 228, 0.0) 70%
|
rgba(0, 164, 228, 0.0) 70%
|
||||||
);
|
);
|
||||||
width: 70vmax;
|
width: 60vmax;
|
||||||
height: 70vmax;
|
height: 60vmax;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
filter: blur(60px);
|
filter: blur(70px);
|
||||||
/* Centered base; keyframes drive all motion from here. */
|
/* Centered base; keyframes drive all motion from here. */
|
||||||
top: 50%; left: 50%;
|
top: 50%; left: 50%;
|
||||||
margin-top: -35vmax; margin-left: -35vmax;
|
margin-top: -35vmax; margin-left: -35vmax;
|
||||||
@@ -68,15 +71,17 @@ html::after {
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: -2;
|
z-index: -2;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
/* Cooled to a faint mint instead of arcade yellow: the warm blob read as a
|
||||||
|
smudge against the cold palette. Kept subtle so it's ambient, not a wash. */
|
||||||
background: radial-gradient(
|
background: radial-gradient(
|
||||||
closest-side,
|
closest-side,
|
||||||
rgba(245, 196, 0, 0.50),
|
rgba(0, 228, 164, 0.07),
|
||||||
rgba(245, 196, 0, 0.0) 70%
|
rgba(0, 228, 164, 0.0) 70%
|
||||||
);
|
);
|
||||||
width: 55vmax;
|
width: 46vmax;
|
||||||
height: 55vmax;
|
height: 46vmax;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
filter: blur(70px);
|
filter: blur(80px);
|
||||||
top: 50%; left: 50%;
|
top: 50%; left: 50%;
|
||||||
margin-top: -27.5vmax; margin-left: -27.5vmax;
|
margin-top: -27.5vmax; margin-left: -27.5vmax;
|
||||||
animation: v-retro-drift-b 29s cubic-bezier(.5,.2,.3,.8) infinite;
|
animation: v-retro-drift-b 29s cubic-bezier(.5,.2,.3,.8) infinite;
|
||||||
@@ -104,13 +109,11 @@ html::after {
|
|||||||
100% { transform: translate3d( 28vw, -25vh, 0) scale(1.00); opacity: 0.70; }
|
100% { transform: translate3d( 28vw, -25vh, 0) scale(1.00); opacity: 0.70; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Display headings: heavier, slightly tighter, with an accent glow that ties
|
/* Refined direction: headings stay clean — no glow, modest weight. The
|
||||||
into the card border gradient. Class-free selectors so existing Tailwind
|
page-title/section-title classes in app.css own the real hierarchy. */
|
||||||
utilities (text-3xl etc.) stack on top untouched. */
|
|
||||||
h1, h2 {
|
h1, h2 {
|
||||||
font-weight: 800;
|
font-weight: 600;
|
||||||
letter-spacing: -0.02em;
|
letter-spacing: -0.01em;
|
||||||
text-shadow: 0 2px 14px rgba(0, 164, 228, 0.30);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -56,14 +56,17 @@ type ResultRow struct {
|
|||||||
Currency string
|
Currency string
|
||||||
Source string
|
Source string
|
||||||
URL string
|
URL string
|
||||||
|
ImageURL string
|
||||||
FoundAt time.Time
|
FoundAt time.Time
|
||||||
Alerted bool
|
Alerted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertRow struct {
|
type AlertRow struct {
|
||||||
ItemName string
|
ItemName string
|
||||||
|
Title string
|
||||||
Price *float64
|
Price *float64
|
||||||
Currency string
|
Currency string
|
||||||
|
ImageURL string
|
||||||
FoundAt time.Time
|
FoundAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,64 +77,71 @@ type AlertRow struct {
|
|||||||
// Layout would inject a nested page (and a duplicate sidebar) into the div.
|
// Layout would inject a nested page (and a duplicate sidebar) into the div.
|
||||||
templ DashboardBody(d DashboardData) {
|
templ DashboardBody(d DashboardData) {
|
||||||
<div hx-get="/dashboard/refresh" hx-trigger="every 60s" hx-swap="outerHTML">
|
<div hx-get="/dashboard/refresh" hx-trigger="every 60s" hx-swap="outerHTML">
|
||||||
<h1 class="text-3xl font-semibold mb-6">Dashboard</h1>
|
<header class="mb-10">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
<h1 class="v-page-title">Dashboard</h1>
|
||||||
@statCard("Total Items", fmt.Sprintf("%d", d.Stats.TotalItems), "")
|
<p class="v-page-sub">{ fmt.Sprintf("Watching %d items · %d active", d.Stats.TotalItems, d.Stats.ActiveItems) }</p>
|
||||||
@statCard("Active", fmt.Sprintf("%d", d.Stats.ActiveItems), "")
|
</header>
|
||||||
@statCard("Results Today", fmt.Sprintf("%d", d.Stats.ResultsToday), "")
|
<div class="grid md:grid-cols-2 gap-8 mb-8">
|
||||||
@statCard("Alerts Today", fmt.Sprintf("%d", d.Stats.AlertsToday), "")
|
<div>
|
||||||
</div>
|
<div class="v-metric-label">Potential spend</div>
|
||||||
<div class="grid md:grid-cols-2 gap-4 mb-6">
|
<div class="v-metric-value" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.PotentialSpend) }</div>
|
||||||
<div class="v-card p-5">
|
<div class="v-metric-sub">
|
||||||
<div class="v-muted text-sm uppercase tracking-wide">Potential Spend</div>
|
{ fmt.Sprintf("across %d priced items", d.Stats.PricedItemCount) }
|
||||||
<div class="font-mono text-4xl mt-2" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.PotentialSpend) }</div>
|
if d.Stats.UnpricedCount > 0 {
|
||||||
<div class="v-muted text-sm mt-1">across { fmt.Sprintf("%d", d.Stats.PricedItemCount) } items</div>
|
{ fmt.Sprintf(" · %d not yet priced", d.Stats.UnpricedCount) }
|
||||||
if d.Stats.UnpricedCount > 0 {
|
}
|
||||||
<div class="v-muted text-xs mt-1">{ fmt.Sprintf("%d items not yet priced.", d.Stats.UnpricedCount) }</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="v-card p-5">
|
<div>
|
||||||
<div class="v-muted text-sm uppercase tracking-wide">Money Saved</div>
|
<div class="v-metric-label">Money saved</div>
|
||||||
<div class="font-mono text-4xl mt-2 v-price-deal" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.MoneySaved) }</div>
|
<div class="v-metric-value v-price-deal" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.MoneySaved) }</div>
|
||||||
<div class="v-muted text-sm mt-1">across { fmt.Sprintf("%d", d.Stats.SavedItemCount) } items, vs 30-day average</div>
|
<div class="v-metric-sub">{ fmt.Sprintf("across %d items, vs 30-day average", d.Stats.SavedItemCount) }</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="v-card p-5 mb-6">
|
<hr class="v-rule mb-8"/>
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 mb-8">
|
||||||
<h2 class="font-semibold">API Usage and Budget</h2>
|
@statBlock("Total items", fmt.Sprintf("%d", d.Stats.TotalItems))
|
||||||
|
@statBlock("Active", fmt.Sprintf("%d", d.Stats.ActiveItems))
|
||||||
|
@statBlock("Results today", fmt.Sprintf("%d", d.Stats.ResultsToday))
|
||||||
|
@statBlock("Alerts today", fmt.Sprintf("%d", d.Stats.AlertsToday))
|
||||||
|
</div>
|
||||||
|
<hr class="v-rule mb-8"/>
|
||||||
|
<section class="mb-10">
|
||||||
|
<div class="flex items-baseline justify-between mb-5">
|
||||||
|
<h2 class="v-section-title">API usage and budget</h2>
|
||||||
<span class="v-muted text-xs">Apify runs cost credits. Visible to everyone.</span>
|
<span class="v-muted text-xs">Apify runs cost credits. Visible to everyone.</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div class="v-muted text-xs uppercase tracking-wide">Apify (month)</div>
|
<div class="v-stat-label">Apify (month)</div>
|
||||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("%d", d.ApifyMonth) }</div>
|
<div class="v-stat-value">{ fmt.Sprintf("%d", d.ApifyMonth) }</div>
|
||||||
if d.ApifyCostPerCall > 0 {
|
if d.ApifyCostPerCall > 0 {
|
||||||
<div class="v-muted text-xs mt-1">{ fmt.Sprintf("~$%.2f est.", d.ApifyMonthCost()) }</div>
|
<div class="v-metric-sub">{ fmt.Sprintf("~$%.2f estimated", d.ApifyMonthCost()) }</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="v-muted text-xs uppercase tracking-wide">Apify (today)</div>
|
<div class="v-stat-label">Apify (today)</div>
|
||||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("%d", d.ApifyToday) }</div>
|
<div class="v-stat-value">{ fmt.Sprintf("%d", d.ApifyToday) }</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="v-muted text-xs uppercase tracking-wide">eBay (today)</div>
|
<div class="v-stat-label">eBay (today)</div>
|
||||||
if d.EbayLimit > 0 {
|
if d.EbayLimit > 0 {
|
||||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("%d / %d", d.EbayToday, d.EbayLimit) }</div>
|
<div class="v-stat-value">{ fmt.Sprintf("%d / %d", d.EbayToday, d.EbayLimit) }</div>
|
||||||
} else {
|
} else {
|
||||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("%d", d.EbayToday) }</div>
|
<div class="v-stat-value">{ fmt.Sprintf("%d", d.EbayToday) }</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
if d.MonthlyBudget > 0 {
|
if d.MonthlyBudget > 0 {
|
||||||
<div>
|
<div>
|
||||||
<div class="v-muted text-xs uppercase tracking-wide">Monthly Budget</div>
|
<div class="v-stat-label">Monthly budget</div>
|
||||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("$%.0f", d.MonthlyBudget) }</div>
|
<div class="v-stat-value">{ fmt.Sprintf("$%.0f", d.MonthlyBudget) }</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
if d.MonthlyBudget > 0 {
|
if d.MonthlyBudget > 0 {
|
||||||
<div class="mt-4">
|
<div class="mt-6 max-w-md">
|
||||||
<progress class="v-progress w-full" value={ fmt.Sprintf("%d", d.BudgetPercent()) } max="100"></progress>
|
<progress class="v-progress w-full" value={ fmt.Sprintf("%d", d.BudgetPercent()) } max="100"></progress>
|
||||||
<div class="flex justify-between text-xs mt-1">
|
<div class="flex justify-between text-xs mt-2">
|
||||||
<span class="v-muted">{ fmt.Sprintf("%d%% of budget used", d.BudgetPercent()) }</span>
|
<span class="v-muted">{ fmt.Sprintf("%d%% of budget used", d.BudgetPercent()) }</span>
|
||||||
if d.BudgetOver() {
|
if d.BudgetOver() {
|
||||||
<span class="v-price-target">Over budget</span>
|
<span class="v-price-target">Over budget</span>
|
||||||
@@ -139,56 +149,84 @@ templ DashboardBody(d DashboardData) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</section>
|
||||||
<div class="grid md:grid-cols-2 gap-6">
|
<hr class="v-rule mb-8"/>
|
||||||
<div class="v-card p-5">
|
<div class="grid md:grid-cols-2 gap-10">
|
||||||
<h2 class="font-semibold mb-3">Recent Results</h2>
|
<section>
|
||||||
|
<h2 class="v-section-title mb-4">Recent results</h2>
|
||||||
if len(d.RecentResults) == 0 {
|
if len(d.RecentResults) == 0 {
|
||||||
<div class="v-muted text-sm">No results yet.</div>
|
<div class="v-empty">Nothing found yet. Veola is watching.</div>
|
||||||
} else {
|
} else {
|
||||||
<table class="v-table">
|
<ul class="v-feed">
|
||||||
<thead>
|
for _, r := range d.RecentResults {
|
||||||
<tr><th>Item</th><th>Price</th><th>Source</th><th>Found</th></tr>
|
<li class="v-feed-row">
|
||||||
</thead>
|
<a href={ templ.SafeURL(fmt.Sprintf("/items/%d/results", r.ItemID)) }>
|
||||||
<tbody>
|
@thumb(r.ImageURL, r.Source)
|
||||||
for _, r := range d.RecentResults {
|
</a>
|
||||||
<tr>
|
<div class="min-w-0 flex-1">
|
||||||
<td><a href={ templ.SafeURL(fmt.Sprintf("/items/%d/results", r.ItemID)) }>{ r.ItemName }</a></td>
|
<a class="v-feed-name" href={ templ.SafeURL(fmt.Sprintf("/items/%d/results", r.ItemID)) }>{ r.ItemName }</a>
|
||||||
<td class="font-mono">{ fmtPrice(r.Price, r.Currency) }</td>
|
<div class="v-feed-meta truncate">{ resultMeta(r) }</div>
|
||||||
<td>{ r.Source }</td>
|
</div>
|
||||||
<td class="v-muted text-sm">{ humanTime(r.FoundAt) }</td>
|
<div class="font-mono v-feed-price">{ fmtPrice(r.Price, r.Currency) }</div>
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div class="v-card p-5">
|
|
||||||
<h2 class="font-semibold mb-3">Recent Alerts</h2>
|
|
||||||
if len(d.RecentAlerts) == 0 {
|
|
||||||
<div class="v-muted text-sm">No alerts sent yet.</div>
|
|
||||||
} else {
|
|
||||||
<ul class="space-y-2">
|
|
||||||
for _, a := range d.RecentAlerts {
|
|
||||||
<li class="flex justify-between items-center border-b border-white/10 pb-2">
|
|
||||||
<span>{ a.ItemName }</span>
|
|
||||||
<span class="font-mono v-price-target">{ fmtPrice(a.Price, a.Currency) }</span>
|
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
}
|
}
|
||||||
</div>
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2 class="v-section-title mb-4">Recent alerts</h2>
|
||||||
|
if len(d.RecentAlerts) == 0 {
|
||||||
|
<div class="v-empty">No alerts sent. Nothing has hit target.</div>
|
||||||
|
} else {
|
||||||
|
<ul class="v-feed">
|
||||||
|
for _, a := range d.RecentAlerts {
|
||||||
|
<li class="v-feed-row">
|
||||||
|
@thumb(a.ImageURL, "")
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="v-feed-name truncate">{ a.ItemName }</div>
|
||||||
|
if a.Title != "" {
|
||||||
|
<div class="v-feed-meta truncate">{ a.Title }</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="font-mono v-feed-price v-price-target">{ fmtPrice(a.Price, a.Currency) }</div>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
templ statCard(label, value, sub string) {
|
// thumb renders a product thumbnail, falling back to a quiet placeholder
|
||||||
<div class="v-card p-4">
|
// (the source initial) when a listing has no image. Marketplace images come
|
||||||
<div class="v-muted text-xs uppercase tracking-wide">{ label }</div>
|
// from an open set of CDNs, which the CSP's img-src allows.
|
||||||
<div class="font-mono text-3xl mt-1" data-countup>{ value }</div>
|
templ thumb(url, source string) {
|
||||||
if sub != "" {
|
if url != "" {
|
||||||
<div class="v-muted text-xs mt-1">{ sub }</div>
|
<img class="v-thumb" src={ url } loading="lazy" alt="" referrerpolicy="no-referrer"/>
|
||||||
}
|
} else {
|
||||||
|
<div class="v-thumb v-thumb-empty">{ sourceInitial(source) }</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resultMeta(r ResultRow) string {
|
||||||
|
if r.Title != "" {
|
||||||
|
return r.Title
|
||||||
|
}
|
||||||
|
return r.Source + " · " + humanTime(r.FoundAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceInitial(source string) string {
|
||||||
|
if source == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string([]rune(source)[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
templ statBlock(label, value string) {
|
||||||
|
<div>
|
||||||
|
<div class="v-stat-label">{ label }</div>
|
||||||
|
<div class="v-stat-value" data-countup>{ value }</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,14 +64,17 @@ type ResultRow struct {
|
|||||||
Currency string
|
Currency string
|
||||||
Source string
|
Source string
|
||||||
URL string
|
URL string
|
||||||
|
ImageURL string
|
||||||
FoundAt time.Time
|
FoundAt time.Time
|
||||||
Alerted bool
|
Alerted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertRow struct {
|
type AlertRow struct {
|
||||||
ItemName string
|
ItemName string
|
||||||
|
Title string
|
||||||
Price *float64
|
Price *float64
|
||||||
Currency string
|
Currency string
|
||||||
|
ImageURL string
|
||||||
FoundAt time.Time
|
FoundAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,402 +104,446 @@ func DashboardBody(d DashboardData) 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 hx-get=\"/dashboard/refresh\" hx-trigger=\"every 60s\" hx-swap=\"outerHTML\"><h1 class=\"text-3xl font-semibold mb-6\">Dashboard</h1><div class=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-6\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div hx-get=\"/dashboard/refresh\" hx-trigger=\"every 60s\" hx-swap=\"outerHTML\"><header class=\"mb-10\"><h1 class=\"v-page-title\">Dashboard</h1><p class=\"v-page-sub\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = statCard("Total Items", fmt.Sprintf("%d", d.Stats.TotalItems), "").Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = statCard("Active", fmt.Sprintf("%d", d.Stats.ActiveItems), "").Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = statCard("Results Today", fmt.Sprintf("%d", d.Stats.ResultsToday), "").Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = statCard("Alerts Today", fmt.Sprintf("%d", d.Stats.AlertsToday), "").Render(ctx, templ_7745c5c3_Buffer)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</div><div class=\"grid md:grid-cols-2 gap-4 mb-6\"><div class=\"v-card p-5\"><div class=\"v-muted text-sm uppercase tracking-wide\">Potential Spend</div><div class=\"font-mono text-4xl mt-2\" data-countup>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var2 string
|
var templ_7745c5c3_Var2 string
|
||||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("$%.2f", d.Stats.PotentialSpend))
|
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Watching %d items · %d active", d.Stats.TotalItems, d.Stats.ActiveItems))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 87, Col: 100}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 82, Col: 113}
|
||||||
}
|
}
|
||||||
_, 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 {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"v-muted text-sm mt-1\">across ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</p></header><div class=\"grid md:grid-cols-2 gap-8 mb-8\"><div><div class=\"v-metric-label\">Potential spend</div><div class=\"v-metric-value\" data-countup>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var3 string
|
var templ_7745c5c3_Var3 string
|
||||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.Stats.PricedItemCount))
|
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("$%.2f", d.Stats.PotentialSpend))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 88, Col: 89}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 87, Col: 91}
|
||||||
}
|
}
|
||||||
_, 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 {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " items</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div><div class=\"v-metric-sub\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var4 string
|
||||||
|
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("across %d priced items", d.Stats.PricedItemCount))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 89, Col: 69}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if d.Stats.UnpricedCount > 0 {
|
if d.Stats.UnpricedCount > 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"v-muted text-xs mt-1\">")
|
var templ_7745c5c3_Var5 string
|
||||||
|
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf(" · %d not yet priced", d.Stats.UnpricedCount))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 91, Col: 67}
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var4 string
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d items not yet priced.", d.Stats.UnpricedCount))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 90, Col: 103}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
|
|
||||||
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, 7, "</div><div class=\"v-card p-5\"><div class=\"v-muted text-sm uppercase tracking-wide\">Money Saved</div><div class=\"font-mono text-4xl mt-2 v-price-deal\" data-countup>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</div></div><div><div class=\"v-metric-label\">Money saved</div><div class=\"v-metric-value v-price-deal\" data-countup>")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var5 string
|
|
||||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("$%.2f", d.Stats.MoneySaved))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 95, Col: 109}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><div class=\"v-muted text-sm mt-1\">across ")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var6 string
|
var templ_7745c5c3_Var6 string
|
||||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.Stats.SavedItemCount))
|
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("$%.2f", d.Stats.MoneySaved))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 96, Col: 88}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 97, Col: 100}
|
||||||
}
|
}
|
||||||
_, 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 {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " items, vs 30-day average</div></div></div><div class=\"v-card p-5 mb-6\"><div class=\"flex items-center justify-between mb-3\"><h2 class=\"font-semibold\">API Usage and Budget</h2><span class=\"v-muted text-xs\">Apify runs cost credits. Visible to everyone.</span></div><div class=\"grid grid-cols-2 md:grid-cols-4 gap-4\"><div><div class=\"v-muted text-xs uppercase tracking-wide\">Apify (month)</div><div class=\"font-mono text-2xl mt-1\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div><div class=\"v-metric-sub\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var7 string
|
var templ_7745c5c3_Var7 string
|
||||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.ApifyMonth))
|
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("across %d items, vs 30-day average", d.Stats.SavedItemCount))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 107, Col: 75}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 98, Col: 105}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
_, 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, 10, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div></div><hr class=\"v-rule mb-8\"><div class=\"grid grid-cols-2 md:grid-cols-4 gap-6 mb-8\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = statBlock("Total items", fmt.Sprintf("%d", d.Stats.TotalItems)).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = statBlock("Active", fmt.Sprintf("%d", d.Stats.ActiveItems)).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = statBlock("Results today", fmt.Sprintf("%d", d.Stats.ResultsToday)).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = statBlock("Alerts today", fmt.Sprintf("%d", d.Stats.AlertsToday)).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div><hr class=\"v-rule mb-8\"><section class=\"mb-10\"><div class=\"flex items-baseline justify-between mb-5\"><h2 class=\"v-section-title\">API usage and budget</h2><span class=\"v-muted text-xs\">Apify runs cost credits. Visible to everyone.</span></div><div class=\"grid grid-cols-2 md:grid-cols-4 gap-6\"><div><div class=\"v-stat-label\">Apify (month)</div><div class=\"v-stat-value\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var8 string
|
||||||
|
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.ApifyMonth))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 117, Col: 64}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if d.ApifyCostPerCall > 0 {
|
if d.ApifyCostPerCall > 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"v-muted text-xs mt-1\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"v-metric-sub\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var8 string
|
var templ_7745c5c3_Var9 string
|
||||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("~$%.2f est.", d.ApifyMonthCost()))
|
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("~$%.2f estimated", d.ApifyMonthCost()))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 109, Col: 88}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 119, Col: 85}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
_, 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, 12, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div>")
|
||||||
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, 13, "</div><div><div class=\"v-muted text-xs uppercase tracking-wide\">Apify (today)</div><div class=\"font-mono text-2xl mt-1\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div><div><div class=\"v-stat-label\">Apify (today)</div><div class=\"v-stat-value\">")
|
||||||
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_Var10 string
|
||||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.ApifyToday))
|
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.ApifyToday))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 114, Col: 75}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 124, Col: 64}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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, 14, "</div></div><div><div class=\"v-muted text-xs uppercase tracking-wide\">eBay (today)</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></div><div><div class=\"v-stat-label\">eBay (today)</div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if d.EbayLimit > 0 {
|
if d.EbayLimit > 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"font-mono text-2xl mt-1\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<div class=\"v-stat-value\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var10 string
|
|
||||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", d.EbayToday, d.EbayLimit))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 119, Col: 93}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<div class=\"font-mono text-2xl mt-1\">")
|
|
||||||
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.JoinStringErrs(fmt.Sprintf("%d", d.EbayToday))
|
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", d.EbayToday, d.EbayLimit))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 121, Col: 75}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 129, Col: 82}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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, 18, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"v-stat-value\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if d.MonthlyBudget > 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<div><div class=\"v-muted text-xs uppercase tracking-wide\">Monthly Budget</div><div class=\"font-mono text-2xl mt-1\">")
|
|
||||||
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(fmt.Sprintf("$%.0f", d.MonthlyBudget))
|
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", d.EbayToday))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 127, Col: 82}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 131, Col: 64}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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, 21, "</div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</div>")
|
||||||
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, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if d.MonthlyBudget > 0 {
|
if d.MonthlyBudget > 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"mt-4\"><progress class=\"v-progress w-full\" value=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div><div class=\"v-stat-label\">Monthly budget</div><div class=\"v-stat-value\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var13 string
|
var templ_7745c5c3_Var13 string
|
||||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", d.BudgetPercent()))
|
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("$%.0f", d.MonthlyBudget))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 133, Col: 85}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 137, Col: 71}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(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, 24, "\" max=\"100\"></progress><div class=\"flex justify-between text-xs mt-1\"><span class=\"v-muted\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</div></div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if d.MonthlyBudget > 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"mt-6 max-w-md\"><progress class=\"v-progress w-full\" value=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var14 string
|
var templ_7745c5c3_Var14 string
|
||||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d%% of budget used", d.BudgetPercent()))
|
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", d.BudgetPercent()))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 135, Col: 83}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 143, Col: 85}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(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, "</span> ")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" max=\"100\"></progress><div class=\"flex justify-between text-xs mt-2\"><span class=\"v-muted\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var15 string
|
||||||
|
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d%% of budget used", d.BudgetPercent()))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 145, Col: 83}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</span> ")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if d.BudgetOver() {
|
if d.BudgetOver() {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<span class=\"v-price-target\">Over budget</span>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<span class=\"v-price-target\">Over budget</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, 27, "</div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></div>")
|
||||||
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, 28, "</div><div class=\"grid md:grid-cols-2 gap-6\"><div class=\"v-card p-5\"><h2 class=\"font-semibold mb-3\">Recent Results</h2>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</section><hr class=\"v-rule mb-8\"><div class=\"grid md:grid-cols-2 gap-10\"><section><h2 class=\"v-section-title mb-4\">Recent results</h2>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if len(d.RecentResults) == 0 {
|
if len(d.RecentResults) == 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<div class=\"v-muted text-sm\">No results yet.</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"v-empty\">Nothing found yet. Veola is watching.</div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<table class=\"v-table\"><thead><tr><th>Item</th><th>Price</th><th>Source</th><th>Found</th></tr></thead> <tbody>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<ul class=\"v-feed\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
for _, r := range d.RecentResults {
|
for _, r := range d.RecentResults {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<tr><td><a href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<li class=\"v-feed-row\"><a href=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var15 templ.SafeURL
|
var templ_7745c5c3_Var16 templ.SafeURL
|
||||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/results", r.ItemID)))
|
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/results", r.ItemID)))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 156, Col: 80}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 163, Col: 75}
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var16 string
|
|
||||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(r.ItemName)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 156, Col: 95}
|
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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, 33, "</a></td><td class=\"font-mono\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var17 string
|
templ_7745c5c3_Err = thumb(r.ImageURL, r.Source).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(r.Price, r.Currency))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 157, Col: 62}
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</a><div class=\"min-w-0 flex-1\"><a class=\"v-feed-name\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var17 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/results", r.ItemID)))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 167, Col: 96}
|
||||||
}
|
}
|
||||||
_, 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, 34, "</td><td>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var18 string
|
var templ_7745c5c3_Var18 string
|
||||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(r.Source)
|
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(r.ItemName)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 158, Col: 23}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 167, Col: 111}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
_, 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, 35, "</td><td class=\"v-muted text-sm\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</a><div class=\"v-feed-meta truncate\">")
|
||||||
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(humanTime(r.FoundAt))
|
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(resultMeta(r))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 159, Col: 59}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 168, Col: 58}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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, 36, "</td></tr>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div></div><div class=\"font-mono v-feed-price\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</tbody></table>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div><div class=\"v-card p-5\"><h2 class=\"font-semibold mb-3\">Recent Alerts</h2>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if len(d.RecentAlerts) == 0 {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"v-muted text-sm\">No alerts sent yet.</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<ul class=\"space-y-2\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
for _, a := range d.RecentAlerts {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<li class=\"flex justify-between items-center border-b border-white/10 pb-2\"><span>")
|
|
||||||
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.JoinStringErrs(a.ItemName)
|
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(r.Price, r.Currency))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 174, Col: 26}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 170, Col: 75}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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, 42, "</span> <span class=\"font-mono v-price-target\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div></li>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</ul>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</section><section><h2 class=\"v-section-title mb-4\">Recent alerts</h2>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if len(d.RecentAlerts) == 0 {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<div class=\"v-empty\">No alerts sent. Nothing has hit target.</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<ul class=\"v-feed\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
for _, a := range d.RecentAlerts {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<li class=\"v-feed-row\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = thumb(a.ImageURL, "").Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<div class=\"min-w-0 flex-1\"><div class=\"v-feed-name truncate\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var21 string
|
var templ_7745c5c3_Var21 string
|
||||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(a.Price, a.Currency))
|
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(a.ItemName)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 175, Col: 78}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 186, Col: 55}
|
||||||
}
|
}
|
||||||
_, 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, 43, "</span></li>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
if a.Title != "" {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<div class=\"v-feed-meta truncate\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var22 string
|
||||||
|
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(a.Title)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 188, Col: 53}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</div>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</div><div class=\"font-mono v-feed-price v-price-target\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var23 string
|
||||||
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(a.Price, a.Currency))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 191, Col: 90}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</div></li>")
|
||||||
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, "</ul>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</ul>")
|
||||||
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, 45, "</div></div></div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</section></div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -504,7 +551,10 @@ func DashboardBody(d DashboardData) templ.Component {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func statCard(label, value, sub string) templ.Component {
|
// thumb renders a product thumbnail, falling back to a quiet placeholder
|
||||||
|
// (the source initial) when a listing has no image. Marketplace images come
|
||||||
|
// from an open set of CDNs, which the CSP's img-src allows.
|
||||||
|
func thumb(url, source string) 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 {
|
||||||
@@ -520,61 +570,114 @@ func statCard(label, value, sub string) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var24 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var22 == nil {
|
if templ_7745c5c3_Var24 == nil {
|
||||||
templ_7745c5c3_Var22 = templ.NopComponent
|
templ_7745c5c3_Var24 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<div class=\"v-card p-4\"><div class=\"v-muted text-xs uppercase tracking-wide\">")
|
if url != "" {
|
||||||
if templ_7745c5c3_Err != nil {
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<img class=\"v-thumb\" src=\"")
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var23 string
|
|
||||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 187, Col: 62}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "</div><div class=\"font-mono text-3xl mt-1\" data-countup>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var24 string
|
|
||||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(value)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 188, Col: 59}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</div>")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
if sub != "" {
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<div class=\"v-muted text-xs mt-1\">")
|
|
||||||
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.JoinStringErrs(sub)
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(url)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 190, Col: 42}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 206, Col: 32}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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, 50, "</div>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "\" loading=\"lazy\" alt=\"\" referrerpolicy=\"no-referrer\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<div class=\"v-thumb v-thumb-empty\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var26 string
|
||||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(sourceInitial(source))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 208, Col: 60}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</div>")
|
||||||
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, "</div>")
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func resultMeta(r ResultRow) string {
|
||||||
|
if r.Title != "" {
|
||||||
|
return r.Title
|
||||||
|
}
|
||||||
|
return r.Source + " · " + humanTime(r.FoundAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sourceInitial(source string) string {
|
||||||
|
if source == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string([]rune(source)[:1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func statBlock(label, value string) templ.Component {
|
||||||
|
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
|
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||||
|
return templ_7745c5c3_CtxErr
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
|
if !templ_7745c5c3_IsBuffer {
|
||||||
|
defer func() {
|
||||||
|
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||||
|
if templ_7745c5c3_Err == nil {
|
||||||
|
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
ctx = templ.InitializeContext(ctx)
|
||||||
|
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
|
||||||
|
if templ_7745c5c3_Var27 == nil {
|
||||||
|
templ_7745c5c3_Var27 = templ.NopComponent
|
||||||
|
}
|
||||||
|
ctx = templ.ClearChildren(ctx)
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<div><div class=\"v-stat-label\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var28 string
|
||||||
|
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(label)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 228, Col: 35}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</div><div class=\"v-stat-value\" data-countup>")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var29 string
|
||||||
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(value)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 229, Col: 48}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</div></div>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -598,9 +701,9 @@ func Dashboard(d DashboardData) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var30 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var26 == nil {
|
if templ_7745c5c3_Var30 == nil {
|
||||||
templ_7745c5c3_Var26 = templ.NopComponent
|
templ_7745c5c3_Var30 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Err = Layout(d.Page, DashboardBody(d)).Render(ctx, templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = Layout(d.Page, DashboardBody(d)).Render(ctx, templ_7745c5c3_Buffer)
|
||||||
|
|||||||
Reference in New Issue
Block a user