Initial commit

This commit is contained in:
2026-05-13 19:42:49 -07:00
commit cfa01bd4ef
54 changed files with 11718 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
package templates
import (
"fmt"
"veola/internal/apify"
)
type PreviewData struct {
CSRFToken string
Form FormValues
Results []apify.UnifiedResult
BestIndex int
MinPrice float64
MaxPrice float64
StoreCount int
Error string
Empty bool
Cached bool
Currency string
}
// FormValues mirrors the Step 1 form so the confirm POST has every field.
type FormValues struct {
Name string
SearchQuery string
URL string
Category string
TargetPrice string
MinPrice string
ExcludeKeywords string
NtfyTopic string
NtfyPriority string
PollIntervalMinutes string
IncludeOutOfStock bool
Marketplaces []string
ListingType string
ActorActive string
ActorSold string
ActorPriceCompare string
UsePriceComparison bool
}
templ ItemPreview(d PreviewData) {
if d.Error != "" {
<div class="v-flash-error">
<div class="font-semibold mb-1">Could not run preview</div>
<div>{ d.Error }</div>
<button class="v-btn-ghost mt-2" hx-get="/items/new" hx-target="#preview-target" hx-swap="innerHTML">Back</button>
</div>
} else if d.Empty {
<div class="v-flash">
<div class="font-semibold mb-1">No results found</div>
<div>Try a broader search query or a different marketplace.</div>
</div>
} else {
<div class="v-card p-5">
<div class="flex items-center justify-between mb-4">
<h2 class="font-semibold">
{ fmt.Sprintf("Found %d results for '%s'", len(d.Results), d.Form.SearchQuery) }
if d.Cached {
<span class="v-pill v-pill-paused ml-2">cached</span>
}
</h2>
</div>
@previewBest(d)
if len(d.Results) > 1 {
<div class="mt-5">
<div class="v-muted text-xs uppercase tracking-wide mb-2">Other results</div>
<ul class="space-y-2">
for i, r := range d.Results {
if i != d.BestIndex && i < d.BestIndex+6 {
<li class="flex items-center gap-3">
if r.ImageURL != "" {
<img src={ r.ImageURL } alt="" class="w-10 h-10 object-cover rounded"/>
}
<div class="flex-1 truncate">
<a href={ templ.SafeURL(r.URL) } target="_blank" rel="noopener" class="text-sm">{ r.Title }</a>
<div class="v-muted text-xs">{ r.Store }</div>
</div>
<div class="font-mono">{ fmtNumber(r.Price, r.Currency) }</div>
</li>
}
}
</ul>
if len(d.Results) > 6 {
<div class="v-muted text-xs mt-2">{ fmt.Sprintf("and %d more", len(d.Results)-6) }</div>
}
</div>
}
<div class="v-divider my-4"></div>
<div class="text-sm v-muted">
{ fmt.Sprintf("Prices range from %s to %s across %d stores", fmtNumber(d.MinPrice, d.Currency), fmtNumber(d.MaxPrice, d.Currency), d.StoreCount) }
</div>
</div>
}
@confirmForm(d)
}
templ previewBest(d PreviewData) {
if d.BestIndex >= 0 && d.BestIndex < len(d.Results) {
<div class="grid md:grid-cols-[160px_1fr] gap-4 items-start">
if d.Results[d.BestIndex].ImageURL != "" {
<img src={ d.Results[d.BestIndex].ImageURL } alt="" class="rounded w-40 h-40 object-cover"/>
} else {
<div class="rounded w-40 h-40 bg-black/30"></div>
}
<div>
<div class="text-xs v-price-deal uppercase tracking-wide mb-1">Best Price</div>
<div class="font-mono text-3xl mb-2">{ fmtNumber(d.Results[d.BestIndex].Price, d.Currency) }</div>
<a class="font-semibold" href={ templ.SafeURL(d.Results[d.BestIndex].URL) } target="_blank" rel="noopener">{ d.Results[d.BestIndex].Title }</a>
<div class="v-muted text-sm mt-1">{ d.Results[d.BestIndex].Store }</div>
if d.Results[d.BestIndex].MatchedQuery != "" {
<div class="v-muted text-xs mt-1">via "{ d.Results[d.BestIndex].MatchedQuery }"</div>
}
</div>
</div>
}
}
templ confirmForm(d PreviewData) {
<form method="post" action="/items" class="mt-5 flex items-center gap-3">
<input type="hidden" name="csrf_token" value={ d.CSRFToken }/>
@hidden("name", d.Form.Name)
@hidden("search_query", d.Form.SearchQuery)
@hidden("url", d.Form.URL)
@hidden("category", d.Form.Category)
@hidden("target_price", d.Form.TargetPrice)
@hidden("min_price", d.Form.MinPrice)
@hidden("exclude_keywords", d.Form.ExcludeKeywords)
@hidden("ntfy_topic", d.Form.NtfyTopic)
@hidden("ntfy_priority", d.Form.NtfyPriority)
@hidden("poll_interval_minutes", d.Form.PollIntervalMinutes)
@hiddenBool("include_out_of_stock", d.Form.IncludeOutOfStock)
for _, m := range d.Form.Marketplaces {
@hidden("marketplace", m)
}
@hidden("listing_type", d.Form.ListingType)
@hidden("actor_active", d.Form.ActorActive)
@hidden("actor_sold", d.Form.ActorSold)
@hidden("actor_price_compare", d.Form.ActorPriceCompare)
@hiddenBool("use_price_comparison", d.Form.UsePriceComparison)
<button type="submit" class="v-btn" disabled?={ d.Empty || d.Error != "" }>Confirm and Track</button>
<a class="v-btn-ghost" href="/items/new">Back</a>
</form>
}
templ hidden(name, value string) {
<input type="hidden" name={ name } value={ value }/>
}
templ hiddenBool(name string, value bool) {
if value {
<input type="hidden" name={ name } value="1"/>
}
}
func fmtNumber(p float64, currency string) string {
if p == 0 {
return "—"
}
return fmt.Sprintf("%s%.2f", currencySymbol(currency), p)
}