plebdevs/src/components/search/SearchBar.js
kiwihodl 85a0756218
feat(search): implement backdrop blur and improve UX
- Add background blur effect to main content when search is active
- Implement click-outside behavior to clear search and remove blur
- Add smooth transition for blur effect
- Add main-content class for proper DOM targeting
- Improve search component interaction handling
2025-03-18 11:57:34 -05:00

201 lines
6.2 KiB
JavaScript

import React, { useState, useRef, useEffect } from "react";
import { InputText } from "primereact/inputtext";
import { InputIcon } from "primereact/inputicon";
import { IconField } from "primereact/iconfield";
import { Dropdown } from "primereact/dropdown";
import { OverlayPanel } from "primereact/overlaypanel";
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
import MessageDropdownItem from "@/components/content/dropdowns/MessageDropdownItem";
import { useContentSearch } from "@/hooks/useContentSearch";
import { useCommunitySearch } from "@/hooks/useCommunitySearch";
import { useRouter } from "next/router";
import useWindowWidth from "@/hooks/useWindowWidth";
import styles from "./searchbar.module.css";
const SearchBar = () => {
const { searchContent, searchResults: contentResults } = useContentSearch();
const { searchCommunity, searchResults: communityResults } =
useCommunitySearch();
const router = useRouter();
const windowWidth = useWindowWidth();
const [selectedSearchOption, setSelectedSearchOption] = useState({
name: "Content",
code: "content",
icon: "pi pi-video",
});
const searchOptions = [
{ name: "Content", code: "content", icon: "pi pi-video" },
{ name: "Community", code: "community", icon: "pi pi-users" },
];
const [searchTerm, setSearchTerm] = useState("");
const [searchResults, setSearchResults] = useState([]);
const op = useRef(null);
const [isSearchActive, setIsSearchActive] = useState(false);
const selectedOptionTemplate = (option, props) => {
if (!props?.placeholder) {
return (
<div className="flex items-center">
<i className={option.icon + " mr-2"}></i>
<span>{option.code}</span>
</div>
);
}
return <i className={option.icon + " text-transparent text-xs"} />;
};
const handleSearch = (e) => {
const term = e.target.value;
setSearchTerm(term);
if (selectedSearchOption.code === "content") {
searchContent(term);
setSearchResults(contentResults);
} else if (selectedSearchOption.code === "community") {
searchCommunity(term);
setSearchResults(communityResults);
}
if (term.length > 2) {
setIsSearchActive(true);
op.current.show(e);
} else {
setIsSearchActive(false);
op.current.hide();
}
};
useEffect(() => {
if (selectedSearchOption.code === "content") {
setSearchResults(contentResults);
} else if (selectedSearchOption.code === "community") {
setSearchResults(communityResults);
}
}, [selectedSearchOption, contentResults, communityResults]);
useEffect(() => {
const mainContent = document.querySelector(".main-content");
if (mainContent) {
if (isSearchActive) {
mainContent.classList.add(styles.blurredContent);
} else {
mainContent.classList.remove(styles.blurredContent);
}
}
}, [isSearchActive]);
useEffect(() => {
const handleClickOutside = (event) => {
const overlayElement = document.querySelector(".p-overlaypanel");
const searchElement = event.target.closest(".search-container");
if (
(!overlayElement || !overlayElement.contains(event.target)) &&
!searchElement
) {
setSearchTerm("");
setIsSearchActive(false);
setSearchResults([]);
op.current?.hide();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
const handleContentSelect = (content) => {
if (content?.type === "course") {
router.push(`/course/${content.id}`);
} else {
router.push(`/details/${content.id}`);
}
setSearchTerm("");
searchContent("");
setIsSearchActive(false);
op.current.hide();
};
return (
<div
className={`search-container absolute ${
windowWidth < 700 ? "left-[40%]" : "left-[50%]"
} transform -translate-x-[50%]`}
>
<IconField iconPosition="left">
<InputIcon className="pi pi-search"> </InputIcon>
<InputText
className={`${windowWidth > 845 ? "w-[300px]" : "w-[160px]"}`}
value={searchTerm}
onChange={handleSearch}
placeholder={`Search ${selectedSearchOption.name.toLowerCase()}`}
pt={{
root: {
className:
"border-none rounded-tr-none rounded-br-none focus:border-none focus:ring-0 pr-0",
},
}}
/>
<Dropdown
pt={{
root: {
className:
"border-none rounded-tl-none rounded-bl-none bg-gray-900/55 hover:bg-gray-900/30",
},
input: {
className: "mx-0 px-0 shadow-lg",
},
}}
className={styles.dropdown}
value={selectedSearchOption}
onChange={(e) => setSelectedSearchOption(e.value)}
options={searchOptions}
optionLabel="name"
placeholder="Search"
dropdownIcon={
<div className="w-full pr-2 flex flex-row items-center justify-between">
<i className={selectedSearchOption.icon + " text-white"} />
<i className="pi pi-chevron-down" />
</div>
}
valueTemplate={selectedOptionTemplate}
itemTemplate={selectedOptionTemplate}
required
/>
</IconField>
<OverlayPanel
ref={op}
className={`w-[600px] max-h-[70vh] overflow-y-auto ${styles.overlayPanel}`}
>
{searchResults.map((item, index) =>
item.type === "discord" ||
item.type === "nostr" ||
item.type === "stackernews" ? (
<MessageDropdownItem
key={index}
message={item}
onSelect={handleContentSelect}
/>
) : (
<ContentDropdownItem
key={index}
content={item}
onSelect={handleContentSelect}
/>
)
)}
{searchResults.length === 0 && searchTerm.length > 2 && (
<div className="p-4 text-center">No results found</div>
)}
</OverlayPanel>
</div>
);
};
export default SearchBar;