import { Pagination } from 'components/Pagination' import React, { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createSearchParams, useNavigate } from 'react-router-dom' import { LoadingSpinner } from '../components/LoadingSpinner' import { ModCard } from '../components/ModCard' import { MOD_FILTER_LIMIT } from '../constants' import { MetadataController } from '../controllers' import { useAppSelector, useDidMount, useMuteLists } from '../hooks' import { appRoutes } from '../routes' import '../styles/filters.css' import '../styles/pagination.css' import '../styles/search.css' import '../styles/styles.css' import { ModDetails } from '../types' 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 = () => { const [isFetching, setIsFetching] = useState(false) const [mods, setMods] = useState([]) const [filterOptions, setFilterOptions] = useState({ sort: SortBy.Latest, nsfw: NSFWFilter.Hide_NSFW, source: window.location.host, moderated: ModeratedFilter.Moderated }) const muteLists = useMuteLists() const [nsfwList, setNSFWList] = useState([]) const [page, setPage] = useState(1) const userState = useAppSelector((state) => state.user) useDidMount(async () => { const metadataController = await MetadataController.getInstance() metadataController.getNSFWList().then((list) => { setNSFWList(list) }) }) useEffect(() => { setIsFetching(true) fetchMods({ source: filterOptions.source }) .then((res) => { setMods(res) }) .finally(() => { setIsFetching(false) }) }, [filterOptions.source]) const handleNext = useCallback(() => { setIsFetching(true) const until = mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined fetchMods({ source: filterOptions.source, until }) .then((res) => { setMods(res) setPage((prev) => prev + 1) }) .finally(() => { setIsFetching(false) }) }, [filterOptions.source, mods]) const handlePrev = useCallback(() => { setIsFetching(true) const since = mods.length > 0 ? mods[0].published_at + 1 : undefined fetchMods({ source: filterOptions.source, since }) .then((res) => { setMods(res) setPage((prev) => prev - 1) }) .finally(() => { setIsFetching(false) }) }, [filterOptions.source, mods]) const filteredModList = 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 ]) return ( <> {isFetching && }
{filteredModList.map((mod) => ( ))}
) } const PageTitleRow = React.memo(() => { const navigate = useNavigate() const searchTermRef = useRef(null) const handleSearch = () => { const value = searchTermRef.current?.value || '' // Access the input value from the ref if (value !== '') { const searchParams = createSearchParams({ searchTerm: value, searching: 'Mods' }) navigate({ pathname: appRoutes.search, search: `?${searchParams}` }) } } // Handle "Enter" key press inside the input const handleKeyDown = (event: React.KeyboardEvent) => { if (event.key === 'Enter') { handleSearch() } } return (

Mods

) }) type FiltersProps = { filterOptions: FilterOptions setFilterOptions: Dispatch> } const Filters = React.memo( ({ filterOptions, setFilterOptions }: FiltersProps) => { const userState = useAppSelector((state) => state.user) return (
{Object.values(SortBy).map((item, index) => (
setFilterOptions((prev) => ({ ...prev, sort: item })) } > {item}
))}
{Object.values(ModeratedFilter).map((item, index) => { if (item === ModeratedFilter.Unmoderated_Fully) { const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB if (!isAdmin) return null } return (
setFilterOptions((prev) => ({ ...prev, moderated: item })) } > {item}
) })}
{Object.values(NSFWFilter).map((item, index) => (
setFilterOptions((prev) => ({ ...prev, nsfw: item })) } > {item}
))}
setFilterOptions((prev) => ({ ...prev, source: window.location.host })) } > Show From: {window.location.host}
setFilterOptions((prev) => ({ ...prev, source: 'Show All' })) } > Show All
) } )