Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 31e622c

Browse files
committed
search feature logic rework to handle search across all languages, categories and snippets
1 parent 2196f7c commit 31e622c

File tree

11 files changed

+348
-110
lines changed

11 files changed

+348
-110
lines changed

‎src/components/CategoryList.tsx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,9 @@ const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
1313
const navigate = useNavigate();
1414
const [searchParams] = useSearchParams();
1515

16-
const { language, category, setCategory } = useAppContext();
16+
const { language, category } = useAppContext();
1717

1818
const handleSelect = () => {
19-
setCategory(name);
2019
navigate({
2120
pathname: `/${slugify(language.name)}/${slugify(name)}`,
2221
search: searchParams.toString(),

‎src/components/Icons.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const ACCENT_ICON_COLOR = "var(--clr-accent)";
55

66
interface IconProps {
77
fillColor?: string;
8+
width?: string;
9+
height?: string;
810
}
911

1012
export const LogoIcon: FC<IconProps> = ({ fillColor = ACCENT_ICON_COLOR }) => (
@@ -123,10 +125,12 @@ export const ExpandIcon: FC<IconProps> = ({
123125

124126
export const CloseIcon: FC<IconProps> = ({
125127
fillColor = DEFAULT_ICON_COLOR,
128+
width = "31",
129+
height = "30",
126130
}) => (
127131
<svg
128-
width="31"
129-
height="30"
132+
width={width}
133+
height={height}
130134
viewBox="0 0 31 30"
131135
fill="none"
132136
xmlns="http://www.w3.org/2000/svg"

‎src/components/LanguageSelector.tsx‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { slugify } from "@utils/slugify";
1515
const LanguageSelector = () => {
1616
const navigate = useNavigate();
1717

18-
const { language, setLanguage, setCategory,setSearchText } = useAppContext();
18+
const { language, setSearchText } = useAppContext();
1919
const { fetchedLanguages, loading, error } = useLanguages();
2020

2121
const dropdownRef = useRef<HTMLDivElement>(null);
@@ -33,8 +33,6 @@ const LanguageSelector = () => {
3333
});
3434

3535
setSearchText("");
36-
setLanguage(newLanguage);
37-
setCategory(newCategory);
3836
navigate(`/${slugify(newLanguage.name)}/${slugify(newCategory)}`);
3937
setIsOpen(false);
4038
};

‎src/components/SearchInput.tsx‎

Lines changed: 181 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,76 @@
1-
import { useCallback, useEffect, useRef } from "react";
2-
import { useSearchParams } from "react-router-dom";
1+
import { useCallback, useEffect, useMemo,useRef,useState } from "react";
2+
import { useNavigate,useSearchParams } from "react-router-dom";
33

44
import { useAppContext } from "@contexts/AppContext";
5+
import { useFetch } from "@hooks/useFetch";
6+
import { AllSnippetsType, SearchItemType } from "@types";
57
import { QueryParams } from "@utils/enums";
8+
import { slugify } from "@utils/slugify";
69

7-
import { SearchIcon } from "./Icons";
10+
import Button from "./Button";
11+
import { CloseIcon, SearchIcon } from "./Icons";
812

913
const SearchInput = () => {
14+
const navigate = useNavigate();
1015
const [searchParams, setSearchParams] = useSearchParams();
1116

1217
const { searchText, setSearchText } = useAppContext();
18+
const { data } = useFetch<AllSnippetsType[]>(`/consolidated/all.json`);
19+
20+
const filteredData: SearchItemType[] = useMemo(() => {
21+
if (!data) {
22+
return [];
23+
}
24+
25+
const searchTerm = searchText.toLowerCase();
26+
27+
return data
28+
.map((language) => {
29+
const filteredCategories = language.categories
30+
.map((category) => {
31+
const filteredSnippets = category.snippets.filter(
32+
(snippet) =>
33+
snippet.title.toLowerCase().includes(searchTerm) ||
34+
snippet.description.toLowerCase().includes(searchTerm) ||
35+
snippet.tags.some((tag) =>
36+
tag.toLowerCase().includes(searchTerm)
37+
)
38+
);
39+
40+
if (filteredSnippets.length > 0) {
41+
return {
42+
categoryName: category.name,
43+
snippets: filteredSnippets,
44+
};
45+
}
46+
47+
return null;
48+
})
49+
.filter(Boolean); // Remove null categories
50+
51+
if (filteredCategories.length > 0) {
52+
return filteredCategories.map((filteredCategory) => ({
53+
languageName: language.languageName,
54+
languageIcon: language.languageIcon,
55+
categoryName: filteredCategory!.categoryName,
56+
snippets: filteredCategory!.snippets,
57+
}));
58+
}
59+
60+
return [];
61+
})
62+
.flat();
63+
}, [data, searchText]);
1364

1465
const inputRef = useRef<HTMLInputElement | null>(null);
1566

67+
const [searchOpen, setSearchOpen] = useState<boolean>(false);
68+
1669
const handleSearchFieldClick = () => {
70+
setSearchOpen(true);
71+
};
72+
73+
const handleInnerSearchFieldClick = () => {
1774
inputRef.current?.focus();
1875
};
1976

@@ -23,31 +80,13 @@ const SearchInput = () => {
2380
setSearchParams(searchParams);
2481
}, [searchParams, setSearchParams, setSearchText]);
2582

26-
const performSearch = useCallback(() => {
27-
// Check if the input element is focused.
28-
if (document.activeElement !== inputRef.current) {
29-
return;
30-
}
31-
32-
const formattedVal = searchText.toLowerCase();
33-
34-
setSearchText(formattedVal);
35-
if (!formattedVal) {
36-
searchParams.delete(QueryParams.SEARCH);
37-
setSearchParams(searchParams);
38-
} else {
39-
searchParams.set(QueryParams.SEARCH, formattedVal);
40-
setSearchParams(searchParams);
41-
}
42-
}, [searchParams, searchText, setSearchParams, setSearchText]);
43-
4483
/**
4584
* Focus the search input when the user presses the `/` key.
4685
*/
4786
const handleSearchKeyPress = (e: KeyboardEvent) => {
4887
if (e.key === "/") {
4988
e.preventDefault();
50-
inputRef.current?.focus();
89+
setSearchOpen(true);
5190
}
5291
};
5392

@@ -60,18 +99,30 @@ const SearchInput = () => {
6099
return;
61100
}
62101

63-
// Check if the input element is focused.
64-
if (document.activeElement !== inputRef.current) {
65-
return;
66-
}
67-
68-
inputRef.current?.blur();
69-
102+
setSearchOpen(false);
70103
clearSearch();
71104
},
72105
[clearSearch]
73106
);
74107

108+
const handleSearchItemClick =
109+
({
110+
languageName,
111+
categoryName,
112+
snippetName,
113+
}: {
114+
languageName: string;
115+
categoryName: string;
116+
snippetName: string;
117+
}) =>
118+
() => {
119+
navigate(
120+
`/${slugify(languageName)}/${slugify(categoryName)}?${QueryParams.SEARCH}=${searchText.toLowerCase()}&${QueryParams.SNIPPET}=${slugify(snippetName)}`,
121+
{ replace: true }
122+
);
123+
setSearchOpen(false);
124+
};
125+
75126
useEffect(() => {
76127
window.addEventListener("keydown", handleSearchKeyPress);
77128
window.addEventListener("keyup", handleEscapeKeyPress);
@@ -82,13 +133,6 @@ const SearchInput = () => {
82133
};
83134
}, [handleEscapeKeyPress]);
84135

85-
/**
86-
* Update the search query in the URL when the search text changes.
87-
*/
88-
useEffect(() => {
89-
performSearch();
90-
}, [searchText, performSearch]);
91-
92136
/**
93137
* Set the search text to the search query from the URL on mount.
94138
*/
@@ -102,30 +146,108 @@ const SearchInput = () => {
102146
// eslint-disable-next-line react-hooks/exhaustive-deps
103147
}, []);
104148

149+
useEffect(() => {
150+
if (searchOpen) {
151+
inputRef.current?.focus();
152+
}
153+
}, [searchOpen]);
154+
105155
return (
106-
<div className="search-field" onClick={handleSearchFieldClick}>
107-
<SearchIcon />
108-
<input
109-
ref={inputRef}
110-
value={searchText}
111-
type="search"
112-
id="search"
113-
autoComplete="off"
114-
onChange={(e) => {
115-
const newValue = e.target.value;
116-
if (!newValue) {
117-
clearSearch();
118-
return;
119-
}
120-
setSearchText(newValue);
121-
}}
122-
/>
123-
{!searchText && (
124-
<label htmlFor="search">
125-
Type <kbd>/</kbd> to search
126-
</label>
127-
)}
128-
</div>
156+
<>
157+
<div className="search-field" onClick={handleSearchFieldClick}>
158+
<SearchIcon />
159+
<input
160+
disabled
161+
id="search"
162+
type="text"
163+
value={searchText}
164+
onChange={() => {}}
165+
/>
166+
{!searchText && (
167+
<label htmlFor="search">
168+
Type <kbd>/</kbd> to search
169+
</label>
170+
)}
171+
{searchText && (
172+
<Button
173+
isIcon={true}
174+
className="search-field__clear"
175+
onClick={(e: React.MouseEvent) => {
176+
e.stopPropagation();
177+
clearSearch();
178+
}}
179+
>
180+
<CloseIcon width="20" height="20" />
181+
</Button>
182+
)}
183+
</div>
184+
185+
<div
186+
className={`search-field__results search-field__results${searchOpen ? "--open" : "--closed"}`}
187+
>
188+
<div
189+
className="search-field search-field--inner"
190+
onClick={handleInnerSearchFieldClick}
191+
>
192+
<SearchIcon />
193+
<input
194+
ref={inputRef}
195+
value={searchText}
196+
type="text"
197+
autoComplete="off"
198+
onChange={(e) => {
199+
const newValue = e.target.value;
200+
if (!newValue) {
201+
clearSearch();
202+
return;
203+
}
204+
setSearchText(newValue);
205+
}}
206+
/>
207+
<Button
208+
isIcon={true}
209+
onClick={() => {
210+
setSearchOpen(false);
211+
clearSearch();
212+
}}
213+
>
214+
<CloseIcon />
215+
</Button>
216+
</div>
217+
218+
<div className="search-field__results__list">
219+
{filteredData.map(
220+
(
221+
{ languageName, languageIcon, categoryName, snippets },
222+
languageIndex
223+
) => (
224+
<div key={`${languageName}-${languageIndex}`}>
225+
<ul>
226+
{snippets.map((snippet, snippetIndex) => (
227+
<li
228+
key={`${languageName}-${categoryName}-${snippetIndex}`}
229+
onClick={handleSearchItemClick({
230+
languageName,
231+
categoryName,
232+
snippetName: snippet.title,
233+
})}
234+
>
235+
<img src={languageIcon} alt={languageName} />
236+
<div>
237+
<h4>
238+
{snippet.title} ({languageName})
239+
</h4>
240+
<p>{snippet.description}</p>
241+
</div>
242+
</li>
243+
))}
244+
</ul>
245+
</div>
246+
)
247+
)}
248+
</div>
249+
</div>
250+
</>
129251
);
130252
};
131253

‎src/components/SnippetList.tsx‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,13 @@ const SnippetList = () => {
3434
setSearchParams(searchParams);
3535
};
3636

37+
const handleSearchKeyPress = (e: KeyboardEvent) => {
38+
if (e.key === "/") {
39+
e.preventDefault();
40+
setIsModalOpen(false);
41+
}
42+
};
43+
3744
/**
3845
* open the relevant modal if the snippet is in the search params
3946
*/
@@ -52,6 +59,14 @@ const SnippetList = () => {
5259
// eslint-disable-next-line react-hooks/exhaustive-deps
5360
}, [fetchedSnippets, searchParams]);
5461

62+
useEffect(() => {
63+
window.addEventListener("keydown", handleSearchKeyPress);
64+
65+
return () => {
66+
window.removeEventListener("keydown", handleSearchKeyPress);
67+
};
68+
}, []);
69+
5570
if (!fetchedSnippets) {
5671
return (
5772
<div>

‎src/contexts/AppContext.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const AppProvider: FC<{ children: React.ReactNode }> = ({
3535
useEffect(() => {
3636
configure();
3737
// eslint-disable-next-line react-hooks/exhaustive-deps
38-
}, [fetchedLanguages]);
38+
}, [fetchedLanguages,languageName,categoryName]);
3939

4040
/**
4141
* Set the default language if the language is not found in the URL.

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /