From 72252d416bbe77fe31fba13c1b1d813175bc7ffb Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 29 Oct 2024 09:35:39 +0100 Subject: [PATCH 1/5] feat: mod search on game page --- src/components/SearchInput.tsx | 39 +++++++++++++++++ src/pages/game.tsx | 78 +++++++++++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 src/components/SearchInput.tsx diff --git a/src/components/SearchInput.tsx b/src/components/SearchInput.tsx new file mode 100644 index 0000000..3bf7d19 --- /dev/null +++ b/src/components/SearchInput.tsx @@ -0,0 +1,39 @@ +import { forwardRef } from 'react' + +interface SearchInputProps { + handleKeyDown: (event: React.KeyboardEvent) => void + handleSearch: () => void +} + +export const SearchInput = forwardRef( + ({ handleKeyDown, handleSearch }, ref) => ( +
+
+
+ + +
+
+
+ ) +) diff --git a/src/pages/game.tsx b/src/pages/game.tsx index 7b94d74..62c65ba 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -6,6 +6,7 @@ import { import { ModCard } from 'components/ModCard' import { ModFilter } from 'components/ModsFilter' import { PaginationWithPageNumbers } from 'components/Pagination' +import { SearchInput } from 'components/SearchInput' import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts' import { useAppSelector, @@ -14,8 +15,8 @@ import { useNDKContext, useNSFWList } from 'hooks' -import { useEffect, useRef, useState } from 'react' -import { useParams } from 'react-router-dom' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useParams, useSearchParams } from 'react-router-dom' import { FilterOptions, ModDetails, @@ -45,8 +46,60 @@ export const GamePage = () => { const userState = useAppSelector((state) => state.user) - const filteredMods = useFilteredMods( - mods, + // Search + const searchTermRef = useRef(null) + const [searchParams, setSearchParams] = useSearchParams() + const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '') + + const handleSearch = () => { + const value = searchTermRef.current?.value || '' // Access the input value from the ref + setSearchTerm(value) + + if (value) { + searchParams.set('q', value) + } else { + searchParams.delete('q') + } + + setSearchParams(searchParams, { + replace: true + }) + } + + // Handle "Enter" key press inside the input + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + handleSearch() + } + } + + const filteredMods = useMemo(() => { + const filterSourceFn = (mod: ModDetails) => { + if (filterOptions.source === window.location.host) { + return mod.rTag === filterOptions.source + } + return true + } + + // If search term is missing, only filter by sources + if (searchTerm === '') return mods.filter(filterSourceFn) + + 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).filter(filterSourceFn) + }, [filterOptions.source, mods, searchTerm]) + + const filteredModList = useFilteredMods( + filteredMods, userState, filterOptions, nsfwList, @@ -54,11 +107,11 @@ export const GamePage = () => { ) // Pagination logic - const totalGames = filteredMods.length + const totalGames = filteredModList.length const totalPages = Math.ceil(totalGames / MAX_MODS_PER_PAGE) const startIndex = (currentPage - 1) * MAX_MODS_PER_PAGE const endIndex = startIndex + MAX_MODS_PER_PAGE - const currentMods = filteredMods.slice(startIndex, endIndex) + const currentMods = filteredModList.slice(startIndex, endIndex) const handlePageChange = (page: number) => { if (page >= 1 && page <= totalPages) { @@ -116,8 +169,21 @@ export const GamePage = () => { {gameName} + {searchTerm !== '' && ( + <> +  —  + + {searchTerm} + + + )} + Date: Tue, 29 Oct 2024 09:39:30 +0100 Subject: [PATCH 2/5] refactor: use SearchInput, search params to q, kind --- src/pages/games.tsx | 38 +++++---------------- src/pages/mods.tsx | 39 +++++----------------- src/pages/search.tsx | 79 ++++++++++++++++++-------------------------- 3 files changed, 49 insertions(+), 107 deletions(-) diff --git a/src/pages/games.tsx b/src/pages/games.tsx index 08d576e..2b77349 100644 --- a/src/pages/games.tsx +++ b/src/pages/games.tsx @@ -9,6 +9,7 @@ import '../styles/styles.css' import { createSearchParams, useNavigate } from 'react-router-dom' import { appRoutes } from 'routes' import { scrollIntoView } from 'utils' +import { SearchInput } from 'components/SearchInput' export const GamesPage = () => { const scrollTargetRef = useRef(null) @@ -74,8 +75,8 @@ export const GamesPage = () => { const value = searchTermRef.current?.value || '' // Access the input value from the ref if (value !== '') { const searchParams = createSearchParams({ - searchTerm: value, - searching: 'Games' + q: value, + kind: 'Games' }) navigate({ pathname: appRoutes.search, search: `?${searchParams}` }) } @@ -100,34 +101,11 @@ export const GamesPage = () => {

Games

-
-
-
- - -
-
-
+
diff --git a/src/pages/mods.tsx b/src/pages/mods.tsx index bc217aa..a5115f7 100644 --- a/src/pages/mods.tsx +++ b/src/pages/mods.tsx @@ -25,6 +25,7 @@ import { SortBy } from '../types' import { scrollIntoView } from 'utils' +import { SearchInput } from 'components/SearchInput' export const ModsPage = () => { const scrollTargetRef = useRef(null) @@ -146,8 +147,8 @@ const PageTitleRow = React.memo(() => { const value = searchTermRef.current?.value || '' // Access the input value from the ref if (value !== '') { const searchParams = createSearchParams({ - searchTerm: value, - searching: 'Mods' + q: value, + kind: 'Mods' }) navigate({ pathname: appRoutes.search, search: `?${searchParams}` }) } @@ -166,35 +167,11 @@ const PageTitleRow = React.memo(() => {

Mods

-
-
-
- - - -
-
-
+
) diff --git a/src/pages/search.tsx b/src/pages/search.tsx index ca31395..47022d2 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -13,6 +13,7 @@ import { ModCard } from 'components/ModCard' import { ModFilter } from 'components/ModsFilter' import { Pagination } from 'components/Pagination' import { Profile } from 'components/ProfileSection' +import { SearchInput } from 'components/SearchInput' import { MAX_GAMES_PER_PAGE, MAX_MODS_PER_PAGE, @@ -59,15 +60,14 @@ enum SearchKindEnum { export const SearchPage = () => { const scrollTargetRef = useRef(null) - const [searchParams] = useSearchParams() + const [searchParams, setSearchParams] = useSearchParams() const muteLists = useMuteLists() const nsfwList = useNSFWList() const searchTermRef = useRef(null) - const [searchKind, setSearchKind] = useState( - (searchParams.get('searching') as SearchKindEnum) || SearchKindEnum.Mods - ) + const searchKind = + (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods const [filterOptions, setFilterOptions] = useState({ sort: SortBy.Latest, @@ -76,13 +76,21 @@ export const SearchPage = () => { moderated: ModeratedFilter.Moderated }) - const [searchTerm, setSearchTerm] = useState( - searchParams.get('searchTerm') || '' - ) + const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '') const handleSearch = () => { const value = searchTermRef.current?.value || '' // Access the input value from the ref setSearchTerm(value) + + if (value) { + searchParams.set('q', value) + } else { + searchParams.delete('q') + } + + setSearchParams(searchParams, { + replace: true + }) } // Handle "Enter" key press inside the input @@ -109,41 +117,16 @@ export const SearchPage = () => { -
-
-
- - -
-
-
+ {searchKind === SearchKindEnum.Mods && ( { type FiltersProps = { filterOptions: FilterOptions setFilterOptions: Dispatch> - searchKind: SearchKindEnum - setSearchKind: Dispatch> } const Filters = React.memo( - ({ - filterOptions, - setFilterOptions, - searchKind, - setSearchKind - }: FiltersProps) => { + ({ filterOptions, setFilterOptions }: FiltersProps) => { const userState = useAppSelector((state) => state.user) + const [searchParams, setSearchParams] = useSearchParams() + const searchKind = + (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods + const handleChangeSearchKind = (kind: SearchKindEnum) => { + searchParams.set('kind', kind) + setSearchParams(searchParams, { + replace: true + }) + } return (
@@ -252,7 +237,7 @@ const Filters = React.memo(
setSearchKind(item)} + onClick={() => handleChangeSearchKind(item)} > {item}
@@ -324,6 +309,7 @@ const ModsResult = ({ }, [searchTerm]) const filteredMods = useMemo(() => { + // Search page requires search term if (searchTerm === '') return [] const lowerCaseSearchTerm = searchTerm.toLowerCase() @@ -338,6 +324,7 @@ const ModsResult = ({ ) > -1 const filterSourceFn = (mod: ModDetails) => { + // Filter by source if selected if (filterOptions.source === window.location.host) { return mod.rTag === filterOptions.source } From 6e07f4b8be564da4ff7a1c4b98804ac7c6af9ac8 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 29 Oct 2024 13:21:12 +0100 Subject: [PATCH 3/5] feat(filter): remember filters, add localstorage hook and utils --- src/hooks/index.ts | 1 + src/hooks/useLocalStorage.tsx | 50 +++++++++++++++++++++++++++++++++++ src/utils/consts.ts | 8 ++++++ src/utils/index.ts | 2 ++ src/utils/localStorage.ts | 32 ++++++++++++++++++++++ 5 files changed, 93 insertions(+) create mode 100644 src/hooks/useLocalStorage.tsx create mode 100644 src/utils/consts.ts create mode 100644 src/utils/localStorage.ts diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 2148b14..3daf9f4 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -7,3 +7,4 @@ export * from './useNSFWList' export * from './useReactions' export * from './useNDKContext' export * from './useScrollDisable' +export * from './useLocalStorage' diff --git a/src/hooks/useLocalStorage.tsx b/src/hooks/useLocalStorage.tsx new file mode 100644 index 0000000..8dc9893 --- /dev/null +++ b/src/hooks/useLocalStorage.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import { + getLocalStorageItem, + removeLocalStorageItem, + setLocalStorageItem +} from 'utils' + +const useLocalStorageSubscribe = (callback: () => void) => { + window.addEventListener('storage', callback) + return () => window.removeEventListener('storage', callback) +} + +export function useLocalStorage( + key: string, + initialValue: T +): [T, React.Dispatch>] { + const getSnapshot = () => getLocalStorageItem(key, initialValue) + + const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot) + + const setState: React.Dispatch> = React.useCallback( + (v: React.SetStateAction) => { + try { + const nextState = + typeof v === 'function' + ? (v as (prevState: T) => T)(JSON.parse(data)) + : v + + if (nextState === undefined || nextState === null) { + removeLocalStorageItem(key) + } else { + setLocalStorageItem(key, JSON.stringify(nextState)) + } + } catch (e) { + console.warn(e) + } + }, + [key, data] + ) + + React.useEffect(() => { + // Set local storage only when it's empty + const data = window.localStorage.getItem(key) + if (data === null) { + setLocalStorageItem(key, JSON.stringify(initialValue)) + } + }, [key, initialValue]) + + return [JSON.parse(data) as T, setState] +} diff --git a/src/utils/consts.ts b/src/utils/consts.ts new file mode 100644 index 0000000..ff8e47b --- /dev/null +++ b/src/utils/consts.ts @@ -0,0 +1,8 @@ +import { FilterOptions, SortBy, NSFWFilter, ModeratedFilter } from 'types' + +export const DEFAULT_FILTER_OPTIONS: FilterOptions = { + sort: SortBy.Latest, + nsfw: NSFWFilter.Hide_NSFW, + source: window.location.host, + moderated: ModeratedFilter.Moderated +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 35ad5bc..06e7ca4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -3,3 +3,5 @@ export * from './nostr' export * from './url' export * from './utils' export * from './zap' +export * from './localStorage' +export * from './consts' diff --git a/src/utils/localStorage.ts b/src/utils/localStorage.ts new file mode 100644 index 0000000..7d3e8aa --- /dev/null +++ b/src/utils/localStorage.ts @@ -0,0 +1,32 @@ +export function getLocalStorageItem(key: string, defaultValue: T): string { + try { + const data = window.localStorage.getItem(key) + if (data === null) return JSON.stringify(defaultValue) + return data + } catch (err) { + console.error(`Error while fetching local storage value: `, err) + return JSON.stringify(defaultValue) + } +} + +export function setLocalStorageItem(key: string, value: string) { + try { + window.localStorage.setItem(key, value) + dispatchLocalStorageEvent(key, value) + } catch (err) { + console.error(`Error while saving local storage value: `, err) + } +} + +export function removeLocalStorageItem(key: string) { + try { + window.localStorage.removeItem(key) + dispatchLocalStorageEvent(key, null) + } catch (err) { + console.error(`Error while deleting local storage value: `, err) + } +} + +function dispatchLocalStorageEvent(key: string, newValue: string | null) { + window.dispatchEvent(new StorageEvent('storage', { key, newValue })) +} From efad0f44f5e440f4741872fbfbbfcac9c81f39f0 Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 29 Oct 2024 13:38:13 +0100 Subject: [PATCH 4/5] refactor: use filter storage state, separate profile page filter --- src/components/ModsFilter.tsx | 18 ++-- src/hooks/useFilteredMods.ts | 7 +- src/pages/game.tsx | 29 +++--- src/pages/mods.tsx | 27 ++--- src/pages/profile.tsx | 28 ++--- src/pages/search.tsx | 186 +++++++++++++++------------------- src/types/modsFilter.ts | 1 - 7 files changed, 126 insertions(+), 170 deletions(-) diff --git a/src/components/ModsFilter.tsx b/src/components/ModsFilter.tsx index 755ff95..292f3a6 100644 --- a/src/components/ModsFilter.tsx +++ b/src/components/ModsFilter.tsx @@ -1,16 +1,20 @@ -import { useAppSelector } from 'hooks' +import { useAppSelector, useLocalStorage } from 'hooks' import React from 'react' -import { Dispatch, SetStateAction } from 'react' import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types' +import { DEFAULT_FILTER_OPTIONS } from 'utils' type Props = { - filterOptions: FilterOptions - setFilterOptions: Dispatch> + author?: string | undefined + filterKey?: string | undefined } export const ModFilter = React.memo( - ({ filterOptions, setFilterOptions }: Props) => { + ({ author, filterKey = 'filter' }: Props) => { const userState = useAppSelector((state) => state.user) + const [filterOptions, setFilterOptions] = useLocalStorage( + filterKey, + DEFAULT_FILTER_OPTIONS + ) return (
@@ -62,9 +66,9 @@ export const ModFilter = React.memo( import.meta.env.VITE_REPORTING_NPUB const isOwnProfile = - filterOptions.author && + author && userState.auth && - userState.user?.pubkey === filterOptions.author + userState.user?.pubkey === author if (!(isAdmin || isOwnProfile)) return null } diff --git a/src/hooks/useFilteredMods.ts b/src/hooks/useFilteredMods.ts index 7514be7..7f1da40 100644 --- a/src/hooks/useFilteredMods.ts +++ b/src/hooks/useFilteredMods.ts @@ -18,7 +18,8 @@ export const useFilteredMods = ( muteLists: { admin: MuteLists user: MuteLists - } + }, + author?: string | undefined ) => { return useMemo(() => { const nsfwFilter = (mods: ModDetails[]) => { @@ -50,7 +51,7 @@ export const useFilteredMods = ( const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB const isOwner = userState.user?.npub && - npubToHex(userState.user.npub as string) === filterOptions.author + npubToHex(userState.user.npub as string) === author const isUnmoderatedFully = filterOptions.moderated === ModeratedFilter.Unmoderated_Fully @@ -84,7 +85,7 @@ export const useFilteredMods = ( filterOptions.sort, filterOptions.moderated, filterOptions.nsfw, - filterOptions.author, + author, mods, muteLists, nsfwList diff --git a/src/pages/game.tsx b/src/pages/game.tsx index 62c65ba..6f82493 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -11,20 +11,20 @@ import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts' import { useAppSelector, useFilteredMods, + useLocalStorage, useMuteLists, useNDKContext, useNSFWList } from 'hooks' import { useEffect, useMemo, useRef, useState } from 'react' import { useParams, useSearchParams } from 'react-router-dom' +import { FilterOptions, ModDetails } from 'types' import { - FilterOptions, - ModDetails, - ModeratedFilter, - NSFWFilter, - SortBy -} from 'types' -import { extractModData, isModDataComplete, scrollIntoView } from 'utils' + DEFAULT_FILTER_OPTIONS, + extractModData, + isModDataComplete, + scrollIntoView +} from 'utils' export const GamePage = () => { const scrollTargetRef = useRef(null) @@ -34,12 +34,10 @@ export const GamePage = () => { const muteLists = useMuteLists() const nsfwList = useNSFWList() - const [filterOptions, setFilterOptions] = useState({ - sort: SortBy.Latest, - nsfw: NSFWFilter.Hide_NSFW, - source: window.location.host, - moderated: ModeratedFilter.Moderated - }) + const [filterOptions] = useLocalStorage( + 'filter', + DEFAULT_FILTER_OPTIONS + ) const [mods, setMods] = useState([]) const [currentPage, setCurrentPage] = useState(1) @@ -186,10 +184,7 @@ export const GamePage = () => { />
- +
{currentMods.map((mod) => ( diff --git a/src/pages/mods.tsx b/src/pages/mods.tsx index a5115f7..31dd301 100644 --- a/src/pages/mods.tsx +++ b/src/pages/mods.tsx @@ -8,6 +8,7 @@ import { MOD_FILTER_LIMIT } from '../constants' import { useAppSelector, useFilteredMods, + useLocalStorage, useMuteLists, useNDKContext, useNSFWList @@ -17,14 +18,8 @@ import '../styles/filters.css' import '../styles/pagination.css' import '../styles/search.css' import '../styles/styles.css' -import { - FilterOptions, - ModDetails, - ModeratedFilter, - NSFWFilter, - SortBy -} from '../types' -import { scrollIntoView } from 'utils' +import { FilterOptions, ModDetails } from '../types' +import { DEFAULT_FILTER_OPTIONS, scrollIntoView } from 'utils' import { SearchInput } from 'components/SearchInput' export const ModsPage = () => { @@ -32,12 +27,11 @@ export const ModsPage = () => { const { fetchMods } = useNDKContext() const [isFetching, setIsFetching] = useState(false) const [mods, setMods] = useState([]) - const [filterOptions, setFilterOptions] = useState({ - sort: SortBy.Latest, - nsfw: NSFWFilter.Hide_NSFW, - source: window.location.host, - moderated: ModeratedFilter.Moderated - }) + + const [filterOptions] = useLocalStorage( + 'filter', + DEFAULT_FILTER_OPTIONS + ) const muteLists = useMuteLists() const nsfwList = useNSFWList() @@ -113,10 +107,7 @@ export const ModsPage = () => { ref={scrollTargetRef} > - +
diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index b69f597..0a1965b 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -9,6 +9,7 @@ import { MOD_FILTER_LIMIT } from '../constants' import { useAppSelector, useFilteredMods, + useLocalStorage, useMuteLists, useNDKContext, useNSFWList @@ -18,16 +19,10 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useParams, Navigate, Link } from 'react-router-dom' import { toast } from 'react-toastify' import { appRoutes, getProfilePageRoute } from 'routes' -import { - FilterOptions, - ModDetails, - ModeratedFilter, - NSFWFilter, - SortBy, - UserRelaysType -} from 'types' +import { FilterOptions, ModDetails, UserRelaysType } from 'types' import { copyTextToClipboard, + DEFAULT_FILTER_OPTIONS, now, npubToHex, scrollIntoView, @@ -230,12 +225,9 @@ export const ProfilePage = () => { // Mods const [mods, setMods] = useState([]) - const [filterOptions, setFilterOptions] = useState({ - sort: SortBy.Latest, - nsfw: NSFWFilter.Hide_NSFW, - source: window.location.host, - moderated: ModeratedFilter.Moderated, - author: profilePubkey + const filterKey = 'filter-profile' + const [filterOptions] = useLocalStorage(filterKey, { + ...DEFAULT_FILTER_OPTIONS }) const muteLists = useMuteLists() const nsfwList = useNSFWList() @@ -304,7 +296,8 @@ export const ProfilePage = () => { userState, filterOptions, nsfwList, - muteLists + muteLists, + profilePubkey ) // Redirect route @@ -470,10 +463,7 @@ export const ProfilePage = () => { {/* Tabs Content */} {tab === 0 && ( <> - +
{filteredModList.map((mod) => ( diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 47022d2..935b8e3 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -23,28 +23,16 @@ import { useAppSelector, useFilteredMods, useGames, + useLocalStorage, useMuteLists, useNDKContext, useNSFWList } from 'hooks' -import React, { - Dispatch, - SetStateAction, - useEffect, - useMemo, - useRef, - useState -} from 'react' +import React, { useEffect, useMemo, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' +import { FilterOptions, ModDetails, ModeratedFilter, MuteLists } from 'types' import { - FilterOptions, - ModDetails, - ModeratedFilter, - MuteLists, - NSFWFilter, - SortBy -} from 'types' -import { + DEFAULT_FILTER_OPTIONS, extractModData, isModDataComplete, log, @@ -69,12 +57,10 @@ export const SearchPage = () => { const searchKind = (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods - const [filterOptions, setFilterOptions] = useState({ - sort: SortBy.Latest, - nsfw: NSFWFilter.Hide_NSFW, - source: window.location.host, - moderated: ModeratedFilter.Moderated - }) + const [filterOptions] = useLocalStorage( + 'filter', + DEFAULT_FILTER_OPTIONS + ) const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '') @@ -124,10 +110,7 @@ export const SearchPage = () => { />
- + {searchKind === SearchKindEnum.Mods && ( { ) } -type FiltersProps = { - filterOptions: FilterOptions - setFilterOptions: Dispatch> -} +const Filters = React.memo(() => { + const [filterOptions, setFilterOptions] = useLocalStorage( + 'filter', + DEFAULT_FILTER_OPTIONS + ) -const Filters = React.memo( - ({ filterOptions, setFilterOptions }: FiltersProps) => { - const userState = useAppSelector((state) => state.user) - const [searchParams, setSearchParams] = useSearchParams() - const searchKind = - (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods - const handleChangeSearchKind = (kind: SearchKindEnum) => { - searchParams.set('kind', kind) - setSearchParams(searchParams, { - replace: true - }) - } + const userState = useAppSelector((state) => state.user) + const [searchParams, setSearchParams] = useSearchParams() + const searchKind = + (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods + const handleChangeSearchKind = (kind: SearchKindEnum) => { + searchParams.set('kind', kind) + setSearchParams(searchParams, { + replace: true + }) + } - 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} -
- ) - })} -
-
-
- )} + return ( +
+
+ {searchKind === SearchKindEnum.Mods && } + {searchKind === SearchKindEnum.Users && (
- {Object.values(SearchKindEnum).map((item, index) => ( -
handleChangeSearchKind(item)} - > - {item} -
- ))} + {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) => ( +
handleChangeSearchKind(item)} + > + {item} +
+ ))} +
+
- ) - } -) +
+ ) +}) type ModsResultProps = { filterOptions: FilterOptions diff --git a/src/types/modsFilter.ts b/src/types/modsFilter.ts index a10542d..8d724ec 100644 --- a/src/types/modsFilter.ts +++ b/src/types/modsFilter.ts @@ -22,5 +22,4 @@ export interface FilterOptions { nsfw: NSFWFilter source: string moderated: ModeratedFilter - author?: string } From 0ee3dba9064924a79b0386458805f19fd8c39cde Mon Sep 17 00:00:00 2001 From: enes Date: Tue, 29 Oct 2024 15:44:41 +0100 Subject: [PATCH 5/5] fix: user search Closes #78 --- src/components/ProfileSection.tsx | 39 +++++++++++++---------- src/pages/profile.tsx | 4 ++- src/pages/search.tsx | 52 ++++++++++++++++++------------- src/pages/settings/profile.tsx | 16 +++++----- 4 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index aa1b956..d6f8a4f 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -116,14 +116,20 @@ export const Profile = ({ pubkey }: ProfileProps) => { }) } + // Try to encode let profileRoute = appRoutes.home - const hexPubkey = npubToHex(pubkey) - if (hexPubkey) { - profileRoute = getProfilePageRoute( - nip19.nprofileEncode({ - pubkey: hexPubkey - }) - ) + let nprofile: string | undefined + try { + const hexPubkey = npubToHex(pubkey) + nprofile = hexPubkey + ? nip19.nprofileEncode({ + pubkey: hexPubkey + }) + : undefined + profileRoute = nprofile ? getProfilePageRoute(nprofile) : appRoutes.home + } catch (error) { + // Silently ignore and redirect to home + log(true, LogType.Error, 'Failed to encode profile.', error) } return ( @@ -148,7 +154,8 @@ export const Profile = ({ pubkey }: ProfileProps) => {

{displayName}

- {nip05 && ( + {/* Nip05 can sometimes be an empty object '{}' which causes the error */} + {typeof nip05 === 'string' && (

{nip05}

)}
@@ -181,8 +188,12 @@ export const Profile = ({ pubkey }: ProfileProps) => {
- - {lud16 && } + {typeof nprofile !== 'undefined' && ( + + )} + {typeof lud16 !== 'undefined' && ( + + )}
@@ -227,20 +238,16 @@ const posts: Post[] = [ ] type QRButtonWithPopUpProps = { - pubkey: string + nprofile: string } export const ProfileQRButtonWithPopUp = ({ - pubkey + nprofile }: QRButtonWithPopUpProps) => { const [isOpen, setIsOpen] = useState(false) useBodyScrollDisable(isOpen) - const nprofile = nip19.nprofileEncode({ - pubkey - }) - const onQrCodeClicked = async () => { const href = `https://njump.me/${nprofile}` const a = document.createElement('a') diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 0a1965b..84e2293 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -23,6 +23,8 @@ import { FilterOptions, ModDetails, UserRelaysType } from 'types' import { copyTextToClipboard, DEFAULT_FILTER_OPTIONS, + log, + LogType, now, npubToHex, scrollIntoView, @@ -42,8 +44,8 @@ export const ProfilePage = () => { : undefined profilePubkey = value?.data.pubkey } catch (error) { - // Failed to decode the nprofile // Silently ignore and redirect to home or logged in user + log(true, LogType.Error, 'Failed to decode nprofile.', error) } const scrollTargetRef = useRef(null) diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 935b8e3..609e5cc 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -37,7 +37,8 @@ import { isModDataComplete, log, LogType, - scrollIntoView + scrollIntoView, + timeout } from 'utils' enum SearchKindEnum { @@ -373,29 +374,38 @@ const UsersResult = ({ if (searchTerm === '') { setProfiles([]) } else { - const filter: NDKFilter = { - kinds: [NDKKind.Metadata], - search: searchTerm + const fetchProfiles = async () => { + setIsFetching(true) + + const filter: NDKFilter = { + kinds: [NDKKind.Metadata], + search: searchTerm + } + + const profiles = await Promise.race([ + fetchEvents(filter), + timeout(10 * 1000) + ]) + .then((events) => { + const results = events.map((event) => { + const ndkEvent = new NDKEvent(undefined, event) + const profile = profileFromEvent(ndkEvent) + return profile + }) + return results + }) + .catch((err) => { + log(true, LogType.Error, 'An error occurred in fetching users', err) + return [] + }) + + setProfiles(profiles) + setIsFetching(false) } - setIsFetching(true) - fetchEvents(filter) - .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) - }) + fetchProfiles() } - }, [searchTerm, fetchEvents]) + }, [fetchEvents, searchTerm]) const filteredProfiles = useMemo(() => { let filtered = [...profiles] diff --git a/src/pages/settings/profile.tsx b/src/pages/settings/profile.tsx index 907d0f0..b553995 100644 --- a/src/pages/settings/profile.tsx +++ b/src/pages/settings/profile.tsx @@ -98,15 +98,15 @@ export const ProfileSettings = () => { // In case user is not logged in clicking on profile link will navigate to homepage let profileRoute = appRoutes.home + let nprofile: string | undefined if (userState.auth && userState.user) { const hexPubkey = npubToHex(userState.user.npub as string) if (hexPubkey) { - profileRoute = getProfilePageRoute( - nip19.nprofileEncode({ - pubkey: hexPubkey - }) - ) + nprofile = nip19.nprofileEncode({ + pubkey: hexPubkey + }) + profileRoute = getProfilePageRoute(nprofile) } } @@ -247,10 +247,8 @@ export const ProfileSettings = () => {
- {typeof userState.user?.pubkey === 'string' && ( - + {typeof nprofile !== 'undefined' && ( + )}