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/popular", h.HandleCatalogPopular)
|
||||||
r.GET("/api/catalog/continue", h.HandleCatalogContinue)
|
r.GET("/api/catalog/continue", h.HandleCatalogContinue)
|
||||||
r.GET("/api/catalog/top-pick", h.HandleCatalogTopPickForYou)
|
r.GET("/api/catalog/top-pick", h.HandleCatalogTopPickForYou)
|
||||||
|
r.GET("/search", h.HandleSearch)
|
||||||
r.GET("/discover", h.HandleDiscover)
|
r.GET("/discover", h.HandleDiscover)
|
||||||
r.GET("/discover/top-picks", h.HandleDiscoverTopPicksForYou)
|
r.GET("/discover/top-picks", h.HandleDiscoverTopPicksForYou)
|
||||||
r.GET("/api/discover/trending", h.HandleDiscoverTrending)
|
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)
|
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) {
|
func producerQueryParams(c *gin.Context) (string, int, int, error) {
|
||||||
q := strings.TrimSpace(c.Query("q"))
|
q := strings.TrimSpace(c.Query("q"))
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const searchResults = document.querySelector(
|
|||||||
) as HTMLElement | null;
|
) as HTMLElement | null;
|
||||||
const searchDialog = document.querySelector("[data-command-palette-dialog]") 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 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 searchOpenButtons = document.querySelectorAll("[data-command-palette-open]");
|
||||||
const searchCloseButtons = document.querySelectorAll("[data-command-palette-close]");
|
const searchCloseButtons = document.querySelectorAll("[data-command-palette-close]");
|
||||||
const searchClearButtons = document.querySelectorAll("[data-command-palette-clear]");
|
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 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) {
|
if (nextItems.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -502,6 +505,7 @@ const fetchSearchItems = (query: string): void => {
|
|||||||
|
|
||||||
if (query === "") {
|
if (query === "") {
|
||||||
clearResults();
|
clearResults();
|
||||||
|
renderEmptyState("");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,18 +624,21 @@ const scheduleFetch = (): void => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const openSearch = (): void => {
|
const openSearch = (): void => {
|
||||||
if (!searchDialog || !searchInput) {
|
if (!searchInput) {
|
||||||
|
window.location.href = "/search";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastFocusedSearchOpener =
|
lastFocusedSearchOpener =
|
||||||
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
setSearchState(true);
|
if (searchDialog) {
|
||||||
searchInput.value = "";
|
setSearchState(true);
|
||||||
lastQuery = "";
|
searchInput.value = "";
|
||||||
cancelScheduledFetch();
|
lastQuery = "";
|
||||||
setClearButtonState(false);
|
cancelScheduledFetch();
|
||||||
clearResults();
|
setClearButtonState(false);
|
||||||
|
clearResults();
|
||||||
|
}
|
||||||
searchInput.focus();
|
searchInput.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -695,7 +702,9 @@ const onDocumentKeydown = (event: KeyboardEvent): void => {
|
|||||||
|
|
||||||
if (commandShortcut && !isTypingTarget(event.target)) {
|
if (commandShortcut && !isTypingTarget(event.target)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (isSearchOpen()) {
|
if (searchPage) {
|
||||||
|
searchInput?.focus();
|
||||||
|
} else if (isSearchOpen()) {
|
||||||
closeSearch();
|
closeSearch();
|
||||||
} else {
|
} else {
|
||||||
openSearch();
|
openSearch();
|
||||||
@@ -705,7 +714,11 @@ const onDocumentKeydown = (event: KeyboardEvent): void => {
|
|||||||
|
|
||||||
if (event.key === "/" && !isTypingTarget(event.target)) {
|
if (event.key === "/" && !isTypingTarget(event.target)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
openSearch();
|
if (searchPage) {
|
||||||
|
searchInput?.focus();
|
||||||
|
} else {
|
||||||
|
openSearch();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,6 +754,17 @@ const initSearchOverlay = (): void => {
|
|||||||
document.addEventListener("click", onDocumentClick);
|
document.addEventListener("click", onDocumentClick);
|
||||||
document.addEventListener("keydown", onDocumentKeydown);
|
document.addEventListener("keydown", onDocumentKeydown);
|
||||||
searchDialog?.setAttribute("aria-hidden", "true");
|
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();
|
initSearchOverlay();
|
||||||
|
|||||||
@@ -99,37 +99,6 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</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">
|
<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">
|
<div class="flex-1 p-4 md:p-8">
|
||||||
{{block "page_container" .}}
|
{{block "page_container" .}}
|
||||||
|
|||||||
@@ -74,18 +74,20 @@
|
|||||||
<nav class="bg-background-sidebar h-full">
|
<nav class="bg-background-sidebar h-full">
|
||||||
<div class="flex h-full flex-col justify-between">
|
<div class="flex h-full flex-col justify-between">
|
||||||
<div class="flex flex-col">
|
<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}}>
|
<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}}">
|
||||||
<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">
|
{{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" />
|
<circle cx="11" cy="11" r="8" />
|
||||||
<path d="m21 21-4.35-4.35" />
|
<path d="m21 21-4.35-4.35" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="nav-label-container ml-4 grid min-w-0 flex-1 grid-cols-[1fr] opacity-100">
|
<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">
|
<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>
|
<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>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
{{range $navItems}}
|
{{range $navItems}}
|
||||||
{{template "nav_item" dict "key" .key "href" .href "label" .label "isActive" .isActive "isCollapsed" $isCollapsed}}
|
{{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