Skip to content

Commit 7c572c2

Browse files
committed
Add basic search functionality
1 parent 600723c commit 7c572c2

File tree

9 files changed

+185
-20
lines changed

9 files changed

+185
-20
lines changed

src/App.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
1-
import { useAppContext } from "./contexts/AppContext";
2-
1+
import { Routes, Route } from "react-router-dom";
32
import Header from "./layouts/Header";
43
import Banner from "./layouts/Banner";
5-
import Sidebar from "./layouts/Sidebar";
64
import Footer from "./layouts/Footer";
7-
8-
import SnippetList from "./components/SnippetList";
5+
import HomePage from "./pages/HomePage.tsx";
6+
import SearchPage from "./pages/SearchPage.tsx";
97

108
const App = () => {
11-
const { category } = useAppContext();
12-
139
return (
1410
<div className="container flow">
1511
<Header />
1612
<Banner />
1713
<main className="main">
18-
<Sidebar />
19-
<section className="flow">
20-
<h2 className="section-title">
21-
{category ? category : "Select a category"}
22-
</h2>
23-
<SnippetList />
24-
</section>
14+
<Routes>
15+
<Route path="/" element={<HomePage />} />
16+
<Route path="/search" element={<SearchPage />} />
17+
</Routes>
2518
</main>
2619
<Footer />
2720
</div>

src/components/SearchFilters.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { useCategories } from "../hooks/useCategories";
2+
import { useAppContext } from "../contexts/AppContext";
3+
4+
const SearchFilters = () => {
5+
const { category, setCategory } = useAppContext();
6+
const { fetchedCategories } = useCategories();
7+
8+
return (
9+
<div className="search-filters">
10+
<select value={category} onChange={(e) => setCategory(e.target.value)}>
11+
<option value="">All Categories</option>
12+
{fetchedCategories.map((cat, idx) => (
13+
<option key={idx} value={cat}>
14+
{cat}
15+
</option>
16+
))}
17+
</select>
18+
</div>
19+
);
20+
};
21+
22+
export default SearchFilters;

src/components/SearchInput.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
import { SearchIcon } from "./Icons";
2+
import { useState, useCallback } from "react";
3+
import { useSearchParams, useNavigate } from "react-router-dom";
24

35
const SearchInput = () => {
6+
const navigate = useNavigate();
7+
const [searchParams, setSearchParams] = useSearchParams();
8+
const [searchValue, setSearchValue] = useState(searchParams.get("q") || "");
9+
10+
const debouncedSearch = useCallback(
11+
debounce((query: string) => {
12+
if (query) {
13+
setSearchParams({ q: query });
14+
navigate(`/search?q=${encodeURIComponent(query.trim().toLowerCase())}`);
15+
} else {
16+
setSearchParams({});
17+
navigate("/");
18+
}
19+
}, 200),
20+
[setSearchParams, navigate]
21+
);
22+
23+
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
24+
const value = e.target.value;
25+
setSearchValue(value);
26+
debouncedSearch(value);
27+
};
28+
429
return (
530
<div className="search-field">
631
<label htmlFor="search">
@@ -9,11 +34,25 @@ const SearchInput = () => {
934
<input
1035
type="search"
1136
id="search"
37+
value={searchValue}
38+
onChange={handleSearch}
1239
placeholder="Search here..."
1340
autoComplete="off"
1441
/>
1542
</div>
1643
);
1744
};
1845

46+
// Debounce utility function
47+
function debounce<T extends (...args: any[]) => any>(
48+
func: T,
49+
wait: number
50+
): (...args: Parameters<T>) => void {
51+
let timeout: ReturnType<typeof setTimeout>;
52+
return (...args: Parameters<T>) => {
53+
clearTimeout(timeout);
54+
timeout = setTimeout(() => func(...args), wait);
55+
};
56+
}
57+
1958
export default SearchInput;

src/components/SearchSnippetList.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useState, useMemo } from "react";
2+
import { SnippetType } from "../types";
3+
import { useAppContext } from "../contexts/AppContext";
4+
import { useSnippets } from "../hooks/useSnippets";
5+
import SnippetModal from "./SnippetModal";
6+
7+
const SearchSnippetList = ({ query }: { query: string | null }) => {
8+
const { language, snippet, setSnippet } = useAppContext();
9+
const { fetchedSnippets, loading } = useSnippets();
10+
const [isModalOpen, setIsModalOpen] = useState(false);
11+
12+
const filteredSnippets = useMemo(() => {
13+
if (!query) return [];
14+
return fetchedSnippets.filter((snippet) =>
15+
snippet.title.toLowerCase().includes(query.toLowerCase())
16+
);
17+
}, [fetchedSnippets, query]);
18+
19+
const handleOpenModal = (activeSnippet: SnippetType) => {
20+
setIsModalOpen(true);
21+
setSnippet(activeSnippet);
22+
};
23+
24+
const handleCloseModal = () => {
25+
setIsModalOpen(false);
26+
setSnippet(null);
27+
};
28+
29+
if (loading) return <div>Searching...</div>;
30+
if (filteredSnippets.length === 0)
31+
return <div>No results found for "{query}"</div>;
32+
33+
return (
34+
<>
35+
<ul role="list" className="snippets">
36+
{filteredSnippets.map((snippet, idx) => (
37+
<li key={idx}>
38+
<button
39+
className="snippet | flow"
40+
data-flow-space="sm"
41+
onClick={() => handleOpenModal(snippet)}
42+
>
43+
<div className="snippet__preview">
44+
<img src={language.icon} alt={language.lang} />
45+
</div>
46+
<h3 className="snippet__title">{snippet.title}</h3>
47+
<p className="snippet__description">{snippet.description}</p>
48+
</button>
49+
</li>
50+
))}
51+
</ul>
52+
53+
{isModalOpen && snippet && (
54+
<SnippetModal
55+
snippet={snippet}
56+
handleCloseModal={handleCloseModal}
57+
language={language.lang}
58+
/>
59+
)}
60+
</>
61+
);
62+
};
63+
64+
export default SearchSnippetList;

src/hooks/useCategories.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ export const useCategories = () => {
1616
);
1717

1818
const fetchedCategories = useMemo(() => {
19-
return data ? data.map((item) => item.categoryName) : [];
19+
const categories = data ? data.map((item) => item.categoryName) : [];
20+
return ["All snippets", ...categories];
2021
}, [data]);
2122

2223
return { fetchedCategories, loading, error };

src/hooks/useSnippets.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ export const useSnippets = () => {
1414
`/data/${slugify(language.lang)}.json`
1515
);
1616

17-
const fetchedSnippets = data
18-
? data.find((item) => item.categoryName === category)?.snippets
17+
const fetchedSnippets: SnippetType[] = data
18+
? category === "All snippets"
19+
? data.flatMap((item) => item.snippets)
20+
: (data.find((item) => item.categoryName === category)?.snippets ?? [])
1921
: [];
2022

2123
return { fetchedSnippets, loading, error };

src/main.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ import { createRoot } from "react-dom/client";
33
import "./styles/main.css";
44
import App from "./App";
55
import { AppProvider } from "./contexts/AppContext";
6+
import { BrowserRouter } from "react-router-dom";
67

78
createRoot(document.getElementById("root")!).render(
89
<StrictMode>
9-
<AppProvider>
10-
<App />
11-
</AppProvider>
10+
<BrowserRouter>
11+
<AppProvider>
12+
<App />
13+
</AppProvider>
14+
</BrowserRouter>
1215
</StrictMode>
1316
);

src/pages/HomePage.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useAppContext } from "../contexts/AppContext";
2+
import SnippetList from "../components/SnippetList";
3+
import Sidebar from "../layouts/Sidebar";
4+
5+
const HomePage = () => {
6+
const { category } = useAppContext();
7+
8+
return (
9+
<>
10+
<Sidebar />
11+
<section className="flow">
12+
<h2 className="section-title">
13+
{category ? category : "Select a category"}
14+
</h2>
15+
<SnippetList />
16+
</section>
17+
</>
18+
);
19+
};
20+
21+
export default HomePage;

src/pages/SearchPage.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { useSearchParams } from "react-router-dom";
2+
import SearchSnippetList from "../components/SearchSnippetList";
3+
import Sidebar from "../layouts/Sidebar";
4+
5+
const SearchPage = () => {
6+
const [searchParams] = useSearchParams();
7+
const query = searchParams.get("q");
8+
9+
return (
10+
<>
11+
<Sidebar />
12+
<section className="flow">
13+
<h2 className="section-title">Search Results for: {query}</h2>
14+
<SearchSnippetList query={query} />
15+
</section>
16+
</>
17+
);
18+
};
19+
20+
export default SearchPage;

0 commit comments

Comments
 (0)