Skip to content

Commit

Permalink
Merge pull request #44 from penumbra-zone/#20-search-endpoint
Browse files Browse the repository at this point in the history
Implemented basic search result table, validators, and client page for #10
  • Loading branch information
ejmg authored Dec 18, 2023
2 parents 62a4d37 + e0b02b6 commit ac60df9
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 38 deletions.
41 changes: 14 additions & 27 deletions src/app/api/search/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export async function POST(req: Request) {
// const pageOffset = (parseInt(pageParam, 10)) * 10;

if (searchQuery.kind === QueryKind.BlockHeight) {
const blocksQuery = db.blocks.findMany({
const blocksQuery = await db.blocks.findFirst({
select: {
height: true,
created_at: true,
Expand All @@ -30,42 +30,29 @@ export async function POST(req: Request) {
height: searchQuery.value as bigint,
},
});
return new Response(JSON.stringify([blocksQuery]));

return new Response(JSON.stringify({
kind: searchQuery.kind,
created_at: blocksQuery?.created_at,
value: blocksQuery?.height,
}));

} else if (searchQuery.kind === QueryKind.TxHash) {
const txQuery = await db.tx_results.findFirstOrThrow({
const txQuery = await db.tx_results.findFirst({
select: {
tx_hash: true,
tx_result: true,
created_at: true,
events: {
select: {
type: true,
attributes: {
select: {
key: true,
value: true,
},
},
},
where: {
NOT: {
type: "tx",
},
},
},
blocks: {
select: {
height: true,
chain_id: true,
},
},
},
where: {
// value will be string when kind is TxHash
tx_hash: searchQuery.value as string,
},
});
return new Response(JSON.stringify([txQuery]));
return new Response(JSON.stringify({
kind: searchQuery.kind,
created_at: txQuery?.created_at,
value: txQuery?.tx_hash,
}));
} else {
// This should be impossible.
return new Response("Error processing query.", { status: 500 });
Expand Down
65 changes: 65 additions & 0 deletions src/app/search/[query]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";
import SearchResultsTable from "@/components/SearchResultsTable";
import { SearchResultValidator } from "@/lib/validators/search";
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { type FC } from "react";

interface PageProps {
params: {
query: string
}
}

const Page : FC<PageProps> = ({ params }) => {
const { query } = params;

const { data: searchResultData , isFetched, isError } = useQuery({
queryFn: async () => {
console.log(`Fetching: GET /api/search?q=${query}`);
const { data } = await axios.post(`/api/search?q=${query}`);
console.log("Fetched result:", data);
const result = SearchResultValidator.safeParse(data);
if (result.success) {
console.log(result.data);
return result.data;
} else {
throw new Error(result.error.message);
}
},
queryKey: ["searchResult", query],
retry: false,
meta: {
errorMessage: "Failed to find any results from the provided query. Please try a different query.",
},
});

if (isError) {
return (
<div className="py-5 flex justify-center">
<h1 className="text-4xl font-semibold">No results found.</h1>
</div>
);
}

return (
<div>
{isFetched ? (
<div>
<h1 className="text-3xl mx-auto py-5 font-semibold">Search results</h1>
{searchResultData ? (
<div className="flex flex-col justify-center w-full">
<SearchResultsTable data={[searchResultData]}/>
</div>
) : (
<p>No results</p>
)}
</div>
) : (
<p>loading...</p>
)}
</div>
);
};

export default Page;
45 changes: 45 additions & 0 deletions src/components/SearchResultsTable/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { QueryKind } from "@/lib/validators/search";
import { type ColumnDef } from "@tanstack/react-table";
import Link from "next/link";

export interface SearchResultsColumns {
kind: string,
created_at?: string,
value?: string | bigint
};

export const columns : Array<ColumnDef<SearchResultsColumns>> = [
{
accessorKey: "kind",
header: () => <div className="font-semibold text-gray-800">Kind</div>,
cell: ({ row }) => {
const kind : string = row.getValue("kind");
return <p className="">{kind}</p>;
},
},
{
accessorKey: "created_at",
header: () => <div className="font-semibold text-gray-800 text-center">Timestamp</div>,
cell: ({ row }) => {
const createdAt : string = row.getValue("created_at");
return <p className="text-xs text-center">{createdAt}</p>;
},
},
{
accessorKey: "value",
header: () => <div className="font-semibold text-gray-800 text-center">value</div>,
cell: ({ row }) => {
console.log(row);
const kind : string = row.getValue("kind");
if (kind === QueryKind.BlockHeight) {
const height: bigint = row.getValue("value");
return <Link href={`/block/${height}`} className="underline">{height.toString()}</Link>;
} else if (kind === QueryKind.TxHash) {
const txHash: string = row.getValue("value");
return <Link href={`/transaction/${txHash}`} className="underline">{txHash}</Link>;
}
},
},
];
22 changes: 22 additions & 0 deletions src/components/SearchResultsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { columns } from "./columns";
import { DataTable } from "../ui/data-table";
// import { type QueryKind } from "@/lib/validators/search";
import { type FC } from "react";

interface Props {
data: Array<{
kind: string,
created_at?: string,
value?: string | bigint
}>,
}

const SearchResultsTable : FC<Props> = ({ data }) => {
return (
<div>
<DataTable columns={columns} data={data}/>
</div>
);
};

export default SearchResultsTable;
18 changes: 8 additions & 10 deletions src/components/Searchbar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Command, CommandInput } from "../ui/command";
import { useToast } from "@/components/ui/use-toast";
import { usePathname, useRouter } from "next/navigation";
import { useOnClickOutside } from "usehooks-ts";
import { BlockHeightValidator, HashResultValidator } from "@/lib/validators/search";
import { SearchValidator } from "@/lib/validators/search";

const SearchBar : FC = () => {
const router = useRouter();
Expand All @@ -25,8 +25,8 @@ const SearchBar : FC = () => {
cmdRef.current?.blur();
}, [pathname]);

const searchCmd = ( endpoint: string ) => {
router.push(`/${endpoint}/${input}`);
const searchCmd = () => {
router.push(`/search/${input}`);
router.refresh();
};

Expand All @@ -45,13 +45,11 @@ const SearchBar : FC = () => {
onKeyDown={(e) => {
// Aside: Now that this is just a single command input, maybe just convert this to a generic input box?
if (e.key === "Enter" && input.length !== 0) {
const hashResult = HashResultValidator.safeParse(input);
const blockResult = BlockHeightValidator.safeParse(input);
if (hashResult.success) {
searchCmd("transaction");
} else if (blockResult.success) {
searchCmd("block");
} else {
const searchQuery = SearchValidator.safeParse(input);
if (searchQuery.success) {
searchCmd();
}
else {
toast({
variant: "destructive",
title: "Invalid search query.",
Expand Down
15 changes: 14 additions & 1 deletion src/lib/validators/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,20 @@ export const SearchValidator = z.union([
BlockHeightSearchValidator,
]);

export type SearchValidatorResult = z.infer<typeof SearchValidator>;
export type SearchValidatorT = z.infer<typeof SearchValidator>;

export const SearchResultValidator = z.discriminatedUnion("kind", [
z.object({
kind: z.literal("TX_HASH"),
value: HashResultValidator.optional(),
created_at: z.string().datetime().optional(),
}),
z.object({
kind: z.literal("BLOCK_HEIGHT"),
value: BlockHeightValidator.optional(),
created_at: z.string().datetime().optional(),
}),
]);

// zod schema equivalent to the /parsed/ JSON data returned by prisma in GET /api/transaction?q=<hash>
export const TransactionResult = z.tuple([
Expand Down

0 comments on commit ac60df9

Please sign in to comment.