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/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/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/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/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/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/pages/game.tsx b/src/pages/game.tsx index 7b94d74..6f82493 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -6,24 +6,25 @@ 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, useFilteredMods, + useLocalStorage, useMuteLists, 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 } 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) @@ -33,20 +34,70 @@ 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) 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 +105,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,14 +167,24 @@ export const GamePage = () => { {gameName} + {searchTerm !== '' && ( + <> +  —  + + {searchTerm} + + + )} + - +
{currentMods.map((mod) => ( 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..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,26 +18,20 @@ 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 = () => { const scrollTargetRef = useRef(null) 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() @@ -112,10 +107,7 @@ export const ModsPage = () => { ref={scrollTargetRef} > - +
@@ -146,8 +138,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 +158,11 @@ const PageTitleRow = React.memo(() => {

Mods

-
-
-
- - - -
-
-
+
) diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index b69f597..84e2293 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,12 @@ 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, + log, + LogType, now, npubToHex, scrollIntoView, @@ -47,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) @@ -230,12 +227,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 +298,8 @@ export const ProfilePage = () => { userState, filterOptions, nsfwList, - muteLists + muteLists, + profilePubkey ) // Redirect route @@ -470,10 +465,7 @@ export const ProfilePage = () => { {/* Tabs Content */} {tab === 0 && ( <> - +
{filteredModList.map((mod) => ( diff --git a/src/pages/search.tsx b/src/pages/search.tsx index ca31395..609e5cc 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, @@ -22,33 +23,22 @@ 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, LogType, - scrollIntoView + scrollIntoView, + timeout } from 'utils' enum SearchKindEnum { @@ -59,30 +49,35 @@ 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] = useLocalStorage( + 'filter', + DEFAULT_FILTER_OPTIONS ) - 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 [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,42 +104,14 @@ export const SearchPage = () => {
-
-
-
- - -
-
-
+
- + {searchKind === SearchKindEnum.Mods && ( { ) } -type FiltersProps = { - filterOptions: FilterOptions - setFilterOptions: Dispatch> - searchKind: SearchKindEnum - setSearchKind: Dispatch> -} +const Filters = React.memo(() => { + const [filterOptions, setFilterOptions] = useLocalStorage( + 'filter', + DEFAULT_FILTER_OPTIONS + ) -const Filters = React.memo( - ({ - filterOptions, - setFilterOptions, - searchKind, - setSearchKind - }: FiltersProps) => { - const userState = useAppSelector((state) => state.user) + 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) => ( -
setSearchKind(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 @@ -324,6 +286,7 @@ const ModsResult = ({ }, [searchTerm]) const filteredMods = useMemo(() => { + // Search page requires search term if (searchTerm === '') return [] const lowerCaseSearchTerm = searchTerm.toLowerCase() @@ -338,6 +301,7 @@ const ModsResult = ({ ) > -1 const filterSourceFn = (mod: ModDetails) => { + // Filter by source if selected if (filterOptions.source === window.location.host) { return mod.rTag === filterOptions.source } @@ -410,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' && ( + )}
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 } 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 })) +}