Skip to content

Feature - Implement basic search functionality v2 #159

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
8bb107c
working search feature
barrymun Jan 3, 2025
bfe0b4b
Merge branch 'main' into feature/search
barrymun Jan 3, 2025
b78c224
Update consolidated snippets
actions-user Jan 3, 2025
cb4e762
Merge branch 'main' into feature/search
barrymun Jan 3, 2025
558dff5
upgrading react-router-dom package to latest and utilising new routes…
barrymun Jan 3, 2025
965cce5
navigation concept (wip)
barrymun Jan 9, 2025
5aa4619
Merge branch 'main' into feature/search
barrymun Jan 9, 2025
43bcf18
working search functionality with language and category set in the ur…
barrymun Jan 10, 2025
1b4e679
create an enum for query params, change search to q
barrymun Jan 10, 2025
4d2eb70
change logic so that the filtering of snippets is set based on the UR…
barrymun Jan 10, 2025
70e716c
Merge branch 'main' into feature/search
barrymun Jan 10, 2025
9904aec
remove keyup listener for Enter button as this is no longer required,…
barrymun Jan 10, 2025
5596366
Merge branch 'main' into feature/search
barrymun Jan 12, 2025
8ab6971
additional tests
barrymun Jan 12, 2025
f465bb8
Set the default language if the language is not found in the URL
barrymun Jan 12, 2025
9ebf95b
description and tags searched as well as title
barrymun Jan 13, 2025
2196f7c
changing document and keyup listeners to window and keydown respectiv…
barrymun Jan 13, 2025
31e622c
search feature logic rework to handle search across all languages, ca…
barrymun Jan 13, 2025
6193c90
Revert "search feature logic rework to handle search across all langu…
barrymun Jan 14, 2025
03a9632
rework the search feature so that an All Snippets category is used. t…
barrymun Jan 14, 2025
dad129c
Merge branch 'main' into feature/search
barrymun Jan 18, 2025
2215ba9
Merge branch 'main' into feature/search
barrymun Jan 23, 2025
c037d18
rework the sharing and search logic to also handle sub languages
barrymun Jan 23, 2025
4d05b77
adding tests for languageUtils
barrymun Jan 24, 2025
c0b3bff
refactor so that consolidated prefix is within the getLanguageFileNam…
barrymun Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
226 changes: 125 additions & 101 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0",
"react-router-dom": "^7.1.1",
"react-syntax-highlighter": "^15.6.1"
},
"devDependencies": {
Expand Down
18 changes: 18 additions & 0 deletions src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Route, Routes } from "react-router-dom";

import App from "@components/App";
import SnippetList from "@components/SnippetList";

const AppRouter = () => {
return (
<Routes>
<Route element={<App />}>
<Route path="/" element={<SnippetList />} />
<Route path="/:languageName" element={<SnippetList />} />
<Route path="/:languageName/:categoryName" element={<SnippetList />} />
</Route>
</Routes>
);
};

export default AppRouter;
17 changes: 17 additions & 0 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { FC } from "react";

import { AppProvider } from "@contexts/AppContext";

import Container from "./Container";

interface AppProps {}

const App: FC<AppProps> = () => {
return (
<AppProvider>
<Container />
</AppProvider>
);
};

export default App;
60 changes: 42 additions & 18 deletions src/components/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,57 @@
import { useEffect } from "react";
import { FC } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";

import { useAppContext } from "@contexts/AppContext";
import { useCategories } from "@hooks/useCategories";
import { slugify } from "@utils/slugify";

interface CategoryListItemProps {
name: string;
}

const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();

const { language, category, setCategory } = useAppContext();

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

return (
<li className="category">
<button
className={`category__btn ${
slugify(name) === slugify(category) ? "category__btn--active" : ""
}`}
onClick={handleSelect}
>
{name}
</button>
</li>
);
};

const CategoryList = () => {
const { category, setCategory } = useAppContext();
const { fetchedCategories, loading, error } = useCategories();

useEffect(() => {
setCategory(fetchedCategories[0]);
}, [setCategory, fetchedCategories]);

if (loading) return <div>Loading...</div>;
if (loading) {
return <div>Loading...</div>;
}

if (error) return <div>Error occurred: {error}</div>;
if (error) {
return <div>Error occurred: {error}</div>;
}

return (
<ul role="list" className="categories">
{fetchedCategories.map((name, idx) => (
<li key={idx} className="category">
<button
className={`category__btn ${
name === category ? "category__btn--active" : ""
}`}
onClick={() => setCategory(name)}
>
{name}
</button>
</li>
<CategoryListItem key={idx} name={name} />
))}
</ul>
);
Expand Down
12 changes: 8 additions & 4 deletions src/App.tsx → src/components/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import SnippetList from "@components/SnippetList";
import { FC } from "react";
import { Outlet } from "react-router-dom";

import { useAppContext } from "@contexts/AppContext";
import Banner from "@layouts/Banner";
import Footer from "@layouts/Footer";
import Header from "@layouts/Header";
import Sidebar from "@layouts/Sidebar";

const App = () => {
interface ContainerProps {}

const Container: FC<ContainerProps> = () => {
const { category } = useAppContext();

return (
Expand All @@ -18,12 +22,12 @@ const App = () => {
<h2 className="section-title">
{category ? category : "Select a category"}
</h2>
<SnippetList />
<Outlet />
</section>
</main>
<Footer />
</div>
);
};

export default App;
export default Container;
39 changes: 32 additions & 7 deletions src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,41 @@
/**
* Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
*/

import { useRef, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";

import { useAppContext } from "@contexts/AppContext";
import { useKeyboardNavigation } from "@hooks/useKeyboardNavigation";
import { useLanguages } from "@hooks/useLanguages";
import { LanguageType } from "@types";

// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
import { configureUserSelection } from "@utils/configureUserSelection";
import { slugify } from "@utils/slugify";

const LanguageSelector = () => {
const { language, setLanguage } = useAppContext();
const navigate = useNavigate();

const { language, setLanguage, setCategory, setSearchText } = useAppContext();
const { fetchedLanguages, loading, error } = useLanguages();

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

const handleSelect = (selected: LanguageType) => {
setLanguage(selected);
/**
* When setting a new language we need to ensure that a category
* has been set given this new language.
* Ensure that the search text is cleared.
*/
const handleSelect = async (selected: LanguageType) => {
const { language: newLanguage, category: newCategory } =
await configureUserSelection({
languageName: selected.name,
});

setSearchText("");
setLanguage(newLanguage);
setCategory(newCategory);
navigate(`/${slugify(newLanguage.name)}/${slugify(newCategory)}`);
setIsOpen(false);
};

Expand Down Expand Up @@ -61,8 +81,13 @@ const LanguageSelector = () => {
}
}, [isOpen, focusedIndex]);

if (loading) return <p>Loading languages...</p>;
if (error) return <p>Error fetching languages: {error}</p>;
if (loading) {
return <p>Loading languages...</p>;
}

if (error) {
return <p>Error fetching languages: {error}</p>;
}

return (
<div
Expand Down
123 changes: 118 additions & 5 deletions src/components/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,130 @@
import { useCallback, useEffect, useRef } from "react";
import { useSearchParams } from "react-router-dom";

import { useAppContext } from "@contexts/AppContext";
import { QueryParams } from "@utils/enums";

import { SearchIcon } from "./Icons";

const SearchInput = () => {
const [searchParams, setSearchParams] = useSearchParams();

const { searchText, setSearchText } = useAppContext();

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

const handleSearchFieldClick = () => {
inputRef.current?.focus();
};

const clearSearch = useCallback(() => {
setSearchText("");
searchParams.delete(QueryParams.SEARCH);
setSearchParams(searchParams);
}, [searchParams, setSearchParams, setSearchText]);

const performSearch = useCallback(() => {
// Check if the input element is focused.
if (document.activeElement !== inputRef.current) {
return;
}

const formattedVal = searchText.toLowerCase();

setSearchText(formattedVal);
if (!formattedVal) {
searchParams.delete(QueryParams.SEARCH);
setSearchParams(searchParams);
} else {
searchParams.set(QueryParams.SEARCH, formattedVal);
setSearchParams(searchParams);
}
}, [searchParams, searchText, setSearchParams, setSearchText]);

/**
* Focus the search input when the user presses the `/` key.
*/
const handleSearchKeyPress = (e: KeyboardEvent) => {
if (e.key === "/") {
e.preventDefault();
inputRef.current?.focus();
}
};

/**
* Clear search text and blur the input when the `Escape` key is pressed.
*/
const handleEscapeKeyPress = useCallback(
(e: KeyboardEvent) => {
if (e.key !== "Escape") {
return;
}

// Check if the input element is focused.
if (document.activeElement !== inputRef.current) {
return;
}

inputRef.current?.blur();

clearSearch();
},
[clearSearch]
);

useEffect(() => {
document.addEventListener("keyup", handleSearchKeyPress);
document.addEventListener("keyup", handleEscapeKeyPress);

return () => {
document.removeEventListener("keyup", handleSearchKeyPress);
document.removeEventListener("keyup", handleEscapeKeyPress);
};
}, [handleEscapeKeyPress]);

/**
* Update the search query in the URL when the search text changes.
*/
useEffect(() => {
performSearch();
}, [searchText, performSearch]);

/**
* Set the search text to the search query from the URL on mount.
*/
useEffect(() => {
const searchQuery = searchParams.get(QueryParams.SEARCH);
if (!searchQuery) {
return;
}

setSearchText(searchQuery);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

return (
<div className="search-field">
<label htmlFor="search">
<SearchIcon />
</label>
<div className="search-field" onClick={handleSearchFieldClick}>
<SearchIcon />
<input
ref={inputRef}
value={searchText}
type="search"
id="search"
placeholder="Search here..."
autoComplete="off"
onChange={(e) => {
const newValue = e.target.value;
if (!newValue) {
clearSearch();
return;
}
setSearchText(newValue);
}}
/>
{!searchText && (
<label htmlFor="search">
Type <kbd>/</kbd> to search
</label>
)}
</div>
);
};
Expand Down
Loading
Loading