Skip to content

Commit

Permalink
re-work to use context and provider
Browse files Browse the repository at this point in the history
  • Loading branch information
pseudopilot committed Nov 14, 2024
1 parent 828fab8 commit 6bffe4b
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 60 deletions.
28 changes: 25 additions & 3 deletions src/components/Input/Input.js
Original file line number Diff line number Diff line change
@@ -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 <input onChange={(e) => updateValue(e.target.value)} />;
return (
<input value={inputValue} onChange={(e) => updateValue(e.target.value)} />
);
}
33 changes: 10 additions & 23 deletions src/components/Main/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className={styles.wrapper}>
<div className={styles.search}>
<Search onSearch={onSearch} />
</div>
<div className={styles.results}>
<Results results={results} onSelect={onSearch} />
<SearchProvider>
<div className={styles.wrapper}>
<div className={styles.search}>
<Search />
</div>
<div className={styles.results}>
<Results />
</div>
</div>
</div>
</SearchProvider>
);
}

function search(v) {
return fetch(`http://localhost:4000/search?text=${v}`)
.then((res) => res.json())
.catch((err) => {
console.error(err);
return [];
});
}
17 changes: 7 additions & 10 deletions src/components/Results/Results.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.wrapper}>
{(results?.length &&
results.map((tb, i) => {
return (
<TranslationBlock
key={i}
translationBlock={tb}
onSelect={onSelect}
/>
);
{(searchResults?.length &&
searchResults.map((tb, i) => {
return <TranslationBlock key={i} translationBlock={tb} />;
})) || <p className={styles.empty}>No results found</p>}
</div>
);
Expand Down
72 changes: 51 additions & 21 deletions src/components/Search/Search.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 (
<div className={styles.wrapper}>
<Input onChange={onChange} />
<Input value={query} onChange={onInputUpdate} />
<div className={styles.results}>
<SearchOptions options={lookupResults} selectedIndex={selectedIndex} />
<div className={styles.divider} />
Expand All @@ -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 [];
});
}
14 changes: 13 additions & 1 deletion src/components/SearchOptions/SearchOptions.js
Original file line number Diff line number Diff line change
@@ -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 && (
<ul className={styles.results}>
{options.map((option, i) => (
<li className={i == selectedIndex ? styles.selected : ""} key={i}>
<li
className={i == selectedIndex ? styles.selected : ""}
key={i}
onClick={() =>
dispatch({
type: "SET_SEARCH_QUERY",
payload: option,
})
}
>
{option}
</li>
))}
Expand Down
13 changes: 11 additions & 2 deletions src/components/TranslationBlock/TranslationBlock.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className={styles.block}>
<TranslationBlockHeader
Expand Down
35 changes: 35 additions & 0 deletions src/context/SearchContext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createContext, useContext, useReducer } from "react";

const searchReducer = (state, action) => {
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 (
<SearchContext.Provider value={search}>
<SearchDispatchContext.Provider value={dispatch}>
{children}
</SearchDispatchContext.Provider>
</SearchContext.Provider>
);
}

0 comments on commit 6bffe4b

Please sign in to comment.