Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/src/app/chapters/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const ChaptersPage = () => {
isLoaded={isLoaded}
onPageChange={handlePageChange}
onSearch={handleSearch}
scopeLabel="Search within chapters"
searchPlaceholder="Search for chapters..."
searchQuery={searchQuery}
totalPages={totalPages}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/committees/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const CommitteesPage = () => {
isLoaded={isLoaded}
onPageChange={handlePageChange}
onSearch={handleSearch}
scopeLabel="Search within committees"
searchPlaceholder="Search for committees..."
searchQuery={searchQuery}
totalPages={totalPages}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/contribute/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const ContributePage = () => {
isLoaded={isLoaded}
onPageChange={handlePageChange}
onSearch={handleSearch}
scopeLabel="Search within issues"
searchPlaceholder="Search for issues..."
searchQuery={searchQuery}
totalPages={totalPages}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/members/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const UsersPage = () => {
isLoaded={isLoaded}
onPageChange={handlePageChange}
onSearch={handleSearch}
scopeLabel="Search within members"
searchPlaceholder="Search for members..."
searchQuery={searchQuery}
totalPages={totalPages}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/mentorship/programs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const ProgramsPage = () => {
isLoaded={isLoaded}
onPageChange={handlePageChange}
onSearch={handleSearch}
scopeLabel="Search within programs"
searchPlaceholder="Search for programs..."
searchQuery={searchQuery}
totalPages={totalPages}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/my/mentorship/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const MyMentorshipPage: React.FC = () => {
setPage(1)
}}
searchQuery={searchQuery}
scopeLabel="Search within your programs"
searchPlaceholder="Search your programs"
indexName="my-programs"
>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/organizations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const OrganizationPage = () => {
isLoaded={isLoaded}
onPageChange={handlePageChange}
onSearch={handleSearch}
scopeLabel="Search within organizations"
searchPlaceholder="Search for organizations..."
searchQuery={searchQuery}
totalPages={totalPages}
Expand Down
9 changes: 0 additions & 9 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import LoadingSpinner from 'components/LoadingSpinner'
import MovingLogos from 'components/LogoCarousel'
import Milestones from 'components/Milestones'
import DialogComp from 'components/Modal'
import MultiSearchBar from 'components/MultiSearch'
import RecentIssues from 'components/RecentIssues'
import RecentPullRequests from 'components/RecentPullRequests'
import RecentReleases from 'components/RecentReleases'
Expand Down Expand Up @@ -146,14 +145,6 @@ export default function Home() {
Your gateway to OWASP. Discover, engage, and help shape the future!
</p>
</div>
<div className="mx-auto mb-8 flex max-w-2xl justify-center">
<MultiSearchBar
eventData={data?.upcomingEvents ?? []}
isLoaded={true}
placeholder="Search the OWASP community"
indexes={['chapters', 'organizations', 'projects', 'users']}
/>
</div>
</div>
<SecondaryCard
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Event search functionality will be lost when MultiSearchBar is moved to global Header. The component relies on eventData={data?.upcomingEvents ?? []} from page-specific GraphQL query, but the relocated component in Header.tsx does not receive this prop. Since events are not in the Algolia indexes array, search will silently return no event results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/app/page.tsx, line 149:

<comment>Event search functionality will be lost when MultiSearchBar is moved to global Header. The component relies on `eventData={data?.upcomingEvents ?? []}` from page-specific GraphQL query, but the relocated component in Header.tsx does not receive this prop. Since events are not in the Algolia `indexes` array, search will silently return no event results.</comment>

<file context>
@@ -146,14 +145,6 @@ export default function Home() {
-            />
-          </div>
         </div>
         <SecondaryCard
           icon={FaCalendarAlt}
</file context>

icon={FaCalendarAlt}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const ProjectsPage = () => {
isLoaded={isLoaded}
onPageChange={handlePageChange}
onSearch={handleSearch}
scopeLabel="Search within projects"
searchPlaceholder="Search for projects..."
searchQuery={searchQuery}
sortChildren={
Expand Down
64 changes: 61 additions & 3 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
FaStar as FaSolidStar,
FaBars,
FaTimes,
FaSearch,
} from 'react-icons/fa'
import { desktopViewMinWidth, headerLinks } from 'utils/constants'
import { cn } from 'utils/utility'
import MultiSearchBar from 'components/MultiSearch'
import ModeToggle from 'components/ModeToggle'
import NavButton from 'components/NavButton'
import NavDropdown from 'components/NavDropDown'
Expand All @@ -22,18 +24,28 @@ import UserMenu from 'components/UserMenu'
export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthEnabled: boolean }) {
const pathname = usePathname()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const toggleMobileMenu = () => setMobileMenuOpen(!mobileMenuOpen)
const [mobileSearchOpen, setMobileSearchOpen] = useState(false)
const toggleMobileMenu = () => {
setMobileMenuOpen((prev) => !prev)
setMobileSearchOpen(false)
}
const toggleMobileSearch = () => {
setMobileSearchOpen((prev) => !prev)
setMobileMenuOpen(false)
}

useEffect(() => {
const handleResize = () => {
if (globalThis.innerWidth >= desktopViewMinWidth) {
setMobileMenuOpen(false)
setMobileSearchOpen(false)
}
}

const handleOutsideClick = (event: Event) => {
const navbar = document.getElementById('navbar-sticky')
const sidebar = document.querySelector('.fixed.inset-y-0')
const mobileSearch = document.getElementById('global-search-mobile')
if (
mobileMenuOpen &&
navbar &&
Expand All @@ -43,6 +55,15 @@ export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthE
) {
setMobileMenuOpen(false)
}
if (
mobileSearchOpen &&
mobileSearch &&
!mobileSearch.contains(event.target as Node) &&
navbar &&
!navbar.contains(event.target as Node)
) {
setMobileSearchOpen(false)
}
}

globalThis.addEventListener('resize', handleResize)
Expand All @@ -60,7 +81,10 @@ export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthE
{/* Logo */}
<Link
href="/"
onClick={() => setMobileMenuOpen(false)}
onClick={() => {
setMobileMenuOpen(false)
setMobileSearchOpen(false)
}}
className="rounded-lg focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
<div className="flex h-full items-center">
Expand Down Expand Up @@ -108,7 +132,26 @@ export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthE
})}
</div>
</div>
<div className="hidden w-80 items-center lg:flex">
<MultiSearchBar
autoFocus={false}
isLoaded={true}
placeholder="Search the OWASP community"
indexes={['chapters', 'organizations', 'projects', 'users']}
containerClassName="w-full max-w-none p-0"
inputClassName="h-10 text-sm"
/>
</div>
<div className="flex items-center justify-normal gap-4">
<div className="lg:hidden">
<Button
onPress={toggleMobileSearch}
className="flex h-11 w-11 items-center justify-center rounded-lg bg-transparent text-slate-300 hover:bg-transparent hover:text-slate-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
<span className="sr-only">Open search</span>
<FaSearch className="h-5 w-5" />
</Button>
</div>
<div className="hidden md:flex">
<NavButton
href="https://github.com/OWASP/Nest"
Expand Down Expand Up @@ -145,6 +188,18 @@ export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthE
</div>
</div>
</div>
{mobileSearchOpen && (
<div id="global-search-mobile" className="bg-owasp-blue px-4 pb-4 lg:hidden dark:bg-slate-800">
<MultiSearchBar
autoFocus={true}
isLoaded={true}
placeholder="Search the OWASP community"
indexes={['chapters', 'organizations', 'projects', 'users']}
containerClassName="w-full max-w-none p-0"
inputClassName="h-10 text-base"
/>
</div>
)}
<div
className={cn(
'bg-owasp-blue fixed inset-y-0 left-0 z-50 w-64 transform shadow-md transition-transform dark:bg-slate-800',
Expand All @@ -156,7 +211,10 @@ export default function Header({ isGitHubAuthEnabled }: { readonly isGitHubAuthE
<div className="flex flex-col justify-center gap-5">
<Link
href="/"
onClick={() => setMobileMenuOpen(false)}
onClick={() => {
setMobileMenuOpen(false)
setMobileSearchOpen(false)
}}
className="rounded-lg focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
<div className="flex h-full items-center">
Expand Down
23 changes: 19 additions & 4 deletions frontend/src/components/MultiSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { Organization } from 'types/organization'
import type { Project } from 'types/project'
import type { MultiSearchBarProps, Suggestion } from 'types/search'
import type { User } from 'types/user'
import { cn } from 'utils/utility'

type SearchHit = Chapter | Event | Organization | Project | User

Expand All @@ -22,6 +23,9 @@ const MultiSearchBar: React.FC<MultiSearchBarProps> = ({
indexes,
initialValue = '',
eventData,
autoFocus = false,
containerClassName,
inputClassName,
}) => {
const [searchQuery, setSearchQuery] = useState(initialValue)
const [suggestions, setSuggestions] = useState<Suggestion[]>([])
Expand Down Expand Up @@ -115,6 +119,10 @@ const MultiSearchBar: React.FC<MultiSearchBarProps> = ({

useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
const isInputFocused = document.activeElement === inputRef.current
if (!isInputFocused && !showSuggestions) {
return
}
if (event.key === 'Escape') {
setShowSuggestions(false)
inputRef.current?.blur()
Expand Down Expand Up @@ -158,8 +166,6 @@ const MultiSearchBar: React.FC<MultiSearchBarProps> = ({
}, [searchQuery, suggestions, highlightedIndex, handleSuggestionClick])

useEffect(() => {
inputRef.current?.focus()

const handleClickOutside = (event: MouseEvent) => {
if (searchBarRef.current && !searchBarRef.current.contains(event.target as Node)) {
setShowSuggestions(false)
Expand All @@ -173,6 +179,12 @@ const MultiSearchBar: React.FC<MultiSearchBarProps> = ({
}
}, [])

useEffect(() => {
if (autoFocus) {
inputRef.current?.focus()
}
Comment on lines +182 to +185
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The autoFocus feature fails when the component mounts with isLoaded=false. The useEffect only depends on [autoFocus] but the input element is conditionally rendered based on isLoaded. When isLoaded becomes true, the effect won't re-run because autoFocus hasn't changed, leaving the input unfocused.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/components/MultiSearch.tsx, line 182:

<comment>The autoFocus feature fails when the component mounts with `isLoaded=false`. The useEffect only depends on `[autoFocus]` but the input element is conditionally rendered based on `isLoaded`. When `isLoaded` becomes `true`, the effect won't re-run because `autoFocus` hasn't changed, leaving the input unfocused.</comment>

<file context>
@@ -173,6 +179,12 @@ const MultiSearchBar: React.FC<MultiSearchBarProps> = ({
     }
   }, [])
 
+  useEffect(() => {
+    if (autoFocus) {
+      inputRef.current?.focus()
</file context>
Suggested change
useEffect(() => {
if (autoFocus) {
inputRef.current?.focus()
}
useEffect(() => {
if (autoFocus && isLoaded) {
inputRef.current?.focus()
}
}, [autoFocus, isLoaded])

}, [autoFocus])

const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newQuery = e.target.value
setSearchQuery(newQuery)
Expand Down Expand Up @@ -221,7 +233,7 @@ const MultiSearchBar: React.FC<MultiSearchBarProps> = ({
}

return (
<div className="w-full max-w-md p-4" ref={searchBarRef}>
<div className={cn('w-full max-w-md p-4', containerClassName)} ref={searchBarRef}>
<div className="relative">
{isLoaded ? (
<>
Expand All @@ -236,7 +248,10 @@ const MultiSearchBar: React.FC<MultiSearchBarProps> = ({
onChange={handleSearchChange}
onFocus={handleFocusSearch}
placeholder={placeholder}
className="h-12 w-full rounded-lg border-1 border-gray-300 bg-white pr-10 pl-10 text-lg text-black focus:ring-1 focus:ring-blue-500 focus:outline-hidden dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:focus:ring-blue-300"
className={cn(
'h-12 w-full rounded-lg border-1 border-gray-300 bg-white pr-10 pl-10 text-lg text-black focus:ring-1 focus:ring-blue-500 focus:outline-hidden dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:focus:ring-blue-300',
inputClassName
)}
/>
{searchQuery && (
<button
Expand Down
21 changes: 15 additions & 6 deletions frontend/src/components/SearchPageLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface SearchPageLayoutProps {

onPageChange: (page: number) => void
searchPlaceholder: string
scopeLabel?: string
empty?: string
indexName: string
loadingImageUrl?: string
Expand All @@ -27,6 +28,7 @@ const SearchPageLayout = ({
onSearch,
onPageChange,
searchPlaceholder,
scopeLabel,
empty,
indexName,
loadingImageUrl = '/img/spinner_light.png',
Expand All @@ -42,12 +44,19 @@ const SearchPageLayout = ({
return (
<div className="text-text flex min-h-screen w-full flex-col items-center justify-normal p-5">
<div className="flex w-full items-center justify-center">
<SearchBar
isLoaded={!isFirstLoad}
onSearch={onSearch}
placeholder={searchPlaceholder}
initialValue={searchQuery}
/>
<div className="flex w-full flex-col items-center">
{scopeLabel && (
<div className="w-full max-w-md px-4 pb-1 text-left text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{scopeLabel}
</div>
)}
<SearchBar
isLoaded={!isFirstLoad}
onSearch={onSearch}
placeholder={searchPlaceholder}
initialValue={searchQuery}
/>
</div>
</div>
{isLoaded ? (
<>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/types/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export interface MultiSearchBarProps {
indexes: string[]
initialValue?: string
eventData?: Event[]
autoFocus?: boolean
containerClassName?: string
inputClassName?: string
}

export type Suggestion = {
Expand Down