Skip to content

Commit 94cf03c

Browse files
authored
Merge pull request #159 from barrymun/feature/search
Feature - Implement basic search functionality v2
2 parents df7dcbf + c0b3bff commit 94cf03c

23 files changed

+1079
-321
lines changed

package-lock.json

Lines changed: 125 additions & 101 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"prismjs": "^1.29.0",
2424
"react": "^18.3.1",
2525
"react-dom": "^18.3.1",
26-
"react-router-dom": "^6.27.0",
26+
"react-router-dom": "^7.1.1",
2727
"react-syntax-highlighter": "^15.6.1"
2828
},
2929
"devDependencies": {

src/AppRouter.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Route, Routes } from "react-router-dom";
2+
3+
import App from "@components/App";
4+
import SnippetList from "@components/SnippetList";
5+
6+
const AppRouter = () => {
7+
return (
8+
<Routes>
9+
<Route element={<App />}>
10+
<Route path="/" element={<SnippetList />} />
11+
<Route path="/:languageName" element={<SnippetList />} />
12+
<Route
13+
path="/:languageName/:subLanguageName/:categoryName"
14+
element={<SnippetList />}
15+
/>
16+
</Route>
17+
</Routes>
18+
);
19+
};
20+
21+
export default AppRouter;

src/components/App.tsx

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { FC } from "react";
2+
3+
import { AppProvider } from "@contexts/AppContext";
4+
5+
import Container from "./Container";
6+
7+
interface AppProps {}
8+
9+
const App: FC<AppProps> = () => {
10+
return (
11+
<AppProvider>
12+
<Container />
13+
</AppProvider>
14+
);
15+
};
16+
17+
export default App;

src/components/CategoryList.tsx

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,58 @@
1-
import { useEffect } from "react";
1+
import { FC } from "react";
2+
import { useNavigate, useSearchParams } from "react-router-dom";
23

34
import { useAppContext } from "@contexts/AppContext";
45
import { useCategories } from "@hooks/useCategories";
6+
import { defaultCategoryName } from "@utils/consts";
7+
import { slugify } from "@utils/slugify";
8+
9+
interface CategoryListItemProps {
10+
name: string;
11+
}
12+
13+
const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
14+
const navigate = useNavigate();
15+
const [searchParams] = useSearchParams();
16+
17+
const { language, subLanguage, category } = useAppContext();
18+
19+
const handleSelect = () => {
20+
navigate({
21+
pathname: `/${slugify(language.name)}/${slugify(subLanguage)}/${slugify(name)}`,
22+
search: searchParams.toString(),
23+
});
24+
};
25+
26+
return (
27+
<li className="category">
28+
<button
29+
className={`category__btn ${
30+
slugify(name) === slugify(category) ? "category__btn--active" : ""
31+
}`}
32+
onClick={handleSelect}
33+
>
34+
{name}
35+
</button>
36+
</li>
37+
);
38+
};
539

640
const CategoryList = () => {
7-
const { category, setCategory } = useAppContext();
841
const { fetchedCategories, loading, error } = useCategories();
942

10-
useEffect(() => {
11-
setCategory(fetchedCategories[0]);
12-
}, [setCategory, fetchedCategories]);
13-
14-
if (loading) return <div>Loading...</div>;
43+
if (loading) {
44+
return <div>Loading...</div>;
45+
}
1546

16-
if (error) return <div>Error occurred: {error}</div>;
47+
if (error) {
48+
return <div>Error occurred: {error}</div>;
49+
}
1750

1851
return (
1952
<ul role="list" className="categories">
53+
<CategoryListItem name={defaultCategoryName} />
2054
{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>
55+
<CategoryListItem key={idx} name={name} />
3156
))}
3257
</ul>
3358
);

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/LanguageSelector.tsx

Lines changed: 116 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,127 @@
1+
/**
2+
* Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
3+
*/
4+
15
import { useRef, useEffect, useState, useMemo } from "react";
6+
import { useNavigate } from "react-router-dom";
27

38
import { useAppContext } from "@contexts/AppContext";
49
import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation";
510
import { useLanguages } from "@hooks/useLanguages";
611
import { LanguageType } from "@types";
12+
import { configureUserSelection } from "@utils/configureUserSelection";
13+
import {
14+
getLanguageDisplayLogo,
15+
getLanguageDisplayName,
16+
} from "@utils/languageUtils";
17+
import { slugify } from "@utils/slugify";
718

819
import SubLanguageSelector from "./SubLanguageSelector";
920

10-
// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
11-
1221
const LanguageSelector = () => {
13-
const { language, setLanguage } = useAppContext();
22+
const navigate = useNavigate();
23+
24+
const { language, subLanguage, setSearchText } = useAppContext();
1425
const { fetchedLanguages, loading, error } = useLanguages();
15-
const allLanguages = useMemo(
16-
() =>
17-
fetchedLanguages.flatMap((lang) =>
18-
lang.subLanguages.length > 0
19-
? [
20-
lang,
21-
...lang.subLanguages.map((subLang) => ({
22-
...subLang,
23-
mainLanguage: lang,
24-
subLanguages: [],
25-
})),
26-
]
27-
: [lang]
28-
),
29-
[fetchedLanguages]
30-
);
3126

3227
const dropdownRef = useRef<HTMLDivElement>(null);
33-
const [isOpen, setIsOpen] = useState(false);
28+
const [isOpen, setIsOpen] = useState<boolean>(false);
3429
const [openedLanguages, setOpenedLanguages] = useState<LanguageType[]>([]);
3530

36-
const handleSelect = (selected: LanguageType) => {
37-
setLanguage(selected);
31+
const keyboardItems = useMemo(() => {
32+
return fetchedLanguages.flatMap((lang) =>
33+
openedLanguages.map((ol) => ol.name).includes(lang.name)
34+
? [
35+
{ languageName: lang.name },
36+
...lang.subLanguages.map((sl) => ({
37+
languageName: lang.name,
38+
subLanguageName: sl.name,
39+
})),
40+
]
41+
: [{ languageName: lang.name }]
42+
);
43+
}, [fetchedLanguages, openedLanguages]);
44+
45+
const displayName = useMemo(
46+
() => getLanguageDisplayName(language.name, subLanguage),
47+
[language.name, subLanguage]
48+
);
49+
50+
const displayLogo = useMemo(
51+
() => getLanguageDisplayLogo(language.name, subLanguage),
52+
[language.name, subLanguage]
53+
);
54+
55+
const handleToggleSubLanguage = (name: LanguageType["name"]) => {
56+
const isAlreadyOpened = openedLanguages.some((lang) => lang.name === name);
57+
const openedLang = fetchedLanguages.find((lang) => lang.name === name);
58+
if (openedLang === undefined || openedLang.subLanguages.length === 0) {
59+
return;
60+
}
61+
62+
if (!isAlreadyOpened) {
63+
setOpenedLanguages((prev) => [...prev, openedLang]);
64+
} else {
65+
setOpenedLanguages((prev) =>
66+
prev.filter((lang) => lang.name !== openedLang.name)
67+
);
68+
}
69+
};
70+
71+
/**
72+
* When setting a new language we need to ensure that a category
73+
* has been set given this new language.
74+
* Ensure that the search text is cleared.
75+
*/
76+
const handleSelect = async (selected: LanguageType) => {
77+
const {
78+
language: newLanguage,
79+
subLanguage: newSubLanguage,
80+
category: newCategory,
81+
} = await configureUserSelection({
82+
languageName: selected.name,
83+
});
84+
85+
setSearchText("");
86+
navigate(
87+
`/${slugify(newLanguage.name)}/${slugify(newSubLanguage)}/${slugify(newCategory)}`
88+
);
3889
setIsOpen(false);
3990
setOpenedLanguages([]);
4091
};
4192

93+
const afterSelect = () => {
94+
setIsOpen(false);
95+
};
96+
97+
const handleSubLanguageSelect = async (
98+
selectedLanguageName: LanguageType["name"],
99+
selectedSubLanguageName:
100+
| LanguageType["subLanguages"][number]["name"]
101+
| undefined
102+
) => {
103+
const {
104+
language: newLanguage,
105+
subLanguage: newSubLanguage,
106+
category: newCategory,
107+
} = await configureUserSelection({
108+
languageName: selectedLanguageName,
109+
subLanguageName: selectedSubLanguageName,
110+
});
111+
112+
setSearchText("");
113+
navigate(
114+
`/${slugify(newLanguage.name)}/${slugify(newSubLanguage)}/${slugify(newCategory)}`
115+
);
116+
afterSelect();
117+
};
118+
42119
const { focusedIndex, handleKeyDown, resetFocus, focusFirst } =
43120
useKeyboardNavigation({
44-
items: allLanguages,
121+
items: keyboardItems,
45122
isOpen,
46-
openedLanguages,
47-
toggleDropdown: (openedLang) => handleToggleSublanguage(openedLang),
48-
onSelect: handleSelect,
123+
toggleDropdown: (l) => handleToggleSubLanguage(l),
124+
onSelect: (l, sl) => handleSubLanguageSelect(l, sl),
49125
onClose: () => setIsOpen(false),
50126
});
51127

@@ -60,20 +136,6 @@ const LanguageSelector = () => {
60136
}, 0);
61137
};
62138

63-
const handleToggleSublanguage = (openedLang: LanguageType) => {
64-
const isAlreadyOpened = openedLanguages.some(
65-
(lang) => lang.name === openedLang.name
66-
);
67-
68-
if (!isAlreadyOpened) {
69-
setOpenedLanguages((prev) => [...prev, openedLang]);
70-
} else {
71-
setOpenedLanguages((prev) =>
72-
prev.filter((lang) => lang.name !== openedLang.name)
73-
);
74-
}
75-
};
76-
77139
const toggleDropdown = () => {
78140
setIsOpen((prev) => {
79141
if (!prev) setTimeout(focusFirst, 0);
@@ -88,13 +150,6 @@ const LanguageSelector = () => {
88150
// eslint-disable-next-line react-hooks/exhaustive-deps
89151
}, [isOpen]);
90152

91-
useEffect(() => {
92-
if (language.mainLanguage) {
93-
handleToggleSublanguage(language.mainLanguage);
94-
}
95-
// eslint-disable-next-line react-hooks/exhaustive-deps
96-
}, [language]);
97-
98153
useEffect(() => {
99154
if (isOpen && focusedIndex >= 0) {
100155
const element = document.querySelector(
@@ -104,8 +159,13 @@ const LanguageSelector = () => {
104159
}
105160
}, [isOpen, focusedIndex]);
106161

107-
if (loading) return <p>Loading languages...</p>;
108-
if (error) return <p>Error fetching languages: {error}</p>;
162+
if (loading) {
163+
return <p>Loading languages...</p>;
164+
}
165+
166+
if (error) {
167+
return <p>Error fetching languages: {error}</p>;
168+
}
109169

110170
return (
111171
<div
@@ -121,8 +181,8 @@ const LanguageSelector = () => {
121181
onClick={toggleDropdown}
122182
>
123183
<div className="selector__value">
124-
<img src={language.icon} alt="" />
125-
<span>{language.name || "Select a language"}</span>
184+
<img src={displayLogo} alt="" />
185+
<span>{displayName}</span>
126186
</div>
127187
<span className="selector__arrow" />
128188
</button>
@@ -136,13 +196,12 @@ const LanguageSelector = () => {
136196
{fetchedLanguages.map((lang, index) =>
137197
lang.subLanguages.length > 0 ? (
138198
<SubLanguageSelector
139-
key={index}
140-
mainLanguage={lang}
141-
afterSelect={() => {
142-
setIsOpen(false);
143-
}}
199+
key={lang.name}
144200
opened={openedLanguages.includes(lang)}
145-
onDropdownToggle={handleToggleSublanguage}
201+
parentLanguage={lang}
202+
onDropdownToggle={handleToggleSubLanguage}
203+
handleParentSelect={handleSelect}
204+
afterSelect={afterSelect}
146205
/>
147206
) : (
148207
<li

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy