refactor: switch ui scripts to data hooks

This commit is contained in:
2026-04-15 00:10:20 +02:00
parent 0a5f64c604
commit 29e49b9fcc
9 changed files with 111 additions and 45 deletions

View File

@@ -196,7 +196,7 @@ func joinStreamingNames(anime jikan.Anime) string {
templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string, animeTitleJapanese string, animeImage string, currentStatus string, airing bool) {
<div class="relative inline-block" id="watchlist-dropdown">
<button class="inline-flex h-8 cursor-pointer items-center gap-2 bg-[var(--panel-soft)] px-2 text-[0.8rem] text-[var(--text)]" onclick="toggleDropdown()">
<button class="inline-flex h-8 cursor-pointer items-center gap-2 bg-[var(--panel-soft)] px-2 text-[0.8rem] text-[var(--text)]" onclick="toggleDropdown()" data-dropdown-trigger>
if currentStatus != "" {
{ formatStatus(currentStatus) }
} else {
@@ -204,7 +204,7 @@ templ WatchlistDropdown(animeID int, animeTitle string, animeTitleEnglish string
}
<span class="text-[0.64rem]">▾</span>
</button>
<div class="invisible absolute left-0 top-[calc(100%+2px)] z-[110] min-w-[210px] bg-[var(--panel)] opacity-0 transition-opacity duration-150" data-dropdown-menu>
<div class="invisible absolute left-0 top-[calc(100%+2px)] z-[110] min-w-[210px] bg-[var(--panel)] opacity-0 transition-opacity duration-150" data-dropdown-menu data-dropdown-open-classes="visible opacity-100" data-dropdown-closed-classes="invisible opacity-0">
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "watching", currentStatus, airing)
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "completed", currentStatus, airing)
@dropdownStatusOption(animeID, animeTitle, animeTitleEnglish, animeTitleJapanese, animeImage, "on_hold", currentStatus, airing)

View File

@@ -19,6 +19,8 @@ templ Discover() {
hx-target="#discover-content"
hx-trigger="click"
data-tab-trigger
data-tab-active-classes="bg-[var(--surface-tab-active)] text-[var(--accent)]"
data-tab-inactive-classes="bg-[var(--panel-soft)] text-[var(--text-muted)]"
>
airing now
</button>
@@ -29,6 +31,8 @@ templ Discover() {
hx-target="#discover-content"
hx-trigger="click"
data-tab-trigger
data-tab-active-classes="bg-[var(--surface-tab-active)] text-[var(--accent)]"
data-tab-inactive-classes="bg-[var(--panel-soft)] text-[var(--text-muted)]"
>
upcoming
</button>

View File

@@ -36,7 +36,7 @@ templ Layout(title string, showHeader bool) {
<div class="relative ml-auto min-w-[240px] w-[min(420px,45vw)] max-[860px]:ml-0 max-[860px]:w-full" data-search-root>
<form action="/search" method="GET" class="w-full" id="search-form">
<input type="text" id="search-input" name="q" class="h-[34px] w-full border border-transparent bg-[var(--surface-search)] px-3 text-[var(--text)] transition-colors duration-120 placeholder:text-[var(--text-faint)] focus:border-[var(--surface-search-focus-border)] focus:outline-none" placeholder="Search anime..." autocomplete="off"/>
<div id="search-dropdown" class="absolute inset-x-0 top-[calc(100%+2px)] z-[120] max-h-[min(70vh,560px)] overflow-y-auto bg-[var(--panel)]"></div>
<div id="search-dropdown" class="absolute inset-x-0 top-[calc(100%+2px)] z-[120] max-h-[min(70vh,560px)] overflow-y-auto bg-[var(--panel)]" data-search-results-container></div>
</form>
</div>
</div>

View File

@@ -1,15 +1,32 @@
// static/js/anime.ts
(() => {
const parseClassList = (value) => {
if (!value) {
return [];
}
return value.split(" ").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
};
const setMenuState = (menu, isOpen) => {
const openClasses = parseClassList(menu.getAttribute("data-dropdown-open-classes"));
const closedClasses = parseClassList(menu.getAttribute("data-dropdown-closed-classes"));
if (isOpen) {
menu.classList.remove(...closedClasses);
menu.classList.add(...openClasses);
return;
}
menu.classList.remove(...openClasses);
menu.classList.add(...closedClasses);
};
const toggleDropdown = () => {
const dropdown = document.getElementById("watchlist-dropdown");
if (!dropdown) {
return;
}
dropdown.classList.toggle("open");
const isOpen = !dropdown.classList.contains("open");
dropdown.classList.toggle("open", isOpen);
const menu = dropdown.querySelector("[data-dropdown-menu]");
if (menu instanceof HTMLElement) {
menu.classList.toggle("invisible");
menu.classList.toggle("opacity-0");
setMenuState(menu, isOpen);
}
};
window.toggleDropdown = toggleDropdown;
@@ -26,8 +43,7 @@
dropdown.classList.remove("open");
const menu = dropdown.querySelector("[data-dropdown-menu]");
if (menu instanceof HTMLElement) {
menu.classList.add("invisible");
menu.classList.add("opacity-0");
setMenuState(menu, false);
}
}
});

View File

@@ -1,15 +1,40 @@
((): void => {
const parseClassList = (value: string | null): string[] => {
if (!value) {
return []
}
return value
.split(' ')
.map((entry: string): string => entry.trim())
.filter((entry: string): boolean => entry.length > 0)
}
const setMenuState = (menu: HTMLElement, isOpen: boolean): void => {
const openClasses = parseClassList(menu.getAttribute('data-dropdown-open-classes'))
const closedClasses = parseClassList(menu.getAttribute('data-dropdown-closed-classes'))
if (isOpen) {
menu.classList.remove(...closedClasses)
menu.classList.add(...openClasses)
return
}
menu.classList.remove(...openClasses)
menu.classList.add(...closedClasses)
}
const toggleDropdown = (): void => {
const dropdown = document.getElementById('watchlist-dropdown')
if (!dropdown) {
return
}
dropdown.classList.toggle('open')
const isOpen = !dropdown.classList.contains('open')
dropdown.classList.toggle('open', isOpen)
const menu = dropdown.querySelector('[data-dropdown-menu]')
if (menu instanceof HTMLElement) {
menu.classList.toggle('invisible')
menu.classList.toggle('opacity-0')
setMenuState(menu, isOpen)
}
}
@@ -30,8 +55,7 @@
dropdown.classList.remove('open')
const menu = dropdown.querySelector('[data-dropdown-menu]')
if (menu instanceof HTMLElement) {
menu.classList.add('invisible')
menu.classList.add('opacity-0')
setMenuState(menu, false)
}
}
})

View File

@@ -1,5 +1,11 @@
// static/js/discover.ts
(() => {
const parseClassList = (value) => {
if (!value) {
return [];
}
return value.split(" ").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
};
const setActiveTab = (clickedTab) => {
const group = clickedTab.closest('[data-tab-group="discover"]');
if (!group) {
@@ -7,13 +13,15 @@
}
const triggers = group.querySelectorAll("[data-tab-trigger]");
triggers.forEach((tab) => {
tab.classList.add("tab-trigger");
tab.classList.remove("bg-[var(--surface-tab-active)]", "text-[var(--accent)]");
tab.classList.add("bg-[var(--panel-soft)]", "text-[var(--text-muted)]");
const activeClasses2 = parseClassList(tab.getAttribute("data-tab-active-classes"));
const inactiveClasses2 = parseClassList(tab.getAttribute("data-tab-inactive-classes"));
tab.classList.remove(...activeClasses2);
tab.classList.add(...inactiveClasses2);
});
clickedTab.classList.add("tab-trigger");
clickedTab.classList.remove("bg-[var(--panel-soft)]", "text-[var(--text-muted)]");
clickedTab.classList.add("bg-[var(--surface-tab-active)]", "text-[var(--accent)]");
const activeClasses = parseClassList(clickedTab.getAttribute("data-tab-active-classes"));
const inactiveClasses = parseClassList(clickedTab.getAttribute("data-tab-inactive-classes"));
clickedTab.classList.remove(...inactiveClasses);
clickedTab.classList.add(...activeClasses);
};
document.addEventListener("click", (event) => {
const target = event.target;

View File

@@ -1,4 +1,15 @@
((): void => {
const parseClassList = (value: string | null): string[] => {
if (!value) {
return []
}
return value
.split(' ')
.map((entry: string): string => entry.trim())
.filter((entry: string): boolean => entry.length > 0)
}
const setActiveTab = (clickedTab: Element): void => {
const group = clickedTab.closest('[data-tab-group="discover"]')
if (!group) {
@@ -7,13 +18,16 @@
const triggers = group.querySelectorAll('[data-tab-trigger]')
triggers.forEach((tab: Element): void => {
tab.classList.add('tab-trigger')
tab.classList.remove('bg-[var(--surface-tab-active)]', 'text-[var(--accent)]')
tab.classList.add('bg-[var(--panel-soft)]', 'text-[var(--text-muted)]')
const activeClasses = parseClassList(tab.getAttribute('data-tab-active-classes'))
const inactiveClasses = parseClassList(tab.getAttribute('data-tab-inactive-classes'))
tab.classList.remove(...activeClasses)
tab.classList.add(...inactiveClasses)
})
clickedTab.classList.add('tab-trigger')
clickedTab.classList.remove('bg-[var(--panel-soft)]', 'text-[var(--text-muted)]')
clickedTab.classList.add('bg-[var(--surface-tab-active)]', 'text-[var(--accent)]')
const activeClasses = parseClassList(clickedTab.getAttribute('data-tab-active-classes'))
const inactiveClasses = parseClassList(clickedTab.getAttribute('data-tab-inactive-classes'))
clickedTab.classList.remove(...inactiveClasses)
clickedTab.classList.add(...activeClasses)
}
document.addEventListener('click', (event: MouseEvent): void => {

View File

@@ -7,7 +7,7 @@
globalWindow.searchInitialized = true;
let searchTimeout;
const searchInput = document.getElementById("search-input");
const searchDropdown = document.getElementById("search-dropdown");
const searchDropdown = document.querySelector("[data-search-results-container]");
if (!searchInput || !searchDropdown) {
return;
}
@@ -31,42 +31,42 @@
return;
}
const searchResults = document.createElement("div");
searchResults.className = "search-results";
searchResults.className = "grid";
const title = document.createElement("div");
title.className = "search-results-title";
title.className = "px-3 py-2 text-[0.68rem] text-[var(--text-faint)]";
title.textContent = "Anime";
searchResults.appendChild(title);
results.forEach((result) => {
const item = document.createElement("a");
item.className = "search-result-item";
item.className = "flex items-start gap-3 px-3 py-2 text-inherit no-underline hover:bg-[var(--panel-soft)] hover:no-underline";
item.setAttribute("href", "/anime/" + encodeURIComponent(String(result.id || "")));
if (isSafeImageUrl(result.image)) {
const img = document.createElement("img");
img.className = "search-result-thumb";
img.className = "aspect-[2/3] w-[42px] shrink-0 object-cover bg-[var(--surface-thumb)]";
img.setAttribute("src", result.image || "");
img.setAttribute("alt", String(result.title || ""));
item.appendChild(img);
} else {
const noImage = document.createElement("div");
noImage.className = "search-result-no-image";
noImage.className = "aspect-[2/3] w-[42px] shrink-0 bg-[var(--surface-thumb)] text-[0] text-transparent";
noImage.textContent = "no image";
item.appendChild(noImage);
}
const info = document.createElement("div");
info.className = "search-result-info";
info.className = "grid min-w-0 gap-px";
const itemTitle = document.createElement("div");
itemTitle.className = "search-result-title";
itemTitle.className = "line-clamp-1 text-[0.86rem] leading-[1.3] text-[var(--text)]";
itemTitle.textContent = String(result.title || "");
info.appendChild(itemTitle);
const itemType = document.createElement("div");
itemType.className = "search-result-type";
itemType.className = "text-[0.67rem] text-[var(--text-faint)]";
itemType.textContent = String(result.type || "");
info.appendChild(itemType);
item.appendChild(info);
searchResults.appendChild(item);
});
const viewAll = document.createElement("a");
viewAll.className = "search-result-view-all";
viewAll.className = "bg-[var(--surface-search-view-all)] px-3 py-2 text-center text-[0.8rem] text-[var(--text-muted)] no-underline hover:bg-[var(--panel-soft)] hover:text-[var(--text)] hover:no-underline";
viewAll.setAttribute("href", "/search?q=" + encodeURIComponent(query));
viewAll.textContent = "View all results for " + query;
searchResults.appendChild(viewAll);

View File

@@ -7,7 +7,7 @@
let searchTimeout: number | undefined
const searchInput = document.getElementById('search-input') as HTMLInputElement | null
const searchDropdown = document.getElementById('search-dropdown')
const searchDropdown = document.querySelector('[data-search-results-container]') as HTMLElement | null
if (!searchInput || !searchDropdown) {
return
@@ -39,41 +39,41 @@
}
const searchResults = document.createElement('div')
searchResults.className = 'search-results'
searchResults.className = 'grid'
const title = document.createElement('div')
title.className = 'search-results-title'
title.className = 'px-3 py-2 text-[0.68rem] text-[var(--text-faint)]'
title.textContent = 'Anime'
searchResults.appendChild(title)
results.forEach((result): void => {
const item = document.createElement('a')
item.className = 'search-result-item'
item.className = 'flex items-start gap-3 px-3 py-2 text-inherit no-underline hover:bg-[var(--panel-soft)] hover:no-underline'
item.setAttribute('href', '/anime/' + encodeURIComponent(String(result.id || '')))
if (isSafeImageUrl(result.image)) {
const img = document.createElement('img')
img.className = 'search-result-thumb'
img.className = 'aspect-[2/3] w-[42px] shrink-0 object-cover bg-[var(--surface-thumb)]'
img.setAttribute('src', result.image || '')
img.setAttribute('alt', String(result.title || ''))
item.appendChild(img)
} else {
const noImage = document.createElement('div')
noImage.className = 'search-result-no-image'
noImage.className = 'aspect-[2/3] w-[42px] shrink-0 bg-[var(--surface-thumb)] text-[0] text-transparent'
noImage.textContent = 'no image'
item.appendChild(noImage)
}
const info = document.createElement('div')
info.className = 'search-result-info'
info.className = 'grid min-w-0 gap-px'
const itemTitle = document.createElement('div')
itemTitle.className = 'search-result-title'
itemTitle.className = 'line-clamp-1 text-[0.86rem] leading-[1.3] text-[var(--text)]'
itemTitle.textContent = String(result.title || '')
info.appendChild(itemTitle)
const itemType = document.createElement('div')
itemType.className = 'search-result-type'
itemType.className = 'text-[0.67rem] text-[var(--text-faint)]'
itemType.textContent = String(result.type || '')
info.appendChild(itemType)
@@ -82,7 +82,7 @@
})
const viewAll = document.createElement('a')
viewAll.className = 'search-result-view-all'
viewAll.className = 'bg-[var(--surface-search-view-all)] px-3 py-2 text-center text-[0.8rem] text-[var(--text-muted)] no-underline hover:bg-[var(--panel-soft)] hover:text-[var(--text)] hover:no-underline'
viewAll.setAttribute('href', '/search?q=' + encodeURIComponent(query))
viewAll.textContent = 'View all results for ' + query
searchResults.appendChild(viewAll)