Sync search parameters with query parameters

This commit is contained in:
bkellam 2024-08-24 19:39:59 -07:00
parent 49c38e99d5
commit d14d76d952
4 changed files with 140 additions and 26 deletions

View file

@ -3,9 +3,11 @@
import Image from "next/image";
import logo from "../../public/sb_logo_large_3.png"
import { Input } from "@/components/ui/input"
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useDebouncedCallback } from 'use-debounce';
import { Separator } from "@/components/ui/separator"
import { useRouter } from "next/navigation";
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
interface ZoekMatch {
URL: string,
@ -35,27 +37,23 @@ interface ZoekSearchResult {
export default function Home() {
const router = useRouter();
const defaultQuery = useNonEmptyQueryParam("query") ?? "";
const defaultNumResults = useNonEmptyQueryParam("numResults");
const [query, setQuery] = useState(defaultQuery);
const [numResults, _setNumResults] = useState(defaultNumResults && !isNaN(Number(defaultNumResults)) ? Number(defaultNumResults) : 100);
const [fileMatches, setFileMatches] = useState<ZoekFileMatch[]>([]);
const onSearchChanged = useDebouncedCallback((query: string) => {
if (query === "") {
setFileMatches([]);
return;
}
console.log('making query...');
fetch(`${document.baseURI}/zoekt/search?query=${query}&numResults=50`)
.then(response => response.json())
.then(({ data }: { data: ZoekSearchResult }) => {
const result = data.result;
setFileMatches(result.FileMatches ?? []);
})
.catch(error => {
console.error('Error:', error);
}).finally(() => {
console.log('done making query');
})
}, 200);
/**
* @note : when the user navigates backwards/forwards, the defaultQuery
* will update, but the query state will not. This effect keeps things in
* sync for that scenario.
*/
useEffect(() => {
setQuery(defaultQuery);
}, [defaultQuery]);
return (
<main className="flex h-screen flex-col">
@ -65,12 +63,14 @@ export default function Home() {
className="h-12 w-auto"
alt={"Sourcebot logo"}
/>
<Input
className="max-w-lg"
placeholder="Search..."
onChange={(e) => {
const query = e.target.value;
onSearchChanged(query);
<SearchBar
query={query}
numResults={numResults}
onQueryChange={(query) => setQuery(query)}
onClear={() => setFileMatches([])}
onSearchResult={({ result }) => {
setFileMatches(result.FileMatches ?? []);
router.push(`?query=${query}&numResults=${numResults}`);
}}
/>
</div>
@ -87,6 +87,59 @@ export default function Home() {
);
}
interface SearchBarProps {
query: string;
numResults: number;
onQueryChange: (query: string) => void;
onSearchResult: (result: ZoekSearchResult) => void,
onClear: () => void,
}
const SearchBar = ({
query,
numResults,
onQueryChange,
onSearchResult,
onClear,
}: SearchBarProps) => {
const SEARCH_DEBOUNCE_MS = 200;
const search = useDebouncedCallback((query: string) => {
if (query === "") {
onClear();
return;
}
console.log('making query...');
fetch(`http://localhost:3000/zoekt/search?query=${query}&numResults=${numResults}`)
.then(response => response.json())
.then(({ data }: { data: ZoekSearchResult }) => {
onSearchResult(data);
})
// @todo : error handling
.catch(error => {
console.error('Error:', error);
}).finally(() => {
console.log('done making query');
})
}, SEARCH_DEBOUNCE_MS);
useEffect(() => {
search(query);
}, [query]);
return (
<Input
value={query}
className="max-w-lg"
placeholder="Search..."
onChange={(e) => {
const query = e.target.value;
onQueryChange(query);
}}
/>
)
}
interface FileMatchProps {
match: ZoekFileMatch;
}

View file

@ -1,3 +1,4 @@
import { createPathWithQueryParams } from '@/lib/utils';
import { type NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
@ -5,7 +6,14 @@ export async function GET(request: NextRequest) {
const query = searchParams.get('query');
const numResults = searchParams.get('numResults');
const res = await fetch(`http://localhost:6070/search?q=${query}&num=${numResults}&format=json`);
const url = createPathWithQueryParams(
"http://localhost:6070/search",
["q", query],
["num", numResults],
["format", "json"],
);
console.log(url);
const res = await fetch(url);
const data = await res.json();
return Response.json({ data })

View file

@ -0,0 +1,33 @@
'use client';
import { useSearchParams } from "next/navigation";
import { useMemo } from "react";
/**
* Helper hook that returns the value of a query parameter if it is:
* a) defined, and
* b) non-empty
*
* otherwise it returns undefined.
*
* For example, let's assume we are calling `useNonEmptyQueryParam('bar')`:
* - `/foo?bar=hello` -> `hello`
* - `/foo?bar=` -> `undefined`
* - `/foo` -> `undefined`
*/
export const useNonEmptyQueryParam = (param: string) => {
const searchParams = useSearchParams();
const inviteId = useMemo(() => {
return getSearchParam(param, searchParams);
}, [param, searchParams]);
return inviteId;
};
/**
* @see useNonEmptyQueryParam
*/
export const getSearchParam = (param: string, searchParams: URLSearchParams | null) => {
const paramValue = searchParams?.get(param) ?? undefined;
return (paramValue !== undefined && paramValue.length > 0) ? paramValue : undefined;
}

View file

@ -4,3 +4,23 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/**
* Adds a list of (potentially undefined) query parameters to a path.
*
* @param path The path to add the query parameters to.
* @param queryParams A list of key-value pairs (key=param name, value=param value) to add to the path.
* @returns The path with the query parameters added.
*/
export const createPathWithQueryParams = (path: string, ...queryParams: [string, string | null][]) => {
// Filter out undefined values
// eslint-disable-next-line @typescript-eslint/no-unused-vars
queryParams = queryParams.filter(([_key, value]) => value !== null);
if (queryParams.length === 0) {
return path;
}
const queryString = queryParams.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value ?? '')}`).join('&');
return `${path}?${queryString}`;
}