mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 04:15:30 +00:00
Sync search parameters with query parameters
This commit is contained in:
parent
49c38e99d5
commit
d14d76d952
4 changed files with 140 additions and 26 deletions
103
src/app/page.tsx
103
src/app/page.tsx
|
|
@ -3,9 +3,11 @@
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import logo from "../../public/sb_logo_large_3.png"
|
import logo from "../../public/sb_logo_large_3.png"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useDebouncedCallback } from 'use-debounce';
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useNonEmptyQueryParam } from "@/hooks/useNonEmptyQueryParam";
|
||||||
|
|
||||||
interface ZoekMatch {
|
interface ZoekMatch {
|
||||||
URL: string,
|
URL: string,
|
||||||
|
|
@ -35,27 +37,23 @@ interface ZoekSearchResult {
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
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 [fileMatches, setFileMatches] = useState<ZoekFileMatch[]>([]);
|
||||||
|
|
||||||
const onSearchChanged = useDebouncedCallback((query: string) => {
|
/**
|
||||||
if (query === "") {
|
* @note : when the user navigates backwards/forwards, the defaultQuery
|
||||||
setFileMatches([]);
|
* will update, but the query state will not. This effect keeps things in
|
||||||
return;
|
* sync for that scenario.
|
||||||
}
|
*/
|
||||||
console.log('making query...');
|
useEffect(() => {
|
||||||
fetch(`${document.baseURI}/zoekt/search?query=${query}&numResults=50`)
|
setQuery(defaultQuery);
|
||||||
.then(response => response.json())
|
}, [defaultQuery]);
|
||||||
.then(({ data }: { data: ZoekSearchResult }) => {
|
|
||||||
const result = data.result;
|
|
||||||
setFileMatches(result.FileMatches ?? []);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
}).finally(() => {
|
|
||||||
console.log('done making query');
|
|
||||||
})
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex h-screen flex-col">
|
<main className="flex h-screen flex-col">
|
||||||
|
|
@ -65,12 +63,14 @@ export default function Home() {
|
||||||
className="h-12 w-auto"
|
className="h-12 w-auto"
|
||||||
alt={"Sourcebot logo"}
|
alt={"Sourcebot logo"}
|
||||||
/>
|
/>
|
||||||
<Input
|
<SearchBar
|
||||||
className="max-w-lg"
|
query={query}
|
||||||
placeholder="Search..."
|
numResults={numResults}
|
||||||
onChange={(e) => {
|
onQueryChange={(query) => setQuery(query)}
|
||||||
const query = e.target.value;
|
onClear={() => setFileMatches([])}
|
||||||
onSearchChanged(query);
|
onSearchResult={({ result }) => {
|
||||||
|
setFileMatches(result.FileMatches ?? []);
|
||||||
|
router.push(`?query=${query}&numResults=${numResults}`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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 {
|
interface FileMatchProps {
|
||||||
match: ZoekFileMatch;
|
match: ZoekFileMatch;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { createPathWithQueryParams } from '@/lib/utils';
|
||||||
import { type NextRequest } from 'next/server'
|
import { type NextRequest } from 'next/server'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
|
|
@ -5,7 +6,14 @@ export async function GET(request: NextRequest) {
|
||||||
const query = searchParams.get('query');
|
const query = searchParams.get('query');
|
||||||
const numResults = searchParams.get('numResults');
|
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();
|
const data = await res.json();
|
||||||
|
|
||||||
return Response.json({ data })
|
return Response.json({ data })
|
||||||
|
|
|
||||||
33
src/hooks/useNonEmptyQueryParam.ts
Normal file
33
src/hooks/useNonEmptyQueryParam.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -4,3 +4,23 @@ import { twMerge } from "tailwind-merge"
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
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}`;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue