web: fix search debounce and client/server boundary
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import { graphql } from "@/gql";
|
||||
import { getClient } from "@/app/client";
|
||||
import {
|
||||
SearchContainer,
|
||||
type SearchResult,
|
||||
} from "@/components/search/SearchContainer";
|
||||
import { Suspense } from "react";
|
||||
SearchResults,
|
||||
} from "@/components/search/SearchResults";
|
||||
import { SearchShell } from "@/components/search/SearchShell";
|
||||
import { graphql } from "@/gql";
|
||||
|
||||
// TODO: seo metadata?
|
||||
|
||||
@@ -69,9 +69,9 @@ export default async function Page({
|
||||
|
||||
return (
|
||||
<main className="site-main" id="main">
|
||||
<Suspense key={query}>
|
||||
<SearchContainer query={query ?? ""} results={results} />
|
||||
</Suspense>
|
||||
<SearchShell initialQuery={query ?? ""}>
|
||||
{query ? <SearchResults results={results} /> : null}
|
||||
</SearchShell>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
+3
-58
@@ -1,18 +1,8 @@
|
||||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { PageHeader } from "../general/PageHeader";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
ImageFragmentDefinition,
|
||||
getSearchPath,
|
||||
stripHtml,
|
||||
} from "@/lib/common";
|
||||
import { ImageFragmentDefinition, stripHtml } from "@/lib/common";
|
||||
import { formatDate, formatOccurrenceMonths } from "@/lib/date";
|
||||
import { unmaskFragment } from "@/gql";
|
||||
import type { ImageFragment, SearchQuery } from "@/gql/graphql";
|
||||
import styles from "./searchContainer.module.scss";
|
||||
import { Icon } from "../general/Icon";
|
||||
import { Image } from "../general/Image";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -29,57 +19,12 @@ const PAGE_TYPES = {
|
||||
type SupportedTypename = keyof typeof PAGE_TYPES;
|
||||
type SupportedResult = Extract<SearchResult, { __typename: SupportedTypename }>;
|
||||
|
||||
export function SearchContainer({
|
||||
query,
|
||||
results,
|
||||
}: {
|
||||
query: string;
|
||||
results: SearchResult[];
|
||||
}) {
|
||||
const { replace } = useRouter();
|
||||
const [inputValue, setInputValue] = useState(query);
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(query);
|
||||
}, [query]);
|
||||
|
||||
const pushQuery = useDebouncedCallback((next: string) => {
|
||||
replace(getSearchPath(next));
|
||||
}, 500);
|
||||
|
||||
return (
|
||||
<div className={styles.searchContainer}>
|
||||
<PageHeader heading="Søk" />
|
||||
<div className={styles.searchField}>
|
||||
<input
|
||||
name="query"
|
||||
type="text"
|
||||
autoFocus
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
pushQuery(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.searchIcon}>
|
||||
<Icon type="search" />
|
||||
</div>
|
||||
</div>
|
||||
{query && <SearchResults results={results} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function capitalizeFirstLetter(s: string) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
function isSupported(result: SearchResult): result is SupportedResult {
|
||||
return (
|
||||
result.__typename in PAGE_TYPES &&
|
||||
"id" in result &&
|
||||
!!result.id
|
||||
);
|
||||
return result.__typename in PAGE_TYPES && "id" in result && !!result.id;
|
||||
}
|
||||
|
||||
function getResultType(result: SupportedResult): string {
|
||||
@@ -131,7 +76,7 @@ function getResultSnippet(result: SupportedResult): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function SearchResults({ results }: { results: SearchResult[] }) {
|
||||
export function SearchResults({ results }: { results: SearchResult[] }) {
|
||||
if (!results.length) {
|
||||
return <div className={styles.noResults}>Ingen resultater</div>;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
import { useEffect, useRef, useState, useTransition, type ReactNode } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { PageHeader } from "../general/PageHeader";
|
||||
import { getSearchPath } from "@/lib/common";
|
||||
import styles from "./searchContainer.module.scss";
|
||||
import { Icon } from "../general/Icon";
|
||||
|
||||
export function SearchShell({
|
||||
initialQuery,
|
||||
children,
|
||||
}: {
|
||||
initialQuery: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const { replace } = useRouter();
|
||||
const [inputValue, setInputValue] = useState(initialQuery);
|
||||
const [, startTransition] = useTransition();
|
||||
const lastPushedRef = useRef(initialQuery);
|
||||
|
||||
const pushQuery = useDebouncedCallback((next: string) => {
|
||||
lastPushedRef.current = next;
|
||||
startTransition(() => {
|
||||
replace(getSearchPath(next));
|
||||
});
|
||||
}, 300);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialQuery !== lastPushedRef.current) {
|
||||
lastPushedRef.current = initialQuery;
|
||||
setInputValue(initialQuery);
|
||||
}
|
||||
}, [initialQuery]);
|
||||
|
||||
return (
|
||||
<div className={styles.searchContainer}>
|
||||
<PageHeader heading="Søk" />
|
||||
<div className={styles.searchField}>
|
||||
<input
|
||||
name="query"
|
||||
type="text"
|
||||
autoFocus
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
setInputValue(e.target.value);
|
||||
pushQuery(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className={styles.searchIcon}>
|
||||
<Icon type="search" />
|
||||
</div>
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user