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 { 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, useGames, useMuteLists } from 'hooks' import { Filter, kinds, nip19 } 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 { getModPageRoute } from 'routes' import { ModDetails, MuteLists } from 'types' import { extractModData, isModDataComplete, log, LogType } from 'utils' enum SortByEnum { Latest = 'Latest', Oldest = 'Oldest', Best_Rated = 'Best Rated', Worst_Rated = 'Worst Rated' } enum ModeratedFilterEnum { Moderated = 'Moderated', Unmoderated = 'Unmoderated', Unmoderated_Fully = 'Unmoderated Fully' } enum SearchingFilterEnum { Mods = 'Mods', Games = 'Games', Users = 'Users' } interface FilterOptions { sort: SortByEnum moderated: ModeratedFilterEnum searching: SearchingFilterEnum } export const SearchPage = () => { const [searchParams] = useSearchParams() const muteLists = useMuteLists() const searchTermRef = useRef(null) const [filterOptions, setFilterOptions] = useState({ sort: SortByEnum.Latest, moderated: ModeratedFilterEnum.Moderated, searching: (searchParams.get('searching') as SearchingFilterEnum) || SearchingFilterEnum.Mods }) 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}

{filterOptions.searching === SearchingFilterEnum.Mods && ( )} {filterOptions.searching === SearchingFilterEnum.Users && ( )} {filterOptions.searching === SearchingFilterEnum.Games && ( )}
) } type FiltersProps = { filterOptions: FilterOptions setFilterOptions: Dispatch> } const Filters = React.memo( ({ filterOptions, setFilterOptions }: FiltersProps) => { const userState = useAppSelector((state) => state.user) return (
{Object.values(SortByEnum).map((item, index) => (
setFilterOptions((prev) => ({ ...prev, sort: item })) } > {item}
))}
{Object.values(ModeratedFilterEnum).map((item, index) => { if (item === ModeratedFilterEnum.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(SearchingFilterEnum).map((item, index) => (
setFilterOptions((prev) => ({ ...prev, searching: item })) } > {item}
))}
) } ) type ModsResultProps = { filterOptions: FilterOptions searchTerm: string muteLists: { admin: MuteLists user: MuteLists } } const ModsResult = ({ filterOptions, searchTerm, muteLists }: 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 = useMemo(() => { let filtered: ModDetails[] = [...filteredMods] const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB const isUnmoderatedFully = filterOptions.moderated === ModeratedFilterEnum.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( (mod) => !muteLists.admin.authors.includes(mod.author) && !muteLists.admin.replaceableEvents.includes(mod.aTag) ) } if (filterOptions.moderated === ModeratedFilterEnum.Moderated) { filtered = filtered.filter( (mod) => !muteLists.user.authors.includes(mod.author) && !muteLists.user.replaceableEvents.includes(mod.aTag) ) } if (filterOptions.sort === SortByEnum.Latest) { filtered.sort((a, b) => b.published_at - a.published_at) } else if (filterOptions.sort === SortByEnum.Oldest) { filtered.sort((a, b) => a.published_at - b.published_at) } return filtered }, [ filteredMods, userState.user?.npub, filterOptions.sort, filterOptions.moderated, 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) => { const route = getModPageRoute( nip19.naddrEncode({ identifier: mod.aTag, pubkey: mod.author, kind: kinds.ClassifiedListing }) ) return ( ) })}
) } type UsersResultProps = { searchTerm: string moderationFilter: ModeratedFilterEnum 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 === ModeratedFilterEnum.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 === ModeratedFilterEnum.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) { 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) => ( ))}
) }