Compare commits

...

22 Commits

Author SHA1 Message Date
12b2fc1627 Merge pull request 'user profile btn in social nav now only is active for current logged in user, added NSFW tag for admin tagged ones, mod search under a specific game, search term and some filters added to url, filter state is saved locally in cache, user search now works' (#108) from staging into master
Reviewed-on: #108
2024-10-30 16:00:24 +00:00
enes
4eb8c7d653 fix: use subscription for user search
All checks were successful
Release to Staging / build_and_release (push) Successful in 38s
2024-10-30 14:59:44 +01:00
43c8ae4066 Update src/styles/cardMod.css
All checks were successful
Release to Staging / build_and_release (push) Successful in 37s
2024-10-30 13:20:24 +00:00
49c1168bb7 Update src/styles/tags.css
All checks were successful
Release to Staging / build_and_release (push) Successful in 39s
2024-10-30 13:17:46 +00:00
69768388e4 Upload files to "src/assets/games"
All checks were successful
Release to Staging / build_and_release (push) Successful in 42s
2024-10-30 10:00:46 +00:00
80172aee07 Update package.json
All checks were successful
Release to Staging / build_and_release (push) Successful in 42s
2024-10-29 15:45:05 +00:00
enes
0f6cd4a9bd chore: trigger release
All checks were successful
Release to Staging / build_and_release (push) Successful in 40s
2024-10-29 16:28:25 +01:00
enes
35fdf2c8b7 chore: trigger release
All checks were successful
Release to Staging / build_and_release (push) Successful in 46s
2024-10-29 16:23:54 +01:00
e41ce32ef2 chore(git): merge pull request #105 from 52-game-page-search into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 42s
Reviewed-on: #105
2024-10-29 14:48:29 +00:00
enes
0ee3dba906 fix: user search
Closes #78
2024-10-29 15:44:41 +01:00
enes
efad0f44f5 refactor: use filter storage state, separate profile page filter 2024-10-29 13:38:13 +01:00
enes
6e07f4b8be feat(filter): remember filters, add localstorage hook and utils 2024-10-29 13:21:12 +01:00
enes
7640bdd53b refactor: use SearchInput, search params to q, kind 2024-10-29 09:39:30 +01:00
enes
72252d416b feat: mod search on game page 2024-10-29 09:35:39 +01:00
enes
6e4fa104c0 fix(search): add mods source filter fn
All checks were successful
Release to Staging / build_and_release (push) Successful in 41s
Closes #77
2024-10-28 14:49:36 +01:00
f2f80a36c6 chore(git): merge pull request #103 from 96-nsfw-list-tag into staging
All checks were successful
Release to Staging / build_and_release (push) Successful in 45s
Reviewed-on: #103
2024-10-28 13:02:54 +00:00
enes
4f4e3a7c85 fix(mod-card): add nsfw tag if mod is in nsfwList, while filtering 2024-10-28 14:00:44 +01:00
enes
0b1d43eac4 fix(mod-page): mark mod as nsfw if found in nsfwList 2024-10-28 13:46:46 +01:00
enes
2dc0ab6cf4 fix(profile): hide block on own profile
All checks were successful
Release to Staging / build_and_release (push) Successful in 39s
2024-10-28 13:10:40 +01:00
enes
15af98359d fix(profile): unblock tag filter
All checks were successful
Release to Staging / build_and_release (push) Successful in 44s
2024-10-28 13:08:47 +01:00
enes
3906c70bc9 fix(profile): rerender after profile link changes
All checks were successful
Release to Staging / build_and_release (push) Successful in 41s
Add useProfile hook, closes #99
2024-10-28 12:43:26 +01:00
enes
9341cd6544 fix(socialNav): active user state icon
All checks were successful
Release to Staging / build_and_release (push) Successful in 41s
Closes #100
2024-10-28 10:21:12 +01:00
23 changed files with 50557 additions and 50385 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "degmods.com", "name": "degmods.com",
"private": true, "private": true,
"version": "0.0.0", "version": "0.0.0-alpha",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -7909,7 +7909,7 @@ Lamborghini R6 125,,
Farming Simulator 2013 Ursus,, Farming Simulator 2013 Ursus,,
Cubemen Soundtrack,, Cubemen Soundtrack,,
Nancy Drew: The Deadly Device,, Nancy Drew: The Deadly Device,,
DmC Devil May Cry,, DmC Devil May Cry,,https://image.nostr.build/917da143fafe8a003865ec4dbbb872dcb04020fad2ca8d3c6cee00f2ac141bde.jpg
Cargo Commander,, Cargo Commander,,
Fairy Bloom Freesia Demo,, Fairy Bloom Freesia Demo,,
Football Manager 2013 Russian Demo,, Football Manager 2013 Russian Demo,,
@ -9057,7 +9057,7 @@ Hacker Evolution Duality Hardcore Package 1,,
MC6T - Cakewalk Expansion Pack - Modern Strings,, MC6T - Cakewalk Expansion Pack - Modern Strings,,
MC6T - Cakewalk Expansion Pack - Guitars,, MC6T - Cakewalk Expansion Pack - Guitars,,
Sorcerer King,, Sorcerer King,,
Assassin's Creed IV Black Flag,, Assassin's Creed IV Black Flag,,https://image.nostr.build/6e468d37073f922b9442e03a9428e10425032dbae19a920316ebc32520c10713.jpg
Joe Danger 2: The Movie,, Joe Danger 2: The Movie,,
Joe Danger 2: Undead Movie Pack,, Joe Danger 2: Undead Movie Pack,,
Vector Thrust,, Vector Thrust,,
@ -39626,7 +39626,7 @@ Red Bull 360: Get the ultimate 360 video experience of drifting,,
8Doors: Arum's Afterlife Adventure,, 8Doors: Arum's Afterlife Adventure,,
Thomaz,, Thomaz,,
Jack & the Cat,, Jack & the Cat,,
Atomic Heart,, Atomic Heart,,https://image.nostr.build/19cf6181c271a6e2d56dd275600a29b797569f3e054a162bae63ca46c255e772.jpg
Omen Exitio: Plague,, Omen Exitio: Plague,,
Pixelum,, Pixelum,,
Wars of Seignior,, Wars of Seignior,,

Can't render this file because it is too large.

View File

@ -1,16 +1,20 @@
import { useAppSelector } from 'hooks' import { useAppSelector, useLocalStorage } from 'hooks'
import React from 'react' import React from 'react'
import { Dispatch, SetStateAction } from 'react'
import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types' import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types'
import { DEFAULT_FILTER_OPTIONS } from 'utils'
type Props = { type Props = {
filterOptions: FilterOptions author?: string | undefined
setFilterOptions: Dispatch<SetStateAction<FilterOptions>> filterKey?: string | undefined
} }
export const ModFilter = React.memo( export const ModFilter = React.memo(
({ filterOptions, setFilterOptions }: Props) => { ({ author, filterKey = 'filter' }: Props) => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const [filterOptions, setFilterOptions] = useLocalStorage<FilterOptions>(
filterKey,
DEFAULT_FILTER_OPTIONS
)
return ( return (
<div className='IBMSecMain'> <div className='IBMSecMain'>
@ -62,9 +66,9 @@ export const ModFilter = React.memo(
import.meta.env.VITE_REPORTING_NPUB import.meta.env.VITE_REPORTING_NPUB
const isOwnProfile = const isOwnProfile =
filterOptions.author && author &&
userState.auth && userState.auth &&
userState.user?.pubkey === filterOptions.author userState.user?.pubkey === author
if (!(isAdmin || isOwnProfile)) return null if (!(isAdmin || isOwnProfile)) return null
} }

View File

@ -14,7 +14,7 @@ import { appRoutes, getProfilePageRoute } from '../routes'
import '../styles/author.css' import '../styles/author.css'
import '../styles/innerPage.css' import '../styles/innerPage.css'
import '../styles/socialPosts.css' import '../styles/socialPosts.css'
import { UserProfile, UserRelaysType } from '../types' import { UserRelaysType } from '../types'
import { import {
copyTextToClipboard, copyTextToClipboard,
hexToNpub, hexToNpub,
@ -27,37 +27,18 @@ import { LoadingSpinner } from './LoadingSpinner'
import { ZapPopUp } from './Zap' import { ZapPopUp } from './Zap'
import placeholder from '../assets/img/DEGMods Placeholder Img.png' import placeholder from '../assets/img/DEGMods Placeholder Img.png'
import { NDKEvent } from '@nostr-dev-kit/ndk' import { NDKEvent } from '@nostr-dev-kit/ndk'
import { useProfile } from 'hooks/useProfile'
type Props = { type Props = {
pubkey: string pubkey: string
} }
export const ProfileSection = ({ pubkey }: Props) => { export const ProfileSection = ({ pubkey }: Props) => {
const { findMetadata } = useNDKContext()
const [profile, setProfile] = useState<UserProfile>()
useDidMount(() => {
findMetadata(pubkey).then((res) => {
setProfile(res)
})
})
const displayName =
profile?.displayName || profile?.name || '[name not set up]'
const about = profile?.bio || profile?.about || '[bio not set up]'
return ( return (
<div className='IBMSMSplitMainSmallSide'> <div className='IBMSMSplitMainSmallSide'>
<div className='IBMSMSplitMainSmallSideSecWrapper'> <div className='IBMSMSplitMainSmallSideSecWrapper'>
<div className='IBMSMSplitMainSmallSideSec'> <div className='IBMSMSplitMainSmallSideSec'>
<Profile <Profile pubkey={pubkey} />
pubkey={pubkey}
displayName={displayName}
about={about}
image={profile?.image}
nip05={profile?.nip05}
lud16={profile?.lud16}
/>
</div> </div>
<div className='IBMSMSplitMainSmallSideSec'> <div className='IBMSMSplitMainSmallSideSec'>
<div className='IBMSMSMSSS_ShortPosts'> <div className='IBMSMSMSSS_ShortPosts'>
@ -109,21 +90,18 @@ export const ProfileSection = ({ pubkey }: Props) => {
type ProfileProps = { type ProfileProps = {
pubkey: string pubkey: string
displayName: string
about: string
image?: string
nip05?: string
lud16?: string
} }
export const Profile = ({ export const Profile = ({ pubkey }: ProfileProps) => {
pubkey, const profile = useProfile(pubkey)
displayName,
about, const displayName =
image, profile?.displayName || profile?.name || '[name not set up]'
nip05, const about = profile?.bio || profile?.about || '[bio not set up]'
lud16 const image = profile?.image || FALLBACK_PROFILE_IMAGE
}: ProfileProps) => { const nip05 = profile?.nip05
const lud16 = profile?.lud16
const npub = hexToNpub(pubkey) const npub = hexToNpub(pubkey)
const handleCopy = async () => { const handleCopy = async () => {
@ -138,14 +116,20 @@ export const Profile = ({
}) })
} }
// Try to encode
let profileRoute = appRoutes.home let profileRoute = appRoutes.home
const hexPubkey = npubToHex(pubkey) let nprofile: string | undefined
if (hexPubkey) { try {
profileRoute = getProfilePageRoute( const hexPubkey = npubToHex(pubkey)
nip19.nprofileEncode({ nprofile = hexPubkey
pubkey: 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 ( return (
@ -162,9 +146,7 @@ export const Profile = ({
<div <div
className='IBMSMSMSSS_Author_Top_PP' className='IBMSMSMSSS_Author_Top_PP'
style={{ style={{
background: `url('${ background: `url('${image}') center / cover no-repeat`
image || FALLBACK_PROFILE_IMAGE
}') center / cover no-repeat`
}} }}
></div> ></div>
</div> </div>
@ -172,7 +154,8 @@ export const Profile = ({
<div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'> <div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'>
<div className='IBMSMSMSSS_Author_TopWrapper'> <div className='IBMSMSMSSS_Author_TopWrapper'>
<p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p> <p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p>
{nip05 && ( {/* Nip05 can sometimes be an empty object '{}' which causes the error */}
{typeof nip05 === 'string' && (
<p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p> <p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p>
)} )}
</div> </div>
@ -205,8 +188,12 @@ export const Profile = ({
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path> <path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
</svg> </svg>
</div> </div>
<ProfileQRButtonWithPopUp pubkey={pubkey} /> {typeof nprofile !== 'undefined' && (
{lud16 && <ZapButtonWithPopUp pubkey={pubkey} />} <ProfileQRButtonWithPopUp nprofile={nprofile} />
)}
{typeof lud16 !== 'undefined' && (
<ZapButtonWithPopUp pubkey={pubkey} />
)}
</div> </div>
</div> </div>
</div> </div>
@ -251,20 +238,16 @@ const posts: Post[] = [
] ]
type QRButtonWithPopUpProps = { type QRButtonWithPopUpProps = {
pubkey: string nprofile: string
} }
export const ProfileQRButtonWithPopUp = ({ export const ProfileQRButtonWithPopUp = ({
pubkey nprofile
}: QRButtonWithPopUpProps) => { }: QRButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
useBodyScrollDisable(isOpen) useBodyScrollDisable(isOpen)
const nprofile = nip19.nprofileEncode({
pubkey
})
const onQrCodeClicked = async () => { const onQrCodeClicked = async () => {
const href = `https://njump.me/${nprofile}` const href = `https://njump.me/${nprofile}`
const a = document.createElement('a') const a = document.createElement('a')

View File

@ -0,0 +1,39 @@
import { forwardRef } from 'react'
interface SearchInputProps {
handleKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void
handleSearch: () => void
}
export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
({ handleKeyDown, handleSearch }, ref) => (
<div className='SearchMain'>
<div className='SearchMainInside'>
<div className='SearchMainInsideWrapper'>
<input
type='text'
className='SMIWInput'
ref={ref}
onKeyDown={handleKeyDown}
placeholder='Enter search term'
/>
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div>
</div>
</div>
)
)

View File

@ -7,3 +7,4 @@ export * from './useNSFWList'
export * from './useReactions' export * from './useReactions'
export * from './useNDKContext' export * from './useNDKContext'
export * from './useScrollDisable' export * from './useScrollDisable'
export * from './useLocalStorage'

View File

@ -18,10 +18,20 @@ export const useFilteredMods = (
muteLists: { muteLists: {
admin: MuteLists admin: MuteLists
user: MuteLists user: MuteLists
} },
author?: string | undefined
) => { ) => {
return useMemo(() => { return useMemo(() => {
const nsfwFilter = (mods: ModDetails[]) => { const nsfwFilter = (mods: ModDetails[]) => {
// Add nsfw tag to mods included in nsfwList
if (filterOptions.nsfw !== NSFWFilter.Hide_NSFW) {
mods = mods.map((mod) => {
return !mod.nsfw && nsfwList.includes(mod.aTag)
? { ...mod, nsfw: true }
: mod
})
}
// Determine the filtering logic based on the NSFW filter option // Determine the filtering logic based on the NSFW filter option
switch (filterOptions.nsfw) { switch (filterOptions.nsfw) {
case NSFWFilter.Hide_NSFW: case NSFWFilter.Hide_NSFW:
@ -41,7 +51,7 @@ export const useFilteredMods = (
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isOwner = const isOwner =
userState.user?.npub && userState.user?.npub &&
npubToHex(userState.user.npub as string) === filterOptions.author npubToHex(userState.user.npub as string) === author
const isUnmoderatedFully = const isUnmoderatedFully =
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
@ -75,7 +85,7 @@ export const useFilteredMods = (
filterOptions.sort, filterOptions.sort,
filterOptions.moderated, filterOptions.moderated,
filterOptions.nsfw, filterOptions.nsfw,
filterOptions.author, author,
mods, mods,
muteLists, muteLists,
nsfwList nsfwList

View File

@ -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<T>(
key: string,
initialValue: T
): [T, React.Dispatch<React.SetStateAction<T>>] {
const getSnapshot = () => getLocalStorageItem(key, initialValue)
const data = React.useSyncExternalStore(useLocalStorageSubscribe, getSnapshot)
const setState: React.Dispatch<React.SetStateAction<T>> = React.useCallback(
(v: React.SetStateAction<T>) => {
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]
}

18
src/hooks/useProfile.tsx Normal file
View File

@ -0,0 +1,18 @@
import { useNDKContext } from 'hooks'
import { useState, useEffect } from 'react'
import { UserProfile } from 'types'
export const useProfile = (pubkey?: string) => {
const { findMetadata } = useNDKContext()
const [profile, setProfile] = useState<UserProfile>()
useEffect(() => {
if (pubkey) {
findMetadata(pubkey).then((res) => {
setProfile(res)
})
}
}, [findMetadata, pubkey])
return profile
}

View File

@ -1,13 +1,28 @@
import { useAppSelector } from 'hooks' import { useAppSelector } from 'hooks'
import { nip19 } from 'nostr-tools'
import { useState } from 'react' import { useState } from 'react'
import { NavLink, NavLinkProps } from 'react-router-dom' import { NavLink, NavLinkProps } from 'react-router-dom'
import { appRoutes, getProfilePageRoute } from 'routes' import { appRoutes, getProfilePageRoute } from 'routes'
import 'styles/socialNav.css' import 'styles/socialNav.css'
import { npubToHex } from 'utils'
export const SocialNav = () => { export const SocialNav = () => {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false) const [isCollapsed, setIsCollapsed] = useState<boolean>(false)
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
let profileRoute = ''
if (userState.auth && userState.user) {
// Redirect to user's profile is no profile is linked
const userHexKey = npubToHex(userState.user.npub as string)
if (userHexKey) {
profileRoute = getProfilePageRoute(
nip19.nprofileEncode({
pubkey: userHexKey
})
)
}
}
const toggleNav = () => { const toggleNav = () => {
setIsCollapsed(!isCollapsed) setIsCollapsed(!isCollapsed)
} }
@ -42,7 +57,7 @@ export const SocialNav = () => {
/> />
{!!userState.auth && ( {!!userState.auth && (
<NavButton <NavButton
to={getProfilePageRoute('')} to={profileRoute}
svgPath='M256 288c79.53 0 144-64.47 144-144s-64.47-144-144-144c-79.52 0-144 64.47-144 144S176.5 288 256 288zM351.1 320H160c-88.36 0-160 71.63-160 160c0 17.67 14.33 32 31.1 32H480c17.67 0 31.1-14.33 31.1-32C512 391.6 440.4 320 351.1 320z' svgPath='M256 288c79.53 0 144-64.47 144-144s-64.47-144-144-144c-79.52 0-144 64.47-144 144S176.5 288 256 288zM351.1 320H160c-88.36 0-160 71.63-160 160c0 17.67 14.33 32 31.1 32H480c17.67 0 31.1-14.33 31.1-32C512 391.6 440.4 320 351.1 320z'
/> />
)} )}

View File

@ -6,24 +6,25 @@ import {
import { ModCard } from 'components/ModCard' import { ModCard } from 'components/ModCard'
import { ModFilter } from 'components/ModsFilter' import { ModFilter } from 'components/ModsFilter'
import { PaginationWithPageNumbers } from 'components/Pagination' import { PaginationWithPageNumbers } from 'components/Pagination'
import { SearchInput } from 'components/SearchInput'
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts' import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
import { import {
useAppSelector, useAppSelector,
useFilteredMods, useFilteredMods,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
} from 'hooks' } from 'hooks'
import { useEffect, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { FilterOptions, ModDetails } from 'types'
import { import {
FilterOptions, DEFAULT_FILTER_OPTIONS,
ModDetails, extractModData,
ModeratedFilter, isModDataComplete,
NSFWFilter, scrollIntoView
SortBy } from 'utils'
} from 'types'
import { extractModData, isModDataComplete, scrollIntoView } from 'utils'
export const GamePage = () => { export const GamePage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
@ -33,20 +34,70 @@ export const GamePage = () => {
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [filterOptions] = useLocalStorage<FilterOptions>(
sort: SortBy.Latest, 'filter',
nsfw: NSFWFilter.Hide_NSFW, DEFAULT_FILTER_OPTIONS
source: window.location.host, )
moderated: ModeratedFilter.Moderated
})
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const filteredMods = useFilteredMods( // Search
mods, const searchTermRef = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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, userState,
filterOptions, filterOptions,
nsfwList, nsfwList,
@ -54,11 +105,11 @@ export const GamePage = () => {
) )
// Pagination logic // Pagination logic
const totalGames = filteredMods.length const totalGames = filteredModList.length
const totalPages = Math.ceil(totalGames / MAX_MODS_PER_PAGE) const totalPages = Math.ceil(totalGames / MAX_MODS_PER_PAGE)
const startIndex = (currentPage - 1) * MAX_MODS_PER_PAGE const startIndex = (currentPage - 1) * MAX_MODS_PER_PAGE
const endIndex = startIndex + 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) => { const handlePageChange = (page: number) => {
if (page >= 1 && page <= totalPages) { if (page >= 1 && page <= totalPages) {
@ -116,14 +167,24 @@ export const GamePage = () => {
<span className='IBMSMTitleMainHeadingSpan'> <span className='IBMSMTitleMainHeadingSpan'>
{gameName} {gameName}
</span> </span>
{searchTerm !== '' && (
<>
&nbsp;&mdash;&nbsp;
<span className='IBMSMTitleMainHeadingSpan'>
{searchTerm}
</span>
</>
)}
</h2> </h2>
</div> </div>
<SearchInput
handleKeyDown={handleKeyDown}
handleSearch={handleSearch}
ref={searchTermRef}
/>
</div> </div>
</div> </div>
<ModFilter <ModFilter />
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
/>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'> <div className='IBMSMList'>
{currentMods.map((mod) => ( {currentMods.map((mod) => (

View File

@ -9,6 +9,7 @@ import '../styles/styles.css'
import { createSearchParams, useNavigate } from 'react-router-dom' import { createSearchParams, useNavigate } from 'react-router-dom'
import { appRoutes } from 'routes' import { appRoutes } from 'routes'
import { scrollIntoView } from 'utils' import { scrollIntoView } from 'utils'
import { SearchInput } from 'components/SearchInput'
export const GamesPage = () => { export const GamesPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
@ -74,8 +75,8 @@ export const GamesPage = () => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref const value = searchTermRef.current?.value || '' // Access the input value from the ref
if (value !== '') { if (value !== '') {
const searchParams = createSearchParams({ const searchParams = createSearchParams({
searchTerm: value, q: value,
searching: 'Games' kind: 'Games'
}) })
navigate({ pathname: appRoutes.search, search: `?${searchParams}` }) navigate({ pathname: appRoutes.search, search: `?${searchParams}` })
} }
@ -100,34 +101,11 @@ export const GamesPage = () => {
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Games</h2> <h2 className='IBMSMTitleMainHeading'>Games</h2>
</div> </div>
<div className='SearchMain'> <SearchInput
<div className='SearchMainInside'> ref={searchTermRef}
<div className='SearchMainInsideWrapper'> handleKeyDown={handleKeyDown}
<input handleSearch={handleSearch}
type='text' />
className='SMIWInput'
ref={searchTermRef}
onKeyDown={handleKeyDown}
placeholder='Enter search term'
/>
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>

View File

@ -15,7 +15,8 @@ import {
useAppSelector, useAppSelector,
useBodyScrollDisable, useBodyScrollDisable,
useDidMount, useDidMount,
useNDKContext useNDKContext,
useNSFWList
} from '../../hooks' } from '../../hooks'
import { getGamePageRoute, getModsEditPageRoute } from '../../routes' import { getGamePageRoute, getModsEditPageRoute } from '../../routes'
import '../../styles/comments.css' import '../../styles/comments.css'
@ -51,10 +52,18 @@ import placeholder from '../../assets/img/DEGMods Placeholder Img.png'
export const ModPage = () => { export const ModPage = () => {
const { naddr } = useParams() const { naddr } = useParams()
const { fetchEvent } = useNDKContext() const { fetchEvent } = useNDKContext()
const [modData, setModData] = useState<ModDetails>() const [mod, setMod] = useState<ModDetails>()
const [isFetching, setIsFetching] = useState(true) const [isFetching, setIsFetching] = useState(true)
const [commentCount, setCommentCount] = useState(0) const [commentCount, setCommentCount] = useState(0)
// Make sure to mark non-nsfw mods as NSFW if found in nsfwList
const nsfwList = useNSFWList()
const isMissingNsfwTag =
!mod?.nsfw && mod?.aTag && nsfwList && nsfwList.includes(mod.aTag)
const modData = isMissingNsfwTag
? ({ ...mod, nsfw: true } as ModDetails)
: mod
useDidMount(async () => { useDidMount(async () => {
if (naddr) { if (naddr) {
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
@ -70,7 +79,7 @@ export const ModPage = () => {
.then((event) => { .then((event) => {
if (event) { if (event) {
const extracted = extractModData(event) const extracted = extractModData(event)
setModData(extracted) setMod(extracted)
} }
}) })
.catch((err) => { .catch((err) => {

View File

@ -8,6 +8,7 @@ import { MOD_FILTER_LIMIT } from '../constants'
import { import {
useAppSelector, useAppSelector,
useFilteredMods, useFilteredMods,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
@ -17,26 +18,20 @@ import '../styles/filters.css'
import '../styles/pagination.css' import '../styles/pagination.css'
import '../styles/search.css' import '../styles/search.css'
import '../styles/styles.css' import '../styles/styles.css'
import { import { FilterOptions, ModDetails } from '../types'
FilterOptions, import { DEFAULT_FILTER_OPTIONS, scrollIntoView } from 'utils'
ModDetails, import { SearchInput } from 'components/SearchInput'
ModeratedFilter,
NSFWFilter,
SortBy
} from '../types'
import { scrollIntoView } from 'utils'
export const ModsPage = () => { export const ModsPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
const { fetchMods } = useNDKContext() const { fetchMods } = useNDKContext()
const [isFetching, setIsFetching] = useState(false) const [isFetching, setIsFetching] = useState(false)
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
sort: SortBy.Latest, const [filterOptions] = useLocalStorage<FilterOptions>(
nsfw: NSFWFilter.Hide_NSFW, 'filter',
source: window.location.host, DEFAULT_FILTER_OPTIONS
moderated: ModeratedFilter.Moderated )
})
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
@ -112,10 +107,7 @@ export const ModsPage = () => {
ref={scrollTargetRef} ref={scrollTargetRef}
> >
<PageTitleRow /> <PageTitleRow />
<ModFilter <ModFilter />
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
/>
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'> <div className='IBMSMList'>
@ -146,8 +138,8 @@ const PageTitleRow = React.memo(() => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref const value = searchTermRef.current?.value || '' // Access the input value from the ref
if (value !== '') { if (value !== '') {
const searchParams = createSearchParams({ const searchParams = createSearchParams({
searchTerm: value, q: value,
searching: 'Mods' kind: 'Mods'
}) })
navigate({ pathname: appRoutes.search, search: `?${searchParams}` }) navigate({ pathname: appRoutes.search, search: `?${searchParams}` })
} }
@ -166,35 +158,11 @@ const PageTitleRow = React.memo(() => {
<div className='IBMSMTitleMain'> <div className='IBMSMTitleMain'>
<h2 className='IBMSMTitleMainHeading'>Mods</h2> <h2 className='IBMSMTitleMainHeading'>Mods</h2>
</div> </div>
<div className='SearchMain'> <SearchInput
<div className='SearchMainInside'> ref={searchTermRef}
<div className='SearchMainInsideWrapper'> handleKeyDown={handleKeyDown}
<input handleSearch={handleSearch}
type='text' />
className='SMIWInput'
ref={searchTermRef}
onKeyDown={handleKeyDown}
placeholder='Enter search term'
/>
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
) )

View File

@ -8,8 +8,8 @@ import { Tabs } from 'components/Tabs'
import { MOD_FILTER_LIMIT } from '../constants' import { MOD_FILTER_LIMIT } from '../constants'
import { import {
useAppSelector, useAppSelector,
useDidMount,
useFilteredMods, useFilteredMods,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
@ -19,17 +19,12 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { useParams, Navigate, Link } from 'react-router-dom' import { useParams, Navigate, Link } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { appRoutes, getProfilePageRoute } from 'routes' import { appRoutes, getProfilePageRoute } from 'routes'
import { import { FilterOptions, ModDetails, UserRelaysType } from 'types'
FilterOptions,
ModDetails,
ModeratedFilter,
NSFWFilter,
SortBy,
UserProfile,
UserRelaysType
} from 'types'
import { import {
copyTextToClipboard, copyTextToClipboard,
DEFAULT_FILTER_OPTIONS,
log,
LogType,
now, now,
npubToHex, npubToHex,
scrollIntoView, scrollIntoView,
@ -37,6 +32,7 @@ import {
signAndPublish signAndPublish
} from 'utils' } from 'utils'
import { CheckboxField } from 'components/Inputs' import { CheckboxField } from 'components/Inputs'
import { useProfile } from 'hooks/useProfile'
export const ProfilePage = () => { export const ProfilePage = () => {
// Try to decode nprofile parameter // Try to decode nprofile parameter
@ -48,27 +44,19 @@ export const ProfilePage = () => {
: undefined : undefined
profilePubkey = value?.data.pubkey profilePubkey = value?.data.pubkey
} catch (error) { } catch (error) {
// Failed to decode the nprofile
// Silently ignore and redirect to home or logged in user // Silently ignore and redirect to home or logged in user
log(true, LogType.Error, 'Failed to decode nprofile.', error)
} }
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
const { ndk, publish, findMetadata, fetchEventFromUserRelays, fetchMods } = const { ndk, publish, fetchEventFromUserRelays, fetchMods } = useNDKContext()
useNDKContext()
const [profile, setProfile] = useState<UserProfile>()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const isOwnProfile = const isOwnProfile =
userState.auth && userState.user?.pubkey === profilePubkey userState.auth && userState.user?.pubkey === profilePubkey
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
useDidMount(() => { const profile = useProfile(profilePubkey)
if (profilePubkey) {
findMetadata(profilePubkey).then((res) => {
setProfile(res)
})
}
})
const displayName = const displayName =
profile?.displayName || profile?.name || '[name not set up]' profile?.displayName || profile?.name || '[name not set up]'
@ -221,7 +209,7 @@ export const ProfilePage = () => {
kind: NDKKind.MuteList, kind: NDKKind.MuteList,
content: muteListEvent.content, content: muteListEvent.content,
created_at: now(), created_at: now(),
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== profilePubkey) tags: tags.filter((item) => item[0] !== 'p' || item[1] !== profilePubkey)
} }
setLoadingSpinnerDesc('Updating mute list event') setLoadingSpinnerDesc('Updating mute list event')
@ -239,12 +227,9 @@ export const ProfilePage = () => {
// Mods // Mods
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const filterKey = 'filter-profile'
sort: SortBy.Latest, const [filterOptions] = useLocalStorage<FilterOptions>(filterKey, {
nsfw: NSFWFilter.Hide_NSFW, ...DEFAULT_FILTER_OPTIONS
source: window.location.host,
moderated: ModeratedFilter.Moderated,
author: profilePubkey
}) })
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
@ -313,7 +298,8 @@ export const ProfilePage = () => {
userState, userState,
filterOptions, filterOptions,
nsfwList, nsfwList,
muteLists muteLists,
profilePubkey
) )
// Redirect route // Redirect route
@ -479,10 +465,7 @@ export const ProfilePage = () => {
{/* Tabs Content */} {/* Tabs Content */}
{tab === 0 && ( {tab === 0 && (
<> <>
<ModFilter <ModFilter filterKey={filterKey} author={profilePubkey} />
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
/>
<div className='IBMSMList IBMSMListAlt'> <div className='IBMSMList IBMSMListAlt'>
{filteredModList.map((mod) => ( {filteredModList.map((mod) => (

View File

@ -4,15 +4,16 @@ import {
NDKKind, NDKKind,
NDKSubscriptionCacheUsage, NDKSubscriptionCacheUsage,
NDKUserProfile, NDKUserProfile,
NostrEvent,
profileFromEvent profileFromEvent
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import { ErrorBoundary } from 'components/ErrorBoundary' import { ErrorBoundary } from 'components/ErrorBoundary'
import { GameCard } from 'components/GameCard' import { GameCard } from 'components/GameCard'
import { LoadingSpinner } from 'components/LoadingSpinner'
import { ModCard } from 'components/ModCard' import { ModCard } from 'components/ModCard'
import { ModFilter } from 'components/ModsFilter' import { ModFilter } from 'components/ModsFilter'
import { Pagination } from 'components/Pagination' import { Pagination } from 'components/Pagination'
import { Profile } from 'components/ProfileSection' import { Profile } from 'components/ProfileSection'
import { SearchInput } from 'components/SearchInput'
import { import {
MAX_GAMES_PER_PAGE, MAX_GAMES_PER_PAGE,
MAX_MODS_PER_PAGE, MAX_MODS_PER_PAGE,
@ -22,32 +23,18 @@ import {
useAppSelector, useAppSelector,
useFilteredMods, useFilteredMods,
useGames, useGames,
useLocalStorage,
useMuteLists, useMuteLists,
useNDKContext, useNDKContext,
useNSFWList useNSFWList
} from 'hooks' } from 'hooks'
import React, { import React, { useEffect, useMemo, useRef, useState } from 'react'
Dispatch,
SetStateAction,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { FilterOptions, ModDetails, ModeratedFilter, MuteLists } from 'types'
import { import {
FilterOptions, DEFAULT_FILTER_OPTIONS,
ModDetails,
ModeratedFilter,
MuteLists,
NSFWFilter,
SortBy
} from 'types'
import {
extractModData, extractModData,
isModDataComplete, isModDataComplete,
log,
LogType,
scrollIntoView scrollIntoView
} from 'utils' } from 'utils'
@ -59,30 +46,35 @@ enum SearchKindEnum {
export const SearchPage = () => { export const SearchPage = () => {
const scrollTargetRef = useRef<HTMLDivElement>(null) const scrollTargetRef = useRef<HTMLDivElement>(null)
const [searchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList() const nsfwList = useNSFWList()
const searchTermRef = useRef<HTMLInputElement>(null) const searchTermRef = useRef<HTMLInputElement>(null)
const [searchKind, setSearchKind] = useState( const searchKind =
(searchParams.get('searching') as SearchKindEnum) || SearchKindEnum.Mods (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods
const [filterOptions] = useLocalStorage<FilterOptions>(
'filter',
DEFAULT_FILTER_OPTIONS
) )
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [searchTerm, setSearchTerm] = useState(searchParams.get('q') || '')
sort: SortBy.Latest,
nsfw: NSFWFilter.Hide_NSFW,
source: window.location.host,
moderated: ModeratedFilter.Moderated
})
const [searchTerm, setSearchTerm] = useState(
searchParams.get('searchTerm') || ''
)
const handleSearch = () => { const handleSearch = () => {
const value = searchTermRef.current?.value || '' // Access the input value from the ref const value = searchTermRef.current?.value || '' // Access the input value from the ref
setSearchTerm(value) setSearchTerm(value)
if (value) {
searchParams.set('q', value)
} else {
searchParams.delete('q')
}
setSearchParams(searchParams, {
replace: true
})
} }
// Handle "Enter" key press inside the input // Handle "Enter" key press inside the input
@ -109,42 +101,14 @@ export const SearchPage = () => {
</span> </span>
</h2> </h2>
</div> </div>
<div className='SearchMain'> <SearchInput
<div className='SearchMainInside'> handleKeyDown={handleKeyDown}
<div className='SearchMainInsideWrapper'> handleSearch={handleSearch}
<input ref={searchTermRef}
type='text' />
className='SMIWInput'
ref={searchTermRef}
onKeyDown={handleKeyDown}
placeholder='Enter search term'
/>
<button
className='btn btnMain SMIWButton'
type='button'
onClick={handleSearch}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
>
<path d='M500.3 443.7l-119.7-119.7c27.22-40.41 40.65-90.9 33.46-144.7C401.8 87.79 326.8 13.32 235.2 1.723C99.01-15.51-15.51 99.01 1.724 235.2c11.6 91.64 86.08 166.7 177.6 178.9c53.8 7.189 104.3-6.236 144.7-33.46l119.7 119.7c15.62 15.62 40.95 15.62 56.57 0C515.9 484.7 515.9 459.3 500.3 443.7zM79.1 208c0-70.58 57.42-128 128-128s128 57.42 128 128c0 70.58-57.42 128-128 128S79.1 278.6 79.1 208z'></path>
</svg>
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
<Filters <Filters />
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
searchKind={searchKind}
setSearchKind={setSearchKind}
/>
{searchKind === SearchKindEnum.Mods && ( {searchKind === SearchKindEnum.Mods && (
<ModsResult <ModsResult
searchTerm={searchTerm} searchTerm={searchTerm}
@ -170,73 +134,29 @@ export const SearchPage = () => {
) )
} }
type FiltersProps = { const Filters = React.memo(() => {
filterOptions: FilterOptions const [filterOptions, setFilterOptions] = useLocalStorage<FilterOptions>(
setFilterOptions: Dispatch<SetStateAction<FilterOptions>> 'filter',
searchKind: SearchKindEnum DEFAULT_FILTER_OPTIONS
setSearchKind: Dispatch<SetStateAction<SearchKindEnum>> )
}
const Filters = React.memo( const userState = useAppSelector((state) => state.user)
({ const [searchParams, setSearchParams] = useSearchParams()
filterOptions, const searchKind =
setFilterOptions, (searchParams.get('kind') as SearchKindEnum) || SearchKindEnum.Mods
searchKind, const handleChangeSearchKind = (kind: SearchKindEnum) => {
setSearchKind searchParams.set('kind', kind)
}: FiltersProps) => { setSearchParams(searchParams, {
const userState = useAppSelector((state) => state.user) replace: true
})
}
return ( return (
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='FiltersMain'> <div className='FiltersMain'>
{searchKind === SearchKindEnum.Mods && ( {searchKind === SearchKindEnum.Mods && <ModFilter />}
<ModFilter
filterOptions={filterOptions}
setFilterOptions={setFilterOptions}
/>
)}
{searchKind === SearchKindEnum.Users && (
<div className='FiltersMainElement'>
<div className='dropdown dropdownMain'>
<button
className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
{filterOptions.moderated}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{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 (
<div
key={`moderatedFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
moderated: item
}))
}
>
{item}
</div>
)
})}
</div>
</div>
</div>
)}
{searchKind === SearchKindEnum.Users && (
<div className='FiltersMainElement'> <div className='FiltersMainElement'>
<div className='dropdown dropdownMain'> <div className='dropdown dropdownMain'>
<button <button
@ -245,26 +165,65 @@ const Filters = React.memo(
data-bs-toggle='dropdown' data-bs-toggle='dropdown'
type='button' type='button'
> >
Searching: {searchKind} {filterOptions.moderated}
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
{Object.values(SearchKindEnum).map((item, index) => ( {Object.values(ModeratedFilter).map((item, index) => {
<div if (item === ModeratedFilter.Unmoderated_Fully) {
key={`searchingFilterItem-${index}`} const isAdmin =
className='dropdown-item dropdownMainMenuItem' userState.user?.npub ===
onClick={() => setSearchKind(item)} import.meta.env.VITE_REPORTING_NPUB
>
{item} if (!isAdmin) return null
</div> }
))}
return (
<div
key={`moderatedFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
moderated: item
}))
}
>
{item}
</div>
)
})}
</div> </div>
</div> </div>
</div> </div>
)}
<div className='FiltersMainElement'>
<div className='dropdown dropdownMain'>
<button
className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
Searching: {searchKind}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(SearchKindEnum).map((item, index) => (
<div
key={`searchingFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() => handleChangeSearchKind(item)}
>
{item}
</div>
))}
</div>
</div>
</div> </div>
</div> </div>
) </div>
} )
) })
type ModsResultProps = { type ModsResultProps = {
filterOptions: FilterOptions filterOptions: FilterOptions
@ -324,6 +283,7 @@ const ModsResult = ({
}, [searchTerm]) }, [searchTerm])
const filteredMods = useMemo(() => { const filteredMods = useMemo(() => {
// Search page requires search term
if (searchTerm === '') return [] if (searchTerm === '') return []
const lowerCaseSearchTerm = searchTerm.toLowerCase() const lowerCaseSearchTerm = searchTerm.toLowerCase()
@ -337,8 +297,16 @@ const ModsResult = ({
tag.toLowerCase().includes(lowerCaseSearchTerm) tag.toLowerCase().includes(lowerCaseSearchTerm)
) > -1 ) > -1
return mods.filter(filterFn) const filterSourceFn = (mod: ModDetails) => {
}, [mods, searchTerm]) // Filter by source if selected
if (filterOptions.source === window.location.host) {
return mod.rTag === filterOptions.source
}
return true
}
return mods.filter(filterFn).filter(filterSourceFn)
}, [filterOptions.source, mods, searchTerm])
const filteredModList = useFilteredMods( const filteredModList = useFilteredMods(
filteredMods, filteredMods,
@ -393,39 +361,70 @@ const UsersResult = ({
moderationFilter, moderationFilter,
muteLists muteLists
}: UsersResultProps) => { }: UsersResultProps) => {
const { fetchEvents } = useNDKContext() const { ndk } = useNDKContext()
const [isFetching, setIsFetching] = useState(false)
const [profiles, setProfiles] = useState<NDKUserProfile[]>([]) const [profiles, setProfiles] = useState<NDKUserProfile[]>([])
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
useEffect(() => { useEffect(() => {
if (searchTerm === '') { if (searchTerm === '') {
setProfiles([]) setProfiles([])
} else { } else {
const filter: NDKFilter = { const sub = ndk.subscribe(
kinds: [NDKKind.Metadata], {
search: searchTerm kinds: [NDKKind.Metadata],
search: searchTerm
},
{
closeOnEose: true,
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
},
undefined,
false
)
// Stop the sub after 10 seconds if we are still searching the same term as before
window.setTimeout(() => {
if (sub.filter.search === searchTerm) {
sub.stop()
}
}, 10000)
const onEvent = (event: NostrEvent | NDKEvent) => {
if (!(event instanceof NDKEvent)) event = new NDKEvent(undefined, event)
const dedupKey = event.deduplicationKey()
const existingEvent = events.get(dedupKey)
if (existingEvent) {
event = dedup(existingEvent, event)
}
event.ndk = this
events.set(dedupKey, event)
// We can't rely on the 'eose' to arrive
// Instead we repeat and sort results on each event
const ndkEvents = Array.from(events.values())
const profiles: NDKUserProfile[] = []
ndkEvents.forEach((event) => {
try {
const profile = profileFromEvent(event)
profiles.push(profile)
} catch (error) {
// If we are unable to parse silently skip over the errors
}
})
setProfiles(profiles)
} }
setIsFetching(true) // Clear previous results
fetchEvents(filter) const events = new Map<string, NDKEvent>()
.then((events) => {
const results = events.map((event) => { // Bind handler and start the sub
const ndkEvent = new NDKEvent(undefined, event) sub.on('event', onEvent)
const profile = profileFromEvent(ndkEvent) sub.start()
return profile return () => {
}) sub.stop()
setProfiles(results) }
})
.catch((err) => {
log(true, LogType.Error, 'An error occurred in fetching users', err)
})
.finally(() => {
setIsFetching(false)
})
} }
}, [searchTerm, fetchEvents]) }, [ndk, searchTerm])
const filteredProfiles = useMemo(() => { const filteredProfiles = useMemo(() => {
let filtered = [...profiles] let filtered = [...profiles]
@ -450,25 +449,13 @@ const UsersResult = ({
}, [userState.user?.npub, moderationFilter, profiles, muteLists]) }, [userState.user?.npub, moderationFilter, profiles, muteLists])
return ( return (
<> <>
{isFetching && <LoadingSpinner desc='Fetching Profiles' />}
<div className='IBMSecMain IBMSMListWrapper'> <div className='IBMSecMain IBMSMListWrapper'>
<div className='IBMSMList'> <div className='IBMSMList'>
{filteredProfiles.map((profile) => { {filteredProfiles.map((profile) => {
if (profile.pubkey) { 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 (
<ErrorBoundary key={profile.pubkey}> <ErrorBoundary key={profile.pubkey}>
<Profile <Profile pubkey={profile.pubkey as string} />
pubkey={profile.pubkey as string}
displayName={displayName}
about={about}
image={profile?.image}
nip05={profile?.nip05}
lud16={profile?.lud16}
/>
</ErrorBoundary> </ErrorBoundary>
) )
} }
@ -536,3 +523,11 @@ const GamesResult = ({ searchTerm }: GamesResultProps) => {
</> </>
) )
} }
function dedup(event1: NDKEvent, event2: NDKEvent) {
// return the newest of the two
if (event1.created_at! > event2.created_at!) {
return event1
}
return event2
}

View File

@ -98,15 +98,15 @@ export const ProfileSettings = () => {
// In case user is not logged in clicking on profile link will navigate to homepage // In case user is not logged in clicking on profile link will navigate to homepage
let profileRoute = appRoutes.home let profileRoute = appRoutes.home
let nprofile: string | undefined
if (userState.auth && userState.user) { if (userState.auth && userState.user) {
const hexPubkey = npubToHex(userState.user.npub as string) const hexPubkey = npubToHex(userState.user.npub as string)
if (hexPubkey) { if (hexPubkey) {
profileRoute = getProfilePageRoute( nprofile = nip19.nprofileEncode({
nip19.nprofileEncode({ pubkey: hexPubkey
pubkey: hexPubkey })
}) profileRoute = getProfilePageRoute(nprofile)
)
} }
} }
@ -247,10 +247,8 @@ export const ProfileSettings = () => {
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path> <path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
</svg> </svg>
</div> </div>
{typeof userState.user?.pubkey === 'string' && ( {typeof nprofile !== 'undefined' && (
<ProfileQRButtonWithPopUp <ProfileQRButtonWithPopUp nprofile={nprofile} />
pubkey={userState.user.pubkey}
/>
)} )}
</div> </div>
</div> </div>

View File

@ -155,3 +155,12 @@
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
background: rgba(35, 35, 35, 0.85); background: rgba(35, 35, 35, 0.85);
} }
.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagRepost.IBMSMSMBSSTagsTagRepostCard {
position: absolute;
bottom: 10px;
left: 10px;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #232323d9;
}

View File

@ -44,3 +44,13 @@
cursor: default; cursor: default;
box-shadow: unset; box-shadow: unset;
} }
.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagRepost {
background: #ffffff1a;
color: #ffffff59;
font-weight: 700;
border: unset;
font-size: 14px;
cursor: default;
box-shadow: unset;
}

View File

@ -22,5 +22,4 @@ export interface FilterOptions {
nsfw: NSFWFilter nsfw: NSFWFilter
source: string source: string
moderated: ModeratedFilter moderated: ModeratedFilter
author?: string
} }

8
src/utils/consts.ts Normal file
View File

@ -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
}

View File

@ -3,3 +3,5 @@ export * from './nostr'
export * from './url' export * from './url'
export * from './utils' export * from './utils'
export * from './zap' export * from './zap'
export * from './localStorage'
export * from './consts'

32
src/utils/localStorage.ts Normal file
View File

@ -0,0 +1,32 @@
export function getLocalStorageItem<T>(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 }))
}