Skip to content

Commit 43bcf18

Browse files
committed
working search functionality with language and category set in the url, search and snippet set as query params
1 parent 5aa4619 commit 43bcf18

File tree

12 files changed

+191
-106
lines changed

12 files changed

+191
-106
lines changed

cspell-dict.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
quicksnip
2+
slugified
23
slugifyed

src/AppRouter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Route, Routes } from "react-router-dom";
22

3-
import Container from "@components/Container";
3+
import App from "@components/App";
44
import SnippetList from "@components/SnippetList";
55

66
const AppRouter = () => {
77
return (
88
<Routes>
9-
<Route element={<Container />}>
9+
<Route element={<App />}>
1010
<Route path="/" element={<SnippetList />} />
1111
<Route path="/:languageName" element={<SnippetList />} />
1212
<Route path="/:languageName/:categoryName" element={<SnippetList />} />

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: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
import { FC } from "react";
2+
import { useNavigate } from "react-router-dom";
23

34
import { useAppContext } from "@contexts/AppContext";
45
import { useCategories } from "@hooks/useCategories";
6+
import { slugify } from "@utils/slugify";
57

68
interface CategoryListItemProps {
79
name: string;
810
}
911

1012
const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
11-
const { category, setCategory } = useAppContext();
13+
const navigate = useNavigate();
14+
15+
const { language, category, setCategory } = useAppContext();
1216

1317
const handleSelect = () => {
1418
setCategory(name);
19+
navigate(`/${slugify(language.name)}/${slugify(name)}`);
1520
};
1621

1722
return (
1823
<li className="category">
1924
<button
2025
className={`category__btn ${
21-
name === category ? "category__btn--active" : ""
26+
slugify(name) === slugify(category) ? "category__btn--active" : ""
2227
}`}
2328
onClick={handleSelect}
2429
>
@@ -31,9 +36,13 @@ const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
3136
const CategoryList = () => {
3237
const { fetchedCategories, loading, error } = useCategories();
3338

34-
if (loading) return <div>Loading...</div>;
39+
if (loading) {
40+
return <div>Loading...</div>;
41+
}
3542

36-
if (error) return <div>Error occurred: {error}</div>;
43+
if (error) {
44+
return <div>Error occurred: {error}</div>;
45+
}
3746

3847
return (
3948
<ul role="list" className="categories">

src/components/Container.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FC } from "react";
22
import { Outlet } from "react-router-dom";
33

4-
import { AppProvider, useAppContext } from "@contexts/AppContext";
4+
import { useAppContext } from "@contexts/AppContext";
55
import Banner from "@layouts/Banner";
66
import Footer from "@layouts/Footer";
77
import Header from "@layouts/Header";
@@ -13,22 +13,20 @@ const Container: FC<ContainerProps> = () => {
1313
const { category } = useAppContext();
1414

1515
return (
16-
<AppProvider>
17-
<div className="container flow">
18-
<Header />
19-
<Banner />
20-
<main className="main">
21-
<Sidebar />
22-
<section className="flow">
23-
<h2 className="section-title">
24-
{category ? category : "Select a category"}
25-
</h2>
26-
<Outlet />
27-
</section>
28-
</main>
29-
<Footer />
30-
</div>
31-
</AppProvider>
16+
<div className="container flow">
17+
<Header />
18+
<Banner />
19+
<main className="main">
20+
<Sidebar />
21+
<section className="flow">
22+
<h2 className="section-title">
23+
{category ? category : "Select a category"}
24+
</h2>
25+
<Outlet />
26+
</section>
27+
</main>
28+
<Footer />
29+
</div>
3230
);
3331
};
3432

src/components/LanguageSelector.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,35 @@
1+
/**
2+
* Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
3+
*/
4+
15
import { useRef, useEffect, useState } from "react";
26
import { useNavigate } from "react-router-dom";
37

48
import { useAppContext } from "@contexts/AppContext";
59
import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation";
610
import { useLanguages } from "@hooks/useLanguages";
711
import { LanguageType } from "@types";
12+
import { configureProfile } from "@utils/configureProfile";
813
import { slugify } from "@utils/slugify";
914

10-
// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
11-
1215
const LanguageSelector = () => {
1316
const navigate = useNavigate();
1417

15-
const { language, setLanguage } = useAppContext();
18+
const { language, setLanguage, setCategory } = useAppContext();
1619
const { fetchedLanguages, loading, error } = useLanguages();
1720

1821
const dropdownRef = useRef<HTMLDivElement>(null);
1922
const [isOpen, setIsOpen] = useState(false);
2023

21-
const handleSelect = (selected: LanguageType) => {
22-
setLanguage(selected);
23-
navigate(`/${slugify(selected.name)}`);
24+
const handleSelect = async (selected: LanguageType) => {
25+
const { language: newLanguage, category: newCategory } =
26+
await configureProfile({
27+
languageName: selected.name,
28+
});
29+
30+
setLanguage(newLanguage);
31+
setCategory(newCategory);
32+
navigate(`/${slugify(newLanguage.name)}/${slugify(newCategory)}`);
2433
setIsOpen(false);
2534
};
2635

@@ -66,8 +75,13 @@ const LanguageSelector = () => {
6675
}
6776
}, [isOpen, focusedIndex]);
6877

69-
if (loading) return <p>Loading languages...</p>;
70-
if (error) return <p>Error fetching languages: {error}</p>;
78+
if (loading) {
79+
return <p>Loading languages...</p>;
80+
}
81+
82+
if (error) {
83+
return <p>Error fetching languages: {error}</p>;
84+
}
7185

7286
return (
7387
<div

src/components/SearchInput.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ const SearchInput = () => {
2727

2828
const clearSearch = useCallback(() => {
2929
setInputVal("");
30-
// setCategory(defaultCategory);
3130
setSearchText("");
32-
setSearchParams({});
33-
}, [setSearchParams, setSearchText]);
31+
searchParams.delete("search");
32+
setSearchParams(searchParams);
33+
}, [searchParams, setSearchParams, setSearchText]);
3434

3535
const handleEscapePress = useCallback(
3636
(e: KeyboardEvent) => {
@@ -61,15 +61,16 @@ const SearchInput = () => {
6161

6262
const formattedVal = inputVal.trim().toLowerCase();
6363

64-
// setCategory(defaultCategory);
6564
setSearchText(formattedVal);
6665
if (!formattedVal) {
67-
setSearchParams({});
66+
searchParams.delete("search");
67+
setSearchParams(searchParams);
6868
} else {
69-
setSearchParams({ search: formattedVal });
69+
searchParams.set("search", formattedVal);
70+
setSearchParams(searchParams);
7071
}
7172
},
72-
[inputVal, setSearchParams, setSearchText]
73+
[inputVal, searchParams, setSearchParams, setSearchText]
7374
);
7475

7576
useEffect(() => {

src/components/SnippetList.tsx

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,56 @@
11
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
2-
import { useState } from "react";
2+
import { useEffect, useState } from "react";
3+
import { useSearchParams } from "react-router-dom";
34

45
import { useAppContext } from "@contexts/AppContext";
56
import { useSnippets } from "@hooks/useSnippets";
67
import { SnippetType } from "@types";
8+
import { slugify } from "@utils/slugify";
79

810
import { LeftAngleArrowIcon } from "./Icons";
911
import SnippetModal from "./SnippetModal";
1012

1113
const SnippetList = () => {
14+
const [searchParams, setSearchParams] = useSearchParams();
1215
const shouldReduceMotion = useReducedMotion();
1316

1417
const { language, snippet, setSnippet } = useAppContext();
1518
const { fetchedSnippets } = useSnippets();
1619

1720
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
1821

19-
if (!fetchedSnippets) {
20-
return (
21-
<div>
22-
<LeftAngleArrowIcon />
23-
</div>
24-
);
25-
}
26-
27-
const handleOpenModal = (activeSnippet: SnippetType) => {
22+
const handleOpenModal = (selected: SnippetType) => () => {
2823
setIsModalOpen(true);
29-
setSnippet(activeSnippet);
24+
setSnippet(selected);
25+
searchParams.set("snippet", slugify(selected.title));
26+
setSearchParams(searchParams);
3027
};
3128

3229
const handleCloseModal = () => {
3330
setIsModalOpen(false);
3431
setSnippet(null);
32+
searchParams.delete("snippet");
33+
setSearchParams(searchParams);
3534
};
3635

36+
/**
37+
* open the relevant modal if the snippet is in the search params
38+
*/
39+
useEffect(() => {
40+
const snippetSlug = searchParams.get("snippet");
41+
if (!snippetSlug) {
42+
return;
43+
}
44+
45+
const selectedSnippet = (fetchedSnippets ?? []).find(
46+
(item) => slugify(item.title) === snippetSlug
47+
);
48+
if (selectedSnippet) {
49+
handleOpenModal(selectedSnippet)();
50+
}
51+
// eslint-disable-next-line react-hooks/exhaustive-deps
52+
}, [fetchedSnippets, searchParams]);
53+
3754
if (!fetchedSnippets) {
3855
return (
3956
<div>
@@ -77,7 +94,7 @@ const SnippetList = () => {
7794
<motion.button
7895
className="snippet | flow"
7996
data-flow-space="sm"
80-
onClick={() => handleOpenModal(snippet)}
97+
onClick={handleOpenModal(snippet)}
8198
whileHover={{ scale: 1.01 }}
8299
whileTap={{ scale: 0.98 }}
83100
>

src/contexts/AppContext.tsx

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
1-
import {
2-
createContext,
3-
FC,
4-
useCallback,
5-
useContext,
6-
useEffect,
7-
useState,
8-
} from "react";
1+
import { createContext, FC, useContext, useEffect, useState } from "react";
92
import { useParams } from "react-router-dom";
103

114
import { useLanguages } from "@hooks/useLanguages";
12-
import { AppState, CategoryType, LanguageType, SnippetType } from "@types";
13-
import { defaultLanguage } from "@utils/consts";
14-
import { slugify } from "@utils/slugify";
5+
import { AppState, LanguageType, SnippetType } from "@types";
6+
import { configureProfile } from "@utils/configureProfile";
157

168
// TODO: add custom loading and error handling
179
const defaultState: AppState = {
@@ -39,50 +31,22 @@ export const AppProvider: FC<{ children: React.ReactNode }> = ({
3931
const [snippet, setSnippet] = useState<SnippetType | null>(null);
4032
const [searchText, setSearchText] = useState<string>("");
4133

42-
const assignLanguage = useCallback(() => {
43-
if (fetchedLanguages.length === 0) {
44-
return;
45-
}
34+
const configure = async () => {
35+
const { language, category } = await configureProfile({
36+
languageName,
37+
categoryName,
38+
});
4639

47-
const language = fetchedLanguages.find(
48-
(lang) => slugify(lang.name) === languageName
49-
);
50-
if (!language) {
51-
setLanguage(defaultLanguage);
52-
return;
53-
}
5440
setLanguage(language);
55-
}, [fetchedLanguages, languageName]);
56-
57-
const assignCategory = useCallback(async () => {
58-
if (!language) {
59-
return;
60-
}
61-
62-
let category: CategoryType | undefined;
63-
try {
64-
const res = await fetch(`/consolidated/${slugify(language.name)}.json`);
65-
const data: CategoryType[] = await res.json();
66-
category = data.find((item) => item.name === categoryName);
67-
if (!category) {
68-
setCategory(data[0].name);
69-
return;
70-
}
71-
setCategory(category.name);
72-
} catch (_error) {
73-
// no-op
74-
}
75-
}, [language, categoryName]);
76-
77-
useEffect(() => {
78-
assignLanguage();
79-
}, [assignLanguage, languageName]);
41+
setCategory(category);
42+
};
8043

8144
useEffect(() => {
82-
assignCategory();
83-
}, [assignCategory, categoryName]);
45+
configure();
46+
// eslint-disable-next-line react-hooks/exhaustive-deps
47+
}, [fetchedLanguages]);
8448

85-
if (!language || !category) {
49+
if (language === null || category === null) {
8650
return <div>Loading...</div>;
8751
}
8852

0 commit comments

Comments
 (0)