diff --git a/index.html b/index.html index 9b3186d..2f1f87c 100644 --- a/index.html +++ b/index.html @@ -9,6 +9,37 @@ DEG Mods - Liberating Game Mods + + + +
diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..8912b25 --- /dev/null +++ b/public/404.html @@ -0,0 +1,51 @@ + + + + + + Single Page Apps for GitHub Pages + + + + diff --git a/src/App.tsx b/src/App.tsx index 60b4df3..fe0ccd1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,6 @@ -import { Route, Routes } from 'react-router-dom' -import { Layout } from './layout' -import { routes } from './routes' +import { RouterProvider } from 'react-router-dom' import { useEffect } from 'react' +import { router } from 'routes' import './styles/styles.css' function App() { @@ -22,19 +21,7 @@ function App() { } }, []) - return ( - - }> - {routes.map((route, index) => ( - - ))} - - - ) + return } export default App diff --git a/src/components/Inputs.tsx b/src/components/Inputs.tsx index 5a30009..e40cc91 100644 --- a/src/components/Inputs.tsx +++ b/src/components/Inputs.tsx @@ -89,13 +89,27 @@ interface CheckboxFieldProps { name: string isChecked: boolean handleChange: (e: React.ChangeEvent) => void + type?: 'default' | 'stylized' } export const CheckboxField = React.memo( - ({ label, name, isChecked, handleChange }: CheckboxFieldProps) => ( -
- + ({ + label, + name, + isChecked, + handleChange, + type = 'default' + }: CheckboxFieldProps) => ( +
+ { name='nsfw' isChecked={formState.nsfw} handleChange={handleCheckboxChange} + type='stylized' />
diff --git a/src/components/ModsFilter.tsx b/src/components/ModsFilter.tsx index 26ed98d..755ff95 100644 --- a/src/components/ModsFilter.tsx +++ b/src/components/ModsFilter.tsx @@ -61,7 +61,12 @@ export const ModFilter = React.memo( userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB - if (!isAdmin) return null + const isOwnProfile = + filterOptions.author && + userState.auth && + userState.user?.pubkey === filterOptions.author + + if (!(isAdmin || isOwnProfile)) return null } return ( diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx new file mode 100644 index 0000000..ebe96a5 --- /dev/null +++ b/src/components/Tabs.tsx @@ -0,0 +1,26 @@ +interface TabsProps { + tabs: string[] + tab: number + setTab: React.Dispatch> +} + +export const Tabs = ({ tabs, tab, setTab }: TabsProps) => { + return ( +
+ {tabs.map((t, i) => { + return ( + + ) + })} +
+ ) +} diff --git a/src/contexts/NDKContext.tsx b/src/contexts/NDKContext.tsx index 33e35da..b76f3f2 100644 --- a/src/contexts/NDKContext.tsx +++ b/src/contexts/NDKContext.tsx @@ -29,6 +29,7 @@ type FetchModsOptions = { until?: number since?: number limit?: number + author?: string } interface NDKContextType { @@ -146,7 +147,8 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { source, until, since, - limit + limit, + author }: FetchModsOptions): Promise => { // Define the filter criteria for fetching mods const filter: NDKFilter = { @@ -154,7 +156,8 @@ export const NDKContextProvider = ({ children }: { children: ReactNode }) => { limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20 '#t': [T_TAG_VALUE], until, // Optional filter to fetch events until this timestamp - since // Optional filter to fetch events from this timestamp + since, // Optional filter to fetch events from this timestamp + authors: author ? [author] : undefined // Optional filter to fetch events from only this author } // If the source matches the current window location, add a filter condition diff --git a/src/hooks/useFilteredMods.ts b/src/hooks/useFilteredMods.ts index 6156727..b067beb 100644 --- a/src/hooks/useFilteredMods.ts +++ b/src/hooks/useFilteredMods.ts @@ -8,6 +8,7 @@ import { NSFWFilter, SortBy } from 'types' +import { npubToHex } from 'utils' export const useFilteredMods = ( mods: ModDetails[], @@ -38,11 +39,15 @@ export const useFilteredMods = ( let filtered = nsfwFilter(mods) const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB + const isOwner = + userState.user?.npub && + npubToHex(userState.user.npub as string) === filterOptions.author 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)) { + // Allow "Unmoderated Fully" when author visits own profile + if (!((isAdmin || isOwner) && isUnmoderatedFully)) { filtered = filtered.filter( (mod) => !muteLists.admin.authors.includes(mod.author) && @@ -70,6 +75,7 @@ export const useFilteredMods = ( filterOptions.sort, filterOptions.moderated, filterOptions.nsfw, + filterOptions.author, mods, muteLists, nsfwList diff --git a/src/layout/feed.tsx b/src/layout/feed.tsx new file mode 100644 index 0000000..2566f31 --- /dev/null +++ b/src/layout/feed.tsx @@ -0,0 +1,10 @@ +import { Outlet } from 'react-router-dom' + +export const FeedLayout = () => { + return ( + <> +

WIP

+ + + ) +} diff --git a/src/layout/socialNav.tsx b/src/layout/socialNav.tsx index 022f555..cdc142c 100644 --- a/src/layout/socialNav.tsx +++ b/src/layout/socialNav.tsx @@ -1,24 +1,17 @@ +import { useAppSelector } from 'hooks' import { useState } from 'react' -import { Link, useLocation } from 'react-router-dom' +import { NavLink, NavLinkProps } from 'react-router-dom' import { appRoutes, getProfilePageRoute } from 'routes' import 'styles/socialNav.css' export const SocialNav = () => { - const location = useLocation() - const currentPath = location.pathname const [isCollapsed, setIsCollapsed] = useState(false) + const userState = useAppSelector((state) => state.user) const toggleNav = () => { setIsCollapsed(!isCollapsed) } - const isOnHomePage = - currentPath === appRoutes.index || currentPath === appRoutes.home - const isOnSearchPage = currentPath === appRoutes.search - const isOnProfilePage = new RegExp( - `^${appRoutes.profile.replace(':nprofile', '[^/]+')}$` - ).test(currentPath) - return (
{
- + {!!userState.auth && ( + + )}
)}
@@ -78,19 +68,23 @@ export const SocialNav = () => { ) } -interface NavButtonProps { - to: string - isActive: boolean +interface NavButtonProps extends NavLinkProps { svgPath: string viewBox?: string } -const NavButton = ({ to, isActive, svgPath, viewBox = '0 0 512 512' }: NavButtonProps) => ( - ( + + `btn btnMain socialNavInsideBtn ${ + isActive ? 'socialNavInsideBtnActive' : '' + }` + } > - -); - - + +) diff --git a/src/main.tsx b/src/main.tsx index af98673..4e3a08a 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,7 +1,6 @@ import React from 'react' import ReactDOM from 'react-dom/client' import { Provider } from 'react-redux' -import { HashRouter } from 'react-router-dom' import { ToastContainer } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' import App from './App.tsx' @@ -12,12 +11,10 @@ import { NDKContextProvider } from 'contexts/NDKContext.tsx' ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - + + + + ) diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..1c53760 --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,30 @@ +import { Link } from 'react-router-dom' +import { appRoutes } from 'routes' + +export const NotFoundPage = () => { + return ( +
+
+
+
+
+

Page not found

+
+
+

The page you're attempting to visit doesn't exist

+
+
+ + Go home + +
+
+
+
+
+ ) +} diff --git a/src/pages/feed.tsx b/src/pages/feed.tsx new file mode 100644 index 0000000..bcfc305 --- /dev/null +++ b/src/pages/feed.tsx @@ -0,0 +1,3 @@ +export const FeedPage = () => { + return

Feed

+} diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index 8a39cc4..0f2e87c 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -45,6 +45,7 @@ import { import { Comments } from './internal/comment' import { Reactions } from './internal/reactions' import { Zap } from './internal/zap' +import { CheckboxField } from 'components/Inputs' import placeholder from '../../assets/img/DEGMods Placeholder Img.png' export const ModPage = () => { @@ -666,18 +667,25 @@ type ReportPopupProps = { handleClose: () => void } +const MOD_REPORT_REASONS = [ + { label: 'Actually CP', key: 'actuallyCP' }, + { label: 'Spam', key: 'spam' }, + { label: 'Scam', key: 'scam' }, + { label: 'Not a game mod', key: 'notAGameMod' }, + { label: 'Stolen game mod', key: 'stolenGameMod' }, + { label: `Wasn't tagged NSFW`, key: 'wasntTaggedNSFW' }, + { label: 'Other reason', key: 'otherReason' } +] + const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { const { ndk, fetchEventFromUserRelays, publish } = useNDKContext() const userState = useAppSelector((state) => state.user) - const [selectedOptions, setSelectedOptions] = useState({ - actuallyCP: false, - spam: false, - scam: false, - notAGameMod: false, - stolenGameMod: false, - wasntTaggedNSFW: false, - otherReason: false - }) + const [selectedOptions, setSelectedOptions] = useState( + MOD_REPORT_REASONS.reduce((acc: { [key: string]: boolean }, cur) => { + acc[cur.key] = false + return acc + }, {}) + ) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -823,86 +831,15 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => { > Why are you reporting this? -
- - handleCheckboxChange('actuallyCP')} + {MOD_REPORT_REASONS.map((r) => ( + handleCheckboxChange(r.key)} /> -
-
- - handleCheckboxChange('spam')} - /> -
-
- - handleCheckboxChange('scam')} - /> -
-
- - handleCheckboxChange('notAGameMod')} - /> -
-
- - handleCheckboxChange('stolenGameMod')} - /> -
-
- - handleCheckboxChange('wasntTaggedNSFW')} - /> -
-
- - handleCheckboxChange('otherReason')} - /> -
+ ))}
+ +
+
+ + + + {/* Tabs Content */} + {tab === 0 && ( + <> + + +
+ {filteredModList.map((mod) => ( + + ))} +
+ + + + )} + + {tab === 1 && <>WIP} + {tab === 2 && <>WIP} +
+
+
+ + + + {showReportPopUp && ( + setShowReportPopUp(false)} + /> + )} + + + ) +} + +type ReportUserPopupProps = { + reportedPubkey: string + handleClose: () => void +} + +const USER_REPORT_REASONS = [ + { label: `User posts actual CP`, key: 'user_actuallyCP' }, + { label: `User is a spammer`, key: 'user_spam' }, + { label: `User is a scammer`, key: 'user_scam' }, + { label: `User posts malware`, key: 'user_malware' }, + { label: `User posts non-mods`, key: 'user_notAGameMod' }, + { label: `User doesn't tag NSFW`, key: 'user_wasntTaggedNSFW' }, + { label: `Other (user)`, key: 'user_otherReason' } +] + +const ReportUserPopup = ({ + reportedPubkey, + handleClose +}: ReportUserPopupProps) => { + const { ndk, fetchEventFromUserRelays, publish } = useNDKContext() + const userState = useAppSelector((state) => state.user) + const [selectedOptions, setSelectedOptions] = useState( + USER_REPORT_REASONS.reduce((acc: { [key: string]: boolean }, cur) => { + acc[cur.key] = false + return acc + }, {}) + ) + const [isLoading, setIsLoading] = useState(false) + const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') + + const handleCheckboxChange = (option: keyof typeof selectedOptions) => { + setSelectedOptions((prevState) => ({ + ...prevState, + [option]: !prevState[option] + })) + } + + const handleSubmit = async () => { + const selectedOptionsCount = Object.values(selectedOptions).filter( + (isSelected) => isSelected + ).length + + if (selectedOptionsCount === 0) { + toast.error('At least one option should be checked!') + return + } + + setIsLoading(true) + setLoadingSpinnerDesc('Getting user pubkey') + let userHexKey: string + if (userState.auth && userState.user?.pubkey) { + userHexKey = userState.user.pubkey as string + } else { + userHexKey = (await window.nostr?.getPublicKey()) as string + } + + if (!userHexKey) { + toast.error('Could not get pubkey for reporting user!') + setIsLoading(false) + return + } + + const reportingNpub = import.meta.env.VITE_REPORTING_NPUB + const reportingPubkey = npubToHex(reportingNpub) + + if (reportingPubkey === userHexKey) { + setLoadingSpinnerDesc(`Finding user's mute list`) + // Define the event filter to search for the user's mute list events. + // We look for events of a specific kind (Mutelist) authored by the given hexPubkey. + const filter: NDKFilter = { + kinds: [NDKKind.MuteList], + authors: [userHexKey] + } + + // Fetch the mute list event from the relays. This returns the event containing the user's mute list. + const muteListEvent = await fetchEventFromUserRelays( + filter, + userHexKey, + UserRelaysType.Write + ) + + let unsignedEvent: UnsignedEvent + + if (muteListEvent) { + // get a list of tags + const tags = muteListEvent.tags + const alreadyExists = + tags.findIndex( + (item) => item[0] === 'p' && item[1] === reportedPubkey + ) !== -1 + + if (alreadyExists) { + setIsLoading(false) + return toast.warn( + `Reporter user's pubkey is already in the mute list` + ) + } + + tags.push(['p', reportedPubkey]) + + unsignedEvent = { + pubkey: muteListEvent.pubkey, + kind: NDKKind.MuteList, + content: muteListEvent.content, + created_at: now(), + tags: [...tags] + } + } else { + unsignedEvent = { + pubkey: userHexKey, + kind: NDKKind.MuteList, + content: '', + created_at: now(), + tags: [['p', reportedPubkey]] + } + } + + setLoadingSpinnerDesc('Updating mute list event') + const isUpdated = await signAndPublish(unsignedEvent, ndk, publish) + if (isUpdated) handleClose() + } else { + const href = window.location.href + let message = `I'd like to report ${href} due to following reasons:\n` + + Object.entries(selectedOptions).forEach(([key, value]) => { + if (value) { + message += `* ${key}\n` + } + }) + + setLoadingSpinnerDesc('Sending report') + const isSent = await sendDMUsingRandomKey( + message, + reportingPubkey!, + ndk, + publish + ) + if (isSent) handleClose() + } + setIsLoading(false) + } + + return ( + <> + {isLoading && } +
+
+
+
+
+
+

Report Post

+
+
+ + + +
+
+
+
+
+ + {USER_REPORT_REASONS.map((r) => ( + handleCheckboxChange(r.key)} + /> + ))} + +
+
+
+
+
+
+
+ + ) } diff --git a/src/pages/write.tsx b/src/pages/write.tsx index 230edd7..58813a5 100644 --- a/src/pages/write.tsx +++ b/src/pages/write.tsx @@ -55,6 +55,7 @@ export const WritePage = () => { name='nsfw' isChecked={false} handleChange={() => {}} + type='stylized' />