Files
jsonhero-web/app/hooks/useJsonSearch.tsx
T
James Ritchie 0632e82a4b Squashed commit of the following:
commit 09bed34946
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 8 14:03:50 2022 +0100

    Fixed the layout of a search list item

commit 90126d43ad
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 17:25:13 2022 +0100

    Insanely complicated path search results highlighitng and truncating

commit a4bbd5521e
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 14:05:01 2022 +0100

    Improved the title editing in the header

commit 5814053968
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 13:38:33 2022 +0100

    Improved text highlight colour

commit 110de37242
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 13:36:57 2022 +0100

    Icon hover state now uses isHighlighted

commit fb6da943d9
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 13:31:11 2022 +0100

    Light mode styling

commit 2c378d5158
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 12:24:43 2022 +0100

    Spacing between search items no longer a hack

commit 85d90bb0ae
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 12:19:10 2022 +0100

    Added ellipsis to search matches if they are windowed

commit 91f3e70676
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 12:08:00 2022 +0100

    Fixed search item icons

commit e384c87c6f
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 11:52:58 2022 +0100

    Improved hover state

commit f4681edc07
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 11:47:33 2022 +0100

    Removed focus state on button

commit faa2d0663c
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 11:26:42 2022 +0100

    made the search palette a little bigger

commit df8bd521fe
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 11:26:14 2022 +0100

    Remove onOverlayClick prop

commit ecd046b3b2
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 11:18:46 2022 +0100

    Close search when the overlay is clicked

commit fba330b5d9
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 11:10:35 2022 +0100

    hitting esc when search input is focused and empty should close the search

commit fc2fe8ae6f
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 11:00:08 2022 +0100

    Highlight the window of the best search match in the search results

commit 45193ec92c
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 10:45:01 2022 +0100

    Now you can arrow down to the last item in the list

commit 0de5fef7e9
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Apr 1 10:37:20 2022 +0100

    Added a hacky margin between items and remove the transition to make it feel snappier

commit 167b548343
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Apr 1 09:30:33 2022 +0100

    WIP implementing search

commit 5655631f12
Author: James Ritchie <james@jamesritchie.co.uk>
Date:   Fri Mar 25 15:28:19 2022 +0000

    Build and styled search modal

commit e368db81c8
Author: Eric Allam <eallam@icloud.com>
Date:   Fri Mar 25 14:42:03 2022 +0000

    Implement JSON search through fuse.js + web worker + react hook

commit 8caa11ba1c
Author: Eric Allam <eallam@icloud.com>
Date:   Wed Mar 23 10:21:33 2022 +0000

    Start of the Command Palette using radix-ui Dialog

commit 904b64d781
Author: Eric Allam <eallam@icloud.com>
Date:   Wed Mar 23 09:58:52 2022 +0000

    Uncommented out the search bar field
2022-04-08 14:07:39 +01:00

259 lines
5.8 KiB
TypeScript

import { useJson } from "./useJson";
import Fuse from "fuse.js";
import { JsonSearchEntry } from "~/utilities/search";
import {
createContext,
useCallback,
useContext,
useEffect,
useReducer,
useRef,
} from "react";
export type InitializeIndexEvent = {
type: "initialize-index";
payload: { json: unknown; fuseOptions: Fuse.IFuseOptions<JsonSearchEntry> };
};
export type SearchEvent = {
type: "search";
payload: { query: string };
};
export type SearchSendWorkerEvent = InitializeIndexEvent | SearchEvent;
export type IndexInitializedEvent = {
type: "index-initialized";
};
export type SearchResultsEvent = {
type: "search-results";
payload: { results: Fuse.FuseResult<JsonSearchEntry>[]; query: string };
};
export type SearchReceiveWorkerEvent =
| IndexInitializedEvent
| SearchResultsEvent;
export type JsonSearchApi = {
search: (query: string) => void;
reset: () => void;
};
const JsonSearchStateContext = createContext<JsonSearchState>(
{} as JsonSearchState
);
const JsonSearchApiContext = createContext<JsonSearchApi>({} as JsonSearchApi);
export type JsonSearchState = {
status: "initializing" | "idle" | "searching";
query?: string;
results?: Fuse.FuseResult<JsonSearchEntry>[];
};
type SearchAction = {
type: "search";
payload: { query: string };
};
type ResetAction = {
type: "reset";
};
type JsonSearchAction = SearchReceiveWorkerEvent | SearchAction | ResetAction;
function reducer(
state: JsonSearchState,
action: JsonSearchAction
): JsonSearchState {
switch (state.status) {
case "initializing": {
if (action.type === "index-initialized") {
return {
...state,
status: "idle",
results: undefined,
};
}
return state;
}
case "idle": {
if (action.type === "reset") {
return {
...state,
query: undefined,
results: undefined,
};
}
if (action.type === "search") {
return {
...state,
status: "searching",
query: action.payload.query,
};
}
return state;
}
case "searching": {
if (action.type === "reset") {
return {
...state,
status: "idle",
query: undefined,
results: undefined,
};
}
if (
action.type === "search-results" &&
state.query === action.payload.query
) {
return {
...state,
status: "idle",
results: action.payload.results,
};
}
return state;
}
}
}
let lastAction: any | undefined;
function wrapReducer<S, A extends { type: string }>(
name: string,
reducer: React.Reducer<S, A>
): React.Reducer<S, A> {
return (state, action) => {
const next = reducer(state, action);
if (process.env.NODE_ENV !== "production") {
if (!lastAction) {
console.groupCollapsed(
`%cAction: %c${
name + " " + action.type
} %cat ${getCurrentTimeFormatted()}`,
"color: lightgreen; font-weight: bold;",
"color: white; font-weight: bold;",
"color: lightblue; font-weight: lighter;"
);
console.log(
"%cPrevious State:",
"color: #9E9E9E; font-weight: 700;",
state
);
console.log("%cAction:", "color: #00A7F7; font-weight: 700;", action);
console.log("%cNext State:", "color: #47B04B; font-weight: 700;", next);
console.groupEnd();
lastAction = action;
} else {
lastAction = undefined;
}
}
return next;
};
}
const getCurrentTimeFormatted = () => {
const currentTime = new Date();
const hours = currentTime.getHours();
const minutes = currentTime.getMinutes();
const seconds = currentTime.getSeconds();
const milliseconds = currentTime.getMilliseconds();
return `${hours}:${minutes}:${seconds}.${milliseconds}`;
};
export function JsonSearchProvider({
children,
}: {
children: React.ReactNode;
}) {
const [json] = useJson();
const [state, dispatch] = useReducer<
React.Reducer<JsonSearchState, JsonSearchAction>
>(wrapReducer("jsonSearch", reducer), { status: "initializing" });
const search = useCallback(
(query: string) => {
dispatch({ type: "search", payload: { query } });
},
[dispatch]
);
const reset = useCallback(() => {
dispatch({ type: "reset" });
}, [dispatch]);
const handleWorkerMessage = useCallback(
(e: MessageEvent<SearchReceiveWorkerEvent>) => dispatch(e.data),
[dispatch]
);
const workerRef = useRef<Worker | null>();
useEffect(() => {
if (typeof window === "undefined" || typeof window.Worker === "undefined") {
return;
}
if (workerRef.current) {
return;
}
const worker = new Worker("/entry.worker.js");
worker.onmessage = handleWorkerMessage;
workerRef.current = worker;
workerRef.current.postMessage({
type: "initialize-index",
payload: {
json,
fuseOptions: {
ignoreLocation: true,
includeScore: true,
includeMatches: true,
minMatchCharLength: 2,
isCaseSensitive: false,
ignoreFieldNorm: true,
},
},
});
}, [json, workerRef.current]);
useEffect(() => {
if (state.status !== "searching") {
return;
}
workerRef.current?.postMessage({
type: "search",
payload: { query: state.query },
});
}, [state.status, workerRef.current]);
return (
<JsonSearchStateContext.Provider value={state}>
<JsonSearchApiContext.Provider value={{ search, reset }}>
{children}
</JsonSearchApiContext.Provider>
</JsonSearchStateContext.Provider>
);
}
export function useJsonSearchState(): JsonSearchState {
return useContext(JsonSearchStateContext);
}
export function useJsonSearchApi(): JsonSearchApi {
return useContext(JsonSearchApiContext);
}