diff --git a/src/components/Input/Input.js b/src/components/Input/Input.js index e486cbd..ed85418 100644 --- a/src/components/Input/Input.js +++ b/src/components/Input/Input.js @@ -1,9 +1,31 @@ +import { useState, useEffect } from "react"; import "./Input.module.scss"; -export function Input({ onChange }) { +export function Input({ value, onChange }) { + const [inputValue, setInputValue] = useState(""); + + let emitValue = true; + + useEffect(() => { + if (value !== inputValue) { + emitValue = false; + setInputValue(value); + } + }, [value]); + const updateValue = (v) => { - onChange(v); + if (v === value) return; + + setInputValue(v); + + if (emitValue) { + onChange(v); + } else { + emitValue = true; + } }; - return updateValue(e.target.value)} />; + return ( + updateValue(e.target.value)} /> + ); } diff --git a/src/components/Main/Main.js b/src/components/Main/Main.js index 9946c3a..8315fb9 100644 --- a/src/components/Main/Main.js +++ b/src/components/Main/Main.js @@ -2,32 +2,19 @@ import { useState } from "react"; import { Search } from "../Search/Search"; import { Results } from "../Results/Results"; import styles from "./Main.module.scss"; +import SearchProvider from "../../context/SearchContext"; export function Main() { - const [results, setResults] = useState([]); - - const onSearch = async (v) => { - const res = await search(v); - setResults(res); - }; - return ( -
-
- -
-
- + +
+
+ +
+
+ +
-
+ ); } - -function search(v) { - return fetch(`http://localhost:4000/search?text=${v}`) - .then((res) => res.json()) - .catch((err) => { - console.error(err); - return []; - }); -} diff --git a/src/components/Results/Results.js b/src/components/Results/Results.js index 723ac25..44f3db0 100644 --- a/src/components/Results/Results.js +++ b/src/components/Results/Results.js @@ -1,18 +1,15 @@ import { TranslationBlock } from "../TranslationBlock/TranslationBlock"; import styles from "./Results.module.scss"; +import { useSearch } from "../../context/SearchContext"; + +export function Results() { + const { searchResults } = useSearch(); -export function Results({ results, onSelect }) { return (
- {(results?.length && - results.map((tb, i) => { - return ( - - ); + {(searchResults?.length && + searchResults.map((tb, i) => { + return ; })) ||

No results found

}
); diff --git a/src/components/Search/Search.js b/src/components/Search/Search.js index 321f3c1..5fc80a9 100644 --- a/src/components/Search/Search.js +++ b/src/components/Search/Search.js @@ -2,24 +2,31 @@ import { useState, useEffect } from "react"; import { Input } from "../Input/Input"; import { SearchOptions } from "../SearchOptions/SearchOptions"; import styles from "./Search.module.scss"; +import { useSearchDispatch, useSearch } from "../../context/SearchContext"; -export function Search({ onSearch }) { +export function Search() { const [query, setQuery] = useState(""); const [lookupResults, setLookupResults] = useState([]); const [selectedIndex, setSelectedIndex] = useState(-1); - const handler = (e) => { + const { searchQuery, searchResults } = useSearch(); + const dispatch = useSearchDispatch(); + + const keyDownHandler = (e) => { + if (!["Escape", "Enter", "ArrowDown", "ArrowUp"].includes(e.key)) return; + + e.preventDefault(); + if (e.key === "Escape") { - e.preventDefault(); - setLookupResults([]); + updateLookupResults([]); } else if (e.key === "Enter") { - e.preventDefault(); - onSelect(lookupResults[selectedIndex] || query); + dispatch({ + type: "SET_SEARCH_QUERY", + payload: lookupResults[selectedIndex] || query, + }); } else if (e.key === "ArrowDown") { - e.preventDefault(); setSelectedIndex((si) => ((si + 2) % (lookupResults.length + 1)) - 1); } else if (e.key === "ArrowUp") { - e.preventDefault(); setSelectedIndex( (si) => ((si + lookupResults.length + 1) % (lookupResults.length + 1)) - 1 @@ -28,32 +35,46 @@ export function Search({ onSearch }) { }; useEffect(() => { - document.addEventListener("keydown", handler); - - return () => document.removeEventListener("keydown", handler); + document.addEventListener("keydown", keyDownHandler); + return () => document.removeEventListener("keydown", keyDownHandler); }, [lookupResults, selectedIndex]); - const onSelect = (v) => { - setSelectedIndex(-1); - setLookupResults([]); - onSearch(v); - }; + useEffect(() => { + setQuery(searchQuery); + searchTranslation(searchQuery); + }, [searchQuery]); + + useEffect(() => { + updateLookupResults([]); + }, [searchResults]); - const onChange = async (v) => { + const onInputUpdate = async (v) => { setQuery(v); - setSelectedIndex(-1); if (!v) { - setLookupResults([]); + updateLookupResults([]); } else { const res = await lookup(v); - setLookupResults(res); + updateLookupResults(res); } }; + const searchTranslation = async (v) => { + const res = await search(v); + dispatch({ + type: "SET_SEARCH_RESULTS", + payload: res, + }); + }; + + const updateLookupResults = (newResults) => { + setSelectedIndex(-1); + setLookupResults(newResults); + }; + return (
- +
@@ -70,3 +91,12 @@ function lookup(v) { return []; }); } + +function search(v) { + return fetch(`http://localhost:4000/search?text=${v}`) + .then((res) => res.json()) + .catch((err) => { + console.error(err); + return []; + }); +} diff --git a/src/components/SearchOptions/SearchOptions.js b/src/components/SearchOptions/SearchOptions.js index 78cecaa..a9ba7b1 100644 --- a/src/components/SearchOptions/SearchOptions.js +++ b/src/components/SearchOptions/SearchOptions.js @@ -1,12 +1,24 @@ +import { useSearchDispatch } from "../../context/SearchContext"; import styles from "./SearchOptions.module.scss"; export function SearchOptions({ options, selectedIndex }) { + const dispatch = useSearchDispatch(); + return ( <> {options?.length > 0 && (
    {options.map((option, i) => ( -
  • +
  • + dispatch({ + type: "SET_SEARCH_QUERY", + payload: option, + }) + } + > {option}
  • ))} diff --git a/src/components/TranslationBlock/TranslationBlock.js b/src/components/TranslationBlock/TranslationBlock.js index 26653be..a7e726f 100644 --- a/src/components/TranslationBlock/TranslationBlock.js +++ b/src/components/TranslationBlock/TranslationBlock.js @@ -1,10 +1,19 @@ -import { Fragment } from "react"; import { TranslationBlockHeader } from "../TranslationBlockHeader/TranslationBlockHeader"; import { TranslationSubject } from "../TranslationSubject/TranslationSubject"; import { TranslationOption } from "../TranslationOption/TranslationOption"; import styles from "./TranslationBlock.module.scss"; +import { useSearchDispatch } from "../../context/SearchContext"; + +export function TranslationBlock({ translationBlock: tb }) { + const dispatch = useSearchDispatch(); + + const onSelect = (v) => { + dispatch({ + type: "SET_SEARCH_QUERY", + payload: v, + }); + }; -export function TranslationBlock({ translationBlock: tb, onSelect }) { return (
    { + switch (action.type) { + case "SET_SEARCH_QUERY": + return { ...state, searchQuery: action.payload }; + case "SET_SEARCH_RESULTS": + return { ...state, searchResults: action.payload }; + default: + return state; + } +}; + +const SearchContext = createContext(null); +const SearchDispatchContext = createContext(null); + +export const useSearch = () => useContext(SearchContext); +export const useSearchDispatch = () => useContext(SearchDispatchContext); + +export default function SearchProvider({ children }) { + const initialSearchState = { + searchQuery: "", + searchResults: [], + }; + + const [search, dispatch] = useReducer(searchReducer, initialSearchState); + + return ( + + + {children} + + + ); +}