Compare commits

..

3 Commits

Author SHA1 Message Date
freakoverse
25fcdce7b0 Merge pull request 'added NSFW filter and source filter to a game's page to see their mods, adjusted landing page content' (#69) from staging into master
All checks were successful
Release to Staging / build_and_release (push) Successful in 46s
Reviewed-on: #69
2024-10-07 12:05:03 +00:00
freakoverse
b48b4478af Update src/constants.ts
All checks were successful
Release to Staging / build_and_release (push) Successful in 48s
2024-10-07 12:02:20 +00:00
daniyal
b6a8fc435d chore: refactor code for mod filter
All checks were successful
Release to Staging / build_and_release (push) Successful in 46s
2024-10-07 15:45:21 +05:00
9 changed files with 373 additions and 556 deletions

View File

@ -0,0 +1,155 @@
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,7 +6,8 @@ export const LANDING_PAGE_DATA = {
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vpcxs6nwwp3x5knyd3evckngetxxcknjdfkx5kngdfhvgukvwfjxsunseqnend73', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vpcxs6nwwp3x5knyd3evckngetxxcknjdfkx5kngdfhvgukvwfjxsunseqnend73',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5dp4xsex2e3cxuknsdryvvkngc3sxcknjef4vcknvvmyvcukyd3kvd3rxdgnuver5', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5dp4xsex2e3cxuknsdryvvkngc3sxcknjef4vcknvvmyvcukyd3kvd3rxdgnuver5',
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj' 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5vf5x9nrxcekxvknjvmzxvkngcfsx5kkzcf3xqknsvmrvgenwe3j8p3nzwgka59vj',
'naddr1qvzqqqrkcgpzph2jv2ejvdk27hn36dt57j6f69f5h0zccve3xceujq5z9jk8ym8wqp4nxvp5xqer5eryx5ervvnzxvervvekvdskvdt9xuckgve4xu6xvdrzxsukgvf4xv6xycnrx5uxxvenxvcnxd3nxd3njvpj8qerycmpvvmnydnrv4jn5wrpv5mrvwpsxgknxwp4xqkngetpxqknjd35xcknsv3cv9jnxvtp8ycxyegq5ndhc'
], ],
awesomeMods: [ awesomeMods: [
'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6', 'naddr1qvzqqqrkcgpzquuz5nxzzap2c034s8cuv5ayr7gjaxz7d22pgwfh0qpmsesy9eflqp4nxvp5xqer5den8qexzdrrvverzde5xfskxvm9xv6nsvtxx93nvdfnvy6rze3exyex2wfcx4jnvcfexscngveexvmnwwpsxd3rsd3kxq6ryef4xdnr5d3excenzvf5xgkkvdny8qkngveex5knjcnxxqkn2efnx3jrxvpcxgukxdggsmal6',
@ -18,7 +19,7 @@ export const LANDING_PAGE_DATA = {
"Baldur's Gate 3", "Baldur's Gate 3",
'Cyberpunk 2077', 'Cyberpunk 2077',
'ELDEN RING', 'ELDEN RING',
'FINAL FANTASY VII REMAKE INTERGRADE' 'The Coffin of Andy and Leyley'
] ]
} }
// 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,5 +1,6 @@
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

@ -0,0 +1,77 @@
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,50 +1,40 @@
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 { useAppSelector, useMuteLists } from 'hooks' import {
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 React, { import { useEffect, useRef, useState } from '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 { ModDetails } from 'types' import {
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: SortByEnum.Latest, sort: SortBy.Latest,
moderated: ModeratedFilterEnum.Moderated nsfw: NSFWFilter.Hide_NSFW,
source: window.location.host,
moderated: ModeratedFilter.Moderated
}) })
const [mods, setMods] = useState<ModDetails[]>([]) const [mods, setMods] = useState<ModDetails[]>([])
@ -54,43 +44,13 @@ export const GamePage = () => {
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
const filteredMods = useMemo(() => { const filteredMods = useFilteredMods(
let filtered: ModDetails[] = [...mods]
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
}, [
mods, mods,
userState.user?.npub, userState,
filterOptions.sort, filterOptions,
filterOptions.moderated, nsfwList,
muteLists muteLists
]) )
// Pagination logic // Pagination logic
const totalGames = filteredMods.length const totalGames = filteredMods.length
@ -172,7 +132,7 @@ export const GamePage = () => {
</div> </div>
</div> </div>
</div> </div>
<Filters <ModFilter
filterOptions={filterOptions} filterOptions={filterOptions}
setFilterOptions={setFilterOptions} setFilterOptions={setFilterOptions}
/> />
@ -194,88 +154,3 @@ 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,52 +1,30 @@
import { ModFilter } from 'components/ModsFilter'
import { Pagination } from 'components/Pagination' import { Pagination } from 'components/Pagination'
import React, { import React, { useCallback, useEffect, useRef, useState } from '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 { useAppSelector, useMuteLists, useNSFWList } from '../hooks' import {
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 { ModDetails } from '../types' import {
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[]>([])
@ -111,61 +89,13 @@ export const ModsPage = () => {
}) })
}, [filterOptions.source, mods]) }, [filterOptions.source, mods])
const filteredModList = useMemo(() => { const filteredModList = useFilteredMods(
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, mods,
muteLists, userState,
nsfwList filterOptions,
]) nsfwList,
muteLists
)
return ( return (
<> <>
@ -174,7 +104,7 @@ export const ModsPage = () => {
<div className='ContainerMain'> <div className='ContainerMain'>
<div className='IBMSecMainGroup IBMSecMainGroupAlt'> <div className='IBMSecMainGroup IBMSecMainGroupAlt'>
<PageTitleRow /> <PageTitleRow />
<Filters <ModFilter
filterOptions={filterOptions} filterOptions={filterOptions}
setFilterOptions={setFilterOptions} setFilterOptions={setFilterOptions}
/> />
@ -261,154 +191,3 @@ 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,6 +3,7 @@ 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 {
@ -11,7 +12,13 @@ import {
T_TAG_VALUE T_TAG_VALUE
} from 'constants.ts' } from 'constants.ts'
import { RelayController } from 'controllers' import { RelayController } from 'controllers'
import { useAppSelector, useGames, useMuteLists } from 'hooks' import {
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, {
@ -24,48 +31,40 @@ 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 { ModDetails, MuteLists } from 'types' import {
FilterOptions,
ModDetails,
ModeratedFilter,
MuteLists,
NSFWFilter,
SortBy
} from 'types'
import { extractModData, isModDataComplete, log, LogType } from 'utils' import { extractModData, isModDataComplete, log, LogType } from 'utils'
enum SortByEnum { enum SearchKindEnum {
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: SortByEnum.Latest, sort: SortBy.Latest,
moderated: ModeratedFilterEnum.Moderated, nsfw: NSFWFilter.Hide_NSFW,
source: window.location.host, source: window.location.host,
searching: moderated: ModeratedFilter.Moderated
(searchParams.get('searching') as SearchingFilterEnum) ||
SearchingFilterEnum.Mods
}) })
const [searchTerm, setSearchTerm] = useState( const [searchTerm, setSearchTerm] = useState(
searchParams.get('searchTerm') || '' searchParams.get('searchTerm') || ''
) )
@ -129,22 +128,25 @@ export const SearchPage = () => {
<Filters <Filters
filterOptions={filterOptions} filterOptions={filterOptions}
setFilterOptions={setFilterOptions} setFilterOptions={setFilterOptions}
searchKind={searchKind}
setSearchKind={setSearchKind}
/> />
{filterOptions.searching === SearchingFilterEnum.Mods && ( {searchKind === SearchKindEnum.Mods && (
<ModsResult <ModsResult
searchTerm={searchTerm} searchTerm={searchTerm}
filterOptions={filterOptions} filterOptions={filterOptions}
muteLists={muteLists} muteLists={muteLists}
nsfwList={nsfwList}
/> />
)} )}
{filterOptions.searching === SearchingFilterEnum.Users && ( {searchKind === SearchKindEnum.Users && (
<UsersResult <UsersResult
searchTerm={searchTerm} searchTerm={searchTerm}
muteLists={muteLists} muteLists={muteLists}
moderationFilter={filterOptions.moderated} moderationFilter={filterOptions.moderated}
/> />
)} )}
{filterOptions.searching === SearchingFilterEnum.Games && ( {searchKind === SearchKindEnum.Games && (
<GamesResult searchTerm={searchTerm} /> <GamesResult searchTerm={searchTerm} />
)} )}
</div> </div>
@ -156,49 +158,30 @@ 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'>
{filterOptions.searching === SearchingFilterEnum.Mods && ( {searchKind === SearchKindEnum.Mods && (
<div className='FiltersMainElement'> <ModFilter
<div className='dropdown dropdownMain'> filterOptions={filterOptions}
<button setFilterOptions={setFilterOptions}
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>
)} )}
{(filterOptions.searching === SearchingFilterEnum.Mods || {searchKind === SearchKindEnum.Users && (
filterOptions.searching === SearchingFilterEnum.Users) && (
<div className='FiltersMainElement'> <div className='FiltersMainElement'>
<div className='dropdown dropdownMain'> <div className='dropdown dropdownMain'>
<button <button
@ -210,8 +193,8 @@ const Filters = React.memo(
{filterOptions.moderated} {filterOptions.moderated}
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
{Object.values(ModeratedFilterEnum).map((item, index) => { {Object.values(ModeratedFilter).map((item, index) => {
if (item === ModeratedFilterEnum.Unmoderated_Fully) { if (item === ModeratedFilter.Unmoderated_Fully) {
const isAdmin = const isAdmin =
userState.user?.npub === userState.user?.npub ===
import.meta.env.VITE_REPORTING_NPUB import.meta.env.VITE_REPORTING_NPUB
@ -239,47 +222,6 @@ const Filters = React.memo(
</div> </div>
)} )}
{filterOptions.searching === SearchingFilterEnum.Mods && (
<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 className='FiltersMainElement'> <div className='FiltersMainElement'>
<div className='dropdown dropdownMain'> <div className='dropdown dropdownMain'>
<button <button
@ -288,19 +230,14 @@ const Filters = React.memo(
data-bs-toggle='dropdown' data-bs-toggle='dropdown'
type='button' type='button'
> >
Searching: {filterOptions.searching} Searching: {searchKind}
</button> </button>
<div className='dropdown-menu dropdownMainMenu'> <div className='dropdown-menu dropdownMainMenu'>
{Object.values(SearchingFilterEnum).map((item, index) => ( {Object.values(SearchKindEnum).map((item, index) => (
<div <div
key={`searchingFilterItem-${index}`} key={`searchingFilterItem-${index}`}
className='dropdown-item dropdownMainMenuItem' className='dropdown-item dropdownMainMenuItem'
onClick={() => onClick={() => setSearchKind(item)}
setFilterOptions((prev) => ({
...prev,
searching: item
}))
}
> >
{item} {item}
</div> </div>
@ -321,12 +258,14 @@ 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)
@ -400,49 +339,13 @@ const ModsResult = ({
return mods.filter(filterFn) return mods.filter(filterFn)
}, [mods, searchTerm]) }, [mods, searchTerm])
const filteredModList = useMemo(() => { const filteredModList = useFilteredMods(
let filtered: ModDetails[] = [...filteredMods]
if (filterOptions.source === window.location.host) {
filtered = filtered.filter((mod) => mod.rTag === window.location.host)
}
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, filteredMods,
userState.user?.npub, userState,
filterOptions.sort, filterOptions,
filterOptions.moderated, nsfwList,
filterOptions.source,
muteLists muteLists
]) )
const handleNext = () => { const handleNext = () => {
setPage((prev) => prev + 1) setPage((prev) => prev + 1)
@ -478,7 +381,7 @@ const ModsResult = ({
type UsersResultProps = { type UsersResultProps = {
searchTerm: string searchTerm: string
moderationFilter: ModeratedFilterEnum moderationFilter: ModeratedFilter
muteLists: { muteLists: {
admin: MuteLists admin: MuteLists
user: MuteLists user: MuteLists
@ -528,7 +431,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 === ModeratedFilterEnum.Unmoderated_Fully moderationFilter === ModeratedFilter.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)) {
@ -537,7 +440,7 @@ const UsersResult = ({
) )
} }
if (moderationFilter === ModeratedFilterEnum.Moderated) { if (moderationFilter === ModeratedFilter.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,4 +1,5 @@
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'

25
src/types/modsFilter.ts Normal file
View File

@ -0,0 +1,25 @@
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
}