refactor: reorganize project structure following go standards
This commit is contained in:
64
web/components/anime_card.templ
Normal file
64
web/components/anime_card.templ
Normal 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
|
||||
}
|
||||
45
web/components/anime_list.templ
Normal file
45
web/components/anime_list.templ
Normal 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(),
|
||||
})
|
||||
}
|
||||
10
web/components/empty_state.templ
Normal file
10
web/components/empty_state.templ
Normal 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>
|
||||
}
|
||||
9
web/components/icons/icons.templ
Normal file
9
web/components/icons/icons.templ
Normal 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>
|
||||
}
|
||||
10
web/components/loading.templ
Normal file
10
web/components/loading.templ
Normal 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>
|
||||
}
|
||||
37
web/components/sort_filter.templ
Normal file
37
web/components/sort_filter.templ
Normal 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>
|
||||
}
|
||||
Reference in New Issue
Block a user