mirror of
https://github.com/AustinKelsay/plebdevs.git
synced 2025-08-22 12:29:23 +00:00
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
This commit is contained in:
parent
c80722a3c1
commit
85a0756218
@ -1,105 +1,154 @@
|
||||
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';
|
||||
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 { searchCommunity, searchResults: communityResults } =
|
||||
useCommunitySearch();
|
||||
const router = useRouter();
|
||||
const windowWidth = useWindowWidth();
|
||||
const [selectedSearchOption, setSelectedSearchOption] = useState({ name: 'Content', code: 'content', icon: 'pi pi-video' });
|
||||
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' },
|
||||
{ name: "Content", code: "content", icon: "pi pi-video" },
|
||||
{ name: "Community", code: "community", icon: "pi pi-users" },
|
||||
];
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
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>
|
||||
<i className={option.icon + " mr-2"}></i>
|
||||
<span>{option.code}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <i className={option.icon + ' text-transparent text-xs'} />
|
||||
return <i className={option.icon + " text-transparent text-xs"} />;
|
||||
};
|
||||
|
||||
const handleSearch = (e) => {
|
||||
const term = e.target.value;
|
||||
setSearchTerm(term);
|
||||
|
||||
if (selectedSearchOption.code === 'content') {
|
||||
if (selectedSearchOption.code === "content") {
|
||||
searchContent(term);
|
||||
setSearchResults(contentResults);
|
||||
} else if (selectedSearchOption.code === 'community') {
|
||||
} 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') {
|
||||
if (selectedSearchOption.code === "content") {
|
||||
setSearchResults(contentResults);
|
||||
} else if (selectedSearchOption.code === 'community') {
|
||||
} 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') {
|
||||
if (content?.type === "course") {
|
||||
router.push(`/course/${content.id}`);
|
||||
} else {
|
||||
router.push(`/details/${content.id}`);
|
||||
}
|
||||
setSearchTerm('');
|
||||
searchContent('');
|
||||
setSearchTerm("");
|
||||
searchContent("");
|
||||
setIsSearchActive(false);
|
||||
op.current.hide();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`absolute ${windowWidth < 700 ? "left-[40%]" : "left-[50%]"} transform -translate-x-[50%]`}>
|
||||
<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]'}`}
|
||||
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'
|
||||
}
|
||||
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'
|
||||
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: "mx-0 px-0 shadow-lg",
|
||||
},
|
||||
}}
|
||||
className={styles.dropdown}
|
||||
value={selectedSearchOption}
|
||||
@ -108,7 +157,7 @@ const SearchBar = () => {
|
||||
optionLabel="name"
|
||||
placeholder="Search"
|
||||
dropdownIcon={
|
||||
<div className='w-full pr-2 flex flex-row items-center justify-between'>
|
||||
<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>
|
||||
@ -119,9 +168,14 @@ const SearchBar = () => {
|
||||
/>
|
||||
</IconField>
|
||||
|
||||
<OverlayPanel ref={op} className="w-[600px] max-h-[70vh] overflow-y-auto">
|
||||
{searchResults.map((item, index) => (
|
||||
item.type === 'discord' || item.type === 'nostr' || item.type === 'stackernews' ? (
|
||||
<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}
|
||||
@ -134,7 +188,7 @@ const SearchBar = () => {
|
||||
onSelect={handleContentSelect}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
)}
|
||||
{searchResults.length === 0 && searchTerm.length > 2 && (
|
||||
<div className="p-4 text-center">No results found</div>
|
||||
)}
|
||||
|
@ -17,3 +17,23 @@
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
} */
|
||||
|
||||
.dropdown {
|
||||
background: rgba(0, 0, 0, 0.3) !important;
|
||||
backdrop-filter: blur(8px) !important;
|
||||
}
|
||||
|
||||
/* Add this to style the overlay panel */
|
||||
.overlayPanel {
|
||||
background: rgba(0, 0, 0, 0.75) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
.overlayPanelContent {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.blurredContent {
|
||||
filter: blur(8px);
|
||||
transition: filter 0.2s ease-in-out;
|
||||
}
|
||||
|
@ -1,36 +1,36 @@
|
||||
import { PrimeReactProvider } from 'primereact/api';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Navbar from '@/components/navbar/Navbar';
|
||||
import { ToastProvider } from '@/hooks/useToast';
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
import Layout from '@/components/Layout';
|
||||
import '@/styles/globals.css'
|
||||
import 'primereact/resources/themes/lara-dark-blue/theme.css'
|
||||
import '@/styles/custom-theme.css'; // custom theme
|
||||
import 'primereact/resources/primereact.min.css';
|
||||
import 'primeicons/primeicons.css';
|
||||
import { PrimeReactProvider } from "primereact/api";
|
||||
import { useEffect, useState } from "react";
|
||||
import Navbar from "@/components/navbar/Navbar";
|
||||
import { ToastProvider } from "@/hooks/useToast";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import Layout from "@/components/Layout";
|
||||
import "@/styles/globals.css";
|
||||
import "primereact/resources/themes/lara-dark-blue/theme.css";
|
||||
import "@/styles/custom-theme.css"; // custom theme
|
||||
import "primereact/resources/primereact.min.css";
|
||||
import "primeicons/primeicons.css";
|
||||
import "@uiw/react-md-editor/markdown-editor.css";
|
||||
import "@uiw/react-markdown-preview/markdown.css";
|
||||
import Sidebar from '@/components/sidebar/Sidebar';
|
||||
import { useRouter } from 'next/router';
|
||||
import { NDKProvider } from '@/context/NDKContext';
|
||||
import { Analytics } from '@vercel/analytics/react';
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from '@tanstack/react-query'
|
||||
import BottomBar from '@/components/BottomBar';
|
||||
import Sidebar from "@/components/sidebar/Sidebar";
|
||||
import { useRouter } from "next/router";
|
||||
import { NDKProvider } from "@/context/NDKContext";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import BottomBar from "@/components/BottomBar";
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export default function MyApp({
|
||||
Component, pageProps: { session, ...pageProps }
|
||||
Component,
|
||||
pageProps: { session, ...pageProps },
|
||||
}) {
|
||||
const [isCourseView, setIsCourseView] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
setIsCourseView(router.pathname.includes('course') && !router.pathname.includes('draft'));
|
||||
setIsCourseView(
|
||||
router.pathname.includes("course") && !router.pathname.includes("draft")
|
||||
);
|
||||
}, [router.pathname]);
|
||||
|
||||
return (
|
||||
@ -42,9 +42,9 @@ export default function MyApp({
|
||||
<Layout>
|
||||
<div className="flex flex-col min-h-screen">
|
||||
<Navbar />
|
||||
<div className='flex'>
|
||||
<div className="flex">
|
||||
<Sidebar course={isCourseView} />
|
||||
<div className='w-[100vw] pl-[14vw] max-sidebar:pl-0 pb-16 max-sidebar:pb-20'>
|
||||
<div className="main-content w-[100vw] pl-[14vw] max-sidebar:pl-0 pb-16 max-sidebar:pb-20">
|
||||
<Component {...pageProps} />
|
||||
<Analytics />
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user