From 7c572c240c226f517ca9b08f92e365c8337b6cff Mon Sep 17 00:00:00 2001 From: GreenMan36 Date: Mon, 30 Dec 2024 16:02:43 +0100 Subject: [PATCH 1/7] Add basic search functionality --- src/App.tsx | 21 +++------ src/components/SearchFilters.tsx | 22 ++++++++++ src/components/SearchInput.tsx | 39 +++++++++++++++++ src/components/SearchSnippetList.tsx | 64 ++++++++++++++++++++++++++++ src/hooks/useCategories.ts | 3 +- src/hooks/useSnippets.ts | 6 ++- src/main.tsx | 9 ++-- src/pages/HomePage.tsx | 21 +++++++++ src/pages/SearchPage.tsx | 20 +++++++++ 9 files changed, 185 insertions(+), 20 deletions(-) create mode 100644 src/components/SearchFilters.tsx create mode 100644 src/components/SearchSnippetList.tsx create mode 100644 src/pages/HomePage.tsx create mode 100644 src/pages/SearchPage.tsx diff --git a/src/App.tsx b/src/App.tsx index fc2d1278..883f1a4a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,27 +1,20 @@ -import { useAppContext } from "./contexts/AppContext"; - +import { Routes, Route } from "react-router-dom"; import Header from "./layouts/Header"; import Banner from "./layouts/Banner"; -import Sidebar from "./layouts/Sidebar"; import Footer from "./layouts/Footer"; - -import SnippetList from "./components/SnippetList"; +import HomePage from "./pages/HomePage.tsx"; +import SearchPage from "./pages/SearchPage.tsx"; const App = () => { - const { category } = useAppContext(); - return (
- -
-

- {category ? category : "Select a category"} -

- -
+ + } /> + } /> +
diff --git a/src/components/SearchFilters.tsx b/src/components/SearchFilters.tsx new file mode 100644 index 00000000..40a44358 --- /dev/null +++ b/src/components/SearchFilters.tsx @@ -0,0 +1,22 @@ +import { useCategories } from "../hooks/useCategories"; +import { useAppContext } from "../contexts/AppContext"; + +const SearchFilters = () => { + const { category, setCategory } = useAppContext(); + const { fetchedCategories } = useCategories(); + + return ( +
+ +
+ ); +}; + +export default SearchFilters; diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx index 03ed5f90..a4ff68e2 100644 --- a/src/components/SearchInput.tsx +++ b/src/components/SearchInput.tsx @@ -1,6 +1,31 @@ import { SearchIcon } from "./Icons"; +import { useState, useCallback } from "react"; +import { useSearchParams, useNavigate } from "react-router-dom"; const SearchInput = () => { + const navigate = useNavigate(); + const [searchParams, setSearchParams] = useSearchParams(); + const [searchValue, setSearchValue] = useState(searchParams.get("q") || ""); + + const debouncedSearch = useCallback( + debounce((query: string) => { + if (query) { + setSearchParams({ q: query }); + navigate(`/search?q=${encodeURIComponent(query.trim().toLowerCase())}`); + } else { + setSearchParams({}); + navigate("/"); + } + }, 200), + [setSearchParams, navigate] + ); + + const handleSearch = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchValue(value); + debouncedSearch(value); + }; + return (

{snippet.title}

- {query && ( -

{snippet.description}

- )} +

{snippet.description}

))} diff --git a/src/styles/main.css b/src/styles/main.css index 576e5a66..8800e4fa 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -528,6 +528,21 @@ abbr { .snippet__title { color: var(--text-primary); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + line-clamp: 1; + overflow: hidden; + text-overflow: "…"; +} + +.snippet__description { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + line-clamp: 1; + overflow: hidden; + text-overflow: "…"; } /*------------------------------------*\ From 53be0bd61d13855c1f75d03bd9d94720f3ce5e37 Mon Sep 17 00:00:00 2001 From: GreenMan36 Date: Mon, 30 Dec 2024 23:04:06 +0100 Subject: [PATCH 5/7] Remove straggler issue template file --- .github/ISSUE_TEMPLATE/ADD-LANGUAGE.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/ADD-LANGUAGE.yml diff --git a/.github/ISSUE_TEMPLATE/ADD-LANGUAGE.yml b/.github/ISSUE_TEMPLATE/ADD-LANGUAGE.yml deleted file mode 100644 index e69de29b..00000000 From 7031525b70c41ec1f3822157277786b56d3eded2 Mon Sep 17 00:00:00 2001 From: GreenMan36 Date: Tue, 31 Dec 2024 14:22:42 +0100 Subject: [PATCH 6/7] Refactor useSnippets with feedback and more Instead of using ternary operators, I made a pure function with early returns for easier readability and maintainability. Also moved the endpoint to a const for clarity/simplicity. --- src/hooks/useSnippets.ts | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/hooks/useSnippets.ts b/src/hooks/useSnippets.ts index 54749591..4dbd2206 100644 --- a/src/hooks/useSnippets.ts +++ b/src/hooks/useSnippets.ts @@ -8,17 +8,28 @@ type CategoryData = { snippets: SnippetType[]; }; +const getSnippetsFromData = ( + data: CategoryData[] | null, + category: string +): SnippetType[] => { + if (!data?.length) { + return []; + } + + if (category === "All snippets") { + return data.flatMap((item) => item.snippets); + } + + const categoryData = data.find((item) => item.categoryName === category); + return categoryData?.snippets ?? []; +}; + export const useSnippets = () => { const { language, category } = useAppContext(); - const { data, loading, error } = useFetch( - `/data/${slugify(language.lang)}.json` - ); + const endpoint = `/data/${slugify(language.lang)}.json`; + const { data, loading, error } = useFetch(endpoint); - const fetchedSnippets: SnippetType[] = data - ? category === "All snippets" - ? data.flatMap((item) => item.snippets) - : (data.find((item) => item.categoryName === category)?.snippets ?? []) - : []; + const fetchedSnippets = getSnippetsFromData(data, category); return { fetchedSnippets, loading, error }; }; From 5edc1b1799f91434835659b78ead08450569bcca Mon Sep 17 00:00:00 2001 From: GreenMan36 Date: Tue, 31 Dec 2024 16:41:15 +0100 Subject: [PATCH 7/7] Add less history spammy but scuffed search I rewrote this five times today, all with similar issues. Either you write too much to history, or you overwrite history too much. I'm pretty sure in the current implementation, we don't even need /search as a page --- src/components/SearchInput.tsx | 68 ++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx index a4ff68e2..c760cdf6 100644 --- a/src/components/SearchInput.tsx +++ b/src/components/SearchInput.tsx @@ -1,33 +1,46 @@ import { SearchIcon } from "./Icons"; import { useState, useCallback } from "react"; -import { useSearchParams, useNavigate } from "react-router-dom"; +import { useSearchParams, useNavigate, useLocation } from "react-router-dom"; const SearchInput = () => { const navigate = useNavigate(); + const location = useLocation(); const [searchParams, setSearchParams] = useSearchParams(); const [searchValue, setSearchValue] = useState(searchParams.get("q") || ""); - const debouncedSearch = useCallback( - debounce((query: string) => { - if (query) { - setSearchParams({ q: query }); - navigate(`/search?q=${encodeURIComponent(query.trim().toLowerCase())}`); - } else { + const navigateToSearch = useCallback( + (query: string, isCompletedSearch = false) => { + const trimmedQuery = query.trim().toLowerCase(); + + if (!trimmedQuery) { + // Remove search params and navigate to home if query is empty setSearchParams({}); - navigate("/"); + navigate("/", { replace: true }); + return; } - }, 200), - [setSearchParams, navigate] - ); - const handleSearch = (e: React.ChangeEvent) => { - const value = e.target.value; - setSearchValue(value); - debouncedSearch(value); - }; + // Set the search params with the query + // Use replace: true for keypresses (when isCompletedSearch is false) + setSearchParams({ q: trimmedQuery }, { replace: !isCompletedSearch }); + + // Only navigate if we're not already on the search page + if (location.pathname !== "/search") { + navigate("/search", { + replace: isCompletedSearch || location.pathname === "/search", + }); + } + }, + [navigate, location.pathname, setSearchParams] + ); return ( -
+
{ + e.preventDefault(); + navigateToSearch(searchValue, true); + }} + className="search-field" + > @@ -35,24 +48,17 @@ const SearchInput = () => { type="search" id="search" value={searchValue} - onChange={handleSearch} + onChange={(e) => { + const newValue = e.target.value; + setSearchValue(newValue); + navigateToSearch(newValue, false); + }} + onBlur={() => navigateToSearch(searchValue, true)} placeholder="Search here..." autoComplete="off" /> -
+ ); }; -// Debounce utility function -function debounce any>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: ReturnType; - return (...args: Parameters) => { - clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} - export default SearchInput;