Skip to content

Commit 8bb107c

Browse files
committed
working search feature
1 parent 48a07f1 commit 8bb107c

File tree

14 files changed

+255
-50
lines changed

14 files changed

+255
-50
lines changed

src/components/CategoryList.tsx

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
1-
import { useEffect } from "react";
1+
import { FC } from "react";
22

33
import { useAppContext } from "@contexts/AppContext";
44
import { useCategories } from "@hooks/useCategories";
5+
import { defaultCategory } from "@utils/consts";
56

6-
const CategoryList = () => {
7+
interface CategoryListItemProps {
8+
name: string;
9+
}
10+
11+
const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
712
const { category, setCategory } = useAppContext();
8-
const { fetchedCategories, loading, error } = useCategories();
913

10-
useEffect(() => {
11-
setCategory(fetchedCategories[0]);
12-
}, [setCategory, fetchedCategories]);
14+
return (
15+
<li className="category">
16+
<button
17+
className={`category__btn ${
18+
name === category ? "category__btn--active" : ""
19+
}`}
20+
onClick={() => setCategory(name)}
21+
>
22+
{name}
23+
</button>
24+
</li>
25+
);
26+
};
27+
28+
const CategoryList = () => {
29+
const { fetchedCategories, loading, error } = useCategories();
1330

1431
if (loading) return <div>Loading...</div>;
1532

1633
if (error) return <div>Error occurred: {error}</div>;
1734

1835
return (
1936
<ul role="list" className="categories">
37+
<CategoryListItem name={defaultCategory} />
2038
{fetchedCategories.map((name, idx) => (
21-
<li key={idx} className="category">
22-
<button
23-
className={`category__btn ${
24-
name === category ? "category__btn--active" : ""
25-
}`}
26-
onClick={() => setCategory(name)}
27-
>
28-
{name}
29-
</button>
30-
</li>
39+
<CategoryListItem key={idx} name={name} />
3140
))}
3241
</ul>
3342
);

src/App.tsx renamed to src/components/Container.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1-
import SnippetList from "@components/SnippetList";
1+
import { FC } from "react";
2+
import { Outlet } from "react-router-dom";
3+
24
import { useAppContext } from "@contexts/AppContext";
35
import Banner from "@layouts/Banner";
46
import Footer from "@layouts/Footer";
57
import Header from "@layouts/Header";
68
import Sidebar from "@layouts/Sidebar";
79

8-
const App = () => {
10+
interface ContainerProps {}
11+
12+
const Container: FC<ContainerProps> = () => {
913
const { category } = useAppContext();
1014

1115
return (
@@ -18,12 +22,12 @@ const App = () => {
1822
<h2 className="section-title">
1923
{category ? category : "Select a category"}
2024
</h2>
21-
<SnippetList />
25+
<Outlet />
2226
</section>
2327
</main>
2428
<Footer />
2529
</div>
2630
);
2731
};
2832

29-
export default App;
33+
export default Container;

src/components/SearchInput.tsx

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,129 @@
1+
import { useCallback, useEffect, useRef, useState } from "react";
2+
import { useSearchParams } from "react-router-dom";
3+
4+
import { useAppContext } from "@contexts/AppContext";
5+
import { defaultCategory } from "@utils/consts";
6+
17
import { SearchIcon } from "./Icons";
28

39
const SearchInput = () => {
10+
const [searchParams, setSearchParams] = useSearchParams();
11+
12+
const { searchText, setSearchText, setCategory } = useAppContext();
13+
14+
const inputRef = useRef<HTMLInputElement | null>(null);
15+
16+
const [inputVal, setInputVal] = useState<string>("");
17+
18+
const handleSearchFieldClick = () => {
19+
inputRef.current?.focus();
20+
};
21+
22+
const handleSearchKeyPress = (e: KeyboardEvent) => {
23+
if (e.key === "/") {
24+
e.preventDefault();
25+
inputRef.current?.focus();
26+
}
27+
};
28+
29+
const clearSearch = useCallback(() => {
30+
setInputVal("");
31+
setCategory(defaultCategory);
32+
setSearchText("");
33+
setSearchParams({});
34+
}, [setCategory, setSearchParams, setSearchText]);
35+
36+
const handleEscapePress = useCallback(
37+
(e: KeyboardEvent) => {
38+
if (e.key !== "Escape") {
39+
return;
40+
}
41+
// check if the input is focused
42+
if (document.activeElement !== inputRef.current) {
43+
return;
44+
}
45+
46+
inputRef.current?.blur();
47+
48+
clearSearch();
49+
},
50+
[clearSearch]
51+
);
52+
53+
const handleReturnPress = useCallback(
54+
(e: KeyboardEvent) => {
55+
if (e.key !== "Enter") {
56+
return;
57+
}
58+
// check if the input is focused
59+
if (document.activeElement !== inputRef.current) {
60+
return;
61+
}
62+
63+
const formattedVal = inputVal.trim().toLowerCase();
64+
65+
setCategory(defaultCategory);
66+
setSearchText(formattedVal);
67+
if (!formattedVal) {
68+
setSearchParams({});
69+
} else {
70+
setSearchParams({ search: formattedVal });
71+
}
72+
},
73+
[inputVal, setCategory, setSearchParams, setSearchText]
74+
);
75+
76+
useEffect(() => {
77+
document.addEventListener("keyup", handleSearchKeyPress);
78+
document.addEventListener("keyup", handleEscapePress);
79+
document.addEventListener("keyup", handleReturnPress);
80+
81+
return () => {
82+
document.removeEventListener("keyup", handleSearchKeyPress);
83+
document.removeEventListener("keyup", handleEscapePress);
84+
document.removeEventListener("keyup", handleReturnPress);
85+
};
86+
}, [handleEscapePress, handleReturnPress]);
87+
88+
/**
89+
* Set the input value and search text to the search query from the URL
90+
*/
91+
useEffect(() => {
92+
const search = searchParams.get("search") || "";
93+
setInputVal(search);
94+
setSearchText(search);
95+
// eslint-disable-next-line react-hooks/exhaustive-deps
96+
}, []);
97+
498
return (
5-
<div className="search-field">
6-
<label htmlFor="search">
7-
<SearchIcon />
8-
</label>
99+
<div className="search-field" onClick={handleSearchFieldClick}>
100+
<SearchIcon />
9101
<input
102+
ref={inputRef}
10103
type="search"
11104
id="search"
12-
placeholder="Search here..."
13105
autoComplete="off"
106+
value={inputVal}
107+
onChange={(e) => {
108+
const newValue = e.target.value;
109+
if (!newValue) {
110+
clearSearch();
111+
return;
112+
}
113+
setInputVal(newValue);
114+
}}
115+
onBlur={() => {
116+
// ensure the input value is always in sync with the search text
117+
if (inputVal !== searchText) {
118+
setInputVal(searchText);
119+
}
120+
}}
14121
/>
122+
{!inputVal && !searchText && (
123+
<label htmlFor="search">
124+
Type <kbd>/</kbd> to search
125+
</label>
126+
)}
15127
</div>
16128
);
17129
};

src/components/SnippetList.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,8 @@ import SnippetModal from "./SnippetModal";
1111
const SnippetList = () => {
1212
const { language, snippet, setSnippet } = useAppContext();
1313
const { fetchedSnippets } = useSnippets();
14-
const [isModalOpen, setIsModalOpen] = useState(false);
1514

16-
if (!fetchedSnippets)
17-
return (
18-
<div>
19-
<LeftAngleArrowIcon />
20-
</div>
21-
);
15+
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
2216

2317
const handleOpenModal = (activeSnippet: SnippetType) => {
2418
setIsModalOpen(true);
@@ -30,6 +24,14 @@ const SnippetList = () => {
3024
setSnippet(null);
3125
};
3226

27+
if (!fetchedSnippets) {
28+
return (
29+
<div>
30+
<LeftAngleArrowIcon />
31+
</div>
32+
);
33+
}
34+
3335
return (
3436
<>
3537
<motion.ul role="list" className="snippets">

src/components/SnippetModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import ReactDOM from "react-dom";
44

55
import { useEscapeKey } from "@hooks/useEscapeKey";
66
import { SnippetType } from "@types";
7-
import { slugify } from "@utils/slugify";
7+
import { slugify } from "@utils/helpers/slugify";
88

99
import Button from "./Button";
1010
import CodePreview from "./CodePreview";

src/contexts/AppContext.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import { createContext, FC, useContext, useState } from "react";
22

33
import { AppState, LanguageType, SnippetType } from "@types";
4-
5-
// tokens
6-
const defaultLanguage: LanguageType = {
7-
lang: "JAVASCRIPT",
8-
icon: "/icons/javascript.svg",
9-
};
4+
import { defaultCategory, defaultLanguage } from "@utils/consts";
105

116
// TODO: add custom loading and error handling
127
const defaultState: AppState = {
138
language: defaultLanguage,
149
setLanguage: () => {},
15-
category: "",
10+
category: defaultCategory,
1611
setCategory: () => {},
1712
snippet: null,
1813
setSnippet: () => {},
14+
searchText: "",
15+
setSearchText: () => {},
1916
};
2017

2118
const AppContext = createContext<AppState>(defaultState);
@@ -24,8 +21,9 @@ export const AppProvider: FC<{ children: React.ReactNode }> = ({
2421
children,
2522
}) => {
2623
const [language, setLanguage] = useState<LanguageType>(defaultLanguage);
27-
const [category, setCategory] = useState<string>("");
24+
const [category, setCategory] = useState<string>(defaultCategory);
2825
const [snippet, setSnippet] = useState<SnippetType | null>(null);
26+
const [searchText, setSearchText] = useState<string>("");
2927

3028
return (
3129
<AppContext.Provider
@@ -36,6 +34,8 @@ export const AppProvider: FC<{ children: React.ReactNode }> = ({
3634
setCategory,
3735
snippet,
3836
setSnippet,
37+
searchText,
38+
setSearchText,
3939
}}
4040
>
4141
{children}

src/hooks/useCategories.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useMemo } from "react";
22

33
import { useAppContext } from "@contexts/AppContext";
44
import { SnippetType } from "@types";
5-
import { slugify } from "@utils/slugify";
5+
import { slugify } from "@utils/helpers/slugify";
66

77
import { useFetch } from "./useFetch";
88

src/hooks/useSnippets.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import { useMemo } from "react";
2+
13
import { useAppContext } from "@contexts/AppContext";
24
import { SnippetType } from "@types";
3-
import { slugify } from "@utils/slugify";
5+
import { defaultCategory } from "@utils/consts";
6+
import { slugify } from "@utils/helpers/slugify";
47

58
import { useFetch } from "./useFetch";
69

@@ -10,14 +13,36 @@ type CategoryData = {
1013
};
1114

1215
export const useSnippets = () => {
13-
const { language, category } = useAppContext();
16+
const { language, category, searchText } = useAppContext();
1417
const { data, loading, error } = useFetch<CategoryData[]>(
1518
`/consolidated/${slugify(language.lang)}.json`
1619
);
1720

18-
const fetchedSnippets = data
19-
? data.find((item) => item.categoryName === category)?.snippets
20-
: [];
21+
const fetchedSnippets = useMemo(() => {
22+
if (!data) {
23+
return [];
24+
}
25+
26+
if (category === defaultCategory) {
27+
if (searchText) {
28+
return data
29+
.flatMap((item) => item.snippets)
30+
.filter((item) =>
31+
item.title.toLowerCase().includes(searchText.toLowerCase())
32+
);
33+
}
34+
return data.flatMap((item) => item.snippets);
35+
}
36+
37+
if (searchText) {
38+
return data
39+
.find((item) => item.categoryName === category)
40+
?.snippets.filter((item) =>
41+
item.title.toLowerCase().includes(searchText.toLowerCase())
42+
);
43+
}
44+
return data.find((item) => item.categoryName === category)?.snippets;
45+
}, [category, data, searchText]);
2146

2247
return { fetchedSnippets, loading, error };
2348
};

src/main.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@ import "@styles/main.css";
22

33
import { StrictMode } from "react";
44
import { createRoot } from "react-dom/client";
5+
import { RouterProvider } from "react-router-dom";
56

67
import { AppProvider } from "@contexts/AppContext";
7-
8-
import App from "./App";
8+
import { router } from "@router";
99

1010
createRoot(document.getElementById("root")!).render(
1111
<StrictMode>
1212
<AppProvider>
13-
<App />
13+
<RouterProvider router={router} />
1414
</AppProvider>
1515
</StrictMode>
1616
);

0 commit comments

Comments
 (0)