refactor: reorganize project structure following go standards

This commit is contained in:
2026-04-20 15:54:35 +02:00
parent 055ec1fca9
commit 6df8788749
70 changed files with 43 additions and 187 deletions

View File

@@ -0,0 +1,64 @@
package ui
import "fmt"
type AnimeCardProps struct {
ID int
Title string
ImageURL string
Href string
// Options to customize the card behavior
Class string // override default wrapper class
ImgClass string // override default image class
TitleClass string // override default title class
HideTitle bool // if true, do not render the default title block
CurrentNode bool // if true, renders a div instead of an anchor tag (useful for graph nodes)
}
templ AnimeCard(props AnimeCardProps) {
if props.CurrentNode {
<div class={ defaultString(props.Class, "min-w-0") }>
@animeCardPoster(props.ImageURL, props.Title, props.ImgClass)
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-sm leading-snug text-(--text)") }>
{ props.Title }
</div>
{ children... }
</div>
} else {
<a href={ templ.URL(cardHref(props)) } class={ defaultString(props.Class, "flex flex-col bg-transparent text-inherit no-underline") }>
@animeCardPoster(props.ImageURL, props.Title, props.ImgClass)
if !props.HideTitle {
<div class={ defaultString(props.TitleClass, "mt-2 line-clamp-2 text-sm leading-snug text-(--text)") }>
{ props.Title }
</div>
}
{ children... }
</a>
}
}
func cardHref(props AnimeCardProps) string {
if props.Href != "" {
return props.Href
}
return fmt.Sprintf("/anime/%d", props.ID)
}
templ animeCardPoster(imageURL, title, imgClass string) {
<div class="flex w-full aspect-2/3 justify-center overflow-hidden">
if imageURL != "" {
<img src={ imageURL } alt={ title } class={ defaultString(imgClass, "block w-full object-cover object-center") } loading="lazy"/>
} else {
<div class="flex w-full justify-center overflow-hidden text-transparent">No image</div>
}
</div>
}
func defaultString(val, fallback string) string {
if val == "" {
return fallback
}
return val
}

View File

@@ -0,0 +1,45 @@
package ui
import (
"fmt"
"mal/internal/jikan"
)
templ InfiniteAnimeList(animes []jikan.Anime, hasNext bool, nextURL string, containerID string) {
for _, anime := range animes {
<div class="min-w-0" data-id={ fmt.Sprintf("%d", anime.MalID) }>
@CatalogItem(anime)
</div>
}
if hasNext {
<div class="col-span-full h-px w-full" hx-get={ nextURL } hx-trigger="revealed" hx-swap="outerHTML"></div>
}
<script data-container={ containerID }>
(function() {
const scripts = document.querySelectorAll('script[data-container]');
const currentScript = scripts[scripts.length - 1];
const actualID = currentScript.getAttribute('data-container');
const container = document.getElementById(actualID) || document;
const items = container.querySelectorAll('[data-id]');
const seen = new Set();
items.forEach(item => {
const id = item.getAttribute('data-id');
if (id) {
if (seen.has(id)) {
item.remove();
} else {
seen.add(id);
}
}
});
})();
</script>
}
templ CatalogItem(anime jikan.Anime) {
@AnimeCard(AnimeCardProps{
ID: anime.MalID,
Title: anime.DisplayTitle(),
ImageURL: anime.ImageURL(),
})
}

View File

@@ -0,0 +1,10 @@
package ui
templ EmptyState(title string) {
<div class="py-4">
<div class="mb-2 text-base">{ title }</div>
<div class="text-sm text-(--text-muted)">
{ children... }
</div>
</div>
}

View File

@@ -0,0 +1,9 @@
package icons
templ LogoIcon(class string) {
<svg xmlns="http://www.w3.org/2000/svg" class={ class } viewBox="0 0 32 32" width="32" height="32" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linejoin="miter">
<!-- clean, superminimal, abstract logo without border-radius -->
<rect x="6" y="10" width="12" height="12"></rect>
<rect x="14" y="6" width="12" height="12"></rect>
</svg>
}

View File

@@ -0,0 +1,10 @@
package ui
templ LoadingIndicator(text string) {
<div class="inline-flex items-center gap-2 text-sm text-(--text-muted)">
<div class="h-1.5 w-1.5 bg-(--text-faint)"></div>
<div class="h-1.5 w-1.5 bg-(--text-faint)"></div>
<div class="h-1.5 w-1.5 bg-(--text-faint)"></div>
<span>{ text }</span>
</div>
}

View File

@@ -0,0 +1,37 @@
package ui
type SortFilterOptions struct {
Sort string // "title", "date"
Order string // "asc", "desc"
View string // for watchlist: "grid", "table"
Status string // for watchlist: "all", "watching", etc
}
templ SortFilter(opts SortFilterOptions) {
<div class="mb-4 flex flex-wrap items-center gap-3 bg-(--panel) p-3 max-lg:flex-col max-lg:items-start max-lg:gap-2">
<div class="flex items-center gap-2">
<label for="sort-select" class="text-xs text-(--text-muted)">Sort by</label>
<select id="sort-select" class="h-8 bg-(--surface-select) px-2 text-xs text-(--text)" onchange="document.getElementById('sort-input').value = this.value; document.getElementById('sort-form').submit()">
<option value="date" selected?={ opts.Sort == "date" }>Date added</option>
<option value="title" selected?={ opts.Sort == "title" }>Title</option>
</select>
</div>
<div class="flex items-center gap-2">
<label for="order-select" class="text-xs text-(--text-muted)">Order</label>
<select id="order-select" class="h-8 bg-(--surface-select) px-2 text-xs text-(--text)" onchange="document.getElementById('order-input').value = this.value; document.getElementById('sort-form').submit()">
<option value="desc" selected?={ opts.Order == "desc" }>Descending</option>
<option value="asc" selected?={ opts.Order == "asc" }>Ascending</option>
</select>
</div>
</div>
<form id="sort-form" method="get" class="hidden">
<input type="hidden" name="sort" id="sort-input" value={ opts.Sort }/>
<input type="hidden" name="order" id="order-input" value={ opts.Order }/>
if opts.View != "" {
<input type="hidden" name="view" value={ opts.View }/>
}
if opts.Status != "" {
<input type="hidden" name="status" value={ opts.Status }/>
}
</form>
}