feat: add standalone search page

This commit is contained in:
2026-06-13 16:27:14 +02:00
parent bf85c3b018
commit 9b7a2cac8f
5 changed files with 88 additions and 46 deletions

View File

@@ -125,6 +125,7 @@ func (h *AnimeHandler) Register(r *gin.Engine) {
r.GET("/api/catalog/popular", h.HandleCatalogPopular)
r.GET("/api/catalog/continue", h.HandleCatalogContinue)
r.GET("/api/catalog/top-pick", h.HandleCatalogTopPickForYou)
r.GET("/search", h.HandleSearch)
r.GET("/discover", h.HandleDiscover)
r.GET("/discover/top-picks", h.HandleDiscoverTopPicksForYou)
r.GET("/api/discover/trending", h.HandleDiscoverTrending)
@@ -142,6 +143,13 @@ func (h *AnimeHandler) Register(r *gin.Engine) {
r.GET("/api/jikan/producers", h.HandleProducers)
}
func (h *AnimeHandler) HandleSearch(c *gin.Context) {
c.HTML(http.StatusOK, "search.gohtml", gin.H{
"User": server.CurrentUser(c),
"CurrentPath": "/search",
})
}
func producerQueryParams(c *gin.Context) (string, int, int, error) {
q := strings.TrimSpace(c.Query("q"))

View File

@@ -25,6 +25,7 @@ const searchResults = document.querySelector(
) as HTMLElement | null;
const searchDialog = document.querySelector("[data-command-palette-dialog]") as HTMLElement | null;
const searchRoot = document.querySelector("[data-command-palette-root]") as HTMLElement | null;
const searchPage = document.querySelector("[data-command-palette-page]") as HTMLElement | null;
const searchOpenButtons = document.querySelectorAll("[data-command-palette-open]");
const searchCloseButtons = document.querySelectorAll("[data-command-palette-close]");
const searchClearButtons = document.querySelectorAll("[data-command-palette-clear]");
@@ -446,7 +447,9 @@ const appendItems = (items: CommandPaletteItem[]): void => {
}
const existingIDs = new Set(resultItems.map((item) => item.id));
const nextItems = dedupeByID(items, (item) => item.id).filter((item) => !existingIDs.has(item.id));
const nextItems = dedupeByID(items, (item) => item.id).filter(
(item) => !existingIDs.has(item.id),
);
if (nextItems.length === 0) {
return;
}
@@ -502,6 +505,7 @@ const fetchSearchItems = (query: string): void => {
if (query === "") {
clearResults();
renderEmptyState("");
return;
}
@@ -620,18 +624,21 @@ const scheduleFetch = (): void => {
};
const openSearch = (): void => {
if (!searchDialog || !searchInput) {
if (!searchInput) {
window.location.href = "/search";
return;
}
lastFocusedSearchOpener =
document.activeElement instanceof HTMLElement ? document.activeElement : null;
if (searchDialog) {
setSearchState(true);
searchInput.value = "";
lastQuery = "";
cancelScheduledFetch();
setClearButtonState(false);
clearResults();
}
searchInput.focus();
};
@@ -695,7 +702,9 @@ const onDocumentKeydown = (event: KeyboardEvent): void => {
if (commandShortcut && !isTypingTarget(event.target)) {
event.preventDefault();
if (isSearchOpen()) {
if (searchPage) {
searchInput?.focus();
} else if (isSearchOpen()) {
closeSearch();
} else {
openSearch();
@@ -705,7 +714,11 @@ const onDocumentKeydown = (event: KeyboardEvent): void => {
if (event.key === "/" && !isTypingTarget(event.target)) {
event.preventDefault();
if (searchPage) {
searchInput?.focus();
} else {
openSearch();
}
return;
}
@@ -741,6 +754,17 @@ const initSearchOverlay = (): void => {
document.addEventListener("click", onDocumentClick);
document.addEventListener("keydown", onDocumentKeydown);
searchDialog?.setAttribute("aria-hidden", "true");
const initialQuery = new URLSearchParams(window.location.search).get("q")?.trim() || "";
if (initialQuery) {
searchInput.value = initialQuery;
fetchSearchItems(initialQuery);
} else {
renderEmptyState("");
}
if (searchPage) {
searchInput.focus();
}
};
initSearchOverlay();

View File

@@ -99,37 +99,6 @@
{{end}}
</div>
<div class="fixed inset-0 z-100 hidden flex-col bg-background" data-command-palette-dialog aria-hidden="true">
<div class="border-b border-(--border) bg-background shadow-[0_10px_28px_rgba(0,0,0,0.18)]" data-command-palette-root role="dialog" aria-modal="true" aria-label="Search anime">
<label for="command-palette-input" class="sr-only">Search commands and anime</label>
<div class="mx-auto flex min-h-32 w-full max-w-4xl items-end gap-4 px-5 pb-6 pt-8 md:min-h-40 md:px-8 md:pb-7">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-3 border-b-2 border-accent">
<svg class="size-9 shrink-0 text-foreground-muted/90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
id="command-palette-input"
name="q"
type="search"
autocomplete="off"
placeholder="Search anime..."
class="min-w-0 flex-1 bg-transparent py-3 text-3xl font-medium text-foreground placeholder:text-foreground-muted outline-none md:text-4xl"
/>
<button type="button" data-unstyled-button class="pointer-events-none flex size-10 shrink-0 items-center justify-center text-foreground-muted opacity-0 transition hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent" data-command-palette-clear aria-label="Clear search">
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<div data-command-palette-results class="min-h-0 flex-1 overflow-y-auto"></div>
</div>
<main class="w-full flex-1 flex flex-col h-[calc(100dvh-3.5rem)] overflow-y-auto lg:h-screen">
<div class="flex-1 p-4 md:p-8">
{{block "page_container" .}}

View File

@@ -74,18 +74,20 @@
<nav class="bg-background-sidebar h-full">
<div class="flex h-full flex-col justify-between">
<div class="flex flex-col">
<button type="button" data-unstyled-button class="group relative flex w-full items-center px-7 py-3 text-left transition-colors hover:bg-surface-hover" data-command-palette-open {{if $isCollapsed}}title="Search"{{end}}>
<svg class="size-6 shrink-0 text-foreground-muted transition-colors duration-200 group-hover:text-foreground" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<a href="/search" class="group relative flex w-full items-center px-7 py-3 text-left transition-colors hover:bg-surface-hover" {{if $isCollapsed}}title="Search"{{end}} aria-current="{{if eq $currentPath "/search"}}page{{end}}">
{{if eq $currentPath "/search"}}
<div class="bg-accent absolute top-1/2 left-0 h-12 w-0.5 -translate-y-1/2 rounded-r-sm"></div>
{{end}}
<svg class="size-6 shrink-0 transition-colors duration-200 {{if eq $currentPath "/search"}}text-accent{{else}}text-foreground-muted group-hover:text-foreground{{end}}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<div class="nav-label-container ml-4 grid min-w-0 flex-1 grid-cols-[1fr] opacity-100">
<div class="flex min-w-0 items-center justify-between gap-3 overflow-hidden">
<span class="whitespace-nowrap text-sm font-medium text-foreground-muted transition-colors duration-200 group-hover:text-foreground">Search</span>
<kbd class="hidden whitespace-nowrap bg-background-surface px-1.5 py-0.5 text-[0.65rem] font-medium text-foreground-muted lg:inline" data-command-palette-shortcut>⌘P</kbd>
<span class="whitespace-nowrap text-sm font-medium transition-colors duration-200 {{if eq $currentPath "/search"}}text-accent{{else}}text-foreground-muted group-hover:text-foreground{{end}}">Search</span>
</div>
</div>
</button>
</a>
{{range $navItems}}
{{template "nav_item" dict "key" .key "href" .href "label" .label "isActive" .isActive "isCollapsed" $isCollapsed}}

39
templates/search.gohtml Normal file
View File

@@ -0,0 +1,39 @@
{{define "title"}}Search{{end}}
{{define "page_container"}}
<div class="-m-4 md:-m-8">
{{template "content" .}}
</div>
{{end}}
{{define "content"}}
<div class="flex min-h-[calc(100dvh-7.5rem)] flex-col bg-background" data-command-palette-page>
<div class="border-b border-(--border) bg-background shadow-[0_10px_28px_rgba(0,0,0,0.18)]" data-command-palette-root aria-label="Search anime">
<label for="command-palette-input" class="sr-only">Search anime</label>
<div class="mx-auto flex min-h-32 w-full max-w-4xl items-end gap-4 px-5 pb-6 pt-8 md:min-h-40 md:px-8 md:pb-7">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-3 border-b-2 border-accent">
<svg class="size-9 shrink-0 text-foreground-muted/90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
id="command-palette-input"
name="q"
type="search"
autocomplete="off"
placeholder="Search anime..."
class="min-w-0 flex-1 bg-transparent py-3 text-3xl font-medium text-foreground placeholder:text-foreground-muted outline-none md:text-4xl"
/>
<button type="button" data-unstyled-button class="pointer-events-none flex size-10 shrink-0 items-center justify-center text-foreground-muted opacity-0 transition hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent" data-command-palette-clear aria-label="Clear search">
<svg class="size-6" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round">
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
</div>
</div>
</div>
<div data-command-palette-results class="min-h-0 flex-1 overflow-y-auto"></div>
</div>
{{end}}