feat: add standalone search page
This commit is contained in:
@@ -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"))
|
||||
|
||||
|
||||
@@ -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;
|
||||
setSearchState(true);
|
||||
searchInput.value = "";
|
||||
lastQuery = "";
|
||||
cancelScheduledFetch();
|
||||
setClearButtonState(false);
|
||||
clearResults();
|
||||
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();
|
||||
openSearch();
|
||||
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();
|
||||
|
||||
@@ -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" .}}
|
||||
|
||||
@@ -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
39
templates/search.gohtml
Normal 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}}
|
||||
Reference in New Issue
Block a user