ui: update catalog and search templates

This commit is contained in:
2026-04-06 19:20:47 +02:00
parent b8b9b522db
commit 564cae2e16
5 changed files with 95 additions and 95 deletions

View File

@@ -51,30 +51,37 @@ func main() {
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// Index page (Search)
// Homepage (Catalog)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
q := r.URL.Query().Get("q")
templates.Index(q).Render(r.Context(), w)
templates.Catalog().Render(r.Context(), w)
})
// Search endpoint initial query
// Search page
mux.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
templates.Search("").Render(r.Context(), w)
return
}
res, err := jikanClient.Search(query, 1)
if err != nil {
log.Printf("search error: %v", err)
http.Error(w, "Failed to search anime", http.StatusInternalServerError)
// Check if HTMX request for results only
if r.Header.Get("HX-Request") == "true" {
res, err := jikanClient.Search(query, 1)
if err != nil {
log.Printf("search error: %v", err)
http.Error(w, "Failed to search anime", http.StatusInternalServerError)
return
}
templates.SearchResultsWrapper(query, res.Animes, 2, res.HasNextPage).Render(r.Context(), w)
return
}
templates.SearchResultsWrapper(query, res.Animes, 2, res.HasNextPage).Render(r.Context(), w)
// Full page with query
templates.Search(query).Render(r.Context(), w)
})
// Search endpoint (HTMX Infinite Scroll)
@@ -96,11 +103,6 @@ func main() {
templates.SearchItems(query, res.Animes, page+1, res.HasNextPage).Render(r.Context(), w)
})
// Catalog page
mux.HandleFunc("/catalog", func(w http.ResponseWriter, r *http.Request) {
templates.Catalog().Render(r.Context(), w)
})
// Catalog endpoint (HTMX Infinite Scroll)
mux.HandleFunc("/api/catalog", func(w http.ResponseWriter, r *http.Request) {
pageStr := r.URL.Query().Get("page")
@@ -135,7 +137,19 @@ func main() {
return
}
templates.AnimeDetails(anime).Render(r.Context(), w)
// Get current watchlist status if user is logged in
currentStatus := ""
if user := middleware.GetUser(r.Context()); user != nil {
entry, err := queries.GetWatchListEntry(r.Context(), database.GetWatchListEntryParams{
UserID: user.ID,
AnimeID: int64(id),
})
if err == nil {
currentStatus = entry.Status
}
}
templates.AnimeDetails(anime, currentStatus).Render(r.Context(), w)
})
// Anime Relations API endpoint (HTMX "Suspense")

View File

@@ -6,8 +6,13 @@ import "fmt"
templ Catalog() {
@Layout("malago - catalog") {
<div class="catalog-grid" id="catalog-content">
<div hx-get="/api/catalog?page=1" hx-trigger="load" hx-swap="outerHTML">
<div class="status-text" style="grid-column: 1 / -1;">> loading catalog...</div>
<div hx-get="/api/catalog?page=1" hx-trigger="load" hx-swap="outerHTML" style="grid-column: 1 / -1;">
<div class="loading-indicator">
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<span>loading catalog</span>
</div>
</div>
</div>
}
@@ -29,8 +34,8 @@ templ CatalogItems(animes []jikan.Anime, nextPage int, hasNext bool) {
templ CatalogItem(anime jikan.Anime) {
<a href={ templ.URL(fmt.Sprintf("/anime/%d", anime.MalID)) }>
if anime.Images.Webp.LargeImageURL != "" {
<img src={ anime.Images.Webp.LargeImageURL } alt={ anime.DisplayTitle() } class="catalog-thumb" loading="lazy" />
if anime.ImageURL() != "" {
<img src={ anime.ImageURL() } alt={ anime.DisplayTitle() } class="catalog-thumb" loading="lazy" />
} else {
<div class="no-image">no image</div>
}

View File

@@ -44,7 +44,7 @@ func Catalog() templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"catalog-grid\" id=\"catalog-content\"><div hx-get=\"/api/catalog?page=1\" hx-trigger=\"load\" hx-swap=\"outerHTML\"><div class=\"status-text\" style=\"grid-column: 1 / -1;\">> loading catalog...</div></div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"catalog-grid\" id=\"catalog-content\"><div hx-get=\"/api/catalog?page=1\" hx-trigger=\"load\" hx-swap=\"outerHTML\" style=\"grid-column: 1 / -1;\"><div class=\"loading-indicator\"><div class=\"loading-dot\"></div><div class=\"loading-dot\"></div><div class=\"loading-dot\"></div><span>loading catalog</span></div></div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -88,7 +88,7 @@ func CatalogItems(animes []jikan.Anime, nextPage int, hasNext bool) templ.Compon
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/catalog?page=%d", nextPage))))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 19, Col: 102}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 24, Col: 102}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
@@ -153,7 +153,7 @@ func CatalogItem(anime jikan.Anime) templ.Component {
var templ_7745c5c3_Var6 templ.SafeURL
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(templ.URL(fmt.Sprintf("/anime/%d", anime.MalID)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 31, Col: 59}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 36, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
@@ -163,15 +163,15 @@ func CatalogItem(anime jikan.Anime) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if anime.Images.Webp.LargeImageURL != "" {
if anime.ImageURL() != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<img src=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(anime.Images.Webp.LargeImageURL)
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(anime.ImageURL())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 33, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 38, Col: 30}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
@@ -184,7 +184,7 @@ func CatalogItem(anime jikan.Anime) templ.Component {
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(anime.DisplayTitle())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 33, Col: 74}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 38, Col: 59}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
@@ -207,7 +207,7 @@ func CatalogItem(anime jikan.Anime) templ.Component {
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(anime.DisplayTitle())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 39, Col: 24}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/catalog.templ`, Line: 44, Col: 24}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {

View File

@@ -6,25 +6,20 @@ import (
"net/url"
)
templ Index(q string) {
templ Search(q string) {
@Layout("malago - search") {
<div class="search-box">
<form action="/" method="GET" class="search-form">
<input type="text" name="q" class="search-input" placeholder="search..." value={ q } autofocus />
<button type="submit" class="search-button">search</button>
</form>
</div>
if q != "" {
<div id="loading" class="htmx-indicator status-text">
> fetching from jikan...
<div class="loading-indicator htmx-indicator" id="loading">
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<span>searching</span>
</div>
<div id="results" hx-get={ string(templ.URL("/search?q=" + url.QueryEscape(q))) } hx-trigger="load" hx-indicator="#loading"></div>
} else {
<div id="results">
<div style="text-align: center; color: var(--text-muted); font-size: 12px; margin-top: 32px;">
no results yet.
</div>
<div class="empty-state">
<div class="empty-state-title">search for anime</div>
<div class="empty-state-text">use the search bar above to find anime to add to your watchlist</div>
</div>
}
}
@@ -32,7 +27,10 @@ templ Index(q string) {
templ SearchResultsWrapper(query string, animes []jikan.Anime, nextPage int, hasNext bool) {
if len(animes) == 0 {
<div style="text-align: center; color: var(--text-muted); font-size: 12px;">no results found.</div>
<div class="empty-state">
<div class="empty-state-title">no results found</div>
<div class="empty-state-text">try a different search term</div>
</div>
} else {
<div class="catalog-grid">
@SearchItems(query, animes, nextPage, hasNext)

View File

@@ -14,7 +14,7 @@ import (
"net/url"
)
func Index(q string) templ.Component {
func Search(q 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 {
@@ -47,43 +47,26 @@ func Index(q string) templ.Component {
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"search-box\"><form action=\"/\" method=\"GET\" class=\"search-form\"><input type=\"text\" name=\"q\" class=\"search-input\" placeholder=\"search...\" value=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(q)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/index.templ`, Line: 13, Col: 86}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" autofocus> <button type=\"submit\" class=\"search-button\">search</button></form></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if q != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div id=\"loading\" class=\"htmx-indicator status-text\">> fetching from jikan...</div><div id=\"results\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"loading-indicator htmx-indicator\" id=\"loading\"><div class=\"loading-dot\"></div><div class=\"loading-dot\"></div><div class=\"loading-dot\"></div><span>searching</span></div><div id=\"results\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL("/search?q=" + url.QueryEscape(q))))
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL("/search?q=" + url.QueryEscape(q))))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/index.templ`, Line: 22, Col: 82}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/index.templ`, Line: 18, Col: 82}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\" hx-trigger=\"load\" hx-indicator=\"#loading\"></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "\" hx-trigger=\"load\" hx-indicator=\"#loading\"></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div id=\"results\"><div style=\"text-align: center; color: var(--text-muted); font-size: 12px; margin-top: 32px;\">no results yet.</div></div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<div class=\"empty-state\"><div class=\"empty-state-title\">search for anime</div><div class=\"empty-state-text\">use the search bar above to find anime to add to your watchlist</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -114,18 +97,18 @@ func SearchResultsWrapper(query string, animes []jikan.Anime, nextPage int, hasN
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
templ_7745c5c3_Var4 := templ.GetChildren(ctx)
if templ_7745c5c3_Var4 == nil {
templ_7745c5c3_Var4 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if len(animes) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div style=\"text-align: center; color: var(--text-muted); font-size: 12px;\">no results found.</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"empty-state\"><div class=\"empty-state-title\">no results found</div><div class=\"empty-state-text\">try a different search term</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"catalog-grid\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"catalog-grid\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -133,7 +116,7 @@ func SearchResultsWrapper(query string, animes []jikan.Anime, nextPage int, hasN
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -158,27 +141,40 @@ func SearchItems(query string, animes []jikan.Anime, nextPage int, hasNext bool)
}()
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var6 := templ.GetChildren(ctx)
if templ_7745c5c3_Var6 == nil {
templ_7745c5c3_Var6 = templ.NopComponent
templ_7745c5c3_Var5 := templ.GetChildren(ctx)
if templ_7745c5c3_Var5 == nil {
templ_7745c5c3_Var5 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
for i, anime := range animes {
if i == len(animes)-1 && hasNext {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"catalog-item\" hx-get=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<div class=\"catalog-item\" hx-get=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/search?q=%s&page=%d", url.QueryEscape(query), nextPage))))
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(string(templ.URL(fmt.Sprintf("/api/search?q=%s&page=%d", url.QueryEscape(query), nextPage))))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/index.templ`, Line: 46, Col: 130}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `internal/templates/index.templ`, Line: 44, Col: 130}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" hx-trigger=\"revealed\" hx-swap=\"afterend\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "\" hx-trigger=\"revealed\" hx-swap=\"afterend\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = CatalogItem(anime).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<div class=\"catalog-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -190,19 +186,6 @@ func SearchItems(query string, animes []jikan.Anime, nextPage int, hasNext bool)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"catalog-item\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = CatalogItem(anime).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
}
return nil