commit09bed34946Author: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 8 14:03:50 2022 +0100 Fixed the layout of a search list item commit90126d43adAuthor: Eric Allam <eallam@icloud.com> Date: Fri Apr 1 17:25:13 2022 +0100 Insanely complicated path search results highlighitng and truncating commita4bbd5521eAuthor: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 1 14:05:01 2022 +0100 Improved the title editing in the header commit5814053968Author: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 1 13:38:33 2022 +0100 Improved text highlight colour commit110de37242Author: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 1 13:36:57 2022 +0100 Icon hover state now uses isHighlighted commitfb6da943d9Author: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 1 13:31:11 2022 +0100 Light mode styling commit2c378d5158Author: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 1 12:24:43 2022 +0100 Spacing between search items no longer a hack commit85d90bb0aeAuthor: Eric Allam <eallam@icloud.com> Date: Fri Apr 1 12:19:10 2022 +0100 Added ellipsis to search matches if they are windowed commit91f3e70676Author: Eric Allam <eallam@icloud.com> Date: Fri Apr 1 12:08:00 2022 +0100 Fixed search item icons commite384c87c6fAuthor: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 1 11:52:58 2022 +0100 Improved hover state commitf4681edc07Author: James Ritchie <james@jamesritchie.co.uk> Date: Fri Apr 1 11:47:33 2022 +0100 Removed focus state on button commitfaa2d0663cAuthor: Eric Allam <eallam@icloud.com> Date: Fri Apr 1 11:26:42 2022 +0100 made the search palette a little bigger commitdf8bd521feAuthor: Eric Allam <eallam@icloud.com> Date: Fri Apr 1 11:26:14 2022 +0100 Remove onOverlayClick prop commitecd046b3b2Author: Eric Allam <eallam@icloud.com> Date: Fri Apr 1 11:18:46 2022 +0100 Close search when the overlay is clicked commitfba330b5d9Author: 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 commitfc2fe8ae6fAuthor: 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 commit45193ec92cAuthor: 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 commit0de5fef7e9Author: 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 commit167b548343Author: Eric Allam <eallam@icloud.com> Date: Fri Apr 1 09:30:33 2022 +0100 WIP implementing search commit5655631f12Author: James Ritchie <james@jamesritchie.co.uk> Date: Fri Mar 25 15:28:19 2022 +0000 Build and styled search modal commite368db81c8Author: Eric Allam <eallam@icloud.com> Date: Fri Mar 25 14:42:03 2022 +0000 Implement JSON search through fuse.js + web worker + react hook commit8caa11ba1cAuthor: Eric Allam <eallam@icloud.com> Date: Wed Mar 23 10:21:33 2022 +0000 Start of the Command Palette using radix-ui Dialog commit904b64d781Author: Eric Allam <eallam@icloud.com> Date: Wed Mar 23 09:58:52 2022 +0000 Uncommented out the search bar field
259 lines
5.8 KiB
TypeScript
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);
|
|
}
|