Skip to content

Commit

Permalink
working search functionality with language and category set in the ur…
Browse files Browse the repository at this point in the history
…l, search and snippet set as query params
  • Loading branch information
barrymun committed Jan 10, 2025
1 parent 5aa4619 commit 43bcf18
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 106 deletions.
1 change: 1 addition & 0 deletions cspell-dict.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
quicksnip
slugified
slugifyed
4 changes: 2 additions & 2 deletions src/AppRouter.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Route, Routes } from "react-router-dom";

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

const AppRouter = () => {
return (
<Routes>
<Route element={<Container />}>
<Route element={<App />}>
<Route path="/" element={<SnippetList />} />
<Route path="/:languageName" element={<SnippetList />} />
<Route path="/:languageName/:categoryName" element={<SnippetList />} />
Expand Down
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;
17 changes: 13 additions & 4 deletions src/components/CategoryList.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import { FC } from "react";
import { useNavigate } 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 { category, setCategory } = useAppContext();
const navigate = useNavigate();

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

const handleSelect = () => {
setCategory(name);
navigate(`/${slugify(language.name)}/${slugify(name)}`);
};

return (
<li className="category">
<button
className={`category__btn ${
name === category ? "category__btn--active" : ""
slugify(name) === slugify(category) ? "category__btn--active" : ""
}`}
onClick={handleSelect}
>
Expand All @@ -31,9 +36,13 @@ const CategoryListItem: FC<CategoryListItemProps> = ({ name }) => {
const CategoryList = () => {
const { fetchedCategories, loading, error } = useCategories();

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">
Expand Down
32 changes: 15 additions & 17 deletions src/components/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FC } from "react";
import { Outlet } from "react-router-dom";

import { AppProvider, useAppContext } from "@contexts/AppContext";
import { useAppContext } from "@contexts/AppContext";
import Banner from "@layouts/Banner";
import Footer from "@layouts/Footer";
import Header from "@layouts/Header";
Expand All @@ -13,22 +13,20 @@ const Container: FC<ContainerProps> = () => {
const { category } = useAppContext();

return (
<AppProvider>
<div className="container flow">
<Header />
<Banner />
<main className="main">
<Sidebar />
<section className="flow">
<h2 className="section-title">
{category ? category : "Select a category"}
</h2>
<Outlet />
</section>
</main>
<Footer />
</div>
</AppProvider>
<div className="container flow">
<Header />
<Banner />
<main className="main">
<Sidebar />
<section className="flow">
<h2 className="section-title">
{category ? category : "Select a category"}
</h2>
<Outlet />
</section>
</main>
<Footer />
</div>
);
};

Expand Down
30 changes: 22 additions & 8 deletions src/components/LanguageSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,35 @@
/**
* 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";
import { configureProfile } from "@utils/configureProfile";
import { slugify } from "@utils/slugify";

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

const LanguageSelector = () => {
const navigate = useNavigate();

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

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

const handleSelect = (selected: LanguageType) => {
setLanguage(selected);
navigate(`/${slugify(selected.name)}`);
const handleSelect = async (selected: LanguageType) => {
const { language: newLanguage, category: newCategory } =
await configureProfile({
languageName: selected.name,
});

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

Expand Down Expand Up @@ -66,8 +75,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
15 changes: 8 additions & 7 deletions src/components/SearchInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ const SearchInput = () => {

const clearSearch = useCallback(() => {
setInputVal("");
// setCategory(defaultCategory);
setSearchText("");
setSearchParams({});
}, [setSearchParams, setSearchText]);
searchParams.delete("search");
setSearchParams(searchParams);
}, [searchParams, setSearchParams, setSearchText]);

const handleEscapePress = useCallback(
(e: KeyboardEvent) => {
Expand Down Expand Up @@ -61,15 +61,16 @@ const SearchInput = () => {

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

// setCategory(defaultCategory);
setSearchText(formattedVal);
if (!formattedVal) {
setSearchParams({});
searchParams.delete("search");
setSearchParams(searchParams);
} else {
setSearchParams({ search: formattedVal });
searchParams.set("search", formattedVal);
setSearchParams(searchParams);
}
},
[inputVal, setSearchParams, setSearchText]
[inputVal, searchParams, setSearchParams, setSearchText]
);

useEffect(() => {
Expand Down
41 changes: 29 additions & 12 deletions src/components/SnippetList.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,56 @@
import { motion, AnimatePresence, useReducedMotion } from "motion/react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";

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

import { LeftAngleArrowIcon } from "./Icons";
import SnippetModal from "./SnippetModal";

const SnippetList = () => {
const [searchParams, setSearchParams] = useSearchParams();
const shouldReduceMotion = useReducedMotion();

const { language, snippet, setSnippet } = useAppContext();
const { fetchedSnippets } = useSnippets();

const [isModalOpen, setIsModalOpen] = useState<boolean>(false);

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

const handleOpenModal = (activeSnippet: SnippetType) => {
const handleOpenModal = (selected: SnippetType) => () => {
setIsModalOpen(true);
setSnippet(activeSnippet);
setSnippet(selected);
searchParams.set("snippet", slugify(selected.title));
setSearchParams(searchParams);
};

const handleCloseModal = () => {
setIsModalOpen(false);
setSnippet(null);
searchParams.delete("snippet");
setSearchParams(searchParams);
};

/**
* open the relevant modal if the snippet is in the search params
*/
useEffect(() => {
const snippetSlug = searchParams.get("snippet");
if (!snippetSlug) {
return;
}

const selectedSnippet = (fetchedSnippets ?? []).find(
(item) => slugify(item.title) === snippetSlug
);
if (selectedSnippet) {
handleOpenModal(selectedSnippet)();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchedSnippets, searchParams]);

if (!fetchedSnippets) {
return (
<div>
Expand Down Expand Up @@ -77,7 +94,7 @@ const SnippetList = () => {
<motion.button
className="snippet | flow"
data-flow-space="sm"
onClick={() => handleOpenModal(snippet)}
onClick={handleOpenModal(snippet)}
whileHover={{ scale: 1.01 }}
whileTap={{ scale: 0.98 }}
>
Expand Down
64 changes: 14 additions & 50 deletions src/contexts/AppContext.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
import {
createContext,
FC,
useCallback,
useContext,
useEffect,
useState,
} from "react";
import { createContext, FC, useContext, useEffect, useState } from "react";
import { useParams } from "react-router-dom";

import { useLanguages } from "@hooks/useLanguages";
import { AppState, CategoryType, LanguageType, SnippetType } from "@types";
import { defaultLanguage } from "@utils/consts";
import { slugify } from "@utils/slugify";
import { AppState, LanguageType, SnippetType } from "@types";
import { configureProfile } from "@utils/configureProfile";

// TODO: add custom loading and error handling
const defaultState: AppState = {
Expand Down Expand Up @@ -39,50 +31,22 @@ export const AppProvider: FC<{ children: React.ReactNode }> = ({
const [snippet, setSnippet] = useState<SnippetType | null>(null);
const [searchText, setSearchText] = useState<string>("");

const assignLanguage = useCallback(() => {
if (fetchedLanguages.length === 0) {
return;
}
const configure = async () => {
const { language, category } = await configureProfile({
languageName,
categoryName,
});

const language = fetchedLanguages.find(
(lang) => slugify(lang.name) === languageName
);
if (!language) {
setLanguage(defaultLanguage);
return;
}
setLanguage(language);
}, [fetchedLanguages, languageName]);

const assignCategory = useCallback(async () => {
if (!language) {
return;
}

let category: CategoryType | undefined;
try {
const res = await fetch(`/consolidated/${slugify(language.name)}.json`);
const data: CategoryType[] = await res.json();
category = data.find((item) => item.name === categoryName);
if (!category) {
setCategory(data[0].name);
return;
}
setCategory(category.name);
} catch (_error) {
// no-op
}
}, [language, categoryName]);

useEffect(() => {
assignLanguage();
}, [assignLanguage, languageName]);
setCategory(category);
};

useEffect(() => {
assignCategory();
}, [assignCategory, categoryName]);
configure();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [fetchedLanguages]);

if (!language || !category) {
if (language === null || category === null) {
return <div>Loading...</div>;
}

Expand Down
Loading

0 comments on commit 43bcf18

Please sign in to comment.