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 2 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
41 changes: 25 additions & 16 deletions src/components/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
import { useEffect } from "react";
import { FC } from "react";

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

const CategoryList = () => {
interface CategoryListItemProps {
name: string;
}

const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
const { category, setCategory } = useAppContext();
const { fetchedCategories, loading, error } = useCategories();

useEffect(() => {
setCategory(fetchedCategories[0]);
}, [setCategory, fetchedCategories]);
return (
<li className="category">
<button
className={`category__btn ${
name === category ? "category__btn--active" : ""
}`}
onClick={() => setCategory(name)}
>
{name}
</button>
</li>
);
};

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

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

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

return (
<ul role="list" className="categories">
<CategoryListItem name={defaultCategory} />
{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;
122 changes: 117 additions & 5 deletions src/components/SearchInput.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,129 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useSearchParams } from "react-router-dom";

import { useAppContext } from "@contexts/AppContext";
import { defaultCategory } from "@utils/consts";

import { SearchIcon } from "./Icons";

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

const { searchText, setSearchText, setCategory } = useAppContext();

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

const [inputVal, setInputVal] = useState<string>("");

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

const handleSearchKeyPress = (e: KeyboardEvent) => {
if (e.key === "/") {
e.preventDefault();
inputRef.current?.focus();
}
};

const clearSearch = useCallback(() => {
setInputVal("");
setCategory(defaultCategory);
setSearchText("");
setSearchParams({});
}, [setCategory, setSearchParams, setSearchText]);

const handleEscapePress = useCallback(
(e: KeyboardEvent) => {
if (e.key !== "Escape") {
return;
}
// check if the input is focused
if (document.activeElement !== inputRef.current) {
return;
}

inputRef.current?.blur();

clearSearch();
},
[clearSearch]
);

const handleReturnPress = useCallback(
(e: KeyboardEvent) => {
if (e.key !== "Enter") {
return;
}
// check if the input is focused
if (document.activeElement !== inputRef.current) {
return;
}

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

setCategory(defaultCategory);
setSearchText(formattedVal);
if (!formattedVal) {
setSearchParams({});
} else {
setSearchParams({ search: formattedVal });
}
},
[inputVal, setCategory, setSearchParams, setSearchText]
);

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

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

/**
* Set the input value and search text to the search query from the URL
*/
useEffect(() => {
const search = searchParams.get("search") || "";
setInputVal(search);
setSearchText(search);
// 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}
type="search"
id="search"
placeholder="Search here..."
autoComplete="off"
value={inputVal}
onChange={(e) => {
const newValue = e.target.value;
if (!newValue) {
clearSearch();
return;
}
setInputVal(newValue);
}}
onBlur={() => {
// ensure the input value is always in sync with the search text
if (inputVal !== searchText) {
setInputVal(searchText);
}
}}
/>
{!inputVal && !searchText && (
<label htmlFor="search">
Type <kbd>/</kbd> to search
</label>
)}
</div>
);
};
Expand Down
16 changes: 9 additions & 7 deletions src/components/SnippetList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,8 @@ import SnippetModal from "./SnippetModal";
const SnippetList = () => {
const { language, snippet, setSnippet } = useAppContext();
const { fetchedSnippets } = useSnippets();
const [isModalOpen, setIsModalOpen] = useState(false);

if (!fetchedSnippets)
return (
<div>
<LeftAngleArrowIcon />
</div>
);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

const handleOpenModal = (activeSnippet: SnippetType) => {
setIsModalOpen(true);
Expand All @@ -30,6 +24,14 @@ const SnippetList = () => {
setSnippet(null);
};

if (!fetchedSnippets) {
return (
<div>
<LeftAngleArrowIcon />
</div>
);
}

return (
<>
<motion.ul role="list" className="snippets">
Expand Down
2 changes: 1 addition & 1 deletion src/components/SnippetModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ReactDOM from "react-dom";

import { useEscapeKey } from "@hooks/useEscapeKey";
import { SnippetType } from "@types";
import { slugify } from "@utils/slugify";
import { slugify } from "@utils/helpers/slugify";

import Button from "./Button";
import CodePreview from "./CodePreview";
Expand Down
16 changes: 8 additions & 8 deletions src/contexts/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { createContext, FC, useContext, useState } from "react";

import { AppState, LanguageType, SnippetType } from "@types";

// tokens
const defaultLanguage: LanguageType = {
lang: "JAVASCRIPT",
icon: "/icons/javascript.svg",
};
import { defaultCategory, defaultLanguage } from "@utils/consts";

// TODO: add custom loading and error handling
const defaultState: AppState = {
language: defaultLanguage,
setLanguage: () => {},
category: "",
category: defaultCategory,
setCategory: () => {},
snippet: null,
setSnippet: () => {},
searchText: "",
setSearchText: () => {},
};

const AppContext = createContext<AppState>(defaultState);
Expand All @@ -24,8 +21,9 @@ export const AppProvider: FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [language, setLanguage] = useState<LanguageType>(defaultLanguage);
const [category, setCategory] = useState<string>("");
const [category, setCategory] = useState<string>(defaultCategory);
const [snippet, setSnippet] = useState<SnippetType | null>(null);
const [searchText, setSearchText] = useState<string>("");

return (
<AppContext.Provider
Expand All @@ -36,6 +34,8 @@ export const AppProvider: FC<{ children: React.ReactNode }> = ({
setCategory,
snippet,
setSnippet,
searchText,
setSearchText,
}}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useCategories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useMemo } from "react";

import { useAppContext } from "@contexts/AppContext";
import { SnippetType } from "@types";
import { slugify } from "@utils/slugify";
import { slugify } from "@utils/helpers/slugify";

import { useFetch } from "./useFetch";

Expand Down
35 changes: 30 additions & 5 deletions src/hooks/useSnippets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useMemo } from "react";

import { useAppContext } from "@contexts/AppContext";
import { SnippetType } from "@types";
import { slugify } from "@utils/slugify";
import { defaultCategory } from "@utils/consts";
import { slugify } from "@utils/helpers/slugify";

import { useFetch } from "./useFetch";

Expand All @@ -10,14 +13,36 @@ type CategoryData = {
};

export const useSnippets = () => {
const { language, category } = useAppContext();
const { language, category, searchText } = useAppContext();
const { data, loading, error } = useFetch<CategoryData[]>(
`/consolidated/${slugify(language.lang)}.json`
);

const fetchedSnippets = data
? data.find((item) => item.categoryName === category)?.snippets
: [];
const fetchedSnippets = useMemo(() => {
if (!data) {
return [];
}

if (category === defaultCategory) {
if (searchText) {
return data
.flatMap((item) => item.snippets)
.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
);
}
return data.flatMap((item) => item.snippets);
}

if (searchText) {
return data
.find((item) => item.categoryName === category)
?.snippets.filter((item) =>
item.title.toLowerCase().includes(searchText.toLowerCase())
);
}
return data.find((item) => item.categoryName === category)?.snippets;
}, [category, data, searchText]);

return { fetchedSnippets, loading, error };
};
6 changes: 3 additions & 3 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import "@styles/main.css";

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";

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

import App from "./App";
import { router } from "@router";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<AppProvider>
<App />
<RouterProvider router={router} />
</AppProvider>
</StrictMode>
);
Loading
Loading