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,146 +1,200 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import { InputText } from 'primereact/inputtext';
|
import { InputText } from "primereact/inputtext";
|
||||||
import { InputIcon } from 'primereact/inputicon';
|
import { InputIcon } from "primereact/inputicon";
|
||||||
import { IconField } from 'primereact/iconfield';
|
import { IconField } from "primereact/iconfield";
|
||||||
import { Dropdown } from 'primereact/dropdown';
|
import { Dropdown } from "primereact/dropdown";
|
||||||
import { OverlayPanel } from 'primereact/overlaypanel';
|
import { OverlayPanel } from "primereact/overlaypanel";
|
||||||
import ContentDropdownItem from '@/components/content/dropdowns/ContentDropdownItem';
|
import ContentDropdownItem from "@/components/content/dropdowns/ContentDropdownItem";
|
||||||
import MessageDropdownItem from '@/components/content/dropdowns/MessageDropdownItem';
|
import MessageDropdownItem from "@/components/content/dropdowns/MessageDropdownItem";
|
||||||
import { useContentSearch } from '@/hooks/useContentSearch';
|
import { useContentSearch } from "@/hooks/useContentSearch";
|
||||||
import { useCommunitySearch } from '@/hooks/useCommunitySearch';
|
import { useCommunitySearch } from "@/hooks/useCommunitySearch";
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from "next/router";
|
||||||
import useWindowWidth from '@/hooks/useWindowWidth';
|
import useWindowWidth from "@/hooks/useWindowWidth";
|
||||||
import styles from './searchbar.module.css';
|
import styles from "./searchbar.module.css";
|
||||||
|
|
||||||
const SearchBar = () => {
|
const SearchBar = () => {
|
||||||
const { searchContent, searchResults: contentResults } = useContentSearch();
|
const { searchContent, searchResults: contentResults } = useContentSearch();
|
||||||
const { searchCommunity, searchResults: communityResults } = useCommunitySearch();
|
const { searchCommunity, searchResults: communityResults } =
|
||||||
const router = useRouter();
|
useCommunitySearch();
|
||||||
const windowWidth = useWindowWidth();
|
const router = useRouter();
|
||||||
const [selectedSearchOption, setSelectedSearchOption] = useState({ name: 'Content', code: 'content', icon: 'pi pi-video' });
|
const windowWidth = useWindowWidth();
|
||||||
const searchOptions = [
|
const [selectedSearchOption, setSelectedSearchOption] = useState({
|
||||||
{ name: 'Content', code: 'content', icon: 'pi pi-video' },
|
name: "Content",
|
||||||
{ name: 'Community', code: 'community', icon: 'pi pi-users' },
|
code: "content",
|
||||||
];
|
icon: "pi pi-video",
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
});
|
||||||
const [searchResults, setSearchResults] = useState([]);
|
const searchOptions = [
|
||||||
const op = useRef(null);
|
{ 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) => {
|
const selectedOptionTemplate = (option, props) => {
|
||||||
if (!props?.placeholder) {
|
if (!props?.placeholder) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<i className={option.icon + ' mr-2'}></i>
|
<i className={option.icon + " mr-2"}></i>
|
||||||
<span>{option.code}</span>
|
<span>{option.code}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <i className={option.icon + ' text-transparent text-xs'} />
|
return <i className={option.icon + " text-transparent text-xs"} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (e) => {
|
const handleSearch = (e) => {
|
||||||
const term = e.target.value;
|
const term = e.target.value;
|
||||||
setSearchTerm(term);
|
setSearchTerm(term);
|
||||||
|
|
||||||
if (selectedSearchOption.code === 'content') {
|
|
||||||
searchContent(term);
|
|
||||||
setSearchResults(contentResults);
|
|
||||||
} else if (selectedSearchOption.code === 'community') {
|
|
||||||
searchCommunity(term);
|
|
||||||
setSearchResults(communityResults);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (term.length > 2) {
|
if (selectedSearchOption.code === "content") {
|
||||||
op.current.show(e);
|
searchContent(term);
|
||||||
} else {
|
setSearchResults(contentResults);
|
||||||
op.current.hide();
|
} else if (selectedSearchOption.code === "community") {
|
||||||
}
|
searchCommunity(term);
|
||||||
};
|
setSearchResults(communityResults);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedSearchOption.code === 'content') {
|
|
||||||
setSearchResults(contentResults);
|
|
||||||
} else if (selectedSearchOption.code === 'community') {
|
|
||||||
setSearchResults(communityResults);
|
|
||||||
}
|
|
||||||
}, [selectedSearchOption, contentResults, communityResults]);
|
|
||||||
|
|
||||||
const handleContentSelect = (content) => {
|
|
||||||
if (content?.type === 'course') {
|
|
||||||
router.push(`/course/${content.id}`);
|
|
||||||
} else {
|
|
||||||
router.push(`/details/${content.id}`);
|
|
||||||
}
|
|
||||||
setSearchTerm('');
|
|
||||||
searchContent('');
|
|
||||||
op.current.hide();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (term.length > 2) {
|
||||||
<div className={`absolute ${windowWidth < 700 ? "left-[40%]" : "left-[50%]"} transform -translate-x-[50%]`}>
|
setIsSearchActive(true);
|
||||||
<IconField iconPosition="left">
|
op.current.show(e);
|
||||||
<InputIcon className="pi pi-search"> </InputIcon>
|
} else {
|
||||||
<InputText
|
setIsSearchActive(false);
|
||||||
className={`${windowWidth > 845 ? 'w-[300px]' : 'w-[160px]'}`}
|
op.current.hide();
|
||||||
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
|
useEffect(() => {
|
||||||
pt={{
|
if (selectedSearchOption.code === "content") {
|
||||||
root: {
|
setSearchResults(contentResults);
|
||||||
className: 'border-none rounded-tl-none rounded-bl-none bg-gray-900/55 hover:bg-gray-900/30'
|
} else if (selectedSearchOption.code === "community") {
|
||||||
},
|
setSearchResults(communityResults);
|
||||||
input: {
|
}
|
||||||
className: 'mx-0 px-0 shadow-lg'
|
}, [selectedSearchOption, contentResults, communityResults]);
|
||||||
}
|
|
||||||
}}
|
|
||||||
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">
|
useEffect(() => {
|
||||||
{searchResults.map((item, index) => (
|
const mainContent = document.querySelector(".main-content");
|
||||||
item.type === 'discord' || item.type === 'nostr' || item.type === 'stackernews' ? (
|
if (mainContent) {
|
||||||
<MessageDropdownItem
|
if (isSearchActive) {
|
||||||
key={index}
|
mainContent.classList.add(styles.blurredContent);
|
||||||
message={item}
|
} else {
|
||||||
onSelect={handleContentSelect}
|
mainContent.classList.remove(styles.blurredContent);
|
||||||
/>
|
}
|
||||||
) : (
|
}
|
||||||
<ContentDropdownItem
|
}, [isSearchActive]);
|
||||||
key={index}
|
|
||||||
content={item}
|
useEffect(() => {
|
||||||
onSelect={handleContentSelect}
|
const handleClickOutside = (event) => {
|
||||||
/>
|
const overlayElement = document.querySelector(".p-overlaypanel");
|
||||||
)
|
const searchElement = event.target.closest(".search-container");
|
||||||
))}
|
|
||||||
{searchResults.length === 0 && searchTerm.length > 2 && (
|
if (
|
||||||
<div className="p-4 text-center">No results found</div>
|
(!overlayElement || !overlayElement.contains(event.target)) &&
|
||||||
)}
|
!searchElement
|
||||||
</OverlayPanel>
|
) {
|
||||||
</div>
|
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;
|
export default SearchBar;
|
||||||
|
@ -17,3 +17,23 @@
|
|||||||
outline: none !important;
|
outline: none !important;
|
||||||
box-shadow: 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,61 +1,61 @@
|
|||||||
import { PrimeReactProvider } from 'primereact/api';
|
import { PrimeReactProvider } from "primereact/api";
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import Navbar from '@/components/navbar/Navbar';
|
import Navbar from "@/components/navbar/Navbar";
|
||||||
import { ToastProvider } from '@/hooks/useToast';
|
import { ToastProvider } from "@/hooks/useToast";
|
||||||
import { SessionProvider } from "next-auth/react"
|
import { SessionProvider } from "next-auth/react";
|
||||||
import Layout from '@/components/Layout';
|
import Layout from "@/components/Layout";
|
||||||
import '@/styles/globals.css'
|
import "@/styles/globals.css";
|
||||||
import 'primereact/resources/themes/lara-dark-blue/theme.css'
|
import "primereact/resources/themes/lara-dark-blue/theme.css";
|
||||||
import '@/styles/custom-theme.css'; // custom theme
|
import "@/styles/custom-theme.css"; // custom theme
|
||||||
import 'primereact/resources/primereact.min.css';
|
import "primereact/resources/primereact.min.css";
|
||||||
import 'primeicons/primeicons.css';
|
import "primeicons/primeicons.css";
|
||||||
import "@uiw/react-md-editor/markdown-editor.css";
|
import "@uiw/react-md-editor/markdown-editor.css";
|
||||||
import "@uiw/react-markdown-preview/markdown.css";
|
import "@uiw/react-markdown-preview/markdown.css";
|
||||||
import Sidebar from '@/components/sidebar/Sidebar';
|
import Sidebar from "@/components/sidebar/Sidebar";
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from "next/router";
|
||||||
import { NDKProvider } from '@/context/NDKContext';
|
import { NDKProvider } from "@/context/NDKContext";
|
||||||
import { Analytics } from '@vercel/analytics/react';
|
import { Analytics } from "@vercel/analytics/react";
|
||||||
import {
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
QueryClient,
|
import BottomBar from "@/components/BottomBar";
|
||||||
QueryClientProvider,
|
|
||||||
} from '@tanstack/react-query'
|
|
||||||
import BottomBar from '@/components/BottomBar';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient()
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export default function MyApp({
|
export default function MyApp({
|
||||||
Component, pageProps: { session, ...pageProps }
|
Component,
|
||||||
|
pageProps: { session, ...pageProps },
|
||||||
}) {
|
}) {
|
||||||
const [isCourseView, setIsCourseView] = useState(false);
|
const [isCourseView, setIsCourseView] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsCourseView(router.pathname.includes('course') && !router.pathname.includes('draft'));
|
setIsCourseView(
|
||||||
}, [router.pathname]);
|
router.pathname.includes("course") && !router.pathname.includes("draft")
|
||||||
|
|
||||||
return (
|
|
||||||
<PrimeReactProvider>
|
|
||||||
<SessionProvider session={session}>
|
|
||||||
<NDKProvider>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<ToastProvider>
|
|
||||||
<Layout>
|
|
||||||
<div className="flex flex-col min-h-screen">
|
|
||||||
<Navbar />
|
|
||||||
<div className='flex'>
|
|
||||||
<Sidebar course={isCourseView} />
|
|
||||||
<div className='w-[100vw] pl-[14vw] max-sidebar:pl-0 pb-16 max-sidebar:pb-20'>
|
|
||||||
<Component {...pageProps} />
|
|
||||||
<Analytics />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<BottomBar />
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</ToastProvider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
</NDKProvider>
|
|
||||||
</SessionProvider>
|
|
||||||
</PrimeReactProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}, [router.pathname]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrimeReactProvider>
|
||||||
|
<SessionProvider session={session}>
|
||||||
|
<NDKProvider>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ToastProvider>
|
||||||
|
<Layout>
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<div className="flex">
|
||||||
|
<Sidebar course={isCourseView} />
|
||||||
|
<div className="main-content w-[100vw] pl-[14vw] max-sidebar:pl-0 pb-16 max-sidebar:pb-20">
|
||||||
|
<Component {...pageProps} />
|
||||||
|
<Analytics />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<BottomBar />
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</ToastProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</NDKProvider>
|
||||||
|
</SessionProvider>
|
||||||
|
</PrimeReactProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user