Compare commits

..

No commits in common. "25fcdce7b0a4c505c6dc635ea032be2e036df9c3" and "1928c0e4e97dcb2a9c1c3fbca5bacb31f916c70d" have entirely different histories.

9 changed files with 556 additions and 373 deletions

View File

@ -1,155 +0,0 @@
import { useAppSelector } from 'hooks'
import React from 'react'
import { Dispatch, SetStateAction } from 'react'
import { FilterOptions, ModeratedFilter, NSFWFilter, SortBy } from 'types'
type Props = {
filterOptions: FilterOptions
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
}
export const ModFilter = React.memo(
({ filterOptions, setFilterOptions }: Props) => {
const userState = useAppSelector((state) => state.user)
return (
<div className='IBMSecMain'>
<div className='FiltersMain'>
<div className='FiltersMainElement'>
<div className='dropdown dropdownMain'>
<button
className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
{filterOptions.sort}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(SortBy).map((item, index) => (
<div
key={`sortByItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
sort: item
}))
}
>
{item}
</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'
>
{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>
<div className='FiltersMainElement'>
<div className='dropdown dropdownMain'>
<button
className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
{filterOptions.nsfw}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(NSFWFilter).map((item, index) => (
<div
key={`nsfwFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
nsfw: item
}))
}
>
{item}
</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'
>
{filterOptions.source === window.location.host
? `Show From: ${filterOptions.source}`
: 'Show All'}
</button>
<div className='dropdown-menu dropdownMainMenu'>
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
source: window.location.host
}))
}
>
Show From: {window.location.host}
</div>
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
source: 'Show All'
}))
}
>
Show All
</div>
</div>
</div>
</div>
</div>
</div>
)
}
)

View File

@ -6,8 +6,7 @@ export const LANDING_PAGE_DATA = {
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vpcxs6nwwp3x5knyd3evckngetxxcknjdfkx5kngdfhvgukvwfjxsunseqnend73', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vpcxs6nwwp3x5knyd3evckngetxxcknjdfkx5kngdfhvgukvwfjxsunseqnend73',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5dp4xsex2e3cxuknsdryvvkngc3sxcknjef4vcknvvmyvcukyd3kvd3rxdgnuver5', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5dp4xsex2e3cxuknsdryvvkngc3sxcknjef4vcknvvmyvcukyd3kvd3rxdgnuver5',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj'
'naddr1qvzqqqrkcgpzph2jv2ejvdk27hn36dt57j6f69f5h0zccve3xceujq5z9jk8ym8wqp4nxvp5xqer5eryx5ervvnzxvervvekvdskvdt9xuckgve4xu6xvdrzxsukgvf4xv6xycnrx5uxxvenxvcnxd3nxd3njvpj8qerycmpvvmnydnrv4jn5wrpv5mrvwpsxgknxwp4xqkngetpxqknjd35xcknsv3cv9jnxvtp8ycxyegq5ndhc'
], ],
awesomeMods: [ awesomeMods: [
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
@ -19,7 +18,7 @@ export const LANDING_PAGE_DATA = {
"Baldur's Gate 3", "Baldur's Gate 3",
'Cyberpunk 2077', 'Cyberpunk 2077',
'ELDEN RING', 'ELDEN RING',
'The Coffin of Andy and Leyley' 'FINAL FANTASY VII REMAKE INTERGRADE'
] ]
} }
// we use this object to check if a user has reacted positively or negatively to a post // we use this object to check if a user has reacted positively or negatively to a post

View File

@ -1,6 +1,5 @@
export * from './redux' export * from './redux'
export * from './useDidMount' export * from './useDidMount'
export * from './useFilteredMods'
export * from './useGames' export * from './useGames'
export * from './useMuteLists' export * from './useMuteLists'
export * from './useNSFWList' export * from './useNSFWList'

View File

@ -1,77 +0,0 @@
import { useMemo } from 'react'
import { IUserState } from 'store/reducers/user'
import {
FilterOptions,
ModDetails,
ModeratedFilter,
MuteLists,
NSFWFilter,
SortBy
} from 'types'
export const useFilteredMods = (
mods: ModDetails[],
userState: IUserState,
filterOptions: FilterOptions,
nsfwList: string[],
muteLists: {
admin: MuteLists
user: MuteLists
}
) => {
return useMemo(() => {
const nsfwFilter = (mods: ModDetails[]) => {
// Determine the filtering logic based on the NSFW filter option
switch (filterOptions.nsfw) {
case NSFWFilter.Hide_NSFW:
// If 'Hide_NSFW' is selected, filter out NSFW mods
return mods.filter((mod) => !mod.nsfw && !nsfwList.includes(mod.aTag))
case NSFWFilter.Show_NSFW:
// If 'Show_NSFW' is selected, return all mods (no filtering)
return mods
case NSFWFilter.Only_NSFW:
// If 'Only_NSFW' is selected, filter to show only NSFW mods
return mods.filter((mod) => mod.nsfw || nsfwList.includes(mod.aTag))
}
}
let filtered = nsfwFilter(mods)
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isUnmoderatedFully =
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
if (!(isAdmin && isUnmoderatedFully)) {
filtered = filtered.filter(
(mod) =>
!muteLists.admin.authors.includes(mod.author) &&
!muteLists.admin.replaceableEvents.includes(mod.aTag)
)
}
if (filterOptions.moderated === ModeratedFilter.Moderated) {
filtered = filtered.filter(
(mod) =>
!muteLists.user.authors.includes(mod.author) &&
!muteLists.user.replaceableEvents.includes(mod.aTag)
)
}
if (filterOptions.sort === SortBy.Latest) {
filtered.sort((a, b) => b.published_at - a.published_at)
} else if (filterOptions.sort === SortBy.Oldest) {
filtered.sort((a, b) => a.published_at - b.published_at)
}
return filtered
}, [
userState.user?.npub,
filterOptions.sort,
filterOptions.moderated,
filterOptions.nsfw,
mods,
muteLists,
nsfwList
])
}

View File

@ -1,40 +1,50 @@
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { ModCard } from 'components/ModCard' import { ModCard } from 'components/ModCard'
import { ModFilter } from 'components/ModsFilter'
import { PaginationWithPageNumbers } from 'components/Pagination' import { PaginationWithPageNumbers } from 'components/Pagination'
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts' import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
import { RelayController } from 'controllers' import { RelayController } from 'controllers'
import { import { useAppSelector, useMuteLists } from 'hooks'
useAppSelector,
useFilteredMods,
useMuteLists,
useNSFWList
} from 'hooks'
import { Filter, kinds } from 'nostr-tools' import { Filter, kinds } from 'nostr-tools'
import { Subscription } from 'nostr-tools/abstract-relay' import { Subscription } from 'nostr-tools/abstract-relay'
import { useEffect, useRef, useState } from 'react' import React, {
Dispatch,
SetStateAction,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { import { ModDetails } from 'types'
FilterOptions,
ModDetails,
ModeratedFilter,
NSFWFilter,
SortBy
} from 'types'
import { extractModData, isModDataComplete, log, LogType } from 'utils' import { extractModData, isModDataComplete, log, LogType } from 'utils'
enum SortByEnum {
Latest = 'Latest',
Oldest = 'Oldest',
Best_Rated = 'Best Rated',
Worst_Rated = 'Worst Rated'
}
enum ModeratedFilterEnum {
Moderated = 'Moderated',
Unmoderated = 'Unmoderated',
Unmoderated_Fully = 'Unmoderated Fully'
}
interface FilterOptions {
sort: SortByEnum
moderated: ModeratedFilterEnum
}
export const GamePage = () => { export const GamePage = () => {
const params = useParams() const params = useParams()
const { name: gameName } = params const { name: gameName } = params
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList()
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [filterOptions, setFilterOptions] = useState<FilterOptions>({
sort: SortBy.Latest, sort: SortByEnum.Latest,
nsfw: NSFWFilter.Hide_NSFW, moderated: ModeratedFilterEnum.Moderated
source: window.location.host,
moderated: ModeratedFilter.Moderated
}) })
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
@ -44,13 +54,43 @@ export const GamePage = () => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const filteredMods = useFilteredMods( const filteredMods = useMemo(() => {
mods, let filtered: ModDetails[] = [...mods]
userState, const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
filterOptions, const isUnmoderatedFully =
nsfwList, filterOptions.moderated === ModeratedFilterEnum.Unmoderated_Fully
muteLists
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
if (!(isAdmin && isUnmoderatedFully)) {
filtered = filtered.filter(
(mod) =>
!muteLists.admin.authors.includes(mod.author) &&
!muteLists.admin.replaceableEvents.includes(mod.aTag)
) )
}
if (filterOptions.moderated === ModeratedFilterEnum.Moderated) {
filtered = filtered.filter(
(mod) =>
!muteLists.user.authors.includes(mod.author) &&
!muteLists.user.replaceableEvents.includes(mod.aTag)
)
}
if (filterOptions.sort === SortByEnum.Latest) {
filtered.sort((a, b) => b.published_at - a.published_at)
} else if (filterOptions.sort === SortByEnum.Oldest) {
filtered.sort((a, b) => a.published_at - b.published_at)
}
return filtered
}, [
mods,
userState.user?.npub,
filterOptions.sort,
filterOptions.moderated,
muteLists
])
// Pagination logic // Pagination logic
const totalGames = filteredMods.length const totalGames = filteredMods.length
@ -132,7 +172,7 @@ export const GamePage = () => {
</div> </div>
</div> </div>
</div> </div>
<ModFilter <Filters
filterOptions={filterOptions} filterOptions={filterOptions}
setFilterOptions={setFilterOptions} setFilterOptions={setFilterOptions}
/> />
@ -154,3 +194,88 @@ export const GamePage = () => {
</> </>
) )
} }
type FiltersProps = {
filterOptions: FilterOptions
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
}
const Filters = React.memo(
({ filterOptions, setFilterOptions }: FiltersProps) => {
const userState = useAppSelector((state) => state.user)
return (
<div className='IBMSecMain'>
<div className='FiltersMain'>
<div className='FiltersMainElement'>
<div className='dropdown dropdownMain'>
<button
className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
{filterOptions.sort}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(SortByEnum).map((item, index) => (
<div
key={`sortByItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
sort: item
}))
}
>
{item}
</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'
>
{filterOptions.moderated}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(ModeratedFilterEnum).map((item, index) => {
if (item === ModeratedFilterEnum.Unmoderated_Fully) {
const isAdmin =
userState.user?.npub ===
import.meta.env.VITE_REPORTING_NPUB
if (!isAdmin) return null
}
return (
<div
key={`moderatedFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
moderated: item
}))
}
>
{item}
</div>
)
})}
</div>
</div>
</div>
</div>
</div>
)
}
)

View File

@ -1,30 +1,52 @@
import { ModFilter } from 'components/ModsFilter'
import { Pagination } from 'components/Pagination' import { Pagination } from 'components/Pagination'
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { createSearchParams, useNavigate } from 'react-router-dom' import { createSearchParams, useNavigate } from 'react-router-dom'
import { LoadingSpinner } from '../components/LoadingSpinner' import { LoadingSpinner } from '../components/LoadingSpinner'
import { ModCard } from '../components/ModCard' import { ModCard } from '../components/ModCard'
import { MOD_FILTER_LIMIT } from '../constants' import { MOD_FILTER_LIMIT } from '../constants'
import { import { useAppSelector, useMuteLists, useNSFWList } from '../hooks'
useAppSelector,
useFilteredMods,
useMuteLists,
useNSFWList
} from '../hooks'
import { appRoutes } from '../routes' import { appRoutes } from '../routes'
import '../styles/filters.css' 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 { ModDetails } from '../types'
FilterOptions,
ModDetails,
ModeratedFilter,
NSFWFilter,
SortBy
} from '../types'
import { fetchMods } from '../utils' import { fetchMods } from '../utils'
enum SortBy {
Latest = 'Latest',
Oldest = 'Oldest',
Best_Rated = 'Best Rated',
Worst_Rated = 'Worst Rated'
}
enum NSFWFilter {
Hide_NSFW = 'Hide NSFW',
Show_NSFW = 'Show NSFW',
Only_NSFW = 'Only NSFW'
}
enum ModeratedFilter {
Moderated = 'Moderated',
Unmoderated = 'Unmoderated',
Unmoderated_Fully = 'Unmoderated Fully'
}
interface FilterOptions {
sort: SortBy
nsfw: NSFWFilter
source: string
moderated: ModeratedFilter
}
export const ModsPage = () => { export const ModsPage = () => {
const [isFetching, setIsFetching] = useState(false) const [isFetching, setIsFetching] = useState(false)
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
@ -89,13 +111,61 @@ export const ModsPage = () => {
}) })
}, [filterOptions.source, mods]) }, [filterOptions.source, mods])
const filteredModList = useFilteredMods( const filteredModList = useMemo(() => {
mods, const nsfwFilter = (mods: ModDetails[]) => {
userState, // Determine the filtering logic based on the NSFW filter option
filterOptions, switch (filterOptions.nsfw) {
nsfwList, case NSFWFilter.Hide_NSFW:
muteLists // If 'Hide_NSFW' is selected, filter out NSFW mods
return mods.filter((mod) => !mod.nsfw && !nsfwList.includes(mod.aTag))
case NSFWFilter.Show_NSFW:
// If 'Show_NSFW' is selected, return all mods (no filtering)
return mods
case NSFWFilter.Only_NSFW:
// If 'Only_NSFW' is selected, filter to show only NSFW mods
return mods.filter((mod) => mod.nsfw || nsfwList.includes(mod.aTag))
}
}
let filtered = nsfwFilter(mods)
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isUnmoderatedFully =
filterOptions.moderated === ModeratedFilter.Unmoderated_Fully
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
if (!(isAdmin && isUnmoderatedFully)) {
filtered = filtered.filter(
(mod) =>
!muteLists.admin.authors.includes(mod.author) &&
!muteLists.admin.replaceableEvents.includes(mod.aTag)
) )
}
if (filterOptions.moderated === ModeratedFilter.Moderated) {
filtered = filtered.filter(
(mod) =>
!muteLists.user.authors.includes(mod.author) &&
!muteLists.user.replaceableEvents.includes(mod.aTag)
)
}
if (filterOptions.sort === SortBy.Latest) {
filtered.sort((a, b) => b.published_at - a.published_at)
} else if (filterOptions.sort === SortBy.Oldest) {
filtered.sort((a, b) => a.published_at - b.published_at)
}
return filtered
}, [
userState.user?.npub,
filterOptions.sort,
filterOptions.moderated,
filterOptions.nsfw,
mods,
muteLists,
nsfwList
])
return ( return (
<> <>
@ -104,7 +174,7 @@ export const ModsPage = () => {
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div className='IBMSecMainGroup IBMSecMainGroupAlt'>
<PageTitleRow /> <PageTitleRow />
<ModFilter <Filters
filterOptions={filterOptions} filterOptions={filterOptions}
setFilterOptions={setFilterOptions} setFilterOptions={setFilterOptions}
/> />
@ -191,3 +261,154 @@ const PageTitleRow = React.memo(() => {
</div> </div>
) )
}) })
type FiltersProps = {
filterOptions: FilterOptions
setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
}
const Filters = React.memo(
({ filterOptions, setFilterOptions }: FiltersProps) => {
const userState = useAppSelector((state) => state.user)
return (
<div className='IBMSecMain'>
<div className='FiltersMain'>
<div className='FiltersMainElement'>
<div className='dropdown dropdownMain'>
<button
className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
{filterOptions.sort}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(SortBy).map((item, index) => (
<div
key={`sortByItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
sort: item
}))
}
>
{item}
</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'
>
{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>
<div className='FiltersMainElement'>
<div className='dropdown dropdownMain'>
<button
className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
{filterOptions.nsfw}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(NSFWFilter).map((item, index) => (
<div
key={`nsfwFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
nsfw: item
}))
}
>
{item}
</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'
>
{filterOptions.source === window.location.host
? `Show From: ${filterOptions.source}`
: 'Show All'}
</button>
<div className='dropdown-menu dropdownMainMenu'>
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
source: window.location.host
}))
}
>
Show From: {window.location.host}
</div>
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
source: 'Show All'
}))
}
>
Show All
</div>
</div>
</div>
</div>
</div>
</div>
)
}
)

View File

@ -3,7 +3,6 @@ import { ErrorBoundary } from 'components/ErrorBoundary'
import { GameCard } from 'components/GameCard' import { GameCard } from 'components/GameCard'
import { LoadingSpinner } from 'components/LoadingSpinner' import { LoadingSpinner } from 'components/LoadingSpinner'
import { ModCard } from 'components/ModCard' import { ModCard } from 'components/ModCard'
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 { import {
@ -12,13 +11,7 @@ import {
T_TAG_VALUE T_TAG_VALUE
} from 'constants.ts' } from 'constants.ts'
import { RelayController } from 'controllers' import { RelayController } from 'controllers'
import { import { useAppSelector, useGames, useMuteLists } from 'hooks'
useAppSelector,
useFilteredMods,
useGames,
useMuteLists,
useNSFWList
} from 'hooks'
import { Filter, kinds } from 'nostr-tools' import { Filter, kinds } from 'nostr-tools'
import { Subscription } from 'nostr-tools/abstract-relay' import { Subscription } from 'nostr-tools/abstract-relay'
import React, { import React, {
@ -31,40 +24,48 @@ import React, {
} from 'react' } from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { toast } from 'react-toastify' import { toast } from 'react-toastify'
import { import { ModDetails, MuteLists } from 'types'
FilterOptions,
ModDetails,
ModeratedFilter,
MuteLists,
NSFWFilter,
SortBy
} from 'types'
import { extractModData, isModDataComplete, log, LogType } from 'utils' import { extractModData, isModDataComplete, log, LogType } from 'utils'
enum SearchKindEnum { enum SortByEnum {
Latest = 'Latest',
Oldest = 'Oldest',
Best_Rated = 'Best Rated',
Worst_Rated = 'Worst Rated'
}
enum ModeratedFilterEnum {
Moderated = 'Moderated',
Unmoderated = 'Unmoderated',
Unmoderated_Fully = 'Unmoderated Fully'
}
enum SearchingFilterEnum {
Mods = 'Mods', Mods = 'Mods',
Games = 'Games', Games = 'Games',
Users = 'Users' Users = 'Users'
} }
interface FilterOptions {
sort: SortByEnum
moderated: ModeratedFilterEnum
searching: SearchingFilterEnum
source: string
}
export const SearchPage = () => { export const SearchPage = () => {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const muteLists = useMuteLists() const muteLists = useMuteLists()
const nsfwList = useNSFWList()
const searchTermRef = useRef<HTMLInputElement>(null) const searchTermRef = useRef<HTMLInputElement>(null)
const [searchKind, setSearchKind] = useState(
(searchParams.get('searching') as SearchKindEnum) || SearchKindEnum.Mods
)
const [filterOptions, setFilterOptions] = useState<FilterOptions>({ const [filterOptions, setFilterOptions] = useState<FilterOptions>({
sort: SortBy.Latest, sort: SortByEnum.Latest,
nsfw: NSFWFilter.Hide_NSFW, moderated: ModeratedFilterEnum.Moderated,
source: window.location.host, source: window.location.host,
moderated: ModeratedFilter.Moderated searching:
(searchParams.get('searching') as SearchingFilterEnum) ||
SearchingFilterEnum.Mods
}) })
const [searchTerm, setSearchTerm] = useState( const [searchTerm, setSearchTerm] = useState(
searchParams.get('searchTerm') || '' searchParams.get('searchTerm') || ''
) )
@ -128,25 +129,22 @@ export const SearchPage = () => {
<Filters <Filters
filterOptions={filterOptions} filterOptions={filterOptions}
setFilterOptions={setFilterOptions} setFilterOptions={setFilterOptions}
searchKind={searchKind}
setSearchKind={setSearchKind}
/> />
{searchKind === SearchKindEnum.Mods && ( {filterOptions.searching === SearchingFilterEnum.Mods && (
<ModsResult <ModsResult
searchTerm={searchTerm} searchTerm={searchTerm}
filterOptions={filterOptions} filterOptions={filterOptions}
muteLists={muteLists} muteLists={muteLists}
nsfwList={nsfwList}
/> />
)} )}
{searchKind === SearchKindEnum.Users && ( {filterOptions.searching === SearchingFilterEnum.Users && (
<UsersResult <UsersResult
searchTerm={searchTerm} searchTerm={searchTerm}
muteLists={muteLists} muteLists={muteLists}
moderationFilter={filterOptions.moderated} moderationFilter={filterOptions.moderated}
/> />
)} )}
{searchKind === SearchKindEnum.Games && ( {filterOptions.searching === SearchingFilterEnum.Games && (
<GamesResult searchTerm={searchTerm} /> <GamesResult searchTerm={searchTerm} />
)} )}
</div> </div>
@ -158,30 +156,49 @@ export const SearchPage = () => {
type FiltersProps = { type FiltersProps = {
filterOptions: FilterOptions filterOptions: FilterOptions
setFilterOptions: Dispatch<SetStateAction<FilterOptions>> setFilterOptions: Dispatch<SetStateAction<FilterOptions>>
searchKind: SearchKindEnum
setSearchKind: Dispatch<SetStateAction<SearchKindEnum>>
} }
const Filters = React.memo( const Filters = React.memo(
({ ({ filterOptions, setFilterOptions }: FiltersProps) => {
filterOptions,
setFilterOptions,
searchKind,
setSearchKind
}: FiltersProps) => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
return ( return (
<div className='IBMSecMain'> <div className='IBMSecMain'>
<div className='FiltersMain'> <div className='FiltersMain'>
{searchKind === SearchKindEnum.Mods && ( {filterOptions.searching === SearchingFilterEnum.Mods && (
<ModFilter <div className='FiltersMainElement'>
filterOptions={filterOptions} <div className='dropdown dropdownMain'>
setFilterOptions={setFilterOptions} <button
/> className='btn dropdown-toggle btnMain btnMainDropdown'
aria-expanded='false'
data-bs-toggle='dropdown'
type='button'
>
{filterOptions.sort}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(SortByEnum).map((item, index) => (
<div
key={`sortByItem-${index}`}
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
sort: item
}))
}
>
{item}
</div>
))}
</div>
</div>
</div>
)} )}
{searchKind === SearchKindEnum.Users && ( {(filterOptions.searching === SearchingFilterEnum.Mods ||
filterOptions.searching === SearchingFilterEnum.Users) && (
<div className='FiltersMainElement'> <div className='FiltersMainElement'>
<div className='dropdown dropdownMain'> <div className='dropdown dropdownMain'>
<button <button
@ -193,8 +210,8 @@ const Filters = React.memo(
{filterOptions.moderated} {filterOptions.moderated}
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
{Object.values(ModeratedFilter).map((item, index) => { {Object.values(ModeratedFilterEnum).map((item, index) => {
if (item === ModeratedFilter.Unmoderated_Fully) { if (item === ModeratedFilterEnum.Unmoderated_Fully) {
const isAdmin = const isAdmin =
userState.user?.npub === userState.user?.npub ===
import.meta.env.VITE_REPORTING_NPUB import.meta.env.VITE_REPORTING_NPUB
@ -222,6 +239,7 @@ const Filters = React.memo(
</div> </div>
)} )}
{filterOptions.searching === SearchingFilterEnum.Mods && (
<div className='FiltersMainElement'> <div className='FiltersMainElement'>
<div className='dropdown dropdownMain'> <div className='dropdown dropdownMain'>
<button <button
@ -230,14 +248,59 @@ const Filters = React.memo(
data-bs-toggle='dropdown' data-bs-toggle='dropdown'
type='button' type='button'
> >
Searching: {searchKind} {filterOptions.source === window.location.host
? `Show From: ${filterOptions.source}`
: 'Show All'}
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
{Object.values(SearchKindEnum).map((item, index) => ( <div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
source: window.location.host
}))
}
>
Show From: {window.location.host}
</div>
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
setFilterOptions((prev) => ({
...prev,
source: 'Show All'
}))
}
>
Show All
</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: {filterOptions.searching}
</button>
<div className='dropdown-menu dropdownMainMenu'>
{Object.values(SearchingFilterEnum).map((item, index) => (
<div <div
key={`searchingFilterItem-${index}`} key={`searchingFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem' className='dropdown-item dropdownMainMenuItem'
onClick={() => setSearchKind(item)} onClick={() =>
setFilterOptions((prev) => ({
...prev,
searching: item
}))
}
> >
{item} {item}
</div> </div>
@ -258,14 +321,12 @@ type ModsResultProps = {
admin: MuteLists admin: MuteLists
user: MuteLists user: MuteLists
} }
nsfwList: string[]
} }
const ModsResult = ({ const ModsResult = ({
filterOptions, filterOptions,
searchTerm, searchTerm,
muteLists, muteLists
nsfwList
}: ModsResultProps) => { }: ModsResultProps) => {
const hasEffectRun = useRef(false) const hasEffectRun = useRef(false)
const [isSubscribing, setIsSubscribing] = useState(false) const [isSubscribing, setIsSubscribing] = useState(false)
@ -339,13 +400,49 @@ const ModsResult = ({
return mods.filter(filterFn) return mods.filter(filterFn)
}, [mods, searchTerm]) }, [mods, searchTerm])
const filteredModList = useFilteredMods( const filteredModList = useMemo(() => {
filteredMods, let filtered: ModDetails[] = [...filteredMods]
userState,
filterOptions, if (filterOptions.source === window.location.host) {
nsfwList, filtered = filtered.filter((mod) => mod.rTag === window.location.host)
muteLists }
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isUnmoderatedFully =
filterOptions.moderated === ModeratedFilterEnum.Unmoderated_Fully
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
if (!(isAdmin && isUnmoderatedFully)) {
filtered = filtered.filter(
(mod) =>
!muteLists.admin.authors.includes(mod.author) &&
!muteLists.admin.replaceableEvents.includes(mod.aTag)
) )
}
if (filterOptions.moderated === ModeratedFilterEnum.Moderated) {
filtered = filtered.filter(
(mod) =>
!muteLists.user.authors.includes(mod.author) &&
!muteLists.user.replaceableEvents.includes(mod.aTag)
)
}
if (filterOptions.sort === SortByEnum.Latest) {
filtered.sort((a, b) => b.published_at - a.published_at)
} else if (filterOptions.sort === SortByEnum.Oldest) {
filtered.sort((a, b) => a.published_at - b.published_at)
}
return filtered
}, [
filteredMods,
userState.user?.npub,
filterOptions.sort,
filterOptions.moderated,
filterOptions.source,
muteLists
])
const handleNext = () => { const handleNext = () => {
setPage((prev) => prev + 1) setPage((prev) => prev + 1)
@ -381,7 +478,7 @@ const ModsResult = ({
type UsersResultProps = { type UsersResultProps = {
searchTerm: string searchTerm: string
moderationFilter: ModeratedFilter moderationFilter: ModeratedFilterEnum
muteLists: { muteLists: {
admin: MuteLists admin: MuteLists
user: MuteLists user: MuteLists
@ -431,7 +528,7 @@ const UsersResult = ({
let filtered = [...profiles] let filtered = [...profiles]
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
const isUnmoderatedFully = const isUnmoderatedFully =
moderationFilter === ModeratedFilter.Unmoderated_Fully moderationFilter === ModeratedFilterEnum.Unmoderated_Fully
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully" // Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
if (!(isAdmin && isUnmoderatedFully)) { if (!(isAdmin && isUnmoderatedFully)) {
@ -440,7 +537,7 @@ const UsersResult = ({
) )
} }
if (moderationFilter === ModeratedFilter.Moderated) { if (moderationFilter === ModeratedFilterEnum.Moderated) {
filtered = filtered.filter( filtered = filtered.filter(
(profile) => !muteLists.user.authors.includes(profile.pubkey as string) (profile) => !muteLists.user.authors.includes(profile.pubkey as string)
) )

View File

@ -1,5 +1,4 @@
export * from './mod' export * from './mod'
export * from './modsFilter'
export * from './nostr' export * from './nostr'
export * from './user' export * from './user'
export * from './zap' export * from './zap'

View File

@ -1,25 +0,0 @@
export enum SortBy {
Latest = 'Latest',
Oldest = 'Oldest',
Best_Rated = 'Best Rated',
Worst_Rated = 'Worst Rated'
}
export enum NSFWFilter {
Hide_NSFW = 'Hide NSFW',
Show_NSFW = 'Show NSFW',
Only_NSFW = 'Only NSFW'
}
export enum ModeratedFilter {
Moderated = 'Moderated',
Unmoderated = 'Unmoderated',
Unmoderated_Fully = 'Unmoderated Fully'
}
export interface FilterOptions {
sort: SortBy
nsfw: NSFWFilter
source: string
moderated: ModeratedFilter
}