Skip to content
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

Feature - Implement basic search functionality v2 #159

Merged
merged 25 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
21 changes: 21 additions & 0 deletions src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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/:subLanguageName/: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;
61 changes: 43 additions & 18 deletions src/components/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,58 @@
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 { defaultCategoryName } from "@utils/consts";
import { slugify } from "@utils/slugify";

interface CategoryListItemProps {
name: string;
}

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

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

const handleSelect = () => {
navigate({
pathname: `/${slugify(language.name)}/${slugify(subLanguage)}/${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">
<CategoryListItem name={defaultCategoryName} />
{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;
173 changes: 116 additions & 57 deletions src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,127 @@
/**
* Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/
*/

import { useRef, useEffect, useState, useMemo } 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";
import { configureUserSelection } from "@utils/configureUserSelection";
import {
getLanguageDisplayLogo,
getLanguageDisplayName,
} from "@utils/languageUtils";
import { slugify } from "@utils/slugify";

import SubLanguageSelector from "./SubLanguageSelector";

// Inspired by https://blog.logrocket.com/creating-custom-select-dropdown-css/

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

const { language, subLanguage, setSearchText } = useAppContext();
const { fetchedLanguages, loading, error } = useLanguages();
const allLanguages = useMemo(
() =>
fetchedLanguages.flatMap((lang) =>
lang.subLanguages.length > 0
? [
lang,
...lang.subLanguages.map((subLang) => ({
...subLang,
mainLanguage: lang,
subLanguages: [],
})),
]
: [lang]
),
[fetchedLanguages]
);

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

const handleSelect = (selected: LanguageType) => {
setLanguage(selected);
const keyboardItems = useMemo(() => {
return fetchedLanguages.flatMap((lang) =>
openedLanguages.map((ol) => ol.name).includes(lang.name)
? [
{ languageName: lang.name },
...lang.subLanguages.map((sl) => ({
languageName: lang.name,
subLanguageName: sl.name,
})),
]
: [{ languageName: lang.name }]
);
}, [fetchedLanguages, openedLanguages]);

const displayName = useMemo(
() => getLanguageDisplayName(language.name, subLanguage),
[language.name, subLanguage]
);

const displayLogo = useMemo(
() => getLanguageDisplayLogo(language.name, subLanguage),
[language.name, subLanguage]
);

const handleToggleSubLanguage = (name: LanguageType["name"]) => {
const isAlreadyOpened = openedLanguages.some((lang) => lang.name === name);
const openedLang = fetchedLanguages.find((lang) => lang.name === name);
if (openedLang === undefined || openedLang.subLanguages.length === 0) {
return;
}

if (!isAlreadyOpened) {
setOpenedLanguages((prev) => [...prev, openedLang]);
} else {
setOpenedLanguages((prev) =>
prev.filter((lang) => lang.name !== openedLang.name)
);
}
};

/**
* 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,
subLanguage: newSubLanguage,
category: newCategory,
} = await configureUserSelection({
languageName: selected.name,
});

setSearchText("");
navigate(
`/${slugify(newLanguage.name)}/${slugify(newSubLanguage)}/${slugify(newCategory)}`
);
setIsOpen(false);
setOpenedLanguages([]);
};

const afterSelect = () => {
setIsOpen(false);
};

const handleSubLanguageSelect = async (
selectedLanguageName: LanguageType["name"],
selectedSubLanguageName:
| LanguageType["subLanguages"][number]["name"]
| undefined
) => {
const {
language: newLanguage,
subLanguage: newSubLanguage,
category: newCategory,
} = await configureUserSelection({
languageName: selectedLanguageName,
subLanguageName: selectedSubLanguageName,
});

setSearchText("");
navigate(
`/${slugify(newLanguage.name)}/${slugify(newSubLanguage)}/${slugify(newCategory)}`
);
afterSelect();
};

const { focusedIndex, handleKeyDown, resetFocus, focusFirst } =
useKeyboardNavigation({
items: allLanguages,
items: keyboardItems,
isOpen,
openedLanguages,
toggleDropdown: (openedLang) => handleToggleSublanguage(openedLang),
onSelect: handleSelect,
toggleDropdown: (l) => handleToggleSubLanguage(l),
onSelect: (l, sl) => handleSubLanguageSelect(l, sl),
onClose: () => setIsOpen(false),
});

Expand All @@ -60,20 +136,6 @@ const LanguageSelector = () => {
}, 0);
};

const handleToggleSublanguage = (openedLang: LanguageType) => {
const isAlreadyOpened = openedLanguages.some(
(lang) => lang.name === openedLang.name
);

if (!isAlreadyOpened) {
setOpenedLanguages((prev) => [...prev, openedLang]);
} else {
setOpenedLanguages((prev) =>
prev.filter((lang) => lang.name !== openedLang.name)
);
}
};

const toggleDropdown = () => {
setIsOpen((prev) => {
if (!prev) setTimeout(focusFirst, 0);
Expand All @@ -88,13 +150,6 @@ const LanguageSelector = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);

useEffect(() => {
if (language.mainLanguage) {
handleToggleSublanguage(language.mainLanguage);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [language]);

useEffect(() => {
if (isOpen && focusedIndex >= 0) {
const element = document.querySelector(
Expand All @@ -104,8 +159,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 All @@ -121,8 +181,8 @@ const LanguageSelector = () => {
onClick={toggleDropdown}
>
<div className="selector__value">
<img src={language.icon} alt="" />
<span>{language.name || "Select a language"}</span>
<img src={displayLogo} alt="" />
<span>{displayName}</span>
</div>
<span className="selector__arrow" />
</button>
Expand All @@ -136,13 +196,12 @@ const LanguageSelector = () => {
{fetchedLanguages.map((lang, index) =>
lang.subLanguages.length > 0 ? (
<SubLanguageSelector
key={index}
mainLanguage={lang}
afterSelect={() => {
setIsOpen(false);
}}
key={lang.name}
opened={openedLanguages.includes(lang)}
onDropdownToggle={handleToggleSublanguage}
parentLanguage={lang}
onDropdownToggle={handleToggleSubLanguage}
handleParentSelect={handleSelect}
afterSelect={afterSelect}
/>
) : (
<li
Expand Down
Loading
Loading