- {content?.title || content?.name}
+ {matches.title
+ ? highlightText(
+ content?.title || content?.name,
+ matches.title.term,
+ 'bg-yellow-500/30 text-white font-bold px-0.5 rounded'
+ )
+ : (content?.title || content?.name)}
{content?.price > 0 ? (
@@ -59,7 +69,13 @@ const ContentDropdownItem = ({ content, onSelect }) => {
{content?.summary && (
- {content.summary}
+ {matches.description
+ ? highlightText(
+ getTextWithMatchContext(content.summary, matches.description.term, 60),
+ matches.description.term,
+ 'bg-yellow-500/30 text-white font-medium px-0.5 rounded'
+ )
+ : content.summary}
)}
diff --git a/src/components/content/dropdowns/MessageDropdownItem.js b/src/components/content/dropdowns/MessageDropdownItem.js
index ede3407..49b38d3 100644
--- a/src/components/content/dropdowns/MessageDropdownItem.js
+++ b/src/components/content/dropdowns/MessageDropdownItem.js
@@ -8,6 +8,7 @@ import Image from 'next/image';
import { formatTimestampToHowLongAgo } from '@/utils/time';
import NostrIcon from '/public/images/nostr.png';
import { useImageProxy } from '@/hooks/useImageProxy';
+import { highlightText, getTextWithMatchContext } from '@/utils/text';
const MessageDropdownItem = ({ message, onSelect }) => {
const { ndk } = useNDKContext();
@@ -20,6 +21,9 @@ const MessageDropdownItem = ({ message, onSelect }) => {
// Stable reference to message to prevent infinite loops
const messageRef = useMemo(() => message, [message?.id]);
+
+ // Get match information
+ const matches = useMemo(() => message?._matches || {}, [message]);
// Determine the platform once when component mounts or message changes
const determinePlatform = useCallback(() => {
@@ -85,6 +89,7 @@ const MessageDropdownItem = ({ message, onSelect }) => {
avatarProxy: authorPicture ? returnImageProxy(authorPicture) : null,
type: 'nostr',
id: messageRef.id,
+ _matches: messageRef._matches
});
}
} else if (currentPlatform === 'discord') {
@@ -104,6 +109,7 @@ const MessageDropdownItem = ({ message, onSelect }) => {
channel: messageRef?.channel || 'discord',
type: 'discord',
id: messageRef.id,
+ _matches: messageRef._matches
});
}
} else if (currentPlatform === 'stackernews') {
@@ -122,6 +128,7 @@ const MessageDropdownItem = ({ message, onSelect }) => {
channel: '~devs',
type: 'stackernews',
id: messageRef.id,
+ _matches: messageRef._matches
});
}
}
@@ -164,6 +171,16 @@ const MessageDropdownItem = ({ message, onSelect }) => {
const messageDate = messageData.timestamp
? formatTimestampToHowLongAgo(messageData.timestamp)
: '';
+
+ // Get the content with highlighting if there's a match
+ const contentMatches = messageData._matches?.content || messageData._matches?.title;
+ const displayContent = contentMatches
+ ? highlightText(
+ getTextWithMatchContext(messageData.content, contentMatches.term, 60),
+ contentMatches.term,
+ 'bg-yellow-500/30 text-white font-medium px-0.5 rounded'
+ )
+ : messageData.content;
return (
@@ -190,7 +207,7 @@ const MessageDropdownItem = ({ message, onSelect }) => {
{messageDate}
- {messageData.content}
+ {displayContent}
@@ -254,8 +271,19 @@ const MessageDropdownItem = ({ message, onSelect }) => {
{messageData?.timestamp ? formatTimestampToHowLongAgo(messageData.timestamp) : ''}
- {messageData?.content}
+ {messageData?._matches?.content || messageData?._matches?.title
+ ? highlightText(
+ getTextWithMatchContext(
+ messageData?.content,
+ (messageData?._matches?.content || messageData?._matches?.title).term,
+ 80
+ ),
+ (messageData?._matches?.content || messageData?._matches?.title).term,
+ 'bg-yellow-500/30 text-white font-medium px-0.5 rounded'
+ )
+ : messageData?.content}
diff --git a/src/components/search/SearchBar.js b/src/components/search/SearchBar.js
index 82e5f2e..e27bbab 100644
--- a/src/components/search/SearchBar.js
+++ b/src/components/search/SearchBar.js
@@ -1,4 +1,4 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useState, useRef, useEffect, useMemo } from 'react';
import { InputText } from 'primereact/inputtext';
import { Dropdown } from 'primereact/dropdown';
import { OverlayPanel } from 'primereact/overlaypanel';
@@ -11,20 +11,20 @@ import useWindowWidth from '@/hooks/useWindowWidth';
import { useNDKContext } from '@/context/NDKContext';
import { ProgressSpinner } from 'primereact/progressspinner';
+const SEARCH_OPTIONS = [
+ { name: 'Content', code: 'content', icon: 'pi pi-video' },
+ { name: 'Community', code: 'community', icon: 'pi pi-users' },
+];
+
+const SEARCH_DELAY = 300; // ms
+const MIN_SEARCH_LENGTH = 3;
+
const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
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 [selectedSearchOption, setSelectedSearchOption] = useState(SEARCH_OPTIONS[0]);
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
@@ -32,6 +32,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
const { ndk, reInitializeNDK } = useNDKContext();
const searchTimeout = useRef(null);
+ // Handle search option template rendering
const selectedOptionTemplate = (option, props) => {
if (isDesktopNav) {
// For desktop nav bar, just show the icon
@@ -53,6 +54,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
return
;
};
+ // Handle search input changes
const handleSearch = e => {
const term = e.target.value;
setSearchTerm(term);
@@ -63,13 +65,13 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
}
// Set loading state if term length is sufficient
- if (term.length > 2) {
+ if (term.length >= MIN_SEARCH_LENGTH) {
setIsSearching(true);
}
// Set a timeout to avoid searching on each keystroke
searchTimeout.current = setTimeout(() => {
- if (term.length > 2) {
+ if (term.length >= MIN_SEARCH_LENGTH) {
if (selectedSearchOption.code === 'content') {
searchContent(term);
} else if (selectedSearchOption.code === 'community' && ndk) {
@@ -78,15 +80,17 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
} else {
setIsSearching(false);
}
- }, 300);
+ }, SEARCH_DELAY);
- if (!isMobileSearch && term.length > 2) {
+ // Show/hide overlay panel based on search term length
+ if (!isMobileSearch && term.length >= MIN_SEARCH_LENGTH) {
op.current.show(e);
} else if (!isMobileSearch) {
op.current.hide();
}
};
+ // Update search results when option or results change
useEffect(() => {
if (selectedSearchOption.code === 'content') {
setSearchResults(contentResults);
@@ -95,7 +99,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
}
// Once we have results, set isSearching to false
- if (searchTerm.length > 2) {
+ if (searchTerm.length >= MIN_SEARCH_LENGTH) {
setIsSearching(false);
}
}, [selectedSearchOption, contentResults, communityResults, searchTerm]);
@@ -109,6 +113,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
};
}, []);
+ // Handle WebSocket errors and reinitialize NDK if needed
useEffect(() => {
const handleError = event => {
if (event.message && event.message.includes('wss://relay.devs.tools')) {
@@ -124,6 +129,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
};
}, [reInitializeNDK]);
+ // Handle item selection from search results
const handleContentSelect = content => {
if (selectedSearchOption.code === 'content') {
if (content?.type === 'course') {
@@ -143,6 +149,12 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
}
}
+ // Reset search state
+ resetSearch();
+ };
+
+ // Reset search state
+ const resetSearch = () => {
setSearchTerm('');
searchContent('');
searchCommunity('');
@@ -166,6 +178,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
}
};
+ // Render search results
const renderSearchResults = () => {
// Show loading spinner while searching
if (isSearching) {
@@ -177,10 +190,11 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
}
// Show no results message
- if (searchResults.length === 0 && searchTerm.length > 2) {
+ if (searchResults.length === 0 && searchTerm.length >= MIN_SEARCH_LENGTH) {
return
No results found
;
}
+ // Render appropriate item component based on type
return searchResults.map((item, index) =>
item.type === 'discord' || item.type === 'nostr' || item.type === 'stackernews' ? (
@@ -195,7 +209,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
setSelectedSearchOption(e.value);
// If there's a search term, run the search again with the new option
- if (searchTerm.length > 2) {
+ if (searchTerm.length >= MIN_SEARCH_LENGTH) {
setIsSearching(true);
if (e.value.code === 'content') {
searchContent(searchTerm);
@@ -205,6 +219,13 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
}
};
+ // Derived styles based on screen size
+ const searchWidth = useMemo(() => {
+ if (windowWidth > 845) return 'w-[300px]';
+ if (isMobileSearch || windowWidth <= 600) return 'w-full';
+ return 'w-[160px]';
+ }, [windowWidth, isMobileSearch]);
+
return (
<>
@@ -244,7 +265,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
}}
value={selectedSearchOption}
onChange={handleSearchOptionChange}
- options={searchOptions}
+ options={SEARCH_OPTIONS}
optionLabel="name"
dropdownIcon={
}
valueTemplate={selectedOptionTemplate}
@@ -266,9 +287,9 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
845 ? 'w-[300px]' : isMobileSearch || windowWidth <= 600 ? 'w-full' : 'w-[160px]'}
- ${isMobileSearch ? 'bg-transparent border-none pl-12 text-lg' : ''}
- `}
+ ${searchWidth}
+ ${isMobileSearch ? 'bg-transparent border-none pl-12 text-lg' : ''}
+ `}
value={searchTerm}
onChange={handleSearch}
placeholder={`Search ${selectedSearchOption.name.toLowerCase()}`}
@@ -282,7 +303,7 @@ const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
{isMobileSearch && (
- {searchOptions.map(option => (
+ {SEARCH_OPTIONS.map(option => (