Fixed search experience for desktop and mobile, replaced hamburber with search icon on mobile

This commit is contained in:
austinkelsay 2025-03-27 13:23:39 -05:00
parent d3092f56af
commit ccaca7b23e
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
5 changed files with 608 additions and 209 deletions

View File

@ -1,47 +1,81 @@
import React, {useEffect} from "react";
import React from "react";
import Image from "next/image";
import { useImageProxy } from "@/hooks/useImageProxy";
import { formatUnixTimestamp } from "@/utils/time";
import { Tag } from "primereact/tag";
import { Message } from "primereact/message";
import GenericButton from "@/components/buttons/GenericButton";
import useWindowWidth from "@/hooks/useWindowWidth";
import { BookOpen } from "lucide-react";
const ContentDropdownItem = ({ content, onSelect }) => {
const { returnImageProxy } = useImageProxy();
const windowWidth = useWindowWidth();
const isMobile = windowWidth <= 600;
return (
<div className="w-full border-t-2 border-gray-700 py-4">
<div className="flex flex-row gap-4 p-2">
<Image
alt="content thumbnail"
src={returnImageProxy(content?.image)}
width={50}
height={50}
className="w-[100px] h-[100px] object-cover object-center border-round"
/>
<div className="flex-1 max-w-[80vw]">
<div className="text-lg text-900 font-bold">{content?.title || content?.name}</div>
<div className="w-full text-sm text-600 text-wrap line-clamp-2">{content?.summary || content?.description && (
<div className="text-xl mt-4">
{content?.summary?.split('\n').map((line, index) => (
<p key={index}>{line}</p>
))}
</div>
)}
</div>
{content?.price && <div className="text-sm pt-6 text-gray-500">Price: {content.price}</div>}
{content?.topics?.length > 0 && (
<div className="text-sm pt-6 text-gray-500">
{content.topics.map((topic) => (
<Tag key={topic} value={topic} size="small" className="mr-2 text-[#f8f8ff]" />
))}
</div>
)}
<div className="text-sm pt-6 text-gray-500">
{(content?.published_at || content?.created_at) ? `Published: ${formatUnixTimestamp(content?.published_at || content?.created_at)}` : "not yet published"}
<div
className="px-6 py-6 border-b border-gray-700 cursor-pointer"
onClick={() => onSelect(content)}
>
<div className={`flex ${isMobile ? 'flex-col' : 'flex-row'} gap-4`}>
<div className={`relative ${isMobile ? 'w-full h-40' : 'w-[160px] h-[90px]'} flex-shrink-0 overflow-hidden rounded-md`}>
<Image
alt="content thumbnail"
src={returnImageProxy(content?.image)}
width={isMobile ? 600 : 160}
height={isMobile ? 240 : 90}
className="w-full h-full object-cover object-center"
/>
<div className="absolute inset-0 bg-gradient-to-br from-primary/80 to-primary-foreground/50 opacity-60" />
<div className="absolute bottom-2 left-2 flex gap-2">
<BookOpen className="w-5 h-5 text-white" />
</div>
</div>
<div className="flex flex-col justify-end">
<GenericButton outlined size="small" label="Select" onClick={() => onSelect(content)} className="py-2" />
<div className="flex-1">
<div className="flex justify-between items-center mb-2">
<h3 className="text-xl font-bold text-[#f8f8ff]">{content?.title || content?.name}</h3>
{content?.price > 0 ? (
<Message severity="info" text={`${content.price} sats`} className="py-1 text-xs whitespace-nowrap" />
) : (
<Message severity="success" text="Free" className="py-1 text-xs whitespace-nowrap" />
)}
</div>
{content?.summary && (
<p className="text-neutral-50/90 line-clamp-2 mb-3 text-sm">{content.summary}</p>
)}
<div className="flex flex-wrap gap-2 mt-2">
{content?.topics?.map((topic) => (
<Tag key={topic} value={topic} className="px-2 py-1 text-sm text-[#f8f8ff]" />
))}
</div>
<div className="flex justify-between items-center mt-3">
<div className="text-sm text-gray-300">
{(content?.published_at || content?.created_at)
? `Published: ${formatUnixTimestamp(content?.published_at || content?.created_at)}`
: "Not yet published"}
</div>
{!isMobile && (
<GenericButton
outlined
size="small"
label="Start Learning"
icon="pi pi-chevron-right"
iconPos="right"
onClick={(e) => {
e.stopPropagation();
onSelect(content);
}}
className="items-center py-1"
/>
)}
</div>
</div>
</div>
</div>

View File

@ -1,87 +1,231 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import CommunityMessage from "@/components/feeds/messages/CommunityMessage";
import { parseMessageEvent, findKind0Fields } from "@/utils/nostr";
import { parseMessageEvent } from "@/utils/nostr";
import { ProgressSpinner } from 'primereact/progressspinner';
import { useNDKContext } from "@/context/NDKContext";
import useWindowWidth from "@/hooks/useWindowWidth";
import Image from "next/image";
import { formatTimestampToHowLongAgo } from "@/utils/time";
import NostrIcon from '/public/images/nostr.png';
import { useImageProxy } from "@/hooks/useImageProxy";
const MessageDropdownItem = ({ message, onSelect }) => {
const { ndk, addSigner } = useNDKContext();
const [author, setAuthor] = useState(null);
const [formattedMessage, setFormattedMessage] = useState(null);
const [messageWithAuthor, setMessageWithAuthor] = useState(null);
const { ndk } = useNDKContext();
const [messageData, setMessageData] = useState(null);
const [loading, setLoading] = useState(true);
const [platform, setPlatform] = useState(null);
const windowWidth = useWindowWidth();
const isMobile = windowWidth <= 600;
const { returnImageProxy } = useImageProxy();
const determinePlatform = () => {
if (message?.channel) {
return "discord";
} else if (message?.kind) {
return "nostr";
} else {
return "stackernews";
// Stable reference to message to prevent infinite loops
const messageRef = useMemo(() => message, [message?.id]);
// Determine the platform once when component mounts or message changes
const determinePlatform = useCallback(() => {
if (messageRef?.channel) return "discord";
if (messageRef?.kind) return "nostr";
return "stackernews";
}, [messageRef]);
// Memoize the fetchNostrAuthor function
const fetchNostrAuthor = useCallback(async (pubkey) => {
if (!ndk || !pubkey) return null;
try {
await ndk.connect();
const user = await ndk.getUser({ pubkey });
const profile = await user.fetchProfile();
// Return the parsed profile data directly - it already contains what we need
return profile;
} catch (error) {
console.error("Error fetching Nostr author:", error);
return null;
}
}
}, [ndk]);
// Process message based on platform type
useEffect(() => {
setPlatform(determinePlatform(message));
}, [message]);
// Prevent execution if no message data or already loaded
if (!messageRef || messageData) return;
useEffect(() => {
if (platform === "nostr") {
const event = parseMessageEvent(message);
setFormattedMessage(event);
const getAuthor = async () => {
try {
await ndk.connect();
const author = await ndk.getUser({ pubkey: message.pubkey });
if (author && author?.content) {
const authorFields = findKind0Fields(JSON.parse(author.content));
if (authorFields) {
setAuthor(authorFields);
}
} else if (author?._pubkey) {
setAuthor(author?._pubkey);
const currentPlatform = determinePlatform();
setPlatform(currentPlatform);
let isMounted = true;
const processMessage = async () => {
try {
if (currentPlatform === "nostr") {
// Format Nostr message
const parsedMessage = parseMessageEvent(messageRef);
// Fetch author data for Nostr messages
let authorData = null;
if (messageRef?.pubkey) {
authorData = await fetchNostrAuthor(messageRef.pubkey);
}
} catch (error) {
console.error(error);
// Extract author details with fallbacks - profile already contains the formatted data
const authorName = authorData?.name || authorData?.displayName || "Unknown User";
const authorPicture = authorData?.picture || authorData?.image || null;
// Only update state if component is still mounted
if (isMounted) {
setMessageData({
...parsedMessage,
timestamp: messageRef.created_at || Math.floor(Date.now() / 1000),
channel: "plebdevs",
author: authorName,
avatar: authorPicture,
avatarProxy: authorPicture ? returnImageProxy(authorPicture) : null,
type: "nostr",
id: messageRef.id
});
}
} else if (currentPlatform === "discord") {
// Format Discord message
const avatarUrl = messageRef?.author?.avatar
? `https://cdn.discordapp.com/avatars/${messageRef.author.id}/${messageRef.author.avatar}.png`
: null;
if (isMounted) {
setMessageData({
content: messageRef?.content,
author: messageRef?.author?.username || "Unknown User",
timestamp: messageRef?.timestamp ? Math.floor(messageRef.timestamp / 1000) : Math.floor(Date.now() / 1000),
avatar: avatarUrl,
avatarProxy: avatarUrl ? returnImageProxy(avatarUrl) : null,
channel: messageRef?.channel || "discord",
type: "discord",
id: messageRef.id
});
}
} else if (currentPlatform === "stackernews") {
// Format StackerNews message
if (isMounted) {
setMessageData({
content: messageRef?.title,
author: messageRef?.user?.name || "Unknown User",
timestamp: messageRef?.created_at ? Math.floor(Date.parse(messageRef.created_at) / 1000) : Math.floor(Date.now() / 1000),
avatar: "https://pbs.twimg.com/profile_images/1403162883941359619/oca7LMQ2_400x400.png",
avatarProxy: returnImageProxy("https://pbs.twimg.com/profile_images/1403162883941359619/oca7LMQ2_400x400.png"),
channel: "~devs",
type: "stackernews",
id: messageRef.id
});
}
}
} catch (error) {
console.error("Error processing message:", error);
} finally {
if (isMounted) {
setLoading(false);
}
}
getAuthor();
} else if (platform === "stackernews") {
setMessageWithAuthor({
content: message?.title,
author: message?.user?.name,
timestamp: message?.created_at ? Date.parse(message.created_at) : null,
avatar: "https://pbs.twimg.com/profile_images/1403162883941359619/oca7LMQ2_400x400.png",
channel: "~devs",
...message
})
}
else {
setLoading(false);
}
}, [ndk, message, platform]);
};
processMessage();
// Cleanup function to prevent state updates after unmount
return () => {
isMounted = false;
};
}, [messageRef, determinePlatform, fetchNostrAuthor, returnImageProxy, messageData]);
useEffect(() => {
if (author && formattedMessage && platform === 'nostr') {
const body = {
...formattedMessage,
timestamp: formattedMessage.created_at || message.created_at || message.timestamp,
channel: "plebdevs",
author: author
};
setMessageWithAuthor(body);
setLoading(false);
const getPlatformIcon = useCallback(() => {
switch (platform) {
case 'nostr':
return <Image src={NostrIcon} alt="Nostr" width={16} height={16} className="mr-1" />;
case 'discord':
return <i className="pi pi-discord mr-1" />;
case 'stackernews':
return <i className="pi pi-bolt mr-1" />;
default:
return <i className="pi pi-globe mr-1" />;
}
}, [author, formattedMessage, message, platform]);
}, [platform]);
// Create a simplified view for mobile search results
const renderSimplifiedMessage = useCallback(() => {
if (!messageData) return null;
const authorName = messageData.author || "Unknown User";
const avatarUrl = messageData.avatarProxy || returnImageProxy(messageData.avatar);
const messageDate = messageData.timestamp ? formatTimestampToHowLongAgo(messageData.timestamp) : '';
return (
<div className="flex flex-col">
<div className="flex gap-3">
<div className="w-10 h-10 rounded-full overflow-hidden bg-gray-700 flex-shrink-0 mt-1">
{avatarUrl ? (
<Image
src={avatarUrl}
alt="avatar"
width={40}
height={40}
className="object-cover w-full h-full"
/>
) : (
<div className="w-full h-full flex items-center justify-center">
<i className="pi pi-user text-gray-400 text-xl" />
</div>
)}
</div>
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<div className="font-medium text-[#f8f8ff]">
{authorName}
</div>
<div className="text-xs text-gray-400">
{messageDate}
</div>
</div>
<p className="text-neutral-50/90 whitespace-pre-wrap mb-3 line-clamp-3">{messageData.content}</p>
<div className="flex flex-wrap gap-2">
<div className="px-3 py-1 bg-blue-500 text-white rounded-full text-sm flex items-center">
{getPlatformIcon()}
{platform}
</div>
<div className="px-3 py-1 bg-gray-700 text-white rounded-full text-sm">
plebdevs
</div>
</div>
</div>
</div>
</div>
);
}, [messageData, returnImageProxy, getPlatformIcon, platform]);
// Memoize the final message object to pass to CommunityMessage
const finalMessage = useMemo(() => {
if (!messageData) return null;
return {
...messageData,
avatar: messageData?.avatarProxy || returnImageProxy(messageData?.avatar)
};
}, [messageData, returnImageProxy]);
return (
<div className="w-full border-t-2 border-gray-700 py-4">
<div
className="px-6 py-6 border-b border-gray-700 cursor-pointer"
onClick={() => !loading && onSelect(messageData || messageRef)}
>
{loading ? (
<div className='w-full h-full flex items-center justify-center'><ProgressSpinner /></div>
<div className='w-full h-[100px] flex items-center justify-center'>
<ProgressSpinner style={{ width: '40px', height: '40px' }} strokeWidth="4" />
</div>
) : (
<CommunityMessage message={messageWithAuthor ? messageWithAuthor : message} platform={platform} />
isMobile ? renderSimplifiedMessage() :
<CommunityMessage
message={finalMessage}
platform={platform}
/>
)}
</div>
);

View File

@ -1,4 +1,4 @@
import React, { useState, useRef } from 'react';
import React, { useState, useRef, useEffect } from 'react';
import Image from 'next/image';
import UserAvatar from './user/UserAvatar';
import { Menubar } from 'primereact/menubar';
@ -10,14 +10,36 @@ import 'primeicons/primeicons.css';
import { useNDKContext } from '@/context/NDKContext';
import useWindowWidth from '@/hooks/useWindowWidth';
// todo: Dropdown on left becomes search bar on mobile (also about and subscribe are linked in user avatart dropdown on mobile)
// MAYBE: Add a 4th button at the bottom that contains both about and subscribe
const Navbar = () => {
const router = useRouter();
const windowWidth = useWindowWidth();
const navbarHeight = '60px';
const {ndk} = useNDKContext();
const { ndk } = useNDKContext();
const [isHovered, setIsHovered] = useState(false);
const [showMobileSearch, setShowMobileSearch] = useState(false);
const menu = useRef(null);
// Debug Navbar mounting
useEffect(() => {
console.log("Navbar mounted, windowWidth:", windowWidth);
}, [windowWidth]);
// Lock/unlock body scroll when mobile search is shown/hidden
useEffect(() => {
if (showMobileSearch && windowWidth <= 600) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
// Cleanup effect
return () => {
document.body.style.overflow = 'unset';
};
}, [showMobileSearch, windowWidth]);
const menuItems = [
{
label: 'Content',
@ -41,54 +63,88 @@ const Navbar = () => {
}
];
const start = (
<div className='flex items-center'>
<div onClick={() => router.push('/')} className="flex flex-row items-center justify-center cursor-pointer">
<Image
alt="logo"
src="/images/plebdevs-icon.png"
width={50}
height={50}
className="rounded-full max-tab:hidden max-mob:hidden"
/>
<h1 className="text-white text-xl font-semibold max-tab:text-2xl max-mob:text-2xl pb-1 pl-2">PlebDevs</h1>
</div>
<div
className={`ml-2 p-2 cursor-pointer transition-all duration-300 flex items-center justify-center ${isHovered ? 'bg-gray-700 rounded-full' : ''}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={(e) => menu.current.toggle(e)}
style={{ width: '40px', height: '40px' }}
>
<div className="flex flex-col items-center justify-center">
{/* Show hamburger menu on mobile (< 600px) and up/down arrows on larger screens */}
{windowWidth <= 600 ? (
<i className="pi pi-bars text-white text-xl" />
) : (
<>
<i className="pi pi-angle-up text-white text-base" />
<i className="pi pi-angle-down text-white text-base" />
</>
)}
</div>
</div>
<Menu model={menuItems} popup ref={menu} />
{ndk && windowWidth > 600 && <SearchBar />}
</div>
);
return (
<>
<div className='w-[100vw] h-fit z-20'>
<Menubar
start={start}
end={UserAvatar}
className='px-10 py-8 bg-gray-800 border-t-0 border-l-0 border-r-0 rounded-none fixed z-10 w-[100vw] max-tab:px-[5%] max-mob:px-[5%]'
style={{ height: navbarHeight }}
/>
<div className='px-10 py-8 bg-gray-800 border-t-0 border-l-0 border-r-0 rounded-none fixed z-10 w-[100vw] max-tab:px-[5%] max-mob:px-[5%] flex justify-between' style={{ height: navbarHeight }}>
{/* Left section */}
<div className='flex items-center flex-1'>
<div onClick={() => router.push('/')} className="flex flex-row items-center justify-center cursor-pointer">
<Image
alt="logo"
src="/images/plebdevs-icon.png"
width={50}
height={50}
className="rounded-full max-tab:hidden max-mob:hidden"
/>
<h1 className="text-white text-xl font-semibold max-tab:text-2xl max-mob:text-2xl pb-1 pl-2">PlebDevs</h1>
</div>
{windowWidth > 600 ? (
<div
className={`ml-2 p-2 cursor-pointer transition-all duration-300 flex items-center justify-center ${isHovered ? 'bg-gray-700 rounded-full' : ''}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={(e) => menu.current.toggle(e)}
style={{ width: '40px', height: '40px' }}
>
<div className="flex flex-col items-center justify-center">
<i className="pi pi-angle-up text-white text-base" />
<i className="pi pi-angle-down text-white text-base" />
</div>
</div>
) : (
<div
className="ml-2 p-2 cursor-pointer transition-all duration-300 flex items-center justify-center hover:bg-gray-700 rounded-full"
onClick={() => setShowMobileSearch(!showMobileSearch)}
style={{ width: '40px', height: '40px' }}
>
<i className="pi pi-search text-white text-xl" />
</div>
)}
<Menu model={menuItems} popup ref={menu} />
</div>
{/* Center section - Search */}
{windowWidth > 600 && (
<div className="flex items-center justify-center flex-1">
<SearchBar isDesktopNav={true} />
</div>
)}
{/* Right section - User Avatar */}
<div className="flex items-center justify-end flex-1">
<UserAvatar />
</div>
</div>
</div>
{/* Placeholder div with the same height as the Navbar */}
<div style={{ height: navbarHeight }}></div>
{/* Mobile Search Overlay */}
{showMobileSearch && windowWidth <= 600 && (
<div className="fixed inset-0 bg-gray-900 z-50 overflow-hidden navbar-mobile-search">
<div className="h-full">
<div className="sticky top-0 z-10 bg-gray-900">
<div className="px-6 py-4 flex items-center justify-between border-b border-gray-700">
<h2 className="text-white text-2xl font-semibold">Search</h2>
<button
onClick={() => setShowMobileSearch(false)}
className="text-white hover:text-gray-300 p-2"
>
<i className="pi pi-times text-2xl" />
</button>
</div>
<div className="px-6 pt-4 pb-2">
<SearchBar
isMobileSearch={true}
onCloseSearch={() => setShowMobileSearch(false)}
/>
</div>
</div>
</div>
</div>
)}
</>
);
};

View File

@ -1,7 +1,5 @@
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';
@ -10,8 +8,9 @@ import { useContentSearch } from '@/hooks/useContentSearch';
import { useCommunitySearch } from '@/hooks/useCommunitySearch';
import { useRouter } from 'next/router';
import useWindowWidth from '@/hooks/useWindowWidth';
import { useNDKContext } from "@/context/NDKContext";
const SearchBar = () => {
const SearchBar = ({ isMobileSearch, isDesktopNav, onCloseSearch }) => {
const { searchContent, searchResults: contentResults } = useContentSearch();
const { searchCommunity, searchResults: communityResults } = useCommunitySearch();
const router = useRouter();
@ -24,8 +23,23 @@ const SearchBar = () => {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState([]);
const op = useRef(null);
const { ndk, reInitializeNDK } = useNDKContext();
// Debug effect to check component mounting and props
useEffect(() => {
console.log("SearchBar mounted with props:", { isMobileSearch, isDesktopNav });
}, [isMobileSearch, isDesktopNav]);
const selectedOptionTemplate = (option, props) => {
if (isDesktopNav) {
// For desktop nav bar, just show the icon
return (
<div className="flex items-center justify-center">
<i className={option.icon + " text-white text-lg"} />
</div>
);
}
if (!props?.placeholder) {
return (
<div className="flex items-center">
@ -44,14 +58,14 @@ const SearchBar = () => {
if (selectedSearchOption.code === 'content') {
searchContent(term);
setSearchResults(contentResults);
} else if (selectedSearchOption.code === 'community') {
} else if (selectedSearchOption.code === 'community' && ndk) {
searchCommunity(term);
setSearchResults(communityResults);
}
if (term.length > 2) {
if (!isMobileSearch && term.length > 2) {
op.current.show(e);
} else {
} else if (!isMobileSearch) {
op.current.hide();
}
};
@ -64,80 +78,230 @@ const SearchBar = () => {
}
}, [selectedSearchOption, contentResults, communityResults]);
useEffect(() => {
const handleError = (event) => {
if (event.message && event.message.includes('wss://relay.devs.tools')) {
console.warn('Nostr relay connection error detected, reinitializing NDK');
reInitializeNDK();
}
};
window.addEventListener('error', handleError);
return () => {
window.removeEventListener('error', handleError);
};
}, [reInitializeNDK]);
const handleContentSelect = (content) => {
if (content?.type === 'course') {
router.push(`/course/${content?.d || content?.id}`);
} else {
router.push(`/details/${content.id}`);
if (selectedSearchOption.code === 'content') {
if (content?.type === 'course') {
router.push(`/course/${content?.d || content?.id}`);
} else {
router.push(`/details/${content.id}`);
}
} else if (selectedSearchOption.code === 'community') {
if (content.type === 'discord') {
router.push('/feed?channel=discord');
} else if (content.type === 'nostr') {
router.push('/feed?channel=nostr');
} else if (content.type === 'stackernews') {
router.push('/feed?channel=stackernews');
} else {
router.push('/feed?channel=global');
}
}
setSearchTerm('');
searchContent('');
op.current.hide();
searchCommunity('');
setSearchResults([]);
if (op.current) {
op.current.hide();
}
if (isMobileSearch && onCloseSearch) {
onCloseSearch();
} else if (isMobileSearch && window.parent) {
const navbar = document.querySelector('.navbar-mobile-search');
if (navbar) {
const closeButton = navbar.querySelector('button');
if (closeButton) {
closeButton.click();
}
}
}
}
const renderSearchResults = () => {
if (searchResults.length === 0 && searchTerm.length > 2) {
return <div className="p-4 text-center text-gray-400">No results found</div>;
}
return 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}
/>
)
));
};
return (
<div className={`absolute ${windowWidth < 950 ? "left-[55%]" : "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'
}
}}
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 className={`${isDesktopNav ? 'w-full max-w-md' : 'w-full'}`}>
{isDesktopNav ? (
// Desktop navbar search with integrated dropdown
<div className="flex items-center bg-gray-900/55 rounded-lg">
<div className="relative flex-1 flex items-center">
<i className="pi pi-search text-gray-400 absolute left-4 z-10" />
<InputText
className="w-full bg-transparent pl-10"
value={searchTerm}
onChange={handleSearch}
placeholder={`Search ${selectedSearchOption.name.toLowerCase()}`}
pt={{
root: {
className: "border-none focus:ring-0 rounded-lg"
}
}}
/>
</div>
<div className="flex items-center px-2 border-l border-gray-700 h-full">
<Dropdown
pt={{
root: {
className: 'border-none bg-transparent'
},
input: {
className: 'mx-0 px-0'
},
trigger: {
className: 'p-0'
},
panel: {
className: 'min-w-[150px]'
}
}}
value={selectedSearchOption}
onChange={(e) => setSelectedSearchOption(e.value)}
options={searchOptions}
optionLabel="name"
dropdownIcon={
<i className="pi pi-chevron-down text-gray-400 ml-1" />
}
valueTemplate={selectedOptionTemplate}
itemTemplate={(option) => (
<div className="flex items-center py-1">
<i className={option.icon + ' mr-2 text-lg'}></i>
<span>{option.name}</span>
</div>
)}
/>
</div>
</div>
) : (
// Original search for other views
<div className={`flex flex-col ${isMobileSearch ? 'gap-4' : ''}`}>
<div className={`relative flex items-center ${isMobileSearch ? 'bg-gray-800 rounded-lg p-2' : ''}`}>
<i className="pi pi-search text-gray-400 absolute left-4 z-10" />
<InputText
className={`
${windowWidth > 845 ? 'w-[300px]' : (isMobileSearch || windowWidth <= 600) ? 'w-full' : 'w-[160px]'}
${isMobileSearch ? 'bg-transparent border-none pl-12 text-lg' : ''}
`}
value={searchTerm}
onChange={handleSearch}
placeholder={`Search ${selectedSearchOption.name.toLowerCase()}`}
pt={{
root: {
className: `${isMobileSearch ? 'focus:ring-0' : 'border-none rounded-tr-none rounded-br-none focus:border-none focus:ring-0 pr-0'}`
}
}}
/>
</div>
}
valueTemplate={selectedOptionTemplate}
itemTemplate={selectedOptionTemplate}
required
/>
</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' ? (
<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>
{isMobileSearch && (
<div className="flex items-center gap-2 mb-3">
{searchOptions.map((option) => (
<button
key={option.code}
onClick={() => setSelectedSearchOption(option)}
className={`flex items-center gap-2 px-4 py-2 rounded-full ${
selectedSearchOption.code === option.code
? 'bg-gray-700 text-white'
: 'bg-gray-800 text-gray-400'
}`}
>
<i className={option.icon} />
<span>{option.name}</span>
</button>
))}
</div>
)}
{!isMobileSearch && (
<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'
}
}}
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
/>
)}
</div>
)}
</OverlayPanel>
</div>
</div>
{/* Desktop Search Results */}
{!isMobileSearch && (
<OverlayPanel
ref={op}
className="w-[600px] max-h-[70vh] overflow-y-auto"
>
{renderSearchResults()}
</OverlayPanel>
)}
{/* Mobile Search Results */}
{isMobileSearch && searchTerm.length > 2 && (
<div
className="fixed inset-x-0 bottom-0 top-[165px] bg-gray-900 overflow-y-auto"
style={{ touchAction: 'pan-y' }}
>
<div className="pb-20">
{renderSearchResults()}
</div>
</div>
)}
</>
);
};

View File

@ -9,6 +9,7 @@ import { useSession } from "next-auth/react";
import { useIsAdmin } from "@/hooks/useIsAdmin";
import { ProgressSpinner } from "primereact/progressspinner";
//todo: Link below connect wallet, relays hidden in ... (open modal)
const Profile = () => {
const router = useRouter();
const { data: session, status } = useSession();