import { NDKEvent, NDKUserProfile, profileFromEvent } from '@nostr-dev-kit/ndk' import { ErrorBoundary } from 'components/ErrorBoundary' import { GameCard } from 'components/GameCard' import { LoadingSpinner } from 'components/LoadingSpinner' import { ModCard } from 'components/ModCard' import { ModFilter } from 'components/ModsFilter' import { Pagination } from 'components/Pagination' import { Profile } from 'components/ProfileSection' import { MAX_GAMES_PER_PAGE, MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts' import { RelayController } from 'controllers' import { useAppSelector, useFilteredMods, useGames, useMuteLists, useNSFWList } from 'hooks' import { Filter, kinds } from 'nostr-tools' import { Subscription } from 'nostr-tools/abstract-relay' import React, { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { toast } from 'react-toastify' import { FilterOptions, ModDetails, ModeratedFilter, MuteLists, NSFWFilter, SortBy } from 'types' import { extractModData, isModDataComplete, log, LogType } from 'utils' enum SearchKindEnum { Mods = 'Mods', Games = 'Games', Users = 'Users' } export const SearchPage = () => { const [searchParams] = useSearchParams() const muteLists = useMuteLists() const nsfwList = useNSFWList() const searchTermRef = useRef(null) const [searchKind, setSearchKind] = useState( (searchParams.get('searching') as SearchKindEnum) || SearchKindEnum.Mods ) const [filterOptions, setFilterOptions] = useState({ sort: SortBy.Latest, nsfw: NSFWFilter.Hide_NSFW, source: window.location.host, moderated: ModeratedFilter.Moderated }) const [searchTerm, setSearchTerm] = useState( searchParams.get('searchTerm') || '' ) const handleSearch = () => { const value = searchTermRef.current?.value || '' // Access the input value from the ref setSearchTerm(value) } // Handle "Enter" key press inside the input const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { handleSearch() } } return (

Search:  {searchTerm}

{searchKind === SearchKindEnum.Mods && ( )} {searchKind === SearchKindEnum.Users && ( )} {searchKind === SearchKindEnum.Games && ( )}
) } type FiltersProps = { filterOptions: FilterOptions setFilterOptions: Dispatch> searchKind: SearchKindEnum setSearchKind: Dispatch> } const Filters = React.memo( ({ filterOptions, setFilterOptions, searchKind, setSearchKind }: FiltersProps) => { const userState = useAppSelector((state) => state.user) return (
{searchKind === SearchKindEnum.Mods && ( )} {searchKind === SearchKindEnum.Users && (
{Object.values(ModeratedFilter).map((item, index) => { if (item === ModeratedFilter.Unmoderated_Fully) { const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB if (!isAdmin) return null } return (
setFilterOptions((prev) => ({ ...prev, moderated: item })) } > {item}
) })}
)}
{Object.values(SearchKindEnum).map((item, index) => (
setSearchKind(item)} > {item}
))}
) } ) type ModsResultProps = { filterOptions: FilterOptions searchTerm: string muteLists: { admin: MuteLists user: MuteLists } nsfwList: string[] } const ModsResult = ({ filterOptions, searchTerm, muteLists, nsfwList }: ModsResultProps) => { const hasEffectRun = useRef(false) const [isSubscribing, setIsSubscribing] = useState(false) const [mods, setMods] = useState([]) const [page, setPage] = useState(1) const userState = useAppSelector((state) => state.user) useEffect(() => { if (hasEffectRun.current) { return } hasEffectRun.current = true // Set it so the effect doesn't run again const filter: Filter = { kinds: [kinds.ClassifiedListing], '#t': [T_TAG_VALUE] } setIsSubscribing(true) let subscriptions: Subscription[] = [] RelayController.getInstance() .subscribeForEvents(filter, [], (event) => { if (isModDataComplete(event)) { const mod = extractModData(event) setMods((prev) => [...prev, mod]) } }) .then((subs) => { subscriptions = subs }) .catch((err) => { log( true, LogType.Error, 'An error occurred in subscribing to relays.', err ) toast.error(err.message || err) }) .finally(() => { setIsSubscribing(false) }) // Cleanup function to stop all subscriptions return () => { subscriptions.forEach((sub) => sub.close()) // close each subscription } }, []) useEffect(() => { setPage(1) }, [searchTerm]) const filteredMods = useMemo(() => { if (searchTerm === '') return [] const lowerCaseSearchTerm = searchTerm.toLowerCase() const filterFn = (mod: ModDetails) => mod.title.toLowerCase().includes(lowerCaseSearchTerm) || mod.game.toLowerCase().includes(lowerCaseSearchTerm) || mod.summary.toLowerCase().includes(lowerCaseSearchTerm) || mod.body.toLowerCase().includes(lowerCaseSearchTerm) || mod.tags.findIndex((tag) => tag.toLowerCase().includes(lowerCaseSearchTerm) ) > -1 return mods.filter(filterFn) }, [mods, searchTerm]) const filteredModList = useFilteredMods( filteredMods, userState, filterOptions, nsfwList, muteLists ) const handleNext = () => { setPage((prev) => prev + 1) } const handlePrev = () => { setPage((prev) => prev - 1) } return ( <> {isSubscribing && ( )}
{filteredModList .slice((page - 1) * MAX_MODS_PER_PAGE, page * MAX_MODS_PER_PAGE) .map((mod) => ( ))}
) } type UsersResultProps = { searchTerm: string moderationFilter: ModeratedFilter muteLists: { admin: MuteLists user: MuteLists } } const UsersResult = ({ searchTerm, moderationFilter, muteLists }: UsersResultProps) => { const [isFetching, setIsFetching] = useState(false) const [profiles, setProfiles] = useState([]) const userState = useAppSelector((state) => state.user) useEffect(() => { if (searchTerm === '') { setProfiles([]) } else { const filter: Filter = { kinds: [kinds.Metadata], search: searchTerm } setIsFetching(true) RelayController.getInstance() .fetchEvents(filter, ['wss://purplepag.es', 'wss://user.kindpag.es']) .then((events) => { const results = events.map((event) => { const ndkEvent = new NDKEvent(undefined, event) const profile = profileFromEvent(ndkEvent) return profile }) setProfiles(results) }) .catch((err) => { log(true, LogType.Error, 'An error occurred in fetching users', err) }) .finally(() => { setIsFetching(false) }) } }, [searchTerm]) const filteredProfiles = useMemo(() => { let filtered = [...profiles] const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB const isUnmoderatedFully = moderationFilter === ModeratedFilter.Unmoderated_Fully // Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully" if (!(isAdmin && isUnmoderatedFully)) { filtered = filtered.filter( (profile) => !muteLists.admin.authors.includes(profile.pubkey as string) ) } if (moderationFilter === ModeratedFilter.Moderated) { filtered = filtered.filter( (profile) => !muteLists.user.authors.includes(profile.pubkey as string) ) } return filtered }, [userState.user?.npub, moderationFilter, profiles, muteLists]) return ( <> {isFetching && }
{filteredProfiles.map((profile) => { if (profile.pubkey) { const displayName = profile?.displayName || profile?.name || '[name not set up]' const about = profile?.bio || profile?.about || '[bio not set up]' return ( ) } return null })}
) } type GamesResultProps = { searchTerm: string } const GamesResult = ({ searchTerm }: GamesResultProps) => { const games = useGames() const [page, setPage] = useState(1) // Reset the page to 1 whenever searchTerm changes useEffect(() => { setPage(1) }, [searchTerm]) const filteredGames = useMemo(() => { if (searchTerm === '') return [] const lowerCaseSearchTerm = searchTerm.toLowerCase() return games.filter((game) => game['Game Name'].toLowerCase().includes(lowerCaseSearchTerm) ) }, [searchTerm, games]) const handleNext = () => { setPage((prev) => prev + 1) } const handlePrev = () => { setPage((prev) => prev - 1) } return ( <>
{filteredGames .slice((page - 1) * MAX_GAMES_PER_PAGE, page * MAX_GAMES_PER_PAGE) .map((game) => ( ))}
) }