From b1d578c32931bf263750278a7e349908e9c3fd98 Mon Sep 17 00:00:00 2001 From: enes Date: Thu, 28 Nov 2024 16:47:10 +0100 Subject: [PATCH] feat: add filtering, split mods and blog filters --- src/components/Filters/BlogsFilter.tsx | 165 +++++++++++++++++ src/components/Filters/Dropdown.tsx | 25 +++ src/components/Filters/ModsFilter.tsx | 182 +++++++++++++++++++ src/components/Filters/Option.tsx | 16 ++ src/components/Filters/index.tsx | 9 + src/components/ModsFilter.tsx | 235 ------------------------- src/hooks/useFilteredMods.ts | 30 +++- src/pages/blogs/index.tsx | 96 ++++------ src/pages/game.tsx | 8 +- src/pages/mods.tsx | 16 +- src/pages/profile/index.tsx | 18 +- src/pages/search.tsx | 11 +- src/types/blog.ts | 9 + src/types/modsFilter.ts | 7 + src/utils/consts.ts | 6 +- 15 files changed, 510 insertions(+), 323 deletions(-) create mode 100644 src/components/Filters/BlogsFilter.tsx create mode 100644 src/components/Filters/Dropdown.tsx create mode 100644 src/components/Filters/ModsFilter.tsx create mode 100644 src/components/Filters/Option.tsx create mode 100644 src/components/Filters/index.tsx delete mode 100644 src/components/ModsFilter.tsx diff --git a/src/components/Filters/BlogsFilter.tsx b/src/components/Filters/BlogsFilter.tsx new file mode 100644 index 0000000..71b4098 --- /dev/null +++ b/src/components/Filters/BlogsFilter.tsx @@ -0,0 +1,165 @@ +import { useAppSelector, useLocalStorage } from 'hooks' +import React from 'react' +import { + FilterOptions, + ModeratedFilter, + NSFWFilter, + SortBy, + WOTFilterOptions +} from 'types' +import { DEFAULT_FILTER_OPTIONS } from 'utils' +import { Dropdown } from './Dropdown' +import { Option } from './Option' +import { Filter } from '.' + +type Props = { + author?: string | undefined + filterKey?: string | undefined +} + +export const BlogsFilter = React.memo( + ({ author, filterKey = 'filter-blog' }: Props) => { + const userState = useAppSelector((state) => state.user) + const [filterOptions, setFilterOptions] = useLocalStorage( + filterKey, + DEFAULT_FILTER_OPTIONS + ) + + return ( + + {/* sort filter options */} + + {Object.values(SortBy).map((item, index) => ( +
+ setFilterOptions((prev) => ({ + ...prev, + sort: item + })) + } + > + {item} +
+ ))} +
+ + {/* moderation filter options */} + + {Object.values(ModeratedFilter).map((item) => { + if (item === ModeratedFilter.Unmoderated_Fully) { + const isAdmin = + userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB + + const isOwnProfile = + author && userState.auth && userState.user?.pubkey === author + + if (!(isAdmin || isOwnProfile)) return null + } + + return ( + + ) + })} + + + {/* wot filter options */} + Trust: {filterOptions.wot}}> + {Object.values(WOTFilterOptions).map((item, index) => { + // when user is not logged in + if (item === WOTFilterOptions.Site_And_Mine && !userState.auth) { + return null + } + + // when logged in user not admin + if ( + item === WOTFilterOptions.None || + item === WOTFilterOptions.Mine_Only || + item === WOTFilterOptions.Exclude + ) { + const isWoTNpub = + userState.user?.npub === import.meta.env.VITE_SITE_WOT_NPUB + + const isOwnProfile = + author && userState.auth && userState.user?.pubkey === author + + if (!(isWoTNpub || isOwnProfile)) return null + } + + return ( + + ) + })} + + + {/* nsfw filter options */} + + {Object.values(NSFWFilter).map((item, index) => ( + + ))} + + + {/* source filter options */} + + + + +
+ ) + } +) diff --git a/src/components/Filters/Dropdown.tsx b/src/components/Filters/Dropdown.tsx new file mode 100644 index 0000000..1bfb02f --- /dev/null +++ b/src/components/Filters/Dropdown.tsx @@ -0,0 +1,25 @@ +import { PropsWithChildren } from 'react' + +interface DropdownProps { + label: React.ReactNode +} +export const Dropdown = ({ + label, + children +}: PropsWithChildren) => { + return ( +
+
+ +
{children}
+
+
+ ) +} diff --git a/src/components/Filters/ModsFilter.tsx b/src/components/Filters/ModsFilter.tsx new file mode 100644 index 0000000..afa43c4 --- /dev/null +++ b/src/components/Filters/ModsFilter.tsx @@ -0,0 +1,182 @@ +import { useAppSelector, useLocalStorage } from 'hooks' +import React from 'react' +import { + FilterOptions, + SortBy, + ModeratedFilter, + WOTFilterOptions, + NSFWFilter, + RepostFilter +} from 'types' +import { DEFAULT_FILTER_OPTIONS } from 'utils' +import { Filter } from '.' +import { Dropdown } from './Dropdown' +import { Option } from './Option' + +type Props = { + author?: string | undefined + filterKey?: string | undefined +} + +export const ModFilter = React.memo( + ({ author, filterKey = 'filter' }: Props) => { + const userState = useAppSelector((state) => state.user) + const [filterOptions, setFilterOptions] = useLocalStorage( + filterKey, + DEFAULT_FILTER_OPTIONS + ) + + return ( + + {/* sort filter options */} + + {Object.values(SortBy).map((item, index) => ( + + ))} + + + {/* moderation filter options */} + + {Object.values(ModeratedFilter).map((item, index) => { + if (item === ModeratedFilter.Unmoderated_Fully) { + const isAdmin = + userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB + + const isOwnProfile = + author && userState.auth && userState.user?.pubkey === author + + if (!(isAdmin || isOwnProfile)) return null + } + + return ( + + ) + })} + + + {/* wot filter options */} + Trust: {filterOptions.wot}}> + {Object.values(WOTFilterOptions).map((item, index) => { + // when user is not logged in + if (item === WOTFilterOptions.Site_And_Mine && !userState.auth) { + return null + } + + // when logged in user not admin + if ( + item === WOTFilterOptions.None || + item === WOTFilterOptions.Mine_Only || + item === WOTFilterOptions.Exclude + ) { + const isWoTNpub = + userState.user?.npub === import.meta.env.VITE_SITE_WOT_NPUB + + const isOwnProfile = + author && userState.auth && userState.user?.pubkey === author + + if (!(isWoTNpub || isOwnProfile)) return null + } + + return ( + + ) + })} + + + {/* nsfw filter options */} + + {Object.values(NSFWFilter).map((item, index) => ( + + ))} + + + {/* repost filter options */} + + {Object.values(RepostFilter).map((item, index) => ( + + ))} + + + {/* source filter options */} + + + + + + ) + } +) diff --git a/src/components/Filters/Option.tsx b/src/components/Filters/Option.tsx new file mode 100644 index 0000000..1f854e8 --- /dev/null +++ b/src/components/Filters/Option.tsx @@ -0,0 +1,16 @@ +import { PropsWithChildren } from 'react' + +interface OptionProps { + onClick: React.MouseEventHandler +} + +export const Option = ({ + onClick, + children +}: PropsWithChildren) => { + return ( +
+ {children} +
+ ) +} diff --git a/src/components/Filters/index.tsx b/src/components/Filters/index.tsx new file mode 100644 index 0000000..4260f79 --- /dev/null +++ b/src/components/Filters/index.tsx @@ -0,0 +1,9 @@ +import { PropsWithChildren } from 'react' + +export const Filter = ({ children }: PropsWithChildren) => { + return ( +
+
{children}
+
+ ) +} diff --git a/src/components/ModsFilter.tsx b/src/components/ModsFilter.tsx deleted file mode 100644 index 8d733c7..0000000 --- a/src/components/ModsFilter.tsx +++ /dev/null @@ -1,235 +0,0 @@ -import { useAppSelector, useLocalStorage } from 'hooks' -import React from 'react' -import { - FilterOptions, - ModeratedFilter, - NSFWFilter, - SortBy, - WOTFilterOptions -} from 'types' -import { DEFAULT_FILTER_OPTIONS } from 'utils' - -type Props = { - author?: string | undefined - filterKey?: string | undefined -} - -export const ModFilter = React.memo( - ({ author, filterKey = 'filter' }: Props) => { - const userState = useAppSelector((state) => state.user) - const [filterOptions, setFilterOptions] = useLocalStorage( - filterKey, - DEFAULT_FILTER_OPTIONS - ) - - return ( -
-
- {/* sort filter options */} -
-
- - -
- {Object.values(SortBy).map((item, index) => ( -
- setFilterOptions((prev) => ({ - ...prev, - sort: item - })) - } - > - {item} -
- ))} -
-
-
- - {/* moderation filter options */} -
-
- -
- {Object.values(ModeratedFilter).map((item, index) => { - if (item === ModeratedFilter.Unmoderated_Fully) { - const isAdmin = - userState.user?.npub === - import.meta.env.VITE_REPORTING_NPUB - - const isOwnProfile = - author && - userState.auth && - userState.user?.pubkey === author - - if (!(isAdmin || isOwnProfile)) return null - } - - return ( -
- setFilterOptions((prev) => ({ - ...prev, - moderated: item - })) - } - > - {item} -
- ) - })} -
-
-
- - {/* wot filter options */} -
-
- -
- {Object.values(WOTFilterOptions).map((item, index) => { - // when user is not logged in - if ( - item === WOTFilterOptions.Site_And_Mine && - !userState.auth - ) { - return null - } - - // when logged in user not admin - if ( - item === WOTFilterOptions.None || - item === WOTFilterOptions.Mine_Only || - item === WOTFilterOptions.Exclude - ) { - const isWoTNpub = - userState.user?.npub === - import.meta.env.VITE_SITE_WOT_NPUB - - const isOwnProfile = - author && - userState.auth && - userState.user?.pubkey === author - - if (!(isWoTNpub || isOwnProfile)) return null - } - - return ( -
- setFilterOptions((prev) => ({ - ...prev, - wot: item - })) - } - > - {item} -
- ) - })} -
-
-
- - {/* nsfw filter options */} -
-
- -
- {Object.values(NSFWFilter).map((item, index) => ( -
- setFilterOptions((prev) => ({ - ...prev, - nsfw: item - })) - } - > - {item} -
- ))} -
-
-
- - {/* source filter options */} -
-
- -
-
- setFilterOptions((prev) => ({ - ...prev, - source: window.location.host - })) - } - > - Show From: {window.location.host} -
-
- setFilterOptions((prev) => ({ - ...prev, - source: 'Show All' - })) - } - > - Show All -
-
-
-
-
-
- ) - } -) diff --git a/src/hooks/useFilteredMods.ts b/src/hooks/useFilteredMods.ts index 7c16b18..ef7c249 100644 --- a/src/hooks/useFilteredMods.ts +++ b/src/hooks/useFilteredMods.ts @@ -6,6 +6,7 @@ import { ModeratedFilter, MuteLists, NSFWFilter, + RepostFilter, SortBy, WOTFilterOptions } from 'types' @@ -22,6 +23,7 @@ export const useFilteredMods = ( admin: MuteLists user: MuteLists }, + repostList: string[], author?: string | undefined ) => { const { siteWot, siteWotLevel, userWot, userWotLevel } = useAppSelector( @@ -53,6 +55,30 @@ export const useFilteredMods = ( } } + const repostFilter = (mods: ModDetails[]) => { + if (filterOptions.repost !== RepostFilter.Hide_Repost) { + // Add repost tag to mods included in repostList + mods = mods.map((mod) => { + return !mod.repost && repostList.includes(mod.aTag) + ? { ...mod, repost: true } + : mod + }) + } + // Determine the filtering logic based on the Repost filter option + switch (filterOptions.repost) { + case RepostFilter.Hide_Repost: + return mods.filter( + (mod) => !mod.repost && !repostList.includes(mod.aTag) + ) + case RepostFilter.Show_Repost: + return mods + case RepostFilter.Only_Repost: + return mods.filter( + (mod) => mod.repost || repostList.includes(mod.aTag) + ) + } + } + const wotFilter = (mods: ModDetails[]) => { // Determine the filtering logic based on the WOT filter option and user state // when user is not logged in use Site_Only @@ -93,7 +119,7 @@ export const useFilteredMods = ( } let filtered = nsfwFilter(mods) - + filtered = repostFilter(filtered) filtered = wotFilter(filtered) const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB @@ -135,10 +161,12 @@ export const useFilteredMods = ( filterOptions.moderated, filterOptions.wot, filterOptions.nsfw, + filterOptions.repost, author, mods, muteLists, nsfwList, + repostList, siteWot, siteWotLevel, userWot, diff --git a/src/pages/blogs/index.tsx b/src/pages/blogs/index.tsx index 08d0848..2155b39 100644 --- a/src/pages/blogs/index.tsx +++ b/src/pages/blogs/index.tsx @@ -11,6 +11,9 @@ import '../../styles/styles.css' import { PaginationWithPageNumbers } from 'components/Pagination' import { scrollIntoView } from 'utils' import { LoadingSpinner } from 'components/LoadingSpinner' +import { Filter } from 'components/Filters' +import { Dropdown } from 'components/Filters/Dropdown' +import { Option } from 'components/Filters/Option' export const BlogsPage = () => { const navigation = useNavigation() @@ -126,66 +129,39 @@ export const BlogsPage = () => { -
-
-
-
- -
- {Object.values(SortBy).map((item, index) => ( -
- setFilterOptions((prev) => ({ - ...prev, - sort: item - })) - } - > - {item} -
- ))} -
-
-
-
-
- -
- {Object.values(NSFWFilter).map((item, index) => ( -
- setFilterOptions((prev) => ({ - ...prev, - nsfw: item - })) - } - > - {item} -
- ))} -
-
-
-
-
+ + + {Object.values(SortBy).map((item, index) => ( + + ))} + + + + {Object.values(NSFWFilter).map((item, index) => ( + + ))} + +
diff --git a/src/pages/game.tsx b/src/pages/game.tsx index 6f82493..da49f87 100644 --- a/src/pages/game.tsx +++ b/src/pages/game.tsx @@ -4,7 +4,7 @@ import { NDKSubscriptionCacheUsage } from '@nostr-dev-kit/ndk' import { ModCard } from 'components/ModCard' -import { ModFilter } from 'components/ModsFilter' +import { ModFilter } from 'components/Filters/ModsFilter' import { PaginationWithPageNumbers } from 'components/Pagination' import { SearchInput } from 'components/SearchInput' import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts' @@ -20,11 +20,13 @@ import { useEffect, useMemo, useRef, useState } from 'react' import { useParams, useSearchParams } from 'react-router-dom' import { FilterOptions, ModDetails } from 'types' import { + CurationSetIdentifiers, DEFAULT_FILTER_OPTIONS, extractModData, isModDataComplete, scrollIntoView } from 'utils' +import { useCuratedSet } from 'hooks/useCuratedSet' export const GamePage = () => { const scrollTargetRef = useRef(null) @@ -33,6 +35,7 @@ export const GamePage = () => { const { ndk } = useNDKContext() const muteLists = useMuteLists() const nsfwList = useNSFWList() + const repostList = useCuratedSet(CurationSetIdentifiers.Repost) const [filterOptions] = useLocalStorage( 'filter', @@ -101,7 +104,8 @@ export const GamePage = () => { userState, filterOptions, nsfwList, - muteLists + muteLists, + repostList ) // Pagination logic diff --git a/src/pages/mods.tsx b/src/pages/mods.tsx index 72c8c7a..cd2ad21 100644 --- a/src/pages/mods.tsx +++ b/src/pages/mods.tsx @@ -1,4 +1,4 @@ -import { ModFilter } from 'components/ModsFilter' +import { ModFilter } from 'components/Filters/ModsFilter' import { Pagination } from 'components/Pagination' import React, { useCallback, useEffect, useRef, useState } from 'react' import { createSearchParams, useNavigate } from 'react-router-dom' @@ -39,6 +39,7 @@ export const ModsPage = () => { ) const muteLists = useMuteLists() const nsfwList = useNSFWList() + const repostList = useCuratedSet(CurationSetIdentifiers.Repost) const [page, setPage] = useState(1) @@ -99,17 +100,10 @@ export const ModsPage = () => { userState, filterOptions, nsfwList, - muteLists + muteLists, + repostList ) - // Add repost tag to mods included in repostList - const repostList = useCuratedSet(CurationSetIdentifiers.Repost) - const filteredModListRepost = filteredModList.map((mod) => { - return !mod.repost && repostList.includes(mod.aTag) - ? { ...mod, repost: true } - : mod - }) - return ( <> {isFetching && } @@ -124,7 +118,7 @@ export const ModsPage = () => {
- {filteredModListRepost.map((mod) => ( + {filteredModList.map((mod) => ( ))}
diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 83dfa51..3761a5b 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -1,7 +1,7 @@ import { NDKFilter, NDKKind } from '@nostr-dev-kit/ndk' import { LoadingSpinner } from 'components/LoadingSpinner' import { ModCard } from 'components/ModCard' -import { ModFilter } from 'components/ModsFilter' +import { ModFilter } from 'components/Filters/ModsFilter' import { Pagination } from 'components/Pagination' import { ProfileSection } from 'components/ProfileSection' import { Tabs } from 'components/Tabs' @@ -39,6 +39,7 @@ import { import { CheckboxField } from 'components/Inputs' import { ProfilePageLoaderResult } from './loader' import { BlogCard } from 'components/BlogCard' +import { BlogsFilter } from 'components/Filters/BlogsFilter' export const ProfilePage = () => { const { @@ -269,16 +270,10 @@ export const ProfilePage = () => { filterOptions, nsfwList, muteLists, + repostList, profilePubkey ) - // Add repost tag to mods included in repostList - const filteredModListRepost = filteredModList.map((mod) => { - return !mod.repost && repostList.includes(mod.aTag) - ? { ...mod, repost: true } - : mod - }) - return (
@@ -429,7 +424,7 @@ export const ProfilePage = () => {
- {filteredModListRepost.map((mod) => ( + {filteredModList.map((mod) => ( ))}
@@ -831,7 +826,10 @@ const ProfileTabBlogs = () => { )} - +
{moderatedAndSortedBlogs.map((b) => ( diff --git a/src/pages/search.tsx b/src/pages/search.tsx index f94be8f..683696f 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -10,7 +10,7 @@ import { import { ErrorBoundary } from 'components/ErrorBoundary' import { GameCard } from 'components/GameCard' import { ModCard } from 'components/ModCard' -import { ModFilter } from 'components/ModsFilter' +import { ModFilter } from 'components/Filters/ModsFilter' import { Pagination } from 'components/Pagination' import { Profile } from 'components/ProfileSection' import { SearchInput } from 'components/SearchInput' @@ -32,11 +32,13 @@ import React, { useEffect, useMemo, useRef, useState } from 'react' import { useSearchParams } from 'react-router-dom' import { FilterOptions, ModDetails, ModeratedFilter, MuteLists } from 'types' import { + CurationSetIdentifiers, DEFAULT_FILTER_OPTIONS, extractModData, isModDataComplete, scrollIntoView } from 'utils' +import { useCuratedSet } from 'hooks/useCuratedSet' enum SearchKindEnum { Mods = 'Mods', @@ -50,6 +52,7 @@ export const SearchPage = () => { const muteLists = useMuteLists() const nsfwList = useNSFWList() + const repostList = useCuratedSet(CurationSetIdentifiers.Repost) const searchTermRef = useRef(null) const searchKind = @@ -115,6 +118,7 @@ export const SearchPage = () => { filterOptions={filterOptions} muteLists={muteLists} nsfwList={nsfwList} + repostList={repostList} el={scrollTargetRef.current} /> )} @@ -233,6 +237,7 @@ type ModsResultProps = { user: MuteLists } nsfwList: string[] + repostList: string[] el: HTMLElement | null } @@ -241,6 +246,7 @@ const ModsResult = ({ searchTerm, muteLists, nsfwList, + repostList, el }: ModsResultProps) => { const { ndk } = useNDKContext() @@ -313,7 +319,8 @@ const ModsResult = ({ userState, filterOptions, nsfwList, - muteLists + muteLists, + repostList ) const handleNext = () => { diff --git a/src/types/blog.ts b/src/types/blog.ts index 7b76549..075a63c 100644 --- a/src/types/blog.ts +++ b/src/types/blog.ts @@ -1,3 +1,5 @@ +import { SortBy, NSFWFilter, ModeratedFilter } from './modsFilter' + export interface BlogForm { title: string content: string @@ -40,3 +42,10 @@ export interface BlogPageLoaderResult { isAddedToNSFW: boolean isBlocked: boolean } + +export interface BlogsFilterOptions { + sort: SortBy + nsfw: NSFWFilter + source: string + moderated: ModeratedFilter +} diff --git a/src/types/modsFilter.ts b/src/types/modsFilter.ts index b0ae63b..6310f1d 100644 --- a/src/types/modsFilter.ts +++ b/src/types/modsFilter.ts @@ -25,10 +25,17 @@ export enum WOTFilterOptions { Exclude = 'Exclude' } +export enum RepostFilter { + Hide_Repost = 'Hide Repost', + Show_Repost = 'Show Repost', + Only_Repost = 'Only Repost' +} + export interface FilterOptions { sort: SortBy nsfw: NSFWFilter source: string moderated: ModeratedFilter wot: WOTFilterOptions + repost: RepostFilter } diff --git a/src/utils/consts.ts b/src/utils/consts.ts index 611a3a8..38da842 100644 --- a/src/utils/consts.ts +++ b/src/utils/consts.ts @@ -3,7 +3,8 @@ import { SortBy, NSFWFilter, ModeratedFilter, - WOTFilterOptions + WOTFilterOptions, + RepostFilter } from 'types' export const DEFAULT_FILTER_OPTIONS: FilterOptions = { @@ -11,5 +12,6 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptions = { nsfw: NSFWFilter.Hide_NSFW, source: window.location.host, moderated: ModeratedFilter.Moderated, - wot: WOTFilterOptions.Site_Only + wot: WOTFilterOptions.Site_Only, + repost: RepostFilter.Show_Repost }